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::{Gauge, Paragraph, Wrap},
10 Frame, Terminal,
11};
12use std::time::Duration as StdDuration;
13
14use crate::{
15 ui::{
16 formatter::Formatter,
17 widgets::{ColorScheme, Throbber},
18 },
19 utils::ipc::IpcClient,
20};
21
22pub struct InteractiveTimer {
23 client: IpcClient,
24 start_time: Option<DateTime<Utc>>,
25 paused_at: Option<DateTime<Utc>>,
26 total_paused: Duration,
27 target_duration: i64, show_milestones: bool,
29 throbber: Throbber,
30}
31
32impl InteractiveTimer {
33 pub async fn new() -> Result<Self> {
34 let socket_path = crate::utils::ipc::get_socket_path()?;
35 let client = if socket_path.exists() {
36 match IpcClient::connect(&socket_path).await {
37 Ok(client) => client,
38 Err(_) => IpcClient::new()?,
39 }
40 } else {
41 IpcClient::new()?
42 };
43
44 Ok(Self {
45 client,
46 start_time: None,
47 paused_at: None,
48 total_paused: Duration::zero(),
49 target_duration: 25 * 60, show_milestones: true,
51 throbber: Throbber::new(),
52 })
53 }
54
55 pub async fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
56 loop {
57 self.update_timer_state().await?;
59
60 terminal.draw(|f| {
61 self.render_timer(f);
62 })?;
63
64 if event::poll(StdDuration::from_millis(100))? {
66 match event::read()? {
67 Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
68 KeyCode::Char('q') | KeyCode::Esc => break,
69 KeyCode::Char(' ') => self.toggle_timer().await?,
70 KeyCode::Char('r') => self.reset_timer().await?,
71 KeyCode::Char('s') => self.set_target().await?,
72 KeyCode::Char('m') => self.show_milestones = !self.show_milestones,
73 _ => {}
74 },
75 _ => {}
76 }
77 }
78 }
79
80 Ok(())
81 }
82
83 fn render_timer(&self, f: &mut Frame) {
84 let chunks = Layout::default()
85 .direction(Direction::Vertical)
86 .constraints([
87 Constraint::Length(3), Constraint::Length(8), Constraint::Length(6), Constraint::Length(6), Constraint::Min(0), ])
93 .split(f.size());
94
95 let title = Paragraph::new("Interactive Timer")
98 .style(
99 Style::default()
100 .fg(ColorScheme::CLEAN_ACCENT)
101 .add_modifier(Modifier::BOLD),
102 )
103 .alignment(Alignment::Center)
104 .block(ColorScheme::clean_block());
105 f.render_widget(title, chunks[0]);
106
107 self.render_timer_display(f, chunks[1]);
109
110 self.render_progress_bar(f, chunks[2]);
112
113 if self.show_milestones {
115 self.render_milestones(f, chunks[3]);
116 }
117
118 self.render_controls(f, chunks[4]);
120 }
121
122 fn render_timer_display(&self, f: &mut Frame, area: Rect) {
123 let elapsed = self.get_elapsed_time();
124 let is_running = self.start_time.is_some() && self.paused_at.is_none();
125
126 let time_display = Formatter::format_duration(elapsed);
127 let status = if is_running {
128 "RUNNING"
129 } else if self.start_time.is_some() {
130 "PAUSED"
131 } else {
132 "STOPPED"
133 };
134 let status_color = if is_running {
135 Color::Green
136 } else if self.start_time.is_some() {
137 Color::Yellow
138 } else {
139 Color::Red
140 };
141
142 let timer_text = vec![
143 Line::from(Span::styled(
144 time_display,
145 Style::default()
146 .fg(ColorScheme::CLEAN_BLUE)
147 .add_modifier(Modifier::BOLD),
148 )),
149 Line::from(Span::raw("")),
150 Line::from(vec![
151 Span::raw("Status: "),
152 Span::styled(
153 status,
154 Style::default()
155 .fg(status_color)
156 .add_modifier(Modifier::BOLD),
157 ),
158 if is_running {
159 Span::styled(
160 format!(" {}", self.throbber.current()),
161 Style::default().fg(ColorScheme::CLEAN_ACCENT),
162 )
163 } else {
164 Span::raw("")
165 },
166 ]),
167 Line::from(vec![
168 Span::raw("Target: "),
169 Span::styled(
170 Formatter::format_duration(self.target_duration),
171 Style::default().fg(ColorScheme::WHITE_TEXT),
172 ),
173 ]),
174 ];
175
176 let timer_block = ColorScheme::clean_block()
177 .title("Timer")
178 .style(Style::default().fg(ColorScheme::WHITE_TEXT));
179
180 let paragraph = Paragraph::new(timer_text)
181 .block(timer_block)
182 .alignment(Alignment::Center)
183 .wrap(Wrap { trim: true });
184 f.render_widget(paragraph, area);
185 }
186
187 fn render_progress_bar(&self, f: &mut Frame, area: Rect) {
188 let elapsed = self.get_elapsed_time();
189 let progress = if self.target_duration > 0 {
190 ((elapsed as f64 / self.target_duration as f64) * 100.0).min(100.0)
191 } else {
192 0.0
193 };
194
195 let progress_color = if progress >= 100.0 {
196 ColorScheme::CLEAN_GREEN
197 } else if progress >= 75.0 {
198 Color::Yellow
199 } else {
200 ColorScheme::CLEAN_ACCENT
201 };
202
203 let progress_bar = Gauge::default()
204 .block(
205 ColorScheme::clean_block()
206 .title("Progress to Target")
207 .style(Style::default().fg(ColorScheme::WHITE_TEXT)),
208 )
209 .gauge_style(Style::default().fg(progress_color))
210 .percent(progress as u16)
211 .label(format!(
212 "{:.1}% ({}/{})",
213 progress,
214 Formatter::format_duration(elapsed),
215 Formatter::format_duration(self.target_duration)
216 ));
217
218 f.render_widget(progress_bar, area);
219 }
220
221 fn render_milestones(&self, f: &mut Frame, area: Rect) {
222 let elapsed = self.get_elapsed_time();
223 let milestones = vec![
224 (5 * 60, "5 min warm-up"),
225 (15 * 60, "15 min focus"),
226 (25 * 60, "Pomodoro complete"),
227 (45 * 60, "45 min deep work"),
228 (60 * 60, "1 hour marathon"),
229 ];
230
231 let mut milestone_lines = vec![];
232 for (duration, name) in milestones {
233 let achieved = elapsed >= duration;
234 let icon = if achieved { "[x]" } else { "[ ]" };
235 let style = if achieved {
236 Style::default().fg(ColorScheme::CLEAN_GREEN)
237 } else {
238 Style::default().fg(ColorScheme::GRAY_TEXT)
239 };
240
241 milestone_lines.push(Line::from(vec![Span::styled(
242 format!("{} {}", icon, name),
243 style,
244 )]));
245 }
246
247 let milestones_block = ColorScheme::clean_block()
248 .title("Milestones")
249 .style(Style::default().fg(ColorScheme::WHITE_TEXT));
250
251 let paragraph = Paragraph::new(milestone_lines)
252 .block(milestones_block)
253 .wrap(Wrap { trim: true });
254 f.render_widget(paragraph, area);
255 }
256
257 fn render_controls(&self, f: &mut Frame, area: Rect) {
258 let controls_text = vec![
259 Line::from(Span::styled(
260 "Controls:",
261 Style::default()
262 .fg(ColorScheme::CLEAN_ACCENT)
263 .add_modifier(Modifier::BOLD),
264 )),
265 Line::from(Span::raw("Space - Start/Pause timer")),
266 Line::from(Span::raw("R - Reset timer")),
267 Line::from(Span::raw("S - Set target duration")),
268 Line::from(Span::raw("M - Toggle milestones")),
269 Line::from(Span::raw("Q/Esc - Quit")),
270 ];
271
272 let controls_block = ColorScheme::clean_block()
273 .title("Controls")
274 .style(Style::default().fg(ColorScheme::WHITE_TEXT));
275
276 let paragraph = Paragraph::new(controls_text)
277 .block(controls_block)
278 .wrap(Wrap { trim: true });
279 f.render_widget(paragraph, area);
280 }
281
282 async fn update_timer_state(&mut self) -> Result<()> {
283 if self.start_time.is_some() && self.paused_at.is_none() {
286 self.throbber.next();
287 }
288 Ok(())
289 }
290
291 async fn toggle_timer(&mut self) -> Result<()> {
292 if self.start_time.is_none() {
293 self.start_time = Some(Utc::now());
295 self.paused_at = None;
296 } else if self.paused_at.is_some() {
297 if let Some(paused_at) = self.paused_at {
299 self.total_paused = self.total_paused + (Utc::now() - paused_at);
300 }
301 self.paused_at = None;
302 } else {
303 self.paused_at = Some(Utc::now());
305 }
306 Ok(())
307 }
308
309 async fn reset_timer(&mut self) -> Result<()> {
310 self.start_time = None;
311 self.paused_at = None;
312 self.total_paused = chrono::Duration::zero();
313 Ok(())
314 }
315
316 async fn set_target(&mut self) -> Result<()> {
317 self.target_duration = match self.target_duration {
320 1500 => 1800, 1800 => 2700, 2700 => 3600, 3600 => 5400, 5400 => 7200, _ => 1500, };
327 Ok(())
328 }
329
330 fn get_elapsed_time(&self) -> i64 {
331 if let Some(start) = self.start_time {
332 let end_time = if let Some(paused) = self.paused_at {
333 paused
334 } else {
335 Utc::now()
336 };
337
338 (end_time - start - self.total_paused).num_seconds().max(0)
339 } else {
340 0
341 }
342 }
343}