diff --git a/src/cli.rs b/src/cli.rs index 3926105..5490a22 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,6 +40,12 @@ pub enum Command { /// List timers #[clap(visible_alias = "l")] List, + /// Toggle timer + #[clap(visible_alias = "t")] + Toggle { + /// name of the timer to toggle + name: String, + }, /// Remove a timer #[clap(visible_alias = "r")] Remove { @@ -83,15 +89,14 @@ pub enum PomodoroCommand { /// List the pomodoro settings and remaining duration #[clap(visible_alias = "l")] List, -} - -fn get_stream(socket_path: &String) -> Result { - UnixStream::connect(socket_path) - .context(format!("Could not connect to socket {}!", socket_path)) + /// Toggle pomodoro + #[clap(visible_alias = "t")] + Toggle, } pub fn send_command(socket_path: &String, command: OtherCommand) -> Result { - let stream = get_stream(socket_path)?; + let stream = UnixStream::connect(socket_path) + .context(format!("Could not connect to socket {}!", socket_path))?; serde_cbor::to_writer(&stream, &command).context("Could not write command!")?; stream .shutdown(Shutdown::Write) diff --git a/src/daemon.rs b/src/daemon.rs index e9cecbb..40fb853 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,4 +1,4 @@ -use crate::notification::send_notifictation; +use crate::helper::send_notifictation; use crate::pomodoro::Pomodoro; pub use crate::timer::Timer; use anyhow::Context; @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::fs::File; use std::path::Path; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::{ io::Write, os::unix::net::{UnixListener, UnixStream}, @@ -18,6 +18,7 @@ use std::{ #[derive(Debug, Serialize, Deserialize)] pub enum Command { Add(Box, Duration), + Toggle(Box), Remove(Box), List, PomodoroStart { @@ -28,6 +29,7 @@ pub enum Command { }, PomodoroRemove, PomodoroList, + PomodoroToggle, } #[derive(Debug, Serialize, Deserialize)] @@ -37,33 +39,20 @@ pub enum Answer { Pomodoro(Option), } -impl Display for Answer { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - match self { - Answer::Ok => write!(f, ""), - Answer::Timers(timers) => { - if timers.is_empty() { - writeln!(f, "No timers running.") - } else { - let strings: Vec = - timers.iter().map(|timer| timer.to_string()).collect(); - writeln!(f, "{}", strings.join("\n")) - } - } - Answer::Pomodoro(pomodoro) => match pomodoro { - Some(p) => write!(f, "{}", p), - None => write!(f, "No pomodoro running."), - }, - } - } -} - #[derive(Debug, thiserror::Error, Serialize, Deserialize)] pub enum AnswerErr { #[error("Timer with name '{}' already exists", .0)] TimerAlreadyExist(Box), #[error("No timer with the name '{}' exists", .0)] NoSuchTimer(Box), + #[error("No pomodoro running")] + NoPomodoro, +} + +#[derive(Debug, thiserror::Error)] +pub enum DaemonErr { + #[error("Daemon already running!")] + AlreadyRunning, } pub struct Daemon { @@ -75,12 +64,6 @@ pub struct Daemon { notify: bool, } -#[derive(Debug, thiserror::Error)] -pub enum DaemonErr { - #[error("Daemon already running!")] - AlreadyRunning, -} - impl Daemon { pub fn new(socket: String, pid_file: String, no_notify: bool) -> anyhow::Result { let pid_file_path = std::path::Path::new(&pid_file); @@ -110,7 +93,11 @@ impl Daemon { } fn has_timer(&mut self, name: &str) -> bool { - self.timers.iter().any(|other| other.name.as_ref() == name) + self.get_timer(name).is_some() + } + + fn get_timer(&mut self, name: &str) -> Option<&mut Timer> { + self.timers.iter_mut().find(|t| t.name.as_ref() == name) } fn handle_command(&mut self, command: Command) -> Result { @@ -137,6 +124,13 @@ impl Daemon { self.timers.push(timer); Ok(Answer::Ok) } + Command::Toggle(name) => match self.get_timer(&name) { + Some(timer) => { + timer.toggle(); + Ok(Answer::Ok) + } + None => Err(AnswerErr::NoSuchTimer(name)), + }, Command::Remove(name) => { if !self.has_timer(&name) { return Err(AnswerErr::NoSuchTimer(name)); @@ -160,6 +154,13 @@ impl Daemon { Ok(Answer::Ok) } Command::PomodoroList => Ok(Answer::Pomodoro(self.pomodoro.clone())), + Command::PomodoroToggle => match &mut self.pomodoro { + Some(ref mut pomodoro) => { + pomodoro.timer.toggle(); + Ok(Answer::Ok) + } + None => Err(AnswerErr::NoPomodoro), + }, } } @@ -172,18 +173,10 @@ impl Daemon { } fn check_timers(&mut self) { - self.timers.retain(|timer| { - if timer.is_expired() { - timer.handle_expiration(self.notify); - } - - !timer.is_expired() - }); + self.timers.retain(|timer| !timer.is_expired()); if let Some(pomodoro) = &mut self.pomodoro { - if pomodoro.is_expired() { - pomodoro.handle_expiration(self.notify); - } + pomodoro.update(); } } @@ -196,15 +189,17 @@ impl Daemon { for sig in signal_hook::consts::TERM_SIGNALS { signal_hook::flag::register(*sig, Arc::clone(&term))?; } + self.main_loop(term) + } + + fn main_loop(&mut self, term: Arc) -> anyhow::Result<()> { while !term.load(Ordering::Relaxed) { - while let Ok((stream, _)) = self.listener.accept() { - if let Err(e) = self.handle_stream(&stream) { - println!("Error while handling stream: {}", e) - } + if let Ok((stream, _)) = self.listener.accept() { + self.handle_stream(&stream)?; } self.check_timers(); sleep(Duration::from_millis(100)); - }; + } Ok(()) } } @@ -221,3 +216,24 @@ impl Drop for Daemon { println!("Stopped successfully!"); } } + +impl Display for Answer { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Answer::Ok => write!(f, ""), + Answer::Timers(timers) => { + if timers.is_empty() { + writeln!(f, "No timers running.") + } else { + let strings: Vec = + timers.iter().map(|timer| timer.to_string()).collect(); + writeln!(f, "{}", strings.join("\n")) + } + } + Answer::Pomodoro(pomodoro) => match pomodoro { + Some(p) => write!(f, "{}", p), + None => write!(f, "No pomodoro running."), + }, + } + } +} diff --git a/src/helper.rs b/src/helper.rs index 164b5e9..ecd9b90 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,3 +1,5 @@ +use notify_rust::Notification; + pub fn getuid() -> u32 { unsafe { libc::getuid() } } @@ -5,3 +7,10 @@ pub fn getuid() -> u32 { pub fn run_path() -> String { format!("/run/user/{}", getuid()) } + +pub fn send_notifictation(msg: &str) { + match Notification::new().summary("󰀠 Timers").body(msg).show() { + Ok(_) => println!("Sent notification sucessfully."), + Err(_) => println!("Failed to send notification."), + }; +} diff --git a/src/main.rs b/src/main.rs index 5b118eb..5d6286d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ fn main() -> Result<(), anyhow::Error> { } CliCommand::List => DaemonCommand::List, CliCommand::Remove { name } => DaemonCommand::Remove(name.into_boxed_str()), + CliCommand::Toggle { name } => DaemonCommand::Toggle(name.into_boxed_str()), CliCommand::Pomodoro(pomodoro) => match pomodoro { PomodoroCommand::Start { work, @@ -36,6 +37,7 @@ fn main() -> Result<(), anyhow::Error> { }, PomodoroCommand::Remove => DaemonCommand::PomodoroRemove, PomodoroCommand::List => DaemonCommand::PomodoroList, + PomodoroCommand::Toggle => DaemonCommand::PomodoroToggle, }, }; let answer = send_command(&args.socket, daemon_command)?; diff --git a/src/notification.rs b/src/notification.rs index 28be7e5..e69de29 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -1,8 +0,0 @@ -use notify_rust::Notification; - -pub fn send_notifictation(msg: &str) { - match Notification::new().summary("󰀠 Timers").body(msg).show() { - Ok(_) => println!("Sent notification sucessfully."), - Err(_) => println!("Failed to send notification."), - }; -} diff --git a/src/pomodoro.rs b/src/pomodoro.rs index 1bc2929..8cd5103 100644 --- a/src/pomodoro.rs +++ b/src/pomodoro.rs @@ -64,8 +64,13 @@ impl Pomodoro { } } - pub fn handle_expiration(&mut self, notify: bool) { - self.timer.handle_expiration(notify); + pub fn update(&mut self) { + if self.timer.is_expired() { + self.switch(); + }; + } + + fn switch(&mut self) { let duration = match self.status { Status::Working => { if self.pauses == self.pauses_till_long { @@ -90,8 +95,4 @@ impl Pomodoro { }; self.timer = Timer::new(self.status.to_string().into_boxed_str(), duration); } - - pub fn is_expired(&self) -> bool { - self.timer.is_expired() - } } diff --git a/src/timer.rs b/src/timer.rs index 44c929c..c8226fa 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -1,10 +1,86 @@ -use crate::notification::send_notifictation; +use crate::helper::send_notifictation; use serde::{Deserialize, Serialize}; use std::{ fmt::{Display, Formatter}, time::{Duration, Instant}, }; +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Timer { + pub name: Box, + #[serde(with = "approx_instant")] + start: Instant, + duration: Duration, + state: State, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub enum State { + Running, + Paused, +} + +impl Timer { + /// Create a new [`Timer`] with the supplied name and duration + /// The timer is instantly started. + pub fn new(name: Box, duration: Duration) -> Timer { + Timer { + name, + start: Instant::now(), + duration, + state: State::Running, + } + } + + /// Returns `true` if this [`Timer`] has expired + pub fn is_expired(&self) -> bool { + let expired = Instant::now() - self.start > self.duration; + if expired { + self.handle_expiration() + }; + expired + } + + /// Returns the remaining duration rounded to seconds of this [`Timer`]. + pub fn remaining(&self) -> Duration { + if self.state == State::Paused { + return self.duration; + }; + let exact = self.duration - (Instant::now() - self.start); + Duration::from_secs(exact.as_secs()) + } + + /// Handles the expiration of this [`Timer`] + fn handle_expiration(&self) { + let msg = format!("Timer {} has expired!", self.name); + println!("{}", &msg); + send_notifictation(msg.as_str()); + } + + /// Toggle [`State`] of this [`Timer`] + /// from [`State::Running`] to [`State::Paused`] and vice versa + pub fn toggle(&mut self) { + if self.state != State::Paused { + self.duration = self.remaining(); + self.state = State::Paused; + } else { + self.start = Instant::now(); + self.state = State::Running; + } + } +} + +impl Display for Timer { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{} has {} remaining.", + self.name, + humantime::format_duration(self.remaining()) + ) + } +} + mod approx_instant { use std::time::{Duration, Instant}; @@ -32,49 +108,3 @@ mod approx_instant { } } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] -pub struct Timer { - pub name: Box, - #[serde(with = "approx_instant")] - start: Instant, - duration: Duration, -} - -impl Display for Timer { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!( - f, - "{} has {} remaining.", - self.name, - humantime::format_duration(self.remaining()) - ) - } -} - -impl Timer { - pub fn new(name: Box, duration: Duration) -> Timer { - Timer { - name, - start: Instant::now(), - duration, - } - } - - pub fn is_expired(&self) -> bool { - Instant::now() - self.start > self.duration - } - - /// Returns the remaining duration rounded to seconds of this [`Timer`]. - pub fn remaining(&self) -> Duration { - let exact = self.duration - (Instant::now() - self.start); - Duration::from_secs(exact.as_secs()) - } - - pub fn handle_expiration(&self, notify: bool) { - let msg = format!("Timer {} has expired!", self.name); - println!("{}", &msg); - if notify { - send_notifictation(msg.as_str()); - } - } -}