feat: add pomodoro timer

This commit is contained in:
Moritz Böhme 2023-07-29 12:15:19 +02:00
parent dfd0b52d50
commit 103c6d779e
Signed by: moritz
GPG key ID: 970C6E89EB0547A9
5 changed files with 182 additions and 17 deletions

View file

@ -30,6 +30,16 @@ pub enum Command {
Remove { Remove {
name: String, name: String,
}, },
Pomodoro {
#[clap(default_value_t = 25)]
work_minutes: u64,
#[clap(default_value_t = 5)]
pause_minutes: u64,
#[clap(default_value_t = 10)]
long_pause_minutes: u64,
#[clap(default_value_t = 3)]
pauses_till_long: u64,
}
} }
fn get_stream(socket_path: &String) -> Result<UnixStream> { fn get_stream(socket_path: &String) -> Result<UnixStream> {

View file

@ -1,3 +1,4 @@
use crate::pomodoro::Pomodoro;
pub use crate::timer::Timer; pub use crate::timer::Timer;
use anyhow::Context; use anyhow::Context;
use notify_rust::Notification; use notify_rust::Notification;
@ -15,25 +16,36 @@ pub enum Command {
Add(Box<str>, Duration), Add(Box<str>, Duration),
Remove(Box<str>), Remove(Box<str>),
List, List,
Pomodoro {
work: Duration,
pause: Duration,
long_pause: Duration,
pauses_till_long: u64,
},
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum Answer { pub enum Answer {
Ok, Ok,
Timers(Vec<Timer>), Timers(Vec<Timer>, Option<Pomodoro>),
} }
impl Display for Answer { impl Display for Answer {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self { match self {
Answer::Ok => write!(f, "Ok"), Answer::Ok => write!(f, "Ok"),
Answer::Timers(timers) => { Answer::Timers(timers, pomodoro) => {
if timers.is_empty() { if timers.is_empty() {
write!(f, "No timers running.") writeln!(f, "No timers running.")?;
} else { } else {
let strings: Vec<String> = let strings: Vec<String> =
timers.iter().map(|timer| timer.to_string()).collect(); timers.iter().map(|timer| timer.to_string()).collect();
write!(f, "{}", strings.join("\n")) writeln!(f, "{}", strings.join("\n"))?;
};
match pomodoro {
Some(p) => write!(f, "{}", p),
None => write!(f, "No pomodoro running."),
} }
} }
} }
@ -51,6 +63,7 @@ pub enum AnswerErr {
pub struct Daemon { pub struct Daemon {
listener: UnixListener, listener: UnixListener,
timers: Vec<Timer>, timers: Vec<Timer>,
pomodoro: Option<Pomodoro>,
notify: bool, notify: bool,
} }
@ -66,6 +79,7 @@ impl Daemon {
Ok(Self { Ok(Self {
listener, listener,
timers: Vec::new(), timers: Vec::new(),
pomodoro: None,
notify, notify,
}) })
} }
@ -77,11 +91,23 @@ impl Daemon {
fn handle_command(&mut self, command: Command) -> Result<Answer, AnswerErr> { fn handle_command(&mut self, command: Command) -> Result<Answer, AnswerErr> {
println!("Received command {:?}", command); println!("Received command {:?}", command);
match command { match command {
Command::List => Ok(Answer::Timers(self.timers.clone())), Command::List => Ok(Answer::Timers(self.timers.clone(), self.pomodoro.clone())),
Command::Add(name, duration) => { Command::Add(name, duration) => {
if self.has_timer(&name) { if self.has_timer(&name) {
return Err(AnswerErr::TimerAlreadyExist(name)); return Err(AnswerErr::TimerAlreadyExist(name));
} }
if self.notify {
match Notification::new()
.summary("󰀠 Timers")
.body(format!("Started timer {} for {:?}", &name, duration).as_str())
.show()
{
Ok(_) => println!("Sent notification sucessfully."),
Err(_) => println!("Failed to send notification."),
};
}
let timer = Timer::new(name, duration); let timer = Timer::new(name, duration);
self.timers.push(timer); self.timers.push(timer);
Ok(Answer::Ok) Ok(Answer::Ok)
@ -94,6 +120,23 @@ impl Daemon {
.retain(|other| other.name.as_ref() != name.as_ref()); .retain(|other| other.name.as_ref() != name.as_ref());
Ok(Answer::Ok) Ok(Answer::Ok)
} }
Command::Pomodoro {
work,
pause,
long_pause,
pauses_till_long,
} => {
match Notification::new()
.summary("󰀠 Timers")
.body("Started pomodoro.")
.show()
{
Ok(_) => println!("Sent notification sucessfully."),
Err(_) => println!("Failed to send notification."),
};
self.pomodoro = Some(Pomodoro::new(work, pause, long_pause, pauses_till_long));
Ok(Answer::Ok)
}
} }
} }
@ -107,21 +150,18 @@ impl Daemon {
fn check_timers(&mut self) { fn check_timers(&mut self) {
self.timers.retain(|timer| { self.timers.retain(|timer| {
let expired = timer.is_expired(); if timer.is_expired() {
timer.handle_expiration(self.notify);
if expired {
let msg = format!("Timer {} has expired!", timer.name);
println!("{}", &msg);
if self.notify {
match Notification::new().summary("󰀠 Timers").body(&msg).show() {
Ok(_) => println!("Send notification sucessfully."),
Err(_) => println!("Failed to send notification."),
}
}
} }
!expired !timer.is_expired()
}); });
if let Some(pomodoro) = &mut self.pomodoro {
if pomodoro.is_expired() {
pomodoro.handle_expiration(self.notify);
}
}
} }
pub fn run(&mut self) -> anyhow::Result<()> { pub fn run(&mut self) -> anyhow::Result<()> {

View file

@ -1,5 +1,6 @@
pub mod cli; pub mod cli;
pub mod daemon; pub mod daemon;
pub mod pomodoro;
pub mod timer; pub mod timer;
use std::time::Duration; use std::time::Duration;
@ -19,6 +20,18 @@ fn main() -> Result<()> {
} => DaemonCommand::Add(name.into_boxed_str(), Duration::from_secs(duration_seconds)), } => DaemonCommand::Add(name.into_boxed_str(), Duration::from_secs(duration_seconds)),
CliCommand::List => DaemonCommand::List, CliCommand::List => DaemonCommand::List,
CliCommand::Remove { name } => DaemonCommand::Remove(name.into_boxed_str()), CliCommand::Remove { name } => DaemonCommand::Remove(name.into_boxed_str()),
CliCommand::Pomodoro {
work_minutes,
pause_minutes,
long_pause_minutes,
pauses_till_long,
} => DaemonCommand::Pomodoro{
work: Duration::from_secs(work_minutes * 60),
pause: Duration::from_secs(pause_minutes * 60),
long_pause: Duration::from_secs(long_pause_minutes * 60),
pauses_till_long
},
}; };
send_command(&args.socket, daemon_command) send_command(&args.socket, daemon_command)
} }

90
src/pomodoro.rs Normal file
View file

@ -0,0 +1,90 @@
use std::{fmt::Display, time::Duration};
use serde::{Deserialize, Serialize};
use crate::daemon::Timer;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Pomodoro {
work: Duration,
pause: Duration,
long_pause: Duration,
pauses_till_long: u64,
pauses: u64,
status: Status,
pub timer: Timer,
}
impl Display for Pomodoro {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Pomodoro ({:?}, {:?}, {:?}) currently {} with {:?} remaining.",
self.work,
self.pause,
self.long_pause,
self.status,
self.timer.remaining()
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
enum Status {
Working,
Pausing,
}
impl Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Status::Working => write!(f, "working"),
Status::Pausing => write!(f, "pausing"),
}
}
}
impl Pomodoro {
pub fn new(
work: Duration,
pause: Duration,
long_pause: Duration,
pauses_till_long: u64,
) -> Self {
Pomodoro {
work,
pause,
long_pause,
pauses_till_long,
pauses: 0,
status: Status::Working,
timer: Timer::new(Status::Working.to_string().into_boxed_str(), work),
}
}
pub fn handle_expiration(&mut self, notify: bool) {
self.timer.handle_expiration(notify);
let duration = match self.status {
Status::Pausing => self.work,
Status::Working => {
if self.pauses == self.pauses_till_long {
self.long_pause
} else {
self.pause
}
}
};
self.status = match self.status {
Status::Working => {
self.pauses += 1;
Status::Pausing
}
Status::Pausing => Status::Working,
};
self.timer = Timer::new(self.status.to_string().into_boxed_str(), duration);
}
pub fn is_expired(&self) -> bool {
self.timer.is_expired()
}
}

View file

@ -1,3 +1,4 @@
use notify_rust::Notification;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fmt::{Display, Formatter}, fmt::{Display, Formatter},
@ -66,4 +67,15 @@ impl Timer {
pub fn remaining(&self) -> Duration { pub fn remaining(&self) -> Duration {
self.duration - (Instant::now() - self.start) self.duration - (Instant::now() - self.start)
} }
pub fn handle_expiration(&self, notify: bool) {
let msg = format!("Timer {} has expired!", self.name);
println!("{}", &msg);
if notify {
match Notification::new().summary("󰀠 Timers").body(&msg).show() {
Ok(_) => println!("Sent notification sucessfully."),
Err(_) => println!("Failed to send notification."),
}
}
}
} }