diff --git a/Cargo.lock b/Cargo.lock index bda1339..83308ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + [[package]] name = "autocfg" version = "1.1.0" @@ -183,6 +189,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" name = "pomodoro" version = "0.1.0" dependencies = [ + "anyhow", "crossterm", "futures", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 73a7969..92d779a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,9 @@ edition = "2021" version = "0.1.0" [dependencies] -futures = { version = "0.3", default-features = false } +anyhow = "1.0" crossterm = { version = "0.26", default-features = false, features = ["event-stream"] } +futures = { version = "0.3", default-features = false } tokio = { version = "1.26", default-features = false, features = ["sync", "time", "rt", "macros"] } [profile.release] diff --git a/src/main.rs b/src/main.rs index 923f13b..d807242 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,48 +3,67 @@ 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::process::{Command, ExitCode}; use std::time::Duration; +use anyhow::{anyhow, bail, Result}; + use crossterm::{cursor, QueueableCommand}; use crossterm::{ cursor::MoveTo, event::{Event, EventStream, KeyCode, KeyModifiers}, terminal::{self, disable_raw_mode, enable_raw_mode}, - ExecutableCommand, Result, + ExecutableCommand, }; use futures::StreamExt; + use tokio::sync::broadcast::{channel, Receiver, Sender}; +use tokio::task::JoinSet; 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<()> { +async fn main() -> ExitCode { + match main2().await { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("{}", e.root_cause().to_string()); + ExitCode::FAILURE + } + } +} + +async fn main2() -> Result<()> { let pid = if let Some(pid) = Pid::check_already_running() { - eprintln!("already running (pid {})", pid.pid); - return Ok(()); + bail!("already running (pid {})", pid.pid) } else { - Pid::create() + Pid::create()? }; + let result = run_all_tasks().await; + + Ui::clear_terminal()?; + pid.remove()?; + + result +} + +async fn run_all_tasks() -> Result<()> { 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 mut tasks = JoinSet::new(); - tokio::select! ( - result = timer_task => result??, - result = ui_task => result??, - ); + tasks.spawn(timer.main_loop()); + tasks.spawn(ui.main_loop()); - Ui::clear_terminal()?; - - pid.remove(); + while let Some(result) = tasks.join_next().await { + result??; + } Ok(()) } @@ -82,16 +101,17 @@ impl Pid { Some(result) } - fn create() -> Self { + fn create() -> Result { 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 } + let mut fd = fs::File::create(&path)?; + write!(fd, "{pid}")?; + let result = Self { pid, path }; + Ok(result) } - fn remove(self) { - fs::remove_file(self.path).unwrap(); + fn remove(self) -> Result<()> { + fs::remove_file(self.path).map_err(|e| anyhow!(e)) } } @@ -112,20 +132,37 @@ 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) { - self.sender.send(Message::Quit).unwrap(); - disable_raw_mode()?; - stdout().execute(cursor::Show)?; - + let should_exit = self.handle_key_event(key_event)?; + if should_exit { return Ok(()); }; - if Self::is_space(key_event) { - self.sender.send(Message::TogglePause).unwrap(); - } } } } + fn handle_key_event(&self, key_event: crossterm::event::KeyEvent) -> Result { + if Self::is_char_q(key_event) { + self.prepare_for_program_quit()?; + return Ok(true); + }; + if Self::is_ctrl_c(key_event) { + self.prepare_for_program_quit()?; + self.sender.send(Message::CtrlC)?; + bail!(""); + } + if Self::is_space(key_event) { + self.sender.send(Message::TogglePause)?; + } + Ok(false) + } + + fn prepare_for_program_quit(&self) -> Result<()> { + self.sender.send(Message::Quit)?; + disable_raw_mode()?; + stdout().execute(cursor::Show)?; + Ok(()) + } + fn is_char_q(key_event: crossterm::event::KeyEvent) -> bool { key_event.code == KeyCode::Char('q') } @@ -200,24 +237,28 @@ impl Pomodoro { 'inner: loop { self.show_status()?; - if self.paused { - if self.receiver.recv().await.unwrap() == Message::TogglePause { - self.toggle_pause(); - continue; - } - } - tokio::select! ( - _ = sleep(ONE_SECOND) => {self.remaining -= ONE_SECOND;} + _ = sleep(ONE_SECOND) => { + if !self.paused { + self.remaining -= ONE_SECOND; + } + }, message = self.receiver.recv() => { if let Ok(m) = message{ match m { - Message::Quit => {return Ok(());}, + Message::Quit => { + return Ok(()); + }, + Message::CtrlC => { + bail!(""); + }, Message::TogglePause => { self.toggle_pause(); continue; } } + } else { + bail!(""); } } ); @@ -310,6 +351,7 @@ fn format(dur: Duration) -> String { #[derive(Debug, Clone, Copy, PartialEq)] enum Message { + CtrlC, Quit, TogglePause, }