From a2b3b23cc1d3f435631cceaf27de3ed6eed95cf1 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn Date: Tue, 28 Jul 2026 09:33:23 -0500 Subject: [PATCH] refactor: extract termato struct and implementations --- src/app.rs | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/app.rs diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..498cfe5 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,285 @@ +use crate::msg::Message; +use crate::notification::send_desktop_notification; + +const SECONDS_PER_MIN: u32 = 60; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum TimerMode { + Work, + Break, +} + +pub struct Termato { + pub work_mins: u32, + pub break_mins: u32, + pub mode: TimerMode, + pub is_running: bool, + pub duration_in_secs: u32, + pub time_remaining_in_sec: u32, + pub show_help: bool, + pub enable_notifications: bool, + pub enable_visualizer: bool, + pub should_quit: bool, +} + +impl Termato { + pub fn new(work_mins: u32, break_mins: u32) -> Self { + Termato { + work_mins, + break_mins, + mode: TimerMode::Work, + is_running: false, + duration_in_secs: work_mins * SECONDS_PER_MIN, + time_remaining_in_sec: work_mins * SECONDS_PER_MIN, + show_help: false, + enable_notifications: false, + enable_visualizer: false, + should_quit: false, + } + } + + pub fn with_notifications(mut self, enable: bool) -> Self { + self.enable_notifications = enable; + self + } + + pub fn with_visualizer(mut self, enable: bool) -> Self { + self.enable_visualizer = enable; + self + } + + pub fn update(&mut self, msg: Message) { + match msg { + Message::Tick => self.tick(), + Message::ToggleRunning => self.toggle_running(), + Message::ToggleMode => self.toggle_mode(), + Message::Reset => self.reset(), + Message::ToggleHelp => self.toggle_help(), + Message::Quit => self.should_quit = true, + } + } + + pub fn toggle_running(&mut self) { + self.is_running = !self.is_running; + } + + pub fn toggle_mode(&mut self) { + self.mode = match self.mode { + TimerMode::Work => TimerMode::Break, + TimerMode::Break => TimerMode::Work, + }; + self.duration_in_secs = match self.mode { + TimerMode::Work => self.work_mins * SECONDS_PER_MIN, + TimerMode::Break => self.break_mins * SECONDS_PER_MIN, + }; + self.time_remaining_in_sec = self.duration_in_secs; + } + + pub fn tick(&mut self) { + if self.is_running && self.time_remaining_in_sec > 0 { + self.time_remaining_in_sec -= 1; + + if self.time_remaining_in_sec == 0 { + if self.enable_notifications { + match self.mode { + TimerMode::Work => send_desktop_notification("Work Done", "Time to take a break."), + TimerMode::Break => send_desktop_notification("Break Over", "Time to focus."), + } + } + + self.toggle_mode(); + } + } + } + + pub fn reset(&mut self) { + self.is_running = false; + self.mode = TimerMode::Work; + self.duration_in_secs = self.work_mins * SECONDS_PER_MIN; + self.time_remaining_in_sec = self.duration_in_secs; + } + + pub fn toggle_help(&mut self) { + self.show_help = !self.show_help; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn termato_new_when_called_it_should_return_expected_starting_state() { + let result = Termato::new(25, 5); + + assert_eq!(result.work_mins, 25); + assert_eq!(result.break_mins, 5); + assert_eq!(result.mode, TimerMode::Work); + assert!(!result.is_running); + assert_eq!(result.duration_in_secs, 1500); + assert_eq!(result.time_remaining_in_sec, 1500); + assert!(!result.show_help); + assert!(!result.enable_notifications); + assert!(!result.enable_visualizer); + assert!(!result.should_quit); + } + + #[test] + fn termato_update_when_called_with_messages_it_should_update_state() { + let mut termato = Termato::new(25, 5); + + termato.update(Message::ToggleRunning); + assert!(termato.is_running); + + termato.update(Message::ToggleHelp); + assert!(termato.show_help); + + termato.update(Message::ToggleMode); + assert_eq!(termato.mode, TimerMode::Break); + + termato.update(Message::Reset); + assert!(!termato.is_running); + assert_eq!(termato.mode, TimerMode::Work); + + termato.update(Message::Quit); + assert!(termato.should_quit); + } + + #[test] + fn termato_tick_when_called_and_not_running_it_should_do_nothing() { + let mut termato = Termato::new(1, 1); + + termato.update(Message::Tick); + + assert_eq!(termato.time_remaining_in_sec, 60); + } + + #[test] + fn termato_tick_when_called_and_no_time_remaining_it_should_do_nothing() { + let mut termato = Termato::new(1, 1); + termato.is_running = true; + termato.time_remaining_in_sec = 0; + + termato.update(Message::Tick); + + assert_eq!(termato.time_remaining_in_sec, 0); + } + + #[test] + fn termato_tick_when_running_and_time_reamining_it_should_reduce_remaining_by_one() { + let mut termato = Termato::new(1, 1); + termato.is_running = true; + + termato.update(Message::Tick); + + assert_eq!(termato.time_remaining_in_sec, 59); + } + + #[test] + fn termato_tick_when_running_on_work_and_time_remaining_reaches_zero_it_should_toggle_to_break() { + let mut termato = Termato::new(1, 2); + termato.is_running = true; + termato.time_remaining_in_sec = 1; + + termato.update(Message::Tick); + + assert_eq!(termato.mode, TimerMode::Break); + assert_eq!(termato.duration_in_secs, 120); + assert_eq!(termato.time_remaining_in_sec, 120); + } + + #[test] + fn termato_tick_when_running_on_break_and_time_remaining_reaches_zero_it_should_toggle_to_work() { + let mut termato = Termato::new(2, 1); + termato.mode = TimerMode::Break; + termato.is_running = true; + termato.time_remaining_in_sec = 1; + + termato.update(Message::Tick); + + assert_eq!(termato.mode, TimerMode::Work); + assert_eq!(termato.duration_in_secs, 120); + assert_eq!(termato.time_remaining_in_sec, 120); + } + + #[test] + fn termato_toggle_mode_when_called_it_should_toggle_the_mode() { + let mut termato = Termato::new(2, 1); + + termato.toggle_mode(); + assert_eq!(termato.mode, TimerMode::Break); + assert_eq!(termato.duration_in_secs, 60); + assert_eq!(termato.time_remaining_in_sec, 60); + + termato.toggle_mode(); + assert_eq!(termato.mode, TimerMode::Work); + assert_eq!(termato.duration_in_secs, 120); + assert_eq!(termato.time_remaining_in_sec, 120); + } + + #[test] + fn termato_toggle_running_when_called_and_already_running_it_should_pause() { + let mut termato = Termato::new(1, 1); + termato.is_running = true; + + termato.toggle_running(); + + assert!(!termato.is_running) + } + + #[test] + fn termato_toggle_running_when_called_and_already_paused_it_should_run() { + let mut termato = Termato::new(1, 1); + + termato.toggle_running(); + + assert!(termato.is_running) + } + + #[test] + fn termato_rest_when_called_it_should_reset_the_apps_state() { + let mut termato = Termato::new(1, 1); + termato.is_running = true; + termato.mode = TimerMode::Break; + termato.duration_in_secs = 1; + termato.time_remaining_in_sec = 1; + + termato.reset(); + + assert!(!termato.is_running); + assert_eq!(termato.mode, TimerMode::Work); + assert_eq!(termato.duration_in_secs, 60); + assert_eq!(termato.time_remaining_in_sec, 60); + } + + #[test] + fn termato_toggle_help_when_called_it_should_toggle_help() { + let mut termato = Termato::new(1, 1); + + termato.toggle_help(); + + assert!(termato.show_help); + + termato.toggle_help(); + + assert!(!termato.show_help); + } + + #[test] + fn termato_notifications_when_called_it_should_verify_opt_in_behavior() { + let result = Termato::new(25, 5); + assert!(!result.enable_notifications); + + let result = result.with_notifications(true); + assert!(result.enable_notifications); + } + + #[test] + fn termato_visualizer_when_called_it_should_verify_opt_in_behavior() { + let result = Termato::new(25, 5); + assert!(!result.enable_visualizer); + + let result = result.with_visualizer(true); + assert!(result.enable_visualizer); + } +}