i3status_rs/formatting/formatter/
datetime.rs1use 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 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}