file_name.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. use std::fmt::Debug;
  2. use std::path::Path;
  3. use ansiterm::{ANSIString, Style};
  4. use crate::fs::mounts::MountedFs;
  5. use crate::fs::{File, FileTarget};
  6. use crate::output::cell::TextCellContents;
  7. use crate::output::escape;
  8. use crate::output::icons::{icon_for_file, iconify_style};
  9. use crate::output::render::FiletypeColours;
  10. /// Basically a file name factory.
  11. #[derive(Debug, Copy, Clone)]
  12. pub struct Options {
  13. /// Whether to append file class characters to file names.
  14. pub classify: Classify,
  15. /// Whether to prepend icon characters before file names.
  16. pub show_icons: ShowIcons,
  17. /// Whether to make file names hyperlinks.
  18. pub embed_hyperlinks: EmbedHyperlinks,
  19. }
  20. impl Options {
  21. /// Create a new `FileName` that prints the given file’s name, painting it
  22. /// with the remaining arguments.
  23. pub fn for_file<'a, 'dir, C>(
  24. self,
  25. file: &'a File<'dir>,
  26. colours: &'a C,
  27. ) -> FileName<'a, 'dir, C> {
  28. FileName {
  29. file,
  30. colours,
  31. link_style: LinkStyle::JustFilenames,
  32. options: self,
  33. target: if file.is_link() {
  34. Some(file.link_target())
  35. } else {
  36. None
  37. },
  38. mount_style: MountStyle::JustDirectoryNames,
  39. mounted_fs: file.mount_point_info(),
  40. }
  41. }
  42. }
  43. /// When displaying a file name, there needs to be some way to handle broken
  44. /// links, depending on how long the resulting Cell can be.
  45. #[derive(PartialEq, Debug, Copy, Clone)]
  46. enum LinkStyle {
  47. /// Just display the file names, but colour them differently if they’re
  48. /// a broken link or can’t be followed.
  49. JustFilenames,
  50. /// Display all files in their usual style, but follow each link with an
  51. /// arrow pointing to their path, colouring the path differently if it’s
  52. /// a broken link, and doing nothing if it can’t be followed.
  53. FullLinkPaths,
  54. }
  55. /// Whether to append file class characters to the file names.
  56. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  57. pub enum Classify {
  58. /// Just display the file names, without any characters.
  59. JustFilenames,
  60. /// Add a character after the file name depending on what class of file
  61. /// it is.
  62. AddFileIndicators,
  63. }
  64. impl Default for Classify {
  65. fn default() -> Self {
  66. Self::JustFilenames
  67. }
  68. }
  69. /// When displaying a directory name, there needs to be some way to handle
  70. /// mount details, depending on how long the resulting Cell can be.
  71. #[derive(PartialEq, Debug, Copy, Clone)]
  72. enum MountStyle {
  73. /// Just display the directory names.
  74. JustDirectoryNames,
  75. /// Display mount points as directories and include information about
  76. /// the filesystem that's mounted there.
  77. MountInfo,
  78. }
  79. /// Whether and how to show icons.
  80. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  81. pub enum ShowIcons {
  82. /// Don’t show icons at all.
  83. Off,
  84. /// Show icons next to file names, with the given number of spaces between
  85. /// the icon and the file name.
  86. On(u32),
  87. }
  88. /// Whether to embed hyperlinks.
  89. #[derive(PartialEq, Eq, Debug, Copy, Clone)]
  90. pub enum EmbedHyperlinks {
  91. Off,
  92. On,
  93. }
  94. /// A **file name** holds all the information necessary to display the name
  95. /// of the given file. This is used in all of the views.
  96. pub struct FileName<'a, 'dir, C> {
  97. /// A reference to the file that we’re getting the name of.
  98. file: &'a File<'dir>,
  99. /// The colours used to paint the file name and its surrounding text.
  100. colours: &'a C,
  101. /// The file that this file points to if it’s a link.
  102. target: Option<FileTarget<'dir>>, // todo: remove?
  103. /// How to handle displaying links.
  104. link_style: LinkStyle,
  105. pub options: Options,
  106. /// The filesystem details for a mounted filesystem.
  107. mounted_fs: Option<&'a MountedFs>,
  108. /// How to handle displaying a mounted filesystem.
  109. mount_style: MountStyle,
  110. }
  111. impl<'a, 'dir, C> FileName<'a, 'dir, C> {
  112. /// Sets the flag on this file name to display link targets with an
  113. /// arrow followed by their path.
  114. pub fn with_link_paths(mut self) -> Self {
  115. self.link_style = LinkStyle::FullLinkPaths;
  116. self
  117. }
  118. /// Sets the flag on this file name to display mounted filesystem
  119. ///details.
  120. pub fn with_mount_details(mut self, enable: bool) -> Self {
  121. self.mount_style = if enable {
  122. MountStyle::MountInfo
  123. } else {
  124. MountStyle::JustDirectoryNames
  125. };
  126. self
  127. }
  128. }
  129. impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
  130. /// Paints the name of the file using the colours, resulting in a vector
  131. /// of coloured cells that can be printed to the terminal.
  132. ///
  133. /// This method returns some `TextCellContents`, rather than a `TextCell`,
  134. /// because for the last cell in a table, it doesn’t need to have its
  135. /// width calculated.
  136. pub fn paint(&self) -> TextCellContents {
  137. let mut bits = Vec::new();
  138. if let ShowIcons::On(spaces_count) = self.options.show_icons {
  139. let style = iconify_style(self.style());
  140. let file_icon = icon_for_file(self.file).to_string();
  141. bits.push(style.paint(file_icon));
  142. match spaces_count {
  143. 1 => bits.push(style.paint(" ")),
  144. 2 => bits.push(style.paint(" ")),
  145. n => bits.push(style.paint(spaces(n))),
  146. }
  147. }
  148. if self.file.parent_dir.is_none() {
  149. if let Some(parent) = self.file.path.parent() {
  150. self.add_parent_bits(&mut bits, parent);
  151. }
  152. }
  153. if !self.file.name.is_empty() {
  154. // The “missing file” colour seems like it should be used here,
  155. // but it’s not! In a grid view, where there’s no space to display
  156. // link targets, the filename has to have a different style to
  157. // indicate this fact. But when showing targets, we can just
  158. // colour the path instead (see below), and leave the broken
  159. // link’s filename as the link colour.
  160. for bit in self.escaped_file_name() {
  161. bits.push(bit);
  162. }
  163. }
  164. if let (LinkStyle::FullLinkPaths, Some(target)) = (self.link_style, self.target.as_ref()) {
  165. match target {
  166. FileTarget::Ok(target) => {
  167. bits.push(Style::default().paint(" "));
  168. bits.push(self.colours.normal_arrow().paint("->"));
  169. bits.push(Style::default().paint(" "));
  170. if let Some(parent) = target.path.parent() {
  171. self.add_parent_bits(&mut bits, parent);
  172. }
  173. if !target.name.is_empty() {
  174. let target_options = Options {
  175. classify: Classify::JustFilenames,
  176. show_icons: ShowIcons::Off,
  177. embed_hyperlinks: EmbedHyperlinks::Off,
  178. };
  179. let target_name = FileName {
  180. file: target,
  181. colours: self.colours,
  182. target: None,
  183. link_style: LinkStyle::FullLinkPaths,
  184. options: target_options,
  185. mounted_fs: None,
  186. mount_style: MountStyle::JustDirectoryNames,
  187. };
  188. for bit in target_name.escaped_file_name() {
  189. bits.push(bit);
  190. }
  191. if let Classify::AddFileIndicators = self.options.classify {
  192. if let Some(class) = self.classify_char(target) {
  193. bits.push(Style::default().paint(class));
  194. }
  195. }
  196. }
  197. }
  198. FileTarget::Broken(broken_path) => {
  199. bits.push(Style::default().paint(" "));
  200. bits.push(self.colours.broken_symlink().paint("->"));
  201. bits.push(Style::default().paint(" "));
  202. escape(
  203. broken_path.display().to_string(),
  204. &mut bits,
  205. self.colours.broken_filename(),
  206. self.colours.broken_control_char(),
  207. );
  208. }
  209. FileTarget::Err(_) => {
  210. // Do nothing — the error gets displayed on the next line
  211. }
  212. }
  213. } else if let Classify::AddFileIndicators = self.options.classify {
  214. if let Some(class) = self.classify_char(self.file) {
  215. bits.push(Style::default().paint(class));
  216. }
  217. }
  218. if let (MountStyle::MountInfo, Some(mount_details)) =
  219. (self.mount_style, self.mounted_fs.as_ref())
  220. {
  221. // This is a filesystem mounted on the directory, output its details
  222. bits.push(Style::default().paint(" ["));
  223. bits.push(Style::default().paint(mount_details.source.clone()));
  224. bits.push(Style::default().paint(" ("));
  225. bits.push(Style::default().paint(mount_details.fstype.clone()));
  226. bits.push(Style::default().paint(")]"));
  227. }
  228. bits.into()
  229. }
  230. /// Adds the bits of the parent path to the given bits vector.
  231. /// The path gets its characters escaped based on the colours.
  232. fn add_parent_bits(&self, bits: &mut Vec<ANSIString<'_>>, parent: &Path) {
  233. let coconut = parent.components().count();
  234. if coconut == 1 && parent.has_root() {
  235. bits.push(
  236. self.colours
  237. .symlink_path()
  238. .paint(std::path::MAIN_SEPARATOR.to_string()),
  239. );
  240. } else if coconut >= 1 {
  241. escape(
  242. parent.to_string_lossy().to_string(),
  243. bits,
  244. self.colours.symlink_path(),
  245. self.colours.control_char(),
  246. );
  247. bits.push(
  248. self.colours
  249. .symlink_path()
  250. .paint(std::path::MAIN_SEPARATOR.to_string()),
  251. );
  252. }
  253. }
  254. /// The character to be displayed after a file when classifying is on, if
  255. /// the file’s type has one associated with it.
  256. #[cfg(unix)]
  257. fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
  258. if file.is_executable_file() {
  259. Some("*")
  260. } else if file.is_directory() {
  261. Some("/")
  262. } else if file.is_pipe() {
  263. Some("|")
  264. } else if file.is_link() {
  265. Some("@")
  266. } else if file.is_socket() {
  267. Some("=")
  268. } else {
  269. None
  270. }
  271. }
  272. #[cfg(windows)]
  273. fn classify_char(&self, file: &File<'_>) -> Option<&'static str> {
  274. if file.is_directory() {
  275. Some("/")
  276. } else if file.is_link() {
  277. Some("@")
  278. } else {
  279. None
  280. }
  281. }
  282. /// Returns at least one ANSI-highlighted string representing this file’s
  283. /// name using the given set of colours.
  284. ///
  285. /// If --hyperlink flag is provided, it will escape the filename accordingly.
  286. ///
  287. /// Ordinarily, this will be just one string: the file’s complete name,
  288. /// coloured according to its file type. If the name contains control
  289. /// characters such as newlines or escapes, though, we can’t just print them
  290. /// to the screen directly, because then there’ll be newlines in weird places.
  291. ///
  292. /// So in that situation, those characters will be escaped and highlighted in
  293. /// a different colour.
  294. fn escaped_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
  295. use percent_encoding::{utf8_percent_encode, CONTROLS};
  296. const HYPERLINK_START: &str = "\x1B]8;;";
  297. const HYPERLINK_END: &str = "\x1B\x5C";
  298. let file_style = self.style();
  299. let mut bits = Vec::new();
  300. let mut display_hyperlink = false;
  301. if self.options.embed_hyperlinks == EmbedHyperlinks::On {
  302. if let Some(abs_path) = self
  303. .file
  304. .absolute_path()
  305. .and_then(|p| p.as_os_str().to_str())
  306. {
  307. let abs_path = utf8_percent_encode(abs_path, CONTROLS).to_string();
  308. // On Windows, `std::fs::canonicalize` adds the Win32 File prefix, which we need to remove
  309. #[cfg(target_os = "windows")]
  310. let abs_path = abs_path.strip_prefix("\\\\?\\").unwrap_or(&abs_path);
  311. bits.push(ANSIString::from(format!(
  312. "{HYPERLINK_START}file://{abs_path}{HYPERLINK_END}"
  313. )));
  314. display_hyperlink = true;
  315. }
  316. }
  317. escape(
  318. self.file.name.clone(),
  319. &mut bits,
  320. file_style,
  321. self.colours.control_char(),
  322. );
  323. if display_hyperlink {
  324. bits.push(ANSIString::from(format!(
  325. "{HYPERLINK_START}{HYPERLINK_END}"
  326. )));
  327. }
  328. bits
  329. }
  330. /// Figures out which colour to paint the filename part of the output,
  331. /// depending on which “type” of file it appears to be — either from the
  332. /// class on the filesystem or from its name. (Or the broken link colour,
  333. /// if there’s nowhere else for that fact to be shown.)
  334. pub fn style(&self) -> Style {
  335. if let LinkStyle::JustFilenames = self.link_style {
  336. if let Some(ref target) = self.target {
  337. if target.is_broken() {
  338. return self.colours.broken_symlink();
  339. }
  340. }
  341. }
  342. #[rustfmt::skip]
  343. return match self.file {
  344. f if f.is_mount_point() => self.colours.mount_point(),
  345. f if f.is_directory() => self.colours.directory(),
  346. #[cfg(unix)]
  347. f if f.is_executable_file() => self.colours.executable_file(),
  348. f if f.is_link() => self.colours.symlink(),
  349. #[cfg(unix)]
  350. f if f.is_pipe() => self.colours.pipe(),
  351. #[cfg(unix)]
  352. f if f.is_block_device() => self.colours.block_device(),
  353. #[cfg(unix)]
  354. f if f.is_char_device() => self.colours.char_device(),
  355. #[cfg(unix)]
  356. f if f.is_socket() => self.colours.socket(),
  357. f if ! f.is_file() => self.colours.special(),
  358. _ => self.colours.colour_file(self.file),
  359. };
  360. }
  361. /// For grid's use, to cover the case of hyperlink escape sequences
  362. pub fn bare_width(&self) -> usize {
  363. self.file.name.len()
  364. }
  365. }
  366. /// The set of colours that are needed to paint a file name.
  367. pub trait Colours: FiletypeColours {
  368. /// The style to paint the path of a symlink’s target, up to but not
  369. /// including the file’s name.
  370. fn symlink_path(&self) -> Style;
  371. /// The style to paint the arrow between a link and its target.
  372. fn normal_arrow(&self) -> Style;
  373. /// The style to paint the filenames of broken links in views that don’t
  374. /// show link targets, and the style to paint the *arrow* between the link
  375. /// and its target in views that *do* show link targets.
  376. fn broken_symlink(&self) -> Style;
  377. /// The style to paint the entire filename of a broken link.
  378. fn broken_filename(&self) -> Style;
  379. /// The style to paint a non-displayable control character in a filename.
  380. fn control_char(&self) -> Style;
  381. /// The style to paint a non-displayable control character in a filename,
  382. /// when the filename is being displayed as a broken link target.
  383. fn broken_control_char(&self) -> Style;
  384. /// The style to paint a file that has its executable bit set.
  385. fn executable_file(&self) -> Style;
  386. /// The style to paint a directory that has a filesystem mounted on it.
  387. fn mount_point(&self) -> Style;
  388. fn colour_file(&self, file: &File<'_>) -> Style;
  389. }
  390. /// Generate a string made of `n` spaces.
  391. fn spaces(width: u32) -> String {
  392. (0..width).map(|_| ' ').collect()
  393. }