tempo_cli/ui/
formatter.rs

1use chrono::{DateTime, Local, Duration};
2use ratatui::{
3    style::{Color, Modifier, Style},
4    text::{Line, Span, Text},
5    widgets::{Block, Borders, Paragraph, Table, Row, Cell},
6};
7use crate::models::{Project, Session};
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().fg(Color::Green).add_modifier(Modifier::BOLD)
54    }
55
56    pub fn create_warning_style() -> Style {
57        Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
58    }
59
60    pub fn create_error_style() -> Style {
61        Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
62    }
63
64    pub fn create_highlight_style() -> Style {
65        Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
66    }
67
68    pub fn format_session_status(session: &Session) -> Text {
69        let status = if session.end_time.is_some() {
70            Span::styled("Completed", Self::create_success_style())
71        } else {
72            Span::styled("Active", Self::create_success_style())
73        };
74
75        // Context-specific colors
76        let context_color = match session.context.to_string().as_str() {
77            "terminal" => Color::Cyan,
78            "ide" => Color::Magenta,
79            "linked" => Color::Yellow,
80            "manual" => Color::Blue,
81            _ => Color::White,
82        };
83
84        let start_time = Self::format_timestamp(&session.start_time.with_timezone(&Local));
85        let duration = if let Some(_end_time) = &session.end_time {
86            let active_duration = session.active_duration().unwrap_or_default();
87            Self::format_duration(active_duration.num_seconds())
88        } else {
89            let current_active = session.current_active_duration();
90            format!("{} (ongoing)", Self::format_duration(current_active.num_seconds()))
91        };
92
93        Text::from(vec![
94            Line::from(vec![
95                Span::raw("Status: "),
96                status,
97            ]),
98            Line::from(vec![
99                Span::raw("Started: "),
100                Span::styled(start_time, Style::default().fg(Color::Gray)),
101            ]),
102            Line::from(vec![
103                Span::raw("Duration: "),
104                Span::styled(duration, Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
105            ]),
106            Line::from(vec![
107                Span::raw("Context: "),
108                Span::styled(session.context.to_string(), Style::default().fg(context_color).add_modifier(Modifier::BOLD)),
109            ]),
110        ])
111    }
112
113    pub fn format_project_info(project: &Project) -> Text {
114        Text::from(vec![
115            Line::from(vec![
116                Span::raw("Name: "),
117                Span::styled(project.name.clone(), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
118            ]),
119            Line::from(vec![
120                Span::raw("Path: "),
121                Span::styled(project.path.to_string_lossy().to_string(), Style::default().fg(Color::Gray)),
122            ]),
123            if let Some(description) = &project.description {
124                Line::from(vec![
125                    Span::raw("Description: "),
126                    Span::styled(description.clone(), Style::default().fg(Color::White)),
127                ])
128            } else {
129                Line::from(Span::raw(""))
130            },
131            Line::from(vec![
132                Span::raw("Created: "),
133                Span::styled(Self::format_timestamp(&project.created_at.with_timezone(&Local)), Style::default().fg(Color::Gray)),
134            ]),
135        ])
136    }
137
138    pub fn format_sessions_summary(sessions: &[Session]) -> String {
139        if sessions.is_empty() {
140            return "No sessions found".to_string();
141        }
142
143        let mut result = String::new();
144        result.push_str("Sessions:\n");
145        
146        for session in sessions.iter().take(5) {
147            let duration = if let Some(_end_time) = &session.end_time {
148                let active_duration = session.active_duration().unwrap_or_default();
149                Self::format_duration(active_duration.num_seconds())
150            } else {
151                let current_active = session.current_active_duration();
152                format!("{} (active)", Self::format_duration(current_active.num_seconds()))
153            };
154
155            let status = if session.end_time.is_some() { "✓" } else { "●" };
156            
157            result.push_str(&format!(
158                "  {} {} | {} | {} | {}\n",
159                status,
160                session.id.unwrap_or(0),
161                Self::format_timestamp(&session.start_time.with_timezone(&Local)),
162                duration,
163                session.context
164            ));
165        }
166
167        if sessions.len() > 5 {
168            result.push_str(&format!("  ... and {} more sessions\n", sessions.len() - 5));
169        }
170
171        result
172    }
173}