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    pub session_pause_count: u32,
46    pub session_pause_seconds: u32,
47    pause_started_at: Option<Instant>,
48}
49
50impl Timer {
51    pub fn new(config: TimerConfig) -> Self {
52        let focus_secs = config.focus_minutes * 60;
53        Self {
54            mode: TimerMode::Focus,
55            state: crate::model::TimerState::Idle,
56            total_seconds: focus_secs,
57            elapsed_seconds: 0,
58            started_at: None,
59            completed_focus_sessions: 0,
60            custom_minutes: config.focus_minutes,
61            config,
62            session_pause_count: 0,
63            session_pause_seconds: 0,
64            pause_started_at: None,
65        }
66    }
67
68    pub fn reset_session_pauses(&mut self) {
69        self.session_pause_count = 0;
70        self.session_pause_seconds = 0;
71        self.pause_started_at = None;
72    }
73
74    pub fn session_meta(&self) -> crate::storage::SessionMeta {
75        let mut pause_seconds = self.session_pause_seconds;
76        if let Some(start) = self.pause_started_at {
77            pause_seconds = pause_seconds.saturating_add(start.elapsed().as_secs() as u32);
78        }
79        crate::storage::SessionMeta {
80            note: String::new(),
81            tags: Vec::new(),
82            pause_count: self.session_pause_count,
83            pause_seconds,
84        }
85    }
86
87    pub fn sync_config(&mut self, config: TimerConfig) {
88        self.config = config;
89        self.custom_minutes = config.focus_minutes;
90        if self.state != crate::model::TimerState::Running {
91            self.total_seconds = self.duration_seconds();
92            if self.state == crate::model::TimerState::Idle {
93                self.elapsed_seconds = 0;
94            }
95        }
96    }
97
98    pub fn duration_seconds(&self) -> u32 {
99        match self.mode {
100            TimerMode::Focus => self.config.focus_minutes * 60,
101            TimerMode::ShortBreak => self.config.short_break_minutes * 60,
102            TimerMode::LongBreak => self.config.long_break_minutes * 60,
103            TimerMode::Custom => self.custom_minutes * 60,
104        }
105    }
106
107    pub fn configure(&mut self, mode: TimerMode) {
108        self.mode = mode;
109        self.total_seconds = self.duration_seconds();
110        self.elapsed_seconds = 0;
111        self.state = crate::model::TimerState::Idle;
112        self.started_at = None;
113    }
114
115    pub fn set_custom_minutes(&mut self, minutes: u32) {
116        self.custom_minutes = minutes.clamp(1, 240);
117        if self.mode == TimerMode::Custom && self.state != crate::model::TimerState::Running {
118            self.total_seconds = self.custom_minutes * 60;
119            self.elapsed_seconds = 0;
120        }
121    }
122
123    pub fn set_focus_minutes(&mut self, minutes: u32) {
124        let m = minutes.clamp(1, 240);
125        self.config.focus_minutes = m;
126        self.custom_minutes = m;
127        if self.mode == TimerMode::Focus && self.state != crate::model::TimerState::Running {
128            self.total_seconds = m * 60;
129            self.elapsed_seconds = 0;
130        }
131    }
132
133    pub fn current_elapsed_secs_f64(&self) -> f64 {
134        if self.state == crate::model::TimerState::Running {
135            if let Some(start) = self.started_at {
136                return start.elapsed().as_secs_f64().min(self.total_seconds as f64);
137            }
138        }
139        self.elapsed_seconds as f64
140    }
141
142    pub fn current_elapsed_seconds(&self) -> u32 {
143        self.current_elapsed_secs_f64() as u32
144    }
145
146    pub fn start(&mut self) {
147        if self.state == crate::model::TimerState::Running {
148            return;
149        }
150        if self.state == crate::model::TimerState::Paused {
151            if let Some(start) = self.pause_started_at.take() {
152                self.session_pause_seconds = self
153                    .session_pause_seconds
154                    .saturating_add(start.elapsed().as_secs() as u32);
155            }
156        }
157        if self.total_seconds == 0 {
158            self.total_seconds = self.duration_seconds();
159        }
160        match self.state {
161            crate::model::TimerState::Paused => {
162                self.started_at =
163                    Some(Instant::now() - Duration::from_secs(self.elapsed_seconds as u64));
164            }
165            crate::model::TimerState::Finished | crate::model::TimerState::Idle => {
166                if self.state == crate::model::TimerState::Finished {
167                    self.elapsed_seconds = 0;
168                }
169                self.started_at = Some(Instant::now());
170            }
171            _ => {}
172        }
173        self.state = crate::model::TimerState::Running;
174    }
175
176    pub fn pause(&mut self) {
177        if self.state != crate::model::TimerState::Running {
178            return;
179        }
180        self.session_pause_count = self.session_pause_count.saturating_add(1);
181        self.pause_started_at = Some(Instant::now());
182        self.elapsed_seconds = self.current_elapsed_seconds();
183        self.started_at = None;
184        self.state = crate::model::TimerState::Paused;
185    }
186
187    pub fn commit_pause_duration(&mut self) {
188        if let Some(start) = self.pause_started_at.take() {
189            self.session_pause_seconds = self
190                .session_pause_seconds
191                .saturating_add(start.elapsed().as_secs() as u32);
192        }
193    }
194
195    pub fn reset(&mut self) {
196        if let Some(start) = self.pause_started_at.take() {
197            self.session_pause_seconds = self
198                .session_pause_seconds
199                .saturating_add(start.elapsed().as_secs() as u32);
200        }
201        self.state = crate::model::TimerState::Idle;
202        self.elapsed_seconds = 0;
203        self.started_at = None;
204        self.total_seconds = self.duration_seconds();
205        self.reset_session_pauses();
206    }
207
208    pub fn tick(&mut self) -> bool {
209        if self.state != crate::model::TimerState::Running {
210            return false;
211        }
212        let new_elapsed = self.current_elapsed_seconds();
213        let just_finished =
214            new_elapsed >= self.total_seconds && self.elapsed_seconds < self.total_seconds;
215        self.elapsed_seconds = new_elapsed;
216        if just_finished {
217            self.state = crate::model::TimerState::Finished;
218            if self.mode == TimerMode::Focus {
219                self.completed_focus_sessions += 1;
220            }
221            return true;
222        }
223        false
224    }
225
226    pub fn skip(&mut self) {
227        self.elapsed_seconds = self.current_elapsed_seconds().max(1);
228        self.state = crate::model::TimerState::Finished;
229        self.started_at = None;
230    }
231
232    pub fn remaining_seconds(&self) -> i32 {
233        let elapsed = self.current_elapsed_seconds();
234        self.total_seconds as i32 - elapsed as i32
235    }
236
237    pub fn is_one_minute_warning(&self) -> bool {
238        self.state == crate::model::TimerState::Running && self.remaining_seconds() <= 60
239    }
240
241    pub fn progress(&self) -> f64 {
242        if self.total_seconds == 0 {
243            return 0.0;
244        }
245        (self.current_elapsed_secs_f64() / self.total_seconds as f64).clamp(0.0, 1.0)
246    }
247
248    pub fn remaining_secs_f64(&self) -> f64 {
249        (self.total_seconds as f64 - self.current_elapsed_secs_f64()).max(0.0)
250    }
251
252    pub fn format_remaining(&self) -> String {
253        self.format_remaining_parts().0
254    }
255
256    pub fn format_remaining_parts(&self) -> (String, String) {
257        let rem = self.remaining_secs_f64();
258        let h = (rem / 3600.0) as u32;
259        let m = ((rem % 3600.0) / 60.0) as u32;
260        let s = rem % 60.0;
261        let main = if h > 0 {
262            format!("{:02}:{:02}:{:02}", h, m, s as u32)
263        } else {
264            format!("{:02}:{:02}", m, s as u32)
265        };
266        let tenths = format!(".{}", (s * 10.0) as u32 % 10);
267        (main, tenths)
268    }
269
270    pub fn session_in_cycle(&self) -> u32 {
271        if self.config.long_break_every == 0 {
272            return 1;
273        }
274        (self.completed_focus_sessions % self.config.long_break_every) + 1
275    }
276
277    pub fn focus_sessions_in_cycle(&self) -> u32 {
278        let cycle = self.config.long_break_every.max(1);
279        let done = self.completed_focus_sessions;
280        if done > 0 && done.is_multiple_of(cycle) {
281            cycle
282        } else {
283            done % cycle
284        }
285    }
286
287    pub fn cycle_label(&self) -> String {
288        let cycle = self.config.long_break_every.max(1);
289        match self.mode {
290            TimerMode::Focus => format!("Focus {} of {}", self.session_in_cycle(), cycle),
291            TimerMode::ShortBreak => format!(
292                "Short break · {}/{} focus done",
293                self.focus_sessions_in_cycle(),
294                cycle
295            ),
296            TimerMode::LongBreak => format!("Long break · {cycle} focus sessions done"),
297            TimerMode::Custom => "Custom session".into(),
298        }
299    }
300}