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, 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, show_milestones: true,
44 })
45 }
46
47 pub async fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
48 loop {
49 self.update_timer_state().await?;
51
52 terminal.draw(|f| {
53 self.render_timer(f);
54 })?;
55
56 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), Constraint::Length(8), Constraint::Length(6), Constraint::Length(6), Constraint::Min(0), ])
85 .split(f.size());
86
87 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 self.render_timer_display(f, chunks[1]);
100
101 self.render_progress_bar(f, chunks[2]);
103
104 if self.show_milestones {
106 self.render_milestones(f, chunks[3]);
107 }
108
109 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 Ok(())
273 }
274
275 async fn toggle_timer(&mut self) -> Result<()> {
276 if self.start_time.is_none() {
277 self.start_time = Some(Utc::now());
279 self.paused_at = None;
280 } else if self.paused_at.is_some() {
281 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 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 self.target_duration = match self.target_duration {
304 1500 => 1800, 1800 => 2700, 2700 => 3600, 3600 => 5400, 5400 => 7200, _ => 1500, };
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}