a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add timer and scheduler

+241
+35
src/idle/mod.rs
··· 1 + use std::time::Duration; 2 + use anyhow::Result; 3 + 4 + pub trait IdleDetector: Send { 5 + /// Returns how long the user has been idle, or None if detection is unavailable. 6 + fn idle_duration(&mut self) -> Result<Option<Duration>>; 7 + } 8 + 9 + #[cfg(target_os = "linux")] 10 + mod linux; 11 + #[cfg(target_os = "windows")] 12 + mod windows; 13 + 14 + pub fn create() -> Box<dyn IdleDetector> { 15 + #[cfg(target_os = "linux")] 16 + { 17 + Box::new(linux::LinuxIdleDetector::new()) 18 + } 19 + #[cfg(target_os = "windows")] 20 + { 21 + Box::new(windows::WindowsIdleDetector) 22 + } 23 + #[cfg(not(any(target_os = "linux", target_os = "windows")))] 24 + { 25 + Box::new(NullIdleDetector) 26 + } 27 + } 28 + 29 + /// Fallback for unsupported platforms. 30 + pub struct NullIdleDetector; 31 + impl IdleDetector for NullIdleDetector { 32 + fn idle_duration(&mut self) -> Result<Option<Duration>> { 33 + Ok(None) 34 + } 35 + }
+206
src/timer/mod.rs
··· 1 + pub mod profile; 2 + pub mod scheduler; 3 + 4 + pub use profile::{BreakMode, BreakLevel, LongBreakTrigger, Profile}; 5 + pub use scheduler::{ScheduledBreak, Scheduler}; 6 + 7 + use std::sync::{Arc, Mutex}; 8 + use std::time::{Duration, Instant}; 9 + use tokio::sync::mpsc; 10 + use tokio::time::interval; 11 + 12 + use crate::config::AppConfig; 13 + 14 + const TICK_INTERVAL: Duration = Duration::from_secs(1); 15 + 16 + /// Commands sent to the timer task from the UI/tray. 17 + #[derive(Debug)] 18 + pub enum TimerCommand { 19 + /// Pause the timer for the given duration. 20 + PauseFor(Duration), 21 + /// Resume from manual pause early. 22 + Resume, 23 + /// Advance to the next scheduled break immediately. 24 + SkipToNextBreak, 25 + /// Called when a break overlay is dismissed (break finished or snoozed). 26 + BreakEnded { snoozed: bool }, 27 + /// Called when idle is detected. 28 + IdleDetected { duration: Duration }, 29 + /// Reload profile (after settings change). 30 + SetProfile(Profile), 31 + /// Quit. 32 + Shutdown, 33 + } 34 + 35 + /// Events sent from the timer task to the UI. 36 + #[derive(Debug, Clone)] 37 + pub enum TimerEvent { 38 + /// A break is starting; show the overlay. 39 + BreakStarting(ScheduledBreak), 40 + /// Snooze period starting; overlay hides temporarily. 41 + SnoozePeriodStarting { remaining_secs: u64 }, 42 + /// Work phase: tick with current countdown to next break. 43 + WorkTick { secs_until_break: u64 }, 44 + /// Timer is paused (manual or idle). 45 + Paused, 46 + /// Timer resumed. 47 + Resumed, 48 + } 49 + 50 + #[derive(Debug, Clone, PartialEq)] 51 + enum TimerState { 52 + Working, 53 + Breaking { snooze_used: bool }, 54 + Snoozed { until: Instant }, 55 + ManualPause { until: Option<Instant> }, 56 + IdlePaused, 57 + } 58 + 59 + pub struct TimerTask { 60 + scheduler: Scheduler, 61 + state: TimerState, 62 + event_tx: mpsc::UnboundedSender<TimerEvent>, 63 + cmd_rx: mpsc::UnboundedReceiver<TimerCommand>, 64 + } 65 + 66 + impl TimerTask { 67 + pub fn spawn( 68 + profile: Profile, 69 + cfg: &AppConfig, 70 + ) -> (mpsc::UnboundedSender<TimerCommand>, mpsc::UnboundedReceiver<TimerEvent>) { 71 + let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); 72 + let (event_tx, event_rx) = mpsc::unbounded_channel(); 73 + let _ = cfg; // reserved for future use 74 + 75 + let task = TimerTask { 76 + scheduler: Scheduler::new(profile), 77 + state: TimerState::Working, 78 + event_tx, 79 + cmd_rx, 80 + }; 81 + 82 + tokio::spawn(async move { task.run().await }); 83 + (cmd_tx, event_rx) 84 + } 85 + 86 + async fn run(mut self) { 87 + let mut tick = interval(TICK_INTERVAL); 88 + let mut last_tick = Instant::now(); 89 + 90 + loop { 91 + tokio::select! { 92 + _ = tick.tick() => { 93 + let now = Instant::now(); 94 + let delta = now.duration_since(last_tick); 95 + last_tick = now; 96 + self.handle_tick(delta, now); 97 + } 98 + Some(cmd) = self.cmd_rx.recv() => { 99 + if !self.handle_command(cmd) { 100 + break; 101 + } 102 + } 103 + } 104 + } 105 + } 106 + 107 + fn handle_tick(&mut self, delta: Duration, now: Instant) { 108 + match &self.state.clone() { 109 + TimerState::ManualPause { until: Some(until) } => { 110 + if now >= *until { 111 + self.state = TimerState::Working; 112 + let _ = self.event_tx.send(TimerEvent::Resumed); 113 + } else { 114 + let _ = self.event_tx.send(TimerEvent::Paused); 115 + } 116 + } 117 + TimerState::ManualPause { until: None } | TimerState::IdlePaused => { 118 + let _ = self.event_tx.send(TimerEvent::Paused); 119 + } 120 + TimerState::Snoozed { until } => { 121 + if now >= *until { 122 + self.state = TimerState::Breaking { snooze_used: true }; 123 + let sched = self.current_break_or_next(); 124 + let _ = self.event_tx.send(TimerEvent::BreakStarting(sched)); 125 + } else { 126 + let remaining = (*until).duration_since(now).as_secs(); 127 + let _ = self.event_tx.send(TimerEvent::SnoozePeriodStarting { remaining_secs: remaining }); 128 + } 129 + } 130 + TimerState::Breaking { .. } => { 131 + // Overlay is up; no timer advancement. 132 + } 133 + TimerState::Working => { 134 + if let Some(break_due) = self.scheduler.tick(delta) { 135 + self.state = TimerState::Breaking { snooze_used: false }; 136 + let _ = self.event_tx.send(TimerEvent::BreakStarting(break_due)); 137 + } else { 138 + let secs = self.scheduler.secs_until_next_break(); 139 + let _ = self.event_tx.send(TimerEvent::WorkTick { secs_until_break: secs }); 140 + } 141 + } 142 + } 143 + } 144 + 145 + fn handle_command(&mut self, cmd: TimerCommand) -> bool { 146 + match cmd { 147 + TimerCommand::PauseFor(duration) => { 148 + let until = Instant::now() + duration; 149 + self.state = TimerState::ManualPause { until: Some(until) }; 150 + let _ = self.event_tx.send(TimerEvent::Paused); 151 + } 152 + TimerCommand::Resume => { 153 + self.state = TimerState::Working; 154 + let _ = self.event_tx.send(TimerEvent::Resumed); 155 + } 156 + TimerCommand::SkipToNextBreak => { 157 + if let Some(b) = self.scheduler.skip_to_next_break() { 158 + self.state = TimerState::Breaking { snooze_used: false }; 159 + let _ = self.event_tx.send(TimerEvent::BreakStarting(b)); 160 + } 161 + } 162 + TimerCommand::BreakEnded { snoozed } => { 163 + if snoozed { 164 + let snooze_dur = self.scheduler.profile().snooze_duration; 165 + let until = Instant::now() + snooze_dur; 166 + self.state = TimerState::Snoozed { until }; 167 + let _ = self.event_tx.send(TimerEvent::SnoozePeriodStarting { 168 + remaining_secs: snooze_dur.as_secs(), 169 + }); 170 + } else { 171 + let was_long = matches!( 172 + &self.state, 173 + TimerState::Breaking { .. } 174 + ) && self.scheduler.primary_cycle_count() == 0; 175 + self.scheduler.record_break_completed(was_long); 176 + self.state = TimerState::Working; 177 + let _ = self.event_tx.send(TimerEvent::Resumed); 178 + } 179 + } 180 + TimerCommand::IdleDetected { duration } => { 181 + if self.state == TimerState::Working { 182 + self.scheduler.record_idle_break(duration); 183 + self.state = TimerState::IdlePaused; 184 + let _ = self.event_tx.send(TimerEvent::Paused); 185 + } 186 + } 187 + TimerCommand::SetProfile(profile) => { 188 + self.scheduler.replace_profile(profile); 189 + self.state = TimerState::Working; 190 + } 191 + TimerCommand::Shutdown => return false, 192 + } 193 + true 194 + } 195 + 196 + fn current_break_or_next(&mut self) -> ScheduledBreak { 197 + self.scheduler 198 + .skip_to_next_break() 199 + .unwrap_or_else(|| ScheduledBreak { 200 + break_duration: self.scheduler.profile().primary_level().break_duration, 201 + label: self.scheduler.profile().primary_level().label.clone(), 202 + is_long_break: false, 203 + level_index: Some(self.scheduler.profile().levels.len() - 1), 204 + }) 205 + } 206 + }