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