From fc60838ff3021ce913b21516c8db0f8879f8644e Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Sun, 2 Jul 2017 01:02:17 +0100 Subject: [PATCH] Extract table from details and grid_details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/exa.rs | 2 +- src/output/details.rs | 414 ++++++++++--------------------------- src/output/grid_details.rs | 91 ++++---- src/output/mod.rs | 1 + src/output/table.rs | 204 ++++++++++++++++++ 5 files changed, 371 insertions(+), 341 deletions(-) create mode 100644 src/output/table.rs diff --git a/src/exa.rs b/src/exa.rs index f262bc5..061ff02 100644 --- a/src/exa.rs +++ b/src/exa.rs @@ -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 { diff --git a/src/output/details.rs b/src/output/details.rs index f514a46..1d8a862 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -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 -//! └── -//! .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 { // 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 *aren’t*. - date_and_year: DateFormat<'static>, - - /// The computer's current time zone. This gets used to determine how to - /// offset files' timestamps. - tz: Option, - - /// Mapping cache of user IDs to usernames. - users: Mutex, -} - -impl Environment { - pub fn lock_users(&self) -> MutexGuard { - 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::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(&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, + errors: Vec<(IOError, Option)>, + dir: Option, + file: &'a File<'a>, +} + +impl<'a> AsRef> for Egg<'a> { + fn as_ref(&self) -> &File<'a> { + self.file + } +} + + +impl<'a> Render<'a> { + pub fn render(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::::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, src: &Vec>, depth: usize) { + fn add_files_to_table<'dir>(&self, mut table: &mut Table, rows: &mut Vec, src: &Vec>, 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, - xattrs: Vec, - errors: Vec<(IOError, Option)>, - dir: Option, - file: &'a File<'a>, - } - - impl<'a> AsRef> 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>, - - /// 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, - pub columns: &'a [Column], - pub colours: &'a Colours, - pub xattr: bool, - pub classify: Classify, - pub env: Arc>, -} - -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) { + fn render_error(&self, error: &IOError, depth: usize, last: bool, path: Option) -> 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, 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 { - 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 { - let mut tree_trunk = TreeTrunk::default(); - let mut cells = Vec::new(); + pub fn iterate(&self, table: &'a Table<'a>, rows: Vec) -> 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 = (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, +} - 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.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, + + /// 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, +} diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index 933021a..5b6ac89 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -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(&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::>(); + let rows = self.files.iter() + .map(|file| first_table.row_for_file(file, file_has_xattrs(file))) + .collect::>(); - let file_names = self.files.iter() - .map(|file| first_table.filename(file, LinkStyle::JustFilenames).promote()) - .collect::>(); + let file_names = self.files.iter() + .map(|file| FileName::new(file, LinkStyle::JustFilenames, self.classify, self.colours).paint().promote()) + .collect::>(); - (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>, columns_for_dir: &'g [Column], colours: &'g Colours, classify: Classify) -> Table { - 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) { + 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>, column_count: usize, columns_for_dir: &'g [Column], file_names: &[TextCell], cells: Vec>, 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, 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::>() + }).collect(); let direction = if self.grid.across { grid::Direction::LeftToRight } else { grid::Direction::TopToBottom }; diff --git a/src/output/mod.rs b/src/output/mod.rs index 1ab9cae..9565536 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -14,3 +14,4 @@ mod colours; mod escape; mod render; mod tree; +mod table; diff --git a/src/output/table.rs b/src/output/table.rs new file mode 100644 index 0000000..cb7b3c6 --- /dev/null +++ b/src/output/table.rs @@ -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 *aren’t*. + date_and_year: DateFormat<'static>, + + /// The computer's current time zone. This gets used to determine how to + /// offset files' timestamps. + tz: Option, + + /// Mapping cache of user IDs to usernames. + users: Mutex, +} + +impl Environment { + pub fn lock_users(&self) -> MutexGuard { + 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::from_file("/etc/localtime") +} + + + + + +pub struct Table<'a> { + columns: &'a [Column], + colours: &'a Colours, + env: &'a Environment, + widths: Vec, +} + +#[derive(Clone)] +pub struct Row { + cells: Vec, +} + +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::>(); + 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 + } +}