tempo_cli/ui/
timer.rs

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