Skip to main content

systemprompt_logging/services/cli/
table.rs

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