i3status_rs/blocks/
time.rs1use chrono::{Timelike as _, Utc};
56use chrono_tz::Tz;
57
58use super::prelude::*;
59
60#[derive(Deserialize, Debug, SmartDefault)]
61#[serde(default)]
62pub struct Config {
63 #[serde(flatten)]
64 pub formats: MaybeMultiFormatConfig,
65 #[default(10.into())]
66 pub interval: Seconds,
67 pub timezone: Option<Timezone>,
68}
69
70#[derive(Deserialize, Debug, Clone)]
71#[serde(untagged)]
72pub enum Timezone {
73 Timezone(Tz),
74 Timezones(Vec<Tz>),
75}
76
77pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
78 let declare = |output: OutputPlan| output.icon("icon", IconChoices::one(icons::TIME));
79 let formats = config
80 .formats
81 .with_default(" $icon $timestamp.datetime() ")?;
82 BlockPlan::new(format_outputs(formats, declare))
83}
84
85pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
86 let mut actions = api.get_actions()?;
87 api.set_default_actions(&[
88 (MouseButton::Left, None, "next_timezone"),
89 (MouseButton::Right, None, "prev_timezone"),
90 ])?;
91
92 let mut formats = FormatRotation::new(plan)?;
93
94 let timezones = match config.timezone.clone() {
95 Some(tzs) => match tzs {
96 Timezone::Timezone(tz) => vec![tz],
97 Timezone::Timezones(tzs) => tzs,
98 },
99 None => Vec::new(),
100 };
101
102 let prev_step_length = timezones.len().saturating_sub(2);
103
104 let mut timezone_iter = timezones.iter().cycle();
105
106 let mut timezone = timezone_iter.next();
107
108 let interval_seconds = config.interval.seconds().max(1);
109
110 let mut timer = tokio::time::interval_at(
111 tokio::time::Instant::now() + Duration::from_secs(interval_seconds),
112 Duration::from_secs(interval_seconds),
113 );
114 timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
115
116 loop {
117 let output = formats.current();
118 let mut widget = output.new_widget();
119 let now = Utc::now();
120
121 widget.set_values(map! {
122 "icon" => Value::icon(icons::TIME),
123 "timestamp" => Value::datetime(now, timezone.copied())
124 });
125
126 api.set_widget(widget)?;
127
128 let phase = now.second() as u64 % interval_seconds;
129 if phase != 0 {
130 timer.reset_after(Duration::from_secs(interval_seconds - phase));
131 }
132
133 tokio::select! {
134 _ = timer.tick() => (),
135 _ = api.wait_for_update_request() => (),
136 Some(action) = actions.recv() => match action.as_ref() {
137 "next_timezone" => {
138 timezone = timezone_iter.next();
139 },
140 "prev_timezone" => {
141 timezone = timezone_iter.nth(prev_step_length);
142 },
143 "next_format" => {
144 formats.next();
145 },
146 "prev_format" => {
147 formats.prev();
148 },
149 _ => (),
150 }
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn config(toml_str: &str) -> Config {
160 toml::from_str(toml_str).unwrap()
161 }
162
163 #[test]
164 fn plan_declares_single_output_with_time_icon() {
165 let plan = prepare(&Config::default()).unwrap();
166 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
167 assert_eq!(ids, ["format"]);
168 let main = plan.output("format").unwrap();
169 assert_eq!(main.single_icon("icon").unwrap(), "time");
170 assert!(main.format().contains_key("timestamp"));
171 }
172
173 #[test]
174 fn plan_uses_configured_format() {
175 let plan = prepare(&config(r#"format = " $timestamp.datetime(f:%R) ""#)).unwrap();
176 let main = plan.output("format").unwrap();
177 assert!(main.format().contains_key("timestamp"));
178 assert!(!main.format().contains_key("icon"));
179 }
180
181 #[test]
182 fn every_configured_format_gets_an_output() {
183 let plan = prepare(&config(
184 r#"format = [" $icon ", " $icon $timestamp.datetime(f:%R) "]"#,
185 ))
186 .unwrap();
187 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
188 assert_eq!(ids, ["format", "format2"]);
189 let second = plan.output("format2").unwrap();
190 assert!(second.format().contains_key("timestamp"));
191 assert_eq!(second.single_icon("icon").unwrap(), "time");
192 }
193}