diff --git a/README.md b/README.md index 47aed380..99723f6b 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,8 @@ These options are available when running with --long (`-l`): - **--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 sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **newest**, while its reverse has the aliases **age** and **oldest**. -- Valid time fields are **modified**, **accessed**, and **created**. +- Valid sort fields are **accessed**, **changed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **newest**, while its reverse has the aliases **age** and **oldest**. +- Valid time fields are **modified**, **changed**, **accessed**, and **created**. - Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**. diff --git a/contrib/completions.bash b/contrib/completions.bash index c058ba43..4a370f37 100644 --- a/contrib/completions.bash +++ b/contrib/completions.bash @@ -14,12 +14,12 @@ _exa() ;; -s|--sort) - COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension date time modified accessed created type inode oldest newest age none --' -- "$cur" ) ) + COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension date time modified changed accessed created type inode oldest newest age none --' -- "$cur" ) ) return ;; -t|--time) - COMPREPLY=( $( compgen -W 'accessed modified created --' -- $cur ) ) + COMPREPLY=( $( compgen -W 'modified changed accessed created --' -- $cur ) ) return ;; diff --git a/contrib/completions.fish b/contrib/completions.fish index b8bf2e46..6a31c886 100644 --- a/contrib/completions.fish +++ b/contrib/completions.fish @@ -25,6 +25,7 @@ complete -c exa -s 'r' -l 'reverse' -d "Reverse the sort order" complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a " accessed\t'Sort by file accessed time' age\t'Sort by file modified time (newest first)' + changed\t'Sort by changed time' created\t'Sort by file modified time' date\t'Sort by file modified time' ext\t'Sort by file extension' @@ -54,13 +55,15 @@ complete -c exa -s 'g' -l 'group' -d "List each file's group" complete -c exa -s 'h' -l 'header' -d "Add a header row to each column" complete -c exa -s 'h' -l 'links' -d "List each file's number of hard links" complete -c exa -s 'g' -l 'group' -d "List each file's inode number" -complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field" complete -c exa -s 'S' -l 'blocks' -d "List each file's number of filesystem blocks" complete -c exa -s 't' -l 'time' -x -d "Which timestamp field to list" -a " + modified\t'Display modified time' + changed\t'Display changed time' accessed\t'Display accessed time' created\t'Display created time' - modified\t'Display modified time' " +complete -c exa -s 'm' -l 'modified' -d "Use the modified timestamp field" +complete -c exa -l 'changed' -d "Use the changed 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 " diff --git a/contrib/completions.zsh b/contrib/completions.zsh index 77a91c8e..e64f9fb4 100644 --- a/contrib/completions.zsh +++ b/contrib/completions.zsh @@ -27,7 +27,7 @@ __exa() { {-d,--list-dirs}"[List directories like regular files]" \ {-L,--level}"+[Limit the depth of recursion]" \ {-r,--reverse}"[Reverse the sort order]" \ - {-s,--sort}="[Which field to sort by]:(sort field):(accessed age created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \ + {-s,--sort}="[Which field to sort by]:(sort field):(accessed age changed created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \ {-I,--ignore-glob}"[Ignore files that match these glob patterns]" \ {-b,--binary}"[List file sizes with binary prefixes]" \ {-B,--bytes}"[List file sizes in bytes, without any prefixes]" \ @@ -37,7 +37,7 @@ __exa() { {-i,--inode}"[List each file's inode number]" \ {-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)" \ + {-t,--time}="[Which time field to show]:(time field):(accessed changed 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]" \ diff --git a/contrib/man/exa.1 b/contrib/man/exa.1 index 6f5fa562..33ff9688 100644 --- a/contrib/man/exa.1 +++ b/contrib/man/exa.1 @@ -1,5 +1,5 @@ .hy -.TH "exa" "1" "2017\-07\-07" "exa 0.7.0" "" +.TH "exa" "1" "2018\-12\-17" "exa 0.9.0" "" .SH NAME .PP exa \- a modern replacement for ls @@ -86,7 +86,7 @@ reverse the sort order .TP .B \-s, \-\-sort=\f[I]SORT_FIELD\f[] which field to sort by. -Valid fields are name, Name, extension, Extension, size, modified, accessed, created, inode, type, and none. +Valid fields are name, Name, extension, Extension, size, modified, changed, accessed, created, inode, type, and none. The modified field has the aliases date, time, and newest, and its reverse order has the aliases age and oldest. Fields starting with a capital letter will sort uppercase before lowercase: 'A' then 'B' then 'a' then 'b'. Fields starting with a lowercase letter will mix them: 'A' then 'a' then 'B' then 'b'. @@ -158,7 +158,7 @@ list each file\[aq]s number of file system blocks .RE .TP .B \-t, \-\-time=\f[I]WORD\f[] -which timestamp field to list (modified, accessed, created) +which timestamp field to list (modified, changed, accessed, created) .RS .RE .TP diff --git a/src/fs/file.rs b/src/fs/file.rs index 7580a71e..63b586eb 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -1,13 +1,15 @@ //! Files, and methods and fields to access their metadata. -use std::fs; +use std::fs::{self, metadata}; use std::io::Error as IOError; use std::io::Result as IOResult; use std::os::unix::fs::{MetadataExt, PermissionsExt, FileTypeExt}; use std::path::{Path, PathBuf}; +use std::time::{UNIX_EPOCH, Duration}; use fs::dir::Dir; use fs::fields as f; +use options::Misfire; /// A **File** is a wrapper around one of Rust's Path objects, along with @@ -325,27 +327,23 @@ impl<'dir> File<'dir> { } /// This file’s last modified timestamp. - pub fn modified_time(&self) -> f::Time { - f::Time { - seconds: self.metadata.mtime(), - nanoseconds: self.metadata.mtime_nsec() - } + pub fn modified_time(&self) -> Duration { + self.metadata.modified().unwrap().duration_since(UNIX_EPOCH).unwrap() } - /// This file’s created timestamp. - pub fn created_time(&self) -> f::Time { - f::Time { - seconds: self.metadata.ctime(), - nanoseconds: self.metadata.ctime_nsec() - } + /// This file’s last changed timestamp. + pub fn changed_time(&self) -> Duration { + Duration::new(self.metadata.ctime() as u64, self.metadata.ctime_nsec() as u32) } /// This file’s last accessed timestamp. - pub fn accessed_time(&self) -> f::Time { - f::Time { - seconds: self.metadata.atime(), - nanoseconds: self.metadata.atime_nsec() - } + pub fn accessed_time(&self) -> Duration { + self.metadata.accessed().unwrap().duration_since(UNIX_EPOCH).unwrap() + } + + /// This file’s created timestamp. + pub fn created_time(&self) -> Duration { + self.metadata.created().unwrap().duration_since(UNIX_EPOCH).unwrap() } /// This file’s ‘type’. @@ -462,6 +460,41 @@ impl<'dir> FileTarget<'dir> { } +pub enum PlatformMetadata { + ModifiedTime, + ChangedTime, + AccessedTime, + CreatedTime, +} + +impl PlatformMetadata { + pub fn check_supported(&self) -> Result<(), Misfire> { + use std::env::temp_dir; + let result = match self { + // Call the functions that return a Result to see if it works + PlatformMetadata::AccessedTime => metadata(temp_dir()).unwrap().accessed(), + PlatformMetadata::ModifiedTime => metadata(temp_dir()).unwrap().modified(), + PlatformMetadata::CreatedTime => metadata(temp_dir()).unwrap().created(), + // We use the Unix API so we know it’s not available elsewhere + PlatformMetadata::ChangedTime => { + if cfg!(target_family = "unix") { + return Ok(()) + } else { + return Err(Misfire::Unsupported( + // for consistency, this error message similar to the one Rust + // use when created time is not available + "status modified time is not available on this platform currently".to_string())); + } + }, + }; + match result { + Ok(_) => Ok(()), + Err(err) => Err(Misfire::Unsupported(err.to_string())) + } + } +} + + /// More readable aliases for the permission bits exposed by libc. #[allow(trivial_numeric_casts)] mod modes { diff --git a/src/fs/filter.rs b/src/fs/filter.rs index 82440ce9..9c865b58 100644 --- a/src/fs/filter.rs +++ b/src/fs/filter.rs @@ -173,13 +173,16 @@ pub enum SortField { /// http://unix.stackexchange.com/a/8842 AccessedDate, - /// The time the file was changed or created (the “ctime”). + /// The time the file was changed (the “ctime”). /// - /// Contrary to the name, this field is used to mark the time when a - /// file’s metadata changed -- its permissions, owners, or link count. + /// This field is used to mark the time when a file’s metadata + /// changed -- its permissions, owners, or link count. /// /// In original Unix, this was, however, meant as creation time. /// https://www.bell-labs.com/usr/dmr/www/cacm.html + ChangedDate, + + /// The time the file was created (the "btime" or "birthtime"). CreatedDate, /// The type of the file: directories, links, pipes, regular, files, etc. @@ -247,6 +250,7 @@ impl SortField { SortField::FileInode => a.metadata.ino().cmp(&b.metadata.ino()), SortField::ModifiedDate => a.modified_time().cmp(&b.modified_time()), SortField::AccessedDate => a.accessed_time().cmp(&b.accessed_time()), + SortField::ChangedDate => a.changed_time().cmp(&b.changed_time()), SortField::CreatedDate => a.created_time().cmp(&b.created_time()), SortField::ModifiedAge => b.modified_time().cmp(&a.modified_time()), // flip b and a diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 3275ccf3..f6ca48e2 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -2,7 +2,7 @@ mod dir; pub use self::dir::{Dir, DotFilter}; mod file; -pub use self::file::{File, FileTarget}; +pub use self::file::{File, FileTarget, PlatformMetadata}; pub mod feature; pub mod fields; diff --git a/src/options/filter.rs b/src/options/filter.rs index d301fd41..ab9d9c24 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -1,6 +1,6 @@ //! Parsing the options for `FileFilter`. -use fs::DotFilter; +use fs::{DotFilter, PlatformMetadata}; use fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore}; use options::{flags, Misfire}; @@ -35,62 +35,59 @@ impl SortField { None => return Ok(SortField::default()), }; - // The field is an OsStr, so can’t be matched. - if word == "name" || word == "filename" { - Ok(SortField::Name(SortCase::AaBbCc)) - } - else if word == "Name" || word == "Filename" { - Ok(SortField::Name(SortCase::ABCabc)) - } - else if word == ".name" || word == ".filename" { - Ok(SortField::NameMixHidden(SortCase::AaBbCc)) - } - else if word == ".Name" || word == ".Filename" { - Ok(SortField::NameMixHidden(SortCase::ABCabc)) - } - else if word == "size" || word == "filesize" { - Ok(SortField::Size) - } - else if word == "ext" || word == "extension" { - Ok(SortField::Extension(SortCase::AaBbCc)) - } - else if word == "Ext" || word == "Extension" { - Ok(SortField::Extension(SortCase::ABCabc)) - } - else if word == "date" || word == "time" || word == "mod" || word == "modified" || word == "new" || word == "newest" { + // Get String because we can’t match an OsStr + let word = match word.to_str() { + Some(ref w) => *w, + None => return Err(Misfire::BadArgument(&flags::SORT, word.into())) + }; + + let field = match word { + "name" | "filename" => SortField::Name(SortCase::AaBbCc), + "Name" | "Filename" => SortField::Name(SortCase::ABCabc), + ".name" | ".filename" => SortField::NameMixHidden(SortCase::AaBbCc), + ".Name" | ".Filename" => SortField::NameMixHidden(SortCase::ABCabc), + "size" | "filesize" => SortField::Size, + "ext" | "extension" => SortField::Extension(SortCase::AaBbCc), + "Ext" | "Extension" => SortField::Extension(SortCase::ABCabc), // “new” sorts oldest at the top and newest at the bottom; “old” // sorts newest at the top and oldest at the bottom. I think this // is the right way round to do this: “size” puts the smallest at // the top and the largest at the bottom, doesn’t it? - Ok(SortField::ModifiedDate) - } - else if word == "age" || word == "old" || word == "oldest" { + "date" | "time" | "mod" | "modified" | "new" | "newest" => SortField::ModifiedDate, // Similarly, “age” means that files with the least age (the // newest files) get sorted at the top, and files with the most // age (the oldest) at the bottom. - Ok(SortField::ModifiedAge) + "age" | "old" | "oldest" => SortField::ModifiedAge, + "ch" | "changed" => SortField::ChangedDate, + "acc" | "accessed" => SortField::AccessedDate, + "cr" | "created" => SortField::CreatedDate, + "inode" => SortField::FileInode, + "type" => SortField::FileType, + "none" => SortField::Unsorted, + _ => return Err(Misfire::BadArgument(&flags::SORT, word.into())) + }; + + match SortField::to_platform_metadata(field) { + Some(m) => match m.check_supported() { + Ok(_) => Ok(field), + Err(misfire) => Err(misfire), + }, + None => Ok(field), } - else if word == "acc" || word == "accessed" { - Ok(SortField::AccessedDate) - } - else if word == "cr" || word == "created" { - Ok(SortField::CreatedDate) - } - else if word == "inode" { - Ok(SortField::FileInode) - } - else if word == "type" { - Ok(SortField::FileType) - } - else if word == "none" { - Ok(SortField::Unsorted) - } - else { - Err(Misfire::BadArgument(&flags::SORT, word.into())) + } + + fn to_platform_metadata(field: Self) -> Option { + match field { + SortField::ModifiedDate => Some(PlatformMetadata::ModifiedTime), + SortField::ChangedDate => Some(PlatformMetadata::ChangedTime), + SortField::AccessedDate => Some(PlatformMetadata::AccessedTime), + SortField::CreatedDate => Some(PlatformMetadata::CreatedTime), + _ => None } } } + // I’ve gone back and forth between whether to sort case-sensitively or // insensitively by default. The default string sort in most programming // languages takes each character’s ASCII value into account, sorting @@ -227,7 +224,7 @@ mod test { test!(empty: SortField <- []; Both => Ok(SortField::default())); // Sort field arguments - test!(one_arg: SortField <- ["--sort=cr"]; Both => Ok(SortField::CreatedDate)); + test!(one_arg: SortField <- ["--sort=mod"]; Both => Ok(SortField::ModifiedDate)); test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size)); test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate)); test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc))); diff --git a/src/options/flags.rs b/src/options/flags.rs index 306a9463..113e5afc 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -32,7 +32,7 @@ pub static GIT_IGNORE: Arg = Arg { short: None, long: "git-ignore", t pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden }; pub static ONLY_DIRS: Arg = Arg { short: Some(b'D'), long: "only-dirs", takes_value: TakesValue::Forbidden }; const SORTS: Values = &[ "name", "Name", "size", "extension", - "Extension", "modified", "accessed", + "Extension", "modified", "changed", "accessed", "created", "inode", "type", "none" ]; // display options @@ -43,12 +43,13 @@ pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_ pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden }; pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden }; pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden }; +pub static CHANGED: Arg = Arg { short: None, long: "changed", takes_value: TakesValue::Forbidden }; pub static BLOCKS: Arg = Arg { short: Some(b'S'), long: "blocks", takes_value: TakesValue::Forbidden }; pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary(Some(TIMES)) }; pub static ACCESSED: Arg = Arg { short: Some(b'u'), long: "accessed", takes_value: TakesValue::Forbidden }; pub static CREATED: Arg = Arg { short: Some(b'U'), long: "created", takes_value: TakesValue::Forbidden }; pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) }; -const TIMES: Values = &["modified", "accessed", "created"]; +const TIMES: Values = &["modified", "changed", "accessed", "created"]; const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso"]; // optional feature options @@ -65,9 +66,8 @@ pub static ALL_ARGS: Args = Args(&[ &ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST, &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, - &BINARY, &BYTES, &GROUP, &HEADER, &INODE, &LINKS, &MODIFIED, &BLOCKS, - &TIME, &ACCESSED, &CREATED, &TIME_STYLE, + &BINARY, &BYTES, &GROUP, &HEADER, &INODE, &LINKS, &MODIFIED, &CHANGED, + &BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &GIT, &EXTENDED, ]); - diff --git a/src/options/misfire.rs b/src/options/misfire.rs index da9174ac..362bb2d0 100644 --- a/src/options/misfire.rs +++ b/src/options/misfire.rs @@ -19,6 +19,9 @@ pub enum Misfire { /// The user supplied an illegal choice to an Argument. BadArgument(&'static Arg, OsString), + /// The user supplied a set of options + Unsupported(String), + /// The user asked for help. This isn’t strictly an error, which is why /// this enum isn’t named Error! Help(HelpString), @@ -83,6 +86,7 @@ impl fmt::Display for Misfire { } }, InvalidOptions(ref e) => write!(f, "{}", e), + Unsupported(ref e) => write!(f, "{}", e), Help(ref text) => write!(f, "{}", text), Version(ref version) => write!(f, "{}", version), Conflict(ref a, ref b) => write!(f, "Option {} conflicts with option {}", a, b), diff --git a/src/options/view.rs b/src/options/view.rs index f45a7ed1..1cf2472a 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -6,6 +6,7 @@ use output::time::TimeFormat; use options::{flags, Misfire, Vars}; use options::parser::MatchedFlags; +use fs::PlatformMetadata; use fs::feature::xattr; @@ -296,38 +297,58 @@ impl TimeTypes { fn deduce(matches: &MatchedFlags) -> Result { let possible_word = matches.get(&flags::TIME)?; let modified = matches.has(&flags::MODIFIED)?; - let created = matches.has(&flags::CREATED)?; + let changed = matches.has(&flags::CHANGED)?; let accessed = matches.has(&flags::ACCESSED)?; + let created = matches.has(&flags::CREATED)?; - if let Some(word) = possible_word { + let time_types = if let Some(word) = possible_word { if modified { - Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME)) + return Err(Misfire::Useless(&flags::MODIFIED, true, &flags::TIME)); } - else if created { - Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME)) + else if changed { + return Err(Misfire::Useless(&flags::CHANGED, true, &flags::TIME)); } else if accessed { - Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME)) + return Err(Misfire::Useless(&flags::ACCESSED, true, &flags::TIME)); + } + else if created { + return Err(Misfire::Useless(&flags::CREATED, true, &flags::TIME)); } else if word == "mod" || word == "modified" { - Ok(TimeTypes { accessed: false, modified: true, created: false }) + TimeTypes { modified: true, changed: false, accessed: false, created: false } + } + else if word == "ch" || word == "changed" { + TimeTypes { modified: false, changed: true, accessed: false, created: false } } else if word == "acc" || word == "accessed" { - Ok(TimeTypes { accessed: true, modified: false, created: false }) + TimeTypes { modified: false, changed: false, accessed: true, created: false } } else if word == "cr" || word == "created" { - Ok(TimeTypes { accessed: false, modified: false, created: true }) + TimeTypes { modified: false, changed: false, accessed: false, created: true } } else { - Err(Misfire::BadArgument(&flags::TIME, word.into())) + return Err(Misfire::BadArgument(&flags::TIME, word.into())); } } - else if modified || created || accessed { - Ok(TimeTypes { accessed, modified, created }) + else if modified || changed || accessed || created { + TimeTypes { modified, changed, accessed, created } } else { - Ok(TimeTypes::default()) + TimeTypes::default() + }; + + let mut fields = vec![]; + if time_types.modified { fields.push(PlatformMetadata::ModifiedTime); } + if time_types.changed { fields.push(PlatformMetadata::ChangedTime); } + if time_types.accessed { fields.push(PlatformMetadata::AccessedTime); } + if time_types.created { fields.push(PlatformMetadata::CreatedTime); } + + for field in fields { + if let Err(misfire) = field.check_supported() { + return Err(misfire); + } } + Ok(time_types) } } @@ -358,7 +379,8 @@ mod test { use options::test::Strictnesses::*; static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, &flags::TIME_STYLE, - &flags::TIME, &flags::MODIFIED, &flags::CREATED, &flags::ACCESSED, + &flags::TIME, &flags::MODIFIED, &flags::CHANGED, + &flags::CREATED, &flags::ACCESSED, &flags::HEADER, &flags::GROUP, &flags::INODE, &flags::GIT, &flags::LINKS, &flags::BLOCKS, &flags::LONG, &flags::LEVEL, &flags::GRID, &flags::ACROSS, &flags::ONE_LINE ]; @@ -493,32 +515,47 @@ mod test { test!(empty: TimeTypes <- []; Both => Ok(TimeTypes::default())); // Modified - test!(modified: TimeTypes <- ["--modified"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false })); - test!(m: TimeTypes <- ["-m"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false })); - test!(time_mod: TimeTypes <- ["--time=modified"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false })); - test!(time_m: TimeTypes <- ["-tmod"]; Both => Ok(TimeTypes { accessed: false, modified: true, created: false })); + test!(modified: TimeTypes <- ["--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); + test!(m: TimeTypes <- ["-m"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); + test!(time_mod: TimeTypes <- ["--time=modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); + test!(time_m: TimeTypes <- ["-tmod"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); + + // Changed + #[cfg(target_family = "unix")] + test!(changed: TimeTypes <- ["--changed"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false })); + #[cfg(target_family = "unix")] + test!(time_ch: TimeTypes <- ["--time=changed"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false })); + #[cfg(target_family = "unix")] + test!(time_c: TimeTypes <- ["-t", "ch"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false })); // Accessed - test!(acc: TimeTypes <- ["--accessed"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false })); - test!(a: TimeTypes <- ["-u"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false })); - test!(time_acc: TimeTypes <- ["--time", "accessed"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false })); - test!(time_a: TimeTypes <- ["-t", "acc"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: false })); + test!(acc: TimeTypes <- ["--accessed"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false })); + test!(a: TimeTypes <- ["-u"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false })); + test!(time_acc: TimeTypes <- ["--time", "accessed"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false })); + test!(time_a: TimeTypes <- ["-t", "acc"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false })); // Created - test!(cr: TimeTypes <- ["--created"]; Both => Ok(TimeTypes { accessed: false, modified: false, created: true })); - test!(c: TimeTypes <- ["-U"]; Both => Ok(TimeTypes { accessed: false, modified: false, created: true })); - test!(time_cr: TimeTypes <- ["--time=created"]; Both => Ok(TimeTypes { accessed: false, modified: false, created: true })); - test!(time_c: TimeTypes <- ["-tcr"]; Both => Ok(TimeTypes { accessed: false, modified: false, created: true })); + #[cfg(not(target_os = "linux"))] + test!(cr: TimeTypes <- ["--created"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true })); + #[cfg(target_os = "linux")] + test!(cr: TimeTypes <- ["--created"]; Both => err Misfire::Unsupported("creation time is not available on this platform currently".to_string())); + #[cfg(not(target_os = "linux"))] + test!(c: TimeTypes <- ["-U"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true })); + #[cfg(not(target_os = "linux"))] + test!(time_cr: TimeTypes <- ["--time=created"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true })); + #[cfg(not(target_os = "linux"))] + test!(time_c: TimeTypes <- ["-tcr"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true })); // Multiples - test!(time_uu: TimeTypes <- ["-uU"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: true })); + test!(time_uu: TimeTypes <- ["-u", "--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: true, created: false })); + // Errors test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("tea"))); test!(time_ea: TimeTypes <- ["-tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("ea"))); // Overriding - test!(overridden: TimeTypes <- ["-tcr", "-tmod"]; Last => Ok(TimeTypes { accessed: false, modified: true, created: false })); + test!(overridden: TimeTypes <- ["-tcr", "-tmod"]; Last => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); test!(overridden_2: TimeTypes <- ["-tcr", "-tmod"]; Complain => err Misfire::Duplicate(Flag::Short(b't'), Flag::Short(b't'))); } diff --git a/src/output/render/mod.rs b/src/output/render/mod.rs index f641d77d..f6248af6 100644 --- a/src/output/render/mod.rs +++ b/src/output/render/mod.rs @@ -23,6 +23,7 @@ mod size; pub use self::size::Colours as SizeColours; mod times; +pub use self::times::Render as TimeRender; // times does too mod users; diff --git a/src/output/render/times.rs b/src/output/render/times.rs index 9f40b357..ce4a791c 100644 --- a/src/output/render/times.rs +++ b/src/output/render/times.rs @@ -1,13 +1,18 @@ use datetime::TimeZone; use ansi_term::Style; -use fs::fields as f; use output::cell::TextCell; use output::time::TimeFormat; -impl f::Time { - pub fn render(self, style: Style, +pub trait Render { + fn render(self, style: Style, + tz: &Option, + format: &TimeFormat) -> TextCell; +} + +impl Render for std::time::Duration { + fn render(self, style: Style, tz: &Option, format: &TimeFormat) -> TextCell { diff --git a/src/output/table.rs b/src/output/table.rs index 2f3e6d7e..b0b6b842 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -12,6 +12,7 @@ use users::UsersCache; use style::Colours; use output::cell::TextCell; +use output::render::TimeRender; use output::time::TimeFormat; use fs::{File, fields as f}; use fs::feature::git::GitCache; @@ -78,6 +79,10 @@ impl Columns { columns.push(Column::Timestamp(TimeType::Modified)); } + if self.time_types.changed { + columns.push(Column::Timestamp(TimeType::Changed)); + } + if self.time_types.created { columns.push(Column::Timestamp(TimeType::Created)); } @@ -175,14 +180,16 @@ impl Default for SizeFormat { /// across most (all?) operating systems. #[derive(PartialEq, Debug, Copy, Clone)] pub enum TimeType { + /// The file’s modified time (`st_mtime`). + Modified, + + /// The file’s changed time (`st_ctime`) + Changed, /// The file’s accessed time (`st_atime`). Accessed, - /// The file’s modified time (`st_mtime`). - Modified, - - /// The file’s creation time (`st_ctime`). + /// The file’s creation time (`btime` or `birthtime`). Created, } @@ -191,8 +198,9 @@ 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::Changed => "Date Changed", + TimeType::Accessed => "Date Accessed", TimeType::Created => "Date Created", } } @@ -206,8 +214,9 @@ impl TimeType { /// the time columns entirely (yet). #[derive(PartialEq, Debug, Copy, Clone)] pub struct TimeTypes { - pub accessed: bool, pub modified: bool, + pub changed: bool, + pub accessed: bool, pub created: bool, } @@ -216,7 +225,7 @@ 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 } + TimeTypes { modified: true, changed: false, accessed: false, created: false } } } @@ -342,6 +351,7 @@ impl<'a, 'f> Table<'a> { Column::GitStatus => self.git_status(file).render(self.colours), Column::Timestamp(Modified) => file.modified_time().render(self.colours.date, &self.env.tz, &self.time_format), + Column::Timestamp(Changed) => file.changed_time() .render(self.colours.date, &self.env.tz, &self.time_format), Column::Timestamp(Created) => file.created_time() .render(self.colours.date, &self.env.tz, &self.time_format), Column::Timestamp(Accessed) => file.accessed_time().render(self.colours.date, &self.env.tz, &self.time_format), } diff --git a/src/output/time.rs b/src/output/time.rs index 6412767c..cec1f344 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -1,12 +1,12 @@ //! Timestamp formatting. +use std::time::Duration; + use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece}; use datetime::fmt::DateFormat; use locale; use std::cmp; -use fs::fields::Time; - /// Every timestamp in exa needs to be rendered by a **time format**. /// Formatting times is tricky, because how a timestamp is rendered can @@ -51,7 +51,7 @@ pub enum TimeFormat { // timestamps are separate types. impl TimeFormat { - pub fn format_local(&self, time: Time) -> String { + pub fn format_local(&self, time: Duration) -> String { match *self { TimeFormat::DefaultFormat(ref fmt) => fmt.format_local(time), TimeFormat::ISOFormat(ref iso) => iso.format_local(time), @@ -60,7 +60,7 @@ impl TimeFormat { } } - pub fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { + pub fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String { match *self { TimeFormat::DefaultFormat(ref fmt) => fmt.format_zoned(time, zone), TimeFormat::ISOFormat(ref iso) => iso.format_zoned(time, zone), @@ -128,8 +128,8 @@ impl DefaultFormat { } #[allow(trivial_numeric_casts)] - fn format_local(&self, time: Time) -> String { - let date = LocalDateTime::at(time.seconds as i64); + fn format_local(&self, time: Duration) -> String { + let date = LocalDateTime::at(time.as_secs() as i64); if self.is_recent(date) { self.date_and_time.format(&date, &self.locale) @@ -140,8 +140,8 @@ impl DefaultFormat { } #[allow(trivial_numeric_casts)] - fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64)); + fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64)); if self.is_recent(date) { self.date_and_time.format(&date, &self.locale) @@ -154,16 +154,16 @@ impl DefaultFormat { #[allow(trivial_numeric_casts)] -fn long_local(time: Time) -> String { - let date = LocalDateTime::at(time.seconds as i64); +fn long_local(time: Duration) -> String { + let date = LocalDateTime::at(time.as_secs() 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)); +fn long_zoned(time: Duration, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64)); format!("{:04}-{:02}-{:02} {:02}:{:02}", date.year(), date.month() as usize, date.day(), date.hour(), date.minute()) @@ -171,23 +171,23 @@ fn long_zoned(time: Time, zone: &TimeZone) -> String { #[allow(trivial_numeric_casts)] -fn full_local(time: Time) -> String { - let date = LocalDateTime::at(time.seconds as i64); +fn full_local(time: Duration) -> String { + let date = LocalDateTime::at(time.as_secs() 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) + date.hour(), date.minute(), date.second(), time.subsec_nanos()) } #[allow(trivial_numeric_casts)] -fn full_zoned(time: Time, zone: &TimeZone) -> String { +fn full_zoned(time: Duration, zone: &TimeZone) -> String { use datetime::Offset; - let local = LocalDateTime::at(time.seconds as i64); + let local = LocalDateTime::at(time.as_secs() 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, + date.hour(), date.minute(), date.second(), time.subsec_nanos(), offset.hours(), offset.minutes().abs()) } @@ -214,8 +214,8 @@ impl ISOFormat { } #[allow(trivial_numeric_casts)] - fn format_local(&self, time: Time) -> String { - let date = LocalDateTime::at(time.seconds as i64); + fn format_local(&self, time: Duration) -> String { + let date = LocalDateTime::at(time.as_secs() as i64); if self.is_recent(date) { format!("{:02}-{:02} {:02}:{:02}", @@ -229,8 +229,8 @@ impl ISOFormat { } #[allow(trivial_numeric_casts)] - fn format_zoned(&self, time: Time, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(time.seconds as i64)); + fn format_zoned(&self, time: Duration, zone: &TimeZone) -> String { + let date = zone.to_zoned(LocalDateTime::at(time.as_secs() as i64)); if self.is_recent(date) { format!("{:02}-{:02} {:02}:{:02}",