Skip to main content

systemprompt_logging/services/cli/
table.rs

1#![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}