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