file_name.rs 19 KB

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