From 10468797bb70f3d16c28127661bb978e5259407c Mon Sep 17 00:00:00 2001 From: Ben S Date: Sat, 14 Nov 2015 23:32:57 +0000 Subject: [PATCH] Move many Options structs to the output module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This cleans up the options module, moving the structs that were *only* in use for the columns view out of it. The new OptionSet trait is used to add the ‘deduce’ methods that used to be present on the values. --- src/column.rs | 92 ------- src/file.rs | 2 +- src/main.rs | 5 +- src/options.rs | 481 ++++++++++++++++--------------------- src/output/column.rs | 231 ++++++++++++++++++ src/output/details.rs | 11 +- src/output/grid_details.rs | 3 +- src/output/mod.rs | 2 + 8 files changed, 446 insertions(+), 381 deletions(-) delete mode 100644 src/column.rs create mode 100644 src/output/column.rs diff --git a/src/column.rs b/src/column.rs deleted file mode 100644 index 0da3651..0000000 --- a/src/column.rs +++ /dev/null @@ -1,92 +0,0 @@ -use ansi_term::Style; -use unicode_width::UnicodeWidthStr; - -use options::{SizeFormat, TimeType}; - - -#[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(_) => Alignment::Right, - Column::HardLinks => Alignment::Right, - Column::Inode => Alignment::Right, - Column::Blocks => Alignment::Right, - 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, Debug, Clone)] -pub struct Cell { - pub length: usize, - pub text: String, -} - -impl Cell { - pub fn empty() -> Cell { - Cell { - text: String::new(), - length: 0, - } - } - - pub fn paint(style: Style, string: &str) -> Cell { - Cell { - text: style.paint(string).to_string(), - length: UnicodeWidthStr::width(string), - } - } - - pub fn add_spaces(&mut self, count: usize) { - self.length += count; - for _ in 0 .. count { - self.text.push(' '); - } - } - - pub fn append(&mut self, other: &Cell) { - self.length += other.length; - self.text.push_str(&*other.text); - } -} diff --git a/src/file.rs b/src/file.rs index e52ebaa..a053521 100644 --- a/src/file.rs +++ b/src/file.rs @@ -10,7 +10,7 @@ use std::path::{Component, Path, PathBuf}; use unicode_width::UnicodeWidthStr; use dir::Dir; -use options::TimeType; +use output::column::TimeType; use self::fields as f; diff --git a/src/main.rs b/src/main.rs index 66bd552..cf01ee8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,6 @@ use file::File; use options::{Options, View}; mod colours; -mod column; mod dir; mod feature; mod file; @@ -99,8 +98,8 @@ impl Exa { } }; - self.options.filter_files(&mut children); - self.options.sort_files(&mut children); + self.options.filter.filter_files(&mut children); + self.options.filter.sort_files(&mut children); if let Some(recurse_opts) = self.options.dir_action.recurse_options() { let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1; diff --git a/src/options.rs b/src/options.rs index bf2f4a3..79e324f 100644 --- a/src/options.rs +++ b/src/options.rs @@ -7,21 +7,26 @@ use getopts; use natord; use colours::Colours; -use column::Column; -use column::Column::*; -use dir::Dir; use feature::xattr; use file::File; use output::{Grid, Details, GridDetails, Lines}; +use output::column::{Columns, TimeTypes, SizeFormat}; use term::dimensions; -/// The *Options* struct represents a parsed version of the user's -/// command-line options. +/// These **options** represent a parsed, error-checked versions of the +/// user's command-line options. #[derive(PartialEq, Debug, Copy, Clone)] pub struct Options { + + /// The action to perform when encountering a directory rather than a + /// regular file. pub dir_action: DirAction, + + /// How to sort and filter files before outputting them. pub filter: FileFilter, + + /// The type of output to use (lines, grid, or details). pub view: View, } @@ -107,14 +112,6 @@ impl Options { }, path_strs)) } - pub fn sort_files(&self, files: &mut Vec) { - self.filter.sort_files(files) - } - - pub fn filter_files(&self, files: &mut Vec) { - self.filter.filter_files(files) - } - /// Whether the View specified in this set of options includes a Git /// status column. It's only worth trying to discover a repository if the /// results will end up being displayed. @@ -128,143 +125,6 @@ impl Options { } -#[derive(Default, PartialEq, Debug, Copy, Clone)] -pub struct FileFilter { - list_dirs_first: bool, - reverse: bool, - show_invisibles: bool, - sort_field: SortField, -} - -impl FileFilter { - pub fn filter_files(&self, files: &mut Vec) { - if !self.show_invisibles { - files.retain(|f| !f.is_dotfile()); - } - } - - pub fn sort_files(&self, files: &mut Vec) { - files.sort_by(|a, b| self.compare_files(a, b)); - - if self.reverse { - files.reverse(); - } - - if self.list_dirs_first { - // This relies on the fact that sort_by is stable. - files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory())); - } - } - - pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering { - match self.sort_field { - SortField::Unsorted => cmp::Ordering::Equal, - SortField::Name => natord::compare(&*a.name, &*b.name), - SortField::Size => a.metadata.len().cmp(&b.metadata.len()), - SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), - SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()), - SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()), - SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()), - SortField::Extension => match a.ext.cmp(&b.ext) { - cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name), - order => order, - }, - } - } -} - -/// User-supplied field to sort by. -#[derive(PartialEq, Debug, Copy, Clone)] -pub enum SortField { - Unsorted, Name, Extension, Size, FileInode, - ModifiedDate, AccessedDate, CreatedDate, -} - -impl Default for SortField { - fn default() -> SortField { - SortField::Name - } -} - -impl SortField { - - /// Find which field to use based on a user-supplied word. - fn from_word(word: String) -> Result { - match &word[..] { - "name" | "filename" => Ok(SortField::Name), - "size" | "filesize" => Ok(SortField::Size), - "ext" | "extension" => Ok(SortField::Extension), - "mod" | "modified" => Ok(SortField::ModifiedDate), - "acc" | "accessed" => Ok(SortField::AccessedDate), - "cr" | "created" => Ok(SortField::CreatedDate), - "none" => Ok(SortField::Unsorted), - "inode" => Ok(SortField::FileInode), - field => Err(SortField::none(field)) - } - } - - /// How to display an error when the word didn't match with anything. - fn none(field: &str) -> Misfire { - Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field))) - } -} - - -/// One of these things could happen instead of listing files. -#[derive(PartialEq, Debug)] -pub enum Misfire { - - /// The getopts crate didn't like these arguments. - InvalidOptions(getopts::Fail), - - /// The user asked for help. This isn't strictly an error, which is why - /// this enum isn't named Error! - Help(String), - - /// The user wanted the version number. - Version, - - /// Two options were given that conflict with one another. - Conflict(&'static str, &'static str), - - /// An option was given that does nothing when another one either is or - /// isn't present. - Useless(&'static str, bool, &'static str), - - /// An option was given that does nothing when either of two other options - /// are not present. - Useless2(&'static str, &'static str, &'static str), - - /// A numeric option was given that failed to be parsed as a number. - FailedParse(ParseIntError), -} - -impl Misfire { - /// The OS return code this misfire should signify. - pub fn error_code(&self) -> i32 { - if let Misfire::Help(_) = *self { 2 } - else { 3 } - } -} - -impl fmt::Display for Misfire { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Misfire::*; - - match *self { - InvalidOptions(ref e) => write!(f, "{}", e), - Help(ref text) => write!(f, "{}", text), - Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")), - Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b), - Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b), - Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b), - Useless2(a, b1, b2) => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2), - FailedParse(ref e) => write!(f, "Failed to parse number: {}", e), - } - } -} - - #[derive(PartialEq, Debug, Copy, Clone)] pub enum View { Details(Details), @@ -274,7 +134,7 @@ pub enum View { } impl View { - pub fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result { + fn deduce(matches: &getopts::Matches, filter: FileFilter, dir_action: DirAction) -> Result { use self::Misfire::*; let long = || { @@ -356,8 +216,8 @@ impl View { } } else { - // If the terminal width couldn't be matched for some reason, such - // as the program's stdout being connected to a file, then + // If the terminal width couldn’t be matched for some reason, such + // as the program’s stdout being connected to a file, then // fallback to the lines view. let lines = Lines { colours: Colours::plain(), @@ -389,21 +249,135 @@ impl View { } -#[derive(PartialEq, Debug, Copy, Clone)] -pub enum SizeFormat { - DecimalBytes, - BinaryBytes, - JustBytes, +trait OptionSet: Sized { + fn deduce(matches: &getopts::Matches) -> Result; } -impl Default for SizeFormat { - fn default() -> SizeFormat { - SizeFormat::DecimalBytes +impl OptionSet for Columns { + fn deduce(matches: &getopts::Matches) -> Result { + Ok(Columns { + size_format: try!(SizeFormat::deduce(matches)), + time_types: try!(TimeTypes::deduce(matches)), + inode: matches.opt_present("inode"), + links: matches.opt_present("links"), + blocks: matches.opt_present("blocks"), + group: matches.opt_present("group"), + git: cfg!(feature="git") && matches.opt_present("git"), + }) } } -impl SizeFormat { - pub fn deduce(matches: &getopts::Matches) -> Result { + +/// The **file filter** processes a vector of files before outputting them, +/// filtering and sorting the files depending on the user’s command-line +/// flags. +#[derive(Default, PartialEq, Debug, Copy, Clone)] +pub struct FileFilter { + list_dirs_first: bool, + reverse: bool, + show_invisibles: bool, + sort_field: SortField, +} + +impl FileFilter { + + /// Remove every file in the given vector that does *not* pass the + /// filter predicate. + pub fn filter_files(&self, files: &mut Vec) { + if !self.show_invisibles { + files.retain(|f| !f.is_dotfile()); + } + } + + /// Sort the files in the given vector based on the sort field option. + pub fn sort_files(&self, files: &mut Vec) { + files.sort_by(|a, b| self.compare_files(a, b)); + + if self.reverse { + files.reverse(); + } + + if self.list_dirs_first { + // This relies on the fact that `sort_by` is stable. + files.sort_by(|a, b| b.is_directory().cmp(&a.is_directory())); + } + } + + pub fn compare_files(&self, a: &File, b: &File) -> cmp::Ordering { + match self.sort_field { + SortField::Unsorted => cmp::Ordering::Equal, + SortField::Name => natord::compare(&*a.name, &*b.name), + SortField::Size => a.metadata.len().cmp(&b.metadata.len()), + SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), + SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()), + SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()), + SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()), + SortField::Extension => match a.ext.cmp(&b.ext) { + cmp::Ordering::Equal => natord::compare(&*a.name, &*b.name), + order => order, + }, + } + } +} + + +/// User-supplied field to sort by. +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum SortField { + Unsorted, Name, Extension, Size, FileInode, + ModifiedDate, AccessedDate, CreatedDate, +} + +impl Default for SortField { + fn default() -> SortField { + SortField::Name + } +} + +impl OptionSet for SortField { + fn deduce(matches: &getopts::Matches) -> Result { + match matches.opt_str("sort") { + Some(word) => SortField::from_word(word), + None => Ok(SortField::default()), + } + } +} + +impl SortField { + + /// Find which field to use based on a user-supplied word. + fn from_word(word: String) -> Result { + match &word[..] { + "name" | "filename" => Ok(SortField::Name), + "size" | "filesize" => Ok(SortField::Size), + "ext" | "extension" => Ok(SortField::Extension), + "mod" | "modified" => Ok(SortField::ModifiedDate), + "acc" | "accessed" => Ok(SortField::AccessedDate), + "cr" | "created" => Ok(SortField::CreatedDate), + "none" => Ok(SortField::Unsorted), + "inode" => Ok(SortField::FileInode), + field => Err(SortField::none(field)) + } + } + + /// How to display an error when the word didn't match with anything. + fn none(field: &str) -> Misfire { + Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--sort {}", field))) + } +} + + +impl OptionSet for SizeFormat { + + /// Determine which file size to use in the file size column based on + /// the user’s options. + /// + /// The default mode is to use the decimal prefixes, as they are the + /// most commonly-understood, and don’t involve trying to parse large + /// strings of digits in your head. Changing the format to anything else + /// involves the `--binary` or `--bytes` flags, and these conflict with + /// each other. + fn deduce(matches: &getopts::Matches) -> Result { let binary = matches.opt_present("binary"); let bytes = matches.opt_present("bytes"); @@ -417,40 +391,18 @@ impl SizeFormat { } -#[derive(PartialEq, Debug, Copy, Clone)] -pub enum TimeType { - FileAccessed, - FileModified, - FileCreated, -} +impl OptionSet for TimeTypes { -impl TimeType { - pub fn header(&self) -> &'static str { - match *self { - TimeType::FileAccessed => "Date Accessed", - TimeType::FileModified => "Date Modified", - TimeType::FileCreated => "Date Created", - } - } -} - - -#[derive(PartialEq, Debug, Copy, Clone)] -pub struct TimeTypes { - accessed: bool, - modified: bool, - created: bool, -} - -impl Default for TimeTypes { - fn default() -> TimeTypes { - TimeTypes { accessed: false, modified: true, created: false } - } -} - -impl TimeTypes { - - /// Find which field to use based on a user-supplied word. + /// Determine which of a file’s time fields should be displayed for it + /// based on the user’s options. + /// + /// There are two separate ways to pick which fields to show: with a + /// flag (such as `--modified`) or with a parameter (such as + /// `--time=modified`). An error is signaled if both ways are used. + /// + /// It’s valid to show more than one column by passing in more than one + /// option, but passing *no* options means that the user just wants to + /// see the default set. fn deduce(matches: &getopts::Matches) -> Result { let possible_word = matches.opt_str("time"); let modified = matches.opt_present("modified"); @@ -468,11 +420,11 @@ impl TimeTypes { return Err(Misfire::Useless("accessed", true, "time")); } - match &word[..] { - "mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }), - "acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }), - "cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }), - field => Err(TimeTypes::none(field)), + match &*word { + "mod" | "modified" => Ok(TimeTypes { accessed: false, modified: true, created: false }), + "acc" | "accessed" => Ok(TimeTypes { accessed: true, modified: false, created: false }), + "cr" | "created" => Ok(TimeTypes { accessed: false, modified: false, created: true }), + otherwise => Err(Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", otherwise)))), } } else { @@ -484,11 +436,6 @@ impl TimeTypes { } } } - - /// How to display an error when the word didn't match with anything. - fn none(field: &str) -> Misfire { - Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--time {}", field))) - } } @@ -568,83 +515,61 @@ impl RecurseOptions { } -#[derive(PartialEq, Copy, Clone, Debug, Default)] -pub struct Columns { - size_format: SizeFormat, - time_types: TimeTypes, - inode: bool, - links: bool, - blocks: bool, - group: bool, - git: bool +/// One of these things could happen instead of listing files. +#[derive(PartialEq, Debug)] +pub enum Misfire { + + /// The getopts crate didn't like these arguments. + InvalidOptions(getopts::Fail), + + /// The user asked for help. This isn't strictly an error, which is why + /// this enum isn't named Error! + Help(String), + + /// The user wanted the version number. + Version, + + /// Two options were given that conflict with one another. + Conflict(&'static str, &'static str), + + /// An option was given that does nothing when another one either is or + /// isn't present. + Useless(&'static str, bool, &'static str), + + /// An option was given that does nothing when either of two other options + /// are not present. + Useless2(&'static str, &'static str, &'static str), + + /// A numeric option was given that failed to be parsed as a number. + FailedParse(ParseIntError), } -impl Columns { - pub fn deduce(matches: &getopts::Matches) -> Result { - Ok(Columns { - size_format: try!(SizeFormat::deduce(matches)), - time_types: try!(TimeTypes::deduce(matches)), - inode: matches.opt_present("inode"), - links: matches.opt_present("links"), - blocks: matches.opt_present("blocks"), - group: matches.opt_present("group"), - git: cfg!(feature="git") && matches.opt_present("git"), - }) - } - - 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(Inode); - } - - columns.push(Permissions); - - if self.links { - columns.push(HardLinks); - } - - columns.push(FileSize(self.size_format)); - - if self.blocks { - columns.push(Blocks); - } - - columns.push(User); - - if self.group { - columns.push(Group); - } - - if self.time_types.modified { - columns.push(Timestamp(TimeType::FileModified)); - } - - if self.time_types.created { - columns.push(Timestamp(TimeType::FileCreated)); - } - - if self.time_types.accessed { - columns.push(Timestamp(TimeType::FileAccessed)); - } - - if cfg!(feature="git") { - if let Some(d) = dir { - if self.should_scan_for_git() && d.has_git_repo() { - columns.push(GitStatus); - } - } - } - - columns +impl Misfire { + /// The OS return code this misfire should signify. + pub fn error_code(&self) -> i32 { + if let Misfire::Help(_) = *self { 2 } + else { 3 } } } +impl fmt::Display for Misfire { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Misfire::*; + + match *self { + InvalidOptions(ref e) => write!(f, "{}", e), + Help(ref text) => write!(f, "{}", text), + Version => write!(f, "exa {}", env!("CARGO_PKG_VERSION")), + Conflict(a, b) => write!(f, "Option --{} conflicts with option {}.", a, b), + Useless(a, false, b) => write!(f, "Option --{} is useless without option --{}.", a, b), + Useless(a, true, b) => write!(f, "Option --{} is useless given option --{}.", a, b), + Useless2(a, b1, b2) => write!(f, "Option --{} is useless without options --{} or --{}.", a, b1, b2), + FailedParse(ref e) => write!(f, "Failed to parse number: {}", e), + } + } +} + + #[cfg(test)] mod test { use super::Options; diff --git a/src/output/column.rs b/src/output/column.rs new file mode 100644 index 0000000..c0a87d5 --- /dev/null +++ b/src/output/column.rs @@ -0,0 +1,231 @@ +use ansi_term::Style; +use unicode_width::UnicodeWidthStr; + +use dir::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(_) => Alignment::Right, + Column::HardLinks => Alignment::Right, + Column::Inode => Alignment::Right, + Column::Blocks => Alignment::Right, + 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::FileModified)); + } + + if self.time_types.created { + columns.push(Column::Timestamp(TimeType::FileCreated)); + } + + if self.time_types.accessed { + columns.push(Column::Timestamp(TimeType::FileAccessed)); + } + + 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`). + FileAccessed, + + /// The file’s modified time (`st_mtime`). + FileModified, + + /// The file’s creation time (`st_ctime`). + FileCreated, +} + +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::FileAccessed => "Date Accessed", + TimeType::FileModified => "Date Modified", + TimeType::FileCreated => "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 } + } +} + + +#[derive(PartialEq, Debug, Clone)] +pub struct Cell { + pub length: usize, + pub text: String, +} + +impl Cell { + pub fn empty() -> Cell { + Cell { + text: String::new(), + length: 0, + } + } + + pub fn paint(style: Style, string: &str) -> Cell { + Cell { + text: style.paint(string).to_string(), + length: UnicodeWidthStr::width(string), + } + } + + pub fn add_spaces(&mut self, count: usize) { + self.length += count; + for _ in 0 .. count { + self.text.push(' '); + } + } + + pub fn append(&mut self, other: &Cell) { + self.length += other.length; + self.text.push_str(&*other.text); + } +} diff --git a/src/output/details.rs b/src/output/details.rs index c2bbfc0..95886ad 100644 --- a/src/output/details.rs +++ b/src/output/details.rs @@ -119,12 +119,12 @@ use std::ops::Add; use std::iter::repeat; use colours::Colours; -use column::{Alignment, Column, Cell}; use dir::Dir; use feature::xattr::{Attribute, FileAttributes}; use file::fields as f; use file::File; -use options::{Columns, FileFilter, RecurseOptions, SizeFormat}; +use options::{FileFilter, RecurseOptions}; +use output::column::{Alignment, Column, Columns, Cell, SizeFormat}; use ansi_term::{ANSIString, ANSIStrings, Style}; @@ -153,7 +153,7 @@ use super::filename; /// /// Almost all the heavy lifting is done in a Table object, which handles the /// columns for each row. -#[derive(PartialEq, Debug, Copy, Clone, Default)] +#[derive(PartialEq, Debug, Copy, Clone)] pub struct Details { /// A Columns object that says which columns should be included in the @@ -658,7 +658,7 @@ impl Table where U: Users { .map(|n| self.rows.iter().map(|row| row.column_width(n)).max().unwrap_or(0)) .collect(); - let total_width: usize = self.columns.len() + column_widths.iter().fold(0,Add::add); + let total_width: usize = self.columns.len() + column_widths.iter().fold(0, Add::add); for row in self.rows.iter() { let mut cell = Cell::empty(); @@ -754,8 +754,7 @@ pub mod test { pub use super::Table; pub use file::File; pub use file::fields as f; - - pub use column::{Cell, Column}; + pub use output::column::{Cell, Column}; pub use users::{User, Group, uid_t, gid_t}; pub use users::mock::MockUsers; diff --git a/src/output/grid_details.rs b/src/output/grid_details.rs index bca3c8b..e482464 100644 --- a/src/output/grid_details.rs +++ b/src/output/grid_details.rs @@ -3,10 +3,11 @@ use std::iter::repeat; use users::OSUsers; use term_grid as grid; -use column::{Column, Cell}; use dir::Dir; use feature::xattr::FileAttributes; use file::File; + +use output::column::{Column, Cell}; use output::details::{Details, Table}; use output::grid::Grid; diff --git a/src/output/mod.rs b/src/output/mod.rs index 5a3fb31..c875cde 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -13,6 +13,8 @@ mod grid; pub mod details; mod lines; mod grid_details; +pub mod column; + pub fn filename(file: &File, colours: &Colours, links: bool) -> String { if links && file.is_link() {