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}