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