i3status_rs/blocks/
tea_timer.rs1use 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 .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}