browser_control/cli/
output.rs1use serde::Serialize;
4use std::io::Write;
5
6const FALLBACK_TERMINAL_WIDTH: usize = 120;
7
8pub fn terminal_width() -> usize {
10 if let Some(width) = terminal_width_from_env() {
11 return width;
12 }
13 terminal_width_from_stdout().unwrap_or(FALLBACK_TERMINAL_WIDTH)
14}
15
16fn terminal_width_from_env() -> Option<usize> {
17 std::env::var("COLUMNS")
18 .ok()
19 .and_then(|s| s.parse::<usize>().ok())
20 .filter(|w| *w > 0)
21}
22
23#[cfg(unix)]
24fn terminal_width_from_stdout() -> Option<usize> {
25 let mut size = libc::winsize {
26 ws_row: 0,
27 ws_col: 0,
28 ws_xpixel: 0,
29 ws_ypixel: 0,
30 };
31 let rc = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut size) };
32 if rc == 0 && size.ws_col > 0 {
33 Some(size.ws_col as usize)
34 } else {
35 None
36 }
37}
38
39#[cfg(not(unix))]
40fn terminal_width_from_stdout() -> Option<usize> {
41 None
42}
43
44pub fn print_table<W: Write>(
50 out: &mut W,
51 headers: &[&str],
52 rows: &[Vec<String>],
53) -> std::io::Result<()> {
54 let ncols = headers.len();
55 let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
56 for row in rows {
57 for (i, cell) in row.iter().enumerate().take(ncols) {
58 if cell.len() > widths[i] {
59 widths[i] = cell.len();
60 }
61 }
62 }
63
64 write_row(out, headers.iter().copied(), &widths)?;
65 let sep: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
66 write_row(out, sep.iter().map(|s| s.as_str()), &widths)?;
67 for row in rows {
68 write_row(
69 out,
70 (0..ncols).map(|i| row.get(i).map(|s| s.as_str()).unwrap_or("")),
71 &widths,
72 )?;
73 }
74 Ok(())
75}
76
77fn write_row<'a, W: Write, I: Iterator<Item = &'a str>>(
78 out: &mut W,
79 cells: I,
80 widths: &[usize],
81) -> std::io::Result<()> {
82 let cells: Vec<&str> = cells.collect();
83 let last = cells.len().saturating_sub(1);
84 for (i, cell) in cells.iter().enumerate() {
85 if i == last {
86 write!(out, "{}", cell)?;
88 } else {
89 write!(out, "{:<width$} ", cell, width = widths[i])?;
90 }
91 }
92 writeln!(out)
93}
94
95pub fn print_json<W: Write, T: Serialize>(out: &mut W, value: &T) -> anyhow::Result<()> {
97 let s = serde_json::to_string_pretty(value)?;
98 out.write_all(s.as_bytes())?;
99 out.write_all(b"\n")?;
100 Ok(())
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn empty_rows_produces_headers_and_separator() {
109 let mut buf: Vec<u8> = Vec::new();
110 print_table(&mut buf, &["A", "BB"], &[]).unwrap();
111 let s = String::from_utf8(buf).unwrap();
112 let lines: Vec<&str> = s.lines().collect();
113 assert_eq!(lines.len(), 2, "got: {:?}", lines);
114 assert!(lines[0].starts_with("A "));
115 assert!(lines[0].contains("BB"));
116 assert!(lines[1].starts_with("-"));
117 assert!(lines[1].contains("--"));
118 }
119
120 #[test]
121 fn column_widths_grow_to_longest_cell() {
122 let mut buf: Vec<u8> = Vec::new();
123 let rows = vec![
124 vec!["short".to_string(), "x".to_string()],
125 vec!["a-very-long-cell".to_string(), "y".to_string()],
126 ];
127 print_table(&mut buf, &["K", "V"], &rows).unwrap();
128 let s = String::from_utf8(buf).unwrap();
129 let lines: Vec<&str> = s.lines().collect();
130 assert!(
133 lines[0].starts_with("K "),
134 "header pad wrong: {:?}",
135 lines[0]
136 );
137 assert!(
139 lines[1].starts_with(&"-".repeat(16)),
140 "sep wrong: {:?}",
141 lines[1]
142 );
143 assert!(lines[2].starts_with("short "));
145 assert!(lines[3].starts_with("a-very-long-cell y"));
146 }
147
148 #[test]
149 fn json_output_is_valid_json() {
150 #[derive(Serialize)]
151 struct X {
152 a: u32,
153 b: Vec<String>,
154 }
155 let mut buf: Vec<u8> = Vec::new();
156 let x = X {
157 a: 7,
158 b: vec!["hi".into(), "there".into()],
159 };
160 print_json(&mut buf, &x).unwrap();
161 let s = String::from_utf8(buf).unwrap();
162 let v: serde_json::Value = serde_json::from_str(&s).unwrap();
163 assert_eq!(v["a"], 7);
164 assert_eq!(v["b"][1], "there");
165 }
166}