tempo_cli/ui/
widgets.rs

1use chrono::{DateTime, Local};
2use ratatui::{
3    style::{Color, Style},
4    widgets::{Block, BorderType, Borders},
5};
6
7use crate::ui::formatter::Formatter;
8
9pub struct StatusWidget;
10pub struct ProgressWidget;
11pub struct SummaryWidget;
12
13// Centralized color scheme with a Neon/Cyberpunk aesthetic
14pub struct ColorScheme;
15
16impl ColorScheme {
17    // Vibrant Colors
18    pub const NEON_CYAN: Color = Color::Rgb(0, 255, 255);
19    pub const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
20    pub const NEON_PINK: Color = Color::Rgb(255, 16, 240);
21    pub const NEON_PURPLE: Color = Color::Rgb(188, 19, 254);
22    pub const NEON_YELLOW: Color = Color::Rgb(255, 240, 31);
23    pub const DARK_BG: Color = Color::Rgb(10, 10, 15);
24    pub const GRAY_TEXT: Color = Color::Rgb(160, 160, 160);
25    pub const WHITE_TEXT: Color = Color::Rgb(240, 240, 240);
26
27    // Professional clean palette
28    pub const CLEAN_BG: Color = Color::Rgb(20, 20, 20);
29    pub const CLEAN_ACCENT: Color = Color::Rgb(217, 119, 87); // Terracotta-ish
30    pub const CLEAN_BLUE: Color = Color::Rgb(100, 150, 255);
31    pub const CLEAN_GREEN: Color = Color::Rgb(100, 200, 100);
32    pub const CLEAN_GOLD: Color = Color::Rgb(217, 179, 87);
33    pub const CLEAN_MAGENTA: Color = Color::Rgb(188, 19, 254);
34
35    pub fn get_context_color(context: &str) -> Color {
36        match context {
37            "terminal" => Self::NEON_CYAN,
38            "ide" => Self::NEON_PURPLE,
39            "linked" => Self::NEON_YELLOW,
40            "manual" => Color::Blue,
41            _ => Self::WHITE_TEXT,
42        }
43    }
44
45    pub fn active_status() -> Color {
46        Self::NEON_GREEN
47    }
48    pub fn project_name() -> Color {
49        Self::NEON_YELLOW
50    }
51    pub fn duration() -> Color {
52        Self::NEON_CYAN
53    }
54    pub fn path() -> Color {
55        Self::GRAY_TEXT
56    }
57    pub fn timestamp() -> Color {
58        Self::GRAY_TEXT
59    }
60    pub fn border() -> Color {
61        Self::NEON_PURPLE
62    }
63    pub fn title() -> Color {
64        Self::NEON_PINK
65    }
66
67    pub fn base_block() -> Block<'static> {
68        Block::default()
69            .borders(Borders::ALL)
70            .border_style(Style::default().fg(Self::border()))
71            .border_type(BorderType::Rounded)
72            .style(Style::default().bg(Self::DARK_BG))
73    }
74
75    pub fn clean_block() -> Block<'static> {
76        Block::default()
77            .borders(Borders::NONE)
78            .style(Style::default().bg(Self::DARK_BG))
79    }
80}
81
82pub struct Spinner {
83    frames: Vec<&'static str>,
84    current: usize,
85}
86
87impl Spinner {
88    pub fn new() -> Self {
89        Self {
90            // Using a simple line spinner that is ASCII safe but looks good
91            frames: vec!["|", "/", "-", "\\"],
92            current: 0,
93        }
94    }
95
96    pub fn next(&mut self) -> &'static str {
97        let frame = self.frames[self.current];
98        self.current = (self.current + 1) % self.frames.len();
99        frame
100    }
101
102    pub fn current(&self) -> &'static str {
103        self.frames[self.current]
104    }
105}
106
107pub struct Throbber {
108    frames: Vec<&'static str>,
109    current: usize,
110}
111
112impl Throbber {
113    pub fn new() -> Self {
114        Self {
115            // A horizontal throbber using ASCII
116            frames: vec![
117                "[=    ]", "[ =   ]", "[  =  ]", "[   = ]", "[    =]", "[   = ]", "[  =  ]",
118                "[ =   ]",
119            ],
120            current: 0,
121        }
122    }
123
124    pub fn next(&mut self) -> &'static str {
125        let frame = self.frames[self.current];
126        self.current = (self.current + 1) % self.frames.len();
127        frame
128    }
129
130    pub fn current(&self) -> &'static str {
131        self.frames[self.current]
132    }
133}
134
135impl StatusWidget {
136    pub fn render_status_text(
137        project_name: &str,
138        duration: i64,
139        start_time: &str,
140        context: &str,
141    ) -> String {
142        format!(
143            "ACTIVE | {} | Time: {} | Started: {} | Context: {}",
144            project_name,
145            Formatter::format_duration(duration),
146            start_time,
147            context
148        )
149    }
150
151    pub fn render_idle_text() -> String {
152        "IDLE | No active time tracking session | Use 'tempo session start' to begin tracking"
153            .to_string()
154    }
155}
156
157impl ProgressWidget {
158    pub fn calculate_daily_progress(completed_seconds: i64, target_hours: f64) -> u16 {
159        let total_hours = completed_seconds as f64 / 3600.0;
160        let progress = (total_hours / target_hours * 100.0).min(100.0) as u16;
161        progress
162    }
163
164    pub fn format_progress_label(completed_seconds: i64, target_hours: f64) -> String {
165        let total_hours = completed_seconds as f64 / 3600.0;
166        let progress = (total_hours / target_hours * 100.0).min(100.0) as u16;
167        format!(
168            "Daily Progress ({:.1}h / {:.1}h) - {}%",
169            total_hours, target_hours, progress
170        )
171    }
172}
173
174impl SummaryWidget {
175    pub fn format_project_summary(
176        project_name: &str,
177        total_time: i64,
178        session_count: usize,
179        active_count: usize,
180    ) -> String {
181        format!(
182            "Project: {} | Total Time: {} | Sessions: {} total, {} active",
183            project_name,
184            Formatter::format_duration(total_time),
185            session_count,
186            active_count
187        )
188    }
189
190    pub fn format_session_line(
191        start_time: &DateTime<Local>,
192        duration: i64,
193        context: &str,
194        is_active: bool,
195    ) -> String {
196        let status_char = if is_active { "*" } else { "+" };
197        let duration_str = if is_active {
198            format!("{} (active)", Formatter::format_duration(duration))
199        } else {
200            Formatter::format_duration(duration)
201        };
202
203        format!(
204            "{} {} | {} | {}",
205            status_char,
206            start_time.format("%H:%M:%S"),
207            duration_str,
208            context
209        )
210    }
211}