diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e2e0b444..865ce2a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for localization from [scarf](https://github.com/scarf005) - Add icons for cjs, cts and mts from [Han Yeong-woo](https://github.com/nix6839) - Fix obsolete Nerd Font icons from [Han Yeong-woo](https://github.com/nix6839) +- Add support to format dates for recent files differently. If `'+'` contains a second format it is applied to files from the last six months. The formats are separated by new-line, similar to `TIME_STYLE` in GNU ls. ### Fixed - Do not quote filename when piping into another program from [TeamTamoad](https://github.com/TeamTamoad) diff --git a/README.md b/README.md index 7c21b7b3a..b45cb65d9 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ color: # When "classic" is set, this is set to "date". # Possible values: date, relative, '+' # `date_format` will be a `strftime` formatted value. e.g. `date: '+%d %b %y %X'` will give you a date like this: 17 Jun 21 20:14:55 +# `date_format` can contain a second format after a new-line. The second format is used to format dates for files from the last six months. + date: date # == Dereference == diff --git a/src/app.rs b/src/app.rs index 340abb1b5..e12bb817b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -188,49 +188,57 @@ fn validate_date_argument(arg: &str) -> Result { } pub fn validate_time_format(formatter: &str) -> Result { - let mut chars = formatter.chars(); - loop { - match chars.next() { - Some('%') => match chars.next() { - Some('.') => match chars.next() { - Some('f') => (), - Some(n @ ('3' | '6' | '9')) => match chars.next() { + let vec: Vec<&str> = formatter.split('\n').collect(); + + if vec.len() > 2 { + return Err("invalid date format, cannot contain more than two entries".to_owned()); + } + + for s in vec { + let mut chars = s.chars(); + loop { + match chars.next() { + Some('%') => match chars.next() { + Some('.') => match chars.next() { Some('f') => (), - Some(c) => return Err(format!("invalid format specifier: %.{}{}", n, c)), + Some(n @ ('3' | '6' | '9')) => match chars.next() { + Some('f') => (), + Some(c) => return Err(format!("invalid format specifier: %.{}{}", n, c)), + None => return Err("missing format specifier".to_owned()), + }, + Some(c) => return Err(format!("invalid format specifier: %.{}", c)), + None => return Err("missing format specifier".to_owned()), + }, + Some(n @ (':' | '#')) => match chars.next() { + Some('z') => (), + Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), + None => return Err("missing format specifier".to_owned()), + }, + Some(n @ ('-' | '_' | '0')) => match chars.next() { + Some( + 'C' | 'd' | 'e' | 'f' | 'G' | 'g' | 'H' | 'I' | 'j' | 'k' | 'l' | 'M' + | 'm' | 'S' | 's' | 'U' | 'u' | 'V' | 'W' | 'w' | 'Y' | 'y', + ) => (), + Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), None => return Err("missing format specifier".to_owned()), }, - Some(c) => return Err(format!("invalid format specifier: %.{}", c)), - None => return Err("missing format specifier".to_owned()), - }, - Some(n @ (':' | '#')) => match chars.next() { - Some('z') => (), - Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), - None => return Err("missing format specifier".to_owned()), - }, - Some(n @ ('-' | '_' | '0')) => match chars.next() { Some( - 'C' | 'd' | 'e' | 'f' | 'G' | 'g' | 'H' | 'I' | 'j' | 'k' | 'l' | 'M' | 'm' - | 'S' | 's' | 'U' | 'u' | 'V' | 'W' | 'w' | 'Y' | 'y', + 'A' | 'a' | 'B' | 'b' | 'C' | 'c' | 'D' | 'd' | 'e' | 'F' | 'f' | 'G' | 'g' + | 'H' | 'h' | 'I' | 'j' | 'k' | 'l' | 'M' | 'm' | 'n' | 'P' | 'p' | 'R' + | 'r' | 'S' | 's' | 'T' | 't' | 'U' | 'u' | 'V' | 'v' | 'W' | 'w' | 'X' + | 'x' | 'Y' | 'y' | 'Z' | 'z' | '+' | '%', ) => (), - Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), - None => return Err("missing format specifier".to_owned()), - }, - Some( - 'A' | 'a' | 'B' | 'b' | 'C' | 'c' | 'D' | 'd' | 'e' | 'F' | 'f' | 'G' | 'g' - | 'H' | 'h' | 'I' | 'j' | 'k' | 'l' | 'M' | 'm' | 'n' | 'P' | 'p' | 'R' | 'r' - | 'S' | 's' | 'T' | 't' | 'U' | 'u' | 'V' | 'v' | 'W' | 'w' | 'X' | 'x' | 'Y' - | 'y' | 'Z' | 'z' | '+' | '%', - ) => (), - Some(n @ ('3' | '6' | '9')) => match chars.next() { - Some('f') => (), - Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), + Some(n @ ('3' | '6' | '9')) => match chars.next() { + Some('f') => (), + Some(c) => return Err(format!("invalid format specifier: %{}{}", n, c)), + None => return Err("missing format specifier".to_owned()), + }, + Some(c) => return Err(format!("invalid format specifier: %{}", c)), None => return Err("missing format specifier".to_owned()), }, - Some(c) => return Err(format!("invalid format specifier: %{}", c)), - None => return Err("missing format specifier".to_owned()), - }, - None => break, - _ => continue, + None => break, + _ => continue, + } } } Ok(formatter.to_owned()) diff --git a/src/meta/date.rs b/src/meta/date.rs index 5675eacfc..fcc9f4660 100644 --- a/src/meta/date.rs +++ b/src/meta/date.rs @@ -58,7 +58,17 @@ impl Date { val.format("%F").to_string() } } - DateFlag::Formatted(format) => val.format_localized(format, locale).to_string(), + DateFlag::Formatted(format) => { + let vec: Vec<&str> = format.split('\n').collect(); + + if vec.len() == 1 { + val.format_localized(format, locale).to_string() + } else if *val > Local::now() - Duration::seconds(15_778_476) { + val.format_localized(vec[1], locale).to_string() + } else { + val.format_localized(vec[0], locale).to_string() + } + } } } else { String::from('-') @@ -309,6 +319,62 @@ mod test { fs::remove_file(file_path).unwrap(); } + #[test] + fn test_recent_format_now() { + let mut file_path = env::temp_dir(); + file_path.push("test_recent_format_now.tmp"); + + let creation_date = Local::now(); + let success = cross_platform_touch(&file_path, &creation_date) + .unwrap() + .success(); + assert_eq!(true, success, "failed to exec touch"); + + let colors = Colors::new(ThemeOption::Default); + let date = Date::from(&file_path.metadata().unwrap()); + + let mut flags = Flags::default(); + flags.date = DateFlag::Formatted(String::from("%F\n%H:%M")); + + assert_eq!( + creation_date + .format("%H:%M") + .to_string() + .with(Color::AnsiValue(40)), + date.render(&colors, &flags) + ); + + fs::remove_file(file_path).unwrap(); + } + + #[test] + fn test_recent_format_year_old() { + let mut file_path = env::temp_dir(); + file_path.push("test_recent_format_year_old.tmp"); + + let creation_date = Local::now() - Duration::days(400); + let success = cross_platform_touch(&file_path, &creation_date) + .unwrap() + .success(); + assert_eq!(true, success, "failed to exec touch"); + + let colors = Colors::new(ThemeOption::Default); + let date = Date::from(&file_path.metadata().unwrap()); + + let mut flags = Flags::default(); + flags.date = DateFlag::Formatted(String::from("%F\n%H:%M")); + + assert_eq!( + creation_date + .format("%F") + .to_string() + .with(Color::AnsiValue(36)), + date.render(&colors, &flags) + ); + + fs::remove_file(file_path).unwrap(); + } + #[test] #[cfg(all(not(windows), target_arch = "x86_64"))] fn test_bad_date() {