1use std::{borrow::Cow, fmt, iter};
14
15use unicode_width::UnicodeWidthStr;
16
17#[derive(Clone, Debug, Default, Eq, PartialEq)]
29pub struct Columns<'a> {
30 pub header: Vec<&'a str>,
32 pub rows: Vec<Vec<String>>,
34}
35
36impl<'a> Columns<'a> {
37 pub fn new(header: impl IntoIterator<Item = &'a str>) -> Self {
39 Self {
40 header: header.into_iter().collect(),
41 rows: Vec::new(),
42 }
43 }
44
45 pub fn push(&mut self, row: impl IntoIterator<Item = impl fmt::Display>) {
47 let row = row.into_iter().map(|cell| cell.to_string());
48 self.rows.push(row.collect());
49 }
50}
51
52impl fmt::Display for Columns<'_> {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 let header = (!self.header.is_empty()).then(|| {
55 let labels = self.header.iter().copied();
56 labels.map(cell).collect::<Vec<_>>()
57 });
58 let rows = self.rows.iter().map(|row| {
59 let cells = row.iter().map(String::as_str);
60 cells.map(cell).collect::<Vec<_>>()
61 });
62 let lines: Vec<_> = iter::once(header).flatten().chain(rows).collect();
63
64 let count = lines.iter().map(Vec::len).max().unwrap_or_default();
65 let mut widths = vec![1; count];
66
67 for line in &lines {
68 for (width, cell) in widths.iter_mut().zip(line) {
69 *width = cell.width().max(*width);
70 }
71 }
72
73 for line in &lines {
74 for (i, width) in widths.iter().enumerate() {
75 let cell = line.get(i).map_or("-", |cell| cell);
76
77 if i > 0 {
78 f.write_str(" ")?;
79 }
80
81 f.write_str(cell)?;
82
83 if i + 1 < count {
84 let pad = width - cell.width();
85 write!(f, "{:pad$}", "")?;
86 }
87 }
88
89 writeln!(f)?;
90 }
91
92 Ok(())
93 }
94}
95
96fn cell(s: &str) -> Cow<'_, str> {
98 if s.is_empty() {
99 Cow::Borrowed("-")
100 } else if s.contains(['\n', '\r']) {
101 Cow::Owned(s.replace(['\n', '\r'], " "))
102 } else {
103 Cow::Borrowed(s)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use crate::columns::*;
110
111 #[test]
112 fn a_wide_last_column_is_not_padded() {
113 let mut columns = Columns::new(["ID", "NAME", "DESCRIPTION"]);
114 columns.push(["1", "Inbox", "Mail received"]);
115 columns.push(["42", "Sent", ""]);
116
117 let expected = "\
118ID NAME DESCRIPTION
1191 Inbox Mail received
12042 Sent -
121";
122 assert_eq!(columns.to_string(), expected);
123 }
124
125 #[test]
126 fn wide_characters_count_their_display_width() {
127 let mut columns = Columns::new(["NAME", "ID"]);
128 columns.push(["日本語", "1"]);
129 columns.push(["🦀", "2"]);
130 columns.push(["ab", "3"]);
131
132 let expected = "\
133NAME ID
134日本語 1
135🦀 2
136ab 3
137";
138 assert_eq!(columns.to_string(), expected);
139 }
140
141 #[test]
142 fn empty_and_missing_cells_print_a_dash() {
143 let mut columns = Columns::new(["A", "B", "C"]);
144 columns.push(["", "b"]);
145 columns.push(["a", "", "c"]);
146
147 let expected = "\
148A B C
149- b -
150a - c
151";
152 assert_eq!(columns.to_string(), expected);
153 }
154
155 #[test]
156 fn a_line_break_in_a_cell_prints_as_a_space() {
157 let mut columns = Columns::new(["SUBJECT", "FROM"]);
158 columns.push(["Re:\nhello", "a\r\nb"]);
159
160 let expected = "\
161SUBJECT FROM
162Re: hello a b
163";
164 assert_eq!(columns.to_string(), expected);
165 }
166
167 #[test]
168 fn a_list_without_rows_prints_its_header() {
169 let columns = Columns::new(["ID", "NAME"]);
170 assert_eq!(columns.to_string(), "ID NAME\n");
171 assert_eq!(Columns::default().to_string(), "");
172 }
173}