//! The **Details** output view displays each file as a row in a table. //! //! It’s used in the following situations: //! //! - Most commonly, when using the `--long` command-line argument to display the //! details of each file, which requires using a table view to hold all the data; //! - When using the `--tree` argument, which uses the same table view to display //! each file on its own line, with the table providing the tree characters; //! - When using both the `--long` and `--grid` arguments, which constructs a //! series of tables to fit all the data on the screen. //! //! You will probably recognise it from the `ls --long` command. It looks like //! this: //! //! ```text //! .rw-r--r-- 9.6k ben 29 Jun 16:16 Cargo.lock //! .rw-r--r-- 547 ben 23 Jun 10:54 Cargo.toml //! .rw-r--r-- 1.1k ben 23 Nov 2014 LICENCE //! .rw-r--r-- 2.5k ben 21 May 14:38 README.md //! .rw-r--r-- 382k ben 8 Jun 21:00 screenshot.png //! drwxr-xr-x - ben 29 Jun 14:50 src //! drwxr-xr-x - ben 28 Jun 19:53 target //! ``` //! //! The table is constructed by creating a `Table` value, which produces a `Row` //! value for each file. These rows can contain a vector of `Cell`s, or they can //! contain depth information for the tree view, or both. These are described //! below. //! //! //! ## Constructing Detail Views //! //! When using the `--long` command-line argument, the details of each file are //! displayed next to its name. //! //! The table holds a vector of all the column types. For each file and column, a //! `Cell` value containing the ANSI-coloured text and Unicode width of each cell //! is generated, with the row and column determined by indexing into both arrays. //! //! The column types vector does not actually include the filename. This is //! because the filename is always the rightmost field, and as such, it does not //! need to have its width queried or be padded with spaces. //! //! To illustrate the above: //! //! ```text //! ┌─────────────────────────────────────────────────────────────────────────┐ //! │ columns: [ Permissions, Size, User, Date(Modified) ] │ //! ├─────────────────────────────────────────────────────────────────────────┤ //! │ rows: cells: filename: │ //! │ row 1: [ ".rw-r--r--", "9.6k", "ben", "29 Jun 16:16" ] Cargo.lock │ //! │ row 2: [ ".rw-r--r--", "547", "ben", "23 Jun 10:54" ] Cargo.toml │ //! │ row 3: [ "drwxr-xr-x", "-", "ben", "29 Jun 14:50" ] src │ //! │ row 4: [ "drwxr-xr-x", "-", "ben", "28 Jun 19:53" ] target │ //! └─────────────────────────────────────────────────────────────────────────┘ //! ``` //! //! Each column in the table needs to be resized to fit its widest argument. This //! means that we must wait until every row has been added to the table before it //! can be displayed, in order to make sure that every column is wide enough. use std::io::{Write, Error as IOError, Result as IOResult}; use std::mem::MaybeUninit; use std::path::PathBuf; use std::vec::IntoIter as VecIntoIter; use ansi_term::{ANSIGenericString, Style}; use scoped_threadpool::Pool; use crate::fs::{Dir, File}; use crate::fs::dir_action::RecurseOptions; use crate::fs::feature::git::GitCache; use crate::fs::feature::xattr::{Attribute, FileAttributes}; use crate::fs::filter::FileFilter; use crate::style::Colours; use crate::output::cell::TextCell; use crate::output::icons::painted_icon; use crate::output::file_name::FileStyle; use crate::output::table::{Table, Options as TableOptions, Row as TableRow}; use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth}; /// With the **Details** view, the output gets formatted into columns, with /// each `Column` object showing some piece of information about the file, /// such as its size, or its permissions. /// /// To do this, the results have to be written to a table, instead of /// displaying each file immediately. Then, the width of each column can be /// calculated based on the individual results, and the fields are padded /// during output. /// /// Almost all the heavy lifting is done in a Table object, which handles the /// columns for each row. #[derive(PartialEq, Debug)] pub struct Options { /// Options specific to drawing a table. /// /// Directories themselves can pick which columns are *added* to this /// list, such as the Git column. pub table: Option, /// Whether to show a header line or not. pub header: bool, /// Whether to show each file’s extended attributes. pub xattr: bool, /// Whether icons mode is enabled. pub icons: bool, } pub struct Render<'a> { pub dir: Option<&'a Dir>, pub files: Vec>, pub colours: &'a Colours, pub style: &'a FileStyle, pub opts: &'a Options, /// Whether to recurse through directories with a tree view, and if so, /// which options to use. This field is only relevant here if the `tree` /// field of the RecurseOptions is `true`. pub recurse: Option, /// How to sort and filter the files after getting their details. pub filter: &'a FileFilter, /// Whether we are skipping Git-ignored files. pub git_ignoring: bool, } struct Egg<'a> { table_row: Option, xattrs: Vec, errors: Vec<(IOError, Option)>, dir: Option, file: &'a File<'a>, icon: Option, } impl<'a> AsRef> for Egg<'a> { fn as_ref(&self) -> &File<'a> { self.file } } impl<'a> Render<'a> { pub fn render(self, mut git: Option<&'a GitCache>, w: &mut W) -> IOResult<()> { let mut pool = Pool::new(num_cpus::get() as u32); let mut rows = Vec::new(); if let Some(ref table) = self.opts.table { match (git, self.dir) { (Some(g), Some(d)) => if ! g.has_anything_for(&d.path) { git = None }, (Some(g), None) => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { git = None }, (None, _) => {/* Keep Git how it is */}, } let mut table = Table::new(table, git, self.colours); if self.opts.header { let header = table.header_row(); table.add_widths(&header); rows.push(self.render_header(header)); } // This is weird, but I can’t find a way around it: // https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6 let mut table = Some(table); self.add_files_to_table(&mut pool, &mut table, &mut rows, &self.files, git, TreeDepth::root()); for row in self.iterate_with_table(table.unwrap(), rows) { writeln!(w, "{}", row.strings())? } } else { self.add_files_to_table(&mut pool, &mut None, &mut rows, &self.files, git, TreeDepth::root()); for row in self.iterate(rows) { writeln!(w, "{}", row.strings())? } } Ok(()) } /// Adds files to the table, possibly recursively. This is easily /// parallelisable, and uses a pool of threads. fn add_files_to_table<'dir, 'ig>(&self, pool: &mut Pool, table: &mut Option>, rows: &mut Vec, src: &[File<'dir>], git: Option<&'ig GitCache>, depth: TreeDepth) { use std::sync::{Arc, Mutex}; use log::*; use crate::fs::feature::xattr; let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::>(); pool.scoped(|scoped| { let file_eggs = Arc::new(Mutex::new(&mut file_eggs)); let table = table.as_ref(); for (idx, file) in src.iter().enumerate() { let file_eggs = Arc::clone(&file_eggs); scoped.execute(move || { let mut errors = Vec::new(); let mut xattrs = Vec::new(); // There are three “levels” of extended attribute support: // // 1. If we’re compiling without that feature, then // exa pretends all files have no attributes. // 2. If the feature is enabled and the --extended flag // has been specified, then display an @ in the // permissions column for files with attributes, the // names of all attributes and their lengths, and any // errors encountered when getting them. // 3. If the --extended flag *hasn’t* been specified, then // display the @, but don’t display anything else. // // For a while, exa took a stricter approach to (3): // if an error occurred while checking a file’s xattrs to // see if it should display the @, exa would display that // error even though the attributes weren’t actually being // shown! This was confusing, as users were being shown // errors for something they didn’t explicitly ask for, // and just cluttered up the output. So now errors aren’t // printed unless the user passes --extended to signify // that they want to see them. if xattr::ENABLED { match file.path.attributes() { Ok(xs) => { xattrs.extend(xs); } Err(e) => { if self.opts.xattr { errors.push((e, None)); } else { error!("Error looking up xattr for {:?}: {:#?}", file.path, e); } } } } let table_row = table.as_ref() .map(|t| t.row_for_file(file, ! xattrs.is_empty())); if ! self.opts.xattr { xattrs.clear(); } let mut dir = None; if let Some(r) = self.recurse { if file.is_directory() && r.tree && ! r.is_too_deep(depth.0) { match file.to_dir() { Ok(d) => { dir = Some(d); }, Err(e) => { errors.push((e, None)) }, } } }; let icon = if self.opts.icons { Some(painted_icon(file, self.style)) } else { None }; let egg = Egg { table_row, xattrs, errors, dir, file, icon }; unsafe { std::ptr::write(file_eggs.lock().unwrap()[idx].as_mut_ptr(), egg) } }); } }); // this is safe because all entries have been initialized above let mut file_eggs = unsafe { std::mem::transmute::<_, Vec>(file_eggs) }; self.filter.sort_files(&mut file_eggs); for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) { let mut files = Vec::new(); let mut errors = egg.errors; if let (Some(ref mut t), Some(row)) = (table.as_mut(), egg.table_row.as_ref()) { t.add_widths(row); } let mut name_cell = TextCell::default(); if let Some(icon) = egg.icon { name_cell.push(ANSIGenericString::from(icon), 2) } let style = self.style.for_file(egg.file, self.colours) .with_link_paths() .paint() .promote(); name_cell.append(style); let row = Row { tree: tree_params, cells: egg.table_row, name: name_cell, }; rows.push(row); if let Some(ref dir) = egg.dir { for file_to_add in dir.files(self.filter.dot_filter, git, self.git_ignoring) { match file_to_add { Ok(f) => { files.push(f); } Err((path, e)) => { errors.push((e, Some(path))); } } } self.filter.filter_child_files(&mut files); if ! files.is_empty() { for xattr in egg.xattrs { rows.push(self.render_xattr(&xattr, TreeParams::new(depth.deeper(), false))); } for (error, path) in errors { rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), false), path)); } self.add_files_to_table(pool, table, rows, &files, git, depth.deeper()); continue; } } let count = egg.xattrs.len(); for (index, xattr) in egg.xattrs.into_iter().enumerate() { let params = TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1); let r = self.render_xattr(&xattr, params); rows.push(r); } let count = errors.len(); for (index, (error, path)) in errors.into_iter().enumerate() { let params = TreeParams::new(depth.deeper(), index == count - 1); let r = self.render_error(&error, params, path); rows.push(r); } } } pub fn render_header(&self, header: TableRow) -> Row { Row { tree: TreeParams::new(TreeDepth::root(), false), cells: Some(header), name: TextCell::paint_str(self.colours.header, "Name"), } } fn render_error(&self, error: &IOError, tree: TreeParams, path: Option) -> Row { use crate::output::file_name::Colours; let error_message = match path { Some(path) => format!("<{}: {}>", path.display(), error), None => format!("<{}>", error), }; // TODO: broken_symlink() doesn’t quite seem like the right name for // the style that’s being used here. Maybe split it in two? let name = TextCell::paint(self.colours.broken_symlink(), error_message); Row { cells: None, name, tree } } fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row { let name = TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)); Row { cells: None, name, tree } } pub fn render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row { Row { cells: Some(cells), name, tree } } pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec) -> TableIter<'a> { TableIter { tree_trunk: TreeTrunk::default(), total_width: table.widths().total(), table, inner: rows.into_iter(), tree_style: self.colours.punctuation, } } pub fn iterate(&'a self, rows: Vec) -> Iter { Iter { tree_trunk: TreeTrunk::default(), inner: rows.into_iter(), tree_style: self.colours.punctuation, } } } pub struct Row { /// Vector of cells to display. /// /// Most of the rows will be used to display files’ metadata, so this will /// almost always be `Some`, containing a vector of cells. It will only be /// `None` for a row displaying an attribute or error, neither of which /// have cells. pub cells: Option, /// This file’s name, in coloured output. The name is treated separately /// from the other cells, as it never requires padding. pub name: TextCell, /// Information used to determine which symbols to display in a tree. pub tree: TreeParams, } pub struct TableIter<'a> { inner: VecIntoIter, table: Table<'a>, total_width: usize, tree_style: Style, tree_trunk: TreeTrunk, } impl<'a> Iterator for TableIter<'a> { type Item = TextCell; fn next(&mut self) -> Option { self.inner.next().map(|row| { let mut cell = if let Some(cells) = row.cells { self.table.render(cells) } else { let mut cell = TextCell::default(); cell.add_spaces(self.total_width); cell }; for tree_part in self.tree_trunk.new_row(row.tree) { cell.push(self.tree_style.paint(tree_part.ascii_art()), 4); } // If any tree characters have been printed, then add an extra // space, which makes the output look much better. if ! row.tree.is_at_root() { cell.add_spaces(1); } cell.append(row.name); cell }) } } pub struct Iter { tree_trunk: TreeTrunk, tree_style: Style, inner: VecIntoIter, } impl Iterator for Iter { type Item = TextCell; fn next(&mut self) -> Option { self.inner.next().map(|row| { let mut cell = TextCell::default(); for tree_part in self.tree_trunk.new_row(row.tree) { cell.push(self.tree_style.paint(tree_part.ascii_art()), 4); } // If any tree characters have been printed, then add an extra // space, which makes the output look much better. if ! row.tree.is_at_root() { cell.add_spaces(1); } cell.append(row.name); cell }) } }