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