1use crate::config::BoxChars;
2
3pub struct TableRenderer {
4 label_width: usize,
5 data_width: usize,
6 chars: BoxChars,
7 total_width: usize,
8}
9
10impl TableRenderer {
11 pub fn new(label_width: usize, data_width: usize, chars: BoxChars) -> Self {
12 let total_width = label_width + data_width + 7;
13 Self {
14 label_width,
15 data_width,
16 chars,
17 total_width,
18 }
19 }
20
21 pub fn render_top_header(&self) -> String {
22 let mut line = String::new();
23 line.push(self.chars.top_left);
24 for _ in 0..(self.total_width - 2) {
25 line.push(self.chars.t_down);
26 }
27 line.push(self.chars.top_right);
28 line.push('\n');
29 line
30 }
31
32 pub fn render_header_bottom(&self) -> String {
33 let mut line = String::new();
34 line.push(self.chars.t_right);
35 for _ in 0..(self.total_width - 2) {
36 line.push(self.chars.t_up);
37 }
38 line.push(self.chars.t_left);
39 line.push('\n');
40 line
41 }
42
43 pub fn render_centered(&self, text: &str) -> String {
44 let text_len = visible_len(text);
45 let inner_width = self.total_width - 2;
46
47 let padding = if text_len >= inner_width {
48 0
49 } else {
50 (inner_width - text_len) / 2
51 };
52 let extra = if text_len >= inner_width {
53 0
54 } else {
55 (inner_width - text_len) % 2
56 };
57
58 let mut line = String::new();
59 line.push(self.chars.vertical);
60 line.push_str(&" ".repeat(padding));
61
62 if text_len > inner_width {
63 let truncated = truncate_visible(text, inner_width.saturating_sub(3));
64 line.push_str(&truncated);
65 line.push_str("...");
66 } else {
67 line.push_str(text);
68 }
69
70 line.push_str(&" ".repeat(padding + extra));
71 line.push(self.chars.vertical);
72 line.push('\n');
73 line
74 }
75
76 pub fn render_full_top(&self) -> String {
77 let mut line = String::new();
78 line.push(self.chars.top_left);
79 for _ in 0..(self.total_width - 2) {
80 line.push(self.chars.horizontal);
81 }
82 line.push(self.chars.top_right);
83 line.push('\n');
84 line
85 }
86
87 pub fn render_top_divider(&self) -> String {
88 let mut line = String::new();
89 line.push(self.chars.t_right);
90 for _ in 0..(self.label_width + 2) {
91 line.push(self.chars.horizontal);
92 }
93 line.push(self.chars.t_down);
94 for _ in 0..(self.data_width + 2) {
95 line.push(self.chars.horizontal);
96 }
97 line.push(self.chars.t_left);
98 line.push('\n');
99 line
100 }
101
102 pub fn render_middle_divider(&self) -> String {
103 let mut line = String::new();
104 line.push(self.chars.t_right);
105 for _ in 0..(self.label_width + 2) {
106 line.push(self.chars.horizontal);
107 }
108 line.push(self.chars.cross);
109 for _ in 0..(self.data_width + 2) {
110 line.push(self.chars.horizontal);
111 }
112 line.push(self.chars.t_left);
113 line.push('\n');
114 line
115 }
116
117 pub fn render_bottom_divider(&self) -> String {
118 let mut line = String::new();
119 line.push(self.chars.t_right);
120 for _ in 0..(self.label_width + 2) {
121 line.push(self.chars.horizontal);
122 }
123 line.push(self.chars.t_up);
124 for _ in 0..(self.data_width + 2) {
125 line.push(self.chars.horizontal);
126 }
127 line.push(self.chars.t_left);
128 line.push('\n');
129 line
130 }
131
132 pub fn render_footer(&self) -> String {
133 let mut line = String::new();
134 line.push(self.chars.bottom_left);
135 for _ in 0..(self.label_width + 2) {
136 line.push(self.chars.horizontal);
137 }
138 line.push(self.chars.t_up);
139 for _ in 0..(self.data_width + 2) {
140 line.push(self.chars.horizontal);
141 }
142 line.push(self.chars.bottom_right);
143 line.push('\n');
144 line
145 }
146
147 pub fn render_full_divider(&self) -> String {
148 let mut line = String::new();
149 line.push(self.chars.t_right);
150 for _ in 0..(self.total_width - 2) {
151 line.push(self.chars.horizontal);
152 }
153 line.push(self.chars.t_left);
154 line.push('\n');
155 line
156 }
157
158 pub fn render_full_bottom(&self) -> String {
159 let mut line = String::new();
160 line.push(self.chars.bottom_left);
161 for _ in 0..(self.total_width - 2) {
162 line.push(self.chars.horizontal);
163 }
164 line.push(self.chars.bottom_right);
165 line.push('\n');
166 line
167 }
168
169 pub fn render_row(&self, label: &str, value: &str) -> String {
170 let label_display = fit_string(label, self.label_width);
171 let value_display = fit_string(value, self.data_width);
172
173 let mut line = String::new();
174 line.push(self.chars.vertical);
175 line.push(' ');
176 line.push_str(&label_display);
177 line.push(' ');
178 line.push(self.chars.vertical);
179 line.push(' ');
180 line.push_str(&value_display);
181 line.push(' ');
182 line.push(self.chars.vertical);
183 line.push('\n');
184 line
185 }
186
187 pub fn render_span_row(&self, text: &str) -> String {
188 let inner = self.total_width - 2;
189 let display = fit_string(text, inner);
190
191 let mut line = String::new();
192 line.push(self.chars.vertical);
193 line.push_str(&display);
194 line.push(self.chars.vertical);
195 line.push('\n');
196 line
197 }
198
199 pub fn total_width(&self) -> usize {
200 self.total_width
201 }
202
203 pub fn label_width(&self) -> usize {
204 self.label_width
205 }
206
207 pub fn data_width(&self) -> usize {
208 self.data_width
209 }
210}
211
212pub fn visible_len(s: &str) -> usize {
214 let mut count = 0;
215 let mut in_escape = false;
216 for c in s.chars() {
217 if c == '\x1b' {
218 in_escape = true;
219 } else if in_escape {
220 if c == 'm' {
221 in_escape = false;
222 }
223 } else {
224 count += 1;
225 }
226 }
227 count
228}
229
230pub fn fit_string(s: &str, width: usize) -> String {
231 let vis_len = visible_len(s);
232 if vis_len > width {
233 if width <= 3 {
235 truncate_visible(s, width)
236 } else {
237 let truncated = truncate_visible(s, width - 3);
238 if s.contains('\x1b') {
240 format!("{}\x1b[0m...", truncated)
241 } else {
242 format!("{}...", truncated)
243 }
244 }
245 } else {
246 let padding = width - vis_len;
248 format!("{}{}", s, " ".repeat(padding))
249 }
250}
251
252fn truncate_visible(s: &str, max_visible: usize) -> String {
254 let mut result = String::new();
255 let mut visible_count = 0;
256 let mut in_escape = false;
257
258 for c in s.chars() {
259 if c == '\x1b' {
260 in_escape = true;
261 result.push(c);
262 } else if in_escape {
263 result.push(c);
264 if c == 'm' {
265 in_escape = false;
266 }
267 } else {
268 if visible_count >= max_visible {
269 break;
270 }
271 result.push(c);
272 visible_count += 1;
273 }
274 }
275
276 result
277}
278
279pub struct ReportBuilder {
280 renderer: TableRenderer,
281 output: String,
282}
283
284impl ReportBuilder {
285 pub fn new(label_width: usize, data_width: usize, chars: BoxChars) -> Self {
286 Self {
287 renderer: TableRenderer::new(label_width, data_width, chars),
288 output: String::new(),
289 }
290 }
291
292 pub fn header(mut self, title: &str, subtitle: &str) -> Self {
293 self.output.push_str(&self.renderer.render_top_header());
294 self.output.push_str(&self.renderer.render_header_bottom());
295 self.output.push_str(&self.renderer.render_centered(title));
296 self.output
297 .push_str(&self.renderer.render_centered(subtitle));
298 self.output.push_str(&self.renderer.render_top_divider());
299 self
300 }
301
302 pub fn section_header(mut self, text: &str) -> Self {
303 self.output.push_str(&self.renderer.render_bottom_divider());
304 self.output
305 .push_str(&self.renderer.render_span_row(&format!(" {}", text)));
306 self.output.push_str(&self.renderer.render_top_divider());
307 self
308 }
309
310 pub fn row(mut self, label: &str, value: &str) -> Self {
311 self.output
312 .push_str(&self.renderer.render_row(label, value));
313 self
314 }
315
316 pub fn divider(mut self) -> Self {
317 self.output.push_str(&self.renderer.render_middle_divider());
318 self
319 }
320
321 pub fn full_top_border(mut self) -> Self {
322 self.output.push_str(&self.renderer.render_full_top());
323 self
324 }
325
326 pub fn span_row(mut self, text: &str) -> Self {
327 self.output.push_str(&self.renderer.render_span_row(text));
328 self
329 }
330
331 pub fn finish(mut self) -> String {
332 self.output.push_str(&self.renderer.render_bottom_divider());
333 self.output.push_str(&self.renderer.render_full_bottom());
334 self.output
335 }
336
337 pub fn build(self) -> String {
338 self.output
339 }
340
341 pub fn renderer(&self) -> &TableRenderer {
342 &self.renderer
343 }
344}