use chrono::{DateTime, FixedOffset, NaiveDate};
use crate::payload::{Body, Payload, TextBlockData};
use crate::time as t;
pub const DEFAULT_FORMAT: &str = "%H:%M";
pub fn now_in(timezone: Option<&str>) -> DateTime<FixedOffset> {
t::now_in(timezone)
}
pub fn parse_tz(name: &str) -> Option<chrono_tz::Tz> {
t::parse_tz(Some(name))
}
pub fn safe_format(dt: &DateTime<FixedOffset>, fmt: &str) -> String {
safe_format_with_locale(dt, fmt, None)
}
pub fn safe_format_with_locale(
dt: &DateTime<FixedOffset>,
fmt: &str,
locale: Option<&str>,
) -> String {
let primary = t::format_local(dt, fmt, locale);
if primary.is_empty() {
t::format_local(dt, DEFAULT_FORMAT, locale)
} else {
primary
}
}
pub fn julian_day(date: NaiveDate) -> i64 {
let epoch = NaiveDate::from_ymd_opt(2000, 1, 1).expect("2000-01-01 always valid");
2_451_545 + (date - epoch).num_days()
}
pub fn placeholder(msg: &str) -> Payload {
Payload {
icon: None,
status: None,
format: None,
body: Body::TextBlock(TextBlockData {
lines: vec![
format!("⚠ {msg}"),
"check [widget.options] in config".into(),
],
}),
}
}
pub fn parse_options<T: serde::de::DeserializeOwned + Default>(
raw: Option<&toml::Value>,
) -> Result<T, String> {
match raw {
None => Ok(T::default()),
Some(value) => value
.clone()
.try_into::<T>()
.map_err(|e| format!("invalid options: {e}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
#[test]
fn safe_format_falls_back_on_invalid_directive() {
let dt = Utc
.with_ymd_and_hms(2026, 4, 22, 12, 34, 0)
.unwrap()
.fixed_offset();
assert_eq!(safe_format(&dt, "%Q"), "12:34");
}
#[test]
fn now_in_invalid_tz_does_not_panic() {
let _ = now_in(Some("Not/AZone"));
}
#[test]
fn julian_day_of_j2000_epoch() {
let d = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
assert_eq!(julian_day(d), 2_451_545);
}
}