From 0c888486c97e941ee18aca8f3539336a0c92e9a1 Mon Sep 17 00:00:00 2001 From: WMR <1621189+fnordpig@users.noreply.github.com> Date: Fri, 23 Jun 2023 13:05:04 -0700 Subject: [PATCH] Add custom datetime format through `strftime` strings (#9500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - improves usability of datetime's in displayed text - # Description Creates a config point for specifying long / short date time formats. Defaults to humanized as we have today. Provides for adding strftime formats into config.nu such as: ```nu datetime_format: { normal: "%Y-%m-%d %H:%M:%S" table: "%Y-%m-%d" } ``` Example: ```bash > $env.config.datetime_format ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ normal ┃ %a, %d %b %Y %H:%M:%S %z ┃ ┃ table ┃ %m/%d/%y %I:%M:%S%p ┃ ┗━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━┛ > let a = (date now) > echo $a Thu, 22 Jun 2023 10:21:23 -0700 > echo [$a] ┏━━━┳━━━━━━━━━━━━━━━━━━━━━┓ ┃ 0 ┃ 06/22/23 10:21:23AM ┃ ┗━━━┻━━━━━━━━━━━━━━━━━━━━━┛ ``` # User-Facing Changes Any place converting a datetime to a user displayed value should be impacted. # Tests + Formatting - `cargo fmt --all -- --check` Done - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect -A clippy::result_large_err` Done - `cargo test --workspace` Done - `cargo run -- crates/nu-std/tests/run.nu` Not done - doesn't seem to work ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr ``` - Done --------- Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com> Co-authored-by: Antoine Stevan <44101798+amtoine@users.noreply.github.com> --- .gitignore | 1 + crates/nu-protocol/Cargo.toml | 2 +- crates/nu-protocol/src/config.rs | 48 +++++++++++++++++++ crates/nu-protocol/src/value/mod.rs | 40 ++++++++++++++-- .../src/sample_config/default_config.nu | 9 ++++ 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 6ef9090cd7..7bd8ce0d6f 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ tarpaulin-report.html *.rsproj *.rsproj.user *.sln +*.code-workspace # direnv .direnv/ diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index 6033a9d10e..43fd76ff12 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -16,7 +16,7 @@ bench = false nu-utils = { path = "../nu-utils", version = "0.81.1" } byte-unit = "4.0" -chrono = { version = "0.4", features = [ "serde", "std", ], default-features = false } +chrono = { version = "0.4", features = [ "serde", "std", "unstable-locales" ], default-features = false } chrono-humanize = "0.2" fancy-regex = "0.11" indexmap = { version = "1.7" } diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 1dbdbb5df8..0d97178020 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -106,6 +106,8 @@ pub struct Config { pub cursor_shape_vi_insert: NuCursorShape, pub cursor_shape_vi_normal: NuCursorShape, pub cursor_shape_emacs: NuCursorShape, + pub datetime_normal_format: Option, + pub datetime_table_format: Option, } impl Default for Config { @@ -150,6 +152,8 @@ impl Default for Config { cursor_shape_vi_insert: NuCursorShape::Block, cursor_shape_vi_normal: NuCursorShape::UnderScore, cursor_shape_emacs: NuCursorShape::Line, + datetime_normal_format: None, + datetime_table_format: None, } } } @@ -1215,6 +1219,50 @@ impl Value { }); } }, + "datetime_format" => { + if let Value::Record { cols, vals, span } = &mut vals[index] { + for index in (0..cols.len()).rev() { + let value = &vals[index]; + let key2 = cols[index].as_str(); + match key2 { + "normal" => { + if let Ok(v) = value.as_string() { + config.datetime_normal_format = Some(v); + } else { + invalid!(Some(*span), "should be a string"); + } + } + "table" => { + if let Ok(v) = value.as_string() { + config.datetime_table_format = Some(v); + } else { + invalid!(Some(*span), "should be a string"); + } + } + x => { + invalid_key!( + cols, + vals, + index, + value.span().ok(), + "$env.config.{key}.{x} is an unknown config setting" + ); + } + } + } + } else { + invalid!(vals[index].span().ok(), "should be a record"); + // Reconstruct + vals[index] = Value::record( + vec!["metric".into(), "format".into()], + vec![ + Value::boolean(config.filesize_metric, *span), + Value::string(config.filesize_format.clone(), *span), + ], + *span, + ); + } + } // Catch all x => { invalid_key!( diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 29be992932..560a99d41e 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -12,7 +12,7 @@ use crate::engine::EngineState; use crate::ShellError; use crate::{did_you_mean, BlockId, Config, Span, Spanned, Type, VarId}; use byte_unit::ByteUnit; -use chrono::{DateTime, Duration, FixedOffset}; +use chrono::{DateTime, Duration, FixedOffset, Locale, TimeZone}; use chrono_humanize::HumanTime; pub use custom_value::CustomValue; use fancy_regex::Regex; @@ -20,10 +20,12 @@ pub use from_value::FromValue; use indexmap::map::IndexMap; pub use lazy_record::LazyRecord; use nu_utils::get_system_locale; +use nu_utils::locale::get_system_locale_string; use num_format::ToFormattedString; pub use range::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt::Write; use std::{ borrow::Cow, fmt::{Display, Formatter, Result as FmtResult}, @@ -534,7 +536,12 @@ impl Value { Value::Float { val, .. } => val.to_string(), Value::Filesize { val, .. } => format_filesize_from_conf(*val, config), Value::Duration { val, .. } => format_duration(*val), - Value::Date { val, .. } => format!("{} ({})", val.to_rfc2822(), HumanTime::from(*val)), + Value::Date { val, .. } => match &config.datetime_normal_format { + Some(format) => self.format_datetime(val, format), + None => { + format!("{} ({})", val.to_rfc2822(), HumanTime::from(*val)) + } + }, Value::Range { val, .. } => { format!( "{}..{}", @@ -586,7 +593,10 @@ impl Value { Value::Float { val, .. } => val.to_string(), Value::Filesize { val, .. } => format_filesize_from_conf(*val, config), Value::Duration { val, .. } => format_duration(*val), - Value::Date { val, .. } => HumanTime::from(*val).to_string(), + Value::Date { val, .. } => match &config.datetime_table_format { + Some(format) => self.format_datetime(val, format), + None => HumanTime::from(*val).to_string(), + }, Value::Range { val, .. } => { format!( "{}..{}", @@ -630,6 +640,26 @@ impl Value { } } + fn format_datetime(&self, date_time: &DateTime, formatter: &str) -> String + where + Tz::Offset: Display, + { + let mut formatter_buf = String::new(); + let locale: Locale = get_system_locale_string() + .map(|l| l.replace('-', "_")) // `chrono::Locale` needs something like `xx_xx`, rather than `xx-xx` + .unwrap_or_else(|| String::from("en_US")) + .as_str() + .try_into() + .unwrap_or(Locale::en_US); + let format = date_time.format_localized(formatter, locale); + + match formatter_buf.write_fmt(format_args!("{format}")) { + Ok(_) => (), + Err(_) => formatter_buf = format!("Invalid format string {}", formatter), + } + formatter_buf + } + /// Convert Value into a debug string pub fn debug_value(&self) -> String { format!("{self:#?}") @@ -798,8 +828,8 @@ impl Value { return Ok(Value::nothing(*origin_span)); // short-circuit } else { return Err(ShellError::AccessBeyondEndOfStream { - span: *origin_span -}); + span: *origin_span + }); } } Value::CustomValue { val, .. } => { diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 10ad7e5b50..6997391433 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -207,6 +207,15 @@ let-env config = { } } + # datetime_format determines what a datetime rendered in the shell would look like. + # Behavior without this configuration point will be to "humanize" the datetime display, + # showing something like "a day ago." + + datetime_format: { + normal: '%a, %d %b %Y %H:%M:%S %z' # shows up in displays of variables or other datetime's outside of tables + # table: '%m/%d/%y %I:%M:%S%p' # generally shows up in tabular outputs such as ls. commenting this out will change it to the default human readable datetime format + } + explore: { help_banner: true exit_esc: true