file_name.rs 15 KB

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