1use unicode_width::UnicodeWidthStr;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum Alignment {
11 Left,
13 Center,
15 Right,
17}
18
19#[derive(Clone, Debug)]
21pub struct TableColumn {
22 pub width: usize,
24 pub alignment: Alignment,
26 pub header: String,
28}
29
30impl TableColumn {
31 pub fn new(header: impl Into<String>, alignment: Alignment) -> Self {
33 let header_str = header.into();
34 let width = UnicodeWidthStr::width(header_str.as_str());
35 Self { width, alignment, header: header_str }
36 }
37
38 pub fn measure_cell(&mut self, content: &str) {
40 let cell_width = UnicodeWidthStr::width(content);
41 self.width = self.width.max(cell_width);
42 }
43}
44
45#[derive(Clone, Debug)]
47pub struct TableFormatter {
48 pub columns: Vec<TableColumn>,
50 pub use_unicode: bool,
52}
53
54impl TableFormatter {
55 pub fn new(columns: Vec<TableColumn>, use_unicode: bool) -> Self {
57 Self { columns, use_unicode }
58 }
59
60 pub fn measure_content(&mut self, rows: &[Vec<String>]) {
62 for row in rows {
63 for (col_idx, cell) in row.iter().enumerate() {
64 if let Some(column) = self.columns.get_mut(col_idx) {
65 column.measure_cell(cell);
66 }
67 }
68 }
69 }
70
71 pub fn render_separator(&self) -> String {
73 let (left, junction, right, line) = if self.use_unicode {
74 ('├', '┼', '┤', '─')
75 } else {
76 ('+', '+', '+', '-')
77 };
78
79 let mut separator = String::from(left);
80 for (idx, column) in self.columns.iter().enumerate() {
81 separator.extend(std::iter::repeat_n(line, column.width + 2));
82 if idx < self.columns.len() - 1 {
83 separator.push(junction);
84 }
85 }
86 separator.push(right);
87 separator
88 }
89
90 fn format_cell(&self, content: &str, alignment: Alignment, width: usize) -> String {
92 let content_width = UnicodeWidthStr::width(content);
93 if content_width >= width {
94 return content.to_string();
95 }
96
97 let padding = width - content_width;
98 match alignment {
99 Alignment::Left => {
100 format!("{}{}", content, " ".repeat(padding))
101 }
102 Alignment::Center => {
103 let left_pad = padding / 2;
104 let right_pad = padding - left_pad;
105 format!("{}{}{}", " ".repeat(left_pad), content, " ".repeat(right_pad))
106 }
107 Alignment::Right => {
108 format!("{}{}", " ".repeat(padding), content)
109 }
110 }
111 }
112
113 pub fn render_row(&self, cells: &[String]) -> String {
115 let (sep, left, right) = if self.use_unicode {
116 ("│", "│", "│")
117 } else {
118 ("|", "|", "|")
119 };
120
121 let mut row = String::from(left);
122 for (idx, (cell, column)) in cells.iter().zip(self.columns.iter()).enumerate() {
123 let formatted = self.format_cell(cell, column.alignment, column.width);
124 row.push(' ');
125 row.push_str(&formatted);
126 row.push(' ');
127 if idx < self.columns.len() - 1 {
128 row.push_str(sep);
129 }
130 }
131 row.push_str(right);
132 row
133 }
134
135 pub fn render_header(&self) -> Vec<String> {
137 let headers: Vec<String> = self.columns.iter().map(|col| col.header.clone()).collect();
138
139 vec![self.render_row(&headers), self.render_separator()]
140 }
141
142 pub fn render_table(&self, rows: &[Vec<String>]) -> Vec<String> {
144 let mut output = Vec::new();
145
146 let (left, right, line) = if self.use_unicode {
148 ('┌', '┐', '─')
149 } else {
150 ('+', '+', '-')
151 };
152 let mut top_border = String::from(left);
153 for (idx, column) in self.columns.iter().enumerate() {
154 top_border.extend(std::iter::repeat_n(line, column.width + 2));
155 if idx < self.columns.len() - 1 {
156 let junction = if self.use_unicode { '┬' } else { '+' };
157 top_border.push(junction);
158 }
159 }
160 top_border.push(right);
161 output.push(top_border);
162
163 output.extend(self.render_header());
165
166 for row in rows {
168 output.push(self.render_row(row));
169 }
170
171 let (left, right) = if self.use_unicode { ('└', '┘') } else { ('+', '+') };
173 let mut bottom_border = String::from(left);
174 for (idx, column) in self.columns.iter().enumerate() {
175 bottom_border.extend(std::iter::repeat_n(line, column.width + 2));
176 if idx < self.columns.len() - 1 {
177 let junction = if self.use_unicode { '┴' } else { '+' };
178 bottom_border.push(junction);
179 }
180 }
181 bottom_border.push(right);
182 output.push(bottom_border);
183
184 output
185 }
186
187 pub fn total_width(&self) -> usize {
189 let content_width: usize = self.columns.iter().map(|c| c.width + 2).sum();
191 let separators = if self.columns.is_empty() {
192 0
193 } else {
194 self.columns.len() - 1
195 };
196 1 + content_width + separators + 1
197 }
198
199 pub fn wrap_to_width(&mut self, max_width: usize) {
201 if self.total_width() <= max_width || self.columns.is_empty() {
202 return;
203 }
204
205 let total_content: usize = self.columns.iter().map(|c| c.width).sum();
207 let available = max_width.saturating_sub(self.columns.len() + 3); let scale = (available as f64) / (total_content as f64);
210 for column in &mut self.columns {
211 #[allow(
212 clippy::cast_sign_loss,
213 reason = "Intentional compatibility, platform, or test-only suppression."
214 )]
215 {
216 column.width = ((column.width as f64) * scale).max(3.0) as usize;
217 }
218 }
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_column_width_calculation() {
228 let mut col = TableColumn::new("Name", Alignment::Left);
229 assert_eq!(col.width, 4);
230 col.measure_cell("Alexander");
231 assert_eq!(col.width, 9);
232 }
233
234 #[test]
235 fn test_format_cell_left_align() {
236 let formatter = TableFormatter::new(vec![], false);
237 let result = formatter.format_cell("Hi", Alignment::Left, 5);
238 assert_eq!(result, "Hi ");
239 }
240
241 #[test]
242 fn test_format_cell_center_align() {
243 let formatter = TableFormatter::new(vec![], false);
244 let result = formatter.format_cell("Hi", Alignment::Center, 5);
245 assert_eq!(result, " Hi ");
246 }
247
248 #[test]
249 fn test_format_cell_right_align() {
250 let formatter = TableFormatter::new(vec![], false);
251 let result = formatter.format_cell("Hi", Alignment::Right, 5);
252 assert_eq!(result, " Hi");
253 }
254
255 #[test]
256 fn test_separator_rendering() {
257 let formatter = TableFormatter::new(
258 vec![
259 TableColumn::new("A", Alignment::Left),
260 TableColumn::new("B", Alignment::Left),
261 ],
262 false,
263 );
264 let sep = formatter.render_separator();
265 assert!(sep.starts_with('+'));
266 assert!(sep.ends_with('+'));
267 assert!(sep.contains("+"));
268 }
269
270 #[test]
271 fn test_total_width() {
272 let formatter = TableFormatter::new(
273 vec![
274 TableColumn::new("Col1", Alignment::Left),
275 TableColumn::new("Col2", Alignment::Left),
276 ],
277 false,
278 );
279 assert_eq!(formatter.total_width(), 15);
281 }
282
283 #[test]
284 fn test_unicode_vs_ascii() {
285 let unicode_fmt = TableFormatter::new(vec![TableColumn::new("A", Alignment::Left)], true);
286 let ascii_fmt = TableFormatter::new(vec![TableColumn::new("A", Alignment::Left)], false);
287
288 let unicode_sep = unicode_fmt.render_separator();
289 let ascii_sep = ascii_fmt.render_separator();
290
291 assert!(unicode_sep.contains("├"));
292 assert!(ascii_sep.contains("+"));
293 }
294}