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..e851405 100644 --- a/src/file.rs +++ b/src/file.rs @@ -10,14 +10,13 @@ use std::path::{Component, Path, PathBuf}; use unicode_width::UnicodeWidthStr; use dir::Dir; -use options::TimeType; use self::fields as f; -// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259 -// which is currently unstable and lacks vision for stabilization, -// see https://github.com/rust-lang/rust/issues/27712 +/// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259 +/// which is currently unstable and lacks vision for stabilization, +/// see https://github.com/rust-lang/rust/issues/27712 #[allow(dead_code)] mod modes { use std::os::unix::raw; @@ -281,15 +280,16 @@ impl<'dir> File<'dir> { } } - /// One of this file's timestamps, as a number in seconds. - pub fn timestamp(&self, time_type: TimeType) -> f::Time { - let time_in_seconds = match time_type { - TimeType::FileAccessed => self.metadata.atime(), - TimeType::FileModified => self.metadata.mtime(), - TimeType::FileCreated => self.metadata.ctime(), - }; + pub fn modified_time(&self) -> f::Time { + f::Time(self.metadata.mtime()) + } - f::Time(time_in_seconds) + pub fn created_time(&self) -> f::Time { + f::Time(self.metadata.ctime()) + } + + pub fn accessed_time(&self) -> f::Time { + f::Time(self.metadata.mtime()) } /// This file's 'type'. diff --git a/src/main.rs b/src/main.rs index 66bd552..8c4761a 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; @@ -43,10 +42,14 @@ struct Exa { } impl Exa { - fn run(&mut self, args_file_names: &[String]) { + fn run(&mut self, mut args_file_names: Vec) { let mut files = Vec::new(); let mut dirs = Vec::new(); + if args_file_names.is_empty() { + args_file_names.push(".".to_owned()); + } + for file_name in args_file_names.iter() { match File::from_path(Path::new(&file_name), None) { Err(e) => { @@ -99,8 +102,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; @@ -146,7 +149,7 @@ fn main() { match Options::getopts(&args) { Ok((options, paths)) => { let mut exa = Exa { options: options }; - exa.run(&paths); + exa.run(paths); }, Err(e) => { println!("{}", e); diff --git a/src/options.rs b/src/options.rs index bf2f4a3..b9828ec 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, } @@ -31,39 +36,45 @@ impl Options { #[allow(unused_results)] pub fn getopts(args: &[String]) -> Result<(Options, Vec), Misfire> { let mut opts = getopts::Options::new(); + + opts.optflag("v", "version", "display version of exa"); + opts.optflag("?", "help", "show list of command-line options"); + + // Display options opts.optflag("1", "oneline", "display one entry per line"); + opts.optflag("G", "grid", "display entries in a grid view (default)"); + opts.optflag("l", "long", "display extended details and attributes"); + opts.optflag("R", "recurse", "recurse into directories"); + opts.optflag("T", "tree", "recurse into subdirectories in a tree view"); + opts.optflag("x", "across", "sort multi-column view entries across"); + + // Filtering and sorting options + opts.optflag("", "group-directories-first", "list directories before other files"); opts.optflag("a", "all", "show dot-files"); + opts.optflag("d", "list-dirs", "list directories as regular files"); + opts.optflag("r", "reverse", "reverse order of files"); + opts.optopt ("s", "sort", "field to sort by", "WORD"); + + // Long view options opts.optflag("b", "binary", "use binary prefixes in file sizes"); opts.optflag("B", "bytes", "list file sizes in bytes, without prefixes"); - opts.optflag("d", "list-dirs", "list directories as regular files"); opts.optflag("g", "group", "show group as well as user"); - opts.optflag("G", "grid", "display entries in a grid view (default)"); - opts.optflag("", "group-directories-first", "list directories before other files"); opts.optflag("h", "header", "show a header row at the top"); opts.optflag("H", "links", "show number of hard links"); opts.optflag("i", "inode", "show each file's inode number"); - opts.optflag("l", "long", "display extended details and attributes"); opts.optopt ("L", "level", "maximum depth of recursion", "DEPTH"); opts.optflag("m", "modified", "display timestamp of most recent modification"); - opts.optflag("r", "reverse", "reverse order of files"); - opts.optflag("R", "recurse", "recurse into directories"); - opts.optopt ("s", "sort", "field to sort by", "WORD"); opts.optflag("S", "blocks", "show number of file system blocks"); opts.optopt ("t", "time", "which timestamp to show for a file", "WORD"); - opts.optflag("T", "tree", "recurse into subdirectories in a tree view"); opts.optflag("u", "accessed", "display timestamp of last access for a file"); opts.optflag("U", "created", "display timestamp of creation for a file"); - opts.optflag("x", "across", "sort multi-column view entries across"); - - opts.optflag("", "version", "display version of exa"); - opts.optflag("?", "help", "show list of command-line options"); if cfg!(feature="git") { opts.optflag("", "git", "show git status"); } if xattr::ENABLED { - opts.optflag("@", "extended", "display extended attribute keys and sizes in long (-l) output"); + opts.optflag("@", "extended", "display extended attribute keys and sizes"); } let matches = match opts.parse(args) { @@ -72,47 +83,32 @@ impl Options { }; if matches.opt_present("help") { - return Err(Misfire::Help(opts.usage("Usage:\n exa [options] [files...]"))); + let mut help_string = "Usage:\n exa [options] [files...]\n".to_owned(); + + if !matches.opt_present("long") { + help_string.push_str(OPTIONS); + } + + help_string.push_str(LONG_OPTIONS); + + if cfg!(feature="git") { + help_string.push_str(GIT_HELP); + help_string.push('\n'); + } + + if xattr::ENABLED { + help_string.push_str(EXTENDED_HELP); + help_string.push('\n'); + } + + return Err(Misfire::Help(help_string)); } else if matches.opt_present("version") { return Err(Misfire::Version); } - let sort_field = match matches.opt_str("sort") { - Some(word) => try!(SortField::from_word(word)), - None => SortField::default(), - }; - - let filter = FileFilter { - list_dirs_first: matches.opt_present("group-directories-first"), - reverse: matches.opt_present("reverse"), - show_invisibles: matches.opt_present("all"), - sort_field: sort_field, - }; - - let path_strs = if matches.free.is_empty() { - vec![ ".".to_string() ] - } - else { - matches.free.clone() - }; - - let dir_action = try!(DirAction::deduce(&matches)); - let view = try!(View::deduce(&matches, filter, dir_action)); - - Ok((Options { - dir_action: dir_action, - view: view, - filter: filter, - }, 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) + let options = try!(Options::deduce(&matches)); + Ok((options, matches.free)) } /// Whether the View specified in this set of options includes a Git @@ -127,140 +123,17 @@ impl Options { } } +impl OptionSet for Options { + fn deduce(matches: &getopts::Matches) -> Result { + let dir_action = try!(DirAction::deduce(&matches)); + let filter = try!(FileFilter::deduce(&matches)); + let view = try!(View::deduce(&matches, filter, dir_action)); -#[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), - } + Ok(Options { + dir_action: dir_action, + view: view, + filter: filter, + }) } } @@ -274,7 +147,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 +229,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 +262,137 @@ 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 OptionSet for FileFilter { + fn deduce(matches: &getopts::Matches) -> Result { + let sort_field = try!(SortField::deduce(&matches)); + + Ok(FileFilter { + list_dirs_first: matches.opt_present("group-directories-first"), + reverse: matches.opt_present("reverse"), + show_invisibles: matches.opt_present("all"), + sort_field: sort_field, + }) + } +} + +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 { + if let Some(word) = matches.opt_str("sort") { + 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(Misfire::bad_argument("sort", field)) + } + } + else { + Ok(SortField::default()) + } + } +} + + +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 +406,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 +435,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::bad_argument("time", otherwise)), } } else { @@ -484,11 +451,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 +530,106 @@ 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"), - }) +impl Misfire { + + /// The OS return code this misfire should signify. + pub fn error_code(&self) -> i32 { + if let Misfire::Help(_) = *self { 2 } + else { 3 } } - 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 + /// The Misfire that happens when an option gets given the wrong + /// argument. This has to use one of the `getopts` failure + /// variants--it’s meant to take just an option name, rather than an + /// option *and* an argument, but it works just as well. + pub fn bad_argument(option: &str, otherwise: &str) -> Misfire { + Misfire::InvalidOptions(getopts::Fail::UnrecognizedOption(format!("--{} {}", option, otherwise))) } } +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), + } + } +} + +static OPTIONS: &'static str = r##" +DISPLAY OPTIONS + -1, --oneline display one entry per line + -G, --grid display entries in a grid view (default) + -l, --long display extended details and attributes + -R, --recurse recurse into directories + -T, --tree recurse into subdirectories in a tree view + -x, --across sort multi-column view entries across + +FILTERING AND SORTING OPTIONS + -a, --all show dot-files + -d, --list-dirs list directories as regular files + -r, --reverse reverse order of files + -s, --sort WORD field to sort by + --group-directories-first list directories before other files +"##; + +static LONG_OPTIONS: &'static str = r##" +LONG VIEW OPTIONS + -b, --binary use binary prefixes in file sizes + -B, --bytes list file sizes in bytes, without prefixes + -g, --group show group as well as user + -h, --header show a header row at the top + -H, --links show number of hard links + -i, --inode show each file's inode number + -L, --level DEPTH maximum depth of recursion + -m, --modified display timestamp of most recent modification + -S, --blocks show number of file system blocks + -t, --time WORD which timestamp to show for a file + -u, --accessed display timestamp of last access for a file + -U, --created display timestamp of creation for a file +"##; + +static GIT_HELP: &'static str = r##" -@, --extended display extended attribute keys and sizes"##; +static EXTENDED_HELP: &'static str = r##" --git show git status for files"##; + + #[cfg(test)] mod test { use super::Options; @@ -679,7 +664,7 @@ mod test { #[test] fn no_args() { let args = Options::getopts(&[]).unwrap().1; - assert_eq!(args, vec![ ".".to_string() ]) + assert!(args.is_empty()); // Listing the `.` directory is done in main.rs } #[test] diff --git a/src/output/column.rs b/src/output/column.rs new file mode 100644 index 0000000..c2b8568 --- /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::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 } + } +} + + +#[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..7e9775f 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 @@ -486,16 +486,20 @@ impl Table where U: Users { } fn display(&mut self, file: &File, column: &Column, xattrs: bool) -> Cell { + use output::column::TimeType::*; + match *column { - Column::Permissions => self.render_permissions(file.permissions(), xattrs), - Column::FileSize(fmt) => self.render_size(file.size(), fmt), - Column::Timestamp(t) => self.render_time(file.timestamp(t)), - Column::HardLinks => self.render_links(file.links()), - Column::Inode => self.render_inode(file.inode()), - Column::Blocks => self.render_blocks(file.blocks()), - Column::User => self.render_user(file.user()), - Column::Group => self.render_group(file.group()), - Column::GitStatus => self.render_git_status(file.git_status()), + Column::Permissions => self.render_permissions(file.permissions(), xattrs), + Column::FileSize(fmt) => self.render_size(file.size(), fmt), + Column::Timestamp(Modified) => self.render_time(file.modified_time()), + Column::Timestamp(Created) => self.render_time(file.created_time()), + Column::Timestamp(Accessed) => self.render_time(file.accessed_time()), + Column::HardLinks => self.render_links(file.links()), + Column::Inode => self.render_inode(file.inode()), + Column::Blocks => self.render_blocks(file.blocks()), + Column::User => self.render_user(file.user()), + Column::Group => self.render_group(file.group()), + Column::GitStatus => self.render_git_status(file.git_status()), } } @@ -658,7 +662,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 +758,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() {