exa/src/output/details.rs
Benjamin Sago 5b1966d261 Move filter and dir_action from options to fs
This commit moves the definitions of Filter and DirAction from the options module to the fs module, but leaves the parts that actually have to do with option parsing alone.

Now, the options module shouldn’t define any types that get used elsewhere in the program: it only adds functionality to types that already exist.
2017-07-24 08:34:50 +01:00

414 lines
15 KiB
Rust

//! 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::path::PathBuf;
use std::vec::IntoIter as VecIntoIter;
use fs::{Dir, File};
use fs::dir_action::RecurseOptions;
use fs::filter::FileFilter;
use fs::feature::xattr::{Attribute, FileAttributes};
use output::colours::Colours;
use output::cell::TextCell;
use output::tree::{TreeTrunk, TreeParams, TreeDepth};
use output::file_name::FileStyle;
use output::table::{Table, Options as TableOptions, Row as TableRow};
/// 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(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,
}
pub struct Render<'a> {
pub dir: Option<&'a Dir>,
pub files: Vec<File<'a>>,
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<RecurseOptions>,
/// How to sort and filter the files after getting their details.
pub filter: &'a FileFilter,
}
struct Egg<'a> {
table_row: Option<TableRow>,
xattrs: Vec<Attribute>,
errors: Vec<(IOError, 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>(self, w: &mut W) -> IOResult<()> {
let mut rows = Vec::new();
if let Some(ref table) = self.opts.table {
let mut table = Table::new(&table, self.dir, &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 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 None, &mut rows, &self.files, 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>(&self, table: &mut Option<Table<'a>>, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, depth: TreeDepth) {
use num_cpus;
use scoped_threadpool::Pool;
use std::sync::{Arc, Mutex};
use fs::feature::xattr;
let mut pool = Pool::new(num_cpus::get() as u32);
let mut file_eggs = Vec::new();
pool.scoped(|scoped| {
let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
let table = table.as_ref();
for file in src {
let file_eggs = file_eggs.clone();
scoped.execute(move || {
let mut errors = Vec::new();
let mut xattrs = Vec::new();
if xattr::ENABLED {
match file.path.attributes() {
Ok(xs) => xattrs.extend(xs),
Err(e) => errors.push((e, None)),
};
}
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(false) {
Ok(d) => { dir = Some(d); },
Err(e) => { errors.push((e, None)) },
}
}
};
let egg = Egg { table_row, xattrs, errors, dir, file };
file_eggs.lock().unwrap().push(egg);
});
}
});
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(ref row)) = (table.as_mut(), egg.table_row.as_ref()) {
t.add_widths(row);
}
let row = Row {
tree: tree_params,
cells: egg.table_row,
name: self.style.for_file(&egg.file, self.colours)
.with_link_paths()
.paint().promote(),
};
rows.push(row);
if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(self.filter.dot_filter) {
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(table, rows, &files, depth.deeper());
continue;
}
}
let count = egg.xattrs.len();
for (index, xattr) in egg.xattrs.into_iter().enumerate() {
rows.push(self.render_xattr(xattr, TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1)));
}
let count = errors.len();
for (index, (error, path)) in errors.into_iter().enumerate() {
rows.push(self.render_error(&error, TreeParams::new(depth.deeper(), index == count - 1), path));
}
}
}
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<PathBuf>) -> Row {
let error_message = match path {
Some(path) => format!("<{}: {}>", path.display(), error),
None => format!("<{}>", error),
};
let name = TextCell::paint(self.colours.broken_arrow, 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<Row>) -> TableIter<'a> {
TableIter {
tree_trunk: TreeTrunk::default(),
total_width: table.widths().total(),
table: table,
inner: rows.into_iter(),
colours: self.colours,
}
}
pub fn iterate(&'a self, rows: Vec<Row>) -> Iter<'a> {
Iter {
tree_trunk: TreeTrunk::default(),
inner: rows.into_iter(),
colours: self.colours,
}
}
}
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> {
table: Table<'a>,
tree_trunk: TreeTrunk,
total_width: usize,
colours: &'a Colours,
inner: VecIntoIter<Row>,
}
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.colours.punctuation.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<'a> {
tree_trunk: TreeTrunk,
colours: &'a Colours,
inner: VecIntoIter<Row>,
}
impl<'a> Iterator for Iter<'a> {
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.colours.punctuation.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
})
}
}