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