1use std::fmt;
2use std::process;
3use std::time;
4
5use notify_rust::Notification;
6
7struct Timer {
8 duration: time::Duration,
9 start_time: Option<time::Instant>,
10 elapsed: time::Duration,
11 is_running: bool,
12}
13
14impl Timer {
15 fn new(minutes: u64, seconds: u64) -> Self {
16 let duration = time::Duration::from_secs(minutes * 60 + seconds);
17 Timer {
18 duration,
19 start_time: None,
20 elapsed: time::Duration::from_secs(0),
21 is_running: false,
22 }
23 }
24
25 fn start_or_pause(&mut self) {
26 if self.is_running {
27 self.elapsed = self.elapsed();
28 self.start_time = None;
29 } else {
30 self.start_time = Some(time::Instant::now());
31 }
32 self.is_running = !self.is_running;
33 }
34
35 fn reset(&mut self) {
36 self.start_time = None;
37 self.elapsed = time::Duration::from_secs(0);
38 self.is_running = false;
39 }
40
41 fn elapsed(&self) -> time::Duration {
42 match self.start_time {
43 Some(start_time) => self.elapsed + start_time.elapsed(),
44 None => self.elapsed,
45 }
46 }
47
48 fn remaining(&self) -> time::Duration {
49 if self.elapsed() >= self.duration {
50 return time::Duration::from_secs(0);
51 }
52 self.duration - self.elapsed()
53 }
54}
55
56impl fmt::Display for Timer {
57 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58 let remaining = self.remaining();
59 let (minutes, seconds) = get_min_sec_from_duration(remaining);
60 write!(f, "{:02}:{:02}", minutes, seconds)
61 }
62}
63
64#[derive(Debug, PartialEq)]
65pub enum PomodoroState {
66 Work,
67 Break,
68}
69
70pub struct Pomodoro {
71 work_timer: Timer,
72 break_timer: Timer,
73 state: PomodoroState,
74}
75
76impl Pomodoro {
77 pub fn new(work_time: (u64, u64), break_time: (u64, u64)) -> Self {
78 Pomodoro {
79 work_timer: Timer::new(work_time.0, work_time.1),
80 break_timer: Timer::new(break_time.0, break_time.1),
81 state: PomodoroState::Work,
82 }
83 }
84
85 pub fn break_time(&self) -> String {
86 self.break_timer.to_string()
87 }
88
89 pub fn work_time(&self) -> String {
90 self.work_timer.to_string()
91 }
92
93 pub fn state(&self) -> &PomodoroState {
94 &self.state
95 }
96
97 pub fn is_running(&self) -> bool {
98 match self.state {
99 PomodoroState::Work => self.work_timer.is_running,
100 PomodoroState::Break => self.break_timer.is_running,
101 }
102 }
103
104 pub fn start_or_pause(&mut self) {
105 match self.state {
106 PomodoroState::Work => {
107 self.work_timer.start_or_pause();
108 }
109 PomodoroState::Break => {
110 self.break_timer.start_or_pause();
111 }
112 }
113 }
114
115 pub fn reset(&mut self) {
116 self.work_timer.reset();
117 self.break_timer.reset();
118 self.state = PomodoroState::Work;
119 }
120
121 pub fn check_and_switch(&mut self) {
122 let (current_timer, next_timer, next_state, message) = match self.state {
123 PomodoroState::Work => (
124 &mut self.work_timer,
125 &mut self.break_timer,
126 PomodoroState::Break,
127 "It's time to have a break.",
128 ),
129 PomodoroState::Break => (
130 &mut self.break_timer,
131 &mut self.work_timer,
132 PomodoroState::Work,
133 "It's time to research.",
134 ),
135 };
136
137 if current_timer.remaining() == time::Duration::from_secs(0) {
138 current_timer.reset();
139 next_timer.start_or_pause();
140 self.state = next_state;
141 show_notification("Pomodoro Timer", message);
142 }
143 }
144}
145
146fn get_min_sec_from_duration(duration: time::Duration) -> (u64, u64) {
147 let total_seconds = duration.as_secs();
148 let minutes = total_seconds / 60;
149 let seconds = total_seconds % 60;
150 (minutes, seconds)
151}
152
153fn show_notification(title: &str, message: &str) {
154 if cfg!(target_os = "macos") {
155 match process::Command::new("osascript")
156 .arg("-e")
157 .arg(format!(
158 "display notification \"{}\" with title \"{}\"",
159 message, title
160 ))
161 .arg("-e")
162 .arg(format!("say \"{}\" using \"Thomas\"", message))
163 .output()
164 {
165 Ok(_) => {}
166 Err(e) => {
167 eprintln!("Failed to send notification: {}", e);
168 }
169 }
170 }
171
172 if cfg!(target_os = "linux") {
173 let _ = Notification::new()
174 .summary(title)
175 .body(message)
176 .show();
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
186
187 #[test]
188 fn test_timer_start_or_pause() {
189 let mut timer = Timer::new(1, 15);
191 timer.start_or_pause();
193 assert!(timer.is_running);
195 assert!(timer.start_time.is_some());
196 let elapsed = timer.elapsed();
197 assert!(timer.elapsed() > time::Duration::from_secs(0));
198 assert!(timer.remaining() < timer.duration);
199 std::thread::sleep(std::time::Duration::from_secs(1));
202 timer.start_or_pause();
204 assert!(!timer.is_running);
206 assert!(timer.elapsed() > elapsed + time::Duration::from_secs(1));
207 assert_eq!(timer.remaining(), timer.duration - timer.elapsed());
208 }
209
210 #[test]
211 fn test_timer_reset() {
212 let mut timer = Timer::new(1, 15);
214 timer.start_or_pause();
215 std::thread::sleep(std::time::Duration::from_secs(1));
216 timer.reset();
218 assert_eq!(timer.elapsed(), time::Duration::from_secs(0));
220 assert!(!timer.is_running);
221 assert!(timer.start_time.is_none());
222 assert_eq!(timer.remaining(), timer.duration);
223 }
224
225 #[test]
226 fn test_timer_remaining() {
227 let mut timer = Timer::new(0, 3);
229 assert_eq!(timer.remaining().as_secs(), 3);
231 timer.start_or_pause();
233 std::thread::sleep(std::time::Duration::from_secs(1));
234 assert!(timer.remaining().as_secs() > 0);
236 std::thread::sleep(std::time::Duration::from_secs(3));
238 let remaining = timer.remaining();
239 assert_eq!(remaining.as_secs(), 0);
241 }
242
243 #[test]
244 fn test_timer_display() {
245 let timer = Timer::new(1, 125);
247 assert_eq!(timer.to_string(), "03:05");
249 }
250
251 #[test]
252 fn test_pomodoro_initialization() {
253 let pomodoro = Pomodoro::new((25, 0), (2, 5));
255 assert_eq!(pomodoro.work_time(), "25:00");
257 assert_eq!(pomodoro.break_time(), "02:05");
258 assert_eq!(*pomodoro.state(), PomodoroState::Work);
259 assert!(!pomodoro.is_running());
260 }
261
262 #[test]
263 fn test_pomodoro_start_or_pause() {
264 let mut pomodoro = Pomodoro::new((0, 3), (0, 2));
266 pomodoro.start_or_pause();
268 assert!(pomodoro.is_running());
270 assert_eq!(pomodoro.work_time(), "00:02");
271 assert_eq!(pomodoro.break_time(), "00:02");
272 assert_eq!(*pomodoro.state(), PomodoroState::Work);
273 pomodoro.start_or_pause();
275 assert!(!pomodoro.is_running());
277 assert_eq!(pomodoro.work_time(), "00:02");
278 assert_eq!(pomodoro.break_time(), "00:02");
279 }
280
281 #[test]
282 fn test_pomodoro_reset() {
283 let mut pomodoro = Pomodoro::new((0, 3), (0, 2));
285 pomodoro.start_or_pause();
286 std::thread::sleep(std::time::Duration::from_secs(1));
287 pomodoro.reset();
289 assert_eq!(pomodoro.work_time(), "00:03");
291 assert_eq!(pomodoro.break_time(), "00:02");
292 assert_eq!(*pomodoro.state(), PomodoroState::Work);
293 assert!(!pomodoro.is_running());
294 }
295
296 #[test]
297 fn test_pomodoro_reset_from_break() {
298 let mut pomodoro = Pomodoro::new((0, 1), (0, 5));
300 pomodoro.start_or_pause();
301 std::thread::sleep(std::time::Duration::from_secs(2));
302 pomodoro.check_and_switch();
303 pomodoro.reset();
305 assert_eq!(pomodoro.work_time(), "00:01");
307 assert_eq!(pomodoro.break_time(), "00:05");
308 assert_eq!(*pomodoro.state(), PomodoroState::Work);
309 assert!(!pomodoro.is_running());
310 }
311
312 #[test]
313 fn test_pomodoro_check_and_switch() {
314 let mut pomodoro = Pomodoro::new((0, 2), (0, 2));
316 pomodoro.start_or_pause();
317 pomodoro.check_and_switch();
319 assert_eq!(*pomodoro.state(), PomodoroState::Work);
321 std::thread::sleep(std::time::Duration::from_secs(2));
323 pomodoro.check_and_switch();
324 assert_eq!(*pomodoro.state(), PomodoroState::Break);
326 std::thread::sleep(std::time::Duration::from_secs(2));
328 pomodoro.check_and_switch();
329 assert_eq!(*pomodoro.state(), PomodoroState::Work);
331 }
332
333 #[test]
334 fn test_get_min_sec_from_duration() {
335 let duration = time::Duration::from_secs(125);
336 let (minutes, seconds) = get_min_sec_from_duration(duration);
337 assert_eq!(minutes, 2);
338 assert_eq!(seconds, 5);
339 }
340}