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