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::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::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 {

View File

@ -58,47 +58,22 @@
//! 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.
//!
//!
//! ## 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::ops::Add;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard};
use std::vec::IntoIter as VecIntoIter;
use datetime::fmt::DateFormat;
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::{Dir, File};
use fs::feature::xattr::{Attribute, FileAttributes};
use options::{FileFilter, RecurseOptions};
use output::colours::Colours;
use output::column::{Alignment, Column, Columns};
use output::cell::{TextCell, TextCellContents};
use output::column::Columns;
use output::cell::TextCell;
use output::tree::TreeTrunk;
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
@ -127,87 +102,6 @@ pub struct Options {
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> {
@ -226,36 +120,42 @@ pub struct Render<'a> {
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
// the current directory.
struct Egg<'a> {
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 {
Some(cols) => cols.for_dir(self.dir),
None => Vec::new(),
};
// Then, retrieve various environment variables.
let env = Arc::new(Environment::<UsersCache>::default());
let env = Environment::default();
let mut table = Table::new(&columns_for_dir, &self.colours, &env);
let mut rows = Vec::new();
// Build the table to put rows in.
let mut table = Table {
columns: &*columns_for_dir,
colours: self.colours,
classify: self.classify,
xattr: self.opts.xattr,
env: env,
rows: Vec::new(),
};
if self.opts.header {
let header = table.header_row();
rows.push(self.render_header(header));
}
// Next, add a header if the user requests it.
if self.opts.header { table.add_header() }
self.add_files_to_table(&mut table, &mut rows, &self.files, 0);
// Then add files to the table and print it out.
self.add_files_to_table(&mut table, &self.files, 0);
for cell in table.print_table() {
writeln!(w, "{}", cell.strings())?;
for row in self.iterate(&table, rows) {
writeln!(w, "{}", row.strings())?
}
Ok(())
@ -263,7 +163,7 @@ impl<'a> Render<'a> {
/// Adds files to the table, possibly recursively. This is easily
/// 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 scoped_threadpool::Pool;
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 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| {
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 {
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();
}
@ -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);
});
}
@ -336,12 +222,12 @@ impl<'a> Render<'a> {
let row = Row {
depth: depth,
cells: Some(egg.cells),
name: FileName::new(&egg.file, LinkStyle::FullLinkPaths, table.classify, table.colours).paint().promote(),
cells: Some(egg.table_row),
name: FileName::new(&egg.file, LinkStyle::FullLinkPaths, self.classify, self.colours).paint().promote(),
last: index == num_eggs - 1,
};
table.rows.push(row);
rows.push(row);
if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(self.filter.dot_filter) {
@ -355,207 +241,109 @@ impl<'a> Render<'a> {
if !files.is_empty() {
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 {
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;
}
}
let count = egg.xattrs.len();
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();
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 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.
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 {
pub fn render_header(&self, header: TableRow) -> Row {
Row {
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"),
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 {
Some(path) => format!("<{}: {}>", path.display(), error),
None => format!("<{}>", error),
};
let row = Row {
Row {
depth: depth,
cells: None,
name: TextCell::paint(self.colours.broken_arrow, error_message),
last: last,
};
self.rows.push(row);
}
}
fn add_xattr(&mut self, xattr: Attribute, depth: usize, last: bool) {
let row = Row {
fn render_xattr(&self, xattr: Attribute, depth: usize, last: bool) -> Row {
Row {
depth: depth,
cells: None,
name: TextCell::paint(self.colours.perms.attribute, format!("{} (len {})", xattr.name, xattr.size)),
last: last,
};
self.rows.push(row);
}
}
pub fn filename(&self, file: &File, links: LinkStyle) -> TextCellContents {
FileName::new(file, links, self.classify, &self.colours).paint()
}
pub fn add_file_with_cells(&mut self, cells: Vec<TextCell>, name_cell: TextCell, depth: usize, last: bool) {
let row = Row {
pub fn render_file(&self, cells: TableRow, name_cell: TextCell, depth: usize, last: bool) -> Row {
Row {
depth: depth,
cells: Some(cells),
name: name_cell,
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.
pub fn print_table(self) -> Vec<TextCell> {
let mut tree_trunk = TreeTrunk::default();
let mut cells = Vec::new();
pub fn iterate(&self, table: &'a Table<'a>, rows: Vec<Row>) -> Iter<'a> {
Iter {
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
// each column, then formatting each cell in that column to be the
// width of that one.
let column_widths: Vec<usize> = (0 .. self.columns.len())
.map(|n| self.rows.iter().map(|row| row.column_width(n)).max().unwrap_or(0))
.collect();
pub struct Iter<'a> {
table: &'a Table<'a>,
tree_trunk: TreeTrunk,
total_width: usize,
colours: &'a Colours,
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 {
let mut cell = TextCell::default();
if let Some(cells) = row.cells {
for (n, (this_cell, width)) in cells.into_iter().zip(column_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);
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 {
cell.add_spaces(total_width)
}
else {
let mut cell = TextCell::default();
cell.add_spaces(self.total_width);
cell
};
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);
}
@ -569,9 +357,31 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
filename.append(row.name);
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::sync::Arc;
use ansi_term::ANSIStrings;
use users::UsersCache;
use term_grid as grid;
use fs::{Dir, File};
use fs::feature::xattr::FileAttributes;
use options::FileFilter;
use output::cell::TextCell;
use output::column::Column;
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::file_name::{Classify, LinkStyle};
use output::file_name::{FileName, LinkStyle, Classify};
use output::table::{Table, Environment, Row as TableRow};
pub struct Render<'a> {
@ -23,9 +23,22 @@ pub struct Render<'a> {
pub classify: Classify,
pub grid: &'a GridOptions,
pub details: &'a DetailsOptions,
pub filter: &'a FileFilter,
}
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<()> {
let columns_for_dir = match self.details.columns {
@ -33,27 +46,24 @@ impl<'a> Render<'a> {
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()
.map(|file| first_table.cells_for_file(file, file_has_xattrs(file)))
.collect::<Vec<_>>();
let rows = self.files.iter()
.map(|file| first_table.row_for_file(file, file_has_xattrs(file)))
.collect::<Vec<TableRow>>();
let file_names = self.files.iter()
.map(|file| first_table.filename(file, LinkStyle::JustFilenames).promote())
.collect::<Vec<_>>();
let file_names = self.files.iter()
.map(|file| FileName::new(file, LinkStyle::JustFilenames, self.classify, self.colours).paint().promote())
.collect::<Vec<TextCell>>();
(cells, file_names)
};
let mut last_working_table = self.make_grid(env.clone(), 1, &columns_for_dir, &file_names, cells.clone(), self.colours, self.classify);
let mut last_working_table = self.make_grid(&env, 1, &columns_for_dir, &file_names, rows.clone(), &drender);
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 d = grid.fit_into_columns(column_count);
@ -71,33 +81,33 @@ impl<'a> Render<'a> {
Ok(())
}
fn make_table<'g>(&'g self, env: Arc<Environment<UsersCache>>, columns_for_dir: &'g [Column], colours: &'g Colours, classify: Classify) -> Table<UsersCache> {
let mut table = Table {
columns: columns_for_dir,
colours, classify, env,
xattr: self.details.xattr,
rows: Vec::new(),
};
fn make_table<'t>(&'a self, env: &'a Environment, columns_for_dir: &'a [Column], drender: &DetailsRender) -> (Table<'a>, Vec<DetailsRow>) {
let mut table = Table::new(columns_for_dir, self.colours, env);
let mut rows = Vec::new();
if self.details.header { table.add_header() }
table
}
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));
if self.details.header {
rows.push(drender.render_header(table.header_row()));
}
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 {
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);
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 {
i % column_count
}
@ -105,10 +115,15 @@ impl<'a> Render<'a> {
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 }
else { grid::Direction::TopToBottom };

View File

@ -14,3 +14,4 @@ mod colours;
mod escape;
mod render;
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
}
}