Skip to main content

nd_300/render/
table.rs

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
212/// Count visible characters (excluding ANSI escape sequences)
213pub 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        // Truncate by visible characters, preserving escape sequences
234        if width <= 3 {
235            truncate_visible(s, width)
236        } else {
237            let truncated = truncate_visible(s, width - 3);
238            // Append reset + ellipsis if string had colors
239            if s.contains('\x1b') {
240                format!("{}\x1b[0m...", truncated)
241            } else {
242                format!("{}...", truncated)
243            }
244        }
245    } else {
246        // Pad with spaces based on visible length
247        let padding = width - vis_len;
248        format!("{}{}", s, " ".repeat(padding))
249    }
250}
251
252/// Truncate a string to `max_visible` visible characters, preserving ANSI escapes
253fn 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}