Extract table from details and grid_details

This commit extracts the common table element from the details and grid_details modules, and makes it its own reusable thing.

- A Table no longer holds the values it’s rendering; it just holds a continually-updated version of the maximum widths for each column. This means that all of the resulting values that turn into Rows — which here are either files, or file eggs — need to be stored *somewhere*, and that somewhere is a secondary vector that gets passed around and modified alongside the Table.
- Likewise, all the mutable methods that were on Table that added a Row now *return* the row that would have been added, hoping that the row does get stored somewhere. (It does, don’t worry.)
- Because rendering with mock users is tested in the user-field-rendering module, we don’t need to bother threading different types of U through the Environment, so now it’s just been specialised to UsersCache.
- Accidentally speed up printing a table by not buffering its entire output first when not necessary.
This commit is contained in:
Benjamin Sago 2017-07-02 01:02:17 +01:00
parent 22c6fb048f
commit fc60838ff3
5 changed files with 371 additions and 341 deletions

View File

@ -174,7 +174,7 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
Mode::Lines => lines::Render { files, colours, classify }.render(self.writer), Mode::Lines => lines::Render { files, colours, classify }.render(self.writer),
Mode::Grid(ref opts) => grid::Render { files, colours, classify, opts }.render(self.writer), Mode::Grid(ref opts) => grid::Render { files, colours, classify, opts }.render(self.writer),
Mode::Details(ref opts) => details::Render { dir, files, colours, classify, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.writer), Mode::Details(ref opts) => details::Render { dir, files, colours, classify, opts, filter: &self.options.filter, recurse: self.options.dir_action.recurse_options() }.render(self.writer),
Mode::GridDetails(ref grid, ref details) => grid_details::Render { dir, files, colours, classify, grid, details }.render(self.writer), Mode::GridDetails(ref grid, ref details) => grid_details::Render { dir, files, colours, classify, grid, details, filter: &self.options.filter }.render(self.writer),
} }
} }
else { else {

View File

@ -58,47 +58,22 @@
//! Each column in the table needs to be resized to fit its widest argument. This //! 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 //! 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. //! can be displayed, in order to make sure that every column is wide enough.
//!
//!
//! ## Extended Attributes and Errors
//!
//! Finally, files' extended attributes and any errors that occur while statting
//! them can also be displayed as their children. It looks like this:
//!
//! ```text
//! .rw-r--r-- 0 ben 3 Sep 13:26 forbidden
//! └── <Permission denied (os error 13)>
//! .rw-r--r--@ 0 ben 3 Sep 13:26 file_with_xattrs
//! ├── another_greeting (len 2)
//! └── greeting (len 5)
//! ```
//!
//! These lines also have `None` cells, and the error string or attribute details
//! are used in place of the filename.
use std::io::{Write, Error as IOError, Result as IOResult}; use std::io::{Write, Error as IOError, Result as IOResult};
use std::ops::Add; use std::ops::Add;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard}; use std::vec::IntoIter as VecIntoIter;
use datetime::fmt::DateFormat; use fs::{Dir, File};
use datetime::{LocalDateTime, DatePiece};
use datetime::TimeZone;
use zoneinfo_compiled::{CompiledData, Result as TZResult};
use locale;
use users::{Users, Groups, UsersCache};
use fs::{Dir, File, fields as f};
use fs::feature::xattr::{Attribute, FileAttributes}; use fs::feature::xattr::{Attribute, FileAttributes};
use options::{FileFilter, RecurseOptions}; use options::{FileFilter, RecurseOptions};
use output::colours::Colours; use output::colours::Colours;
use output::column::{Alignment, Column, Columns}; use output::column::Columns;
use output::cell::{TextCell, TextCellContents}; use output::cell::TextCell;
use output::tree::TreeTrunk; use output::tree::TreeTrunk;
use output::file_name::{FileName, LinkStyle, Classify}; use output::file_name::{FileName, LinkStyle, Classify};
use output::table::{Table, Environment, Row as TableRow};
/// With the **Details** view, the output gets formatted into columns, with /// With the **Details** view, the output gets formatted into columns, with
@ -127,87 +102,6 @@ pub struct Options {
pub xattr: bool, pub xattr: bool,
} }
/// The **environment** struct contains any data that could change between
/// running instances of exa, depending on the user's computer's configuration.
///
/// Any environment field should be able to be mocked up for test runs.
pub struct Environment<U> { // where U: Users+Groups
/// The year of the current time. This gets used to determine which date
/// format to use.
current_year: i64,
/// Localisation rules for formatting numbers.
numeric: locale::Numeric,
/// Localisation rules for formatting timestamps.
time: locale::Time,
/// Date format for printing out timestamps that are in the current year.
date_and_time: DateFormat<'static>,
/// Date format for printing out timestamps that *arent*.
date_and_year: DateFormat<'static>,
/// The computer's current time zone. This gets used to determine how to
/// offset files' timestamps.
tz: Option<TimeZone>,
/// Mapping cache of user IDs to usernames.
users: Mutex<U>,
}
impl<U> Environment<U> {
pub fn lock_users(&self) -> MutexGuard<U> {
self.users.lock().unwrap()
}
}
impl Default for Environment<UsersCache> {
fn default() -> Self {
use unicode_width::UnicodeWidthStr;
let tz = determine_time_zone();
if let Err(ref e) = tz {
println!("Unable to determine time zone: {}", e);
}
let numeric = locale::Numeric::load_user_locale()
.unwrap_or_else(|_| locale::Numeric::english());
let time = locale::Time::load_user_locale()
.unwrap_or_else(|_| locale::Time::english());
// Some locales use a three-character wide month name (Jan to Dec);
// others vary between three and four (1月 to 12月). We assume that
// December is the month with the maximum width, and use the width of
// that to determine how to pad the other months.
let december_width = UnicodeWidthStr::width(&*time.short_month_name(11));
let date_and_time = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
_ => DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap(),
};
let date_and_year = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {5>:Y}").unwrap(),
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
};
Environment {
current_year: LocalDateTime::now().year(),
numeric: numeric,
date_and_time: date_and_time,
date_and_year: date_and_year,
time: time,
tz: tz.ok(),
users: Mutex::new(UsersCache::new()),
}
}
}
fn determine_time_zone() -> TZResult<TimeZone> {
TimeZone::from_file("/etc/localtime")
}
pub struct Render<'a> { pub struct Render<'a> {
@ -226,36 +120,42 @@ pub struct Render<'a> {
pub filter: &'a FileFilter, pub filter: &'a FileFilter,
} }
impl<'a> Render<'a> {
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
// First, transform the Columns object into a vector of columns for struct Egg<'a> {
// the current directory. table_row: 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 columns_for_dir = match self.opts.columns { let columns_for_dir = match self.opts.columns {
Some(cols) => cols.for_dir(self.dir), Some(cols) => cols.for_dir(self.dir),
None => Vec::new(), None => Vec::new(),
}; };
// Then, retrieve various environment variables. let env = Environment::default();
let env = Arc::new(Environment::<UsersCache>::default()); let mut table = Table::new(&columns_for_dir, &self.colours, &env);
let mut rows = Vec::new();
// Build the table to put rows in. if self.opts.header {
let mut table = Table { let header = table.header_row();
columns: &*columns_for_dir, rows.push(self.render_header(header));
colours: self.colours, }
classify: self.classify,
xattr: self.opts.xattr,
env: env,
rows: Vec::new(),
};
// Next, add a header if the user requests it. self.add_files_to_table(&mut table, &mut rows, &self.files, 0);
if self.opts.header { table.add_header() }
// Then add files to the table and print it out. for row in self.iterate(&table, rows) {
self.add_files_to_table(&mut table, &self.files, 0); writeln!(w, "{}", row.strings())?
for cell in table.print_table() {
writeln!(w, "{}", cell.strings())?;
} }
Ok(()) Ok(())
@ -263,7 +163,7 @@ impl<'a> Render<'a> {
/// Adds files to the table, possibly recursively. This is easily /// Adds files to the table, possibly recursively. This is easily
/// parallelisable, and uses a pool of threads. /// parallelisable, and uses a pool of threads.
fn add_files_to_table<'dir, U: Users+Groups+Send>(&self, mut table: &mut Table<U>, src: &Vec<File<'dir>>, depth: usize) { fn add_files_to_table<'dir>(&self, mut table: &mut Table, rows: &mut Vec<Row>, src: &Vec<File<'dir>>, depth: usize) {
use num_cpus; use num_cpus;
use scoped_threadpool::Pool; use scoped_threadpool::Pool;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -272,23 +172,9 @@ impl<'a> Render<'a> {
let mut pool = Pool::new(num_cpus::get() as u32); let mut pool = Pool::new(num_cpus::get() as u32);
let mut file_eggs = Vec::new(); let mut file_eggs = Vec::new();
struct Egg<'a> {
cells: Vec<TextCell>,
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
}
}
pool.scoped(|scoped| { pool.scoped(|scoped| {
let file_eggs = Arc::new(Mutex::new(&mut file_eggs)); let file_eggs = Arc::new(Mutex::new(&mut file_eggs));
let table = Arc::new(&mut table); let table = Arc::new(Mutex::new(&mut table));
for file in src { for file in src {
let file_eggs = file_eggs.clone(); let file_eggs = file_eggs.clone();
@ -305,9 +191,9 @@ impl<'a> Render<'a> {
}; };
} }
let cells = table.cells_for_file(&file, !xattrs.is_empty()); let table_row = table.lock().unwrap().row_for_file(&file, !xattrs.is_empty());
if !table.xattr { if !self.opts.xattr {
xattrs.clear(); xattrs.clear();
} }
@ -321,7 +207,7 @@ impl<'a> Render<'a> {
} }
}; };
let egg = Egg { cells, xattrs, errors, dir, file }; let egg = Egg { table_row, xattrs, errors, dir, file };
file_eggs.lock().unwrap().push(egg); file_eggs.lock().unwrap().push(egg);
}); });
} }
@ -336,12 +222,12 @@ impl<'a> Render<'a> {
let row = Row { let row = Row {
depth: depth, depth: depth,
cells: Some(egg.cells), cells: Some(egg.table_row),
name: FileName::new(&egg.file, LinkStyle::FullLinkPaths, table.classify, table.colours).paint().promote(), name: FileName::new(&egg.file, LinkStyle::FullLinkPaths, self.classify, self.colours).paint().promote(),
last: index == num_eggs - 1, last: index == num_eggs - 1,
}; };
table.rows.push(row); rows.push(row);
if let Some(ref dir) = egg.dir { if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(self.filter.dot_filter) { for file_to_add in dir.files(self.filter.dot_filter) {
@ -355,207 +241,109 @@ impl<'a> Render<'a> {
if !files.is_empty() { if !files.is_empty() {
for xattr in egg.xattrs { for xattr in egg.xattrs {
table.add_xattr(xattr, depth + 1, false); rows.push(self.render_xattr(xattr, depth + 1, false));
} }
for (error, path) in errors { for (error, path) in errors {
table.add_error(&error, depth + 1, false, path); rows.push(self.render_error(&error, depth + 1, false, path));
} }
self.add_files_to_table(table, &files, depth + 1); self.add_files_to_table(table, rows, &files, depth + 1);
continue; continue;
} }
} }
let count = egg.xattrs.len(); let count = egg.xattrs.len();
for (index, xattr) in egg.xattrs.into_iter().enumerate() { for (index, xattr) in egg.xattrs.into_iter().enumerate() {
table.add_xattr(xattr, depth + 1, errors.is_empty() && index == count - 1); rows.push(self.render_xattr(xattr, depth + 1, errors.is_empty() && index == count - 1));
} }
let count = errors.len(); let count = errors.len();
for (index, (error, path)) in errors.into_iter().enumerate() { for (index, (error, path)) in errors.into_iter().enumerate() {
table.add_error(&error, depth + 1, index == count - 1, path); rows.push(self.render_error(&error, depth + 1, index == count - 1, path));
} }
} }
} }
}
pub fn render_header(&self, header: TableRow) -> Row {
pub struct Row { 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.
cells: Option<Vec<TextCell>>,
/// This file's name, in coloured output. The name is treated separately
/// from the other cells, as it never requires padding.
name: TextCell,
/// How many directories deep into the tree structure this is. Directories
/// on top have depth 0.
depth: usize,
/// Whether this is the last entry in the directory. This flag is used
/// when calculating the tree view.
last: bool,
}
impl Row {
/// Gets the Unicode display width of the indexed column, if present. If
/// not, returns 0.
fn column_width(&self, index: usize) -> usize {
match self.cells {
Some(ref cells) => *cells[index].width,
None => 0,
}
}
}
/// A **Table** object gets built up by the view as it lists files and
/// directories.
pub struct Table<'a, U: 'a> { // where U: Users+Groups
pub rows: Vec<Row>,
pub columns: &'a [Column],
pub colours: &'a Colours,
pub xattr: bool,
pub classify: Classify,
pub env: Arc<Environment<U>>,
}
impl<'a, U: Users+Groups+'a> Table<'a, U> {
/// Add a dummy "header" row to the table, which contains the names of all
/// the columns, underlined. This has dummy data for the cases that aren't
/// actually used, such as the depth or list of attributes.
pub fn add_header(&mut self) {
let row = Row {
depth: 0, depth: 0,
cells: Some(self.columns.iter().map(|c| TextCell::paint_str(self.colours.header, c.header())).collect()), cells: Some(header),
name: TextCell::paint_str(self.colours.header, "Name"), name: TextCell::paint_str(self.colours.header, "Name"),
last: false, last: false,
}; }
self.rows.push(row);
} }
fn add_error(&mut self, error: &IOError, depth: usize, last: bool, path: Option<PathBuf>) { fn render_error(&self, error: &IOError, depth: usize, last: bool, path: Option<PathBuf>) -> Row {
let error_message = match path { let error_message = match path {
Some(path) => format!("<{}: {}>", path.display(), error), Some(path) => format!("<{}: {}>", path.display(), error),
None => format!("<{}>", error), None => format!("<{}>", error),
}; };
let row = Row { Row {
depth: depth, depth: depth,
cells: None, cells: None,
name: TextCell::paint(self.colours.broken_arrow, error_message), name: TextCell::paint(self.colours.broken_arrow, error_message),
last: last, last: last,
}; }
self.rows.push(row);
} }
fn add_xattr(&mut self, xattr: Attribute, depth: usize, last: bool) { fn render_xattr(&self, xattr: Attribute, depth: usize, last: bool) -> Row {
let row = Row { Row {
depth: depth, depth: depth,
cells: None, cells: None,
name: TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)), name: TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)),
last: last, last: last,
}; }
self.rows.push(row);
} }
pub fn filename(&self, file: &File, links: LinkStyle) -> TextCellContents { pub fn render_file(&self, cells: TableRow, name_cell: TextCell, depth: usize, last: bool) -> Row {
FileName::new(file, links, self.classify, &self.colours).paint() Row {
}
pub fn add_file_with_cells(&mut self, cells: Vec<TextCell>, name_cell: TextCell, depth: usize, last: bool) {
let row = Row {
depth: depth, depth: depth,
cells: Some(cells), cells: Some(cells),
name: name_cell, name: name_cell,
last: last, last: last,
};
self.rows.push(row);
}
/// Use the list of columns to find which cells should be produced for
/// this file, per-column.
pub fn cells_for_file(&self, file: &File, xattrs: bool) -> Vec<TextCell> {
self.columns.iter()
.map(|c| self.display(file, c, xattrs))
.collect()
}
fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
f::PermissionsPlus {
file_type: file.type_char(),
permissions: file.permissions(),
xattrs: xattrs,
}
}
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
use output::column::TimeType::*;
match *column {
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours),
Column::FileSize(fmt) => file.size().render(&self.colours, fmt, &self.env.numeric),
Column::Timestamp(Modified) => file.modified_time().render(&self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::HardLinks => file.links().render(&self.colours, &self.env.numeric),
Column::Inode => file.inode().render(&self.colours),
Column::Blocks => file.blocks().render(&self.colours),
Column::User => file.user().render(&self.colours, &*self.env.lock_users()),
Column::Group => file.group().render(&self.colours, &*self.env.lock_users()),
Column::GitStatus => file.git_status().render(&self.colours),
} }
} }
/// Render the table as a vector of Cells, to be displayed on standard output. /// Render the table as a vector of Cells, to be displayed on standard output.
pub fn print_table(self) -> Vec<TextCell> { pub fn iterate(&self, table: &'a Table<'a>, rows: Vec<Row>) -> Iter<'a> {
let mut tree_trunk = TreeTrunk::default(); Iter {
let mut cells = Vec::new(); tree_trunk: TreeTrunk::default(),
total_width: table.columns_count() + table.widths().iter().fold(0, Add::add),
table: table,
inner: rows.into_iter(),
colours: self.colours,
}
}
}
// Work out the list of column widths by finding the longest cell for pub struct Iter<'a> {
// each column, then formatting each cell in that column to be the table: &'a Table<'a>,
// width of that one. tree_trunk: TreeTrunk,
let column_widths: Vec<usize> = (0 .. self.columns.len()) total_width: usize,
.map(|n| self.rows.iter().map(|row| row.column_width(n)).max().unwrap_or(0)) colours: &'a Colours,
.collect(); inner: VecIntoIter<Row>,
}
let total_width: usize = self.columns.len() + column_widths.iter().fold(0, Add::add); impl<'a> Iterator for Iter<'a> {
type Item = TextCell;
for row in self.rows { fn next(&mut self) -> Option<Self::Item> {
let mut cell = TextCell::default(); self.inner.next().map(|row| {
let mut cell =
if let Some(cells) = row.cells { if let Some(cells) = row.cells {
for (n, (this_cell, width)) in cells.into_iter().zip(column_widths.iter()).enumerate() { self.table.render(cells)
let padding = width - *this_cell.width;
match self.columns[n].alignment() {
Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); }
Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); }
}
cell.add_spaces(1);
} }
} else {
else { let mut cell = TextCell::default();
cell.add_spaces(total_width) cell.add_spaces(self.total_width);
} cell
};
let mut filename = TextCell::default(); let mut filename = TextCell::default();
for tree_part in tree_trunk.new_row(row.depth, row.last) { for tree_part in self.tree_trunk.new_row(row.depth, row.last) {
filename.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4); filename.push(self.colours.punctuation.paint(tree_part.ascii_art()), 4);
} }
@ -569,9 +357,31 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
filename.append(row.name); filename.append(row.name);
cell.append(filename); cell.append(filename);
cells.push(cell);
}
cells cell
})
} }
} }
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,
/// How many directories deep into the tree structure this is. Directories
/// on top have depth 0.
pub depth: usize,
/// Whether this is the last entry in the directory. This flag is used
/// when calculating the tree view.
pub last: bool,
}

View File

@ -1,19 +1,19 @@
use std::io::{Write, Result as IOResult}; use std::io::{Write, Result as IOResult};
use std::sync::Arc;
use ansi_term::ANSIStrings; use ansi_term::ANSIStrings;
use users::UsersCache;
use term_grid as grid; use term_grid as grid;
use fs::{Dir, File}; use fs::{Dir, File};
use fs::feature::xattr::FileAttributes; use fs::feature::xattr::FileAttributes;
use options::FileFilter;
use output::cell::TextCell; use output::cell::TextCell;
use output::column::Column; use output::column::Column;
use output::colours::Colours; use output::colours::Colours;
use output::details::{Table, Environment, Options as DetailsOptions}; use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender};
use output::grid::Options as GridOptions; use output::grid::Options as GridOptions;
use output::file_name::{Classify, LinkStyle}; use output::file_name::{FileName, LinkStyle, Classify};
use output::table::{Table, Environment, Row as TableRow};
pub struct Render<'a> { pub struct Render<'a> {
@ -23,9 +23,22 @@ pub struct Render<'a> {
pub classify: Classify, pub classify: Classify,
pub grid: &'a GridOptions, pub grid: &'a GridOptions,
pub details: &'a DetailsOptions, pub details: &'a DetailsOptions,
pub filter: &'a FileFilter,
} }
impl<'a> Render<'a> { impl<'a> Render<'a> {
pub fn details(&self) -> DetailsRender<'a> {
DetailsRender {
dir: self.dir.clone(),
files: Vec::new(),
colours: self.colours,
classify: self.classify,
opts: self.details,
recurse: None,
filter: self.filter,
}
}
pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> { pub fn render<W: Write>(&self, w: &mut W) -> IOResult<()> {
let columns_for_dir = match self.details.columns { let columns_for_dir = match self.details.columns {
@ -33,27 +46,24 @@ impl<'a> Render<'a> {
None => Vec::new(), None => Vec::new(),
}; };
let env = Arc::new(Environment::default()); let env = Environment::default();
let (cells, file_names) = { let drender = self.clone().details();
let first_table = self.make_table(env.clone(), &*columns_for_dir, self.colours, self.classify); let (mut first_table, _) = self.make_table(&env, &columns_for_dir, &drender);
let cells = self.files.iter() let rows = self.files.iter()
.map(|file| first_table.cells_for_file(file, file_has_xattrs(file))) .map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
.collect::<Vec<_>>(); .collect::<Vec<TableRow>>();
let file_names = self.files.iter() let file_names = self.files.iter()
.map(|file| first_table.filename(file, LinkStyle::JustFilenames).promote()) .map(|file| FileName::new(file, LinkStyle::JustFilenames, self.classify, self.colours).paint().promote())
.collect::<Vec<_>>(); .collect::<Vec<TextCell>>();
(cells, file_names) let mut last_working_table = self.make_grid(&env, 1, &columns_for_dir, &file_names, rows.clone(), &drender);
};
let mut last_working_table = self.make_grid(env.clone(), 1, &columns_for_dir, &file_names, cells.clone(), self.colours, self.classify);
for column_count in 2.. { for column_count in 2.. {
let grid = self.make_grid(env.clone(), column_count, &columns_for_dir, &file_names, cells.clone(), self.colours, self.classify); let grid = self.make_grid(&env, column_count, &columns_for_dir, &file_names, rows.clone(), &drender);
let the_grid_fits = { let the_grid_fits = {
let d = grid.fit_into_columns(column_count); let d = grid.fit_into_columns(column_count);
@ -71,33 +81,33 @@ impl<'a> Render<'a> {
Ok(()) Ok(())
} }
fn make_table<'g>(&'g self, env: Arc<Environment<UsersCache>>, columns_for_dir: &'g [Column], colours: &'g Colours, classify: Classify) -> Table<UsersCache> { fn make_table<'t>(&'a self, env: &'a Environment, columns_for_dir: &'a [Column], drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
let mut table = Table { let mut table = Table::new(columns_for_dir, self.colours, env);
columns: columns_for_dir, let mut rows = Vec::new();
colours, classify, env,
xattr: self.details.xattr,
rows: Vec::new(),
};
if self.details.header { table.add_header() } if self.details.header {
table rows.push(drender.render_header(table.header_row()));
}
fn make_grid<'g>(&'g self, env: Arc<Environment<UsersCache>>, column_count: usize, columns_for_dir: &'g [Column], file_names: &[TextCell], cells: Vec<Vec<TextCell>>, colours: &'g Colours, classify: Classify) -> grid::Grid {
let mut tables = Vec::new();
for _ in 0 .. column_count {
tables.push(self.make_table(env.clone(), columns_for_dir, colours, classify));
} }
let mut num_cells = cells.len(); (table, rows)
}
fn make_grid(&'a self, env: &'a Environment, column_count: usize, columns_for_dir: &'a [Column], file_names: &[TextCell], rows: Vec<TableRow>, drender: &DetailsRender) -> grid::Grid {
let mut tables = Vec::new();
for _ in 0 .. column_count {
tables.push(self.make_table(env.clone(), columns_for_dir, drender));
}
let mut num_cells = rows.len();
if self.details.header { if self.details.header {
num_cells += column_count; num_cells += column_count;
} }
let original_height = divide_rounding_up(cells.len(), column_count); let original_height = divide_rounding_up(rows.len(), column_count);
let height = divide_rounding_up(num_cells, column_count); let height = divide_rounding_up(num_cells, column_count);
for (i, (file_name, row)) in file_names.iter().zip(cells.into_iter()).enumerate() { for (i, (file_name, row)) in file_names.iter().zip(rows.into_iter()).enumerate() {
let index = if self.grid.across { let index = if self.grid.across {
i % column_count i % column_count
} }
@ -105,10 +115,15 @@ impl<'a> Render<'a> {
i / original_height i / original_height
}; };
tables[index].add_file_with_cells(row, file_name.clone(), 0, false); let (ref mut table, ref mut rows) = tables[index];
table.add_widths(&row);
let details_row = drender.render_file(row, file_name.clone(), 0, false);
rows.push(details_row);
} }
let columns: Vec<_> = tables.into_iter().map(|t| t.print_table()).collect(); let columns: Vec<_> = tables.into_iter().map(|(table, details_rows)| {
drender.iterate(&table, details_rows).collect::<Vec<_>>()
}).collect();
let direction = if self.grid.across { grid::Direction::LeftToRight } let direction = if self.grid.across { grid::Direction::LeftToRight }
else { grid::Direction::TopToBottom }; else { grid::Direction::TopToBottom };

View File

@ -14,3 +14,4 @@ mod colours;
mod escape; mod escape;
mod render; mod render;
mod tree; mod tree;
mod table;

204
src/output/table.rs Normal file
View File

@ -0,0 +1,204 @@
use std::cmp::max;
use std::sync::{Mutex, MutexGuard};
use datetime::fmt::DateFormat;
use datetime::{LocalDateTime, DatePiece};
use datetime::TimeZone;
use zoneinfo_compiled::{CompiledData, Result as TZResult};
use locale;
use users::UsersCache;
use output::cell::TextCell;
use output::colours::Colours;
use output::column::{Alignment, Column};
use fs::{File, fields as f};
/// The **environment** struct contains any data that could change between
/// running instances of exa, depending on the user's computer's configuration.
///
/// Any environment field should be able to be mocked up for test runs.
pub struct Environment {
/// The year of the current time. This gets used to determine which date
/// format to use.
current_year: i64,
/// Localisation rules for formatting numbers.
numeric: locale::Numeric,
/// Localisation rules for formatting timestamps.
time: locale::Time,
/// Date format for printing out timestamps that are in the current year.
date_and_time: DateFormat<'static>,
/// Date format for printing out timestamps that *arent*.
date_and_year: DateFormat<'static>,
/// The computer's current time zone. This gets used to determine how to
/// offset files' timestamps.
tz: Option<TimeZone>,
/// Mapping cache of user IDs to usernames.
users: Mutex<UsersCache>,
}
impl Environment {
pub fn lock_users(&self) -> MutexGuard<UsersCache> {
self.users.lock().unwrap()
}
}
impl Default for Environment {
fn default() -> Self {
use unicode_width::UnicodeWidthStr;
let tz = determine_time_zone();
if let Err(ref e) = tz {
println!("Unable to determine time zone: {}", e);
}
let numeric = locale::Numeric::load_user_locale()
.unwrap_or_else(|_| locale::Numeric::english());
let time = locale::Time::load_user_locale()
.unwrap_or_else(|_| locale::Time::english());
// Some locales use a three-character wide month name (Jan to Dec);
// others vary between three and four (1月 to 12月). We assume that
// December is the month with the maximum width, and use the width of
// that to determine how to pad the other months.
let december_width = UnicodeWidthStr::width(&*time.short_month_name(11));
let date_and_time = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {2>:h}:{02>:m}").unwrap(),
_ => DateFormat::parse("{2>:D} {:M} {2>:h}:{02>:m}").unwrap(),
};
let date_and_year = match december_width {
4 => DateFormat::parse("{2>:D} {4>:M} {5>:Y}").unwrap(),
_ => DateFormat::parse("{2>:D} {:M} {5>:Y}").unwrap()
};
Environment {
current_year: LocalDateTime::now().year(),
numeric: numeric,
date_and_time: date_and_time,
date_and_year: date_and_year,
time: time,
tz: tz.ok(),
users: Mutex::new(UsersCache::new()),
}
}
}
fn determine_time_zone() -> TZResult<TimeZone> {
TimeZone::from_file("/etc/localtime")
}
pub struct Table<'a> {
columns: &'a [Column],
colours: &'a Colours,
env: &'a Environment,
widths: Vec<usize>,
}
#[derive(Clone)]
pub struct Row {
cells: Vec<TextCell>,
}
impl<'a, 'f> Table<'a> {
pub fn new(columns: &'a [Column], colours: &'a Colours, env: &'a Environment) -> Table<'a> {
let widths = vec![ 0; columns.len() ];
Table { columns, colours, env, widths }
}
pub fn columns_count(&self) -> usize {
self.columns.len()
}
pub fn widths(&self) -> &[usize] {
&self.widths
}
pub fn header_row(&mut self) -> Row {
let mut cells = Vec::with_capacity(self.columns.len());
for (old_width, column) in self.widths.iter_mut().zip(self.columns.iter()) {
let column = TextCell::paint_str(self.colours.header, column.header());
*old_width = max(*old_width, *column.width);
cells.push(column);
}
Row { cells }
}
pub fn row_for_file(&mut self, file: &File, xattrs: bool) -> Row {
let mut cells = Vec::with_capacity(self.columns.len());
let other = self.columns.iter().map(|c| self.display(file, c, xattrs)).collect::<Vec<_>>();
for (old_width, column) in self.widths.iter_mut().zip(other.into_iter()) {
*old_width = max(*old_width, *column.width);
cells.push(column);
}
Row { cells }
}
pub fn add_widths(&mut self, row: &Row) {
for (old_width, cell) in self.widths.iter_mut().zip(row.cells.iter()) {
*old_width = max(*old_width, *cell.width);
}
}
fn permissions_plus(&self, file: &File, xattrs: bool) -> f::PermissionsPlus {
f::PermissionsPlus {
file_type: file.type_char(),
permissions: file.permissions(),
xattrs: xattrs,
}
}
fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell {
use output::column::TimeType::*;
match *column {
Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours),
Column::FileSize(fmt) => file.size().render(&self.colours, fmt, &self.env.numeric),
Column::Timestamp(Modified) => file.modified_time().render(&self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.env.date_and_time, &self.env.date_and_year, &self.env.time, self.env.current_year),
Column::HardLinks => file.links().render(&self.colours, &self.env.numeric),
Column::Inode => file.inode().render(&self.colours),
Column::Blocks => file.blocks().render(&self.colours),
Column::User => file.user().render(&self.colours, &*self.env.lock_users()),
Column::Group => file.group().render(&self.colours, &*self.env.lock_users()),
Column::GitStatus => file.git_status().render(&self.colours),
}
}
pub fn render(&self, row: Row) -> TextCell {
let mut cell = TextCell::default();
for (n, (this_cell, width)) in row.cells.into_iter().zip(self.widths.iter()).enumerate() {
let padding = width - *this_cell.width;
match self.columns[n].alignment() {
Alignment::Left => { cell.append(this_cell); cell.add_spaces(padding); }
Alignment::Right => { cell.add_spaces(padding); cell.append(this_cell); }
}
cell.add_spaces(1);
}
cell
}
}