i3status_rs/formatting/formatter/
datetime.rs

1use chrono::format::{Fixed, Item, StrftimeItems};
2use chrono::{DateTime, Datelike, Local, LocalResult, Locale, TimeZone, Timelike};
3use chrono_tz::{OffsetName, 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);
38                }
39                "locale" | "l" => {
40                    locale = Some(arg.val);
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>) -> Item
99    where
100        Self: TimeZone;
101}
102
103impl TimezoneName for Tz {
104    fn timezone_name(datetime: &DateTime<Tz>) -> Item {
105        Item::Literal(datetime.offset().abbreviation())
106    }
107}
108
109impl TimezoneName for Local {
110    fn timezone_name(datetime: &DateTime<Local>) -> Item {
111        let fallback = Item::Fixed(Fixed::TimezoneName);
112        let Ok(tz_name) = iana_time_zone::get_timezone() else {
113            error!("Could not get local timezone");
114            return fallback;
115        };
116        let tz = match tz_name.parse::<Tz>() {
117            Ok(tz) => tz,
118            Err(e) => {
119                error!("{}", e);
120                return fallback;
121            }
122        };
123
124        match tz.with_ymd_and_hms(
125            datetime.year(),
126            datetime.month(),
127            datetime.day(),
128            datetime.hour(),
129            datetime.minute(),
130            datetime.second(),
131        ) {
132            LocalResult::Single(tz_datetime) => {
133                Item::OwnedLiteral(tz_datetime.offset().abbreviation().into())
134            }
135            LocalResult::Ambiguous(..) => {
136                error!("Timezone is ambiguous");
137                fallback
138            }
139            LocalResult::None => {
140                error!("Timezone is none");
141                fallback
142            }
143        }
144    }
145}
146
147fn borrow_item<'a>(item: &'a Item) -> Item<'a> {
148    match item {
149        Item::Literal(s) => Item::Literal(s),
150        Item::OwnedLiteral(s) => Item::Literal(s),
151        Item::Space(s) => Item::Space(s),
152        Item::OwnedSpace(s) => Item::Space(s),
153        Item::Numeric(n, p) => Item::Numeric(n.clone(), *p),
154        Item::Fixed(f) => Item::Fixed(f.clone()),
155        Item::Error => Item::Error,
156    }
157}
158
159impl Formatter for DatetimeFormatter {
160    fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
161        #[allow(clippy::unnecessary_wraps)]
162        fn for_generic_datetime<T>(
163            this: &DatetimeFormatter,
164            datetime: DateTime<T>,
165        ) -> Result<String, FormatError>
166        where
167            T: TimeZone + TimezoneName,
168            T::Offset: Display,
169        {
170            Ok(match this {
171                DatetimeFormatter::Chrono { items, locale } => {
172                    let new_items = items.iter().map(|item| match item {
173                        Item::Fixed(Fixed::TimezoneName) => T::timezone_name(&datetime),
174                        item => borrow_item(item),
175                    });
176                    match *locale {
177                        Some(locale) => datetime
178                            .format_localized_with_items(new_items, locale)
179                            .to_string(),
180                        None => datetime.format_with_items(new_items).to_string(),
181                    }
182                }
183                #[cfg(feature = "icu_calendar")]
184                DatetimeFormatter::Icu { locale, length } => {
185                    use chrono::Datelike as _;
186                    let date = icu_calendar::Date::try_new_iso_date(
187                        datetime.year(),
188                        datetime.month() as u8,
189                        datetime.day() as u8,
190                    )
191                    .ok()
192                    .error("Current date should be a valid date")?;
193                    let date = date.to_any();
194                    let dft =
195                        icu_datetime::DateFormatter::try_new_with_length(&locale.into(), *length)
196                            .ok()
197                            .error("locale should be present in compiled data")?;
198                    dft.format_to_string(&date)
199                        .ok()
200                        .error("formatting date using icu failed")?
201                }
202            })
203        }
204        match val {
205            Value::Datetime(datetime, timezone) => match timezone {
206                Some(tz) => for_generic_datetime(self, datetime.with_timezone(tz)),
207                None => for_generic_datetime(self, datetime.with_timezone(&Local)),
208            },
209            other => Err(FormatError::IncompatibleFormatter {
210                ty: other.type_name(),
211                fmt: "datetime",
212            }),
213        }
214    }
215}