From f582bd7e8ac150c48058b454a9b29cf4dc57bccb Mon Sep 17 00:00:00 2001 From: ariasuni Date: Sat, 8 May 2021 23:04:13 +0200 Subject: [PATCH] Replace datetime and zoneinfo_compiled crates with chrono - Improve compatibility with other OSes, timezone are handled for us - Reduce significantly code handling and rendering time --- Cargo.lock | 82 +++++++------ Cargo.toml | 12 +- build.rs | 6 +- src/fs/file.rs | 36 ++---- src/output/render/times.rs | 24 ++-- src/output/table.rs | 51 ++------ src/output/time.rs | 235 ++++++++++++------------------------- 7 files changed, 153 insertions(+), 293 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ee181dfa..38da705d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,12 +23,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - [[package]] name = "cc" version = "1.0.67" @@ -45,15 +39,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "datetime" -version = "0.5.2" +name = "chrono" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c3f7a77f3e57fedf80e09136f2d8777ebf621207306f6d96d610af048354bc" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ "libc", - "locale", - "pad", - "redox_syscall", + "num-integer", + "num-traits", + "time", "winapi", ] @@ -62,7 +56,7 @@ name = "exa" version = "0.10.1" dependencies = [ "ansi_term", - "datetime", + "chrono", "git2", "glob", "lazy_static", @@ -77,7 +71,6 @@ dependencies = [ "terminal_size", "unicode-width", "users", - "zoneinfo_compiled", ] [[package]] @@ -205,6 +198,25 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -244,15 +256,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "pad" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" -dependencies = [ - "unicode-width", -] - [[package]] name = "percent-encoding" version = "2.1.0" @@ -265,12 +268,6 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -296,6 +293,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "tinyvec" version = "1.2.0" @@ -363,6 +371,12 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "winapi" version = "0.3.9" @@ -384,13 +398,3 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "zoneinfo_compiled" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fbebe65e899530f43bd760b23fda8f141118f4db49952b02998cbd0907a5de" -dependencies = [ - "byteorder", - "datetime", -] diff --git a/Cargo.toml b/Cargo.toml index 18aeacdc4..8fb1d0696 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ name = "exa" [dependencies] ansi_term = "0.12" +chrono = "0.4" glob = "0.3" lazy_static = "1.3" libc = "0.2" @@ -31,21 +32,14 @@ term_grid = "0.2.0" terminal_size = "0.1.16" unicode-width = "0.1" users = "0.11" -zoneinfo_compiled = "0.5.1" - -[dependencies.datetime] -version = "0.5.2" -default-features = false -features = ["format"] [dependencies.git2] version = "0.13" optional = true default-features = false -[build-dependencies.datetime] -version = "0.5.2" -default-features = false +[build-dependencies] +chrono = "0.4" [features] default = [ "git" ] diff --git a/build.rs b/build.rs index a0a135146..3730178a8 100644 --- a/build.rs +++ b/build.rs @@ -15,7 +15,7 @@ use std::fs::File; use std::io::{self, Write}; use std::path::PathBuf; -use datetime::{LocalDateTime, ISO}; +use chrono::prelude::*; /// The build script entry point. @@ -118,6 +118,6 @@ fn nonstandard_features_string() -> String { /// Formats the current date as an ISO 8601 string. fn build_date() -> String { - let now = LocalDateTime::now(); - format!("{}", now.date().iso()) + let now = Local::now(); + now.date().format("%Y-%m-%d").to_string() } diff --git a/src/fs/file.rs b/src/fs/file.rs index ea83f08b1..609afdb04 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -3,7 +3,8 @@ use std::io; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use chrono::prelude::*; use log::*; @@ -336,37 +337,26 @@ impl<'dir> File<'dir> { } /// This file’s last modified timestamp, if available on this platform. - pub fn modified_time(&self) -> Option { - self.metadata.modified().ok() + pub fn modified_time(&self) -> Option { + self.metadata.modified().map(|st| DateTime::::from(st).naive_utc()).ok() } /// This file’s last changed timestamp, if available on this platform. - pub fn changed_time(&self) -> Option { - let (mut sec, mut nanosec) = (self.metadata.ctime(), self.metadata.ctime_nsec()); - - if sec < 0 { - if nanosec > 0 { - sec += 1; - nanosec -= 1_000_000_000; - } - - let duration = Duration::new(sec.abs() as u64, nanosec.abs() as u32); - Some(UNIX_EPOCH - duration) - } - else { - let duration = Duration::new(sec as u64, nanosec as u32); - Some(UNIX_EPOCH + duration) - } + pub fn changed_time(&self) -> Option { + Some(NaiveDateTime::from_timestamp( + self.metadata.ctime(), + self.metadata.ctime_nsec() as u32, + )) } /// This file’s last accessed timestamp, if available on this platform. - pub fn accessed_time(&self) -> Option { - self.metadata.accessed().ok() + pub fn accessed_time(&self) -> Option { + self.metadata.accessed().map(|st| DateTime::::from(st).naive_utc()).ok() } /// This file’s created timestamp, if available on this platform. - pub fn created_time(&self) -> Option { - self.metadata.created().ok() + pub fn created_time(&self) -> Option { + self.metadata.created().map(|st| DateTime::::from(st).naive_utc()).ok() } /// This file’s ‘type’. diff --git a/src/output/render/times.rs b/src/output/render/times.rs index 72374f950..0e5156b48 100644 --- a/src/output/render/times.rs +++ b/src/output/render/times.rs @@ -1,27 +1,19 @@ -use std::time::SystemTime; - -use datetime::TimeZone; -use ansi_term::Style; - use crate::output::cell::TextCell; use crate::output::time::TimeFormat; +use ansi_term::Style; +use chrono::prelude::*; + pub trait Render { - fn render(self, style: Style, tz: &Option, format: TimeFormat) -> TextCell; + fn render(self, style: Style, time_offset: FixedOffset, time_format: TimeFormat) -> TextCell; } -impl Render for Option { - fn render(self, style: Style, tz: &Option, format: TimeFormat) -> TextCell { +impl Render for Option { + fn render(self, style: Style, time_offset: FixedOffset, time_format: TimeFormat) -> TextCell { let datestamp = if let Some(time) = self { - if let Some(ref tz) = tz { - format.format_zoned(time, tz) - } - else { - format.format_local(time) - } - } - else { + time_format.format(&DateTime::::from_utc(time, time_offset)) + } else { String::from("-") }; diff --git a/src/output/table.rs b/src/output/table.rs index cf8b6cf24..a1a5c3887 100644 --- a/src/output/table.rs +++ b/src/output/table.rs @@ -1,10 +1,8 @@ use std::cmp::max; -use std::env; use std::ops::Deref; use std::sync::{Mutex, MutexGuard}; -use datetime::TimeZone; -use zoneinfo_compiled::{CompiledData, Result as TZResult}; +use chrono::prelude::*; use lazy_static::lazy_static; use log::*; @@ -266,13 +264,12 @@ impl Default for TimeTypes { /// Any environment field should be able to be mocked up for test runs. pub struct Environment { + /// The computer’s current time offset, determined from time zone. + time_offset: FixedOffset, + /// Localisation rules for formatting numbers. numeric: locale::Numeric, - /// The computer’s current time zone. This gets used to determine how to - /// offset files’ timestamps. - tz: Option, - /// Mapping cache of user IDs to usernames. users: Mutex, } @@ -283,42 +280,14 @@ impl Environment { } fn load_all() -> Self { - let tz = match determine_time_zone() { - Ok(t) => { - Some(t) - } - Err(ref e) => { - println!("Unable to determine time zone: {}", e); - None - } - }; + let time_offset = *Local::now().offset(); let numeric = locale::Numeric::load_user_locale() .unwrap_or_else(|_| locale::Numeric::english()); let users = Mutex::new(UsersCache::new()); - Self { numeric, tz, users } - } -} - -fn determine_time_zone() -> TZResult { - if let Ok(file) = env::var("TZ") { - TimeZone::from_file({ - if file.starts_with('/') { - file - } else { - format!("/usr/share/zoneinfo/{}", { - if file.starts_with(':') { - file.replacen(":", "", 1) - } else { - file - } - }) - } - }) - } else { - TimeZone::from_file("/etc/localtime") + Self { time_offset, numeric, users } } } @@ -430,16 +399,16 @@ impl<'a, 'f> Table<'a> { } Column::Timestamp(TimeType::Modified) => { - file.modified_time().render(self.theme.ui.date, &self.env.tz, self.time_format) + file.modified_time().render(self.theme.ui.date, self.env.time_offset, self.time_format) } Column::Timestamp(TimeType::Changed) => { - file.changed_time().render(self.theme.ui.date, &self.env.tz, self.time_format) + file.changed_time().render(self.theme.ui.date, self.env.time_offset, self.time_format) } Column::Timestamp(TimeType::Created) => { - file.created_time().render(self.theme.ui.date, &self.env.tz, self.time_format) + file.created_time().render(self.theme.ui.date, self.env.time_offset, self.time_format) } Column::Timestamp(TimeType::Accessed) => { - file.accessed_time().render(self.theme.ui.date, &self.env.tz, self.time_format) + file.accessed_time().render(self.theme.ui.date, self.env.time_offset, self.time_format) } } } diff --git a/src/output/time.rs b/src/output/time.rs index cb18c54b3..675d968ec 100644 --- a/src/output/time.rs +++ b/src/output/time.rs @@ -1,10 +1,6 @@ //! Timestamp formatting. -use std::time::{SystemTime, UNIX_EPOCH}; - -use datetime::{LocalDateTime, TimeZone, DatePiece, TimePiece}; -use datetime::fmt::DateFormat; - +use chrono::prelude::*; use lazy_static::lazy_static; use unicode_width::UnicodeWidthStr; @@ -52,192 +48,107 @@ pub enum TimeFormat { // timestamps are separate types. impl TimeFormat { - pub fn format_local(self, time: SystemTime) -> String { - match self { - Self::DefaultFormat => default_local(time), - Self::ISOFormat => iso_local(time), - Self::LongISO => long_local(time), - Self::FullISO => full_local(time), - } - } - - pub fn format_zoned(self, time: SystemTime, zone: &TimeZone) -> String { + pub fn format(self, time: &DateTime) -> String { match self { - Self::DefaultFormat => default_zoned(time, zone), - Self::ISOFormat => iso_zoned(time, zone), - Self::LongISO => long_zoned(time, zone), - Self::FullISO => full_zoned(time, zone), + Self::DefaultFormat => default(time), + Self::ISOFormat => iso(time), + Self::LongISO => long(time), + Self::FullISO => full(time), } } } - -#[allow(trivial_numeric_casts)] -fn default_local(time: SystemTime) -> String { - let date = LocalDateTime::at(systemtime_epoch(time)); - let date_format = get_dateformat(&date); - date_format.format(&date, &*LOCALE) -} - -#[allow(trivial_numeric_casts)] -fn default_zoned(time: SystemTime, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time))); - let date_format = get_dateformat(&date); - date_format.format(&date, &*LOCALE) -} - -fn get_dateformat(date: &LocalDateTime) -> &'static DateFormat<'static> { - match (is_recent(&date), *MAXIMUM_MONTH_WIDTH) { - (true, 4) => &FOUR_WIDE_DATE_TIME, - (true, 5) => &FIVE_WIDE_DATE_TIME, - (true, _) => &OTHER_WIDE_DATE_TIME, - (false, 4) => &FOUR_WIDE_DATE_YEAR, - (false, 5) => &FIVE_WIDE_DATE_YEAR, - (false, _) => &OTHER_WIDE_DATE_YEAR, - } -} - -#[allow(trivial_numeric_casts)] -fn long_local(time: SystemTime) -> String { - let date = LocalDateTime::at(systemtime_epoch(time)); - 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: SystemTime, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time))); - format!("{:04}-{:02}-{:02} {:02}:{:02}", - date.year(), date.month() as usize, date.day(), - date.hour(), date.minute()) -} - -#[allow(trivial_numeric_casts)] -fn full_local(time: SystemTime) -> String { - let date = LocalDateTime::at(systemtime_epoch(time)); - format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}", - date.year(), date.month() as usize, date.day(), - date.hour(), date.minute(), date.second(), systemtime_nanos(time)) -} - -#[allow(trivial_numeric_casts)] -fn full_zoned(time: SystemTime, zone: &TimeZone) -> String { - use datetime::Offset; - - let local = LocalDateTime::at(systemtime_epoch(time)); - 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(), systemtime_nanos(time), - offset.hours(), offset.minutes().abs()) +fn default(time: &DateTime) -> String { + let month = &*LOCALE.short_month_name(time.month0() as usize); + let month_width = short_month_padding(*MAX_MONTH_WIDTH, &month); + let format = if time.year() == *CURRENT_YEAR { + format!("%_d {: String { - let date = LocalDateTime::at(systemtime_epoch(time)); - - if is_recent(&date) { - format!("{:02}-{:02} {:02}:{:02}", - date.month() as usize, date.day(), - date.hour(), date.minute()) - } - else { - format!("{:04}-{:02}-{:02}", - date.year(), date.month() as usize, date.day()) - } +/// Convert between Unicode width and width in chars to use in format!. +/// ex: in Japanese, 月 is one character, but it has the width of two. +/// For alignement purposes, we take the real display width into account. +/// So, MAXIMUM_MONTH_WIDTH (“12月”) = 4, but if we use `{:4}` in format!, +/// it will add a space (“ 12月”) because format! counts characters. +/// Conversely, a char can have a width of zero (like combining diacritics) +fn short_month_padding(max_month_width: usize, month: &str) -> usize { + let shift = month.chars().count() as isize - UnicodeWidthStr::width(month) as isize; + (max_month_width as isize + shift) as usize } -#[allow(trivial_numeric_casts)] -fn iso_zoned(time: SystemTime, zone: &TimeZone) -> String { - let date = zone.to_zoned(LocalDateTime::at(systemtime_epoch(time))); - - if is_recent(&date) { - format!("{:02}-{:02} {:02}:{:02}", - date.month() as usize, date.day(), - date.hour(), date.minute()) - } - else { - format!("{:04}-{:02}-{:02}", - date.year(), date.month() as usize, date.day()) +fn iso(time: &DateTime) -> String { + if time.year() == *CURRENT_YEAR { + time.format("%m-%d %H:%M").to_string() + } else { + time.format("%Y-%m-%d").to_string() } } - -fn systemtime_epoch(time: SystemTime) -> i64 { - time.duration_since(UNIX_EPOCH) - .map(|t| t.as_secs() as i64) - .unwrap_or_else(|e| { - let diff = e.duration(); - let mut secs = diff.as_secs(); - if diff.subsec_nanos() > 0 { - secs += 1; - } - -(secs as i64) - }) -} - -fn systemtime_nanos(time: SystemTime) -> u32 { - time.duration_since(UNIX_EPOCH) - .map(|t| t.subsec_nanos()) - .unwrap_or_else(|e| { - let nanos = e.duration().subsec_nanos(); - if nanos > 0 { - 1_000_000_000 - nanos - } else { - nanos - } - }) +fn long(time: &DateTime) -> String { + time.format("%Y-%m-%d %H:%M").to_string() } -fn is_recent(date: &LocalDateTime) -> bool { - date.year() == *CURRENT_YEAR +fn full(time: &DateTime) -> String { + time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string() } lazy_static! { - static ref CURRENT_YEAR: i64 = LocalDateTime::now().year(); + static ref CURRENT_YEAR: i32 = Local::now().year(); static ref LOCALE: locale::Time = { locale::Time::load_user_locale() .unwrap_or_else(|_| locale::Time::english()) }; - static ref MAXIMUM_MONTH_WIDTH: usize = { + static ref MAX_MONTH_WIDTH: usize = { // Some locales use a three-character wide month name (Jan to Dec); // others vary between three to four (1月 to 12月, juil.). We check each month width // to detect the longest and set the output format accordingly. - let mut maximum_month_width = 0; - for i in 0..11 { - let current_month_width = UnicodeWidthStr::width(&*LOCALE.short_month_name(i)); - maximum_month_width = std::cmp::max(maximum_month_width, current_month_width); - } - maximum_month_width + (0..11).map(|i| UnicodeWidthStr::width(&*LOCALE.short_month_name(i))).max().unwrap() }; +} - static ref FOUR_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse( - "{2>:D} {4<:M} {02>:h}:{02>:m}" - ).unwrap(); - - static ref FIVE_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse( - "{2>:D} {5<:M} {02>:h}:{02>:m}" - ).unwrap(); - - static ref OTHER_WIDE_DATE_TIME: DateFormat<'static> = DateFormat::parse( - "{2>:D} {:M} {02>:h}:{02>:m}" - ).unwrap(); - - static ref FOUR_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse( - "{2>:D} {4<:M} {5>:Y}" - ).unwrap(); - - static ref FIVE_WIDE_DATE_YEAR: DateFormat<'static> = DateFormat::parse( - "{2>:D} {5<:M} {5>:Y}" - ).unwrap(); +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn short_month_width_japanese() { + let max_month_width = 4; + let month = "1\u{2F49}"; // 1月 + let padding = short_month_padding(max_month_width, month); + let final_str = format!("{: = DateFormat::parse( - "{2>:D} {:M} {5>:Y}" - ).unwrap(); + #[test] + fn short_month_width_hindi() { + let max_month_width = 4; + assert_eq!(true, [ + "\u{091C}\u{0928}\u{0970}", + "\u{092B}\u{093C}\u{0930}\u{0970}", + "\u{092E}\u{093E}\u{0930}\u{094D}\u{091A}", + "\u{0905}\u{092A}\u{094D}\u{0930}\u{0948}\u{0932}", + "\u{092E}\u{0908}", + "\u{091C}\u{0942}\u{0928}", + "\u{091C}\u{0941}\u{0932}\u{0970}", + "\u{0905}\u{0917}\u{0970}", + "\u{0938}\u{093F}\u{0924}\u{0970}", + "\u{0905}\u{0915}\u{094D}\u{0924}\u{0942}\u{0970}", + "\u{0928}\u{0935}\u{0970}", + "\u{0926}\u{093F}\u{0938}\u{0970}", + ].iter() + .map(|month| format!( + "{: