Skip to content

Commit

Permalink
Add datetime value parser
Browse files Browse the repository at this point in the history
Resolves greshake#1708

Allow quoted values in format arg parse
  • Loading branch information
bim9262 committed Feb 16, 2023
1 parent efa7b55 commit 1067702
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 44 deletions.
57 changes: 17 additions & 40 deletions src/blocks/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,30 @@
//!
//! 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
//! --------------|---------------------------------------------|--------|-----
//! `icon` | A static icon | Icon | -
//! Placeholder | Value | Type | Unit
//! --------------|---------------------------------------------|----------|-----
//! `icon` | A static icon | Icon | -
//! `timestamp` | The current time | Datetime | -
//!
//! # Example
//!
//! ```toml
//! [[block]]
//! block = "time"
//! 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::Utc;
use chrono_tz::Tz;

use super::prelude::*;
Expand All @@ -42,7 +40,6 @@ pub struct Config {
#[default(1.into())]
interval: Seconds,
timezone: Option<Tz>,
locale: Option<String>,
}

pub async fn run(config: Config, mut api: CommonApi) -> Result<()> {
Expand All @@ -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();

Expand All @@ -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::datetime(Utc::now(), timezone)
));

api.set_widget(&widget).await?;

Expand All @@ -87,22 +80,6 @@ pub async fn run(config: Config, mut api: CommonApi) -> Result<()> {
}
}

fn get_time(format: &str, timezone: Option<Tz>, locale: Option<Locale>) -> 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
Expand Down
7 changes: 7 additions & 0 deletions src/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@
//!
//! No arguments.
//!
//! ## `datetime` - Display datetime
//!
//! Argument | Description |Default value
//! -----------------------|-----------------------------------------------------------------------------------------------------------|-------------
//! `format` or `f` | [chrono docs](https://docs.rs/chrono/0.3.0/chrono/format/strftime/index.html#specifiers) for all options. | `'%a %d/%m %R'`
//! `locale` or `l` | Locale to apply when formatting the time | System locale
//!
//! # Handling missing placeholders and incorrect types
//!
//! Some blocks allow missing placeholders, for example [bluetooth](crate::blocks::bluetooth)'s
Expand Down
116 changes: 114 additions & 2 deletions src/formatting/formatter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use chrono::format::{Item, StrftimeItems};
use chrono::Local;
use once_cell::sync::Lazy;

use std::fmt::Debug;
use std::iter::repeat;
use std::time::{Duration, Instant};
Expand All @@ -18,6 +22,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,
Expand All @@ -37,6 +43,9 @@ pub const DEFAULT_NUMBER_FORMATTER: EngFormatter = EngFormatter(EngFixConfig {
prefix_forced: false,
});

pub static DEFAULT_DATETIME_FORMATTER: Lazy<DatetimeFormatter> =
Lazy::new(|| DatetimeFormatter::new(DEFAULT_DATETIME_FORMAT, None).unwrap());

pub const DEFAULT_FLAG_FORMATTER: FlagFormatter = FlagFormatter;

pub trait Formatter: Debug + Send + Sync {
Expand Down Expand Up @@ -112,14 +121,38 @@ pub fn new_formatter(name: &str, args: &[Arg]) -> Result<Box<dyn Formatter>> {
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}'")));
}
}
}
Ok(Box::new(BarFormatter { width, max_value }))
}
"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);
}
"locale" | "l" => {
locale = Some(arg.val);
}
other => {
return Err(Error::new(format!(
"Unknown argument for 'datetime': '{other}'"
)));
}
}
}

Ok(Box::new(DatetimeFormatter::new(
format.unwrap_or(DEFAULT_DATETIME_FORMAT),
locale,
)?))
}
_ => Err(Error::new(format!("Unknown formatter: '{name}'"))),
}
}
Expand Down Expand Up @@ -163,6 +196,9 @@ impl Formatter for StrFormatter {
Value::Number { .. } => Err(Error::new_format(
"A number cannot be formatted with 'str' formatter",
)),
Value::Datetime(..) => Err(Error::new_format(
"A datetime cannot be formatted with 'str' formatter",
)),
Value::Flag => Err(Error::new_format(
"A flag cannot be formatted with 'str' formatter",
)),
Expand All @@ -184,6 +220,9 @@ impl Formatter for PangoStrFormatter {
Value::Number { .. } => Err(Error::new_format(
"A number cannot be formatted with 'str' formatter",
)),
Value::Datetime(..) => Err(Error::new_format(
"A datetime cannot be formatted with 'str' formatter",
)),
Value::Flag => Err(Error::new_format(
"A flag cannot be formatted with 'str' formatter",
)),
Expand Down Expand Up @@ -220,6 +259,9 @@ impl Formatter for BarFormatter {
Value::Icon(_) => Err(Error::new_format(
"An icon cannot be formatted with 'bar' formatter",
)),
Value::Datetime(..) => Err(Error::new_format(
"A datetime cannot be formatted with 'bar' formatter",
)),
Value::Flag => Err(Error::new_format(
"A flag cannot be formatted with 'bar' formatter",
)),
Expand Down Expand Up @@ -367,6 +409,9 @@ impl Formatter for EngFormatter {
Value::Icon(_) => Err(Error::new_format(
"An icon cannot be formatted with 'eng' formatter",
)),
Value::Datetime(..) => Err(Error::new_format(
"A datetime cannot be formatted with 'eng' formatter",
)),
Value::Flag => Err(Error::new_format(
"A flag cannot be formatted with 'eng' formatter",
)),
Expand All @@ -392,6 +437,71 @@ impl Formatter for FixFormatter {
Value::Icon(_) => Err(Error::new_format(
"An icon cannot be formatted with 'fix' formatter",
)),
Value::Datetime(..) => Err(Error::new_format(
"A datetime cannot be formatted with 'fix' formatter",
)),
Value::Flag => Err(Error::new_format(
"A flag cannot be formatted with 'fix' formatter",
)),
}
}
}

#[derive(Debug)]
pub struct DatetimeFormatter {
items: Vec<Item<'static>>,
}

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 DatetimeFormatter {
fn new(format: &str, locale: Option<&str>) -> Result<Self> {
let items = match locale {
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 })
}
}

impl Formatter for DatetimeFormatter {
fn format(&self, val: &Value) -> Result<String> {
match val {
Value::Datetime(datetime, timezone) => Ok(match timezone {
Some(tz) => datetime
.with_timezone(tz)
.format_with_items(self.items.iter())
.to_string(),
None => datetime
.with_timezone(&Local)
.format_with_items(self.items.iter())
.to_string(),
}),
Value::Text(_) => Err(Error::new_format(
"Text cannot be formatted with 'fix' formatter",
)),
Value::Number { .. } => Err(Error::new_format(
"Number cannot be formatted with 'fix' formatter",
)),
Value::Icon(_) => Err(Error::new_format(
"An icon cannot be formatted with 'fix' formatter",
)),
Value::Flag => Err(Error::new_format(
"A flag cannot be formatted with 'fix' formatter",
)),
Expand All @@ -405,7 +515,9 @@ pub struct FlagFormatter;
impl Formatter for FlagFormatter {
fn format(&self, val: &Value) -> Result<String> {
match val {
Value::Number { .. } | Value::Text(_) | Value::Icon(_) => unreachable!(),
Value::Number { .. } | Value::Text(_) | Value::Icon(_) | Value::Datetime(..) => {
unreachable!()
}
Value::Flag => Ok(String::new()),
}
}
Expand Down
19 changes: 17 additions & 2 deletions src/formatting/parse.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use nom::{
branch::alt,
bytes::complete::{escaped_transform, tag, take_while, take_while1},
bytes::complete::{escaped_transform, is_not, tag, take_while, take_while1},
character::complete::{anychar, char},
combinator::{cut, eof, map, not, opt},
multi::{many0, separated_list0},
Expand Down Expand Up @@ -81,8 +81,13 @@ fn alphanum1(i: &str) -> IResult<&str, &str, PError> {
take_while1(|x: char| x.is_alphanumeric() || x == '_' || x == '-')(i)
}

//val
//'val ue'
fn arg1(i: &str) -> IResult<&str, &str, PError> {
take_while1(|x: char| x.is_alphanumeric() || x == '_' || x == '-' || x == '.')(i)
alt((
take_while1(|x: char| x.is_alphanumeric() || x == '_' || x == '-' || x == '.' || x == '%'),
preceded(char('\''), cut(terminated(is_not("\'"), char('\'')))),
))(i)
}

// `key:val`
Expand Down Expand Up @@ -207,6 +212,16 @@ mod tests {
}
))
);
assert_eq!(
parse_arg("key:'val ue',"),
Ok((
",",
Arg {
key: "key",
val: "val ue"
}
))
);
assert!(parse_arg("key:,").is_err());
}

Expand Down
8 changes: 8 additions & 0 deletions src/formatting/value.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use super::formatter;
use super::unit::Unit;
use super::Metadata;
use chrono::{DateTime, Utc};
use chrono_tz::Tz;

#[derive(Debug, Clone)]
pub struct Value {
Expand All @@ -13,6 +15,7 @@ pub enum ValueInner {
Text(String),
Icon(String),
Number { val: f64, unit: Unit },
Datetime(DateTime<Utc>, Option<Tz>),
Flag,
}

Expand Down Expand Up @@ -46,6 +49,10 @@ impl Value {
Self::new(ValueInner::Flag)
}

pub fn datetime(datetime: DateTime<Utc>, tz: Option<Tz>) -> Self {
Self::new(ValueInner::Datetime(datetime, tz))
}

pub fn icon(icon: String) -> Self {
Self::new(ValueInner::Icon(icon))
}
Expand Down Expand Up @@ -108,6 +115,7 @@ impl Value {
match &self.inner {
ValueInner::Text(_) | ValueInner::Icon(_) => &formatter::DEFAULT_STRING_FORMATTER,
ValueInner::Number { .. } => &formatter::DEFAULT_NUMBER_FORMATTER,
ValueInner::Datetime { .. } => &*formatter::DEFAULT_DATETIME_FORMATTER,
ValueInner::Flag => &formatter::DEFAULT_FLAG_FORMATTER,
}
}
Expand Down

0 comments on commit 1067702

Please sign in to comment.