Skip to main content

browser_control/cli/
output.rs

1//! Output helpers for CLI subcommands: aligned tables and JSON.
2
3use serde::Serialize;
4use std::io::Write;
5
6/// Print rows as a simple aligned table.
7///
8/// Column widths are computed from the headers plus all cell contents.
9/// Columns are separated by a two-space gutter, and a separator line of
10/// dashes is printed between the header row and the data rows.
11pub fn print_table<W: Write>(
12    out: &mut W,
13    headers: &[&str],
14    rows: &[Vec<String>],
15) -> std::io::Result<()> {
16    let ncols = headers.len();
17    let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
18    for row in rows {
19        for (i, cell) in row.iter().enumerate().take(ncols) {
20            if cell.len() > widths[i] {
21                widths[i] = cell.len();
22            }
23        }
24    }
25
26    write_row(out, headers.iter().copied(), &widths)?;
27    let sep: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
28    write_row(out, sep.iter().map(|s| s.as_str()), &widths)?;
29    for row in rows {
30        write_row(
31            out,
32            (0..ncols).map(|i| row.get(i).map(|s| s.as_str()).unwrap_or("")),
33            &widths,
34        )?;
35    }
36    Ok(())
37}
38
39fn write_row<'a, W: Write, I: Iterator<Item = &'a str>>(
40    out: &mut W,
41    cells: I,
42    widths: &[usize],
43) -> std::io::Result<()> {
44    let cells: Vec<&str> = cells.collect();
45    let last = cells.len().saturating_sub(1);
46    for (i, cell) in cells.iter().enumerate() {
47        if i == last {
48            // Don't pad the last column.
49            write!(out, "{}", cell)?;
50        } else {
51            write!(out, "{:<width$}  ", cell, width = widths[i])?;
52        }
53    }
54    writeln!(out)
55}
56
57/// Print a serializable value as pretty JSON, followed by a newline.
58pub fn print_json<W: Write, T: Serialize>(out: &mut W, value: &T) -> anyhow::Result<()> {
59    let s = serde_json::to_string_pretty(value)?;
60    out.write_all(s.as_bytes())?;
61    out.write_all(b"\n")?;
62    Ok(())
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn empty_rows_produces_headers_and_separator() {
71        let mut buf: Vec<u8> = Vec::new();
72        print_table(&mut buf, &["A", "BB"], &[]).unwrap();
73        let s = String::from_utf8(buf).unwrap();
74        let lines: Vec<&str> = s.lines().collect();
75        assert_eq!(lines.len(), 2, "got: {:?}", lines);
76        assert!(lines[0].starts_with("A "));
77        assert!(lines[0].contains("BB"));
78        assert!(lines[1].starts_with("-"));
79        assert!(lines[1].contains("--"));
80    }
81
82    #[test]
83    fn column_widths_grow_to_longest_cell() {
84        let mut buf: Vec<u8> = Vec::new();
85        let rows = vec![
86            vec!["short".to_string(), "x".to_string()],
87            vec!["a-very-long-cell".to_string(), "y".to_string()],
88        ];
89        print_table(&mut buf, &["K", "V"], &rows).unwrap();
90        let s = String::from_utf8(buf).unwrap();
91        let lines: Vec<&str> = s.lines().collect();
92        // First column width should be at least len("a-very-long-cell") = 16.
93        // Header line has "K" left-padded to 16, then two spaces, then "V".
94        assert!(
95            lines[0].starts_with("K               "),
96            "header pad wrong: {:?}",
97            lines[0]
98        );
99        // Separator first column should be 16 dashes.
100        assert!(
101            lines[1].starts_with(&"-".repeat(16)),
102            "sep wrong: {:?}",
103            lines[1]
104        );
105        // Data row 1: "short" padded to 16.
106        assert!(lines[2].starts_with("short           "));
107        assert!(lines[3].starts_with("a-very-long-cell  y"));
108    }
109
110    #[test]
111    fn json_output_is_valid_json() {
112        #[derive(Serialize)]
113        struct X {
114            a: u32,
115            b: Vec<String>,
116        }
117        let mut buf: Vec<u8> = Vec::new();
118        let x = X {
119            a: 7,
120            b: vec!["hi".into(), "there".into()],
121        };
122        print_json(&mut buf, &x).unwrap();
123        let s = String::from_utf8(buf).unwrap();
124        let v: serde_json::Value = serde_json::from_str(&s).unwrap();
125        assert_eq!(v["a"], 7);
126        assert_eq!(v["b"][1], "there");
127    }
128}