file.rs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. use colours::{Plain, Style, Black, Red, Green, Yellow, Blue, Purple, Cyan, Fixed};
  2. use std::io::{fs, IoResult};
  3. use std::io;
  4. use std::str::from_utf8_lossy;
  5. use column::{Column, Permissions, FileName, FileSize, User, Group, HardLinks, Inode, Blocks};
  6. use format::{format_metric_bytes, format_IEC_bytes};
  7. use unix::Unix;
  8. use sort::SortPart;
  9. use dir::Dir;
  10. use filetype::HasType;
  11. // Instead of working with Rust's Paths, we have our own File object
  12. // that holds the Path and various cached information. Each file is
  13. // definitely going to have its filename used at least once, its stat
  14. // information queried at least once, and its file extension extracted
  15. // at least once, so we may as well carry around that information with
  16. // the actual path.
  17. pub struct File<'a> {
  18. pub name: String,
  19. pub dir: &'a Dir<'a>,
  20. pub ext: Option<String>,
  21. pub path: &'a Path,
  22. pub stat: io::FileStat,
  23. pub parts: Vec<SortPart>,
  24. }
  25. impl<'a> File<'a> {
  26. pub fn from_path(path: &'a Path, parent: &'a Dir) -> IoResult<File<'a>> {
  27. let v = path.filename().unwrap(); // fails if / or . or ..
  28. let filename = from_utf8_lossy(v).to_string();
  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. fs::lstat(path).map(|stat| File {
  33. path: path,
  34. dir: parent,
  35. stat: stat,
  36. name: filename.clone(),
  37. ext: File::ext(filename.clone()),
  38. parts: SortPart::split_into_parts(filename.clone()),
  39. })
  40. }
  41. fn ext(name: String) -> Option<String> {
  42. // The extension is the series of characters after a dot at
  43. // the end of a filename. This deliberately also counts
  44. // dotfiles - the ".git" folder has the extension "git".
  45. let re = regex!(r"\.([^.]+)$");
  46. re.captures(name.as_slice()).map(|caps| caps.at(1).to_string())
  47. }
  48. pub fn is_dotfile(&self) -> bool {
  49. self.name.as_slice().starts_with(".")
  50. }
  51. pub fn is_tmpfile(&self) -> bool {
  52. let name = self.name.as_slice();
  53. name.ends_with("~") || (name.starts_with("#") && name.ends_with("#"))
  54. }
  55. // Highlight the compiled versions of files. Some of them, like .o,
  56. // get special highlighting when they're alone because there's no
  57. // point in existing without their source. Others can be perfectly
  58. // content without their source files, such as how .js is valid
  59. // without a .coffee.
  60. pub fn get_source_files(&self) -> Vec<Path> {
  61. let ext = self.ext.clone().unwrap();
  62. match ext.as_slice() {
  63. "class" => vec![self.path.with_extension("java")], // Java
  64. "elc" => vec![self.path.with_extension("el")], // Emacs Lisp
  65. "hi" => vec![self.path.with_extension("hs")], // Haskell
  66. "o" => vec![self.path.with_extension("c"), self.path.with_extension("cpp")], // C, C++
  67. "pyc" => vec![self.path.with_extension("py")], // Python
  68. "js" => vec![self.path.with_extension("coffee"), self.path.with_extension("ts")], // CoffeeScript, TypeScript
  69. "css" => vec![self.path.with_extension("sass"), self.path.with_extension("less")], // SASS, Less
  70. "aux" => vec![self.path.with_extension("tex")], // TeX: auxiliary file
  71. "bbl" => vec![self.path.with_extension("tex")], // BibTeX bibliography file
  72. "blg" => vec![self.path.with_extension("tex")], // BibTeX log file
  73. "lof" => vec![self.path.with_extension("tex")], // list of figures
  74. "log" => vec![self.path.with_extension("tex")], // TeX log file
  75. "lot" => vec![self.path.with_extension("tex")], // list of tables
  76. "toc" => vec![self.path.with_extension("tex")], // table of contents
  77. _ => vec![],
  78. }
  79. }
  80. pub fn display(&self, column: &Column, unix: &mut Unix) -> String {
  81. match *column {
  82. Permissions => self.permissions_string(),
  83. FileName => self.file_name(),
  84. FileSize(use_iec) => self.file_size(use_iec),
  85. // A file with multiple links is interesting, but
  86. // directories and suchlike can have multiple links all
  87. // the time.
  88. HardLinks => {
  89. let style = if self.stat.kind == io::TypeFile && self.stat.unstable.nlink > 1 { Red.on(Yellow) } else { Red.normal() };
  90. style.paint(self.stat.unstable.nlink.to_str().as_slice())
  91. },
  92. Inode => Purple.paint(self.stat.unstable.inode.to_str().as_slice()),
  93. Blocks => {
  94. if self.stat.kind == io::TypeFile || self.stat.kind == io::TypeSymlink {
  95. Cyan.paint(self.stat.unstable.blocks.to_str().as_slice())
  96. }
  97. else {
  98. Fixed(244).paint("-")
  99. }
  100. },
  101. // Display the ID if the user/group doesn't exist, which
  102. // usually means it was deleted but its files weren't.
  103. User => {
  104. let uid = self.stat.unstable.uid as u32;
  105. unix.load_user(uid);
  106. let style = if unix.uid == uid { Yellow.bold() } else { Plain };
  107. let string = unix.get_user_name(uid).unwrap_or(uid.to_str());
  108. style.paint(string.as_slice())
  109. },
  110. Group => {
  111. let gid = self.stat.unstable.gid as u32;
  112. unix.load_group(gid);
  113. let name = unix.get_group_name(gid).unwrap_or(gid.to_str());
  114. let style = if unix.is_group_member(gid) { Yellow.normal() } else { Plain };
  115. style.paint(name.as_slice())
  116. },
  117. }
  118. }
  119. fn file_name(&self) -> String {
  120. let name = self.name.as_slice();
  121. let displayed_name = self.file_colour().paint(name);
  122. if self.stat.kind == io::TypeSymlink {
  123. match fs::readlink(self.path) {
  124. Ok(path) => {
  125. let target_path = if path.is_absolute() { path } else { self.dir.path.join(path) };
  126. format!("{} {}", displayed_name, self.target_file_name_and_arrow(target_path))
  127. }
  128. Err(_) => displayed_name,
  129. }
  130. }
  131. else {
  132. displayed_name
  133. }
  134. }
  135. fn target_file_name_and_arrow(&self, target_path: Path) -> String {
  136. let v = target_path.filename().unwrap();
  137. let filename = from_utf8_lossy(v).to_string();
  138. let link_target = fs::stat(&target_path).map(|stat| File {
  139. path: &target_path,
  140. dir: self.dir,
  141. stat: stat,
  142. name: filename.clone(),
  143. ext: File::ext(filename.clone()),
  144. parts: vec![], // not needed
  145. });
  146. // Statting a path usually fails because the file at the other
  147. // end doesn't exist. Show this by highlighting the target
  148. // file in red instead of displaying an error, because the
  149. // error would be shown out of context and it's almost always
  150. // that reason anyway.
  151. match link_target {
  152. Ok(file) => format!("{} {}", Fixed(244).paint("=>"), file.file_colour().paint(filename.as_slice())),
  153. Err(_) => format!("{} {}", Red.paint("=>"), Red.underline().paint(filename.as_slice())),
  154. }
  155. }
  156. fn file_size(&self, use_iec_prefixes: bool) -> String {
  157. // Don't report file sizes for directories. I've never looked
  158. // at one of those numbers and gained any information from it.
  159. if self.stat.kind == io::TypeDirectory {
  160. Fixed(244).paint("-")
  161. } else {
  162. let (size, suffix) = if use_iec_prefixes {
  163. format_IEC_bytes(self.stat.size)
  164. } else {
  165. format_metric_bytes(self.stat.size)
  166. };
  167. return format!("{}{}", Green.bold().paint(size.as_slice()), Green.paint(suffix.as_slice()));
  168. }
  169. }
  170. fn type_char(&self) -> String {
  171. return match self.stat.kind {
  172. io::TypeFile => ".".to_string(),
  173. io::TypeDirectory => Blue.paint("d"),
  174. io::TypeNamedPipe => Yellow.paint("|"),
  175. io::TypeBlockSpecial => Purple.paint("s"),
  176. io::TypeSymlink => Cyan.paint("l"),
  177. io::TypeUnknown => "?".to_string(),
  178. }
  179. }
  180. fn file_colour(&self) -> Style {
  181. self.get_type().style()
  182. }
  183. fn permissions_string(&self) -> String {
  184. let bits = self.stat.perm;
  185. return format!("{}{}{}{}{}{}{}{}{}{}",
  186. self.type_char(),
  187. // The first three are bold because they're the ones used
  188. // most often.
  189. File::permission_bit(bits, io::UserRead, "r", Yellow.bold()),
  190. File::permission_bit(bits, io::UserWrite, "w", Red.bold()),
  191. File::permission_bit(bits, io::UserExecute, "x", Green.bold().underline()),
  192. File::permission_bit(bits, io::GroupRead, "r", Yellow.normal()),
  193. File::permission_bit(bits, io::GroupWrite, "w", Red.normal()),
  194. File::permission_bit(bits, io::GroupExecute, "x", Green.normal()),
  195. File::permission_bit(bits, io::OtherRead, "r", Yellow.normal()),
  196. File::permission_bit(bits, io::OtherWrite, "w", Red.normal()),
  197. File::permission_bit(bits, io::OtherExecute, "x", Green.normal()),
  198. );
  199. }
  200. fn permission_bit(bits: io::FilePermission, bit: io::FilePermission, character: &'static str, style: Style) -> String {
  201. if bits.contains(bit) {
  202. style.paint(character.as_slice())
  203. } else {
  204. Black.bold().paint("-".as_slice())
  205. }
  206. }
  207. }