tempo_cli/ui/
timer.rs

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