From 1d950086bcebfe42672e19efa3fe7cf5956934e4 Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sat, 30 Apr 2022 14:38:45 +0200 Subject: [PATCH 1/7] Resolves #270: add support for --time-style=relative --- Cargo.lock | 11 +++++++++-- Cargo.toml | 1 + README.md | 2 +- completions/bash/exa | 2 +- completions/fish/exa.fish | 1 + completions/zsh/_exa | 2 +- man/exa.1.md | 2 +- src/options/flags.rs | 2 +- src/options/help.rs | 2 +- src/options/view.rs | 4 ++++ src/output/time.rs | 26 ++++++++++++++++++++++++-- xtests/input-options.toml | 2 +- xtests/outputs/help.ansitxt | 2 +- 13 files changed, 47 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ee181df..f1835651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" @@ -75,6 +75,7 @@ dependencies = [ "scoped_threadpool", "term_grid", "terminal_size", + "timeago", "unicode-width", "users", "zoneinfo_compiled", @@ -296,6 +297,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "timeago" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ec32dde57efb15c035ac074118d7f32820451395f28cb0524a01d4e94983b26" + [[package]] name = "tinyvec" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 10dea67c..df1456f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ number_prefix = "0.4" scoped_threadpool = "0.1" term_grid = "0.2.0" terminal_size = "0.1.16" +timeago = { version = "0.3.1", default-features = false } unicode-width = "0.1" zoneinfo_compiled = "0.5.1" diff --git a/README.md b/README.md index 4bf682c5..7c46b35e 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Some of the options accept parameters: - Valid **--color** options are **always**, **automatic**, and **never**. - 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**. +- Valid time styles are **default**, **iso**, **long-iso**, **full-iso**, and **relative**. --- diff --git a/completions/bash/exa b/completions/bash/exa index d0447278..e63a9874 100644 --- a/completions/bash/exa +++ b/completions/bash/exa @@ -29,7 +29,7 @@ _exa() ;; --time-style) - COMPREPLY=( $( compgen -W 'default iso long-iso full-iso --' -- "$cur" ) ) + COMPREPLY=( $( compgen -W 'default iso long-iso full-iso relative --' -- "$cur" ) ) return ;; esac diff --git a/completions/fish/exa.fish b/completions/fish/exa.fish index daf6a4be..4c2b9afe 100755 --- a/completions/fish/exa.fish +++ b/completions/fish/exa.fish @@ -79,6 +79,7 @@ complete -c exa -l 'time-style' -d "How to format timestamps" -x -a " iso\t'Display brief ISO timestamps' long-iso\t'Display longer ISO timestaps, up to the minute' full-iso\t'Display full ISO timestamps, up to the nanosecond' + relative\t'Display relative timestamps' " complete -c exa -l 'no-permissions' -d "Suppress the permissions field" complete -c exa -l 'octal-permissions' -d "List each file's permission in octal format" diff --git a/completions/zsh/_exa b/completions/zsh/_exa index b915a5d0..49f4fd3d 100644 --- a/completions/zsh/_exa +++ b/completions/zsh/_exa @@ -43,7 +43,7 @@ __exa() { {-n,--numeric}"[List numeric user and group IDs.]" \ {-S,--blocks}"[List each file's number of filesystem blocks]" \ {-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)" \ + --time-style="[How to format timestamps]:(time style):(default iso long-iso full-iso relative)" \ --no-permissions"[Suppress the permissions field]" \ --octal-permissions"[List each file's permission in octal format]" \ --no-filesize"[Suppress the filesize field]" \ diff --git a/man/exa.1.md b/man/exa.1.md index 7bafd3f7..bc04aeae 100644 --- a/man/exa.1.md +++ b/man/exa.1.md @@ -157,7 +157,7 @@ These options are available when running with `--long` (`-l`): `--time-style=STYLE` : How to format timestamps. -: Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, and ‘`full-iso`’. +: Valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, and ‘`relative`’. `-u`, `--accessed` : Use the accessed timestamp field. diff --git a/src/options/flags.rs b/src/options/flags.rs index 1761d66a..930b6308 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -52,7 +52,7 @@ pub static ACCESSED: Arg = Arg { short: Some(b'u'), long: "accessed", takes_ 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", "changed", "accessed", "created"]; -const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso"]; +const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso", "relative"]; // suppressing columns pub static NO_PERMISSIONS: Arg = Arg { short: None, long: "no-permissions", takes_value: TakesValue::Forbidden }; diff --git a/src/options/help.rs b/src/options/help.rs index 02993934..a8f0dd71 100644 --- a/src/options/help.rs +++ b/src/options/help.rs @@ -54,7 +54,7 @@ LONG VIEW OPTIONS -u, --accessed use the accessed timestamp field -U, --created use the created timestamp field --changed use the changed timestamp field - --time-style how to format timestamps (default, iso, long-iso, full-iso) + --time-style how to format timestamps (default, iso, long-iso, full-iso, relative) --no-permissions suppress the permissions field --octal-permissions list each file's permission in octal format --no-filesize suppress the filesize field diff --git a/src/options/view.rs b/src/options/view.rs index 078d4a6b..43e24da4 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -257,6 +257,9 @@ impl TimeFormat { if &word == "default" { Ok(Self::DefaultFormat) } + else if &word == "relative" { + Ok(Self::Relative) + } else if &word == "iso" { Ok(Self::ISOFormat) } @@ -464,6 +467,7 @@ mod test { // Individual settings test!(default: TimeFormat <- ["--time-style=default"], None; Both => like Ok(TimeFormat::DefaultFormat)); test!(iso: TimeFormat <- ["--time-style", "iso"], None; Both => like Ok(TimeFormat::ISOFormat)); + test!(relative: TimeFormat <- ["--time-style", "relative"], None; Both => like Ok(TimeFormat::Relative)); test!(long_iso: TimeFormat <- ["--time-style=long-iso"], None; Both => like Ok(TimeFormat::LongISO)); test!(full_iso: TimeFormat <- ["--time-style", "full-iso"], None; Both => like Ok(TimeFormat::FullISO)); diff --git a/src/output/time.rs b/src/output/time.rs index 081333f1..970a45bd 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -1,8 +1,10 @@ //! Timestamp formatting. -use std::time::{SystemTime, UNIX_EPOCH}; +use std::convert::TryInto; +use std::cmp::max; +use std::time::{SystemTime, UNIX_EPOCH, Duration}; -use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece}; +use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece, Instant}; use datetime::fmt::DateFormat; use lazy_static::lazy_static; @@ -46,6 +48,9 @@ pub enum TimeFormat { /// millisecond and includes its offset down to the minute. This too uses /// only numbers so doesn’t require any special consideration. FullISO, + + /// Use a relative but fixed width representation. + Relative, } // There are two different formatting functions because local and zoned @@ -58,6 +63,7 @@ impl TimeFormat { Self::ISOFormat => iso_local(time), Self::LongISO => long_local(time), Self::FullISO => full_local(time), + Self::Relative => relative(time), } } @@ -67,6 +73,7 @@ impl TimeFormat { Self::ISOFormat => iso_zoned(time, zone), Self::LongISO => long_zoned(time, zone), Self::FullISO => full_zoned(time, zone), + Self::Relative => relative(time), } } } @@ -113,6 +120,21 @@ fn long_zoned(time: SystemTime, zone: &TimeZone) -> String { date.hour(), date.minute()) } +#[allow(trivial_numeric_casts)] +fn relative(time: SystemTime) -> String { + format!( + "{:>13}", + timeago::Formatter::new().convert( + Duration::from_secs( + max(0, Instant::now().seconds() - systemtime_epoch(time)) + // this .unwrap is safe since the call above can never result in a + // value < 0 + .try_into().unwrap() + ) + ) + ) +} + #[allow(trivial_numeric_casts)] fn full_local(time: SystemTime) -> String { let date = LocalDateTime::at(systemtime_epoch(time)); diff --git a/xtests/input-options.toml b/xtests/input-options.toml index 6961a4e8..89e557e1 100644 --- a/xtests/input-options.toml +++ b/xtests/input-options.toml @@ -42,7 +42,7 @@ tags = [ 'options' ] name = "exa displays an error for option that takes the wrong parameter" shell = "exa -l --time-style=24" stdout = { empty = true } -stderr = { string = "Option --time-style has no \"24\" setting (choices: default, long-iso, full-iso, iso)" } +stderr = { string = "Option --time-style has no \"24\" setting (choices: default, long-iso, full-iso, iso, relative)" } status = 3 tags = [ 'options' ] diff --git a/xtests/outputs/help.ansitxt b/xtests/outputs/help.ansitxt index 4e8cf520..042a270d 100644 --- a/xtests/outputs/help.ansitxt +++ b/xtests/outputs/help.ansitxt @@ -46,7 +46,7 @@ LONG VIEW OPTIONS -u, --accessed use the accessed timestamp field -U, --created use the created timestamp field --changed use the changed timestamp field - --time-style how to format timestamps (default, iso, long-iso, full-iso) + --time-style how to format timestamps (default, iso, long-iso, full-iso, relative) --no-permissions suppress the permissions field --octal-permissions list each file's permission in octal format --no-filesize suppress the filesize field From 2e9080d8b1f0dc22e5066f782b5b86d7cffc59ee Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sat, 30 Apr 2022 16:03:49 +0200 Subject: [PATCH 2/7] correctly right-align timestamp column --- src/output/table.rs | 1 + src/output/time.rs | 15 ++++++--------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/output/table.rs b/src/output/table.rs index 06e13b9a..15cd730f 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -157,6 +157,7 @@ impl Column { Self::HardLinks | Self::Inode | Self::Blocks | + Self::Timestamp(_) | Self::GitStatus => Alignment::Right, _ => Alignment::Left, } diff --git a/src/output/time.rs b/src/output/time.rs index 970a45bd..fe71ba7e 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -122,15 +122,12 @@ fn long_zoned(time: SystemTime, zone: &TimeZone) -> String { #[allow(trivial_numeric_casts)] fn relative(time: SystemTime) -> String { - format!( - "{:>13}", - timeago::Formatter::new().convert( - Duration::from_secs( - max(0, Instant::now().seconds() - systemtime_epoch(time)) - // this .unwrap is safe since the call above can never result in a - // value < 0 - .try_into().unwrap() - ) + timeago::Formatter::new().convert( + Duration::from_secs( + max(0, Instant::now().seconds() - systemtime_epoch(time)) + // this .unwrap is safe since the call above can never result in a + // value < 0 + .try_into().unwrap() ) ) } From bb3eadb3be6462b7d1821ddffbd5142472b068b3 Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sat, 30 Apr 2022 16:25:38 +0200 Subject: [PATCH 3/7] remove "ago" from relative dates --- src/output/time.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/output/time.rs b/src/output/time.rs index fe71ba7e..4f569084 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -122,14 +122,16 @@ fn long_zoned(time: SystemTime, zone: &TimeZone) -> String { #[allow(trivial_numeric_casts)] fn relative(time: SystemTime) -> String { - timeago::Formatter::new().convert( - Duration::from_secs( - max(0, Instant::now().seconds() - systemtime_epoch(time)) - // this .unwrap is safe since the call above can never result in a - // value < 0 - .try_into().unwrap() + timeago::Formatter::new() + .ago("") + .convert( + Duration::from_secs( + max(0, Instant::now().seconds() - systemtime_epoch(time)) + // this .unwrap is safe since the call above can never result in a + // value < 0 + .try_into().unwrap() + ) ) - ) } #[allow(trivial_numeric_casts)] From 4911ac6714458bd1d87244405305a8d826dccce8 Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sat, 30 Apr 2022 19:47:26 +0200 Subject: [PATCH 4/7] add test for relative date format --- xtests/details-view-dates.toml | 8 ++++++++ xtests/outputs/dates_long_timestyle_relative.ansitxt | 3 +++ 2 files changed, 11 insertions(+) create mode 100644 xtests/outputs/dates_long_timestyle_relative.ansitxt diff --git a/xtests/details-view-dates.toml b/xtests/details-view-dates.toml index 30cefebf..e2324b7f 100644 --- a/xtests/details-view-dates.toml +++ b/xtests/details-view-dates.toml @@ -46,6 +46,14 @@ stderr = { empty = true } status = 0 tags = [ 'long', 'time-style' ] +[[cmd]] +name = "‘exa -l --time-style=relative’ produces a table using the relative date format" +shell = "exa -l --time-style=relative /testcases/dates" +stdout = { file = "outputs/dates_long_timestyle_relative.ansitxt" } +stderr = { empty = true } +status = 0 +tags = [ 'long', 'time-style' ] + [[cmd]] name = "‘exa -l --time-style=full-iso’ produces a table using the full-iso date format" shell = "exa -l --time-style=full-iso /testcases/dates" diff --git a/xtests/outputs/dates_long_timestyle_relative.ansitxt b/xtests/outputs/dates_long_timestyle_relative.ansitxt new file mode 100644 index 00000000..d4f972d1 --- /dev/null +++ b/xtests/outputs/dates_long_timestyle_relative.ansitxt @@ -0,0 +1,3 @@ +.rw-rw-r-- 0 cassowary 15 years peach +.rw-rw-r-- 0 cassowary 19 years pear +.rw-rw-r-- 0 cassowary 12 years plum From 1b54874ce88bc5277454f670009942645f0d55b0 Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sat, 30 Apr 2022 19:59:38 +0200 Subject: [PATCH 5/7] use match for timeformat-parsing --- src/options/error.rs | 2 +- src/options/view.rs | 25 ++++++++----------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/options/error.rs b/src/options/error.rs index 27212827..d7633f31 100644 --- a/src/options/error.rs +++ b/src/options/error.rs @@ -14,7 +14,7 @@ pub enum OptionsError { Parse(ParseError), /// The user supplied an illegal choice to an Argument. - BadArgument(&'static Arg, OsString), + BadArgument(&'static Arg, String), /// The user supplied a set of options that are unsupported Unsupported(String), diff --git a/src/options/view.rs b/src/options/view.rs index 43e24da4..1ec5e10e 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -254,23 +254,14 @@ impl TimeFormat { } }; - if &word == "default" { - Ok(Self::DefaultFormat) - } - else if &word == "relative" { - Ok(Self::Relative) - } - else if &word == "iso" { - Ok(Self::ISOFormat) - } - else if &word == "long-iso" { - Ok(Self::LongISO) - } - else if &word == "full-iso" { - Ok(Self::FullISO) - } - else { - Err(OptionsError::BadArgument(&flags::TIME_STYLE, word)) + let word = word.to_string_lossy(); + match word.as_ref() { + "default" => Ok(Self::DefaultFormat), + "relative" => Ok(Self::Relative), + "iso" => Ok(Self::ISOFormat), + "long-iso" => Ok(Self::LongISO), + "full-iso" => Ok(Self::FullISO), + _ => Err(OptionsError::BadArgument(&flags::TIME_STYLE, word.to_string())) } } } From 0bf3148db047dd6a49014e2c1d37d7cb368f45e4 Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sat, 30 Apr 2022 20:49:24 +0200 Subject: [PATCH 6/7] fix build --- src/options/error.rs | 2 +- src/options/view.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/options/error.rs b/src/options/error.rs index d7633f31..27212827 100644 --- a/src/options/error.rs +++ b/src/options/error.rs @@ -14,7 +14,7 @@ pub enum OptionsError { Parse(ParseError), /// The user supplied an illegal choice to an Argument. - BadArgument(&'static Arg, String), + BadArgument(&'static Arg, OsString), /// The user supplied a set of options that are unsupported Unsupported(String), diff --git a/src/options/view.rs b/src/options/view.rs index 1ec5e10e..53dc60f4 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -254,14 +254,13 @@ impl TimeFormat { } }; - let word = word.to_string_lossy(); - match word.as_ref() { + match word.to_string_lossy().as_ref() { "default" => Ok(Self::DefaultFormat), "relative" => Ok(Self::Relative), "iso" => Ok(Self::ISOFormat), "long-iso" => Ok(Self::LongISO), "full-iso" => Ok(Self::FullISO), - _ => Err(OptionsError::BadArgument(&flags::TIME_STYLE, word.to_string())) + _ => Err(OptionsError::BadArgument(&flags::TIME_STYLE, word)) } } } From 32b2c5b2f3c34211e0d3729bd3a925c4462c05b2 Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Mon, 2 May 2022 20:27:05 +0200 Subject: [PATCH 7/7] add test for far future dates --- xtests/details-view-dates.toml | 7 +++++++ xtests/outputs/far_dates_relative.ansitxt | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 xtests/outputs/far_dates_relative.ansitxt diff --git a/xtests/details-view-dates.toml b/xtests/details-view-dates.toml index e2324b7f..db1b6b88 100644 --- a/xtests/details-view-dates.toml +++ b/xtests/details-view-dates.toml @@ -35,6 +35,13 @@ stderr = { empty = true } status = 0 tags = [ 'long', 'time' ] +[[cmd]] +name = "‘exa -l --time-style=relative’ handles dates far past and future dates" +shell = "exa -l --time-style=relative /testcases/far-dates" +stdout = { file = "outputs/far_dates_relative.ansitxt" } +stderr = { empty = true } +status = 0 +tags = [ 'long', 'time' ] # alternate date formats diff --git a/xtests/outputs/far_dates_relative.ansitxt b/xtests/outputs/far_dates_relative.ansitxt new file mode 100644 index 00000000..7bea5c5e --- /dev/null +++ b/xtests/outputs/far_dates_relative.ansitxt @@ -0,0 +1,2 @@ +.rw-rw-r-- 0 vagrant now beyond-the-future +.rw-rw-r-- 0 vagrant now the-distant-past