Skip to main content

i3status_rs/blocks/
tea_timer.rs

1//! Timer
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `format` | A string to customise the output of this block. See below for available placeholders. | <code>\" $icon {$time.duration(hms:true) \|}\"</code>
8//! `increment` | The numbers of seconds to add each time the block is clicked. | 30
9//! `done_cmd` | A command to run in `sh` when timer finishes. | None
10//!
11//! Placeholder            | Value                                                          | Type     | Unit
12//! -----------------------|----------------------------------------------------------------|----------|---------------
13//! `icon`                 | A static icon                                                  | Icon     | -
14//! `time`                 | The time remaining on the timer                                | Duration | -
15//! `hours` *DEPRECATED*   | The hours remaining on the timer                               | Text     | h
16//! `minutes` *DEPRECATED* | The minutes remaining on the timer                             | Text     | mn
17//! `seconds` *DEPRECATED* | The seconds remaining on the timer                             | Text     | s
18//!
19//! `time`, `hours`, `minutes`, and `seconds` are unset when the timer is inactive.
20//!
21//! `hours`, `minutes`, and `seconds` have been deprecated in favor of `time`.
22//!
23//! Action      | Default button
24//! ------------|---------------
25//! `increment` | Left / Wheel Up
26//! `decrement` | Wheel Down
27//! `reset`     | Right
28//!
29//! # Example
30//!
31//! ```toml
32//! [[block]]
33//! block = "tea_timer"
34//! format = " $icon {$minutes:$seconds |}"
35//! done_cmd = "notify-send 'Timer Finished'"
36//! ```
37//!
38//! # Icons Used
39//! - `tea` (`$icon`)
40
41use super::prelude::*;
42use crate::subprocess::spawn_shell;
43
44use std::time::{Duration, Instant};
45
46#[derive(Deserialize, Debug, SmartDefault)]
47#[serde(deny_unknown_fields, default)]
48pub struct Config {
49    pub format: FormatConfig,
50    pub increment: Option<u64>,
51    pub done_cmd: Option<String>,
52}
53
54pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
55    BlockPlan::new(vec![
56        OutputPlan::new(
57            "main",
58            config
59                .format
60                .with_default(" $icon {$time.duration(hms:true) |}")?,
61        )
62        // `time`, `hours`, `minutes` and `seconds` are only set while the
63        // timer is active, so they are not guaranteed.
64        .icon("icon", IconChoices::one(icons::TEA)),
65    ])
66}
67
68pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
69    let mut actions = api.get_actions()?;
70    api.set_default_actions(&[
71        (MouseButton::Left, None, "increment"),
72        (MouseButton::WheelUp, None, "increment"),
73        (MouseButton::WheelDown, None, "decrement"),
74        (MouseButton::Right, None, "reset"),
75    ])?;
76
77    let interval: Seconds = 1.into();
78    let mut timer = interval.timer();
79
80    let output_main = plan.output("main")?;
81    let format = output_main.format().clone();
82
83    let increment = Duration::from_secs(config.increment.unwrap_or(30));
84    let mut timer_end = Instant::now();
85
86    let mut timer_was_active = false;
87
88    loop {
89        let mut widget = output_main.new_widget();
90
91        let remaining_time = timer_end - Instant::now();
92        let is_timer_active = !remaining_time.is_zero();
93
94        if !is_timer_active
95            && timer_was_active
96            && let Some(cmd) = &config.done_cmd
97        {
98            spawn_shell(cmd).error("done_cmd error")?;
99        }
100        timer_was_active = is_timer_active;
101
102        let mut values = map!(
103            "icon" => Value::icon(icons::TEA),
104        );
105
106        if is_timer_active {
107            values.insert("time".into(), Value::duration(remaining_time));
108            let mut seconds = remaining_time.as_secs();
109
110            if format.contains_key("hours") {
111                let hours = seconds / 3_600;
112                values.insert("hours".into(), Value::text(format!("{hours:02}")));
113                seconds %= 3_600;
114            }
115
116            if format.contains_key("minutes") {
117                let minutes = seconds / 60;
118                values.insert("minutes".into(), Value::text(format!("{minutes:02}")));
119                seconds %= 60;
120            }
121
122            values.insert("seconds".into(), Value::text(format!("{seconds:02}")));
123        }
124
125        widget.set_values(values);
126
127        api.set_widget(widget)?;
128
129        select! {
130            _ = timer.tick(), if is_timer_active => (),
131            _ = api.wait_for_update_request() => (),
132            Some(action) = actions.recv() => {
133                let now = Instant::now();
134                match action.as_ref() {
135                    "increment" if is_timer_active => timer_end += increment,
136                    "increment" => timer_end = now + increment,
137                    "decrement" if is_timer_active => timer_end -= increment,
138                    "reset" => timer_end = now,
139                    _ => (),
140                }
141            }
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn plan_declares_main_output_with_tea_icon() {
152        let plan = prepare(&Config::default()).unwrap();
153        let declared: Vec<_> = plan.outputs().map(|o| o.id()).collect();
154        assert_eq!(declared, ["main"]);
155        let output = plan.output("main").unwrap();
156        assert_eq!(output.single_icon("icon").unwrap(), "tea");
157    }
158
159    #[test]
160    fn custom_format_is_respected() {
161        let config = Config {
162            format: " $minutes:$seconds ".parse().unwrap(),
163            ..Config::default()
164        };
165        let plan = prepare(&config).unwrap();
166        let output = plan.output("main").unwrap();
167        assert!(output.format().contains_key("minutes"));
168        assert!(output.format().contains_key("seconds"));
169        assert!(!output.format().contains_key("icon"));
170    }
171}