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