file.rs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan, Fixed};
  2. use std::io::fs;
  3. use std::io;
  4. use column::{Column, Permissions, FileName, FileSize, User, Group};
  5. use format::{format_metric_bytes, format_IEC_bytes};
  6. use unix::{get_user_name, get_group_name};
  7. use sort::SortPart;
  8. use dir::Dir;
  9. use filetype::HasType;
  10. // Instead of working with Rust's Paths, we have our own File object
  11. // that holds the Path and various cached information. Each file is
  12. // definitely going to have its filename used at least once, its stat
  13. // information queried at least once, and its file extension extracted
  14. // at least once, so we may as well carry around that information with
  15. // the actual path.
  16. pub struct File<'a> {
  17. pub name: &'a str,
  18. pub dir: &'a Dir<'a>,
  19. pub ext: Option<&'a str>,
  20. pub path: &'a Path,
  21. pub stat: io::FileStat,
  22. pub parts: Vec<SortPart>,
  23. }
  24. impl<'a> File<'a> {
  25. pub fn from_path(path: &'a Path, parent: &'a Dir) -> File<'a> {
  26. // Getting the string from a filename fails whenever it's not
  27. // UTF-8 representable - just assume it is for now.
  28. let filename: &str = path.filename_str().unwrap();
  29. // Use lstat here instead of file.stat(), as it doesn't follow
  30. // symbolic links. Otherwise, the stat() call will fail if it
  31. // encounters a link that's target is non-existent.
  32. let stat: io::FileStat = match fs::lstat(path) {
  33. Ok(stat) => stat,
  34. Err(e) => fail!("Couldn't stat {}: {}", filename, e),
  35. };
  36. return File {
  37. path: path,
  38. dir: parent,
  39. stat: stat,
  40. name: filename,
  41. ext: File::ext(filename),
  42. parts: SortPart::split_into_parts(filename),
  43. };
  44. }
  45. fn ext(name: &'a str) -> Option<&'a str> {
  46. // The extension is the series of characters after a dot at
  47. // the end of a filename. This deliberately also counts
  48. // dotfiles - the ".git" folder has the extension "git".
  49. let re = regex!(r"\.([^.]+)$");
  50. re.captures(name).map(|caps| caps.at(1))
  51. }
  52. pub fn is_dotfile(&self) -> bool {
  53. self.name.starts_with(".")
  54. }
  55. pub fn is_tmpfile(&self) -> bool {
  56. self.name.ends_with("~") || (self.name.starts_with("#") && self.name.ends_with("#"))
  57. }
  58. // Highlight the compiled versions of files. Some of them, like .o,
  59. // get special highlighting when they're alone because there's no
  60. // point in existing without their source. Others can be perfectly
  61. // content without their source files, such as how .js is valid
  62. // without a .coffee.
  63. pub fn get_source_files(&self) -> Vec<Path> {
  64. match self.ext {
  65. Some("class") => vec![self.path.with_extension("java")], // Java
  66. Some("elc") => vec![self.path.with_extension("el")], // Emacs Lisp
  67. Some("hi") => vec![self.path.with_extension("hs")], // Haskell
  68. Some("o") => vec![self.path.with_extension("c"), self.path.with_extension("cpp")], // C, C++
  69. Some("pyc") => vec![self.path.with_extension("py")], // Python
  70. _ => vec![],
  71. }
  72. }
  73. pub fn get_source_files_usual(&self) -> Vec<Path> {
  74. match self.ext {
  75. Some("js") => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript
  76. Some("css") => vec![self.path.with_extension("sass"), self.path.with_extension("less")], // SASS, Less
  77. Some("aux") => vec![self.path.with_extension("tex")], // TeX: auxiliary file
  78. Some("bbl") => vec![self.path.with_extension("tex")], // BibTeX bibliography file
  79. Some("blg") => vec![self.path.with_extension("tex")], // BibTeX log file
  80. Some("lof") => vec![self.path.with_extension("tex")], // list of figures
  81. Some("log") => vec![self.path.with_extension("tex")], // TeX log file
  82. Some("lot") => vec![self.path.with_extension("tex")], // list of tables
  83. Some("toc") => vec![self.path.with_extension("tex")], // table of contents
  84. _ => vec![],
  85. }
  86. }
  87. pub fn display(&self, column: &Column) -> String {
  88. match *column {
  89. Permissions => self.permissions_string(),
  90. FileName => self.file_colour().paint(self.name),
  91. FileSize(use_iec) => self.file_size(use_iec),
  92. // Display the ID if the user/group doesn't exist, which
  93. // usually means it was deleted but its files weren't.
  94. User(uid) => {
  95. let style = if uid == self.stat.unstable.uid { Yellow.bold() } else { Plain };
  96. let string = get_user_name(self.stat.unstable.uid as i32).unwrap_or(self.stat.unstable.uid.to_str());
  97. return style.paint(string.as_slice());
  98. },
  99. Group => get_group_name(self.stat.unstable.gid as u32).unwrap_or(self.stat.unstable.gid.to_str()),
  100. }
  101. }
  102. fn file_size(&self, use_iec_prefixes: bool) -> String {
  103. // Don't report file sizes for directories. I've never looked
  104. // at one of those numbers and gained any information from it.
  105. if self.stat.kind == io::TypeDirectory {
  106. Black.bold().paint("-")
  107. } else {
  108. let (size, suffix) = if use_iec_prefixes {
  109. format_IEC_bytes(self.stat.size)
  110. } else {
  111. format_metric_bytes(self.stat.size)
  112. };
  113. return format!("{}{}", Green.bold().paint(size.as_slice()), Green.paint(suffix.as_slice()));
  114. }
  115. }
  116. fn type_char(&self) -> String {
  117. return match self.stat.kind {
  118. io::TypeFile => ".".to_string(),
  119. io::TypeDirectory => Blue.paint("d"),
  120. io::TypeNamedPipe => Yellow.paint("|"),
  121. io::TypeBlockSpecial => Purple.paint("s"),
  122. io::TypeSymlink => Cyan.paint("l"),
  123. _ => "?".to_string(),
  124. }
  125. }
  126. fn file_colour(&self) -> Style {
  127. self.get_type().style()
  128. }
  129. fn permissions_string(&self) -> String {
  130. let bits = self.stat.perm;
  131. return format!("{}{}{}{}{}{}{}{}{}{}",
  132. self.type_char(),
  133. // The first three are bold because they're the ones used
  134. // most often.
  135. File::permission_bit(bits, io::UserRead, "r", Yellow.bold()),
  136. File::permission_bit(bits, io::UserWrite, "w", Red.bold()),
  137. File::permission_bit(bits, io::UserExecute, "x", Green.bold().underline()),
  138. File::permission_bit(bits, io::GroupRead, "r", Yellow.normal()),
  139. File::permission_bit(bits, io::GroupWrite, "w", Red.normal()),
  140. File::permission_bit(bits, io::GroupExecute, "x", Green.normal()),
  141. File::permission_bit(bits, io::OtherRead, "r", Yellow.normal()),
  142. File::permission_bit(bits, io::OtherWrite, "w", Red.normal()),
  143. File::permission_bit(bits, io::OtherExecute, "x", Green.normal()),
  144. );
  145. }
  146. fn permission_bit(bits: io::FilePermission, bit: io::FilePermission, character: &'static str, style: Style) -> String {
  147. if bits.contains(bit) {
  148. style.paint(character.as_slice())
  149. } else {
  150. Black.bold().paint("-".as_slice())
  151. }
  152. }
  153. }