From bade745310d6b3b7cc833d53ff14178bc13cf4da Mon Sep 17 00:00:00 2001 From: Bryan Malyn Date: Sun, 12 Feb 2023 14:23:18 -0600 Subject: [PATCH] Add datetime value parser Resolves #1708 --- src/blocks/time.rs | 47 +++++------------ src/formatting/formatter.rs | 100 +++++++++++++++++++++++++++++++++++- src/formatting/parse.rs | 9 +++- src/formatting/value.rs | 6 +++ 4 files changed, 123 insertions(+), 39 deletions(-) diff --git a/src/blocks/time.rs b/src/blocks/time.rs index 7f57084509..b1e00467a9 100644 --- a/src/blocks/time.rs +++ b/src/blocks/time.rs @@ -4,10 +4,9 @@ //! //! Key | Values | Default //! ----|--------|-------- -//! `format` | Format string. See [chrono docs](https://docs.rs/chrono/0.3.0/chrono/format/strftime/index.html#specifiers) for all options. | `" $icon %a %d/%m %R "` +//! `format` | Format string. See [chrono docs](https://docs.rs/chrono/0.3.0/chrono/format/strftime/index.html#specifiers) for all options. | `" $icon $timestamp.datetime() "` //! `interval` | Update interval in seconds | `10` //! `timezone` | A timezone specifier (e.g. "Europe/Lisbon") | Local timezone -//! `locale` | Locale to apply when formatting the time | System locale //! //! Placeholder | Value | Type | Unit //! --------------|---------------------------------------------|--------|----- @@ -21,15 +20,13 @@ //! interval = 60 //! locale = "fr_BE" //! [block.format] -//! full = " $icon %d/%m %R " -//! short = " $icon %R " +//! full = " $icon $timestamp.datetime(f:%a %Y-%m-%d %R %Z, l:fr_BE) " +//! short = " $icon $timestamp.datetime(f:%R) " //! ``` //! //! # Icons Used //! - `time` -use chrono::offset::{Local, Utc}; -use chrono::Locale; use chrono_tz::Tz; use super::prelude::*; @@ -52,14 +49,13 @@ pub async fn run(config: Config, mut api: CommonApi) -> Result<()> { .format .full .as_deref() - .unwrap_or(" $icon %a %d/%m %R "); - let format_short = config.format.short.as_deref(); + .unwrap_or(" $icon $timestamp.datetime() "); + + let format_short = config.format.short.as_deref().unwrap_or_default(); + + widget.set_format(FormatConfig::default().with_defaults(format, format_short)?); let timezone = config.timezone; - let locale = match config.locale.as_deref() { - Some(locale) => Some(locale.try_into().ok().error("invalid locale")?), - None => None, - }; let mut timer = config.interval.timer(); @@ -70,13 +66,10 @@ pub async fn run(config: Config, mut api: CommonApi) -> Result<()> { unsafe { tzset() }; } - let full_time = get_time(format, timezone, locale); - let short_time = format_short - .map(|f| get_time(f, timezone, locale)) - .unwrap_or_else(|| "".into()); - - widget.set_format(FormatConfig::default().with_defaults(&full_time, &short_time)?); - widget.set_values(map!("icon" => Value::icon(api.get_icon("time")?))); + widget.set_values(map!( + "icon" => Value::icon(api.get_icon("time")?), + "timestamp" => Value::timestamp(timezone) + )); api.set_widget(&widget).await?; @@ -87,22 +80,6 @@ pub async fn run(config: Config, mut api: CommonApi) -> Result<()> { } } -fn get_time(format: &str, timezone: Option, locale: Option) -> String { - match locale { - Some(locale) => match timezone { - Some(tz) => Utc::now() - .with_timezone(&tz) - .format_localized(format, locale) - .to_string(), - None => Local::now().format_localized(format, locale).to_string(), - }, - None => match timezone { - Some(tz) => Utc::now().with_timezone(&tz).format(format).to_string(), - None => Local::now().format(format).to_string(), - }, - } -} - extern "C" { /// The tzset function initializes the tzname variable from the value of the TZ environment /// variable. It is not usually necessary for your program to call this function, because it is diff --git a/src/formatting/formatter.rs b/src/formatting/formatter.rs index 2bde71ba5e..58ff169272 100644 --- a/src/formatting/formatter.rs +++ b/src/formatting/formatter.rs @@ -1,3 +1,6 @@ +use chrono::format::{Item, StrftimeItems}; +use chrono::{Local, Utc}; + use std::fmt::Debug; use std::iter::repeat; use std::time::{Duration, Instant}; @@ -18,6 +21,8 @@ const DEFAULT_BAR_MAX_VAL: f64 = 100.0; const DEFAULT_NUMBER_WIDTH: usize = 2; +const DEFAULT_DATETIME_FORMAT: &str = "%a %d/%m %R"; + pub const DEFAULT_STRING_FORMATTER: StrFormatter = StrFormatter { min_width: DEFAULT_STR_MIN_WIDTH, max_width: DEFAULT_STR_MAX_WIDTH, @@ -37,6 +42,8 @@ pub const DEFAULT_NUMBER_FORMATTER: EngFormatter = EngFormatter(EngFixConfig { prefix_forced: false, }); +pub const DEFAULT_TIMESTAMP_FORMATTER: TimestampFormatter = TimestampFormatter { items: None }; + pub const DEFAULT_FLAG_FORMATTER: FlagFormatter = FlagFormatter; pub trait Formatter: Debug + Send + Sync { @@ -112,7 +119,7 @@ pub fn new_formatter(name: &str, args: &[Arg]) -> Result> { max_value = arg.val.parse().error("Max value must be a number")?; } other => { - return Err(Error::new(format!("Unknown argumnt for 'bar': '{other}'"))); + return Err(Error::new(format!("Unknown argument for 'bar': '{other}'"))); } } } @@ -120,6 +127,27 @@ pub fn new_formatter(name: &str, args: &[Arg]) -> Result> { } "eng" => Ok(Box::new(EngFormatter(EngFixConfig::from_args(args)?))), "fix" => Ok(Box::new(FixFormatter(EngFixConfig::from_args(args)?))), + "datetime" => { + let mut format = None; + let mut locale = None; + for arg in args { + match arg.key { + "format" | "f" => { + format = Some(arg.val.parse().error("format value must be a string")?); + } + "locale" | "l" => { + locale = arg.val.parse().ok(); + } + other => { + return Err(Error::new(format!( + "Unknown argument for 'datetime': '{other}'" + ))); + } + } + } + + Ok(Box::new(TimestampFormatter::new(format, locale)?)) + } _ => Err(Error::new(format!("Unknown formatter: '{name}'"))), } } @@ -163,6 +191,9 @@ impl Formatter for StrFormatter { Value::Number { .. } => Err(Error::new_format( "A number cannot be formatted with 'str' formatter", )), + Value::Timestamp(..) => Err(Error::new_format( + "A timestamp cannot be formatted with 'str' formatter", + )), Value::Flag => Err(Error::new_format( "A flag cannot be formatted with 'str' formatter", )), @@ -184,6 +215,9 @@ impl Formatter for PangoStrFormatter { Value::Number { .. } => Err(Error::new_format( "A number cannot be formatted with 'str' formatter", )), + Value::Timestamp(..) => Err(Error::new_format( + "A timestamp cannot be formatted with 'str' formatter", + )), Value::Flag => Err(Error::new_format( "A flag cannot be formatted with 'str' formatter", )), @@ -220,6 +254,9 @@ impl Formatter for BarFormatter { Value::Icon(_) => Err(Error::new_format( "An icon cannot be formatted with 'bar' formatter", )), + Value::Timestamp(..) => Err(Error::new_format( + "A timestamp cannot be formatted with 'bar' formatter", + )), Value::Flag => Err(Error::new_format( "A flag cannot be formatted with 'bar' formatter", )), @@ -367,6 +404,9 @@ impl Formatter for EngFormatter { Value::Icon(_) => Err(Error::new_format( "An icon cannot be formatted with 'eng' formatter", )), + Value::Timestamp(..) => Err(Error::new_format( + "A timestamp cannot be formatted with 'eng' formatter", + )), Value::Flag => Err(Error::new_format( "A flag cannot be formatted with 'eng' formatter", )), @@ -392,6 +432,9 @@ impl Formatter for FixFormatter { Value::Icon(_) => Err(Error::new_format( "An icon cannot be formatted with 'fix' formatter", )), + Value::Timestamp(..) => Err(Error::new_format( + "A timestamp cannot be formatted with 'fix' formatter", + )), Value::Flag => Err(Error::new_format( "A flag cannot be formatted with 'fix' formatter", )), @@ -399,13 +442,66 @@ impl Formatter for FixFormatter { } } +#[derive(Debug)] +pub struct TimestampFormatter { + items: Option>>, +} + +fn make_static_item(item: Item<'_>) -> Item<'static> { + match item { + Item::Literal(str) => Item::OwnedLiteral(str.into()), + Item::OwnedLiteral(boxed) => Item::OwnedLiteral(boxed), + Item::Space(str) => Item::OwnedSpace(str.into()), + Item::OwnedSpace(boxed) => Item::OwnedSpace(boxed), + Item::Numeric(numeric, pad) => Item::Numeric(numeric, pad), + Item::Fixed(fixed) => Item::Fixed(fixed), + Item::Error => Item::Error, + } +} + +impl TimestampFormatter { + fn new(format: Option, locale: Option) -> Result { + let format = format.as_deref().unwrap_or(DEFAULT_DATETIME_FORMAT); + + let items = match locale.as_deref() { + Some(locale) => { + let locale = locale.try_into().ok().error("invalid locale")?; + StrftimeItems::new_with_locale(format, locale) + } + None => StrftimeItems::new(format), + } + .map(make_static_item) + .collect(); + + Ok(Self { items: Some(items) }) + } +} + +impl Formatter for TimestampFormatter { + fn format(&self, val: &Value) -> Result { + let items = self.items.clone().unwrap(); + match val { + Value::Number { .. } | Value::Text(_) | Value::Icon(_) | Value::Flag => unreachable!(), + Value::Timestamp(timezone) => Ok(match timezone { + Some(tz) => Utc::now() + .with_timezone(tz) + .format_with_items(items.iter()) + .to_string(), + None => Local::now().format_with_items(items.iter()).to_string(), + }), + } + } +} + #[derive(Debug)] pub struct FlagFormatter; impl Formatter for FlagFormatter { fn format(&self, val: &Value) -> Result { match val { - Value::Number { .. } | Value::Text(_) | Value::Icon(_) => unreachable!(), + Value::Number { .. } | Value::Text(_) | Value::Icon(_) | Value::Timestamp(..) => { + unreachable!() + } Value::Flag => Ok(String::new()), } } diff --git a/src/formatting/parse.rs b/src/formatting/parse.rs index d6cb847fe2..c01745e1db 100644 --- a/src/formatting/parse.rs +++ b/src/formatting/parse.rs @@ -82,14 +82,19 @@ fn alphanum1(i: &str) -> IResult<&str, &str, PError> { } fn arg1(i: &str) -> IResult<&str, &str, PError> { - take_while1(|x: char| x.is_alphanumeric() || x == '_' || x == '-' || x == '.')(i) + take_while1(|x: char| { + x.is_alphanumeric() || x == ' ' || x == '%' || x == '/' || x == '_' || x == '-' || x == '.' + })(i) } // `key:val` fn parse_arg(i: &str) -> IResult<&str, Arg, PError> { map( separated_pair(alphanum1, cut(char(':')), cut(arg1)), - |(key, val)| Arg { key, val }, + |(key, val)| Arg { + key, + val: val.trim(), + }, )(i) } diff --git a/src/formatting/value.rs b/src/formatting/value.rs index 09f03b617b..ddc223e5c9 100644 --- a/src/formatting/value.rs +++ b/src/formatting/value.rs @@ -1,6 +1,7 @@ use super::formatter; use super::unit::Unit; use super::Metadata; +use chrono_tz::Tz; #[derive(Debug, Clone)] pub struct Value { @@ -13,6 +14,7 @@ pub enum ValueInner { Text(String), Icon(String), Number { val: f64, unit: Unit }, + Timestamp(Option), Flag, } @@ -46,6 +48,9 @@ impl Value { Self::new(ValueInner::Flag) } + pub fn timestamp(tz: Option) -> Self { + Self::new(ValueInner::Timestamp(tz)) + } pub fn icon(icon: String) -> Self { Self::new(ValueInner::Icon(icon)) } @@ -108,6 +113,7 @@ impl Value { match &self.inner { ValueInner::Text(_) | ValueInner::Icon(_) => &formatter::DEFAULT_STRING_FORMATTER, ValueInner::Number { .. } => &formatter::DEFAULT_NUMBER_FORMATTER, + ValueInner::Timestamp { .. } => &formatter::DEFAULT_TIMESTAMP_FORMATTER, ValueInner::Flag => &formatter::DEFAULT_FLAG_FORMATTER, } }