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