Skip to main content

void/app/
timer_ops.rs

1use super::*;
2use crate::model::TimerMode;
3
4impl App {
5    pub(crate) fn maybe_complete_task_estimate(&mut self, task_id: Option<u64>) {
6        let Some(id) = task_id else {
7            return;
8        };
9        let estimated = self
10            .data
11            .tasks
12            .iter()
13            .find(|t| t.id == id)
14            .map(|t| (t.title.clone(), t.actual_minutes, t.estimated_minutes));
15        let Some((title, actual, estimate)) = estimated else {
16            return;
17        };
18        if actual < estimate {
19            return;
20        }
21        match self.data.estimate_complete {
22            EstimateCompleteBehavior::Nudge => {
23                self.set_status(
24                    format!("Estimate reached for \"{title}\" — mark done?"),
25                    false,
26                );
27            }
28            EstimateCompleteBehavior::AutoDone => {
29                self.persist_data(|db, data| storage::mark_task_done(db, data, id));
30                if self.active_task == Some(id) {
31                    self.active_task = None;
32                    self.data.active_task_id = None;
33                    self.persist(|db| db.persist_active_task(None));
34                }
35                self.bump_data();
36                self.set_status(format!("\"{title}\" auto-completed (estimate met)."), false);
37                self.check_queue_empty();
38            }
39            EstimateCompleteBehavior::None => {}
40        }
41    }
42
43    pub fn end_session(&mut self) {
44        if self.timer.state == TimerState::Running {
45            self.pause_timer();
46        }
47        let today = storage::today_focus_minutes(&self.data);
48        let goal = self.data.daily_goal_minutes;
49        let queue_note = if self.queue_empty() {
50            "all tasks done"
51        } else {
52            "tasks remain"
53        };
54        self.set_status(
55            format!(
56                "Session ended — today {}/{} min · goal streak {} days · {queue_note}",
57                today, goal, self.data.goal_streak_days
58            ),
59            false,
60        );
61    }
62
63    pub fn on_tick(&mut self) {
64        let _ = storage::ensure_today_reset(&self.db, &mut self.data);
65        if self.data.auto_pause_idle_minutes > 0
66            && self.timer.state == TimerState::Running
67            && self.last_activity.elapsed()
68                > Duration::from_secs(self.data.auto_pause_idle_minutes as u64 * 60)
69        {
70            self.pause_timer();
71            self.set_status("Auto-paused — terminal idle.", false);
72        }
73        if self.data.warn_one_minute
74            && self.timer.is_one_minute_warning()
75            && !self.end_warning_shown
76        {
77            self.end_warning_shown = true;
78            if self.data.sound_enabled {
79                sound::play_pause();
80            }
81            self.set_status("1 minute remaining!", false);
82        }
83        if !self.timer.is_one_minute_warning() {
84            self.end_warning_shown = false;
85        }
86        let just_finished = self.timer.tick();
87        if just_finished {
88            self.on_timer_finished(false);
89        }
90        if self.status.is_some()
91            && !self.status_error
92            && self.last_status_set.elapsed() > Duration::from_secs(4)
93        {
94            self.status = None;
95        }
96    }
97
98    pub(crate) fn on_timer_finished(&mut self, skipped: bool) {
99        let mode = self.timer.mode;
100        if mode == TimerMode::Focus {
101            let mins = self.elapsed_minutes(skipped);
102            let task_id = self.active_task;
103            let meta = self.timer.session_meta();
104            self.persist_data(|db, data| {
105                storage::record_focus_session_with_meta(db, data, mins, task_id, mode, meta)
106            });
107            self.maybe_complete_task_estimate(task_id);
108            if self.data.sound_enabled {
109                sound::play_finish();
110            }
111            if self.data.notify_on_finish {
112                let msg = if skipped {
113                    format!("Logged {} min (skipped early)", mins)
114                } else {
115                    format!("+{} min logged — time for a break", mins)
116                };
117                let kind = if skipped {
118                    sound::NotifyKind::SessionSkipped
119                } else {
120                    sound::NotifyKind::FocusComplete
121                };
122                sound::notify_typed(kind, "Void · Focus complete", &msg);
123            }
124            self.set_status(
125                format!(
126                    "Focus {}: +{} min",
127                    if skipped { "skipped" } else { "complete" },
128                    mins
129                ),
130                false,
131            );
132            self.maybe_advance_task();
133            self.bump_data();
134            if !skipped {
135                self.persist_timer_state();
136            }
137            self.timer.reset_session_pauses();
138            self.advance_to_break();
139        } else if mode == TimerMode::Custom {
140            let mins = self.elapsed_minutes(skipped);
141            let task_id = self.active_task;
142            let meta = self.timer.session_meta();
143            self.persist_data(|db, data| {
144                storage::record_focus_session_with_meta(db, data, mins, task_id, mode, meta)
145            });
146            if self.data.sound_enabled {
147                sound::play_finish();
148            }
149            self.set_status(format!("Custom session complete: +{} min", mins), false);
150            self.bump_data();
151            self.timer.configure(TimerMode::Focus);
152            self.timer.reset_session_pauses();
153            self.persist_timer_state();
154        } else {
155            let break_mins = self.elapsed_minutes(false);
156            self.persist_data(|db, data| storage::record_break_session(db, data, mode, break_mins));
157            if self.data.sound_enabled {
158                sound::play_finish();
159            }
160            if self.data.notify_on_finish {
161                sound::notify_typed(
162                    sound::NotifyKind::BreakComplete,
163                    "Void · Break over",
164                    "Break finished — ready to focus again",
165                );
166            }
167            self.set_status("Break finished. Ready for focus.", false);
168            self.bump_data();
169            self.advance_to_focus();
170        }
171    }
172
173    pub(crate) fn advance_to_break(&mut self) {
174        let long_break = self.timer.completed_focus_sessions > 0
175            && self
176                .timer
177                .completed_focus_sessions
178                .is_multiple_of(self.timer.config.long_break_every);
179        let next = if long_break {
180            TimerMode::LongBreak
181        } else {
182            TimerMode::ShortBreak
183        };
184        self.timer.configure(next);
185        self.persist_timer_state();
186        if self.data.auto_start_breaks {
187            self.timer.start();
188            if self.data.sound_enabled {
189                sound::play_start();
190            }
191            self.set_status(format!("{} started.", next.label()), false);
192        }
193    }
194
195    pub(crate) fn advance_to_focus(&mut self) {
196        self.timer.configure(TimerMode::Focus);
197        self.persist_timer_state();
198        if self.queue_empty() && self.data.empty_queue_behavior == EmptyQueueBehavior::PauseTimer {
199            self.set_status("All tasks done — timer waiting. [E] end session", false);
200            return;
201        }
202        self.auto_pick_task_if_needed();
203        if self.data.auto_start_focus {
204            self.timer.start();
205            if self.data.sound_enabled {
206                sound::play_start();
207            }
208            self.set_status("Focus started.", false);
209        }
210    }
211
212    pub fn toggle_timer(&mut self) {
213        if self.timer.state == TimerState::Running {
214            self.pause_timer();
215        } else {
216            self.start_timer();
217        }
218    }
219
220    pub fn start_timer(&mut self) {
221        if self.timer.state == TimerState::Running {
222            return;
223        }
224        if self.timer.state == TimerState::Finished {
225            self.timer.reset();
226        }
227        if self.timer.mode == TimerMode::Focus {
228            self.auto_pick_task_if_needed();
229        }
230        self.timer.start();
231        self.end_warning_shown = false;
232        if self.data.sound_enabled {
233            sound::play_start();
234        }
235        self.set_status("Timer started.", false);
236    }
237
238    pub fn pause_timer(&mut self) {
239        if self.timer.state != TimerState::Running {
240            return;
241        }
242        let elapsed = self.timer.current_elapsed_seconds();
243        self.timer.pause();
244        if self.data.sound_enabled {
245            sound::play_pause();
246        }
247        let active_minutes = (elapsed / 60).max(1);
248        self.set_status(
249            format!(
250                "Paused at {} ({} min in).",
251                self.timer.format_remaining(),
252                active_minutes
253            ),
254            false,
255        );
256    }
257
258    pub fn reset_timer(&mut self) {
259        self.timer.reset();
260        self.set_status("Timer reset.", false);
261    }
262
263    pub fn cycle_mode(&mut self) {
264        if self.timer.state == TimerState::Running || self.timer.state == TimerState::Paused {
265            self.set_status("Stop the timer before changing mode.", true);
266            return;
267        }
268        let next = match self.timer.mode {
269            TimerMode::Focus => TimerMode::ShortBreak,
270            TimerMode::ShortBreak => TimerMode::LongBreak,
271            TimerMode::LongBreak => TimerMode::Custom,
272            TimerMode::Custom => TimerMode::Focus,
273        };
274        self.timer.configure(next);
275        self.set_status(format!("Mode: {}", next.label()), false);
276    }
277
278    pub fn cycle_timer_preset(&mut self) {
279        if self.timer.state == TimerState::Running || self.timer.state == TimerState::Paused {
280            self.set_status("Stop the timer before switching preset.", true);
281            return;
282        }
283        if let Some(preset) = storage::cycle_timer_preset(&mut self.data) {
284            self.timer
285                .sync_config(TimerConfig::from_app_data(&self.data));
286            if let Err(e) = self.db.persist_timer_settings(&self.data) {
287                self.set_status(format!("Save error: {e}"), true);
288            }
289            self.persist_setting(
290                "active_preset",
291                self.data.active_preset.clone().unwrap_or_default(),
292            );
293            self.set_status(format!("Preset: {}", preset.name), false);
294        }
295    }
296
297    pub fn adjust_minutes(&mut self, delta: i32) {
298        if self.timer.state == TimerState::Running || self.timer.state == TimerState::Paused {
299            self.set_status("Stop the timer before adjusting duration.", true);
300            return;
301        }
302        match self.timer.mode {
303            TimerMode::Focus => {
304                let cur = self.timer.config.focus_minutes as i32 + delta;
305                let v = cur.clamp(1, 240) as u32;
306                self.timer.set_focus_minutes(v);
307                self.data.focus_minutes = v;
308            }
309            TimerMode::ShortBreak => {
310                let cur = self.timer.config.short_break_minutes as i32 + delta;
311                let v = cur.clamp(1, 60) as u32;
312                self.timer.config.short_break_minutes = v;
313                self.data.short_break_minutes = v;
314                self.timer.total_seconds = self.timer.duration_seconds();
315            }
316            TimerMode::LongBreak => {
317                let cur = self.timer.config.long_break_minutes as i32 + delta;
318                let v = cur.clamp(1, 120) as u32;
319                self.timer.config.long_break_minutes = v;
320                self.data.long_break_minutes = v;
321                self.timer.total_seconds = self.timer.duration_seconds();
322            }
323            TimerMode::Custom => {
324                let cur = self.timer.custom_minutes as i32 + delta;
325                let v = cur.clamp(1, 240) as u32;
326                self.timer.set_custom_minutes(v);
327            }
328        }
329        if let Err(e) = self.db.persist_timer_settings(&self.data) {
330            self.set_status(format!("Save error: {e}"), true);
331        }
332        self.set_status(
333            format!(
334                "{} length: {} min",
335                self.timer.mode.label(),
336                self.timer.duration_seconds() / 60
337            ),
338            false,
339        );
340    }
341}