| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- //! 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::{self, Write};
- use std::mem::MaybeUninit;
- use std::path::PathBuf;
- use std::vec::IntoIter as VecIntoIter;
- use ansi_term::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;
- use crate::fs::fields::SecurityContextType;
- use crate::fs::filter::FileFilter;
- use crate::output::cell::TextCell;
- use crate::output::file_name::Options as FileStyle;
- use crate::output::table::{Table, Options as TableOptions, Row as TableRow};
- use crate::output::tree::{TreeTrunk, TreeParams, TreeDepth};
- use crate::theme::Theme;
- /// 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, Eq, 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<TableOptions>,
- /// Whether to show a header line or not.
- pub header: bool,
- /// Whether to show each fileโs extended attributes.
- pub xattr: bool,
- /// Whether to show each file's security attribute.
- pub secattr: bool,
- }
- pub struct Render<'a> {
- pub dir: Option<&'a Dir>,
- pub files: Vec<File<'a>>,
- pub theme: &'a Theme,
- pub file_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<RecurseOptions>,
- /// 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,
- pub git: Option<&'a GitCache>,
- }
- struct Egg<'a> {
- table_row: Option<TableRow>,
- xattrs: &'a [Attribute],
- errors: Vec<(io::Error, Option<PathBuf>)>,
- dir: Option<Dir>,
- file: &'a File<'a>,
- }
- impl<'a> AsRef<File<'a>> for Egg<'a> {
- fn as_ref(&self) -> &File<'a> {
- self.file
- }
- }
- impl<'a> Render<'a> {
- pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<()> {
- let n_cpus = match num_cpus::get() as u32 {
- 0 => 1,
- n => n,
- };
- let mut pool = Pool::new(n_cpus);
- let mut rows = Vec::new();
- if let Some(ref table) = self.opts.table {
- match (self.git, self.dir) {
- (Some(g), Some(d)) => if ! g.has_anything_for(&d.path) { self.git = None },
- (Some(g), None) => if ! self.files.iter().any(|f| g.has_anything_for(&f.path)) { self.git = None },
- (None, _) => {/* Keep Git how it is */},
- }
- let mut table = Table::new(table, self.git, self.theme);
- 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, 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, TreeDepth::root());
- for row in self.iterate(rows) {
- writeln!(w, "{}", row.strings())?
- }
- }
- Ok(())
- }
- /// Whether to show the extended attribute hint
- pub fn show_xattr_hint(&self, file: &File<'_>) -> bool {
- // Do not show the hint '@' if the only extended attribute is the security
- // attribute and the security attribute column is active.
- let xattr_count = file.extended_attributes.len();
- let selinux_ctx_shown = self.opts.secattr && match file.security_context().context {
- SecurityContextType::SELinux(_) => true,
- SecurityContextType::None => false,
- };
- xattr_count > 1 || (xattr_count == 1 && !selinux_ctx_shown)
- }
- /// Adds files to the table, possibly recursively. This is easily
- /// parallelisable, and uses a pool of threads.
- fn add_files_to_table<'dir>(&self, pool: &mut Pool, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &[File<'dir>], depth: TreeDepth) {
- use std::sync::{Arc, Mutex};
- use crate::fs::feature::xattr;
- let mut file_eggs = (0..src.len()).map(|_| MaybeUninit::uninit()).collect::<Vec<_>>();
- 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();
- // 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 values, 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.
- let xattrs: &[Attribute] = if xattr::ENABLED && self.opts.xattr {
- &file.extended_attributes
- } else {
- &[]
- };
- let table_row = table.as_ref()
- .map(|t| t.row_for_file(file, self.show_xattr_hint(file)));
- 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 egg = Egg { table_row, xattrs, errors, dir, file };
- 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<Egg<'_>>>(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 file_name = self.file_style.for_file(egg.file, self.theme)
- .with_link_paths()
- .paint()
- .promote();
- let row = Row {
- tree: tree_params,
- cells: egg.table_row,
- name: file_name,
- };
- rows.push(row);
- if let Some(ref dir) = egg.dir {
- for file_to_add in dir.files(self.filter.dot_filter, self.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, 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.theme.ui.header, "Name"),
- }
- }
- fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row {
- use crate::output::file_name::Colours;
- let error_message = if let Some(path) = path {
- format!("<{}: {}>", path.display(), error)
- } else {
- 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.theme.broken_symlink(), error_message);
- Row { cells: None, name, tree }
- }
- fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
- let name = TextCell::paint(self.theme.ui.perms.attribute, format!("{}=\"{}\"", xattr.name, xattr.value));
- 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<Row>) -> TableIter<'a> {
- TableIter {
- tree_trunk: TreeTrunk::default(),
- total_width: table.widths().total(),
- table,
- inner: rows.into_iter(),
- tree_style: self.theme.ui.punctuation,
- }
- }
- pub fn iterate(&'a self, rows: Vec<Row>) -> Iter {
- Iter {
- tree_trunk: TreeTrunk::default(),
- inner: rows.into_iter(),
- tree_style: self.theme.ui.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<TableRow>,
- /// 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<Row>,
- 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::Item> {
- 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<Row>,
- }
- impl Iterator for Iter {
- type Item = TextCell;
- fn next(&mut self) -> Option<Self::Item> {
- 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
- })
- }
- }
|