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::{Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, Borders, Gauge, Paragraph},
10    Frame, Terminal,
11};
12use std::time::Duration as StdDuration;
13
14use crate::ui::{
15    formatter::Formatter,
16    widgets::{ColorScheme, Throbber},
17};
18
19pub struct InteractiveTimer {
20    start_time: Option<DateTime<Utc>>,
21    paused_at: Option<DateTime<Utc>>,
22    total_paused: Duration,
23    target_duration: i64, // in seconds
24    show_milestones: bool,
25    throbber: Throbber,
26}
27
28impl InteractiveTimer {
29    pub async fn new() -> Result<Self> {
30        Ok(Self {
31            start_time: None,
32            paused_at: None,
33            total_paused: Duration::zero(),
34            target_duration: 25 * 60, // Default 25 minutes (Pomodoro)
35            show_milestones: true,
36            throbber: Throbber::new(),
37        })
38    }
39
40    pub async fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
41        loop {
42            // Update timer state
43            self.update_timer_state().await?;
44
45            terminal.draw(|f| {
46                self.render_timer(f);
47            })?;
48
49            // Handle input
50            if event::poll(StdDuration::from_millis(100))? {
51                match event::read()? {
52                    Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
53                        KeyCode::Char('q') | KeyCode::Esc => break,
54                        KeyCode::Char(' ') => self.toggle_timer().await?,
55                        KeyCode::Char('r') => self.reset_timer().await?,
56                        KeyCode::Char('s') => self.set_target().await?,
57                        KeyCode::Char('m') => self.show_milestones = !self.show_milestones,
58                        _ => {}
59                    },
60                    _ => {}
61                }
62            }
63        }
64
65        Ok(())
66    }
67
68    fn render_timer(&self, f: &mut Frame) {
69        // Focused Mode Layout:
70        // Centered box with:
71        // 1. Project Context (Top)
72        // 2. Large Timer (Center)
73        // 3. Metadata & Progress (Bottom)
74
75        let area = f.size();
76        let vertical_center = Layout::default()
77            .direction(Direction::Vertical)
78            .constraints([
79                Constraint::Percentage(20),
80                Constraint::Percentage(60),
81                Constraint::Percentage(20),
82            ])
83            .split(area);
84
85        let horizontal_center = Layout::default()
86            .direction(Direction::Horizontal)
87            .constraints([
88                Constraint::Percentage(20),
89                Constraint::Percentage(60),
90                Constraint::Percentage(20),
91            ])
92            .split(vertical_center[1]);
93
94        let main_area = horizontal_center[1];
95
96        // Main Block with subtle border
97        let block = Block::default()
98            .borders(Borders::ALL)
99            .border_style(Style::default().fg(ColorScheme::GRAY_TEXT))
100            .style(Style::default().bg(ColorScheme::CLEAN_BG));
101
102        f.render_widget(block.clone(), main_area);
103
104        let inner_area = block.inner(main_area);
105        let chunks = Layout::default()
106            .direction(Direction::Vertical)
107            .constraints([
108                Constraint::Length(2), // Project Context
109                Constraint::Length(1), // Spacer
110                Constraint::Length(3), // Large Timer
111                Constraint::Length(1), // Spacer
112                Constraint::Length(1), // Progress Indicator
113                Constraint::Length(1), // Spacer
114                Constraint::Min(1),    // Metadata
115            ])
116            .margin(2)
117            .split(inner_area);
118
119        // 1. Project Context
120        self.render_project_context(f, chunks[0]);
121
122        // 2. Large Timer
123        self.render_large_timer(f, chunks[2]);
124
125        // 3. Progress Indicator
126        self.render_progress_indicator(f, chunks[4]);
127
128        // 4. Metadata
129        self.render_metadata(f, chunks[6]);
130    }
131
132    fn render_project_context(&self, f: &mut Frame, area: Rect) {
133        // Placeholder for project info - ideally fetched from state
134        let project_name = "Current Project";
135        let description = "Deep Work Session";
136
137        let text = vec![
138            Line::from(Span::styled(
139                project_name,
140                Style::default()
141                    .fg(ColorScheme::CLEAN_ACCENT)
142                    .add_modifier(Modifier::BOLD),
143            )),
144            Line::from(Span::styled(
145                description,
146                Style::default().fg(ColorScheme::GRAY_TEXT),
147            )),
148        ];
149
150        f.render_widget(Paragraph::new(text).alignment(Alignment::Center), area);
151    }
152
153    fn render_large_timer(&self, f: &mut Frame, area: Rect) {
154        let elapsed = self.get_elapsed_time();
155        let time_str = Formatter::format_duration_clock(elapsed);
156
157        // In a real terminal, "large text" is hard without ASCII art libraries.
158        // We'll use bold and bright colors for now.
159        let text = Paragraph::new(time_str)
160            .style(
161                Style::default()
162                    .fg(ColorScheme::WHITE_TEXT)
163                    .add_modifier(Modifier::BOLD),
164            ) // .add_modifier(Modifier::ITALIC) ?
165            .alignment(Alignment::Center);
166
167        f.render_widget(text, area);
168    }
169
170    fn render_progress_indicator(&self, f: &mut Frame, area: Rect) {
171        let elapsed = self.get_elapsed_time();
172        let progress = if self.target_duration > 0 {
173            ((elapsed as f64 / self.target_duration as f64) * 100.0).min(100.0)
174        } else {
175            0.0
176        };
177
178        let gauge = Gauge::default()
179            .gauge_style(Style::default().fg(ColorScheme::CLEAN_BLUE))
180            .percent(progress as u16)
181            .label(""); // Minimalist, no label inside
182
183        f.render_widget(gauge, area);
184    }
185
186    fn render_metadata(&self, f: &mut Frame, area: Rect) {
187        let start_time_str = if let Some(start) = self.start_time {
188            start.format("%H:%M").to_string()
189        } else {
190            "--:--".to_string()
191        };
192
193        let meta_text = vec![
194            Line::from(vec![
195                Span::raw("Started: "),
196                Span::styled(start_time_str, Style::default().fg(ColorScheme::WHITE_TEXT)),
197                Span::raw(" • "),
198                Span::raw("Target: "),
199                Span::styled(
200                    Formatter::format_duration(self.target_duration),
201                    Style::default().fg(ColorScheme::WHITE_TEXT),
202                ),
203            ]),
204            Line::from(""),
205            Line::from(Span::styled(
206                "[Space] Pause  [R] Reset  [Q] Quit",
207                Style::default().fg(ColorScheme::GRAY_TEXT),
208            )),
209        ];
210
211        f.render_widget(Paragraph::new(meta_text).alignment(Alignment::Center), area);
212    }
213
214    async fn update_timer_state(&mut self) -> Result<()> {
215        // This would sync with the actual session state from the daemon
216        // For now, we'll keep local state
217        if self.start_time.is_some() && self.paused_at.is_none() {
218            self.throbber.next();
219        }
220        Ok(())
221    }
222
223    async fn toggle_timer(&mut self) -> Result<()> {
224        if self.start_time.is_none() {
225            // Start timer
226            self.start_time = Some(Utc::now());
227            self.paused_at = None;
228        } else if self.paused_at.is_some() {
229            // Resume timer
230            if let Some(paused_at) = self.paused_at {
231                self.total_paused += Utc::now() - paused_at;
232            }
233            self.paused_at = None;
234        } else {
235            // Pause timer
236            self.paused_at = Some(Utc::now());
237        }
238        Ok(())
239    }
240
241    async fn reset_timer(&mut self) -> Result<()> {
242        self.start_time = None;
243        self.paused_at = None;
244        self.total_paused = chrono::Duration::zero();
245        Ok(())
246    }
247
248    async fn set_target(&mut self) -> Result<()> {
249        // In a full implementation, this would show an input dialog
250        // For now, cycle through common durations
251        self.target_duration = match self.target_duration {
252            1500 => 1800, // 25min -> 30min
253            1800 => 2700, // 30min -> 45min
254            2700 => 3600, // 45min -> 1hour
255            3600 => 5400, // 1hour -> 1.5hour
256            5400 => 7200, // 1.5hour -> 2hour
257            _ => 1500,    // Default back to 25min (Pomodoro)
258        };
259        Ok(())
260    }
261
262    fn get_elapsed_time(&self) -> i64 {
263        if let Some(start) = self.start_time {
264            let end_time = if let Some(paused) = self.paused_at {
265                paused
266            } else {
267                Utc::now()
268            };
269
270            (end_time - start - self.total_paused).num_seconds().max(0)
271        } else {
272            0
273        }
274    }
275}