use crate::config::Config; use chrono::{Datelike, Timelike}; use serde::{Deserialize, Serialize}; use std::time::Instant; #[derive(Debug, Clone, Copy, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub enum TimerState { Running, Paused, BreakActive, } impl Default for TimerState { fn default() -> Self { Self::Paused } } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum AppView { Dashboard, BreakScreen, Settings, Stats, } /// Snapshot of the full timer state, sent to the frontend on every tick #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TimerSnapshot { pub state: TimerState, pub current_view: AppView, pub time_remaining: u64, pub total_duration: u64, pub progress: f64, pub has_had_break: bool, pub seconds_since_last_break: u64, pub prebreak_warning: bool, pub snoozes_used: u32, pub can_snooze: bool, pub break_title: String, pub break_message: String, pub break_progress: f64, pub break_time_remaining: u64, pub break_total_duration: u64, pub break_past_half: bool, pub settings_modified: bool, pub idle_paused: bool, pub natural_break_occurred: bool, pub smart_breaks_enabled: bool, pub smart_break_threshold: u32, // F1: Microbreaks pub microbreak_enabled: bool, pub microbreak_active: bool, pub microbreak_time_remaining: u64, pub microbreak_total_duration: u64, pub microbreak_countdown: u64, // seconds until next microbreak pub microbreak_frequency: u32, // F3: Pomodoro pub pomodoro_enabled: bool, pub pomodoro_cycle_position: u32, pub pomodoro_total_in_cycle: u32, pub pomodoro_is_long_break: bool, pub pomodoro_next_is_long: bool, // F5: Screen dimming pub screen_dim_active: bool, pub screen_dim_progress: f64, // F2: Presentation mode pub presentation_mode_active: bool, pub deferred_break_pending: bool, // F10: Gamification pub is_long_break: bool, } /// Events emitted by the timer to the frontend #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct BreakStartedPayload { pub title: String, pub message: String, pub duration: u64, pub strict_mode: bool, pub snooze_duration: u32, pub fullscreen_mode: bool, pub is_long_break: bool, } pub struct TimerManager { pub state: TimerState, pub current_view: AppView, pub config: Config, pub time_until_next_break: u64, pub time_until_break_end: u64, pub break_total_duration: u64, pub snoozes_used: u32, pub seconds_since_last_break: u64, pub has_had_break: bool, pub prebreak_notification_active: bool, pub settings_modified: bool, // Pending config: edited in settings but not yet saved pub pending_config: Config, // Idle detection: true when auto-paused due to inactivity pub idle_paused: bool, // Smart breaks: track when idle started for natural break detection pub idle_start_time: Option, pub natural_break_occurred: bool, // F1: Microbreaks pub microbreak_time_remaining: u64, pub microbreak_active: bool, pub microbreak_time_until_end: u64, pub microbreak_total_duration: u64, // F3: Pomodoro pub pomodoro_cycle_position: u32, pub pomodoro_is_long_break: bool, // F2: Presentation mode pub presentation_mode_active: bool, pub deferred_break_pending: bool, } impl TimerManager { pub fn new() -> Self { let config = Config::load_or_default(); let freq = config.break_frequency_seconds(); let pending = config.clone(); let auto_start = config.auto_start; let microbreak_freq = config.microbreak_frequency as u64 * 60; Self { state: if auto_start { TimerState::Running } else { TimerState::Paused }, current_view: AppView::Dashboard, config, time_until_next_break: freq, time_until_break_end: 0, break_total_duration: 0, snoozes_used: 0, seconds_since_last_break: 0, has_had_break: false, prebreak_notification_active: false, settings_modified: false, pending_config: pending, idle_paused: false, idle_start_time: None, natural_break_occurred: false, // F1: Microbreaks microbreak_time_remaining: microbreak_freq, microbreak_active: false, microbreak_time_until_end: 0, microbreak_total_duration: 0, // F3: Pomodoro pomodoro_cycle_position: 0, pomodoro_is_long_break: false, // F2: Presentation mode presentation_mode_active: false, deferred_break_pending: false, } } /// Check idle state and auto-pause/resume accordingly. /// Returns IdleCheckResult indicating what happened. pub fn check_idle(&mut self) -> IdleCheckResult { if !self.config.idle_detection_enabled { // If idle detection disabled but we were idle-paused, resume if self.idle_paused { self.idle_paused = false; self.state = TimerState::Running; } return IdleCheckResult::None; } let idle_secs = get_idle_seconds(); let threshold = self.config.idle_timeout as u64; if idle_secs >= threshold && self.state == TimerState::Running { // Just became idle - record start time for smart breaks if self.idle_start_time.is_none() { self.idle_start_time = Some(Instant::now()); } self.idle_paused = true; self.state = TimerState::Paused; IdleCheckResult::JustPaused } else if idle_secs < threshold && self.idle_paused { // Just returned from idle self.idle_paused = false; self.state = TimerState::Running; // Check if this was a natural break if let Some(start_time) = self.idle_start_time { let idle_duration = start_time.elapsed().as_secs(); self.idle_start_time = None; if self.config.smart_breaks_enabled && self.state != TimerState::BreakActive && idle_duration >= self.config.smart_break_threshold as u64 { // Natural break detected! return IdleCheckResult::NaturalBreakDetected(idle_duration); } } IdleCheckResult::JustResumed } else { // Still idle - update idle_start_time if needed if self.state == TimerState::Paused && self.idle_start_time.is_none() && idle_secs >= threshold { // We were already paused when idle detection kicked in // Start tracking from now self.idle_start_time = Some(Instant::now()); } IdleCheckResult::None } } /// F2: Check if the foreground window is fullscreen (presentation mode) pub fn check_presentation_mode(&mut self) -> bool { if !self.config.presentation_mode_enabled { self.presentation_mode_active = false; return false; } let fs = is_foreground_fullscreen(); self.presentation_mode_active = fs; fs } /// Called every second. Returns what events should be emitted. pub fn tick(&mut self) -> TickResult { // Idle detection and natural break detection let idle_result = self.check_idle(); // Handle natural break detection if let IdleCheckResult::NaturalBreakDetected(duration) = idle_result { // Reset the timer completely since they took a natural break self.reset_timer(); self.has_had_break = true; self.seconds_since_last_break = 0; self.natural_break_occurred = true; return TickResult::NaturalBreakDetected { duration_seconds: duration, }; } // Track time since last break (always, even when paused) if self.has_had_break && self.state != TimerState::BreakActive { self.seconds_since_last_break += 1; } match self.state { TimerState::Running => { if !self.is_within_working_hours() { return TickResult::None; } // Pre-break notification let threshold = self.config.notification_before_break as u64; let fire_prebreak = self.config.notification_enabled && !self.config.immediately_start_breaks && threshold > 0 && self.time_until_next_break <= threshold && !self.prebreak_notification_active; if fire_prebreak { self.prebreak_notification_active = true; } if self.time_until_next_break > 0 { self.time_until_next_break -= 1; if fire_prebreak { TickResult::PreBreakWarning { seconds_until_break: self.time_until_next_break, } } else { TickResult::None } } else { // F2: Check presentation mode before starting break if self.check_presentation_mode() { self.deferred_break_pending = true; // Keep time at 0 so it triggers immediately when cleared self.time_until_next_break = 0; return TickResult::BreakDeferred; } // Clear any deferred state self.deferred_break_pending = false; self.start_break(); TickResult::BreakStarted(self.make_break_payload()) } } TimerState::BreakActive => { if self.time_until_break_end > 0 { self.time_until_break_end -= 1; TickResult::None } else { // Break completed naturally self.has_had_break = true; self.seconds_since_last_break = 0; self.advance_pomodoro_cycle(); self.reset_timer(); TickResult::BreakEnded } } TimerState::Paused => { // F2: Check if deferred break can now proceed if self.deferred_break_pending && !self.idle_paused { if !self.check_presentation_mode() { self.deferred_break_pending = false; self.state = TimerState::Running; self.start_break(); return TickResult::BreakStarted(self.make_break_payload()); } } TickResult::None } } } /// F1: Called every second for microbreak logic. Returns microbreak events. pub fn tick_microbreak(&mut self) -> MicrobreakTickResult { if !self.config.microbreak_enabled { return MicrobreakTickResult::None; } // Don't tick microbreaks during main breaks if self.config.microbreak_pause_during_break && self.state == TimerState::BreakActive { return MicrobreakTickResult::None; } // Don't tick when manually paused (but not during idle-pause which auto-resumes) if self.state == TimerState::Paused && !self.idle_paused { return MicrobreakTickResult::None; } // F2: Defer microbreaks during presentation mode if self.config.presentation_mode_defer_microbreaks && self.presentation_mode_active { return MicrobreakTickResult::None; } if self.microbreak_active { // Counting down microbreak if self.microbreak_time_until_end > 0 { self.microbreak_time_until_end -= 1; MicrobreakTickResult::None } else { // Microbreak ended self.microbreak_active = false; self.reset_microbreak_timer(); MicrobreakTickResult::MicrobreakEnded } } else { // Counting down to next microbreak if self.microbreak_time_remaining > 0 { self.microbreak_time_remaining -= 1; MicrobreakTickResult::None } else { // Start microbreak self.microbreak_active = true; self.microbreak_total_duration = self.config.microbreak_duration as u64; self.microbreak_time_until_end = self.microbreak_total_duration; MicrobreakTickResult::MicrobreakStarted } } } fn reset_microbreak_timer(&mut self) { self.microbreak_time_remaining = self.config.microbreak_frequency as u64 * 60; } fn make_break_payload(&self) -> BreakStartedPayload { BreakStartedPayload { title: if self.pomodoro_is_long_break { self.config.pomodoro_long_break_title.clone() } else { self.config.break_title.clone() }, message: if self.pomodoro_is_long_break { self.config.pomodoro_long_break_message.clone() } else { self.config.break_message.clone() }, duration: self.break_total_duration, strict_mode: self.config.strict_mode, snooze_duration: self.config.snooze_duration, fullscreen_mode: self.config.fullscreen_mode, is_long_break: self.pomodoro_is_long_break, } } pub fn start_break(&mut self) { // F3: Determine if this should be a long break (Pomodoro) if self.config.pomodoro_enabled { // cycle_position counts from 0. Position == short_breaks means it's the long break. self.pomodoro_is_long_break = self.pomodoro_cycle_position >= self.config.pomodoro_short_breaks; } else { self.pomodoro_is_long_break = false; } self.state = TimerState::BreakActive; self.current_view = AppView::BreakScreen; if self.pomodoro_is_long_break { self.break_total_duration = self.config.pomodoro_long_break_duration as u64 * 60; } else { self.break_total_duration = self.config.break_duration_seconds(); } self.time_until_break_end = self.break_total_duration; self.prebreak_notification_active = false; self.snoozes_used = 0; } /// F3: Advance the Pomodoro cycle position after a break completes fn advance_pomodoro_cycle(&mut self) { if !self.config.pomodoro_enabled { return; } if self.pomodoro_is_long_break { // After long break, reset cycle self.pomodoro_cycle_position = 0; } else { self.pomodoro_cycle_position += 1; } } pub fn reset_timer(&mut self) { self.state = TimerState::Running; self.current_view = AppView::Dashboard; self.time_until_next_break = self.config.break_frequency_seconds(); self.prebreak_notification_active = false; self.pomodoro_is_long_break = false; } pub fn toggle_timer(&mut self) { match self.state { TimerState::Running => { self.state = TimerState::Paused; } TimerState::Paused => { self.state = TimerState::Running; } _ => {} } } pub fn start_break_now(&mut self) -> Option { if self.state == TimerState::Running || self.state == TimerState::Paused { self.start_break(); Some(self.make_break_payload()) } else { None } } pub fn cancel_break(&mut self) -> bool { if self.state != TimerState::BreakActive { return false; } if self.config.strict_mode { return false; } let total = self.break_total_duration; let elapsed = total.saturating_sub(self.time_until_break_end); let past_half = total > 0 && elapsed * 2 >= total; if past_half && self.config.allow_end_early { // "End break" - counts as completed self.has_had_break = true; self.seconds_since_last_break = 0; self.advance_pomodoro_cycle(); self.reset_timer(); true } else if !past_half { // "Cancel break" - doesn't count // F3: Pomodoro reset-on-skip if self.config.pomodoro_enabled && self.config.pomodoro_reset_on_skip { self.pomodoro_cycle_position = 0; } self.reset_timer(); true } else { false } } pub fn snooze(&mut self) -> bool { if self.config.strict_mode || !self.can_snooze() { return false; } self.snoozes_used += 1; self.time_until_next_break = self.config.snooze_duration_seconds(); self.state = TimerState::Running; self.current_view = AppView::Dashboard; self.prebreak_notification_active = false; true } pub fn can_snooze(&self) -> bool { if self.config.snooze_limit == 0 { return true; // unlimited } self.snoozes_used < self.config.snooze_limit } pub fn is_within_working_hours(&self) -> bool { if !self.config.working_hours_enabled { return true; } let now = chrono::Local::now(); let day_of_week = now.weekday().num_days_from_monday() as usize; // 0 = Monday, 6 = Sunday let current_minutes = now.hour() * 60 + now.minute(); // Get the schedule for today if let Some(day_schedule) = self.config.working_hours_schedule.get(day_of_week) { if !day_schedule.enabled { return false; // Day is disabled, timer doesn't run } // Check if current time falls within any of the time ranges for range in &day_schedule.ranges { let start_mins = Config::time_to_minutes(&range.start); let end_mins = Config::time_to_minutes(&range.end); if current_minutes >= start_mins && current_minutes < end_mins { return true; } } } false // Not within any time range } pub fn set_view(&mut self, view: AppView) { self.current_view = view; } pub fn update_config(&mut self, config: Config) { self.pending_config = config; self.settings_modified = true; } pub fn save_config(&mut self) -> Result<(), String> { let validated = self.pending_config.clone().validate(); validated.save().map_err(|e| e.to_string())?; self.config = validated.clone(); self.pending_config = validated; self.settings_modified = false; Ok(()) } pub fn reset_config(&mut self) { self.pending_config = Config::default(); self.settings_modified = true; } pub fn snapshot(&self) -> TimerSnapshot { let total_freq = self.config.break_frequency_seconds(); let timer_progress = if total_freq > 0 { self.time_until_next_break as f64 / total_freq as f64 } else { 0.0 }; let break_elapsed = self .break_total_duration .saturating_sub(self.time_until_break_end); let break_progress = if self.break_total_duration > 0 { break_elapsed as f64 / self.break_total_duration as f64 } else { 0.0 }; let break_past_half = self.break_total_duration > 0 && break_elapsed * 2 >= self.break_total_duration; // Use pending_config for settings display, active config for timer let display_config = if self.current_view == AppView::Settings { &self.pending_config } else { &self.config }; // F5: Screen dim active when running and close to break let dim_secs = self.config.screen_dim_seconds as u64; let screen_dim_active = self.config.screen_dim_enabled && self.state == TimerState::Running && self.time_until_next_break <= dim_secs && self.time_until_next_break > 0 && !self.deferred_break_pending; let screen_dim_progress = if screen_dim_active && dim_secs > 0 { 1.0 - (self.time_until_next_break as f64 / dim_secs as f64) } else { 0.0 }; // F3: Pomodoro info let pomo_total = self.config.pomodoro_short_breaks + 1; let pomo_next_is_long = self.config.pomodoro_enabled && self.pomodoro_cycle_position + 1 >= pomo_total && self.state != TimerState::BreakActive; TimerSnapshot { state: self.state, current_view: self.current_view, time_remaining: self.time_until_next_break, total_duration: total_freq, progress: timer_progress, has_had_break: self.has_had_break, seconds_since_last_break: self.seconds_since_last_break, prebreak_warning: self.prebreak_notification_active, snoozes_used: self.snoozes_used, can_snooze: self.can_snooze(), break_title: if self.pomodoro_is_long_break { self.config.pomodoro_long_break_title.clone() } else { display_config.break_title.clone() }, break_message: if self.pomodoro_is_long_break { self.config.pomodoro_long_break_message.clone() } else { display_config.break_message.clone() }, break_progress, break_time_remaining: self.time_until_break_end, break_total_duration: self.break_total_duration, break_past_half, settings_modified: self.settings_modified, idle_paused: self.idle_paused, natural_break_occurred: self.natural_break_occurred, smart_breaks_enabled: display_config.smart_breaks_enabled, smart_break_threshold: display_config.smart_break_threshold, // F1: Microbreaks microbreak_enabled: self.config.microbreak_enabled, microbreak_active: self.microbreak_active, microbreak_time_remaining: self.microbreak_time_until_end, microbreak_total_duration: self.microbreak_total_duration, microbreak_countdown: self.microbreak_time_remaining, microbreak_frequency: self.config.microbreak_frequency, // F3: Pomodoro pomodoro_enabled: self.config.pomodoro_enabled, pomodoro_cycle_position: self.pomodoro_cycle_position, pomodoro_total_in_cycle: pomo_total, pomodoro_is_long_break: self.pomodoro_is_long_break, pomodoro_next_is_long: pomo_next_is_long, // F5: Screen dimming screen_dim_active, screen_dim_progress, // F2: Presentation mode presentation_mode_active: self.presentation_mode_active, deferred_break_pending: self.deferred_break_pending, // F10 is_long_break: self.pomodoro_is_long_break, } } } pub enum TickResult { None, BreakStarted(BreakStartedPayload), BreakEnded, PreBreakWarning { seconds_until_break: u64 }, NaturalBreakDetected { duration_seconds: u64 }, BreakDeferred, // F2 } /// F1: Microbreak tick result pub enum MicrobreakTickResult { None, MicrobreakStarted, MicrobreakEnded, } /// Result of checking idle state pub enum IdleCheckResult { None, JustPaused, JustResumed, NaturalBreakDetected(u64), // duration in seconds } /// Returns the number of seconds since last user input (mouse/keyboard). #[cfg(windows)] pub fn get_idle_seconds() -> u64 { use std::mem; use winapi::um::sysinfoapi::GetTickCount; use winapi::um::winuser::{GetLastInputInfo, LASTINPUTINFO}; unsafe { let mut lii: LASTINPUTINFO = mem::zeroed(); lii.cbSize = mem::size_of::() as u32; if GetLastInputInfo(&mut lii) != 0 { let tick = GetTickCount(); let idle_ms = tick.wrapping_sub(lii.dwTime); (idle_ms / 1000) as u64 } else { 0 } } } #[cfg(not(windows))] pub fn get_idle_seconds() -> u64 { 0 } /// F2: Check if the foreground window is a fullscreen application #[cfg(windows)] pub fn is_foreground_fullscreen() -> bool { use std::mem; use winapi::shared::windef::{HWND, RECT}; use winapi::um::winuser::{ GetForegroundWindow, GetWindowRect, MonitorFromWindow, GetMonitorInfoW, MONITORINFO, MONITOR_DEFAULTTONEAREST, }; unsafe { let hwnd: HWND = GetForegroundWindow(); if hwnd.is_null() { return false; } // Get the monitor this window is on let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); if monitor.is_null() { return false; } let mut mi: MONITORINFO = mem::zeroed(); mi.cbSize = mem::size_of::() as u32; if GetMonitorInfoW(monitor, &mut mi) == 0 { return false; } let mut wr: RECT = mem::zeroed(); if GetWindowRect(hwnd, &mut wr) == 0 { return false; } // Check if window rect covers the monitor rect let mr = mi.rcMonitor; wr.left <= mr.left && wr.top <= mr.top && wr.right >= mr.right && wr.bottom >= mr.bottom } } #[cfg(not(windows))] pub fn is_foreground_fullscreen() -> bool { false } /// F9: Get all monitor rects for multi-monitor break enforcement #[cfg(windows)] pub fn get_all_monitors() -> Vec { use winapi::shared::windef::{HMONITOR, HDC, LPRECT}; use winapi::um::winuser::{EnumDisplayMonitors, GetMonitorInfoW, MONITORINFO}; unsafe extern "system" fn callback( monitor: HMONITOR, _hdc: HDC, _rect: LPRECT, data: isize, ) -> i32 { let monitors = &mut *(data as *mut Vec); let mut mi: MONITORINFO = std::mem::zeroed(); mi.cbSize = std::mem::size_of::() as u32; if GetMonitorInfoW(monitor, &mut mi) != 0 { let r = mi.rcMonitor; monitors.push(MonitorInfo { x: r.left, y: r.top, width: (r.right - r.left) as u32, height: (r.bottom - r.top) as u32, is_primary: (mi.dwFlags & 1) != 0, // MONITORINFOF_PRIMARY = 1 }); } 1 // continue enumeration } let mut monitors: Vec = Vec::new(); unsafe { EnumDisplayMonitors( std::ptr::null_mut(), std::ptr::null(), Some(callback), &mut monitors as *mut Vec as isize, ); } monitors } #[cfg(not(windows))] pub fn get_all_monitors() -> Vec { Vec::new() } #[derive(Debug, Clone)] pub struct MonitorInfo { pub x: i32, pub y: i32, pub width: u32, pub height: u32, pub is_primary: bool, }