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_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 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}