1use console::measure_text_width;
2use terminal_size::{terminal_size, Width};
3
4pub enum TableMode {
5 Flex,
6 Fixed(usize),
7 Full,
8}
9
10pub enum TableStyle {
11 Ascii,
12 Rounded,
13 Heavy,
14}
15
16pub fn render_table(
17 headers: &[&str],
18 rows: &[Vec<&str>],
19 mode: TableMode,
20 style: TableStyle,
21) -> String {
22 let term_width = terminal_size()
23 .map(|(Width(w), _)| w as usize)
24 .unwrap_or(80);
25 let col_count = headers.len().max(1);
26 let padding = 1;
27 let total_padding = (col_count - 1) * padding;
28
29 let col_width = match mode {
30 TableMode::Fixed(width) => width,
31 TableMode::Full => {
32 let border_space = col_count + 1; let usable = term_width.saturating_sub(border_space);
34 usable / col_count
35 }
36 TableMode::Flex => {
37 let content_max = headers
38 .iter()
39 .map(|h| measure_text_width(h))
40 .chain(
41 rows.iter()
42 .flat_map(|r| r.iter().map(|c| measure_text_width(c))),
43 )
44 .max()
45 .unwrap_or(10);
46 content_max.min((term_width.saturating_sub(total_padding)) / col_count)
47 }
48 };
49
50 let border = match style {
51 TableStyle::Ascii => BorderSet::ascii(),
52 TableStyle::Rounded => BorderSet::rounded(),
53 TableStyle::Heavy => BorderSet::heavy(),
54 };
55
56 let mut out = String::with_capacity(128);
57
58 out.push(border.top_left);
60 for i in 0..col_count {
61 out.push_str(&border.horizontal.to_string().repeat(col_width));
62 if i < col_count - 1 {
63 out.push(border.top_cross);
64 }
65 }
66 out.push(border.top_right);
67 out.push('\n');
68
69 out.push(border.vertical);
71 for h in headers.iter() {
72 out.push_str(&pad_cell(h, col_width));
73 out.push(border.vertical);
74 }
75 out.push('\n');
76
77 out.push(border.mid_left);
79 for i in 0..col_count {
80 out.push_str(&border.inner_horizontal.to_string().repeat(col_width));
81 if i < col_count - 1 {
82 out.push(border.mid_cross);
83 }
84 }
85 out.push(border.mid_right);
86 out.push('\n');
87
88 for row in rows {
90 out.push(border.vertical);
91 for cell in row {
92 out.push_str(&pad_cell(cell, col_width));
93 out.push(border.vertical);
94 }
95 out.push('\n');
96 }
97
98 out.push(border.bottom_left);
100 for i in 0..col_count {
101 out.push_str(&border.horizontal.to_string().repeat(col_width));
102 if i < col_count - 1 {
103 out.push(border.bottom_cross);
104 }
105 }
106 out.push(border.bottom_right);
107 out.push('\n');
108
109 out
110}
111
112fn pad_cell(cell: &str, width: usize) -> String {
115 let truncated = truncate_to_width(cell, width);
116 let visual = measure_text_width(&truncated);
117 let pad = width.saturating_sub(visual);
118 format!("{truncated}{}", " ".repeat(pad))
119}
120
121fn truncate_to_width(cell: &str, width: usize) -> String {
124 if width == 0 {
125 return String::new();
126 }
127 let visual = measure_text_width(cell);
128 if visual <= width {
129 return cell.to_string();
130 }
131
132 let mut out = String::new();
133 let target = width.saturating_sub(1);
135 for ch in cell.chars() {
136 let next = format!("{out}{ch}");
137 if measure_text_width(&next) > target {
138 break;
139 }
140 out.push(ch);
141 }
142 out.push('…');
143 out
144}
145
146struct BorderSet {
147 top_left: char,
148 top_right: char,
149 bottom_left: char,
150 bottom_right: char,
151 top_cross: char,
152 bottom_cross: char,
153 mid_cross: char,
154 mid_left: char,
155 mid_right: char,
156 horizontal: char,
157 inner_horizontal: char,
158 vertical: char,
159}
160
161impl BorderSet {
162 fn ascii() -> Self {
163 Self {
164 top_left: '+',
165 top_right: '+',
166 bottom_left: '+',
167 bottom_right: '+',
168 top_cross: '+',
169 bottom_cross: '+',
170 mid_cross: '+',
171 mid_left: '+',
172 mid_right: '+',
173 horizontal: '-',
174 inner_horizontal: '-',
175 vertical: '|',
176 }
177 }
178
179 fn rounded() -> Self {
180 Self {
181 top_left: '╭',
182 top_right: '╮',
183 bottom_left: '╰',
184 bottom_right: '╯',
185 top_cross: '┬',
186 bottom_cross: '┴',
187 mid_cross: '┼',
188 mid_left: '├',
189 mid_right: '┤',
190 horizontal: '─',
191 inner_horizontal: '─',
192 vertical: '│',
193 }
194 }
195
196 fn heavy() -> Self {
197 Self {
198 top_left: '┏',
199 top_right: '┓',
200 bottom_left: '┗',
201 bottom_right: '┛',
202 top_cross: '┳',
203 bottom_cross: '┻',
204 mid_cross: '╋',
205 mid_left: '┣',
206 mid_right: '┫',
207 horizontal: '━',
208 inner_horizontal: '━',
209 vertical: '┃',
210 }
211 }
212}