use super::Alert; use crossterm::event::{self, Event, KeyCode, KeyModifiers}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::error::Error; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::io::{self, Write}; use std::time::{Duration, Instant}; static WORK_TIME: u64 = 1500; // Default: 1500 (25min) static SHORT_BREAK_TIME: u64 = 300; // Default: 300 (5min) static LONG_BREAK_TIME: u64 = 600; // Default: 600 (10min) static POLLING_RATE: Duration = Duration::from_millis(100); #[derive(Debug, Copy, Clone)] pub struct Domasi { state: State, count: u64, wait_start: Option, paused: PausedState, } impl Default for Domasi { fn default() -> Self { Self { state: State::Start, count: 0, wait_start: None, paused: Default::default(), } } } impl Domasi { pub fn new() -> Self { Self::default() } pub fn start(&mut self, maybe_alert: Option) -> crossterm::Result<()> { enable_raw_mode()?; 'main: loop { if let Status::Complete = self.poll(maybe_alert.as_ref()) { let _ = Self::print("Start next session? (Y/N)"); 'input: loop { if let Event::Key(key_event) = event::read()? { match key_event.code { KeyCode::Char('y') | KeyCode::Char('Y') => { *self = Self { count: self.count, ..Default::default() }; break 'input; } KeyCode::Char('n') | KeyCode::Char('N') => break 'main, _ => {} } } } } if event::poll(POLLING_RATE)? { if let Event::Key(key_event) = event::read()? { match key_event.code { KeyCode::Char('p') | KeyCode::Char('P') => { self.paused.toggle(); if !self.paused.value { // Apply the elapsed time let duration = self.paused.duration; match self.wait_start { Some(instant) => self.wait_start = Some(instant + duration), None => { unreachable!("Unable to resume from a non-existent timer.") } } self.paused = Default::default(); } } KeyCode::Char('r') | KeyCode::Char('R') => self.restart(), KeyCode::Char('q') | KeyCode::Char('Q') => break, KeyCode::Char('c') if key_event.modifiers == KeyModifiers::CONTROL => break, _ => {} } } } } disable_raw_mode()?; Ok(()) } fn restart(&mut self) { self.wait_start = Some(Instant::now()); self.paused = Default::default(); } fn notify_complete(&self) -> Result<(), Box> { let mut toast = notify_rust::Notification::new(); toast.summary("Pomodoro cycle complete!"); toast.body("Continue?"); toast.show()?; Ok(()) } fn notify(&self) -> Result<(), Box> { fn cycle_count(i: u64) -> String { let i = i / 4; match i { 1 => "After this, you will have completed 1 cycle.".to_string(), _ => format!("After this, you will have completed {} cycles.", i), } } fn session_num(i: u64) -> &'static str { match i % 4 { 0 => "Time for your 1st work session.", 1 => "Time for your 2nd work session.", 2 => "Time for your 3rd work session.", 3 => "Time for your last work session.", _ => unreachable!(), } } let mut toast = notify_rust::Notification::new(); match self.state { State::Work => { toast .summary(session_num(self.count)) .body("Remember to stay focused.") .show()?; } State::ShortBreak => { toast .summary("Time for a quick break.") .body("Sit back and relax.") .show()?; } State::LongBreak => { toast .summary("Enjoy your long break!") .body(&cycle_count(self.count)) .show()?; } State::UserWait | State::Start => {} }; Ok(()) } fn poll(&mut self, maybe_alert: Option<&Alert>) -> Status { let status = self.check(); match status { Status::Paused => { assert!(self.paused.value); let now = Instant::now(); self.paused.duration += match self.paused.previous { Some(earlier) => now - earlier, None => Default::default(), }; self.paused.previous = Some(now); self.display_paused(); } Status::Active => self.display_time(), Status::NextState => { let (update_count, new_state) = self.next(); if let Count::Increase = update_count { self.count += 1; } self.state = new_state; self.wait_start = match new_state { State::UserWait => None, State::Work | State::ShortBreak | State::LongBreak => Some(Instant::now()), State::Start => unreachable!("Domasi#next should never return State::Start"), }; if let Some(alert) = maybe_alert { alert.play() } let _ = self.notify(); self.display_time(); } Status::Complete => { let _ = self.notify_complete(); } }; status } fn display_time(&self) { if let Some(start) = self.wait_start { let remainder: Clock = (Self::wait_times(self.state) - (Instant::now() - start)).into(); let _ = Self::print(&format!("{} {}", self.state, remainder)); } } fn display_paused(&self) { if let Some(start) = self.wait_start { let stop_time: Clock = (Self::wait_times(self.state) - ((Instant::now() - self.paused.duration) - start)) .into(); let _ = Self::print(&format!("[PAUSED] {} {}", self.state, stop_time)); } } fn print(text: &str) -> io::Result<()> { let out = io::stdout(); let mut handle = out.lock(); // Empty String so that we can clear line before writing // from the most-left side of the terminal again. // // We write 25 spaces so that the entire line is always // written over handle.write_all(b"\r ")?; handle.write_all("\r".as_bytes())?; handle.write_all(text.as_bytes())?; handle.flush()?; Ok(()) } fn next(&self) -> (Count, State) { match self.state { State::Work => { let state: State = if (self.count + 1) % 4 == 0 { State::LongBreak } else { State::ShortBreak }; (Count::Increase, state) } State::ShortBreak | State::Start => (Count::Remain, State::Work), State::LongBreak => (Count::Remain, State::UserWait), _ => unreachable!("Domasi#next should never be called on State::Inactive"), } } fn check(&self) -> Status { if self.paused.value { return Status::Paused; } match self.wait_start { Some(earlier) => { let diff: Duration = (Instant::now() - self.paused.duration) - earlier; if diff > Self::wait_times(self.state) { Status::NextState } else { Status::Active } } None => match self.state { State::UserWait if self.count % 4 == 0 => Status::Complete, _ => Status::NextState, }, } } fn wait_times(state: State) -> Duration { match state { State::Work => Duration::from_secs(WORK_TIME), State::ShortBreak => Duration::from_secs(SHORT_BREAK_TIME), State::LongBreak => Duration::from_secs(LONG_BREAK_TIME), _ => unreachable!("Can not have Domasi state = State::Inactive and wait_start = Some(...) at the same time.") } } } struct Clock { hours: u64, minutes: u64, seconds: u64, } impl From for Clock { fn from(dur: Duration) -> Self { let dur = dur.as_secs(); let hours = dur / 3600; let minutes = (dur - (hours * 3600)) / 60; let seconds = dur - (hours * 3600) - (minutes * 60); Self { hours, minutes, seconds, } } } impl Display for Clock { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { if self.hours > 0 { // No need to print the hours if there are none. if self.hours < 10 { write!(f, "0{}:", self.hours)?; } else { write!(f, "{}:", self.hours)?; } } if self.minutes < 10 { write!(f, "0{}:", self.minutes)?; } else { write!(f, "{}:", self.minutes)?; } if self.seconds < 10 { write!(f, "0{}", self.seconds) } else { write!(f, "{}", self.seconds) } } } #[derive(Debug, Copy, Clone, Default)] struct PausedState { value: bool, duration: Duration, previous: Option, } impl PausedState { pub fn toggle(&mut self) { self.value = !self.value; } } #[derive(Debug, Copy, Clone)] enum Count { Increase, Remain, } #[derive(Debug, Copy, Clone)] enum Status { Paused, Active, NextState, Complete, } #[derive(Debug, Copy, Clone)] enum State { Start, UserWait, Work, ShortBreak, LongBreak, } impl Default for State { fn default() -> Self { State::UserWait } } impl Display for State { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match *self { State::Work => f.write_str("Working:"), State::ShortBreak => f.write_str("Resting:"), State::LongBreak => f.write_str("REALLY Resting:"), State::UserWait => f.write_str("Inactive:"), _ => unreachable!("State::Start should never be displayed."), } } }