modcli/output/
table.rs

1use terminal_size::{terminal_size, Width};
2use console::measure_text_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().map(|(Width(w), _)| w as usize).unwrap_or(80);
18    let col_count = headers.len().max(1);
19    let padding = 1;
20    let total_padding = (col_count - 1) * padding;
21
22    let col_width = match mode {
23        TableMode::Fixed(width) => width,
24        TableMode::Full => {
25            let border_space = col_count + 1; // ┏┃┃┃┓ = 4 columns + 2 sides = 5 chars
26            let usable = term_width.saturating_sub(border_space);
27            usable / col_count
28        },
29        TableMode::Flex => {
30            let content_max = headers.iter()
31                .map(|h| measure_text_width(h))
32                .chain(rows.iter().flat_map(|r| r.iter().map(|c| measure_text_width(c))))
33                .max()
34                .unwrap_or(10);
35            content_max.min((term_width.saturating_sub(total_padding)) / col_count)
36        }
37    };
38
39    let border = match style {
40        TableStyle::Ascii => BorderSet::ascii(),
41        TableStyle::Rounded => BorderSet::rounded(),
42        TableStyle::Heavy => BorderSet::heavy(),
43    };
44
45    // Top Border
46    print!("{}", border.top_left);
47    for i in 0..col_count {
48        print!("{}", border.horizontal.to_string().repeat(col_width));
49        if i < col_count - 1 {
50            print!("{}", border.top_cross);
51        }
52    }
53    println!("{}", border.top_right);
54
55    // Header Row
56    print!("{}", border.vertical);
57    for (_i, h) in headers.iter().enumerate() {
58        print!("{}{}", pad_cell(h, col_width), border.vertical);
59    }
60    println!();
61
62    // Mid Border
63    print!("{}", border.mid_left);
64    for i in 0..col_count {
65        print!("{}", border.inner_horizontal.to_string().repeat(col_width));
66        if i < col_count - 1 {
67            print!("{}", border.mid_cross);
68        }
69    }
70    println!("{}", border.mid_right);
71
72    // Body Rows
73    for row in rows {
74        print!("{}", border.vertical);
75        for cell in row {
76            print!("{}{}", pad_cell(cell, col_width), border.vertical);
77        }
78        println!();
79    }
80
81    // Bottom Border
82    print!("{}", border.bottom_left);
83    for i in 0..col_count {
84        print!("{}", border.horizontal.to_string().repeat(col_width));
85        if i < col_count - 1 {
86            print!("{}", border.bottom_cross);
87        }
88    }
89    println!("{}", border.bottom_right);
90}
91
92fn pad_cell(cell: &str, width: usize) -> String {
93    let visual = measure_text_width(cell);
94    let pad = width.saturating_sub(visual);
95    format!("{}{}", cell, " ".repeat(pad))
96}
97
98struct BorderSet {
99    top_left: char,
100    top_right: char,
101    bottom_left: char,
102    bottom_right: char,
103    top_cross: char,
104    bottom_cross: char,
105    mid_cross: char,
106    mid_left: char,
107    mid_right: char,
108    horizontal: char,
109    inner_horizontal: char,
110    vertical: char,
111}
112
113impl BorderSet {
114    fn ascii() -> Self {
115        Self {
116            top_left: '+',
117            top_right: '+',
118            bottom_left: '+',
119            bottom_right: '+',
120            top_cross: '+',
121            bottom_cross: '+',
122            mid_cross: '+',
123            mid_left: '+',
124            mid_right: '+',
125            horizontal: '-',
126            inner_horizontal: '-',
127            vertical: '|',
128        }
129    }
130
131    fn rounded() -> Self {
132        Self {
133            top_left: '╭',
134            top_right: '╮',
135            bottom_left: '╰',
136            bottom_right: '╯',
137            top_cross: '┬',
138            bottom_cross: '┴',
139            mid_cross: '┼',
140            mid_left: '├',
141            mid_right: '┤',
142            horizontal: '─',
143            inner_horizontal: '─',
144            vertical: '│',
145        }
146    }
147
148    fn heavy() -> 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}