1use num_traits::{Num, NumAssignOps, SaturatingSub};
62use tokio::sync::mpsc;
63
64use super::prelude::*;
65use crate::{
66 formatting::Format,
67 subprocess::{spawn_shell, spawn_shell_sync},
68};
69use std::time::Instant;
70
71make_log_macro!(debug, "pomodoro");
72
73#[derive(Deserialize, Debug, SmartDefault)]
74#[serde(deny_unknown_fields, default)]
75pub struct Config {
76 pub format: FormatConfig,
77 pub pomodoro_format: FormatConfig,
78 pub break_format: FormatConfig,
79 #[default("Pomodoro over! Take a break!".into())]
80 pub message: String,
81 #[default("Break over! Time to work!".into())]
82 pub break_message: String,
83 pub notify_cmd: Option<String>,
84 pub blocking_cmd: bool,
85}
86
87enum PomodoroState {
88 Idle,
89 Prompt,
90 Notify,
91 Break,
92 PomodoroRunning,
93 PomodoroPaused,
94}
95
96impl PomodoroState {
97 fn get_block_state(&self) -> State {
98 use PomodoroState::*;
99 match self {
100 Idle | PomodoroPaused => State::Idle,
101 Prompt => State::Warning,
102 Notify => State::Good,
103 Break | PomodoroRunning => State::Info,
104 }
105 }
106
107 fn get_status_icon(&self) -> Option<&'static str> {
108 use PomodoroState::*;
109 match self {
110 Idle => Some("pomodoro_stopped"),
111 Break => Some("pomodoro_break"),
112 PomodoroRunning => Some("pomodoro_started"),
113 PomodoroPaused => Some("pomodoro_paused"),
114 _ => None,
115 }
116 }
117}
118
119struct Block<'a> {
120 widget: Widget,
121 actions: mpsc::UnboundedReceiver<BlockAction>,
122 api: &'a CommonApi,
123 config: &'a Config,
124 state: PomodoroState,
125 format: Format,
126 pomodoro_format: Format,
127 break_format: Format,
128}
129
130impl Block<'_> {
131 async fn set_text(&mut self, additional_values: Values) -> Result<()> {
132 let mut values = map! {
133 "icon" => Value::icon("pomodoro"),
134 };
135 values.extend(additional_values);
136
137 if let Some(icon) = self.state.get_status_icon() {
138 values.insert("status_icon".into(), Value::icon(icon));
139 }
140 self.widget.set_format(match self.state {
141 PomodoroState::Idle | PomodoroState::Prompt | PomodoroState::Notify => {
142 self.format.clone()
143 }
144 PomodoroState::Break => self.break_format.clone(),
145 PomodoroState::PomodoroRunning | PomodoroState::PomodoroPaused => {
146 self.pomodoro_format.clone()
147 }
148 });
149 self.widget.state = self.state.get_block_state();
150 debug!("{:?}", values);
151 self.widget.set_values(values);
152 self.api.set_widget(self.widget.clone())
153 }
154
155 async fn wait_for_click(&mut self, button: &str) -> Result<()> {
156 while self.actions.recv().await.error("channel closed")? != button {}
157 Ok(())
158 }
159
160 async fn read_params(&mut self) -> Result<Option<(Duration, Duration, usize)>> {
161 self.state = PomodoroState::Prompt;
162 let task_len = match self.read_number(25, "Task length:").await? {
163 Some(task_len) => task_len,
164 None => return Ok(None),
165 };
166 let break_len = match self.read_number(5, "Break length:").await? {
167 Some(break_len) => break_len,
168 None => return Ok(None),
169 };
170 let pomodoros = match self.read_number(4, "Pomodoros:").await? {
171 Some(pomodoros) => pomodoros,
172 None => return Ok(None),
173 };
174 Ok(Some((
175 Duration::from_secs(task_len * 60),
176 Duration::from_secs(break_len * 60),
177 pomodoros,
178 )))
179 }
180
181 async fn read_number<T: Num + NumAssignOps + SaturatingSub + std::fmt::Display>(
182 &mut self,
183 mut number: T,
184 msg: &str,
185 ) -> Result<Option<T>> {
186 loop {
187 self.set_text(map! {"message" => Value::text(format!("{msg} {number}"))})
188 .await?;
189 match &*self.actions.recv().await.error("channel closed")? {
190 "_left" => break,
191 "_up" => number += T::one(),
192 "_down" => number = number.saturating_sub(&T::one()),
193 "_middle" | "_right" => return Ok(None),
194 _ => (),
195 }
196 }
197 Ok(Some(number))
198 }
199
200 async fn set_notification(&mut self, message: &str) -> Result<()> {
201 self.state = PomodoroState::Notify;
202 self.set_text(map! {"message" => Value::text(message.to_string())})
203 .await?;
204 if let Some(cmd) = &self.config.notify_cmd {
205 let cmd = cmd.replace("{msg}", message);
206 if self.config.blocking_cmd {
207 spawn_shell_sync(&cmd)
208 .await
209 .error("failed to run notify_cmd")?;
210 } else {
211 spawn_shell(&cmd).error("failed to run notify_cmd")?;
212 self.wait_for_click("_left").await?;
213 }
214 } else {
215 self.wait_for_click("_left").await?;
216 }
217 Ok(())
218 }
219
220 async fn run_pomodoro(
221 &mut self,
222 task_len: Duration,
223 break_len: Duration,
224 pomodoros: usize,
225 ) -> Result<()> {
226 let interval: Seconds = 1.into();
227 let mut update_timer = interval.timer();
228 for pomodoro in 0..pomodoros {
229 let mut total_elapsed = Duration::ZERO;
230 'pomodoro_run: loop {
231 self.state = PomodoroState::PomodoroRunning;
233 let timer = Instant::now();
234 loop {
235 let elapsed = timer.elapsed();
236 if total_elapsed + elapsed >= task_len {
237 break 'pomodoro_run;
238 }
239 let remaining_time = task_len - total_elapsed - elapsed;
240 let values = map! {
241 [if pomodoro != 0] "completed_pomodoros" => Value::number(pomodoro),
242 "time_remaining" => Value::duration(remaining_time),
243 };
244 self.set_text(values.clone()).await?;
245 select! {
246 _ = update_timer.tick() => (),
247 Some(action) = self.actions.recv() => match action.as_ref() {
248 "_middle" | "_right" => return Ok(()),
249 "_left" => {
250 self.state = PomodoroState::PomodoroPaused;
251 self.set_text(values).await?;
252 total_elapsed += timer.elapsed();
253 loop {
254 match self.actions.recv().await.as_deref() {
255 Some("_middle") | Some("_right") => return Ok(()),
256 Some("_left") => {
257 continue 'pomodoro_run;
258 },
259 _ => ()
260
261 }
262 }
263 },
264 _ => ()
265 }
266 }
267 }
268 }
269
270 self.set_notification(&self.config.message).await?;
272
273 if pomodoro == pomodoros - 1 {
275 break;
276 }
277
278 self.state = PomodoroState::Break;
280 let timer = Instant::now();
281 loop {
282 let elapsed = timer.elapsed();
283 if elapsed >= break_len {
284 break;
285 }
286 let remaining_time = break_len - elapsed;
287 self.set_text(map! {
288 "time_remaining" => Value::duration(remaining_time),
289 })
290 .await?;
291 select! {
292 _ = update_timer.tick() => (),
293 Some(action) = self.actions.recv() => match action.as_ref() {
294 "_middle" | "_right" => return Ok(()),
295 _ => ()
296 }
297 }
298 }
299
300 self.set_notification(&self.config.break_message).await?;
302 }
303
304 Ok(())
305 }
306}
307
308pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
309 api.set_default_actions(&[
310 (MouseButton::Left, None, "_left"),
311 (MouseButton::Middle, None, "_middle"),
312 (MouseButton::Right, None, "_right"),
313 (MouseButton::WheelUp, None, "_up"),
314 (MouseButton::WheelDown, None, "_down"),
315 ])?;
316
317 let format = config.format.clone().with_default(" $icon{ $message|} ")?;
318
319 let pomodoro_format = config.pomodoro_format.clone().with_default(
320 " $icon $status_icon{ $completed_pomodoros.tally()|} $time_remaining.duration(hms:true) ",
321 )?;
322
323 let break_format = config
324 .break_format
325 .clone()
326 .with_default(" $icon $status_icon Break: $time_remaining.duration(hms:true) ")?;
327
328 let widget = Widget::new();
329
330 let mut block = Block {
331 widget,
332 actions: api.get_actions()?,
333 api,
334 config,
335 state: PomodoroState::Idle,
336 format,
337 pomodoro_format,
338 break_format,
339 };
340
341 loop {
342 block.state = PomodoroState::Idle;
344 block.set_text(Values::default()).await?;
345
346 block.wait_for_click("_left").await?;
347
348 if let Some((task_len, break_len, pomodoros)) = block.read_params().await? {
349 block.run_pomodoro(task_len, break_len, pomodoros).await?;
350 }
351 }
352}