1use chrono::{DateTime, Local};
2use ratatui::{
3 buffer::Buffer,
4 layout::{Constraint, Direction, Layout, Rect},
5 style::{Color, Style},
6 text::{Line, Span},
7 widgets::{Block, BorderType, Borders, Paragraph, Widget},
8};
9use std::time::{Duration, Instant};
10
11use crate::ui::formatter::Formatter;
12
13pub struct StatusWidget;
14pub struct ProgressWidget;
15pub struct SummaryWidget;
16
17pub struct ColorScheme;
19
20impl ColorScheme {
21 pub const NEON_CYAN: Color = Color::Rgb(0, 255, 255);
23 pub const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
24 pub const NEON_PINK: Color = Color::Rgb(255, 16, 240);
25 pub const NEON_PURPLE: Color = Color::Rgb(188, 19, 254);
26 pub const NEON_YELLOW: Color = Color::Rgb(255, 240, 31);
27 pub const DARK_BG: Color = Color::Rgb(10, 10, 15);
28 pub const GRAY_TEXT: Color = Color::Rgb(160, 160, 160);
29 pub const WHITE_TEXT: Color = Color::Rgb(240, 240, 240);
30
31 pub const CLEAN_BG: Color = Color::Rgb(20, 20, 20);
33 pub const CLEAN_ACCENT: Color = Color::Rgb(217, 119, 87); pub const CLEAN_BLUE: Color = Color::Rgb(100, 150, 255);
35 pub const CLEAN_GREEN: Color = Color::Rgb(100, 200, 100);
36 pub const CLEAN_GOLD: Color = Color::Rgb(217, 179, 87);
37 pub const CLEAN_MAGENTA: Color = Color::Rgb(188, 19, 254);
38
39 pub fn get_context_color(context: &str) -> Color {
40 match context {
41 "terminal" => Self::NEON_CYAN,
42 "ide" => Self::NEON_PURPLE,
43 "linked" => Self::NEON_YELLOW,
44 "manual" => Color::Blue,
45 _ => Self::WHITE_TEXT,
46 }
47 }
48
49 pub fn active_status() -> Color {
50 Self::NEON_GREEN
51 }
52 pub fn project_name() -> Color {
53 Self::NEON_YELLOW
54 }
55 pub fn duration() -> Color {
56 Self::NEON_CYAN
57 }
58 pub fn path() -> Color {
59 Self::GRAY_TEXT
60 }
61 pub fn timestamp() -> Color {
62 Self::GRAY_TEXT
63 }
64 pub fn border() -> Color {
65 Self::NEON_PURPLE
66 }
67 pub fn title() -> Color {
68 Self::NEON_PINK
69 }
70
71 pub fn base_block() -> Block<'static> {
72 Block::default()
73 .borders(Borders::ALL)
74 .border_style(Style::default().fg(Self::border()))
75 .border_type(BorderType::Rounded)
76 .style(Style::default().bg(Self::DARK_BG))
77 }
78
79 pub fn clean_block() -> Block<'static> {
80 Block::default()
81 .borders(Borders::NONE)
82 .style(Style::default().bg(Self::DARK_BG))
83 }
84}
85
86pub struct Spinner {
87 frames: Vec<&'static str>,
88 current_frame: usize,
89 last_update: Instant,
90 interval: Duration,
91}
92
93impl Spinner {
94 pub fn new() -> Self {
95 Self {
96 frames: vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
97 current_frame: 0,
98 last_update: Instant::now(),
99 interval: Duration::from_millis(100),
100 }
101 }
102
103 pub fn with_speed(mut self, interval: Duration) -> Self {
104 self.interval = interval;
105 self
106 }
107
108 pub fn next(&mut self) {
109 if self.last_update.elapsed() >= self.interval {
110 self.current_frame = (self.current_frame + 1) % self.frames.len();
111 self.last_update = Instant::now();
112 }
113 }
114
115 pub fn current(&self) -> &str {
116 self.frames[self.current_frame]
117 }
118}
119
120pub struct Throbber {
121 frames: Vec<&'static str>,
122 current: usize,
123}
124
125impl Throbber {
126 pub fn new() -> Self {
127 Self {
128 frames: vec![
130 "[= ]", "[ = ]", "[ = ]", "[ = ]", "[ =]", "[ = ]", "[ = ]",
131 "[ = ]",
132 ],
133 current: 0,
134 }
135 }
136
137 pub fn next(&mut self) -> &'static str {
138 let frame = self.frames[self.current];
139 self.current = (self.current + 1) % self.frames.len();
140 frame
141 }
142
143 pub fn current(&self) -> &'static str {
144 self.frames[self.current]
145 }
146}
147
148impl StatusWidget {
149 pub fn render_status_text(
150 project_name: &str,
151 duration: i64,
152 start_time: &str,
153 context: &str,
154 ) -> String {
155 format!(
156 "ACTIVE | {} | Time: {} | Started: {} | Context: {}",
157 project_name,
158 Formatter::format_duration(duration),
159 start_time,
160 context
161 )
162 }
163
164 pub fn render_idle_text() -> String {
165 "IDLE | No active time tracking session | Use 'tempo session start' to begin tracking"
166 .to_string()
167 }
168}
169
170impl ProgressWidget {
171 pub fn calculate_daily_progress(completed_seconds: i64, target_hours: f64) -> u16 {
172 let total_hours = completed_seconds as f64 / 3600.0;
173 let progress = (total_hours / target_hours * 100.0).min(100.0) as u16;
174 progress
175 }
176
177 pub fn format_progress_label(completed_seconds: i64, target_hours: f64) -> String {
178 let total_hours = completed_seconds as f64 / 3600.0;
179 let progress = (total_hours / target_hours * 100.0).min(100.0) as u16;
180 format!(
181 "Daily Progress ({:.1}h / {:.1}h) - {}%",
182 total_hours, target_hours, progress
183 )
184 }
185}
186
187impl SummaryWidget {
188 pub fn format_project_summary(
189 project_name: &str,
190 total_time: i64,
191 session_count: usize,
192 active_count: usize,
193 ) -> String {
194 format!(
195 "Project: {} | Total Time: {} | Sessions: {} total, {} active",
196 project_name,
197 Formatter::format_duration(total_time),
198 session_count,
199 active_count
200 )
201 }
202 pub fn format_session_line(
203 start_time: &DateTime<Local>,
204 duration: i64,
205 project_name: &str,
206 status: &str,
207 ) -> String {
208 format!(
209 "{} - {} ({}) [{}]",
210 start_time.format("%H:%M"),
211 project_name,
212 Formatter::format_duration(duration),
213 status
214 )
215 }
216}
217
218#[allow(dead_code)]
219pub enum StatusIndicator {
220 Online,
221 Offline,
222 Syncing,
223 Error,
224 Custom(String, Color),
225}
226
227impl StatusIndicator {
228 #[allow(dead_code)]
229 pub fn render(&self) -> Span {
230 match self {
231 StatusIndicator::Online => Span::styled("●", Style::default().fg(Color::Green)),
232 StatusIndicator::Offline => Span::styled("○", Style::default().fg(Color::Gray)),
233 StatusIndicator::Syncing => Span::styled("⟳", Style::default().fg(Color::Blue)),
234 StatusIndicator::Error => Span::styled("⚠", Style::default().fg(Color::Red)),
235 StatusIndicator::Custom(symbol, color) => {
236 Span::styled(symbol.clone(), Style::default().fg(*color))
237 }
238 }
239 }
240}
241
242#[allow(dead_code)]
243pub struct GradientProgressBar;
244
245impl GradientProgressBar {
246 #[allow(dead_code)]
247 pub fn get_color(progress: u16) -> Color {
248 match progress {
249 0..=25 => Color::Red,
250 26..=50 => Color::Yellow,
251 51..=75 => Color::Green,
252 _ => Color::Cyan,
253 }
254 }
255
256 #[allow(dead_code)]
257 pub fn render(progress: u16, width: u16) -> Line<'static> {
258 let filled_width = (width as f64 * (progress as f64 / 100.0)).round() as u16;
259 let empty_width = width.saturating_sub(filled_width);
260
261 let color = Self::get_color(progress);
262
263 let filled = Span::styled(
264 "█".repeat(filled_width as usize),
265 Style::default().fg(color),
266 );
267 let empty = Span::styled(
268 "░".repeat(empty_width as usize),
269 Style::default().fg(Color::DarkGray),
270 );
271
272 Line::from(vec![filled, empty])
273 }
274}
275
276#[allow(dead_code)]
277pub struct SessionStatsWidget;
278
279impl SessionStatsWidget {
280 #[allow(dead_code)]
281 pub fn render(
282 daily_stats: &(i64, i64, i64), weekly_total: i64,
284 area: Rect,
285 buf: &mut Buffer,
286 ) {
287 let (daily_sessions, daily_total, _) = daily_stats;
288
289 let block = Block::default()
290 .borders(Borders::ALL)
291 .border_style(Style::default().fg(ColorScheme::border()))
292 .title(Span::styled(
293 " Session Stats ",
294 Style::default().fg(ColorScheme::title()),
295 ));
296
297 let inner_area = block.inner(area);
298 block.render(area, buf);
299
300 let layout = Layout::default()
301 .direction(Direction::Vertical)
302 .constraints([
303 Constraint::Length(1), Constraint::Length(1), Constraint::Min(0),
306 ])
307 .split(inner_area);
308
309 let daily_text = Line::from(vec![
311 Span::styled("Today: ", Style::default().fg(ColorScheme::GRAY_TEXT)),
312 Span::styled(
313 format!("{} sessions, ", daily_sessions),
314 Style::default().fg(ColorScheme::WHITE_TEXT),
315 ),
316 Span::styled(
317 Formatter::format_duration(*daily_total),
318 Style::default().fg(ColorScheme::NEON_CYAN),
319 ),
320 ]);
321 Paragraph::new(daily_text).render(layout[0], buf);
322
323 let weekly_text = Line::from(vec![
325 Span::styled("This Week: ", Style::default().fg(ColorScheme::GRAY_TEXT)),
326 Span::styled(
327 Formatter::format_duration(weekly_total),
328 Style::default().fg(ColorScheme::NEON_PURPLE),
329 ),
330 ]);
331 Paragraph::new(weekly_text).render(layout[1], buf);
332 }
333}