tempo_cli/ui/
timer.rs

1use anyhow::Result;
2use chrono::{DateTime, Duration, Utc};
3use crossterm::event::{self, Event, KeyCode, KeyEventKind};
4use ratatui::{
5    backend::Backend,
6    layout::{Alignment, Constraint, Direction, Layout, Rect},
7    style::{Color, Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, Gauge, Paragraph, Wrap},
10    Frame, Terminal,
11};
12use std::time::Duration as StdDuration;
13
14use crate::{ui::formatter::Formatter, utils::ipc::IpcClient};
15
16pub struct InteractiveTimer {
17    client: IpcClient,
18    start_time: Option<DateTime<Utc>>,
19    paused_at: Option<DateTime<Utc>>,
20    total_paused: Duration,
21    target_duration: i64, // in seconds
22    show_milestones: bool,
23}
24
25impl InteractiveTimer {
26    pub async fn new() -> Result<Self> {
27        let socket_path = crate::utils::ipc::get_socket_path()?;
28        let client = if socket_path.exists() {
29            match IpcClient::connect(&socket_path).await {
30                Ok(client) => client,
31                Err(_) => IpcClient::new()?,
32            }
33        } else {
34            IpcClient::new()?
35        };
36
37        Ok(Self {
38            client,
39            start_time: None,
40            paused_at: None,
41            total_paused: Duration::zero(),
42            target_duration: 25 * 60, // Default 25 minutes (Pomodoro)
43            show_milestones: true,
44        })
45    }
46
47    pub async fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
48        loop {
49            // Update timer state
50            self.update_timer_state().await?;
51
52            terminal.draw(|f| {
53                self.render_timer(f);
54            })?;
55
56            // Handle input
57            if event::poll(StdDuration::from_millis(100))? {
58                match event::read()? {
59                    Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
60                        KeyCode::Char('q') | KeyCode::Esc => break,
61                        KeyCode::Char(' ') => self.toggle_timer().await?,
62                        KeyCode::Char('r') => self.reset_timer().await?,
63                        KeyCode::Char('s') => self.set_target().await?,
64                        KeyCode::Char('m') => self.show_milestones = !self.show_milestones,
65                        _ => {}
66                    },
67                    _ => {}
68                }
69            }
70        }
71
72        Ok(())
73    }
74
75    fn render_timer(&self, f: &mut Frame) {
76        let chunks = Layout::default()
77            .direction(Direction::Vertical)
78            .constraints([
79                Constraint::Length(3), // Title
80                Constraint::Length(8), // Timer display
81                Constraint::Length(6), // Progress bar
82                Constraint::Length(6), // Milestones
83                Constraint::Min(0),    // Controls
84            ])
85            .split(f.size());
86
87        // Title
88        let title = Paragraph::new("Interactive Timer")
89            .style(
90                Style::default()
91                    .fg(Color::Cyan)
92                    .add_modifier(Modifier::BOLD),
93            )
94            .alignment(Alignment::Center)
95            .block(Block::default().borders(Borders::ALL));
96        f.render_widget(title, chunks[0]);
97
98        // Timer display
99        self.render_timer_display(f, chunks[1]);
100
101        // Progress bar
102        self.render_progress_bar(f, chunks[2]);
103
104        // Milestones
105        if self.show_milestones {
106            self.render_milestones(f, chunks[3]);
107        }
108
109        // Controls
110        self.render_controls(f, chunks[4]);
111    }
112
113    fn render_timer_display(&self, f: &mut Frame, area: Rect) {
114        let elapsed = self.get_elapsed_time();
115        let is_running = self.start_time.is_some() && self.paused_at.is_none();
116
117        let time_display = Formatter::format_duration(elapsed);
118        let status = if is_running {
119            "RUNNING"
120        } else if self.start_time.is_some() {
121            "PAUSED"
122        } else {
123            "STOPPED"
124        };
125        let status_color = if is_running {
126            Color::Green
127        } else if self.start_time.is_some() {
128            Color::Yellow
129        } else {
130            Color::Red
131        };
132
133        let timer_text = vec![
134            Line::from(Span::styled(
135                time_display,
136                Style::default()
137                    .fg(Color::Cyan)
138                    .add_modifier(Modifier::BOLD),
139            )),
140            Line::from(Span::raw("")),
141            Line::from(vec![
142                Span::raw("Status: "),
143                Span::styled(
144                    status,
145                    Style::default()
146                        .fg(status_color)
147                        .add_modifier(Modifier::BOLD),
148                ),
149            ]),
150            Line::from(vec![
151                Span::raw("Target: "),
152                Span::styled(
153                    Formatter::format_duration(self.target_duration),
154                    Style::default().fg(Color::White),
155                ),
156            ]),
157        ];
158
159        let timer_block = Block::default()
160            .borders(Borders::ALL)
161            .title("Timer")
162            .style(Style::default().fg(Color::White));
163
164        let paragraph = Paragraph::new(timer_text)
165            .block(timer_block)
166            .alignment(Alignment::Center)
167            .wrap(Wrap { trim: true });
168        f.render_widget(paragraph, area);
169    }
170
171    fn render_progress_bar(&self, f: &mut Frame, area: Rect) {
172        let elapsed = self.get_elapsed_time();
173        let progress = if self.target_duration > 0 {
174            ((elapsed as f64 / self.target_duration as f64) * 100.0).min(100.0)
175        } else {
176            0.0
177        };
178
179        let progress_color = if progress >= 100.0 {
180            Color::Green
181        } else if progress >= 75.0 {
182            Color::Yellow
183        } else {
184            Color::Cyan
185        };
186
187        let progress_bar = Gauge::default()
188            .block(
189                Block::default()
190                    .borders(Borders::ALL)
191                    .title("Progress to Target")
192                    .style(Style::default().fg(Color::White)),
193            )
194            .gauge_style(Style::default().fg(progress_color))
195            .percent(progress as u16)
196            .label(format!(
197                "{:.1}% ({}/{})",
198                progress,
199                Formatter::format_duration(elapsed),
200                Formatter::format_duration(self.target_duration)
201            ));
202
203        f.render_widget(progress_bar, area);
204    }
205
206    fn render_milestones(&self, f: &mut Frame, area: Rect) {
207        let elapsed = self.get_elapsed_time();
208        let milestones = vec![
209            (5 * 60, "5 min warm-up"),
210            (15 * 60, "15 min focus"),
211            (25 * 60, "Pomodoro complete"),
212            (45 * 60, "45 min deep work"),
213            (60 * 60, "1 hour marathon"),
214        ];
215
216        let mut milestone_lines = vec![];
217        for (duration, name) in milestones {
218            let achieved = elapsed >= duration;
219            let icon = if achieved { "[x]" } else { "[ ]" };
220            let style = if achieved {
221                Style::default().fg(Color::Green)
222            } else {
223                Style::default().fg(Color::Gray)
224            };
225
226            milestone_lines.push(Line::from(vec![Span::styled(
227                format!("{} {}", icon, name),
228                style,
229            )]));
230        }
231
232        let milestones_block = Block::default()
233            .borders(Borders::ALL)
234            .title("Milestones")
235            .style(Style::default().fg(Color::White));
236
237        let paragraph = Paragraph::new(milestone_lines)
238            .block(milestones_block)
239            .wrap(Wrap { trim: true });
240        f.render_widget(paragraph, area);
241    }
242
243    fn render_controls(&self, f: &mut Frame, area: Rect) {
244        let controls_text = vec![
245            Line::from(Span::styled(
246                "Controls:",
247                Style::default()
248                    .fg(Color::Cyan)
249                    .add_modifier(Modifier::BOLD),
250            )),
251            Line::from(Span::raw("Space - Start/Pause timer")),
252            Line::from(Span::raw("R - Reset timer")),
253            Line::from(Span::raw("S - Set target duration")),
254            Line::from(Span::raw("M - Toggle milestones")),
255            Line::from(Span::raw("Q/Esc - Quit")),
256        ];
257
258        let controls_block = Block::default()
259            .borders(Borders::ALL)
260            .title("Controls")
261            .style(Style::default().fg(Color::White));
262
263        let paragraph = Paragraph::new(controls_text)
264            .block(controls_block)
265            .wrap(Wrap { trim: true });
266        f.render_widget(paragraph, area);
267    }
268
269    async fn update_timer_state(&mut self) -> Result<()> {
270        // This would sync with the actual session state from the daemon
271        // For now, we'll keep local state
272        Ok(())
273    }
274
275    async fn toggle_timer(&mut self) -> Result<()> {
276        if self.start_time.is_none() {
277            // Start timer
278            self.start_time = Some(Utc::now());
279            self.paused_at = None;
280        } else if self.paused_at.is_some() {
281            // Resume timer
282            if let Some(paused_at) = self.paused_at {
283                self.total_paused = self.total_paused + (Utc::now() - paused_at);
284            }
285            self.paused_at = None;
286        } else {
287            // Pause timer
288            self.paused_at = Some(Utc::now());
289        }
290        Ok(())
291    }
292
293    async fn reset_timer(&mut self) -> Result<()> {
294        self.start_time = None;
295        self.paused_at = None;
296        self.total_paused = chrono::Duration::zero();
297        Ok(())
298    }
299
300    async fn set_target(&mut self) -> Result<()> {
301        // In a full implementation, this would show an input dialog
302        // For now, cycle through common durations
303        self.target_duration = match self.target_duration {
304            1500 => 1800, // 25min -> 30min
305            1800 => 2700, // 30min -> 45min
306            2700 => 3600, // 45min -> 1hour
307            3600 => 5400, // 1hour -> 1.5hour
308            5400 => 7200, // 1.5hour -> 2hour
309            _ => 1500,    // Default back to 25min (Pomodoro)
310        };
311        Ok(())
312    }
313
314    fn get_elapsed_time(&self) -> i64 {
315        if let Some(start) = self.start_time {
316            let end_time = if let Some(paused) = self.paused_at {
317                paused
318            } else {
319                Utc::now()
320            };
321
322            (end_time - start - self.total_paused).num_seconds().max(0)
323        } else {
324            0
325        }
326    }
327}