1use std::fmt::Write as _;
2
3pub fn render_table(headers: &[String], rows: &[Vec<String>]) -> String {
4 let column_count = headers.len();
5 let mut widths = headers.iter().map(|h| display_width(h)).collect::<Vec<_>>();
6
7 for row in rows {
8 for (idx, cell) in row.iter().enumerate().take(column_count) {
9 widths[idx] = widths[idx].max(display_width(cell));
10 }
11 }
12
13 let mut output = String::new();
14
15 let header_line = format_row(headers, &widths);
17 let _ = writeln!(output, "{header_line}");
18
19 let separator = widths
21 .iter()
22 .map(|w| "-".repeat(*w.max(&3) + 2))
23 .collect::<Vec<_>>()
24 .join("+");
25 let _ = writeln!(output, "{separator}");
26
27 for row in rows {
29 let row_line = format_row(row, &widths);
30 let _ = writeln!(output, "{row_line}");
31 }
32
33 output
34}
35
36pub fn print_table(headers: &[String], rows: &[Vec<String>]) {
37 let rendered = render_table(headers, rows);
38 print!("{rendered}");
39}
40
41fn format_row(values: &[String], widths: &[usize]) -> String {
42 values
43 .iter()
44 .enumerate()
45 .map(|(idx, value)| format!(" {:<width$} ", value, width = widths[idx]))
46 .collect::<Vec<_>>()
47 .join("|")
48}
49
50fn display_width(value: &str) -> usize {
51 let mut width = 0usize;
52 let mut chars = value.chars();
53 while let Some(ch) = chars.next() {
54 if ch == '\u{1b}' {
55 for next in chars.by_ref() {
57 if next == 'm' {
58 break;
59 }
60 }
61 } else {
62 width += 1;
63 }
64 }
65 width
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn render_table_aligns_columns() {
74 let headers = vec!["id".to_string(), "name".to_string()];
75 let rows = vec![
76 vec!["1".to_string(), "Alice".to_string()],
77 vec!["2".to_string(), "Bob".to_string()],
78 ];
79
80 let rendered = render_table(&headers, &rows);
81 let lines: Vec<&str> = rendered.lines().collect();
82
83 assert_eq!(lines.len(), 4);
84 assert_eq!(lines[0], " id | name ");
85 assert!(lines[1].contains("---"));
86 assert_eq!(lines[2], " 1 | Alice ");
87 assert_eq!(lines[3], " 2 | Bob ");
88 }
89
90 #[test]
91 fn display_width_counts_characters() {
92 assert_eq!(display_width("abc"), 3);
93 assert_eq!(display_width(""), 0);
94 assert_eq!(display_width("résumé"), 6);
95 }
96
97 #[test]
98 fn display_width_ignores_ansi_sequences() {
99 let value = "\u{1b}[31minvalid\u{1b}[0m";
100 assert_eq!(display_width(value), "invalid".len());
101 }
102}