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