systemprompt_logging/services/cli/
table.rs1#![allow(clippy::print_stdout)]
2
3use std::time::Duration;
4
5use crate::services::cli::theme::{BrandColors, ServiceStatus};
6
7#[derive(Debug, Clone)]
8pub struct ServiceTableEntry {
9 pub name: String,
10 pub service_type: String,
11 pub port: Option<u16>,
12 pub status: ServiceStatus,
13}
14
15impl ServiceTableEntry {
16 pub fn new(
17 name: impl Into<String>,
18 service_type: impl Into<String>,
19 port: Option<u16>,
20 status: ServiceStatus,
21 ) -> Self {
22 Self {
23 name: name.into(),
24 service_type: service_type.into(),
25 port,
26 status,
27 }
28 }
29}
30
31pub fn truncate_to_width(s: &str, width: usize) -> String {
32 if s.chars().count() <= width {
33 return s.to_string();
34 }
35 let truncate_to = width.saturating_sub(3);
36 let truncated: String = s.chars().take(truncate_to).collect();
37 format!("{truncated}...")
38}
39
40fn calculate_column_widths(headers: &[&str], rows: &[Vec<String>]) -> Vec<usize> {
41 let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
42
43 for row in rows {
44 for (i, cell) in row.iter().enumerate() {
45 if i < widths.len() {
46 widths[i] = widths[i].max(cell.len());
47 }
48 }
49 }
50
51 widths
52}
53
54fn render_table_border(widths: &[usize], left: &str, middle: &str, right: &str) {
55 print!("{left}");
56 for (i, &width) in widths.iter().enumerate() {
57 print!("{}", "─".repeat(width + 2));
58 if i < widths.len() - 1 {
59 print!("{middle}");
60 }
61 }
62 println!("{right}");
63}
64
65fn render_table_row(cells: &[&str], widths: &[usize]) {
66 print!("│");
67 for (i, (&cell, &width)) in cells.iter().zip(widths.iter()).enumerate() {
68 let truncated = truncate_to_width(cell, width);
69 print!(" {truncated:<width$} ");
70 if i < widths.len() - 1 {
71 print!("│");
72 }
73 }
74 println!("│");
75}
76
77pub fn render_table(headers: &[&str], rows: &[Vec<String>]) {
78 if rows.is_empty() {
79 return;
80 }
81
82 let widths = calculate_column_widths(headers, rows);
83
84 render_table_border(&widths, "┌", "┬", "┐");
85 render_table_row(headers, &widths);
86 render_table_border(&widths, "├", "┼", "┤");
87
88 for row in rows {
89 let cells: Vec<&str> = row.iter().map(String::as_str).collect();
90 render_table_row(&cells, &widths);
91 }
92
93 render_table_border(&widths, "└", "┴", "┘");
94}
95
96pub fn render_service_table(title: &str, services: &[ServiceTableEntry]) {
97 if services.is_empty() {
98 return;
99 }
100
101 let name_width = services
102 .iter()
103 .map(|s| s.name.len())
104 .max()
105 .unwrap_or(4)
106 .max(4);
107 let type_width = services
108 .iter()
109 .map(|s| s.service_type.len())
110 .max()
111 .unwrap_or(4)
112 .max(4);
113 let port_width = 5;
114 let status_width = 10;
115
116 let total_width = name_width + type_width + port_width + status_width + 13;
117
118 println!();
119 println!("┌{}┐", "─".repeat(total_width));
120 println!(
121 "│ {:<width$} │",
122 BrandColors::white_bold(title),
123 width = total_width - 3
124 );
125
126 println!(
127 "├{}┬{}┬{}┬{}┤",
128 "─".repeat(name_width + 2),
129 "─".repeat(type_width + 2),
130 "─".repeat(port_width + 2),
131 "─".repeat(status_width + 2)
132 );
133
134 println!(
135 "│ {:<name_width$} │ {:<type_width$} │ {:<port_width$} │ {:<status_width$} │",
136 BrandColors::dim("Name"),
137 BrandColors::dim("Type"),
138 BrandColors::dim("Port"),
139 BrandColors::dim("Status"),
140 );
141
142 println!(
143 "├{}┼{}┼{}┼{}┤",
144 "─".repeat(name_width + 2),
145 "─".repeat(type_width + 2),
146 "─".repeat(port_width + 2),
147 "─".repeat(status_width + 2)
148 );
149
150 for service in services {
151 let port_str = service
152 .port
153 .map_or_else(|| "-".to_string(), |p| p.to_string());
154
155 let status_display = format!("{} {}", service.status.symbol(), service.status.text());
156 let colored_status = match service.status {
157 ServiceStatus::Running => format!("{}", BrandColors::running(&status_display)),
158 ServiceStatus::Starting => format!("{}", BrandColors::starting(&status_display)),
159 ServiceStatus::Stopped | ServiceStatus::Failed => {
160 format!("{}", BrandColors::stopped(&status_display))
161 },
162 ServiceStatus::Unknown => format!("{}", BrandColors::dim(&status_display)),
163 };
164
165 println!(
166 "│ {:<name_width$} │ {:<type_width$} │ {:>port_width$} │ {:<status_width$} │",
167 service.name, service.service_type, port_str, colored_status,
168 );
169 }
170
171 println!(
172 "└{}┴{}┴{}┴{}┘",
173 "─".repeat(name_width + 2),
174 "─".repeat(type_width + 2),
175 "─".repeat(port_width + 2),
176 "─".repeat(status_width + 2)
177 );
178}
179
180pub fn render_startup_complete(duration: Duration, api_url: &str) {
181 let secs = duration.as_secs_f64();
182 println!();
183 println!(
184 "{} {} {}",
185 BrandColors::running("✓"),
186 BrandColors::white_bold("All services started successfully"),
187 BrandColors::dim(format!("({:.1}s)", secs))
188 );
189 println!(
190 " {} {}",
191 BrandColors::dim("API:"),
192 BrandColors::highlight(api_url)
193 );
194 println!();
195}