i3status_rs/formatting/formatter/
datetime.rs

1use chrono::format::{Fixed, Item, StrftimeItems};
2use chrono::{DateTime, Local, Locale, TimeZone};
3use chrono_tz::{OffsetName as _, Tz};
4
5use std::fmt::Display;
6use std::sync::LazyLock;
7
8use super::*;
9
10make_log_macro!(error, "datetime");
11
12const DEFAULT_DATETIME_FORMAT: &str = "%a %d/%m %R";
13
14pub static DEFAULT_DATETIME_FORMATTER: LazyLock<DatetimeFormatter> =
15    LazyLock::new(|| DatetimeFormatter::new(Some(DEFAULT_DATETIME_FORMAT), None).unwrap());
16
17#[derive(Debug)]
18pub enum DatetimeFormatter {
19    Chrono {
20        items: Vec<Item<'static>>,
21        locale: Option<Locale>,
22    },
23    #[cfg(feature = "icu_calendar")]
24    Icu {
25        length: icu_datetime::options::length::Date,
26        locale: icu_locid::Locale,
27    },
28}
29
30impl DatetimeFormatter {
31    pub(super) fn from_args(args: &[Arg]) -> Result<Self> {
32        let mut format = None;
33        let mut locale = None;
34        for arg in args {
35            match arg.key {
36                "format" | "f" => {
37                    format = Some(arg.val.error("format must be specified")?);
38                }
39                "locale" | "l" => {
40                    locale = Some(arg.val.error("locale must be specified")?);
41                }
42                other => {
43                    return Err(Error::new(format!(
44                        "Unknown argument for 'datetime': '{other}'"
45                    )));
46                }
47            }
48        }
49        Self::new(format, locale)
50    }
51
52    fn new(format: Option<&str>, locale: Option<&str>) -> Result<Self> {
53        let (items, locale) = match locale {
54            Some(locale) => {
55                #[cfg(feature = "icu_calendar")]
56                let Ok(locale) = locale.try_into() else {
57                    use std::str::FromStr as _;
58                    // try with icu4x
59                    let locale = icu_locid::Locale::from_str(locale)
60                        .ok()
61                        .error("invalid locale")?;
62                    let length = match format {
63                        Some("full") => icu_datetime::options::length::Date::Full,
64                        None | Some("long") => icu_datetime::options::length::Date::Long,
65                        Some("medium") => icu_datetime::options::length::Date::Medium,
66                        Some("short") => icu_datetime::options::length::Date::Short,
67                        _ => return Err(Error::new("Unknown format option for icu based locale")),
68                    };
69                    return Ok(Self::Icu { locale, length });
70                };
71                #[cfg(not(feature = "icu_calendar"))]
72                let locale = locale.try_into().ok().error("invalid locale")?;
73                (
74                    StrftimeItems::new_with_locale(
75                        format.unwrap_or(DEFAULT_DATETIME_FORMAT),
76                        locale,
77                    ),
78                    Some(locale),
79                )
80            }
81            None => (
82                StrftimeItems::new(format.unwrap_or(DEFAULT_DATETIME_FORMAT)),
83                None,
84            ),
85        };
86
87        Ok(Self::Chrono {
88            items: items.parse_to_owned().error(format!(
89                "Invalid format: \"{}\"",
90                format.unwrap_or(DEFAULT_DATETIME_FORMAT)
91            ))?,
92            locale,
93        })
94    }
95}
96
97pub(crate) trait TimezoneName {
98    fn timezone_name(datetime: &DateTime<Self>) -> Result<Item>
99    where
100        Self: TimeZone;
101}
102
103impl TimezoneName for Tz {
104    fn timezone_name(datetime: &DateTime<Tz>) -> Result<Item> {
105        Ok(Item::Literal(
106            datetime
107                .offset()
108                .abbreviation()
109                .error("Timezone name unknown")?,
110        ))
111    }
112}
113
114impl TimezoneName for Local {
115    fn timezone_name(datetime: &DateTime<Local>) -> Result<Item> {
116        let tz_name = iana_time_zone::get_timezone().error("Could not get local timezone")?;
117        let tz = tz_name
118            .parse::<Tz>()
119            .error("Could not parse local timezone")?;
120        Tz::timezone_name(&datetime.with_timezone(&tz)).map(|x| x.to_owned())
121    }
122}
123
124fn borrow_item<'a>(item: &'a Item) -> Item<'a> {
125    match item {
126        Item::Literal(s) => Item::Literal(s),
127        Item::OwnedLiteral(s) => Item::Literal(s),
128        Item::Space(s) => Item::Space(s),
129        Item::OwnedSpace(s) => Item::Space(s),
130        Item::Numeric(n, p) => Item::Numeric(n.clone(), *p),
131        Item::Fixed(f) => Item::Fixed(f.clone()),
132        Item::Error => Item::Error,
133    }
134}
135
136impl Formatter for DatetimeFormatter {
137    fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
138        #[allow(clippy::unnecessary_wraps)]
139        fn for_generic_datetime<T>(
140            this: &DatetimeFormatter,
141            datetime: DateTime<T>,
142        ) -> Result<String, FormatError>
143        where
144            T: TimeZone + TimezoneName,
145            T::Offset: Display,
146        {
147            Ok(match this {
148                DatetimeFormatter::Chrono { items, locale } => {
149                    let new_items = items.iter().map(|item| match item {
150                        Item::Fixed(Fixed::TimezoneName) => match T::timezone_name(&datetime) {
151                            Ok(name) => name,
152                            Err(e) => {
153                                error!("{e}");
154                                Item::Fixed(Fixed::TimezoneName)
155                            }
156                        },
157                        item => borrow_item(item),
158                    });
159                    match *locale {
160                        Some(locale) => datetime
161                            .format_localized_with_items(new_items, locale)
162                            .to_string(),
163                        None => datetime.format_with_items(new_items).to_string(),
164                    }
165                }
166                #[cfg(feature = "icu_calendar")]
167                DatetimeFormatter::Icu { locale, length } => {
168                    use chrono::Datelike as _;
169                    let date = icu_calendar::Date::try_new_iso_date(
170                        datetime.year(),
171                        datetime.month() as u8,
172                        datetime.day() as u8,
173                    )
174                    .ok()
175                    .error("Current date should be a valid date")?;
176                    let date = date.to_any();
177                    let dft =
178                        icu_datetime::DateFormatter::try_new_with_length(&locale.into(), *length)
179                            .ok()
180                            .error("locale should be present in compiled data")?;
181                    dft.format_to_string(&date)
182                        .ok()
183                        .error("formatting date using icu failed")?
184                }
185            })
186        }
187        match val {
188            Value::Datetime(datetime, timezone) => match timezone {
189                Some(tz) => for_generic_datetime(self, datetime.with_timezone(tz)),
190                None => for_generic_datetime(self, datetime.with_timezone(&Local)),
191            },
192            other => Err(FormatError::IncompatibleFormatter {
193                ty: other.type_name(),
194                fmt: "datetime",
195            }),
196        }
197    }
198}