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}