1 //! The **Details** output view displays each file as a row in a table.
2 //!
3 //! It’s used in the following situations:
4 //!
5 //! - Most commonly, when using the `--long` command-line argument to display the
6 //!   details of each file, which requires using a table view to hold all the data;
7 //! - When using the `--tree` argument, which uses the same table view to display
8 //!   each file on its own line, with the table providing the tree characters;
9 //! - When using both the `--long` and `--grid` arguments, which constructs a
10 //!   series of tables to fit all the data on the screen.
11 //!
12 //! You will probably recognise it from the `ls --long` command. It looks like
13 //! this:
14 //!
15 //! ```text
16 //!     .rw-r--r--  9.6k ben 29 Jun 16:16 Cargo.lock
17 //!     .rw-r--r--   547 ben 23 Jun 10:54 Cargo.toml
18 //!     .rw-r--r--  1.1k ben 23 Nov  2014 LICENCE
19 //!     .rw-r--r--  2.5k ben 21 May 14:38 README.md
20 //!     .rw-r--r--  382k ben  8 Jun 21:00 screenshot.png
21 //!     drwxr-xr-x     - ben 29 Jun 14:50 src
22 //!     drwxr-xr-x     - ben 28 Jun 19:53 target
23 //! ```
24 //!
25 //! The table is constructed by creating a `Table` value, which produces a `Row`
26 //! value for each file. These rows can contain a vector of `Cell`s, or they can
27 //! contain depth information for the tree view, or both. These are described
28 //! below.
29 //!
30 //!
31 //! ## Constructing Detail Views
32 //!
33 //! When using the `--long` command-line argument, the details of each file are
34 //! displayed next to its name.
35 //!
36 //! The table holds a vector of all the column types. For each file and column, a
37 //! `Cell` value containing the ANSI-coloured text and Unicode width of each cell
38 //! is generated, with the row and column determined by indexing into both arrays.
39 //!
40 //! The column types vector does not actually include the filename. This is
41 //! because the filename is always the rightmost field, and as such, it does not
42 //! need to have its width queried or be padded with spaces.
43 //!
44 //! To illustrate the above:
45 //!
46 //! ```text
47 //!     ┌─────────────────────────────────────────────────────────────────────────┐
48 //!     │ columns: [ Permissions,  Size,   User,  Date(Modified) ]                │
49 //!     ├─────────────────────────────────────────────────────────────────────────┤
50 //!     │   rows:  cells:                                            filename:    │
51 //!     │   row 1: [ ".rw-r--r--", "9.6k", "ben", "29 Jun 16:16" ]   Cargo.lock   │
52 //!     │   row 2: [ ".rw-r--r--",  "547", "ben", "23 Jun 10:54" ]   Cargo.toml   │
53 //!     │   row 3: [ "drwxr-xr-x",    "-", "ben", "29 Jun 14:50" ]   src          │
54 //!     │   row 4: [ "drwxr-xr-x",    "-", "ben", "28 Jun 19:53" ]   target       │
55 //!     └─────────────────────────────────────────────────────────────────────────┘
56 //! ```
57 //!
58 //! Each column in the table needs to be resized to fit its widest argument. This
59 //! means that we must wait until every row has been added to the table before it
60 //! can be displayed, in order to make sure that every column is wide enough.
61 
62 
63 use std::io::{self, Write};
64 use std::mem::MaybeUninit;
65 use std::path::PathBuf;
66 use std::vec::IntoIter as VecIntoIter;
67 
68 use ansi_term::Style;
69 use scoped_threadpool::Pool;
70 
71 use crate::fs::{Dir, File};
72 use crate::fs::dir_action::RecurseOptions;
73 use crate::fs::feature::git::GitCache;
74 use crate::fs::feature::xattr::{Attribute, FileAttributes};
75 use crate::fs::filter::FileFilter;
76 use crate::output::cell::TextCell;
77 use crate::output::file_name::Options as FileStyle;
78 use crate::output::table::{Table, Options as TableOptions, Row as TableRow};
79 use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth};
80 use crate::theme::Theme;
81 
82 
83 /// With the **Details** view, the output gets formatted into columns, with
84 /// each `Column` object showing some piece of information about the file,
85 /// such as its size, or its permissions.
86 ///
87 /// To do this, the results have to be written to a table, instead of
88 /// displaying each file immediately. Then, the width of each column can be
89 /// calculated based on the individual results, and the fields are padded
90 /// during output.
91 ///
92 /// Almost all the heavy lifting is done in a Table object, which handles the
93 /// columns for each row.
94 #[derive(PartialEq, Debug)]
95 pub struct Options {
96 
97     /// Options specific to drawing a table.
98     ///
99     /// Directories themselves can pick which columns are *added* to this
100     /// list, such as the Git column.
101     pub table: Option<TableOptions>,
102 
103     /// Whether to show a header line or not.
104     pub header: bool,
105 
106     /// Whether to show each file’s extended attributes.
107     pub xattr: bool,
108 }
109 
110 
111 pub struct Render<'a> {
112     pub dir: Option<&'a Dir>,
113     pub files: Vec<File<'a>>,
114     pub theme: &'a Theme,
115     pub file_style: &'a FileStyle,
116     pub opts: &'a Options,
117 
118     /// Whether to recurse through directories with a tree view, and if so,
119     /// which options to use. This field is only relevant here if the `tree`
120     /// field of the RecurseOptions is `true`.
121     pub recurse: Option<RecurseOptions>,
122 
123     /// How to sort and filter the files after getting their details.
124     pub filter: &'a FileFilter,
125 
126     /// Whether we are skipping Git-ignored files.
127     pub git_ignoring: bool,
128 
129     pub git: Option<&'a GitCache>,
130 }
131 
132 
133 struct Egg<'a> {
134     table_row: Option<TableRow>,
135     xattrs:    Vec<Attribute>,
136     errors:    Vec<(io::Error, Option<PathBuf>)>,
137     dir:       Option<Dir>,
138     file:      &'a File<'a>,
139 }
140 
141 impl<'a> AsRef<File<'a>> for Egg<'a> {
as_ref(&self) -> &File<'a>142     fn as_ref(&self) -> &File<'a> {
143         self.file
144     }
145 }
146 
147 
148 impl<'a> Render<'a> {
render<W: Write>(mut self, w: &mut W) -> io::Result<()>149     pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
150         let mut pool = Pool::new(num_cpus::get() as u32);
151         let mut rows = Vec::new();
152 
153         if let Some(ref table) = self.opts.table {
154             match (self.git, self.dir) {
155                 (Some(g), Some(d))  => if ! g.has_anything_for(&d.path) { self.git = None },
156                 (Some(g), None)     => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { self.git = None },
157                 (None,    _)        => {/* Keep Git how it is */},
158             }
159 
160             let mut table = Table::new(table, self.git, &self.theme);
161 
162             if self.opts.header {
163                 let header = table.header_row();
164                 table.add_widths(&header);
165                 rows.push(self.render_header(header));
166             }
167 
168             // This is weird, but I can’t find a way around it:
169             // https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
170             let mut table = Some(table);
171             self.add_files_to_table(&mut pool, &mut table, &mut rows, &self.files, TreeDepth::root());
172 
173             for row in self.iterate_with_table(table.unwrap(), rows) {
174                 writeln!(w, "{}", row.strings())?
175             }
176         }
177         else {
178             self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, TreeDepth::root());
179 
180             for row in self.iterate(rows) {
181                 writeln!(w, "{}", row.strings())?
182             }
183         }
184 
185         Ok(())
186     }
187 
188     /// Adds files to the table, possibly recursively. This is easily
189     /// parallelisable, and uses a pool of threads.
add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth)190     fn add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth) {
191         use std::sync::{Arc, Mutex};
192         use log::*;
193         use crate::fs::feature::xattr;
194 
195         let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::<Vec<_>>();
196 
197         pool.scoped(|scoped| {
198             let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
199             let table = table.as_ref();
200 
201             for (idx, file) in src.iter().enumerate() {
202                 let file_eggs = Arc::clone(&file_eggs);
203 
204                 scoped.execute(move || {
205                     let mut errors = Vec::new();
206                     let mut xattrs = Vec::new();
207 
208                     // There are three “levels” of extended attribute support:
209                     //
210                     // 1. If we’re compiling without that feature, then
211                     //    exa pretends all files have no attributes.
212                     // 2. If the feature is enabled and the --extended flag
213                     //    has been specified, then display an @ in the
214                     //    permissions column for files with attributes, the
215                     //    names of all attributes and their lengths, and any
216                     //    errors encountered when getting them.
217                     // 3. If the --extended flag *hasn’t* been specified, then
218                     //    display the @, but don’t display anything else.
219                     //
220                     // For a while, exa took a stricter approach to (3):
221                     // if an error occurred while checking a file’s xattrs to
222                     // see if it should display the @, exa would display that
223                     // error even though the attributes weren’t actually being
224                     // shown! This was confusing, as users were being shown
225                     // errors for something they didn’t explicitly ask for,
226                     // and just cluttered up the output. So now errors aren’t
227                     // printed unless the user passes --extended to signify
228                     // that they want to see them.
229 
230                     if xattr::ENABLED {
231                         match file.path.attributes() {
232                             Ok(xs) => {
233                                 xattrs.extend(xs);
234                             }
235                             Err(e) => {
236                                 if self.opts.xattr {
237                                     errors.push((e, None));
238                                 }
239                                 else {
240                                     error!("Error looking up xattr for {:?}: {:#?}", file.path, e);
241                                 }
242                             }
243                         }
244                     }
245 
246                     let table_row = table.as_ref()
247                                          .map(|t| t.row_for_file(file, ! xattrs.is_empty()));
248 
249                     if ! self.opts.xattr {
250                         xattrs.clear();
251                     }
252 
253                     let mut dir = None;
254                     if let Some(r) = self.recurse {
255                         if file.is_directory() && r.tree && ! r.is_too_deep(depth.0) {
256                             match file.to_dir() {
257                                 Ok(d) => {
258                                     dir = Some(d);
259                                 }
260                                 Err(e) => {
261                                     errors.push((e, None));
262                                 }
263                             }
264                         }
265                     };
266 
267                     let egg = Egg { table_row, xattrs, errors, dir, file };
268                     unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) }
269                 });
270             }
271         });
272 
273         // this is safe because all entries have been initialized above
274         let mut file_eggs = unsafe { std::mem::transmute::<_, Vec<Egg<'_>>>(file_eggs) };
275         self.filter.sort_files(&mut file_eggs);
276 
277         for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) {
278             let mut files = Vec::new();
279             let mut errors = egg.errors;
280 
281             if let (Some(ref mut t), Some(row)) = (table.as_mut(), egg.table_row.as_ref()) {
282                 t.add_widths(row);
283             }
284 
285             let file_name = self.file_style.for_file(egg.file, self.theme)
286                                 .with_link_paths()
287                                 .paint()
288                                 .promote();
289 
290             let row = Row {
291                 tree:   tree_params,
292                 cells:  egg.table_row,
293                 name:   file_name,
294             };
295 
296             rows.push(row);
297 
298             if let Some(ref dir) = egg.dir {
299                 for file_to_add in dir.files(self.filter.dot_filter, self.git, self.git_ignoring) {
300                     match file_to_add {
301                         Ok(f) => {
302                             files.push(f);
303                         }
304                         Err((path, e)) => {
305                             errors.push((e, Some(path)));
306                         }
307                     }
308                 }
309 
310                 self.filter.filter_child_files(&mut files);
311 
312                 if ! files.is_empty() {
313                     for xattr in egg.xattrs {
314                         rows.push(self.render_xattr(&xattr, TreeParams::new(depth.deeper(), false)));
315                     }
316 
317                     for (error, path) in errors {
318                         rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path));
319                     }
320 
321                     self.add_files_to_table(pool, table, rows, &files, depth.deeper());
322                     continue;
323                 }
324             }
325 
326             let count = egg.xattrs.len();
327             for (index, xattr) in egg.xattrs.into_iter().enumerate() {
328                 let params = TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
329                 let r = self.render_xattr(&xattr, params);
330                 rows.push(r);
331             }
332 
333             let count = errors.len();
334             for (index, (error, path)) in errors.into_iter().enumerate() {
335                 let params = TreeParams::new(depth.deeper(), index == count - 1);
336                 let r = self.render_error(&error, params, path);
337                 rows.push(r);
338             }
339         }
340     }
341 
render_header(&self, header: TableRow) -> Row342     pub fn render_header(&self, header: TableRow) -> Row {
343         Row {
344             tree:     TreeParams::new(TreeDepth::root(), false),
345             cells:    Some(header),
346             name:     TextCell::paint_str(self.theme.ui.header, "Name"),
347         }
348     }
349 
render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row350     fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row {
351         use crate::output::file_name::Colours;
352 
353         let error_message = match path {
354             Some(path) => format!("<{}: {}>", path.display(), error),
355             None       => format!("<{}>", error),
356         };
357 
358         // TODO: broken_symlink() doesn’t quite seem like the right name for
359         // the style that’s being used here. Maybe split it in two?
360         let name = TextCell::paint(self.theme.broken_symlink(), error_message);
361         Row { cells: None, name, tree }
362     }
363 
render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row364     fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
365         let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{} (len {})", xattr.name, xattr.size));
366         Row { cells: None, name, tree }
367     }
368 
render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row369     pub fn render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row {
370         Row { cells: Some(cells), name, tree }
371     }
372 
iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a>373     pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> {
374         TableIter {
375             tree_trunk: TreeTrunk::default(),
376             total_width: table.widths().total(),
377             table,
378             inner: rows.into_iter(),
379             tree_style: self.theme.ui.punctuation,
380         }
381     }
382 
iterate(&'a self, rows: Vec<Row>) -> Iter383     pub fn iterate(&'a self, rows: Vec<Row>) -> Iter {
384         Iter {
385             tree_trunk: TreeTrunk::default(),
386             inner: rows.into_iter(),
387             tree_style: self.theme.ui.punctuation,
388         }
389     }
390 }
391 
392 
393 pub struct Row {
394 
395     /// Vector of cells to display.
396     ///
397     /// Most of the rows will be used to display files’ metadata, so this will
398     /// almost always be `Some`, containing a vector of cells. It will only be
399     /// `None` for a row displaying an attribute or error, neither of which
400     /// have cells.
401     pub cells: Option<TableRow>,
402 
403     /// This file’s name, in coloured output. The name is treated separately
404     /// from the other cells, as it never requires padding.
405     pub name: TextCell,
406 
407     /// Information used to determine which symbols to display in a tree.
408     pub tree: TreeParams,
409 }
410 
411 
412 pub struct TableIter<'a> {
413     inner: VecIntoIter<Row>,
414     table: Table<'a>,
415 
416     total_width: usize,
417     tree_style:  Style,
418     tree_trunk:  TreeTrunk,
419 }
420 
421 impl<'a> Iterator for TableIter<'a> {
422     type Item = TextCell;
423 
next(&mut self) -> Option<Self::Item>424     fn next(&mut self) -> Option<Self::Item> {
425         self.inner.next().map(|row| {
426             let mut cell =
427                 if let Some(cells) = row.cells {
428                     self.table.render(cells)
429                 }
430                 else {
431                     let mut cell = TextCell::default();
432                     cell.add_spaces(self.total_width);
433                     cell
434                 };
435 
436             for tree_part in self.tree_trunk.new_row(row.tree) {
437                 cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
438             }
439 
440             // If any tree characters have been printed, then add an extra
441             // space, which makes the output look much better.
442             if ! row.tree.is_at_root() {
443                 cell.add_spaces(1);
444             }
445 
446             cell.append(row.name);
447             cell
448         })
449     }
450 }
451 
452 
453 pub struct Iter {
454     tree_trunk: TreeTrunk,
455     tree_style: Style,
456     inner: VecIntoIter<Row>,
457 }
458 
459 impl Iterator for Iter {
460     type Item = TextCell;
461 
next(&mut self) -> Option<Self::Item>462     fn next(&mut self) -> Option<Self::Item> {
463         self.inner.next().map(|row| {
464             let mut cell = TextCell::default();
465 
466             for tree_part in self.tree_trunk.new_row(row.tree) {
467                 cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
468             }
469 
470             // If any tree characters have been printed, then add an extra
471             // space, which makes the output look much better.
472             if ! row.tree.is_at_root() {
473                 cell.add_spaces(1);
474             }
475 
476             cell.append(row.name);
477             cell
478         })
479     }
480 }
481