1 use std::fmt::Debug;
2 use std::path::Path;
3 
4 use ansi_term::{ANSIString, Style};
5 
6 use crate::fs::{File, FileTarget};
7 use crate::output::cell::TextCellContents;
8 use crate::output::escape;
9 use crate::output::icons::{icon_for_file, iconify_style};
10 use crate::output::render::FiletypeColours;
11 
12 
13 /// Basically a file name factory.
14 #[derive(Debug, Copy, Clone)]
15 pub struct Options {
16 
17     /// Whether to append file class characters to file names.
18     pub classify: Classify,
19 
20     /// Whether to prepend icon characters before file names.
21     pub show_icons: ShowIcons,
22 }
23 
24 impl Options {
25 
26     /// Create a new `FileName` that prints the given file’s name, painting it
27     /// with the remaining arguments.
for_file<'a, 'dir, C>(self, file: &'a File<'dir>, colours: &'a C) -> FileName<'a, 'dir, C>28     pub fn for_file<'a, 'dir, C>(self, file: &'a File<'dir>, colours: &'a C) -> FileName<'a, 'dir, C> {
29         FileName {
30             file,
31             colours,
32             link_style: LinkStyle::JustFilenames,
33             options:    self,
34             target:     if file.is_link() { Some(file.link_target()) }
35                                      else { None }
36         }
37     }
38 }
39 
40 /// When displaying a file name, there needs to be some way to handle broken
41 /// links, depending on how long the resulting Cell can be.
42 #[derive(PartialEq, Debug, Copy, Clone)]
43 enum LinkStyle {
44 
45     /// Just display the file names, but colour them differently if they’re
46     /// a broken link or can’t be followed.
47     JustFilenames,
48 
49     /// Display all files in their usual style, but follow each link with an
50     /// arrow pointing to their path, colouring the path differently if it’s
51     /// a broken link, and doing nothing if it can’t be followed.
52     FullLinkPaths,
53 }
54 
55 
56 /// Whether to append file class characters to the file names.
57 #[derive(PartialEq, Debug, Copy, Clone)]
58 pub enum Classify {
59 
60     /// Just display the file names, without any characters.
61     JustFilenames,
62 
63     /// Add a character after the file name depending on what class of file
64     /// it is.
65     AddFileIndicators,
66 }
67 
68 impl Default for Classify {
default() -> Self69     fn default() -> Self {
70         Self::JustFilenames
71     }
72 }
73 
74 
75 /// Whether and how to show icons.
76 #[derive(PartialEq, Debug, Copy, Clone)]
77 pub enum ShowIcons {
78 
79     /// Don’t show icons at all.
80     Off,
81 
82     /// Show icons next to file names, with the given number of spaces between
83     /// the icon and the file name.
84     On(u32),
85 }
86 
87 
88 /// A **file name** holds all the information necessary to display the name
89 /// of the given file. This is used in all of the views.
90 pub struct FileName<'a, 'dir, C> {
91 
92     /// A reference to the file that we’re getting the name of.
93     file: &'a File<'dir>,
94 
95     /// The colours used to paint the file name and its surrounding text.
96     colours: &'a C,
97 
98     /// The file that this file points to if it’s a link.
99     target: Option<FileTarget<'dir>>,  // todo: remove?
100 
101     /// How to handle displaying links.
102     link_style: LinkStyle,
103 
104     options: Options,
105 }
106 
107 impl<'a, 'dir, C> FileName<'a, 'dir, C> {
108 
109     /// Sets the flag on this file name to display link targets with an
110     /// arrow followed by their path.
with_link_paths(mut self) -> Self111     pub fn with_link_paths(mut self) -> Self {
112         self.link_style = LinkStyle::FullLinkPaths;
113         self
114     }
115 }
116 
117 impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
118 
119     /// Paints the name of the file using the colours, resulting in a vector
120     /// of coloured cells that can be printed to the terminal.
121     ///
122     /// This method returns some `TextCellContents`, rather than a `TextCell`,
123     /// because for the last cell in a table, it doesn’t need to have its
124     /// width calculated.
paint(&self) -> TextCellContents125     pub fn paint(&self) -> TextCellContents {
126         let mut bits = Vec::new();
127 
128         if let ShowIcons::On(spaces_count) = self.options.show_icons {
129             let style = iconify_style(self.style());
130             let file_icon = icon_for_file(self.file).to_string();
131 
132             bits.push(style.paint(file_icon));
133 
134             match spaces_count {
135                 1 => bits.push(style.paint(" ")),
136                 2 => bits.push(style.paint("  ")),
137                 n => bits.push(style.paint(spaces(n))),
138             }
139         }
140 
141         if self.file.parent_dir.is_none() {
142             if let Some(parent) = self.file.path.parent() {
143                 self.add_parent_bits(&mut bits, parent);
144             }
145         }
146 
147         if ! self.file.name.is_empty() {
148         	// The “missing file” colour seems like it should be used here,
149         	// but it’s not! In a grid view, where there’s no space to display
150         	// link targets, the filename has to have a different style to
151         	// indicate this fact. But when showing targets, we can just
152         	// colour the path instead (see below), and leave the broken
153         	// link’s filename as the link colour.
154             for bit in self.coloured_file_name() {
155                 bits.push(bit);
156             }
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 
166                     if let Some(parent) = target.path.parent() {
167                         self.add_parent_bits(&mut bits, parent);
168                     }
169 
170                     if ! target.name.is_empty() {
171                         let target_options = Options {
172                             classify: Classify::JustFilenames,
173                             show_icons: ShowIcons::Off,
174                         };
175 
176                         let target_name = FileName {
177                             file: target,
178                             colours: self.colours,
179                             target: None,
180                             link_style: LinkStyle::FullLinkPaths,
181                             options: target_options,
182                         };
183 
184                         for bit in target_name.coloured_file_name() {
185                             bits.push(bit);
186                         }
187 
188                         if let Classify::AddFileIndicators = self.options.classify {
189                             if let Some(class) = self.classify_char(target) {
190                                 bits.push(Style::default().paint(class));
191                             }
192                         }
193                     }
194                 }
195 
196                 FileTarget::Broken(broken_path) => {
197                     bits.push(Style::default().paint(" "));
198                     bits.push(self.colours.broken_symlink().paint("->"));
199                     bits.push(Style::default().paint(" "));
200 
201                     escape(
202                         broken_path.display().to_string(),
203                         &mut bits,
204                         self.colours.broken_filename(),
205                         self.colours.broken_control_char(),
206                     );
207                 }
208 
209                 FileTarget::Err(_) => {
210                     // Do nothing — the error gets displayed on the next line
211                 }
212             }
213         }
214         else if let Classify::AddFileIndicators = self.options.classify {
215             if let Some(class) = self.classify_char(self.file) {
216                 bits.push(Style::default().paint(class));
217             }
218         }
219 
220         bits.into()
221     }
222 
223     /// Adds the bits of the parent path to the given bits vector.
224     /// The path gets its characters escaped based on the colours.
add_parent_bits(&self, bits: &mut Vec<ANSIString<'_>>, parent: &Path)225     fn add_parent_bits(&self, bits: &mut Vec<ANSIString<'_>>, parent: &Path) {
226         let coconut = parent.components().count();
227 
228         if coconut == 1 && parent.has_root() {
229             bits.push(self.colours.symlink_path().paint("/"));
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("/"));
239         }
240     }
241 
242     /// The character to be displayed after a file when classifying is on, if
243     /// the file’s type has one associated with it.
classify_char(&self, file: &File<'_>) -> Option<&'static str>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 
265     /// Returns at least one ANSI-highlighted string representing this file’s
266     /// name using the given set of colours.
267     ///
268     /// Ordinarily, this will be just one string: the file’s complete name,
269     /// coloured according to its file type. If the name contains control
270     /// characters such as newlines or escapes, though, we can’t just print them
271     /// to the screen directly, because then there’ll be newlines in weird places.
272     ///
273     /// So in that situation, those characters will be escaped and highlighted in
274     /// a different colour.
coloured_file_name<'unused>(&self) -> Vec<ANSIString<'unused>>275     fn coloured_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
276         let file_style = self.style();
277         let mut bits = Vec::new();
278 
279         escape(
280             self.file.name.clone(),
281             &mut bits,
282             file_style,
283             self.colours.control_char(),
284         );
285 
286         bits
287     }
288 
289     /// Figures out which colour to paint the filename part of the output,
290     /// depending on which “type” of file it appears to be — either from the
291     /// class on the filesystem or from its name. (Or the broken link colour,
292     /// if there’s nowhere else for that fact to be shown.)
style(&self) -> Style293     pub fn style(&self) -> Style {
294         if let LinkStyle::JustFilenames = self.link_style {
295             if let Some(ref target) = self.target {
296                 if target.is_broken() {
297                     return self.colours.broken_symlink();
298                 }
299             }
300         }
301 
302         match self.file {
303             f if f.is_directory()        => self.colours.directory(),
304             f if f.is_executable_file()  => self.colours.executable_file(),
305             f if f.is_link()             => self.colours.symlink(),
306             f if f.is_pipe()             => self.colours.pipe(),
307             f if f.is_block_device()     => self.colours.block_device(),
308             f if f.is_char_device()      => self.colours.char_device(),
309             f if f.is_socket()           => self.colours.socket(),
310             f if ! f.is_file()           => self.colours.special(),
311             _                            => self.colours.colour_file(self.file),
312         }
313     }
314 }
315 
316 
317 /// The set of colours that are needed to paint a file name.
318 pub trait Colours: FiletypeColours {
319 
320     /// The style to paint the path of a symlink’s target, up to but not
321     /// including the file’s name.
symlink_path(&self) -> Style322     fn symlink_path(&self) -> Style;
323 
324     /// The style to paint the arrow between a link and its target.
normal_arrow(&self) -> Style325     fn normal_arrow(&self) -> Style;
326 
327 	/// The style to paint the filenames of broken links in views that don’t
328 	/// show link targets, and the style to paint the *arrow* between the link
329 	/// and its target in views that *do* show link targets.
broken_symlink(&self) -> Style330     fn broken_symlink(&self) -> Style;
331 
332     /// The style to paint the entire filename of a broken link.
broken_filename(&self) -> Style333     fn broken_filename(&self) -> Style;
334 
335     /// The style to paint a non-displayable control character in a filename.
control_char(&self) -> Style336     fn control_char(&self) -> Style;
337 
338     /// The style to paint a non-displayable control character in a filename,
339     /// when the filename is being displayed as a broken link target.
broken_control_char(&self) -> Style340     fn broken_control_char(&self) -> Style;
341 
342     /// The style to paint a file that has its executable bit set.
executable_file(&self) -> Style343     fn executable_file(&self) -> Style;
344 
colour_file(&self, file: &File<'_>) -> Style345     fn colour_file(&self, file: &File<'_>) -> Style;
346 }
347 
348 
349 /// Generate a string made of `n` spaces.
spaces(width: u32) -> String350 fn spaces(width: u32) -> String {
351     (0 .. width).into_iter().map(|_| ' ').collect()
352 }
353