Skip to main content

void/
timer.rs

1use std::time::{Duration, Instant};
2
3use crate::model::TimerMode;
4
5#[derive(Debug, Clone, Copy)]
6pub struct TimerConfig {
7    pub focus_minutes: u32,
8    pub short_break_minutes: u32,
9    pub long_break_minutes: u32,
10    pub long_break_every: u32,
11}
12
13impl Default for TimerConfig {
14    fn default() -> Self {
15        Self {
16            focus_minutes: 25,
17            short_break_minutes: 5,
18            long_break_minutes: 15,
19            long_break_every: 4,
20        }
21    }
22}
23
24impl TimerConfig {
25    pub fn from_app_data(data: &crate::model::AppData) -> Self {
26        Self {
27            focus_minutes: data.focus_minutes,
28            short_break_minutes: data.short_break_minutes,
29            long_break_minutes: data.long_break_minutes,
30            long_break_every: data.long_break_every.max(1),
31        }
32    }
33}
34
35#[derive(Debug, Clone, Copy)]
36pub struct Timer {
37    pub mode: TimerMode,
38    pub state: crate::model::TimerState,
39    pub total_seconds: u32,
40    pub elapsed_seconds: u32,
41    pub started_at: Option<Instant>,
42    pub completed_focus_sessions: u32,
43    pub custom_minutes: u32,
44    pub config: TimerConfig,
45}
46
47impl Timer {
48    pub fn new(config: TimerConfig) -> Self {
49        let focus_secs = config.focus_minutes * 60;
50        Self {
51            mode: TimerMode::Focus,
52            state: crate::model::TimerState::Idle,
53            total_seconds: focus_secs,
54            elapsed_seconds: 0,
55            started_at: None,
56            completed_focus_sessions: 0,
57            custom_minutes: config.focus_minutes,
58            config,
59        }
60    }
61
62    pub fn sync_config(&mut self, config: TimerConfig) {
63        self.config = config;
64        self.custom_minutes = config.focus_minutes;
65        if self.state != crate::model::TimerState::Running {
66            self.total_seconds = self.duration_seconds();
67            if self.state == crate::model::TimerState::Idle {
68                self.elapsed_seconds = 0;
69            }
70        }
71    }
72
73    pub fn duration_seconds(&self) -> u32 {
74        match self.mode {
75            TimerMode::Focus => self.config.focus_minutes * 60,
76            TimerMode::ShortBreak => self.config.short_break_minutes * 60,
77            TimerMode::LongBreak => self.config.long_break_minutes * 60,
78            TimerMode::Custom => self.custom_minutes * 60,
79        }
80    }
81
82    pub fn configure(&mut self, mode: TimerMode) {
83        self.mode = mode;
84        self.total_seconds = self.duration_seconds();
85        self.elapsed_seconds = 0;
86        self.state = crate::model::TimerState::Idle;
87        self.started_at = None;
88    }
89
90    pub fn set_custom_minutes(&mut self, minutes: u32) {
91        self.custom_minutes = minutes.clamp(1, 240);
92        if self.mode == TimerMode::Custom && self.state != crate::model::TimerState::Running {
93            self.total_seconds = self.custom_minutes * 60;
94            self.elapsed_seconds = 0;
95        }
96    }
97
98    pub fn set_focus_minutes(&mut self, minutes: u32) {
99        let m = minutes.clamp(1, 240);
100        self.config.focus_minutes = m;
101        self.custom_minutes = m;
102        if self.mode == TimerMode::Focus && self.state != crate::model::TimerState::Running {
103            self.total_seconds = m * 60;
104            self.elapsed_seconds = 0;
105        }
106    }
107
108    pub fn current_elapsed_secs_f64(&self) -> f64 {
109        if self.state == crate::model::TimerState::Running {
110            if let Some(start) = self.started_at {
111                return start.elapsed().as_secs_f64().min(self.total_seconds as f64);
112            }
113        }
114        self.elapsed_seconds as f64
115    }
116
117    pub fn current_elapsed_seconds(&self) -> u32 {
118        self.current_elapsed_secs_f64() as u32
119    }
120
121    pub fn start(&mut self) {
122        if self.state == crate::model::TimerState::Running {
123            return;
124        }
125        if self.total_seconds == 0 {
126            self.total_seconds = self.duration_seconds();
127        }
128        match self.state {
129            crate::model::TimerState::Paused => {
130                self.started_at =
131                    Some(Instant::now() - Duration::from_secs(self.elapsed_seconds as u64));
132            }
133            crate::model::TimerState::Finished | crate::model::TimerState::Idle => {
134                if self.state == crate::model::TimerState::Finished {
135                    self.elapsed_seconds = 0;
136                }
137                self.started_at = Some(Instant::now());
138            }
139            _ => {}
140        }
141        self.state = crate::model::TimerState::Running;
142    }
143
144    pub fn pause(&mut self) {
145        if self.state != crate::model::TimerState::Running {
146            return;
147        }
148        self.elapsed_seconds = self.current_elapsed_seconds();
149        self.started_at = None;
150        self.state = crate::model::TimerState::Paused;
151    }
152
153    pub fn reset(&mut self) {
154        self.state = crate::model::TimerState::Idle;
155        self.elapsed_seconds = 0;
156        self.started_at = None;
157        self.total_seconds = self.duration_seconds();
158    }
159
160    pub fn tick(&mut self) -> bool {
161        if self.state != crate::model::TimerState::Running {
162            return false;
163        }
164        let new_elapsed = self.current_elapsed_seconds();
165        let just_finished =
166            new_elapsed >= self.total_seconds && self.elapsed_seconds < self.total_seconds;
167        self.elapsed_seconds = new_elapsed;
168        if just_finished {
169            self.state = crate::model::TimerState::Finished;
170            if self.mode == TimerMode::Focus {
171                self.completed_focus_sessions += 1;
172            }
173            return true;
174        }
175        false
176    }
177
178    pub fn skip(&mut self) {
179        self.elapsed_seconds = self.current_elapsed_seconds().max(1);
180        self.state = crate::model::TimerState::Finished;
181        self.started_at = None;
182    }
183
184    pub fn remaining_seconds(&self) -> i32 {
185        let elapsed = self.current_elapsed_seconds();
186        self.total_seconds as i32 - elapsed as i32
187    }
188
189    pub fn progress(&self) -> f64 {
190        if self.total_seconds == 0 {
191            return 0.0;
192        }
193        (self.current_elapsed_secs_f64() / self.total_seconds as f64).clamp(0.0, 1.0)
194    }
195
196    pub fn remaining_secs_f64(&self) -> f64 {
197        (self.total_seconds as f64 - self.current_elapsed_secs_f64()).max(0.0)
198    }
199
200    pub fn format_remaining(&self) -> String {
201        self.format_remaining_parts().0
202    }
203
204    pub fn format_remaining_parts(&self) -> (String, String) {
205        let rem = self.remaining_secs_f64();
206        let h = (rem / 3600.0) as u32;
207        let m = ((rem % 3600.0) / 60.0) as u32;
208        let s = rem % 60.0;
209        let main = if h > 0 {
210            format!("{:02}:{:02}:{:02}", h, m, s as u32)
211        } else {
212            format!("{:02}:{:02}", m, s as u32)
213        };
214        let tenths = format!(".{}", (s * 10.0) as u32 % 10);
215        (main, tenths)
216    }
217
218    pub fn session_in_cycle(&self) -> u32 {
219        if self.config.long_break_every == 0 {
220            return 1;
221        }
222        (self.completed_focus_sessions % self.config.long_break_every) + 1
223    }
224
225    pub fn focus_sessions_in_cycle(&self) -> u32 {
226        let cycle = self.config.long_break_every.max(1);
227        let done = self.completed_focus_sessions;
228        if done > 0 && done.is_multiple_of(cycle) {
229            cycle
230        } else {
231            done % cycle
232        }
233    }
234
235    pub fn cycle_label(&self) -> String {
236        let cycle = self.config.long_break_every.max(1);
237        match self.mode {
238            TimerMode::Focus => format!("Focus {} of {}", self.session_in_cycle(), cycle),
239            TimerMode::ShortBreak => format!(
240                "Short break · {}/{} focus done",
241                self.focus_sessions_in_cycle(),
242                cycle
243            ),
244            TimerMode::LongBreak => format!("Long break · {cycle} focus sessions done"),
245            TimerMode::Custom => "Custom session".into(),
246        }
247    }
248}