forked from extern/nushell
Add custom datetime format through strftime
strings (#9500)
- 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>
This commit is contained in:
parent
e16c1b7c88
commit
0c888486c9
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,6 +42,7 @@ tarpaulin-report.html
|
|||||||
*.rsproj
|
*.rsproj
|
||||||
*.rsproj.user
|
*.rsproj.user
|
||||||
*.sln
|
*.sln
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
# direnv
|
# direnv
|
||||||
.direnv/
|
.direnv/
|
||||||
|
@ -16,7 +16,7 @@ bench = false
|
|||||||
nu-utils = { path = "../nu-utils", version = "0.81.1" }
|
nu-utils = { path = "../nu-utils", version = "0.81.1" }
|
||||||
|
|
||||||
byte-unit = "4.0"
|
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"
|
chrono-humanize = "0.2"
|
||||||
fancy-regex = "0.11"
|
fancy-regex = "0.11"
|
||||||
indexmap = { version = "1.7" }
|
indexmap = { version = "1.7" }
|
||||||
|
@ -106,6 +106,8 @@ pub struct Config {
|
|||||||
pub cursor_shape_vi_insert: NuCursorShape,
|
pub cursor_shape_vi_insert: NuCursorShape,
|
||||||
pub cursor_shape_vi_normal: NuCursorShape,
|
pub cursor_shape_vi_normal: NuCursorShape,
|
||||||
pub cursor_shape_emacs: NuCursorShape,
|
pub cursor_shape_emacs: NuCursorShape,
|
||||||
|
pub datetime_normal_format: Option<String>,
|
||||||
|
pub datetime_table_format: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@ -150,6 +152,8 @@ impl Default for Config {
|
|||||||
cursor_shape_vi_insert: NuCursorShape::Block,
|
cursor_shape_vi_insert: NuCursorShape::Block,
|
||||||
cursor_shape_vi_normal: NuCursorShape::UnderScore,
|
cursor_shape_vi_normal: NuCursorShape::UnderScore,
|
||||||
cursor_shape_emacs: NuCursorShape::Line,
|
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
|
// Catch all
|
||||||
x => {
|
x => {
|
||||||
invalid_key!(
|
invalid_key!(
|
||||||
|
@ -12,7 +12,7 @@ use crate::engine::EngineState;
|
|||||||
use crate::ShellError;
|
use crate::ShellError;
|
||||||
use crate::{did_you_mean, BlockId, Config, Span, Spanned, Type, VarId};
|
use crate::{did_you_mean, BlockId, Config, Span, Spanned, Type, VarId};
|
||||||
use byte_unit::ByteUnit;
|
use byte_unit::ByteUnit;
|
||||||
use chrono::{DateTime, Duration, FixedOffset};
|
use chrono::{DateTime, Duration, FixedOffset, Locale, TimeZone};
|
||||||
use chrono_humanize::HumanTime;
|
use chrono_humanize::HumanTime;
|
||||||
pub use custom_value::CustomValue;
|
pub use custom_value::CustomValue;
|
||||||
use fancy_regex::Regex;
|
use fancy_regex::Regex;
|
||||||
@ -20,10 +20,12 @@ pub use from_value::FromValue;
|
|||||||
use indexmap::map::IndexMap;
|
use indexmap::map::IndexMap;
|
||||||
pub use lazy_record::LazyRecord;
|
pub use lazy_record::LazyRecord;
|
||||||
use nu_utils::get_system_locale;
|
use nu_utils::get_system_locale;
|
||||||
|
use nu_utils::locale::get_system_locale_string;
|
||||||
use num_format::ToFormattedString;
|
use num_format::ToFormattedString;
|
||||||
pub use range::*;
|
pub use range::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Write;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
fmt::{Display, Formatter, Result as FmtResult},
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
@ -534,7 +536,12 @@ impl Value {
|
|||||||
Value::Float { val, .. } => val.to_string(),
|
Value::Float { val, .. } => val.to_string(),
|
||||||
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
|
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
|
||||||
Value::Duration { val, .. } => format_duration(*val),
|
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, .. } => {
|
Value::Range { val, .. } => {
|
||||||
format!(
|
format!(
|
||||||
"{}..{}",
|
"{}..{}",
|
||||||
@ -586,7 +593,10 @@ impl Value {
|
|||||||
Value::Float { val, .. } => val.to_string(),
|
Value::Float { val, .. } => val.to_string(),
|
||||||
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
|
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
|
||||||
Value::Duration { val, .. } => format_duration(*val),
|
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, .. } => {
|
Value::Range { val, .. } => {
|
||||||
format!(
|
format!(
|
||||||
"{}..{}",
|
"{}..{}",
|
||||||
@ -630,6 +640,26 @@ impl Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_datetime<Tz: TimeZone>(&self, date_time: &DateTime<Tz>, 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
|
/// Convert Value into a debug string
|
||||||
pub fn debug_value(&self) -> String {
|
pub fn debug_value(&self) -> String {
|
||||||
format!("{self:#?}")
|
format!("{self:#?}")
|
||||||
|
@ -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: {
|
explore: {
|
||||||
help_banner: true
|
help_banner: true
|
||||||
exit_esc: true
|
exit_esc: true
|
||||||
|
Loading…
Reference in New Issue
Block a user