pomodoro_tui/
lib.rs

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    // This module tests the functionalities fo the Pomodoro timer.
183    // Some tests for the Timer struct are included to check more
184    // thoroughly the timer functionalities.
185    use super::*;
186
187    #[test]
188    fn test_timer_start_or_pause() {
189        // Given
190        let mut timer = Timer::new(1, 15);
191        // When
192        timer.start_or_pause();
193        // Then
194        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        // Testing pause
200        // Given
201        std::thread::sleep(std::time::Duration::from_secs(1));
202        // When
203        timer.start_or_pause();
204        // Then
205        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        // Given
213        let mut timer = Timer::new(1, 15);
214        timer.start_or_pause();
215        std::thread::sleep(std::time::Duration::from_secs(1));
216        // When
217        timer.reset();
218        // Then
219        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        // When
228        let mut timer = Timer::new(0, 3);
229        // Then
230        assert_eq!(timer.remaining().as_secs(), 3);
231        // When
232        timer.start_or_pause();
233        std::thread::sleep(std::time::Duration::from_secs(1));
234        // Then
235        assert!(timer.remaining().as_secs() > 0);
236        // When
237        std::thread::sleep(std::time::Duration::from_secs(3));
238        let remaining = timer.remaining();
239        // Then
240        assert_eq!(remaining.as_secs(), 0);
241    }
242
243    #[test]
244    fn test_timer_display() {
245        // When
246        let timer = Timer::new(1, 125);
247        // Then
248        assert_eq!(timer.to_string(), "03:05");
249    }
250
251    #[test]
252    fn test_pomodoro_initialization() {
253        // When
254        let pomodoro = Pomodoro::new((25, 0), (2, 5));
255        // Then
256        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        // Given
265        let mut pomodoro = Pomodoro::new((0, 3), (0, 2));
266        // When
267        pomodoro.start_or_pause();
268        // Then
269        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        // When paused
274        pomodoro.start_or_pause();
275        // Then
276        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        // Given
284        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        // When
288        pomodoro.reset();
289        // Then
290        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        // Given
299        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        // When
304        pomodoro.reset();
305        // Then
306        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        // Given
315        let mut pomodoro = Pomodoro::new((0, 2), (0, 2));
316        pomodoro.start_or_pause();
317        // When
318        pomodoro.check_and_switch();
319        // Then
320        assert_eq!(*pomodoro.state(), PomodoroState::Work);
321        // When expected to switch to break
322        std::thread::sleep(std::time::Duration::from_secs(2));
323        pomodoro.check_and_switch();
324        // Then
325        assert_eq!(*pomodoro.state(), PomodoroState::Break);
326        // When expected to switch to work
327        std::thread::sleep(std::time::Duration::from_secs(2));
328        pomodoro.check_and_switch();
329        // Then
330        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}