tempo_cli/ui/
formatter.rs1use 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 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}