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}