From 7bf3f410e16e05edd45dae1575a2b154a5e1f1bf Mon Sep 17 00:00:00 2001 From: "Dr. Matthias Ratajczak" Date: Thu, 2 Mar 2023 14:56:15 +0100 Subject: [PATCH] add pause, pidfile --- Cargo.toml | 2 +- src/main.rs | 140 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 129 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed8fb18..508899f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" [dependencies] futures = { version = "0.3", default-features = false } crossterm = { version = "0.26", default-features = false, features = ["event-stream"] } -tokio = { version = "1.25", default-features = false, features = ["time", "macros", "rt"] } +tokio = { version = "1.25", default-features = false, features = ["time", "macros", "rt", "sync"] } [profile.release] opt-level = "z" diff --git a/src/main.rs b/src/main.rs index 380256f..923f13b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,8 @@ +use std::ffi::OsString; +use std::fs::{self, read_to_string}; use std::io::{stdout, Stdout, Write}; +use std::os::unix::prelude::OsStringExt; +use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Duration; @@ -10,16 +14,28 @@ use crossterm::{ ExecutableCommand, Result, }; use futures::StreamExt; +use tokio::sync::broadcast::{channel, Receiver, Sender}; use tokio::time::sleep; const ONE_SECOND: Duration = Duration::from_secs(1); +const PID_FILE: &str = "/tmp/pomodoro.pid"; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { - let timer = Pomodoro::new(); + let pid = if let Some(pid) = Pid::check_already_running() { + eprintln!("already running (pid {})", pid.pid); + return Ok(()); + } else { + Pid::create() + }; + + let (tx, rx) = channel::(10); + + let timer = Pomodoro::new(rx); + let ui = Ui::new(tx); let timer_task = tokio::spawn(async move { timer.main_loop().await }); - let ui_task = tokio::spawn(async move { Ui::main_loop().await }); + let ui_task = tokio::spawn(async move { ui.main_loop().await }); tokio::select! ( result = timer_task => result??, @@ -28,13 +44,67 @@ async fn main() -> Result<()> { Ui::clear_terminal()?; + pid.remove(); + Ok(()) } -struct Ui; +struct Pid { + pid: u32, + path: PathBuf, +} + +impl Pid { + fn check_already_running() -> Option { + let path = PathBuf::from(PID_FILE); + Self::get_pid_from_file(&path) + .and_then(Self::check_process_exists) + .map(|pid| Self { pid, path }) + } + + fn check_process_exists(pid: u32) -> Option { + let mut path = PathBuf::from("/proc"); + path.push(pid.to_string()); + path.push("cmdline"); + let content = fs::read(path).ok()?; + let cmdline = OsString::from_vec(content).to_string_lossy().to_string(); + if cmdline.contains("pomodoro") { + Some(pid) + } else { + None + } + } + + fn get_pid_from_file(file: &Path) -> Option { + let content = read_to_string(file).ok()?; + let line = content.lines().nth(0)?; + let result: u32 = line.parse().ok()?; + Some(result) + } + + fn create() -> Self { + let pid = std::process::id(); + let path = PathBuf::from(PID_FILE); + let mut fd = fs::File::create(&path).unwrap(); + write!(fd, "{pid}").unwrap(); + Self { pid, path } + } + + fn remove(self) { + fs::remove_file(self.path).unwrap(); + } +} + +struct Ui { + sender: Sender, +} impl Ui { - async fn main_loop() -> Result<()> { + fn new(sender: Sender) -> Self { + Self { sender } + } + + async fn main_loop(self) -> Result<()> { let mut reader = EventStream::new(); loop { @@ -42,11 +112,15 @@ impl Ui { stdout().execute(cursor::Hide)?; if let Some(Ok(Event::Key(key_event))) = reader.next().await { - if Self::is_char_q(key_event) || Self::is_ctrl_c(&key_event) { + if Self::is_char_q(key_event) || Self::is_ctrl_c(key_event) { + self.sender.send(Message::Quit).unwrap(); disable_raw_mode()?; stdout().execute(cursor::Show)?; return Ok(()); + }; + if Self::is_space(key_event) { + self.sender.send(Message::TogglePause).unwrap(); } } } @@ -56,7 +130,11 @@ impl Ui { key_event.code == KeyCode::Char('q') } - fn is_ctrl_c(key_event: &crossterm::event::KeyEvent) -> bool { + fn is_space(key_event: crossterm::event::KeyEvent) -> bool { + key_event.code == KeyCode::Char(' ') + } + + fn is_ctrl_c(key_event: crossterm::event::KeyEvent) -> bool { key_event.code == KeyCode::Char('c') && key_event.modifiers.contains(KeyModifiers::CONTROL) } @@ -75,10 +153,12 @@ struct Pomodoro { intervals: [Interval; 8], remaining: Duration, stdout: Stdout, + receiver: Receiver, + paused: bool, } impl Pomodoro { - fn new() -> Self { + fn new(receiver: Receiver) -> Self { let intervals = Interval::default_sequence(); let interval_idx = 0; let remaining = intervals[interval_idx].get_duration(); @@ -88,6 +168,8 @@ impl Pomodoro { intervals, remaining, stdout: stdout(), + receiver, + paused: false, } } @@ -102,16 +184,43 @@ impl Pomodoro { )?; self.stdout.queue(MoveTo(0, 1))?; write!(self.stdout, "remaining: {}", format(self.remaining))?; + if self.paused { + write!(self.stdout, " (paused)")?; + } self.stdout.flush()?; Ok(()) } + fn toggle_pause(&mut self) { + self.paused ^= true; + } + async fn main_loop(mut self) -> Result<()> { loop { 'inner: loop { self.show_status()?; - sleep(ONE_SECOND).await; - self.remaining -= ONE_SECOND; + + if self.paused { + if self.receiver.recv().await.unwrap() == Message::TogglePause { + self.toggle_pause(); + continue; + } + } + + tokio::select! ( + _ = sleep(ONE_SECOND) => {self.remaining -= ONE_SECOND;} + message = self.receiver.recv() => { + if let Ok(m) = message{ + match m { + Message::Quit => {return Ok(());}, + Message::TogglePause => { + self.toggle_pause(); + continue; + } + } + } + } + ); if self.remaining.is_zero() { self.show_status()?; @@ -121,7 +230,7 @@ impl Pomodoro { self.next_interval(); self.remaining = self.current_interval().get_duration(); - self.send_notification(self.current_interval().get_message()); + self.send_notification(self.current_interval().get_message())?; } } @@ -134,10 +243,11 @@ impl Pomodoro { self.intervals[self.interval_idx] } - fn send_notification(&self, message: &str) { + fn send_notification(&self, message: &str) -> Result<()> { let mut command = Command::new("notify-send"); command.args(["-t", "0", message]); - command.status().unwrap(); + command.status()?; + Ok(()) } } @@ -197,3 +307,9 @@ fn format(dur: Duration) -> String { format!("{min}:{sec:0>2}") } + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Message { + Quit, + TogglePause, +}