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