Skip to main content

codanna/display/
tables.rs

1//! Table formatting utilities for structured output.
2
3use comfy_table::{
4    Attribute, Cell, Color, Table, modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL,
5};
6
7/// Builder for creating formatted tables.
8pub struct TableBuilder {
9    table: Table,
10}
11
12impl Default for TableBuilder {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl TableBuilder {
19    /// Create a new table builder.
20    pub fn new() -> Self {
21        let mut table = Table::new();
22        table.load_preset(UTF8_FULL);
23        // Apply rounded corners
24        table.apply_modifier(UTF8_ROUND_CORNERS);
25        Self { table }
26    }
27
28    /// Set the table headers.
29    pub fn set_headers(mut self, headers: Vec<&str>) -> Self {
30        let header_cells: Vec<Cell> = headers
31            .into_iter()
32            .map(|h| Cell::new(h).add_attribute(Attribute::Bold))
33            .collect();
34        self.table.set_header(header_cells);
35        self
36    }
37
38    /// Add a row to the table.
39    pub fn add_row(mut self, row: Vec<String>) -> Self {
40        self.table.add_row(row);
41        self
42    }
43
44    /// Build and return the formatted table.
45    pub fn build(self) -> String {
46        self.table.to_string()
47    }
48}
49
50/// Create a benchmark results table.
51pub fn create_benchmark_table(
52    language: &str,
53    file_path: Option<&str>,
54    symbols: usize,
55    avg_time: std::time::Duration,
56    rate: f64,
57) -> String {
58    let mut table = Table::new();
59    table.load_preset(UTF8_FULL);
60    // Apply rounded corners for a modern look
61    table.apply_modifier(UTF8_ROUND_CORNERS);
62
63    // Create the header
64    table.set_header(vec![
65        Cell::new("Metric").add_attribute(Attribute::Bold),
66        Cell::new("Value").add_attribute(Attribute::Bold),
67    ]);
68
69    // Add rows without ANSI colors (comfy-table doesn't handle them well)
70    table.add_row(vec!["Language", language]);
71
72    if let Some(path) = file_path {
73        table.add_row(vec!["File", path]);
74    } else {
75        table.add_row(vec!["File", "<generated benchmark code>"]);
76    }
77
78    table.add_row(vec!["Symbols parsed", &symbols.to_string()]);
79
80    table.add_row(vec!["Average time", &format!("{avg_time:?}")]);
81
82    table.add_row(vec!["Rate", &format!("{rate:.0} symbols/second")]);
83
84    // Performance indicator with color
85    let performance_ratio = rate / 10_000.0;
86    let (performance_text, color) = if performance_ratio >= 1.0 {
87        (
88            format!("✓ {performance_ratio:.1}x faster than target"),
89            Color::Green,
90        )
91    } else {
92        (
93            format!("⚠ {performance_ratio:.1}x of target"),
94            Color::Yellow,
95        )
96    };
97
98    table.add_row(vec![
99        Cell::new("Performance"),
100        Cell::new(performance_text)
101            .fg(color)
102            .add_attribute(Attribute::Bold),
103    ]);
104
105    table.to_string()
106}
107
108/// Create a summary table for indexing results.
109pub fn create_summary_table(
110    results: Vec<(String, usize, usize, std::time::Duration)>, // (language, files, symbols, time)
111) -> String {
112    let mut table = Table::new();
113    table.load_preset(UTF8_FULL);
114    // Apply rounded corners for consistency
115    table.apply_modifier(UTF8_ROUND_CORNERS);
116
117    // Header
118    table.set_header(vec![
119        Cell::new("Language").add_attribute(Attribute::Bold),
120        Cell::new("Files").add_attribute(Attribute::Bold),
121        Cell::new("Symbols").add_attribute(Attribute::Bold),
122        Cell::new("Time").add_attribute(Attribute::Bold),
123        Cell::new("Rate").add_attribute(Attribute::Bold),
124    ]);
125
126    // Data rows
127    let mut total_files = 0;
128    let mut total_symbols = 0;
129    let mut total_time = std::time::Duration::ZERO;
130
131    for (lang, files, symbols, time) in results {
132        total_files += files;
133        total_symbols += symbols;
134        total_time += time;
135
136        let rate = if time.as_secs_f64() > 0.0 {
137            symbols as f64 / time.as_secs_f64()
138        } else {
139            0.0
140        };
141
142        table.add_row(vec![
143            lang,
144            files.to_string(),
145            symbols.to_string(),
146            format!("{:?}", time),
147            format!("{:.0}/s", rate),
148        ]);
149    }
150
151    // Total row
152    if total_time.as_secs_f64() > 0.0 {
153        let total_rate = total_symbols as f64 / total_time.as_secs_f64();
154        table.add_row(vec![
155            Cell::new("TOTAL").add_attribute(Attribute::Bold),
156            Cell::new(total_files).add_attribute(Attribute::Bold),
157            Cell::new(total_symbols).add_attribute(Attribute::Bold),
158            Cell::new(format!("{total_time:?}")).add_attribute(Attribute::Bold),
159            Cell::new(format!("{total_rate:.0}/s")).add_attribute(Attribute::Bold),
160        ]);
161    }
162
163    table.to_string()
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_table_builder() {
172        let table = TableBuilder::new()
173            .set_headers(vec!["Column 1", "Column 2"])
174            .add_row(vec!["Value 1".to_string(), "Value 2".to_string()])
175            .build();
176
177        assert!(table.contains("Column 1"));
178        assert!(table.contains("Value 1"));
179    }
180}