Compare commits

..

4 commits

Author SHA1 Message Date
0c954962cc
feat: better durations and allow stopping pomodoro 2023-07-29 12:55:20 +02:00
103c6d779e
feat: add pomodoro timer 2023-07-29 12:15:19 +02:00
dfd0b52d50
feat: add notifications 2023-07-29 10:41:56 +02:00
b3885f80dd
perf: make daemon more memory efficient
Use Box<str> instead of String in daemon. Because we don't need to
change names of timers in the daemon this is way more memory efficient.
2023-07-29 10:15:46 +02:00
7 changed files with 1357 additions and 61 deletions

1118
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,8 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.71" anyhow = "1.0.71"
clap = { version = "4.3.4", features = ["derive"] } clap = { version = "4.3.4", features = ["derive"] }
humantime = "2.1.0"
notify-rust = "4.8.0"
serde = { version = "1.0.164", features = ["derive"] } serde = { version = "1.0.164", features = ["derive"] }
serde_cbor = "0.11.2" serde_cbor = "0.11.2"
thiserror = "1.0.44" thiserror = "1.0.44"

View file

@ -1,8 +1,9 @@
use crate::daemon::{Answer, Command as OtherCommand, AnswerErr}; use crate::daemon::{Answer, AnswerErr, Command as OtherCommand};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::net::Shutdown; use std::net::Shutdown;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::time::Duration;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "timers")] #[command(name = "timers")]
@ -18,10 +19,35 @@ pub struct Cli {
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum Command { pub enum Command {
Daemon, Daemon {
Add { name: String, duration_seconds: u64 }, #[arg(short, long)]
notify: bool,
},
Add {
name: String,
duration: humantime::Duration,
},
List, List,
Remove { name: String }, Remove {
name: String,
},
#[command(subcommand)]
Pomodoro(PomodoroCommand)
}
#[derive(Debug, Subcommand)]
pub enum PomodoroCommand {
Start {
#[clap(default_value_t = Duration::from_secs(25 * 60).into())]
work: humantime::Duration,
#[clap(default_value_t = Duration::from_secs(5 * 60).into())]
pause: humantime::Duration,
#[clap(default_value_t = Duration::from_secs(10 * 60).into())]
long_pause: humantime::Duration,
#[clap(default_value_t = 3)]
pauses_till_long: u64,
},
Stop,
} }
fn get_stream(socket_path: &String) -> Result<UnixStream> { fn get_stream(socket_path: &String) -> Result<UnixStream> {
@ -35,7 +61,8 @@ pub fn send_command(socket_path: &String, command: OtherCommand) -> Result<()> {
stream stream
.shutdown(Shutdown::Write) .shutdown(Shutdown::Write)
.context("Could not shutdown write!")?; .context("Could not shutdown write!")?;
let answer: Result<Answer, AnswerErr> = serde_cbor::from_reader(&stream).context("Could not read answer!")?; let answer: Result<Answer, AnswerErr> =
serde_cbor::from_reader(&stream).context("Could not read answer!")?;
match answer { match answer {
Ok(answer) => println!("{}", answer), Ok(answer) => println!("{}", answer),
Err(err) => println!("Error: {}", err), Err(err) => println!("Error: {}", err),

View file

@ -1,5 +1,7 @@
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 serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::{ use std::{
@ -11,28 +13,40 @@ use std::{
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum Command { pub enum Command {
Add(String, Duration), Add(Box<str>, Duration),
Remove(String), Remove(Box<str>),
List, List,
PomodoroStart {
work: Duration,
pause: Duration,
long_pause: Duration,
pauses_till_long: u64,
},
PomodoroStop
} }
#[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."),
} }
} }
} }
@ -42,19 +56,20 @@ impl Display for Answer {
#[derive(Debug, thiserror::Error, Serialize, Deserialize)] #[derive(Debug, thiserror::Error, Serialize, Deserialize)]
pub enum AnswerErr { pub enum AnswerErr {
#[error("Timer with name '{}' already exists", .0)] #[error("Timer with name '{}' already exists", .0)]
TimerAlreadyExist(String), TimerAlreadyExist(Box<str>),
#[error("No timer with the name '{}' exists", .0)] #[error("No timer with the name '{}' exists", .0)]
NoSuchTimer(String), NoSuchTimer(Box<str>),
} }
pub struct Daemon { pub struct Daemon {
listener: UnixListener, listener: UnixListener,
timers: Vec<Timer>, timers: Vec<Timer>,
pomodoro: Option<Pomodoro>,
notify: bool,
} }
impl Daemon { impl Daemon {
pub fn new(socket_path: String) -> anyhow::Result<Self> { pub fn new(socket_path: String, notify: bool) -> anyhow::Result<Self> {
let path = std::path::Path::new(&socket_path); let path = std::path::Path::new(&socket_path);
if path.exists() { if path.exists() {
std::fs::remove_file(path) std::fs::remove_file(path)
@ -65,21 +80,42 @@ impl Daemon {
Ok(Self { Ok(Self {
listener, listener,
timers: Vec::new(), timers: Vec::new(),
pomodoro: None,
notify,
}) })
} }
fn has_timer(&mut self, name: &String) -> bool { fn has_timer(&mut self, name: &str) -> bool {
self.timers.iter().any(|other| &other.name == name) self.timers.iter().any(|other| other.name.as_ref() == name)
} }
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.to_vec())), 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,
humantime::format_duration(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)
@ -88,14 +124,31 @@ impl Daemon {
if !self.has_timer(&name) { if !self.has_timer(&name) {
return Err(AnswerErr::NoSuchTimer(name)); return Err(AnswerErr::NoSuchTimer(name));
} }
self.timers = self self.timers
.timers .retain(|other| other.name.as_ref() != name.as_ref());
.iter()
.cloned()
.filter(|other| other.name != name)
.collect();
Ok(Answer::Ok) Ok(Answer::Ok)
} }
Command::PomodoroStart {
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)
}
Command::PomodoroStop => {
self.pomodoro = None;
Ok(Answer::Ok)
},
} }
} }
@ -108,18 +161,19 @@ impl Daemon {
} }
fn check_timers(&mut self) { fn check_timers(&mut self) {
self.timers = self self.timers.retain(|timer| {
.timers if timer.is_expired() {
.iter() timer.handle_expiration(self.notify);
.cloned() }
.filter(|timer| {
let expired = timer.is_expired(); !timer.is_expired()
if expired { });
println!("Timer {} is expired!", timer.name);
if let Some(pomodoro) = &mut self.pomodoro {
if pomodoro.is_expired() {
pomodoro.handle_expiration(self.notify);
}
} }
!expired
})
.collect();
} }
pub fn run(&mut self) -> anyhow::Result<()> { pub fn run(&mut self) -> anyhow::Result<()> {

View file

@ -1,24 +1,37 @@
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 crate::cli::{send_command, Cli, Command as CliCommand}; use crate::cli::{send_command, Cli, Command as CliCommand};
use crate::daemon::{Command as DaemonCommand, Daemon}; use crate::daemon::{Command as DaemonCommand, Daemon};
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use cli::PomodoroCommand;
fn main() -> Result<()> { fn main() -> Result<()> {
let args = Cli::parse(); let args = Cli::parse();
let daemon_command = match args.command { let daemon_command = match args.command {
CliCommand::Daemon => return Daemon::new(args.socket)?.run(), CliCommand::Daemon { notify } => return Daemon::new(args.socket, notify)?.run(),
CliCommand::Add { CliCommand::Add { name, duration } => {
name, DaemonCommand::Add(name.into_boxed_str(), duration.into())
duration_seconds, }
} => DaemonCommand::Add(name, Duration::from_secs(duration_seconds)),
CliCommand::List => DaemonCommand::List, CliCommand::List => DaemonCommand::List,
CliCommand::Remove { name } => DaemonCommand::Remove(name), CliCommand::Remove { name } => DaemonCommand::Remove(name.into_boxed_str()),
CliCommand::Pomodoro(pomodoro) => match pomodoro {
PomodoroCommand::Start {
work,
pause,
long_pause,
pauses_till_long,
} => DaemonCommand::PomodoroStart {
work: work.into(),
pause: pause.into(),
long_pause: long_pause.into(),
pauses_till_long,
},
PomodoroCommand::Stop => DaemonCommand::PomodoroStop,
},
}; };
send_command(&args.socket, daemon_command) send_command(&args.socket, daemon_command)
} }

92
src/pomodoro.rs Normal file
View file

@ -0,0 +1,92 @@
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.",
humantime::format_duration(self.work),
humantime::format_duration(self.pause),
humantime::format_duration(self.long_pause),
self.status,
humantime::format_duration(self.timer.remaining())
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
enum Status {
Working,
Pausing,
LongPause,
}
impl Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Status::Working => write!(f, "pomodoro work"),
Status::Pausing => write!(f, "pomodoro pause"),
Status::LongPause => write!(f, "pomodoro long pause"),
}
}
}
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::Working => {
if self.pauses == self.pauses_till_long {
self.long_pause
} else {
self.pause
}
}
_ => self.work,
};
self.status = match self.status {
Status::Working => {
self.pauses += 1;
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},
@ -33,7 +34,7 @@ mod approx_instant {
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Timer { pub struct Timer {
pub name: String, pub name: Box<str>,
#[serde(with = "approx_instant")] #[serde(with = "approx_instant")]
start: Instant, start: Instant,
duration: Duration, duration: Duration,
@ -43,15 +44,15 @@ impl Display for Timer {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!( write!(
f, f,
"{} has {}s remaining.", "{} has {} remaining.",
self.name, self.name,
self.remaining().as_secs() humantime::format_duration(self.remaining())
) )
} }
} }
impl Timer { impl Timer {
pub fn new(name: String, duration: Duration) -> Timer { pub fn new(name: Box<str>, duration: Duration) -> Timer {
Timer { Timer {
name, name,
start: Instant::now(), start: Instant::now(),
@ -63,7 +64,20 @@ impl Timer {
Instant::now() - self.start > self.duration Instant::now() - self.start > self.duration
} }
/// Returns the remaining duration rounded to seconds of this [`Timer`].
pub fn remaining(&self) -> Duration { pub fn remaining(&self) -> Duration {
self.duration - (Instant::now() - self.start) 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 {
match Notification::new().summary("󰀠 Timers").body(&msg).show() {
Ok(_) => println!("Sent notification sucessfully."),
Err(_) => println!("Failed to send notification."),
}
}
} }
} }