sparrow/tui/formatters/
table.rs1pub struct TableStyles {
7 pub header: &'static str,
8 pub cell: &'static str,
9 pub separator: &'static str,
10 pub border: &'static str,
11 pub reset: &'static str,
12}
13
14impl Default for TableStyles {
15 fn default() -> Self {
16 Self {
17 header: "\x1b[1;38;2;242;169;60m", cell: "\x1b[38;2;236;226;207m", separator: "\x1b[38;2;92;83;70m", border: "\x1b[38;2;92;83;70m", reset: "\x1b[0m",
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum Align {
29 Left,
30 Center,
31 Right,
32}
33
34#[derive(Debug, Clone)]
36pub struct ColumnConfig {
37 pub header: String,
38 pub align: Align,
39 pub min_width: usize,
40 pub max_width: Option<usize>,
41}
42
43impl ColumnConfig {
44 pub fn new(header: &str) -> Self {
45 Self {
46 header: header.to_string(),
47 align: Align::Left,
48 min_width: header.len(),
49 max_width: None,
50 }
51 }
52
53 pub fn with_align(mut self, align: Align) -> Self {
54 self.align = align;
55 self
56 }
57}
58
59pub fn render_table(
69 headers: &[&str],
70 rows: &[Vec<String>],
71 max_width: Option<usize>,
72 styles: Option<&TableStyles>,
73) -> String {
74 if headers.is_empty() {
75 return String::new();
76 }
77
78 let styles = styles.unwrap_or(&TABLE_STYLES_DEFAULT);
79 let col_count = headers.len();
80
81 let mut col_widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
83
84 for row in rows {
86 for (i, cell) in row.iter().enumerate() {
87 if i >= col_count {
88 break;
89 }
90 let cell_len = cell.chars().count();
91 if cell_len > col_widths[i] {
92 col_widths[i] = cell_len;
93 }
94 }
95 }
96
97 for w in col_widths.iter_mut() {
99 *w = (*w).max(3);
100 }
101
102 if let Some(max_w) = max_width {
104 let separator_width = (col_count + 1) * 3; let total_content: usize = col_widths.iter().sum::<usize>() + separator_width;
106 if total_content > max_w {
107 let available = max_w.saturating_sub(separator_width);
109 let scale = available as f64 / (total_content - separator_width) as f64;
110 for w in col_widths.iter_mut() {
111 *w = ((*w as f64) * scale).max(3.0) as usize;
112 }
113 }
114 }
115
116 let mut out = String::new();
118
119 render_horizontal_line(&mut out, &col_widths, &styles);
121
122 let header_cells: Vec<String> = headers.iter().map(|h| h.to_string()).collect();
124 render_row(&mut out, &header_cells, &col_widths, &styles, true);
125
126 render_separator(&mut out, &col_widths, &styles);
128
129 for row in rows {
131 let cells: Vec<String> = row.iter().map(|c| c.to_string()).collect();
132 render_row(&mut out, &cells, &col_widths, &styles, false);
133 }
134
135 render_horizontal_line(&mut out, &col_widths, &styles);
137
138 out
139}
140
141pub fn render_table_with_config(
143 columns: &[ColumnConfig],
144 rows: &[Vec<String>],
145 max_width: Option<usize>,
146 styles: Option<&TableStyles>,
147) -> String {
148 let headers: Vec<&str> = columns.iter().map(|c| c.header.as_str()).collect();
149 render_table(&headers, rows, max_width, styles)
150}
151
152static TABLE_STYLES_DEFAULT: TableStyles = TableStyles {
155 header: "\x1b[1;38;2;242;169;60m",
156 cell: "\x1b[38;2;236;226;207m",
157 separator: "\x1b[38;2;92;83;70m",
158 border: "\x1b[38;2;92;83;70m",
159 reset: "\x1b[0m",
160};
161
162fn render_horizontal_line(out: &mut String, widths: &[usize], s: &TableStyles) {
163 out.push_str(s.border);
164 for &w in widths {
165 out.push_str("├");
166 out.push_str(&"─".repeat(w + 2));
167 }
168 out.push_str("┤");
169 out.push_str(s.reset);
170 out.push('\n');
171}
172
173fn render_separator(out: &mut String, widths: &[usize], s: &TableStyles) {
174 out.push_str(s.border);
175 for &w in widths {
176 out.push_str("├");
177 out.push_str(&"─".repeat(w + 2));
178 }
179 out.push_str("┤");
180 out.push_str(s.reset);
181 out.push('\n');
182}
183
184fn render_row(
185 out: &mut String,
186 cells: &[String],
187 widths: &[usize],
188 s: &TableStyles,
189 is_header: bool,
190) {
191 let style = if is_header { s.header } else { s.cell };
192
193 out.push_str(s.border);
194 out.push_str("│");
195
196 for (i, cell) in cells.iter().enumerate() {
197 if i >= widths.len() {
198 break;
199 }
200 let w = widths[i];
201 let display = truncate_cell(cell, w);
202 out.push(' ');
203 out.push_str(style);
204 out.push_str(&display);
205 let display_len = display.chars().count();
207 if display_len < w {
208 out.push_str(&" ".repeat(w - display_len));
209 }
210 out.push_str(s.reset);
211 out.push(' ');
212 out.push_str(s.border);
213 out.push_str("│");
214 }
215
216 for i in cells.len()..widths.len() {
218 let w = widths[i];
219 out.push(' ');
220 out.push_str(&" ".repeat(w));
221 out.push(' ');
222 out.push_str(s.border);
223 out.push_str("│");
224 }
225
226 out.push_str(s.reset);
227 out.push('\n');
228}
229
230fn truncate_cell(text: &str, width: usize) -> String {
232 if width == 0 {
233 return String::new();
234 }
235 if text.chars().count() <= width {
236 return text.to_string();
237 }
238 if width <= 1 {
239 return "…".to_string();
240 }
241 let mut out: String = text.chars().take(width - 1).collect();
242 out.push('…');
243 out
244}