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, 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, 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 self.update_timer_state().await?;
44
45 terminal.draw(|f| {
46 self.render_timer(f);
47 })?;
48
49 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 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 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), Constraint::Length(1), Constraint::Length(3), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(1), ])
116 .margin(2)
117 .split(inner_area);
118
119 self.render_project_context(f, chunks[0]);
121
122 self.render_large_timer(f, chunks[2]);
124
125 self.render_progress_indicator(f, chunks[4]);
127
128 self.render_metadata(f, chunks[6]);
130 }
131
132 fn render_project_context(&self, f: &mut Frame, area: Rect) {
133 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 let text = Paragraph::new(time_str)
160 .style(
161 Style::default()
162 .fg(ColorScheme::WHITE_TEXT)
163 .add_modifier(Modifier::BOLD),
164 ) .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(""); 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 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 self.start_time = Some(Utc::now());
227 self.paused_at = None;
228 } else if self.paused_at.is_some() {
229 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 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 self.target_duration = match self.target_duration {
252 1500 => 1800, 1800 => 2700, 2700 => 3600, 3600 => 5400, 5400 => 7200, _ => 1500, };
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}