rskit_cli/render/
table.rs1use std::fmt;
4
5pub struct OutputTable {
7 title: Option<String>,
8 columns: Vec<String>,
9 rows: Vec<Vec<String>>,
10}
11
12impl OutputTable {
13 #[must_use]
15 pub fn new(columns: Vec<impl Into<String>>) -> Self {
16 Self {
17 title: None,
18 columns: columns.into_iter().map(Into::into).collect(),
19 rows: Vec::new(),
20 }
21 }
22
23 #[must_use]
25 pub fn with_title(mut self, title: impl Into<String>) -> Self {
26 self.title = Some(title.into());
27 self
28 }
29
30 pub fn add_row(&mut self, row: Vec<impl Into<String>>) {
36 let mut cells: Vec<String> = row.into_iter().map(Into::into).collect();
37 cells.truncate(self.columns.len());
38 cells.resize(self.columns.len(), String::new());
39 self.rows.push(cells);
40 }
41}
42
43impl fmt::Display for OutputTable {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 let mut widths: Vec<usize> = self.columns.iter().map(|c| c.chars().count()).collect();
46 for row in &self.rows {
47 for (i, cell) in row.iter().enumerate() {
48 if i < widths.len() {
49 widths[i] = widths[i].max(cell.chars().count());
50 }
51 }
52 }
53
54 if let Some(title) = &self.title {
55 writeln!(f, "\n{title}")?;
56 }
57
58 let separator: String = widths
59 .iter()
60 .map(|w| "─".repeat(w + 2))
61 .collect::<Vec<_>>()
62 .join("┬");
63 writeln!(f, "┌{separator}┐")?;
64
65 let header: String = self
66 .columns
67 .iter()
68 .enumerate()
69 .map(|(i, c)| format!(" {:width$} ", c, width = widths[i]))
70 .collect::<Vec<_>>()
71 .join("│");
72 writeln!(f, "│{header}│")?;
73
74 let separator: String = widths
75 .iter()
76 .map(|w| "─".repeat(w + 2))
77 .collect::<Vec<_>>()
78 .join("┼");
79 writeln!(f, "├{separator}┤")?;
80
81 for row in &self.rows {
82 let cells: String = row
83 .iter()
84 .enumerate()
85 .map(|(i, c)| {
86 let w = widths.get(i).copied().unwrap_or(0);
87 format!(" {c:w$} ")
88 })
89 .collect::<Vec<_>>()
90 .join("│");
91 writeln!(f, "│{cells}│")?;
92 }
93
94 let separator: String = widths
95 .iter()
96 .map(|w| "─".repeat(w + 2))
97 .collect::<Vec<_>>()
98 .join("┴");
99 write!(f, "└{separator}┘")?;
100
101 Ok(())
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::OutputTable;
108
109 #[test]
110 fn output_table_renders() {
111 let mut table = OutputTable::new(vec!["Name", "Count"]);
112 table.add_row(vec!["real", "500"]);
113 table.add_row(vec!["ai", "500"]);
114 let output = table.to_string();
115 assert!(output.contains("Name"));
116 assert!(output.contains("500"));
117 }
118
119 #[test]
120 fn rows_are_normalized_to_column_count() {
121 let mut table = OutputTable::new(vec!["A", "B"]);
124 table.add_row(vec!["only-one"]);
125 table.add_row(vec!["x", "y", "extra"]);
126 let output = table.to_string();
127
128 let widths: Vec<usize> = output.lines().map(|l| l.chars().count()).collect();
130 assert!(
131 widths.windows(2).all(|w| w[0] == w[1]),
132 "ragged lines: {output}"
133 );
134 assert!(!output.contains("extra"));
135 }
136}