file_name.rs 15 KB

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