use super::locale::current_locale; use crate::color::{ColoredString, Colors, Elem}; use crate::flags::{DateFlag, Flags}; use chrono::{DateTime, Duration, Local}; use chrono_humanize::HumanTime; use std::fs::Metadata; use std::panic; use std::time::SystemTime; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Date { Date(DateTime), Invalid, } // Note that this is split from the From for Metadata so we can test this one (as we can't mock Metadata) impl From for Date { fn from(systime: SystemTime) -> Self { // FIXME: This should really involve a result, but there's upstream issues in chrono. See https://github.com/chronotope/chrono/issues/110 let res = panic::catch_unwind(|| systime.into()); res.map_or(Date::Invalid, Date::Date) } } impl From<&Metadata> for Date { fn from(meta: &Metadata) -> Self { meta.modified() .expect("failed to retrieve modified date") .into() } } impl Date { pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString { let now = Local::now(); let elem = match self { &Date::Date(modified) if modified > now - Duration::hours(1) => Elem::HourOld, &Date::Date(modified) if modified > now - Duration::days(1) => Elem::DayOld, &Date::Date(_) | Date::Invalid => Elem::Older, }; colors.colorize(self.date_string(flags), &elem) } fn date_string(&self, flags: &Flags) -> String { let locale = current_locale(); if let Date::Date(val) = self { match &flags.date { DateFlag::Date => val.format("%c").to_string(), DateFlag::Locale => val.format_localized("%c", locale).to_string(), DateFlag::Relative => HumanTime::from(*val - Local::now()).to_string(), DateFlag::Iso => { // 365.2425 * 24 * 60 * 60 = 31556952 seconds per year // 15778476 seconds are 6 months if *val > Local::now() - Duration::seconds(15_778_476) { val.format("%m-%d %R").to_string() } else { val.format("%F").to_string() } } DateFlag::Formatted(format) => val.format_localized(format, locale).to_string(), } } else { String::from('-') } } } #[cfg(test)] mod test { use super::Date; use crate::color::{Colors, ThemeOption}; use crate::flags::{DateFlag, Flags}; use crate::meta::locale::current_locale; use chrono::{DateTime, Duration, Local}; use crossterm::style::{Color, Stylize}; use std::io; use std::path::Path; use std::process::{Command, ExitStatus}; use std::{env, fs}; #[cfg(unix)] fn cross_platform_touch(path: &Path, date: &DateTime) -> io::Result { Command::new("touch") .arg("-t") .arg(date.format("%Y%m%d%H%M.%S").to_string()) .arg(path) .status() } #[cfg(windows)] fn cross_platform_touch(path: &Path, date: &DateTime) -> io::Result { use std::process::Stdio; let copy_success = Command::new("cmd") .arg("/C") .arg("copy") .arg("NUL") .arg(path) .stdout(Stdio::null()) // Windows doesn't have a quiet flag .status()? .success(); assert!(copy_success, "failed to create empty file"); Command::new("powershell") .arg("-Command") .arg(format!( r#"$(Get-Item {}).lastwritetime=$(Get-Date "{}")"#, path.display(), date.to_rfc3339() )) .status() } #[test] fn test_an_hour_old_file_color() { let mut file_path = env::temp_dir(); file_path.push("test_an_hour_old_file_color.tmp"); let creation_date = Local::now() - chrono::Duration::seconds(4); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags::default(); assert_eq!( creation_date .format("%c") .to_string() .with(Color::AnsiValue(40)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_a_day_old_file_color() { let mut file_path = env::temp_dir(); file_path.push("test_a_day_old_file_color.tmp"); let creation_date = Local::now() - chrono::Duration::hours(4); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags::default(); assert_eq!( creation_date .format("%c") .to_string() .with(Color::AnsiValue(42)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_a_several_days_old_file_color() { let mut file_path = env::temp_dir(); file_path.push("test_a_several_days_old_file_color.tmp"); let creation_date = Local::now() - chrono::Duration::days(2); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags::default(); assert_eq!( creation_date .format("%c") .to_string() .with(Color::AnsiValue(36)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_with_relative_date() { let mut file_path = env::temp_dir(); file_path.push("test_with_relative_date.tmp"); let creation_date = Local::now() - chrono::Duration::days(2); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Relative, ..Default::default() }; assert_eq!( "2 days ago".to_string().with(Color::AnsiValue(36)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_with_relative_date_now() { let mut file_path = env::temp_dir(); file_path.push("test_with_relative_date_now.tmp"); let creation_date = Local::now(); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Relative, ..Default::default() }; assert_eq!( "now".to_string().with(Color::AnsiValue(40)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_iso_format_now() { let mut file_path = env::temp_dir(); file_path.push("test_iso_format_now.tmp"); let creation_date = Local::now(); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Iso, ..Default::default() }; assert_eq!( creation_date .format("%m-%d %R") .to_string() .with(Color::AnsiValue(40)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_iso_format_year_old() { let mut file_path = env::temp_dir(); file_path.push("test_iso_format_year_old.tmp"); let creation_date = Local::now() - Duration::days(400); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Iso, ..Default::default() }; assert_eq!( creation_date .format("%F") .to_string() .with(Color::AnsiValue(36)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] fn test_locale_format_now() { let mut file_path = env::temp_dir(); file_path.push("test_locale_format_now.tmp"); let creation_date = Local::now(); let success = cross_platform_touch(&file_path, &creation_date) .unwrap() .success(); assert!(success, "failed to exec touch"); let colors = Colors::new(ThemeOption::Default); let date = Date::from(&file_path.metadata().unwrap()); let flags = Flags { date: DateFlag::Locale, ..Default::default() }; assert_eq!( creation_date .format_localized("%c", current_locale()) .to_string() .with(Color::AnsiValue(40)), date.render(&colors, &flags) ); fs::remove_file(file_path).unwrap(); } #[test] #[cfg(all(not(windows), target_arch = "x86_64"))] fn test_bad_date() { // 4437052 is the bad year taken from https://github.com/lsd-rs/lsd/issues/529 that we know is both // a) high enough to break chrono // b) not high enough to break SystemTime (as Duration::MAX would) let end_time = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::new(4437052 * 365 * 24 * 60 * 60, 0); let colors = Colors::new(ThemeOption::Default); let date = Date::from(end_time); let flags = Flags { date: DateFlag::Date, ..Default::default() }; assert_eq!( "-".to_string().with(Color::AnsiValue(36)), date.render(&colors, &flags) ); } }