tempo_cli/ui/
formatter.rs

1use crate::models::{Project, Session};
2use chrono::{DateTime, Duration, Local};
3use ratatui::{
4    style::{Color, Modifier, Style},
5    text::{Line, Span, Text},
6    widgets::{Block, Borders},
7};
8
9pub struct Formatter;
10
11impl Formatter {
12    pub fn format_duration(seconds: i64) -> String {
13        let duration = Duration::seconds(seconds);
14        let hours = duration.num_hours();
15        let minutes = duration.num_minutes() % 60;
16        let seconds = duration.num_seconds() % 60;
17
18        if hours > 0 {
19            format!("{}h {}m {}s", hours, minutes, seconds)
20        } else if minutes > 0 {
21            format!("{}m {}s", minutes, seconds)
22        } else {
23            format!("{}s", seconds)
24        }
25    }
26
27    pub fn format_duration_clock(seconds: i64) -> String {
28        let duration = Duration::seconds(seconds);
29        let hours = duration.num_hours();
30        let minutes = duration.num_minutes() % 60;
31        let seconds = duration.num_seconds() % 60;
32
33        format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
34    }
35
36    pub fn format_timestamp(timestamp: &DateTime<Local>) -> String {
37        timestamp.format("%Y-%m-%d %H:%M:%S").to_string()
38    }
39
40    pub fn format_time_only(timestamp: &DateTime<Local>) -> String {
41        timestamp.format("%H:%M:%S").to_string()
42    }
43
44    pub fn format_date_only(timestamp: &DateTime<Local>) -> String {
45        timestamp.format("%Y-%m-%d").to_string()
46    }
47
48    pub fn create_header_block(title: &str) -> Block {
49        Block::default()
50            .title(title)
51            .borders(Borders::ALL)
52            .style(Style::default().fg(Color::Cyan))
53    }
54
55    pub fn create_info_block() -> Block<'static> {
56        Block::default()
57            .borders(Borders::ALL)
58            .style(Style::default().fg(Color::White))
59    }
60
61    pub fn create_success_style() -> Style {
62        Style::default()
63            .fg(Color::Green)
64            .add_modifier(Modifier::BOLD)
65    }
66
67    pub fn create_warning_style() -> Style {
68        Style::default()
69            .fg(Color::Yellow)
70            .add_modifier(Modifier::BOLD)
71    }
72
73    pub fn create_error_style() -> Style {
74        Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
75    }
76
77    pub fn create_highlight_style() -> Style {
78        Style::default()
79            .fg(Color::Cyan)
80            .add_modifier(Modifier::BOLD)
81    }
82
83    pub fn format_session_status(session: &Session) -> Text {
84        let status = if session.end_time.is_some() {
85            Span::styled("Completed", Self::create_success_style())
86        } else {
87            Span::styled("Active", Self::create_success_style())
88        };
89
90        // Context-specific colors
91        let context_color = match session.context.to_string().as_str() {
92            "terminal" => Color::Cyan,
93            "ide" => Color::Magenta,
94            "linked" => Color::Yellow,
95            "manual" => Color::Blue,
96            _ => Color::White,
97        };
98
99        let start_time = Self::format_timestamp(&session.start_time.with_timezone(&Local));
100        let duration = if let Some(_end_time) = &session.end_time {
101            let active_duration = session.active_duration().unwrap_or_default();
102            Self::format_duration(active_duration.num_seconds())
103        } else {
104            let current_active = session.current_active_duration();
105            format!(
106                "{} (ongoing)",
107                Self::format_duration(current_active.num_seconds())
108            )
109        };
110
111        Text::from(vec![
112            Line::from(vec![Span::raw("Status: "), status]),
113            Line::from(vec![
114                Span::raw("Started: "),
115                Span::styled(start_time, Style::default().fg(Color::Gray)),
116            ]),
117            Line::from(vec![
118                Span::raw("Duration: "),
119                Span::styled(
120                    duration,
121                    Style::default()
122                        .fg(Color::Green)
123                        .add_modifier(Modifier::BOLD),
124                ),
125            ]),
126            Line::from(vec![
127                Span::raw("Context: "),
128                Span::styled(
129                    session.context.to_string(),
130                    Style::default()
131                        .fg(context_color)
132                        .add_modifier(Modifier::BOLD),
133                ),
134            ]),
135        ])
136    }
137
138    pub fn format_project_info(project: &Project) -> Text {
139        Text::from(vec![
140            Line::from(vec![
141                Span::raw("Name: "),
142                Span::styled(
143                    project.name.clone(),
144                    Style::default()
145                        .fg(Color::Yellow)
146                        .add_modifier(Modifier::BOLD),
147                ),
148            ]),
149            Line::from(vec![
150                Span::raw("Path: "),
151                Span::styled(
152                    project.path.to_string_lossy().to_string(),
153                    Style::default().fg(Color::Gray),
154                ),
155            ]),
156            if let Some(description) = &project.description {
157                Line::from(vec![
158                    Span::raw("Description: "),
159                    Span::styled(description.clone(), Style::default().fg(Color::White)),
160                ])
161            } else {
162                Line::from(Span::raw(""))
163            },
164            Line::from(vec![
165                Span::raw("Created: "),
166                Span::styled(
167                    Self::format_timestamp(&project.created_at.with_timezone(&Local)),
168                    Style::default().fg(Color::Gray),
169                ),
170            ]),
171        ])
172    }
173
174    pub fn format_sessions_summary(sessions: &[Session]) -> String {
175        if sessions.is_empty() {
176            return "No sessions found".to_string();
177        }
178
179        let mut result = String::new();
180        result.push_str("Sessions:\n");
181
182        for session in sessions.iter().take(5) {
183            let duration = if let Some(_end_time) = &session.end_time {
184                let active_duration = session.active_duration().unwrap_or_default();
185                Self::format_duration(active_duration.num_seconds())
186            } else {
187                let current_active = session.current_active_duration();
188                format!(
189                    "{} (active)",
190                    Self::format_duration(current_active.num_seconds())
191                )
192            };
193
194            let status = if session.end_time.is_some() { "+" } else { "*" };
195
196            result.push_str(&format!(
197                "  {} {} | {} | {} | {}\n",
198                status,
199                session.id.unwrap_or(0),
200                Self::format_timestamp(&session.start_time.with_timezone(&Local)),
201                duration,
202                session.context
203            ));
204        }
205
206        if sessions.len() > 5 {
207            result.push_str(&format!("  ... and {} more sessions\n", sessions.len() - 5));
208        }
209
210        result
211    }
212}