diff --git a/Cargo.lock b/Cargo.lock index 0554e89401..e68a54a10f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1256,6 +1256,7 @@ dependencies = [ "nix 0.28.0", "nom", "notmuch", + "num-traits", "pipewire", "regex", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 86255703ab..a5e8d494f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ neli-wifi = { version = "0.6", features = ["async"] } nix = { version = "0.28", features = ["fs", "process"] } nom = "7.1.2" notmuch = { version = "0.8", optional = true } +num-traits = "0.2" pipewire = { version = "0.8", default-features = false, optional = true } regex = "1.5" reqwest = { version = "0.11", features = ["json"] } diff --git a/src/blocks/pomodoro.rs b/src/blocks/pomodoro.rs index ae0ea70692..1c8a24d0fa 100644 --- a/src/blocks/pomodoro.rs +++ b/src/blocks/pomodoro.rs @@ -15,16 +15,21 @@ //! //! Key | Values | Default //! ----|--------|-------- -//! `format` | A string to customise the output of this block. | \" $icon{ $message|} \" +//! `format` | The format used when in idle, prompt, or notify states | \" $icon{ $message\|} \" +//! `pomodoro_format` | The format used when the pomodoro is running or paused | \" $icon $status_icon{ $completed_pomodoros.tally()\|} $time_remaining.duration(hms:true) \" +//! `break_format` |The format used when the pomodoro is during the break | \" $icon $status_icon Break: $time_remaining.duration(hms:true) \" //! `message` | Message when timer expires | `"Pomodoro over! Take a break!"` //! `break_message` | Message when break is over | `"Break over! Time to work!"` //! `notify_cmd` | A shell command to run as a notifier. `{msg}` will be substituted with either `message` or `break_message`. | `None` //! `blocking_cmd` | Is `notify_cmd` blocking? If it is, then pomodoro block will wait until the command finishes before proceeding. Otherwise, you will have to click on the block in order to proceed. | `false` //! -//! Placeholder | Value | Type -//! ------------|-------------------------------------|------ -//! `icon` | A static icon | Icon -//! `message` | Current message | Text +//! Placeholder | Value | Type | Supported by +//! ----------------------|-----------------------------------------------|----------|-------------- +//! `icon` | A static icon | Icon | All formats +//! `status_icon` | An icon that reflects the pomodoro state | Icon | `pomodoro_format`, `break_format` +//! `message` | Current message | Text | `format` +//! `time_remaining` | How much time is left (minutes) | Duration | `pomodoro_format`, `break_format` +//! `completed_pomodoros` | The number of completed pomodoros | Number | `pomodoro_format` //! //! # Example //! @@ -52,21 +57,25 @@ //! - `pomodoro_stopped` //! - `pomodoro_paused` //! - `pomodoro_break` -//! -//! # TODO -//! - Use different icons. -//! - Use format strings. +use num_traits::{Num, NumAssignOps, SaturatingSub}; use tokio::sync::mpsc; use super::prelude::*; -use crate::subprocess::{spawn_shell, spawn_shell_sync}; +use crate::{ + formatting::Format, + subprocess::{spawn_shell, spawn_shell_sync}, +}; use std::time::Instant; +make_log_macro!(debug, "pomodoro"); + #[derive(Deserialize, Debug, SmartDefault)] #[serde(deny_unknown_fields, default)] pub struct Config { pub format: FormatConfig, + pub pomodoro_format: FormatConfig, + pub break_format: FormatConfig, #[default("Pomodoro over! Take a break!".into())] pub message: String, #[default("Break over! Time to work!".into())] @@ -75,21 +84,70 @@ pub struct Config { pub blocking_cmd: bool, } +enum PomodoroState { + Idle, + Prompt, + Notify, + Break, + PomodoroRunning, + PomodoroPaused, +} + +impl PomodoroState { + fn get_block_state(&self) -> State { + use PomodoroState::*; + match self { + Idle | PomodoroPaused => State::Idle, + Prompt => State::Warning, + Notify => State::Good, + Break | PomodoroRunning => State::Info, + } + } + + fn get_status_icon(&self) -> Option> { + use PomodoroState::*; + match self { + Idle => Some("pomodoro_stopped".into()), + Break => Some("pomodoro_break".into()), + PomodoroRunning => Some("pomodoro_started".into()), + PomodoroPaused => Some("pomodoro_paused".into()), + _ => None, + } + } +} + struct Block<'a> { widget: Widget, actions: mpsc::UnboundedReceiver, api: &'a CommonApi, - block_config: &'a Config, + config: &'a Config, + state: PomodoroState, + format: Format, + pomodoro_format: Format, + break_format: Format, } impl Block<'_> { - async fn set_text(&mut self, text: String) -> Result<()> { - let mut values = map!( + async fn set_text(&mut self, additional_values: Values) -> Result<()> { + let mut values = map! { "icon" => Value::icon("pomodoro"), - ); - if !text.is_empty() { - values.insert("message".into(), Value::text(text)); + }; + values.extend(additional_values); + + if let Some(icon) = self.state.get_status_icon() { + values.insert("status_icon".into(), Value::icon(icon)); } + self.widget.set_format(match self.state { + PomodoroState::Idle | PomodoroState::Prompt | PomodoroState::Notify => { + self.format.clone() + } + PomodoroState::Break => self.break_format.clone(), + PomodoroState::PomodoroRunning | PomodoroState::PomodoroPaused => { + self.pomodoro_format.clone() + } + }); + self.widget.state = self.state.get_block_state(); + debug!("{:?}", values); self.widget.set_values(values); self.api.set_widget(self.widget.clone()) } @@ -99,78 +157,118 @@ impl Block<'_> { Ok(()) } - async fn read_params(&mut self) -> Result<(Duration, Duration, u64)> { - let task_len = self.read_u64(25, "Task length:").await?; - let break_len = self.read_u64(5, "Break length:").await?; - let pomodoros = self.read_u64(4, "Pomodoros:").await?; - Ok(( + async fn read_params(&mut self) -> Result> { + self.state = PomodoroState::Prompt; + let task_len = match self.read_number(25, "Task length:").await? { + Some(task_len) => task_len, + None => return Ok(None), + }; + let break_len = match self.read_number(5, "Break length:").await? { + Some(break_len) => break_len, + None => return Ok(None), + }; + let pomodoros = match self.read_number(4, "Pomodoros:").await? { + Some(pomodoros) => pomodoros, + None => return Ok(None), + }; + Ok(Some(( Duration::from_secs(task_len * 60), Duration::from_secs(break_len * 60), pomodoros, - )) + ))) } - async fn read_u64(&mut self, mut number: u64, msg: &str) -> Result { + async fn read_number( + &mut self, + mut number: T, + msg: &str, + ) -> Result> { loop { - self.set_text(format!("{msg} {number}")).await?; + self.set_text(map! {"message" => Value::text(format!("{msg} {number}"))}) + .await?; match &*self.actions.recv().await.error("channel closed")? { "_left" => break, - "_up" => number += 1, - "_down" => number = number.saturating_sub(1), + "_up" => number += T::one(), + "_down" => number = number.saturating_sub(&T::one()), + "_middle" | "_right" => return Ok(None), _ => (), } } - Ok(number) + Ok(Some(number)) + } + + async fn set_notification(&mut self, message: &str) -> Result<()> { + self.state = PomodoroState::Notify; + self.set_text(map! {"message" => Value::text(message.to_string())}) + .await?; + if let Some(cmd) = &self.config.notify_cmd { + let cmd = cmd.replace("{msg}", message); + if self.config.blocking_cmd { + spawn_shell_sync(&cmd) + .await + .error("failed to run notify_cmd")?; + } else { + spawn_shell(&cmd).error("failed to run notify_cmd")?; + self.wait_for_click("_left").await?; + } + } else { + self.wait_for_click("_left").await?; + } + Ok(()) } async fn run_pomodoro( &mut self, task_len: Duration, break_len: Duration, - pomodoros: u64, + pomodoros: usize, ) -> Result<()> { + let interval: Seconds = 1.into(); + let mut update_timer = interval.timer(); for pomodoro in 0..pomodoros { - // Task timer - self.widget.state = State::Idle; - let timer = Instant::now(); - loop { - let elapsed = timer.elapsed(); - if elapsed >= task_len { - break; - } - let left = task_len - elapsed; - let text = if pomodoro == 0 { - format!("{} min", (left.as_secs() + 59) / 60,) - } else { - format!( - "{} {} min", - "|".repeat(pomodoro as usize), - (left.as_secs() + 59) / 60, - ) - }; - self.set_text(text).await?; - select! { - _ = sleep(Duration::from_secs(10)) => (), - _ = self.wait_for_click("_middle") => return Ok(()), + let mut total_elapsed = Duration::ZERO; + 'pomodoro_run: loop { + // Task timer + self.state = PomodoroState::PomodoroRunning; + let timer = Instant::now(); + loop { + let elapsed = timer.elapsed(); + if total_elapsed + elapsed >= task_len { + break 'pomodoro_run; + } + let remaining_time = task_len - total_elapsed - elapsed; + let values = map! { + [if pomodoro != 0] "completed_pomodoros" => Value::number(pomodoro), + "time_remaining" => Value::duration(remaining_time), + }; + self.set_text(values.clone()).await?; + select! { + _ = update_timer.tick() => (), + Some(action) = self.actions.recv() => match action.as_ref() { + "_middle" | "_right" => return Ok(()), + "_left" => { + self.state = PomodoroState::PomodoroPaused; + self.set_text(values).await?; + total_elapsed += timer.elapsed(); + loop { + match self.actions.recv().await.as_deref() { + Some("_middle") | Some("_right") => return Ok(()), + Some("_left") => { + continue 'pomodoro_run; + }, + _ => () + + } + } + }, + _ => () + } + } } } // Show break message - self.widget.state = State::Good; - self.set_text(self.block_config.message.clone()).await?; - if let Some(cmd) = &self.block_config.notify_cmd { - let cmd = cmd.replace("{msg}", &self.block_config.message); - if self.block_config.blocking_cmd { - spawn_shell_sync(&cmd) - .await - .error("failed to run notify_cmd")?; - } else { - spawn_shell(&cmd).error("failed to run notify_cmd")?; - self.wait_for_click("_left").await?; - } - } else { - self.wait_for_click("_left").await?; - } + self.set_notification(&self.config.message).await?; // No break after the last pomodoro if pomodoro == pomodoros - 1 { @@ -178,45 +276,36 @@ impl Block<'_> { } // Break timer + self.state = PomodoroState::Break; let timer = Instant::now(); loop { let elapsed = timer.elapsed(); if elapsed >= break_len { break; } - let left = break_len - elapsed; - self.set_text(format!("Break: {} min", (left.as_secs() + 59) / 60,)) - .await?; + let remaining_time = break_len - elapsed; + self.set_text(map! { + "time_remaining" => Value::duration(remaining_time), + }) + .await?; select! { - _ = sleep(Duration::from_secs(10)) => (), - _ = self.wait_for_click("_middle") => return Ok(()), + _ = update_timer.tick() => (), + Some(action) = self.actions.recv() => match action.as_ref() { + "_middle" | "_right" => return Ok(()), + _ => () + } } } // Show task message - self.widget.state = State::Good; - self.set_text(self.block_config.break_message.clone()) - .await?; - if let Some(cmd) = &self.block_config.notify_cmd { - let cmd = cmd.replace("{msg}", &self.block_config.break_message); - if self.block_config.blocking_cmd { - spawn_shell_sync(&cmd) - .await - .error("failed to run notify_cmd")?; - } else { - spawn_shell(&cmd).error("failed to run notify_cmd")?; - self.wait_for_click("_left").await?; - } - } else { - self.wait_for_click("_left").await?; - } + self.set_notification(&self.config.break_message).await?; } Ok(()) } } -pub async fn run(block_config: &Config, api: &CommonApi) -> Result<()> { +pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { api.set_default_actions(&[ (MouseButton::Left, None, "_left"), (MouseButton::Middle, None, "_middle"), @@ -225,27 +314,39 @@ pub async fn run(block_config: &Config, api: &CommonApi) -> Result<()> { (MouseButton::WheelDown, None, "_down"), ])?; - let format = block_config - .format + let format = config.format.clone().with_default(" $icon{ $message|} ")?; + + let pomodoro_format = config.pomodoro_format.clone().with_default( + " $icon $status_icon{ $completed_pomodoros.tally()|} $time_remaining.duration(hms:true) ", + )?; + + let break_format = config + .break_format .clone() - .with_default(" $icon{ $message|} ")?; - let widget = Widget::new().with_format(format); + .with_default(" $icon $status_icon Break: $time_remaining.duration(hms:true) ")?; + + let widget = Widget::new(); let mut block = Block { widget, actions: api.get_actions()?, api, - block_config, + config, + state: PomodoroState::Idle, + format, + pomodoro_format, + break_format, }; loop { // Send collaped block - block.widget.state = State::Idle; - block.set_text(String::new()).await?; + block.state = PomodoroState::Idle; + block.set_text(Values::default()).await?; block.wait_for_click("_left").await?; - let (task_len, break_len, pomodoros) = block.read_params().await?; - block.run_pomodoro(task_len, break_len, pomodoros).await?; + if let Some((task_len, break_len, pomodoros)) = block.read_params().await? { + block.run_pomodoro(task_len, break_len, pomodoros).await?; + } } }