From d27812f81964e09e040110575f8cb8d59357e2ba Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 5 Jul 2017 08:21:24 +0100 Subject: [PATCH 01/11] Environment Default trait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Environment struct only used the Default trait so it could have the same call for both Environment and Environment. There’s no reason to keep it around anymore. --- src/output/details.rs | 2 +- src/output/grid_details.rs | 2 +- src/output/table.rs | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/output/details.rs b/src/output/details.rs index 960b0c6f..da786802 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -140,7 +140,7 @@ impl<'a> Render<'a> { let mut rows = Vec::new(); if let Some(columns) = self.opts.columns { - let env = Environment::default(); + let env = Environment::load_all(); let colz = columns.for_dir(self.dir); let mut table = Table::new(&colz, &self.colours, &env); diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index d8895070..3d30cb0a 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -47,7 +47,7 @@ impl<'a> Render<'a> { None => Vec::new(), }; - let env = Environment::default(); + let env = Environment::load_all(); let drender = self.clone().details(); diff --git a/src/output/table.rs b/src/output/table.rs index ae6e6af0..6e5c80cc 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -41,10 +41,8 @@ impl Environment { pub fn lock_users(&self) -> MutexGuard { self.users.lock().unwrap() } -} -impl Default for Environment { - fn default() -> Self { + pub fn load_all() -> Self { let tz = match determine_time_zone() { Ok(t) => Some(t), Err(ref e) => { From 268b7d52dc0fbcd1a24ec04d403f56e36a321cb3 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 5 Jul 2017 20:16:04 +0100 Subject: [PATCH 02/11] Rename Columns to table::Options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The views have been renamed to be the Optionses of their module; now the options for the Table — Columns — has followed suit. This works out, because the table module depended on everything in the columns module. It opens the door for other only-table-specific things to be included. The casualty was that by making it non-Clone and non-PartialEq, a bunch of other #[derive]-d types had to have their derivions removed too. --- src/options/mod.rs | 10 +- src/options/view.rs | 14 +-- src/output/column.rs | 193 ----------------------------------- src/output/details.rs | 16 +-- src/output/grid_details.rs | 5 +- src/output/mod.rs | 3 +- src/output/render/size.rs | 4 +- src/output/table.rs | 201 ++++++++++++++++++++++++++++++++++++- 8 files changed, 223 insertions(+), 223 deletions(-) delete mode 100644 src/output/column.rs diff --git a/src/options/mod.rs b/src/options/mod.rs index fccc08c4..a881476d 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -23,7 +23,7 @@ pub use self::view::{View, Mode}; /// These **options** represent a parsed, error-checked versions of the /// user’s command-line options. -#[derive(PartialEq, Debug, Clone)] +#[derive(Debug)] pub struct Options { /// The action to perform when encountering a directory rather than a @@ -124,8 +124,8 @@ impl Options { /// results will end up being displayed. pub fn should_scan_for_git(&self) -> bool { match self.view.mode { - Mode::Details(details::Options { columns: Some(cols), .. }) | - Mode::GridDetails(_, details::Options { columns: Some(cols), .. }) => cols.should_scan_for_git(), + Mode::Details(details::Options { columns: Some(ref cols), .. }) | + Mode::GridDetails(_, details::Options { columns: Some(ref cols), .. }) => cols.should_scan_for_git(), _ => false, } } @@ -201,13 +201,13 @@ mod test { #[test] fn long_across() { let opts = Options::getopts(&[ "--long", "--across" ]); - assert_eq!(opts, Err(Misfire::Useless("across", true, "long"))) + assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "long")) } #[test] fn oneline_across() { let opts = Options::getopts(&[ "--oneline", "--across" ]); - assert_eq!(opts, Err(Misfire::Useless("across", true, "oneline"))) + assert_eq!(opts.unwrap_err(), Misfire::Useless("across", true, "oneline")) } #[test] diff --git a/src/options/view.rs b/src/options/view.rs index 349fda38..f0c7a070 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -4,14 +4,14 @@ use getopts; use output::Colours; use output::{grid, details}; -use output::column::{Columns, TimeTypes, SizeFormat}; +use output::table::{TimeTypes, SizeFormat, Options as TableOptions}; use output::file_name::Classify; use options::Misfire; use fs::feature::xattr; /// The **view** contains all information about how to format output. -#[derive(PartialEq, Debug, Clone)] +#[derive(Debug)] pub struct View { pub mode: Mode, pub colours: Colours, @@ -31,7 +31,7 @@ impl View { /// The **mode** is the “type” of output. -#[derive(PartialEq, Debug, Clone)] +#[derive(Debug)] pub enum Mode { Grid(grid::Options), Details(details::Options), @@ -54,7 +54,7 @@ impl Mode { } else { Ok(details::Options { - columns: Some(Columns::deduce(matches)?), + columns: Some(TableOptions::deduce(matches)?), header: matches.opt_present("header"), xattr: xattr::ENABLED && matches.opt_present("extended"), }) @@ -194,9 +194,9 @@ impl TerminalWidth { } -impl Columns { - fn deduce(matches: &getopts::Matches) -> Result { - Ok(Columns { +impl TableOptions { + fn deduce(matches: &getopts::Matches) -> Result { + Ok(TableOptions { size_format: SizeFormat::deduce(matches)?, time_types: TimeTypes::deduce(matches)?, inode: matches.opt_present("inode"), diff --git a/src/output/column.rs b/src/output/column.rs deleted file mode 100644 index 36a85117..00000000 --- a/src/output/column.rs +++ /dev/null @@ -1,193 +0,0 @@ -use fs::Dir; - - -#[derive(PartialEq, Debug, Copy, Clone)] -pub enum Column { - Permissions, - FileSize(SizeFormat), - Timestamp(TimeType), - Blocks, - User, - Group, - HardLinks, - Inode, - - GitStatus, -} - -/// Each column can pick its own **Alignment**. Usually, numbers are -/// right-aligned, and text is left-aligned. -#[derive(Copy, Clone)] -pub enum Alignment { - Left, Right, -} - -impl Column { - - /// Get the alignment this column should use. - pub fn alignment(&self) -> Alignment { - match *self { - Column::FileSize(_) - | Column::HardLinks - | Column::Inode - | Column::Blocks - | Column::GitStatus => Alignment::Right, - _ => Alignment::Left, - } - } - - /// Get the text that should be printed at the top, when the user elects - /// to have a header row printed. - pub fn header(&self) -> &'static str { - match *self { - Column::Permissions => "Permissions", - Column::FileSize(_) => "Size", - Column::Timestamp(t) => t.header(), - Column::Blocks => "Blocks", - Column::User => "User", - Column::Group => "Group", - Column::HardLinks => "Links", - Column::Inode => "inode", - Column::GitStatus => "Git", - } - } -} - - -#[derive(PartialEq, Copy, Clone, Debug, Default)] -pub struct Columns { - pub size_format: SizeFormat, - pub time_types: TimeTypes, - pub inode: bool, - pub links: bool, - pub blocks: bool, - pub group: bool, - pub git: bool -} - -impl Columns { - pub fn should_scan_for_git(&self) -> bool { - self.git - } - - pub fn for_dir(&self, dir: Option<&Dir>) -> Vec { - let mut columns = vec![]; - - if self.inode { - columns.push(Column::Inode); - } - - columns.push(Column::Permissions); - - if self.links { - columns.push(Column::HardLinks); - } - - columns.push(Column::FileSize(self.size_format)); - - if self.blocks { - columns.push(Column::Blocks); - } - - columns.push(Column::User); - - if self.group { - columns.push(Column::Group); - } - - if self.time_types.modified { - columns.push(Column::Timestamp(TimeType::Modified)); - } - - if self.time_types.created { - columns.push(Column::Timestamp(TimeType::Created)); - } - - if self.time_types.accessed { - columns.push(Column::Timestamp(TimeType::Accessed)); - } - - if cfg!(feature="git") { - if let Some(d) = dir { - if self.should_scan_for_git() && d.has_git_repo() { - columns.push(Column::GitStatus); - } - } - } - - columns - } -} - - -/// Formatting options for file sizes. -#[derive(PartialEq, Debug, Copy, Clone)] -pub enum SizeFormat { - - /// Format the file size using **decimal** prefixes, such as “kilo”, - /// “mega”, or “giga”. - DecimalBytes, - - /// Format the file size using **binary** prefixes, such as “kibi”, - /// “mebi”, or “gibi”. - BinaryBytes, - - /// Do no formatting and just display the size as a number of bytes. - JustBytes, -} - -impl Default for SizeFormat { - fn default() -> SizeFormat { - SizeFormat::DecimalBytes - } -} - - -/// The types of a file’s time fields. These three fields are standard -/// across most (all?) operating systems. -#[derive(PartialEq, Debug, Copy, Clone)] -pub enum TimeType { - - /// The file’s accessed time (`st_atime`). - Accessed, - - /// The file’s modified time (`st_mtime`). - Modified, - - /// The file’s creation time (`st_ctime`). - Created, -} - -impl TimeType { - - /// Returns the text to use for a column’s heading in the columns output. - pub fn header(&self) -> &'static str { - match *self { - TimeType::Accessed => "Date Accessed", - TimeType::Modified => "Date Modified", - TimeType::Created => "Date Created", - } - } -} - - -/// Fields for which of a file’s time fields should be displayed in the -/// columns output. -/// -/// There should always be at least one of these--there's no way to disable -/// the time columns entirely (yet). -#[derive(PartialEq, Debug, Copy, Clone)] -pub struct TimeTypes { - pub accessed: bool, - pub modified: bool, - pub created: bool, -} - -impl Default for TimeTypes { - - /// By default, display just the ‘modified’ time. This is the most - /// common option, which is why it has this shorthand. - fn default() -> TimeTypes { - TimeTypes { accessed: false, modified: true, created: false } - } -} diff --git a/src/output/details.rs b/src/output/details.rs index da786802..02587ff7 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -68,11 +68,10 @@ use fs::{Dir, File}; use fs::feature::xattr::{Attribute, FileAttributes}; use options::{FileFilter, RecurseOptions}; use output::colours::Colours; -use output::column::Columns; use output::cell::TextCell; use output::tree::{TreeTrunk, TreeParams, TreeDepth}; use output::file_name::{FileName, LinkStyle, Classify}; -use output::table::{Table, Environment, Row as TableRow}; +use output::table::{Table, Environment, Options as TableOptions, Row as TableRow}; /// With the **Details** view, the output gets formatted into columns, with @@ -86,13 +85,14 @@ use output::table::{Table, Environment, Row as TableRow}; /// /// Almost all the heavy lifting is done in a Table object, which handles the /// columns for each row. -#[derive(PartialEq, Debug, Clone, Default)] +#[derive(Debug)] pub struct Options { - /// A Columns object that says which columns should be included in the - /// output in the general case. Directories themselves can pick which - /// columns are *added* to this list, such as the Git column. - pub columns: Option, + /// Options specific to drawing a table. + /// + /// Directories themselves can pick which columns are *added* to this + /// list, such as the Git column. + pub columns: Option, /// Whether to show a header line or not. pub header: bool, @@ -139,7 +139,7 @@ impl<'a> Render<'a> { pub fn render(self, w: &mut W) -> IOResult<()> { let mut rows = Vec::new(); - if let Some(columns) = self.opts.columns { + if let Some(ref columns) = self.opts.columns { let env = Environment::load_all(); let colz = columns.for_dir(self.dir); let mut table = Table::new(&colz, &self.colours, &env); diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index 3d30cb0a..bfa5d4eb 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -8,12 +8,11 @@ use fs::feature::xattr::FileAttributes; use options::FileFilter; use output::cell::TextCell; -use output::column::Column; use output::colours::Colours; use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender}; use output::grid::Options as GridOptions; use output::file_name::{FileName, LinkStyle, Classify}; -use output::table::{Table, Environment, Row as TableRow}; +use output::table::{Table, Column, Environment, Row as TableRow}; use output::tree::{TreeParams, TreeDepth}; @@ -43,7 +42,7 @@ impl<'a> Render<'a> { pub fn render(&self, w: &mut W) -> IOResult<()> { let columns_for_dir = match self.details.columns { - Some(cols) => cols.for_dir(self.dir), + Some(ref cols) => cols.for_dir(self.dir), None => Vec::new(), }; diff --git a/src/output/mod.rs b/src/output/mod.rs index 1d0da6d2..60ba9618 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -2,12 +2,12 @@ pub use self::cell::{TextCell, TextCellContents, DisplayWidth}; pub use self::colours::Colours; pub use self::escape::escape; -pub mod column; pub mod details; pub mod file_name; pub mod grid_details; pub mod grid; pub mod lines; +pub mod table; pub mod time; mod cell; @@ -15,4 +15,3 @@ mod colours; mod escape; mod render; mod tree; -mod table; diff --git a/src/output/render/size.rs b/src/output/render/size.rs index 7bde0897..cd63c6fa 100644 --- a/src/output/render/size.rs +++ b/src/output/render/size.rs @@ -1,7 +1,7 @@ use fs::fields as f; -use output::column::SizeFormat; use output::cell::{TextCell, DisplayWidth}; use output::colours::Colours; +use output::table::SizeFormat; use locale; @@ -68,8 +68,8 @@ impl f::DeviceIDs { #[cfg(test)] pub mod test { use output::colours::Colours; - use output::column::SizeFormat; use output::cell::{TextCell, DisplayWidth}; + use output::table::SizeFormat; use fs::fields as f; use locale; diff --git a/src/output/table.rs b/src/output/table.rs index 6e5c80cc..28932059 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -11,10 +11,205 @@ use users::UsersCache; use output::cell::TextCell; use output::colours::Colours; -use output::column::{Alignment, Column}; use output::time::TimeFormat; -use fs::{File, fields as f}; +use fs::{File, Dir, fields as f}; + + + +/// Options for displaying a table. +#[derive(Debug)] +pub struct Options { + pub size_format: SizeFormat, + pub time_types: TimeTypes, + pub inode: bool, + pub links: bool, + pub blocks: bool, + pub group: bool, + pub git: bool +} + +impl Options { + pub fn should_scan_for_git(&self) -> bool { + self.git + } + + pub fn for_dir(&self, dir: Option<&Dir>) -> Vec { + let mut columns = vec![]; + + if self.inode { + columns.push(Column::Inode); + } + + columns.push(Column::Permissions); + + if self.links { + columns.push(Column::HardLinks); + } + + columns.push(Column::FileSize(self.size_format)); + + if self.blocks { + columns.push(Column::Blocks); + } + + columns.push(Column::User); + + if self.group { + columns.push(Column::Group); + } + + if self.time_types.modified { + columns.push(Column::Timestamp(TimeType::Modified)); + } + + if self.time_types.created { + columns.push(Column::Timestamp(TimeType::Created)); + } + + if self.time_types.accessed { + columns.push(Column::Timestamp(TimeType::Accessed)); + } + + if cfg!(feature="git") { + if let Some(d) = dir { + if self.should_scan_for_git() && d.has_git_repo() { + columns.push(Column::GitStatus); + } + } + } + + columns + } +} + + +/// A table contains these. +#[derive(Debug)] +pub enum Column { + Permissions, + FileSize(SizeFormat), + Timestamp(TimeType), + Blocks, + User, + Group, + HardLinks, + Inode, + GitStatus, +} + +/// Each column can pick its own **Alignment**. Usually, numbers are +/// right-aligned, and text is left-aligned. +#[derive(Copy, Clone)] +pub enum Alignment { + Left, Right, +} + +impl Column { + + /// Get the alignment this column should use. + pub fn alignment(&self) -> Alignment { + match *self { + Column::FileSize(_) + | Column::HardLinks + | Column::Inode + | Column::Blocks + | Column::GitStatus => Alignment::Right, + _ => Alignment::Left, + } + } + + /// Get the text that should be printed at the top, when the user elects + /// to have a header row printed. + pub fn header(&self) -> &'static str { + match *self { + Column::Permissions => "Permissions", + Column::FileSize(_) => "Size", + Column::Timestamp(t) => t.header(), + Column::Blocks => "Blocks", + Column::User => "User", + Column::Group => "Group", + Column::HardLinks => "Links", + Column::Inode => "inode", + Column::GitStatus => "Git", + } + } +} + + +/// Formatting options for file sizes. +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum SizeFormat { + + /// Format the file size using **decimal** prefixes, such as “kilo”, + /// “mega”, or “giga”. + DecimalBytes, + + /// Format the file size using **binary** prefixes, such as “kibi”, + /// “mebi”, or “gibi”. + BinaryBytes, + + /// Do no formatting and just display the size as a number of bytes. + JustBytes, +} + +impl Default for SizeFormat { + fn default() -> SizeFormat { + SizeFormat::DecimalBytes + } +} + + +/// The types of a file’s time fields. These three fields are standard +/// across most (all?) operating systems. +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum TimeType { + + /// The file’s accessed time (`st_atime`). + Accessed, + + /// The file’s modified time (`st_mtime`). + Modified, + + /// The file’s creation time (`st_ctime`). + Created, +} + +impl TimeType { + + /// Returns the text to use for a column’s heading in the columns output. + pub fn header(&self) -> &'static str { + match *self { + TimeType::Accessed => "Date Accessed", + TimeType::Modified => "Date Modified", + TimeType::Created => "Date Created", + } + } +} + + +/// Fields for which of a file’s time fields should be displayed in the +/// columns output. +/// +/// There should always be at least one of these--there's no way to disable +/// the time columns entirely (yet). +#[derive(PartialEq, Debug, Copy, Clone)] +pub struct TimeTypes { + pub accessed: bool, + pub modified: bool, + pub created: bool, +} + +impl Default for TimeTypes { + + /// By default, display just the ‘modified’ time. This is the most + /// common option, which is why it has this shorthand. + fn default() -> TimeTypes { + TimeTypes { accessed: false, modified: true, created: false } + } +} + + /// The **environment** struct contains any data that could change between @@ -121,7 +316,7 @@ impl<'a, 'f> Table<'a> { } fn display(&self, file: &File, column: &Column, xattrs: bool) -> TextCell { - use output::column::TimeType::*; + use output::table::TimeType::*; match *column { Column::Permissions => self.permissions_plus(file, xattrs).render(&self.colours), From d93e168b4d1ba9f12fd49ef620b79438adae7f9a Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 5 Jul 2017 21:01:01 +0100 Subject: [PATCH 03/11] =?UTF-8?q?Move=20Environment=20to=20a=20table?= =?UTF-8?q?=E2=80=99s=20Options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit moves the Environment field from the Table to its Options, and properly gets rid of the name ‘columns’ from the last commit. Having it in the Options is important, because it means it can be generated from some command-line options. Also, it reduces the number of arguments that need to be passed to Table::new; there would have been 4 with the inclusion of the Environment, but by moving some of the code into the function, we can avoid this (and any further arguments). --- src/options/mod.rs | 4 ++-- src/options/view.rs | 9 +++++---- src/output/details.rs | 10 ++++------ src/output/grid_details.rs | 23 +++++++++-------------- src/output/table.rs | 20 +++++++++++++++----- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/options/mod.rs b/src/options/mod.rs index a881476d..4cde0c8e 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -124,8 +124,8 @@ impl Options { /// results will end up being displayed. pub fn should_scan_for_git(&self) -> bool { match self.view.mode { - Mode::Details(details::Options { columns: Some(ref cols), .. }) | - Mode::GridDetails(_, details::Options { columns: Some(ref cols), .. }) => cols.should_scan_for_git(), + Mode::Details(details::Options { table: Some(ref table), .. }) | + Mode::GridDetails(_, details::Options { table: Some(ref table), .. }) => table.should_scan_for_git(), _ => false, } } diff --git a/src/options/view.rs b/src/options/view.rs index f0c7a070..48ea834f 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -4,7 +4,7 @@ use getopts; use output::Colours; use output::{grid, details}; -use output::table::{TimeTypes, SizeFormat, Options as TableOptions}; +use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions}; use output::file_name::Classify; use options::Misfire; use fs::feature::xattr; @@ -54,7 +54,7 @@ impl Mode { } else { Ok(details::Options { - columns: Some(TableOptions::deduce(matches)?), + table: Some(TableOptions::deduce(matches)?), header: matches.opt_present("header"), xattr: xattr::ENABLED && matches.opt_present("extended"), }) @@ -94,7 +94,7 @@ impl Mode { } else if matches.opt_present("tree") { let details = details::Options { - columns: None, + table: None, header: false, xattr: false, }; @@ -117,7 +117,7 @@ impl Mode { if matches.opt_present("tree") { let details = details::Options { - columns: None, + table: None, header: false, xattr: false, }; @@ -197,6 +197,7 @@ impl TerminalWidth { impl TableOptions { fn deduce(matches: &getopts::Matches) -> Result { Ok(TableOptions { + env: Environment::load_all(), size_format: SizeFormat::deduce(matches)?, time_types: TimeTypes::deduce(matches)?, inode: matches.opt_present("inode"), diff --git a/src/output/details.rs b/src/output/details.rs index 02587ff7..09cf475a 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -71,7 +71,7 @@ use output::colours::Colours; use output::cell::TextCell; use output::tree::{TreeTrunk, TreeParams, TreeDepth}; use output::file_name::{FileName, LinkStyle, Classify}; -use output::table::{Table, Environment, Options as TableOptions, Row as TableRow}; +use output::table::{Table, Options as TableOptions, Row as TableRow}; /// With the **Details** view, the output gets formatted into columns, with @@ -92,7 +92,7 @@ pub struct Options { /// /// Directories themselves can pick which columns are *added* to this /// list, such as the Git column. - pub columns: Option, + pub table: Option, /// Whether to show a header line or not. pub header: bool, @@ -139,10 +139,8 @@ impl<'a> Render<'a> { pub fn render(self, w: &mut W) -> IOResult<()> { let mut rows = Vec::new(); - if let Some(ref columns) = self.opts.columns { - let env = Environment::load_all(); - let colz = columns.for_dir(self.dir); - let mut table = Table::new(&colz, &self.colours, &env); + if let Some(ref table) = self.opts.table { + let mut table = Table::new(&table, self.dir, &self.colours); if self.opts.header { let header = table.header_row(); diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index bfa5d4eb..788c3f25 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -12,7 +12,7 @@ use output::colours::Colours; use output::details::{Options as DetailsOptions, Row as DetailsRow, Render as DetailsRender}; use output::grid::Options as GridOptions; use output::file_name::{FileName, LinkStyle, Classify}; -use output::table::{Table, Column, Environment, Row as TableRow}; +use output::table::{Table, Row as TableRow, Options as TableOptions}; use output::tree::{TreeParams, TreeDepth}; @@ -41,16 +41,11 @@ impl<'a> Render<'a> { pub fn render(&self, w: &mut W) -> IOResult<()> { - let columns_for_dir = match self.details.columns { - Some(ref cols) => cols.for_dir(self.dir), - None => Vec::new(), - }; - - let env = Environment::load_all(); + let options = self.details.table.as_ref().expect("Details table options not given!"); let drender = self.clone().details(); - let (first_table, _) = self.make_table(&env, &columns_for_dir, &drender); + let (first_table, _) = self.make_table(options, &drender); let rows = self.files.iter() .map(|file| first_table.row_for_file(file, file_has_xattrs(file))) @@ -60,10 +55,10 @@ impl<'a> Render<'a> { .map(|file| FileName::new(file, LinkStyle::JustFilenames, self.classify, self.colours).paint().promote()) .collect::>(); - 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(1, options, &file_names, rows.clone(), &drender); for column_count in 2.. { - let grid = self.make_grid(&env, column_count, &columns_for_dir, &file_names, rows.clone(), &drender); + let grid = self.make_grid(column_count, options, &file_names, rows.clone(), &drender); let the_grid_fits = { let d = grid.fit_into_columns(column_count); @@ -81,8 +76,8 @@ impl<'a> Render<'a> { Ok(()) } - 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); + fn make_table<'t>(&'a self, options: &'a TableOptions, drender: &DetailsRender) -> (Table<'a>, Vec) { + let mut table = Table::new(options, self.dir, self.colours); let mut rows = Vec::new(); if self.details.header { @@ -94,11 +89,11 @@ impl<'a> Render<'a> { (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 { + fn make_grid(&'a self, column_count: usize, options: &'a TableOptions, 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)); + tables.push(self.make_table(options, drender)); } let mut num_cells = rows.len(); diff --git a/src/output/table.rs b/src/output/table.rs index 28932059..f5dce9de 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -1,4 +1,5 @@ use std::cmp::max; +use std::fmt; use std::ops::Deref; use std::sync::{Mutex, MutexGuard}; @@ -18,8 +19,8 @@ use fs::{File, Dir, fields as f}; /// Options for displaying a table. -#[derive(Debug)] pub struct Options { + pub env: Environment, pub size_format: SizeFormat, pub time_types: TimeTypes, pub inode: bool, @@ -29,6 +30,14 @@ pub struct Options { pub git: bool } +impl fmt::Debug for Options { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + // I had to make other types derive Debug, + // and Mutex is not that! + writeln!(f, "") + } +} + impl Options { pub fn should_scan_for_git(&self) -> bool { self.git @@ -266,7 +275,7 @@ fn determine_time_zone() -> TZResult { pub struct Table<'a> { - columns: &'a [Column], + columns: Vec, colours: &'a Colours, env: &'a Environment, widths: TableWidths, @@ -278,9 +287,10 @@ pub struct Row { } impl<'a, 'f> Table<'a> { - pub fn new(columns: &'a [Column], colours: &'a Colours, env: &'a Environment) -> Table<'a> { - let widths = TableWidths::zero(columns.len()); - Table { columns, colours, env, widths } + pub fn new(options: &'a Options, dir: Option<&'a Dir>, colours: &'a Colours) -> Table<'a> { + let colz = options.for_dir(dir); + let widths = TableWidths::zero(colz.len()); + Table { columns: colz, colours, env: &options.env, widths } } pub fn widths(&self) -> &TableWidths { From ba335bb6e7d8924571ed9a4450bb6ef4f78b0be9 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 5 Jul 2017 21:54:43 +0100 Subject: [PATCH 04/11] Separate TimeFormat from the Environment By moving it outside of the Environment::load_all() constructor, it can be set to different values. --- src/options/view.rs | 2 ++ src/output/table.rs | 17 +++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/options/view.rs b/src/options/view.rs index 48ea834f..734685ff 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -6,6 +6,7 @@ use output::Colours; use output::{grid, details}; use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions}; use output::file_name::Classify; +use output::time::TimeFormat; use options::Misfire; use fs::feature::xattr; @@ -198,6 +199,7 @@ impl TableOptions { fn deduce(matches: &getopts::Matches) -> Result { Ok(TableOptions { env: Environment::load_all(), + time_format: TimeFormat::deduce(), size_format: SizeFormat::deduce(matches)?, time_types: TimeTypes::deduce(matches)?, inode: matches.opt_present("inode"), diff --git a/src/output/table.rs b/src/output/table.rs index f5dce9de..0df306dc 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -22,6 +22,7 @@ use fs::{File, Dir, fields as f}; pub struct Options { pub env: Environment, pub size_format: SizeFormat, + pub time_format: TimeFormat, pub time_types: TimeTypes, pub inode: bool, pub links: bool, @@ -230,9 +231,6 @@ pub struct Environment { /// Localisation rules for formatting numbers. numeric: locale::Numeric, - /// Rules for formatting timestamps. - time_format: TimeFormat, - /// The computer's current time zone. This gets used to determine how to /// offset files' timestamps. tz: Option, @@ -255,14 +253,12 @@ impl Environment { } }; - let time_format = TimeFormat::deduce(); - let numeric = locale::Numeric::load_user_locale() .unwrap_or_else(|_| locale::Numeric::english()); let users = Mutex::new(UsersCache::new()); - Environment { tz, time_format, numeric, users } + Environment { tz, numeric, users } } } @@ -279,6 +275,7 @@ pub struct Table<'a> { colours: &'a Colours, env: &'a Environment, widths: TableWidths, + time_format: &'a TimeFormat, } #[derive(Clone)] @@ -290,7 +287,7 @@ impl<'a, 'f> Table<'a> { pub fn new(options: &'a Options, dir: Option<&'a Dir>, colours: &'a Colours) -> Table<'a> { let colz = options.for_dir(dir); let widths = TableWidths::zero(colz.len()); - Table { columns: colz, colours, env: &options.env, widths } + Table { columns: colz, colours, env: &options.env, widths, time_format: &options.time_format } } pub fn widths(&self) -> &TableWidths { @@ -338,9 +335,9 @@ impl<'a, 'f> Table<'a> { Column::Group => file.group().render(&self.colours, &*self.env.lock_users()), Column::GitStatus => file.git_status().render(&self.colours), - Column::Timestamp(Modified) => file.modified_time().render(&self.colours, &self.env.tz, &self.env.time_format), - Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.env.time_format), - Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.env.time_format), + Column::Timestamp(Modified) => file.modified_time().render(&self.colours, &self.env.tz, &self.time_format), + Column::Timestamp(Created) => file.created_time().render( &self.colours, &self.env.tz, &self.time_format), + Column::Timestamp(Accessed) => file.accessed_time().render(&self.colours, &self.env.tz, &self.time_format), } } From 5bdf6304bb9e4bc55e510594f8bbe2c0fe96c9d2 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 5 Jul 2017 22:07:03 +0100 Subject: [PATCH 05/11] Fix bug where accessed times were wrong! It used the mtime, rather than the atime. Copy and paste error. Whoops! --- Vagrantfile | 2 ++ src/fs/file.rs | 2 +- xtests/dates_accessed | 4 ++++ xtests/dates_modified | 4 ++++ xtests/run.sh | 5 +++++ 5 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 xtests/dates_accessed create mode 100644 xtests/dates_modified diff --git a/Vagrantfile b/Vagrantfile index 10c2e79d..999ed95f 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -320,6 +320,8 @@ Vagrant.configure(2) do |config| touch -t #{old} -a "#{test_dir}/dates/plum" touch -t #{med} -a "#{test_dir}/dates/pear" touch -t #{new} -a "#{test_dir}/dates/peach" + + sudo chown #{user}:#{user} -R "#{test_dir}/dates" EOF diff --git a/src/fs/file.rs b/src/fs/file.rs index 3defdd78..6e99ca7d 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -282,7 +282,7 @@ impl<'dir> File<'dir> { } pub fn accessed_time(&self) -> f::Time { - f::Time(self.metadata.mtime()) + f::Time(self.metadata.atime()) } /// This file's 'type'. diff --git a/xtests/dates_accessed b/xtests/dates_accessed new file mode 100644 index 00000000..3d80658a --- /dev/null +++ b/xtests/dates_accessed @@ -0,0 +1,4 @@ +Permissions Size User Date Accessed Name +.rw-rw-r-- 0 cassowary  3 Mar 2003 plum +.rw-rw-r-- 0 cassowary 15 Jun 2006 pear +.rw-rw-r-- 0 cassowary 22 Jul 2009 peach diff --git a/xtests/dates_modified b/xtests/dates_modified new file mode 100644 index 00000000..166b68fa --- /dev/null +++ b/xtests/dates_modified @@ -0,0 +1,4 @@ +Permissions Size User Date Modified Name +.rw-rw-r-- 0 cassowary  3 Mar 2003 pear +.rw-rw-r-- 0 cassowary 15 Jun 2006 peach +.rw-rw-r-- 0 cassowary 22 Jul 2009 plum diff --git a/xtests/run.sh b/xtests/run.sh index 8493f971..42200e05 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -107,6 +107,11 @@ $exa $testcases/file-names-exts/music.* -I "*.OGG" -1 2>&1 | diff -q - $re $exa $testcases/file-names-exts/music.* -I "*.OGG|*.mp3" -1 2>&1 | diff -q - $results/empty || exit 1 +# Dates and times +$exa $testcases/dates -lh --accessed --sort=accessed 2>&1 | diff -q - $results/dates_accessed || exit 1 +$exa $testcases/dates -lh --sort=modified 2>&1 | diff -q - $results/dates_modified || exit 1 + + # Paths and directories # These directories are created in the VM user’s home directory (the default # location) when a Cargo build is done. From aa5b1867dd4fb9b48985a4f33632e93dcd0d3f80 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 5 Jul 2017 23:07:27 +0100 Subject: [PATCH 06/11] Make nanoseconds available to times The information was always in the Metadata struct; exa just never used it. --- src/fs/fields.rs | 6 +++++- src/fs/file.rs | 28 ++++++++++++++++++++-------- src/output/render/times.rs | 7 +++---- src/output/time.rs | 10 +++++----- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/fs/fields.rs b/src/fs/fields.rs index 07b41dc8..75301a6d 100644 --- a/src/fs/fields.rs +++ b/src/fs/fields.rs @@ -166,7 +166,11 @@ pub struct DeviceIDs { /// One of a file’s timestamps (created, accessed, or modified). -pub struct Time(pub time_t); +#[derive(Copy, Clone)] +pub struct Time { + pub seconds: time_t, + pub nanoseconds: time_t, +} /// A file’s status in a Git repository. Whether a file is in a repository or diff --git a/src/fs/file.rs b/src/fs/file.rs index 6e99ca7d..ff0460a4 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -273,23 +273,35 @@ impl<'dir> File<'dir> { } } + /// This file’s last modified timestamp. pub fn modified_time(&self) -> f::Time { - f::Time(self.metadata.mtime()) + f::Time { + seconds: self.metadata.mtime(), + nanoseconds: self.metadata.mtime_nsec() + } } + /// This file’s created timestamp. pub fn created_time(&self) -> f::Time { - f::Time(self.metadata.ctime()) + f::Time { + seconds: self.metadata.ctime(), + nanoseconds: self.metadata.ctime_nsec() + } } + /// This file’s last accessed timestamp. pub fn accessed_time(&self) -> f::Time { - f::Time(self.metadata.atime()) + f::Time { + seconds: self.metadata.atime(), + nanoseconds: self.metadata.atime_nsec() + } } - /// This file's 'type'. + /// This file’s ‘type’. /// - /// This is used in the leftmost column of the permissions column. - /// Although the file type can usually be guessed from the colour of the - /// file, `ls` puts this character there, so people will expect it. + /// This is used a the leftmost character of the permissions column. + /// The file type can usually be guessed from the colour of the file, but + /// ls puts this character there. pub fn type_char(&self) -> f::Type { if self.is_file() { f::Type::File @@ -341,7 +353,7 @@ impl<'dir> File<'dir> { } } - /// Whether this file's extension is any of the strings that get passed in. + /// Whether this file’s extension is any of the strings that get passed in. /// /// This will always return `false` if the file has no extension. pub fn extension_is_one_of(&self, choices: &[&str]) -> bool { diff --git a/src/output/render/times.rs b/src/output/render/times.rs index 03bcf089..7c32943b 100644 --- a/src/output/render/times.rs +++ b/src/output/render/times.rs @@ -6,18 +6,17 @@ use output::colours::Colours; use output::time::TimeFormat; -#[allow(trivial_numeric_casts)] impl f::Time { - pub fn render(&self, colours: &Colours, + pub fn render(self, colours: &Colours, tz: &Option, style: &TimeFormat) -> TextCell { if let Some(ref tz) = *tz { - let datestamp = style.format_zoned(self.0 as i64, tz); + let datestamp = style.format_zoned(self, tz); TextCell::paint(colours.date, datestamp) } else { - let datestamp = style.format_local(self.0 as i64); + let datestamp = style.format_local(self); TextCell::paint(colours.date, datestamp) } } diff --git a/src/output/time.rs b/src/output/time.rs index d0e71621..efd0a342 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -2,7 +2,7 @@ use datetime::{LocalDateTime, TimeZone, DatePiece}; use datetime::fmt::DateFormat; use locale; -use fs::fields::time_t; +use fs::fields::Time; #[derive(Debug, Clone)] @@ -28,8 +28,8 @@ impl TimeFormat { } #[allow(trivial_numeric_casts)] - pub fn format_local(&self, time: time_t) -> String { - let date = LocalDateTime::at(time as i64); + pub fn format_local(&self, time: Time) -> String { + let date = LocalDateTime::at(time.seconds as i64); if self.is_recent(date) { self.date_and_time.format(&date, &self.locale) @@ -40,8 +40,8 @@ impl TimeFormat { } #[allow(trivial_numeric_casts)] - pub fn format_zoned(&self, time: time_t, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(time as i64)); + pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64)); if self.is_recent(date) { self.date_and_time.format(&date, &self.locale) From 98b63705be706fb115811cc495ab8e6d6aafe769 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 5 Jul 2017 23:27:48 +0100 Subject: [PATCH 07/11] Expect different time formats --- src/options/view.rs | 13 +++++-- src/output/time.rs | 82 ++++++++++++++++++++++++++++----------------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/options/view.rs b/src/options/view.rs index 734685ff..3f16225c 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -6,7 +6,7 @@ use output::Colours; use output::{grid, details}; use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions}; use output::file_name::Classify; -use output::time::TimeFormat; +use output::time::{TimeFormat, DefaultFormat}; use options::Misfire; use fs::feature::xattr; @@ -199,7 +199,7 @@ impl TableOptions { fn deduce(matches: &getopts::Matches) -> Result { Ok(TableOptions { env: Environment::load_all(), - time_format: TimeFormat::deduce(), + time_format: TimeFormat::deduce(matches)?, size_format: SizeFormat::deduce(matches)?, time_types: TimeTypes::deduce(matches)?, inode: matches.opt_present("inode"), @@ -236,6 +236,15 @@ impl SizeFormat { } +impl TimeFormat { + + /// Determine how time should be formatted in timestamp columns. + fn deduce(_matches: &getopts::Matches) -> Result { + Ok(TimeFormat::DefaultFormat(DefaultFormat::new())) + } +} + + impl TimeTypes { /// Determine which of a file’s time fields should be displayed for it diff --git a/src/output/time.rs b/src/output/time.rs index efd0a342..bfbe2976 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -5,8 +5,27 @@ use locale; use fs::fields::Time; +pub enum TimeFormat { + DefaultFormat(DefaultFormat), +} + +impl TimeFormat { + pub fn format_local(&self, time: Time) -> String { + match *self { + TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time), + } + } + + pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { + match *self { + TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone), + } + } +} + + #[derive(Debug, Clone)] -pub struct TimeFormat { +pub struct DefaultFormat { /// The year of the current time. This gets used to determine which date /// format to use. @@ -22,13 +41,42 @@ pub struct TimeFormat { pub date_and_year: DateFormat<'static>, } -impl TimeFormat { +impl DefaultFormat { + pub fn new() -> DefaultFormat { + use unicode_width::UnicodeWidthStr; + + let locale = locale::Time::load_user_locale() + .unwrap_or_else(|_| locale::Time::english()); + + let current_year = LocalDateTime::now().year(); + + // 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(&*locale.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() + }; + + DefaultFormat { current_year, locale, date_and_time, date_and_year } + } + fn is_recent(&self, date: LocalDateTime) -> bool { date.year() == self.current_year } +} + +impl DefaultFormat { #[allow(trivial_numeric_casts)] - pub fn format_local(&self, time: Time) -> String { + fn format_local(&self, time: Time) -> String { let date = LocalDateTime::at(time.seconds as i64); if self.is_recent(date) { @@ -40,7 +88,7 @@ impl TimeFormat { } #[allow(trivial_numeric_casts)] - pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { + fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64)); if self.is_recent(date) { @@ -50,30 +98,4 @@ impl TimeFormat { self.date_and_year.format(&date, &self.locale) } } - - pub fn deduce() -> TimeFormat { - use unicode_width::UnicodeWidthStr; - - let locale = locale::Time::load_user_locale() - .unwrap_or_else(|_| locale::Time::english()); - - let current_year = LocalDateTime::now().year(); - - // 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(&*locale.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() - }; - - TimeFormat { current_year, locale, date_and_time, date_and_year } - } } From 786e8f4d7fd3668c3e7d4bd122ef55db93af75c6 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Thu, 6 Jul 2017 00:01:45 +0100 Subject: [PATCH 08/11] Add long-iso style and --time-style option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This has to do its own number formatting because *somebody* didn’t add “print the current month number” functionality to rust-datetime! --- src/options/mod.rs | 23 ++++++++++++----------- src/options/view.rs | 18 +++++++++++++++--- src/output/time.rs | 27 +++++++++++++++++++++++---- xtests/dates_long_iso | 3 +++ xtests/run.sh | 1 + 5 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 xtests/dates_long_iso diff --git a/src/options/mod.rs b/src/options/mod.rs index 4cde0c8e..4215c797 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -77,17 +77,18 @@ impl Options { opts.optopt ("I", "ignore-glob", "ignore files that match these glob patterns", "GLOB1|GLOB2..."); // Long view options - opts.optflag("b", "binary", "list file sizes with binary prefixes"); - opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes"); - opts.optflag("g", "group", "list each file's group"); - opts.optflag("h", "header", "add a header row to each column"); - opts.optflag("H", "links", "list each file's number of hard links"); - opts.optflag("i", "inode", "list each file's inode number"); - opts.optflag("m", "modified", "use the modified timestamp field"); - opts.optflag("S", "blocks", "list each file's number of file system blocks"); - opts.optopt ("t", "time", "which timestamp field to show", "WORD"); - opts.optflag("u", "accessed", "use the accessed timestamp field"); - opts.optflag("U", "created", "use the created timestamp field"); + opts.optflag("b", "binary", "list file sizes with binary prefixes"); + opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes"); + opts.optflag("g", "group", "list each file's group"); + opts.optflag("h", "header", "add a header row to each column"); + opts.optflag("H", "links", "list each file's number of hard links"); + opts.optflag("i", "inode", "list each file's inode number"); + opts.optflag("m", "modified", "use the modified timestamp field"); + opts.optflag("S", "blocks", "list each file's number of file system blocks"); + opts.optopt ("t", "time", "which timestamp field to show", "WORD"); + opts.optflag("u", "accessed", "use the accessed timestamp field"); + opts.optflag("U", "created", "use the created timestamp field"); + opts.optopt ("", "time-style", "how to format timestamp fields", "STYLE"); if cfg!(feature="git") { opts.optflag("", "git", "list each file's git status"); diff --git a/src/options/view.rs b/src/options/view.rs index 3f16225c..df47e4ef 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -6,7 +6,7 @@ use output::Colours; use output::{grid, details}; use output::table::{TimeTypes, Environment, SizeFormat, Options as TableOptions}; use output::file_name::Classify; -use output::time::{TimeFormat, DefaultFormat}; +use output::time::TimeFormat; use options::Misfire; use fs::feature::xattr; @@ -239,8 +239,20 @@ impl SizeFormat { impl TimeFormat { /// Determine how time should be formatted in timestamp columns. - fn deduce(_matches: &getopts::Matches) -> Result { - Ok(TimeFormat::DefaultFormat(DefaultFormat::new())) + fn deduce(matches: &getopts::Matches) -> Result { + pub use output::time::{DefaultFormat, LongISO}; + const STYLES: &[&str] = &["default", "long-iso"]; + + if let Some(word) = matches.opt_str("time-style") { + match &*word { + "default" => Ok(TimeFormat::DefaultFormat(DefaultFormat::new())), + "long-iso" => Ok(TimeFormat::LongISO(LongISO)), + otherwise => Err(Misfire::bad_argument("time-style", otherwise, STYLES)) + } + } + else { + Ok(TimeFormat::DefaultFormat(DefaultFormat::new())) + } } } diff --git a/src/output/time.rs b/src/output/time.rs index bfbe2976..18ebc851 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -1,4 +1,4 @@ -use datetime::{LocalDateTime, TimeZone, DatePiece}; +use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece}; use datetime::fmt::DateFormat; use locale; @@ -7,18 +7,21 @@ use fs::fields::Time; pub enum TimeFormat { DefaultFormat(DefaultFormat), + LongISO(LongISO), } impl TimeFormat { pub fn format_local(&self, time: Time) -> String { match *self { TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time), + TimeFormat::LongISO(ref iso) => iso.format_local(time), } } pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { match *self { TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone), + TimeFormat::LongISO(ref iso) => iso.format_zoned(time, zone), } } } @@ -71,9 +74,6 @@ impl DefaultFormat { fn is_recent(&self, date: LocalDateTime) -> bool { date.year() == self.current_year } -} - -impl DefaultFormat { #[allow(trivial_numeric_casts)] fn format_local(&self, time: Time) -> String { @@ -99,3 +99,22 @@ impl DefaultFormat { } } } + + +pub struct LongISO; + +impl LongISO { + #[allow(trivial_numeric_casts)] + fn format_local(&self, time: Time) -> String { + let date = LocalDateTime::at(time.seconds as i64); + format!("{:04}-{:02}-{:02} {:02}:{:02}", + date.year(), date.month() as usize, date.day(), date.hour(), date.minute()) + } + + #[allow(trivial_numeric_casts)] + fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64)); + format!("{:04}-{:02}-{:02} {:02}:{:02}", + date.year(), date.month() as usize, date.day(), date.hour(), date.minute()) + } +} diff --git a/xtests/dates_long_iso b/xtests/dates_long_iso new file mode 100644 index 00000000..2694a17a --- /dev/null +++ b/xtests/dates_long_iso @@ -0,0 +1,3 @@ +.rw-rw-r-- 0 cassowary 2006-06-15 23:14 peach +.rw-rw-r-- 0 cassowary 2003-03-03 00:00 pear +.rw-rw-r-- 0 cassowary 2009-07-22 10:38 plum diff --git a/xtests/run.sh b/xtests/run.sh index 42200e05..56928612 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -110,6 +110,7 @@ $exa $testcases/file-names-exts/music.* -I "*.OGG|*.mp3" -1 2>&1 | diff -q - $re # Dates and times $exa $testcases/dates -lh --accessed --sort=accessed 2>&1 | diff -q - $results/dates_accessed || exit 1 $exa $testcases/dates -lh --sort=modified 2>&1 | diff -q - $results/dates_modified || exit 1 +$exa $testcases/dates -l --time-style=long-iso 2>&1 | diff -q - $results/dates_long_iso || exit 1 # Paths and directories From f0eed9fde41e7f5fe09291fa887f39d9db5cd0ee Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Thu, 6 Jul 2017 00:21:38 +0100 Subject: [PATCH 09/11] Add full-iso time style --- src/options/view.rs | 7 +++--- src/output/time.rs | 57 ++++++++++++++++++++++++++++++------------- xtests/dates_full_iso | 3 +++ xtests/run.sh | 1 + 4 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 xtests/dates_full_iso diff --git a/src/options/view.rs b/src/options/view.rs index df47e4ef..caec38b4 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -240,13 +240,14 @@ impl TimeFormat { /// Determine how time should be formatted in timestamp columns. fn deduce(matches: &getopts::Matches) -> Result { - pub use output::time::{DefaultFormat, LongISO}; - const STYLES: &[&str] = &["default", "long-iso"]; + pub use output::time::{DefaultFormat}; + const STYLES: &[&str] = &["default", "long-iso", "full-iso"]; if let Some(word) = matches.opt_str("time-style") { match &*word { "default" => Ok(TimeFormat::DefaultFormat(DefaultFormat::new())), - "long-iso" => Ok(TimeFormat::LongISO(LongISO)), + "long-iso" => Ok(TimeFormat::LongISO), + "full-iso" => Ok(TimeFormat::FullISO), otherwise => Err(Misfire::bad_argument("time-style", otherwise, STYLES)) } } diff --git a/src/output/time.rs b/src/output/time.rs index 18ebc851..49196398 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -7,21 +7,24 @@ use fs::fields::Time; pub enum TimeFormat { DefaultFormat(DefaultFormat), - LongISO(LongISO), + LongISO, + FullISO, } impl TimeFormat { pub fn format_local(&self, time: Time) -> String { match *self { TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time), - TimeFormat::LongISO(ref iso) => iso.format_local(time), + TimeFormat::LongISO => long_local(time), + TimeFormat::FullISO => full_local(time), } } pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { match *self { TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone), - TimeFormat::LongISO(ref iso) => iso.format_zoned(time, zone), + TimeFormat::LongISO => long_zoned(time, zone), + TimeFormat::FullISO => full_zoned(time, zone), } } } @@ -101,20 +104,40 @@ impl DefaultFormat { } -pub struct LongISO; +#[allow(trivial_numeric_casts)] +fn long_local(time: Time) -> String { + let date = LocalDateTime::at(time.seconds as i64); + format!("{:04}-{:02}-{:02} {:02}:{:02}", + date.year(), date.month() as usize, date.day(), + date.hour(), date.minute()) +} -impl LongISO { - #[allow(trivial_numeric_casts)] - fn format_local(&self, time: Time) -> String { - let date = LocalDateTime::at(time.seconds as i64); - format!("{:04}-{:02}-{:02} {:02}:{:02}", - date.year(), date.month() as usize, date.day(), date.hour(), date.minute()) - } +#[allow(trivial_numeric_casts)] +fn long_zoned(time: Time, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64)); + format!("{:04}-{:02}-{:02} {:02}:{:02}", + date.year(), date.month() as usize, date.day(), + date.hour(), date.minute()) +} - #[allow(trivial_numeric_casts)] - fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64)); - format!("{:04}-{:02}-{:02} {:02}:{:02}", - date.year(), date.month() as usize, date.day(), date.hour(), date.minute()) - } + +#[allow(trivial_numeric_casts)] +fn full_local(time: Time) -> String { + let date = LocalDateTime::at(time.seconds as i64); + format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}", + date.year(), date.month() as usize, date.day(), + date.hour(), date.minute(), date.second(), time.nanoseconds) +} + +#[allow(trivial_numeric_casts)] +fn full_zoned(time: Time, zone: &TimeZone) -> String { + use datetime::Offset; + + let local = LocalDateTime::at(time.seconds as i64); + let date = zone.to_zoned(local); + let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range"); + format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}", + date.year(), date.month() as usize, date.day(), + date.hour(), date.minute(), date.second(), time.nanoseconds, + offset.hours(), offset.minutes().abs()) } diff --git a/xtests/dates_full_iso b/xtests/dates_full_iso new file mode 100644 index 00000000..397d7716 --- /dev/null +++ b/xtests/dates_full_iso @@ -0,0 +1,3 @@ +.rw-rw-r-- 0 cassowary 2006-06-15 23:14:29.000000000 +0000 peach +.rw-rw-r-- 0 cassowary 2003-03-03 00:00:00.000000000 +0000 pear +.rw-rw-r-- 0 cassowary 2009-07-22 10:38:53.000000000 +0000 plum diff --git a/xtests/run.sh b/xtests/run.sh index 56928612..da06d331 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -111,6 +111,7 @@ $exa $testcases/file-names-exts/music.* -I "*.OGG|*.mp3" -1 2>&1 | diff -q - $re $exa $testcases/dates -lh --accessed --sort=accessed 2>&1 | diff -q - $results/dates_accessed || exit 1 $exa $testcases/dates -lh --sort=modified 2>&1 | diff -q - $results/dates_modified || exit 1 $exa $testcases/dates -l --time-style=long-iso 2>&1 | diff -q - $results/dates_long_iso || exit 1 +$exa $testcases/dates -l --time-style=full-iso 2>&1 | diff -q - $results/dates_full_iso || exit 1 # Paths and directories From 3251378e91a52616bbbd17958ce19ce9e257db71 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Thu, 6 Jul 2017 00:39:54 +0100 Subject: [PATCH 10/11] Add iso time style --- src/options/view.rs | 7 +++--- src/output/time.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++ xtests/dates_iso | 3 +++ xtests/run.sh | 1 + 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 xtests/dates_iso diff --git a/src/options/view.rs b/src/options/view.rs index caec38b4..2f1c7fa7 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -240,15 +240,16 @@ impl TimeFormat { /// Determine how time should be formatted in timestamp columns. fn deduce(matches: &getopts::Matches) -> Result { - pub use output::time::{DefaultFormat}; - const STYLES: &[&str] = &["default", "long-iso", "full-iso"]; + pub use output::time::{DefaultFormat, ISOFormat}; + const STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"]; if let Some(word) = matches.opt_str("time-style") { match &*word { "default" => Ok(TimeFormat::DefaultFormat(DefaultFormat::new())), + "iso" => Ok(TimeFormat::ISOFormat(ISOFormat::new())), "long-iso" => Ok(TimeFormat::LongISO), "full-iso" => Ok(TimeFormat::FullISO), - otherwise => Err(Misfire::bad_argument("time-style", otherwise, STYLES)) + otherwise => Err(Misfire::bad_argument("time-style", otherwise, STYLES)), } } else { diff --git a/src/output/time.rs b/src/output/time.rs index 49196398..55630205 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -7,6 +7,7 @@ use fs::fields::Time; pub enum TimeFormat { DefaultFormat(DefaultFormat), + ISOFormat(ISOFormat), LongISO, FullISO, } @@ -15,6 +16,7 @@ impl TimeFormat { pub fn format_local(&self, time: Time) -> String { match *self { TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time), + TimeFormat::ISOFormat(ref iso) => iso.format_local(time), TimeFormat::LongISO => long_local(time), TimeFormat::FullISO => full_local(time), } @@ -23,6 +25,7 @@ impl TimeFormat { pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { match *self { TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone), + TimeFormat::ISOFormat(ref iso) => iso.format_zoned(time, zone), TimeFormat::LongISO => long_zoned(time, zone), TimeFormat::FullISO => full_zoned(time, zone), } @@ -141,3 +144,54 @@ fn full_zoned(time: Time, zone: &TimeZone) -> String { date.hour(), date.minute(), date.second(), time.nanoseconds, offset.hours(), offset.minutes().abs()) } + + + +#[derive(Debug, Clone)] +pub struct ISOFormat { + + /// The year of the current time. This gets used to determine which date + /// format to use. + pub current_year: i64, +} + +impl ISOFormat { + pub fn new() -> Self { + let current_year = LocalDateTime::now().year(); + ISOFormat { current_year } + } + + fn is_recent(&self, date: LocalDateTime) -> bool { + date.year() == self.current_year + } + + #[allow(trivial_numeric_casts)] + fn format_local(&self, time: Time) -> String { + let date = LocalDateTime::at(time.seconds as i64); + + if self.is_recent(date) { + format!("{:04}-{:02}-{:02}", + date.year(), date.month() as usize, date.day()) + } + else { + format!("{:02}-{:02} {:02}:{:02}", + date.month() as usize, date.day(), + date.hour(), date.minute()) + } + } + + #[allow(trivial_numeric_casts)] + fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64)); + + if self.is_recent(date) { + format!("{:04}-{:02}-{:02}", + date.year(), date.month() as usize, date.day()) + } + else { + format!("{:02}-{:02} {:02}:{:02}", + date.month() as usize, date.day(), + date.hour(), date.minute()) + } + } +} diff --git a/xtests/dates_iso b/xtests/dates_iso new file mode 100644 index 00000000..27670195 --- /dev/null +++ b/xtests/dates_iso @@ -0,0 +1,3 @@ +.rw-rw-r-- 0 cassowary 06-15 23:14 peach +.rw-rw-r-- 0 cassowary 03-03 00:00 pear +.rw-rw-r-- 0 cassowary 07-22 10:38 plum diff --git a/xtests/run.sh b/xtests/run.sh index da06d331..36046848 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -112,6 +112,7 @@ $exa $testcases/dates -lh --accessed --sort=accessed 2>&1 | diff -q - $results/d $exa $testcases/dates -lh --sort=modified 2>&1 | diff -q - $results/dates_modified || exit 1 $exa $testcases/dates -l --time-style=long-iso 2>&1 | diff -q - $results/dates_long_iso || exit 1 $exa $testcases/dates -l --time-style=full-iso 2>&1 | diff -q - $results/dates_full_iso || exit 1 +$exa $testcases/dates -l --time-style=iso 2>&1 | diff -q - $results/dates_iso || exit 1 # Paths and directories From 6afde85e18fc5e20d8deb92d96a7d2a93f975671 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Thu, 6 Jul 2017 00:52:27 +0100 Subject: [PATCH 11/11] Document --time-style, and completions --- README.md | 2 ++ contrib/completions.bash | 5 +++++ contrib/completions.fish | 10 ++++++++-- contrib/completions.zsh | 1 + contrib/man/exa.1 | 5 +++++ src/options/help.rs | 3 ++- xtests/help | 1 + xtests/help_long | 1 + 8 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 312045d4..2ad0b6e2 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,12 @@ These options are available when running with --long (`-l`): - **-U**, **--created**: use the created timestamp field - **-@**, **--extended**: list each file's extended attributes and sizes - **--git**: list each file's Git status, if tracked +- **--time-style**: how to format timestamps - Valid **--color** options are **always**, **automatic**, and **never**. - Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter are case-sensitive. - Valid time fields are **modified**, **accessed**, and **created**. +- Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**. ## Installation diff --git a/contrib/completions.bash b/contrib/completions.bash index 47ea47f2..9a54c23b 100644 --- a/contrib/completions.bash +++ b/contrib/completions.bash @@ -22,6 +22,11 @@ _exa() COMPREPLY=( $( compgen -W 'accessed modified created --' -- $cur ) ) return ;; + + --time-style) + COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- $cur ) ) + return + ;; esac case "$cur" in diff --git a/contrib/completions.fish b/contrib/completions.fish index 9f0d6287..5622494c 100644 --- a/contrib/completions.fish +++ b/contrib/completions.fish @@ -55,8 +55,14 @@ complete -c exa -s 't' -l 'time' -x -d "Which timestamp field to list" -a " created\t'Display created time' modified\t'Display modified time' " -complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field" -complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field" +complete -c exa -s 'u' -l 'accessed' -d "Use the accessed timestamp field" +complete -c exa -s 'U' -l 'created' -d "Use the created timestamp field" +complete -c exa -l 'time-style' -x -d "How to format timestamps" -a " + default\t'Use the default time style' + iso\t'Display brief ISO timestamps' + long-iso\t'Display longer ISO timestaps, up to the minute' + full-iso\t'Display full ISO timestamps, up to the nanosecond' +" # Optional extras complete -c exa -s 'g' -l 'git' -d "List each file's Git status, if tracked" diff --git a/contrib/completions.zsh b/contrib/completions.zsh index e1287758..0f5f04d9 100644 --- a/contrib/completions.zsh +++ b/contrib/completions.zsh @@ -29,6 +29,7 @@ __exa() { {-m,--modified}"[Use the modified timestamp field]" \ {-S,--blocks}"[List each file's number of filesystem blocks]" \ {-t,--time}"[Which time field to show]:(time field):(accessed created modified)" \ + --time-style"[How to format timestamps]:(time style):(default iso long-iso full-iso)" \ {-u,--accessed}"[Use the accessed timestamp field]" \ {-U,--created}"[Use the created timestamp field]" \ --git"[List each file's Git status, if tracked]" \ diff --git a/contrib/man/exa.1 b/contrib/man/exa.1 index 33b1a8b1..8ef83b9c 100644 --- a/contrib/man/exa.1 +++ b/contrib/man/exa.1 @@ -145,6 +145,11 @@ which timestamp field to list (modified, accessed, created) .RS .RE .TP +.B \-\-time\-style=\f[I]STYLE\f[] +how to format timestamps (default, iso, long-iso, full-iso) +.RS +.RE +.TP .B \-u, \-\-accessed use the accessed timestamp field .RS diff --git a/src/options/help.rs b/src/options/help.rs index a391b9c5..3d5e8719 100644 --- a/src/options/help.rs +++ b/src/options/help.rs @@ -40,7 +40,8 @@ LONG VIEW OPTIONS -S, --blocks show number of file system blocks -t, --time FIELD which timestamp field to list (modified, accessed, created) -u, --accessed use the accessed timestamp field - -U, --created use the created timestamp field"##; + -U, --created use the created timestamp field + --time-style how to format timestamps (default, iso, long-iso, full-iso)"##; static GIT_HELP: &str = r##" --git list each file's Git status, if tracked"##; static EXTENDED_HELP: &str = r##" -@, --extended list each file's extended attributes and sizes"##; diff --git a/xtests/help b/xtests/help index 3e2f39a9..a0ddf712 100644 --- a/xtests/help +++ b/xtests/help @@ -38,5 +38,6 @@ LONG VIEW OPTIONS -t, --time FIELD which timestamp field to list (modified, accessed, created) -u, --accessed use the accessed timestamp field -U, --created use the created timestamp field + --time-style how to format timestamps (default, iso, long-iso, full-iso) --git list each file's Git status, if tracked -@, --extended list each file's extended attributes and sizes diff --git a/xtests/help_long b/xtests/help_long index e952ea04..343de422 100644 --- a/xtests/help_long +++ b/xtests/help_long @@ -14,5 +14,6 @@ LONG VIEW OPTIONS -t, --time FIELD which timestamp field to list (modified, accessed, created) -u, --accessed use the accessed timestamp field -U, --created use the created timestamp field + --time-style how to format timestamps (default, iso, long-iso, full-iso) --git list each file's Git status, if tracked -@, --extended list each file's extended attributes and sizes