Compare commits

...

2 Commits

7 changed files with 167 additions and 112 deletions

View File

@ -40,6 +40,12 @@ pub enum Command {
/// List timers /// List timers
#[clap(visible_alias = "l")] #[clap(visible_alias = "l")]
List, List,
/// Toggle timer
#[clap(visible_alias = "t")]
Toggle {
/// name of the timer to toggle
name: String,
},
/// Remove a timer /// Remove a timer
#[clap(visible_alias = "r")] #[clap(visible_alias = "r")]
Remove { Remove {
@ -83,15 +89,14 @@ pub enum PomodoroCommand {
/// List the pomodoro settings and remaining duration /// List the pomodoro settings and remaining duration
#[clap(visible_alias = "l")] #[clap(visible_alias = "l")]
List, List,
} /// Toggle pomodoro
#[clap(visible_alias = "t")]
fn get_stream(socket_path: &String) -> Result<UnixStream> { Toggle,
UnixStream::connect(socket_path)
.context(format!("Could not connect to socket {}!", socket_path))
} }
pub fn send_command(socket_path: &String, command: OtherCommand) -> Result<Answer> { pub fn send_command(socket_path: &String, command: OtherCommand) -> Result<Answer> {
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!")?; serde_cbor::to_writer(&stream, &command).context("Could not write command!")?;
stream stream
.shutdown(Shutdown::Write) .shutdown(Shutdown::Write)

View File

@ -1,4 +1,4 @@
use crate::notification::send_notifictation; use crate::helper::send_notifictation;
use crate::pomodoro::Pomodoro; use crate::pomodoro::Pomodoro;
pub use crate::timer::Timer; pub use crate::timer::Timer;
use anyhow::Context; use anyhow::Context;
@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::fs::File; use std::fs::File;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::{ use std::{
io::Write, io::Write,
os::unix::net::{UnixListener, UnixStream}, os::unix::net::{UnixListener, UnixStream},
@ -18,6 +18,7 @@ use std::{
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum Command { pub enum Command {
Add(Box<str>, Duration), Add(Box<str>, Duration),
Toggle(Box<str>),
Remove(Box<str>), Remove(Box<str>),
List, List,
PomodoroStart { PomodoroStart {
@ -28,6 +29,7 @@ pub enum Command {
}, },
PomodoroRemove, PomodoroRemove,
PomodoroList, PomodoroList,
PomodoroToggle,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -37,33 +39,20 @@ pub enum Answer {
Pomodoro(Option<Pomodoro>), Pomodoro(Option<Pomodoro>),
} }
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<String> =
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)] #[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(Box<str>), TimerAlreadyExist(Box<str>),
#[error("No timer with the name '{}' exists", .0)] #[error("No timer with the name '{}' exists", .0)]
NoSuchTimer(Box<str>), NoSuchTimer(Box<str>),
#[error("No pomodoro running")]
NoPomodoro,
}
#[derive(Debug, thiserror::Error)]
pub enum DaemonErr {
#[error("Daemon already running!")]
AlreadyRunning,
} }
pub struct Daemon { pub struct Daemon {
@ -75,12 +64,6 @@ pub struct Daemon {
notify: bool, notify: bool,
} }
#[derive(Debug, thiserror::Error)]
pub enum DaemonErr {
#[error("Daemon already running!")]
AlreadyRunning,
}
impl Daemon { impl Daemon {
pub fn new(socket: String, pid_file: String, no_notify: bool) -> anyhow::Result<Self> { pub fn new(socket: String, pid_file: String, no_notify: bool) -> anyhow::Result<Self> {
let pid_file_path = std::path::Path::new(&pid_file); let pid_file_path = std::path::Path::new(&pid_file);
@ -110,7 +93,11 @@ impl Daemon {
} }
fn has_timer(&mut self, name: &str) -> bool { 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<Answer, AnswerErr> { fn handle_command(&mut self, command: Command) -> Result<Answer, AnswerErr> {
@ -137,6 +124,13 @@ impl Daemon {
self.timers.push(timer); self.timers.push(timer);
Ok(Answer::Ok) 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) => { Command::Remove(name) => {
if !self.has_timer(&name) { if !self.has_timer(&name) {
return Err(AnswerErr::NoSuchTimer(name)); return Err(AnswerErr::NoSuchTimer(name));
@ -160,6 +154,13 @@ impl Daemon {
Ok(Answer::Ok) Ok(Answer::Ok)
} }
Command::PomodoroList => Ok(Answer::Pomodoro(self.pomodoro.clone())), 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) { fn check_timers(&mut self) {
self.timers.retain(|timer| { self.timers.retain(|timer| !timer.is_expired());
if timer.is_expired() {
timer.handle_expiration(self.notify);
}
!timer.is_expired()
});
if let Some(pomodoro) = &mut self.pomodoro { if let Some(pomodoro) = &mut self.pomodoro {
if pomodoro.is_expired() { pomodoro.update();
pomodoro.handle_expiration(self.notify);
}
} }
} }
@ -196,15 +189,17 @@ impl Daemon {
for sig in signal_hook::consts::TERM_SIGNALS { for sig in signal_hook::consts::TERM_SIGNALS {
signal_hook::flag::register(*sig, Arc::clone(&term))?; signal_hook::flag::register(*sig, Arc::clone(&term))?;
} }
while !term.load(Ordering::Relaxed) { self.main_loop(term)
while let Ok((stream, _)) = self.listener.accept() {
if let Err(e) = self.handle_stream(&stream) {
println!("Error while handling stream: {}", e)
} }
fn main_loop(&mut self, term: Arc<AtomicBool>) -> anyhow::Result<()> {
while !term.load(Ordering::Relaxed) {
if let Ok((stream, _)) = self.listener.accept() {
self.handle_stream(&stream)?;
} }
self.check_timers(); self.check_timers();
sleep(Duration::from_millis(100)); sleep(Duration::from_millis(100));
}; }
Ok(()) Ok(())
} }
} }
@ -221,3 +216,24 @@ impl Drop for Daemon {
println!("Stopped successfully!"); 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<String> =
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."),
},
}
}
}

View File

@ -1,3 +1,5 @@
use notify_rust::Notification;
pub fn getuid() -> u32 { pub fn getuid() -> u32 {
unsafe { libc::getuid() } unsafe { libc::getuid() }
} }
@ -5,3 +7,10 @@ pub fn getuid() -> u32 {
pub fn run_path() -> String { pub fn run_path() -> String {
format!("/run/user/{}", getuid()) 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."),
};
}

View File

@ -22,6 +22,7 @@ fn main() -> Result<(), anyhow::Error> {
} }
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::Toggle { name } => DaemonCommand::Toggle(name.into_boxed_str()),
CliCommand::Pomodoro(pomodoro) => match pomodoro { CliCommand::Pomodoro(pomodoro) => match pomodoro {
PomodoroCommand::Start { PomodoroCommand::Start {
work, work,
@ -36,6 +37,7 @@ fn main() -> Result<(), anyhow::Error> {
}, },
PomodoroCommand::Remove => DaemonCommand::PomodoroRemove, PomodoroCommand::Remove => DaemonCommand::PomodoroRemove,
PomodoroCommand::List => DaemonCommand::PomodoroList, PomodoroCommand::List => DaemonCommand::PomodoroList,
PomodoroCommand::Toggle => DaemonCommand::PomodoroToggle,
}, },
}; };
let answer = send_command(&args.socket, daemon_command)?; let answer = send_command(&args.socket, daemon_command)?;

View File

@ -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."),
};
}

View File

@ -64,8 +64,13 @@ impl Pomodoro {
} }
} }
pub fn handle_expiration(&mut self, notify: bool) { pub fn update(&mut self) {
self.timer.handle_expiration(notify); if self.timer.is_expired() {
self.switch();
};
}
fn switch(&mut self) {
let duration = match self.status { let duration = match self.status {
Status::Working => { Status::Working => {
if self.pauses == self.pauses_till_long { 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); 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,10 +1,86 @@
use crate::notification::send_notifictation; use crate::helper::send_notifictation;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fmt::{Display, Formatter}, fmt::{Display, Formatter},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Timer {
pub name: Box<str>,
#[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<str>, 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 { mod approx_instant {
use std::time::{Duration, 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<str>,
#[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<str>, 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());
}
}
}