Skip to main content

i3status_rs/blocks/
time.rs

1//! The current time.
2//!
3//! # Configuration
4//!
5//! Key        | Values | Default
6//! -----------|--------|--------
7//! `format`   | [MultiFormat][MaybeMultiFormatConfig] string. See [chrono docs](https://docs.rs/chrono/0.3.0/chrono/format/strftime/index.html#specifiers) for all options. | `[" $icon $timestamp.datetime() "]`
8//! `interval` | Update interval in seconds | `10`
9//! `timezone` | A timezone specifier (e.g. "Europe/Lisbon") | Local timezone
10//!
11//! Placeholder   | Value                                       | Type     | Unit
12//! --------------|---------------------------------------------|----------|-----
13//! `icon`        | A static icon                               | Icon     | -
14//! `timestamp`   | The current time                            | Datetime | -
15//!
16//! Action          | Default button
17//! ----------------|---------------
18//! `next_timezone` | Left
19//! `prev_timezone` | Right
20//! `next_format`   | -
21//! `prev_format`   | -
22//!
23//! # Example
24//!
25//! ```toml
26//! [[block]]
27//! block = "time"
28//! interval = 60
29//! [block.format]
30//! full = " $icon $timestamp.datetime(f:'%a %Y-%m-%d %R %Z', l:fr_BE) "
31//! short = " $icon $timestamp.datetime(f:%R) "
32//! ```
33//!
34//! # Non Gregorian calendars
35//!
36//! You can use calendars other than the Gregorian calendar by adding the calendar specifier in the locale string. When using
37//! this feature you can't use chrono style format string, and you should use one of the options provided by
38//! the `icu4x` crate: `short`, `medium`, `long`, `full`.
39//! If you set `precision` to `hours`/`hour`/`h`, `minutes`/`minute`/`m`, or `seconds`/`second`/`s` then then the datetime will be formatted accordingly, otherwise only the date will be displayed.
40//!
41//! ** Only available using feature `icu_calendar`. **
42//!
43//! ## Example
44//!
45//! ```toml
46//! [[block]]
47//! block = "time"
48//! interval = 60
49//! format = "$timestamp.datetime(locale:'fa-IR-u-ca-persian', f:'full', precision: minutes)"
50//! ```
51//!
52//! # Icons Used
53//! - `time` (`$icon`)
54
55use 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}