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