alixt_table/
table.rs

1use colored::{ColoredString, Colorize};
2use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
3
4use crate::TableError;
5
6pub const TOP_LEFT: &str = "╭";
7pub const TOP_RIGHT: &str = "╮";
8pub const BOTTOM_LEFT: &str = "╰";
9pub const BOTTOM_RIGHT: &str = "╯";
10pub const HORIZONTAL: &str = "─";
11pub const VERTICAL: &str = "│";
12pub const CROSS: &str = "┼";
13pub const TOP_T: &str = "┬";
14pub const BOTTOM_T: &str = "┴";
15pub const LEFT_T: &str = "├";
16pub const RIGHT_T: &str = "┤";
17
18pub const SPACES: &str = "                                                                                                                               ";
19
20pub enum TablePartial<const N: usize> {
21    Table(Table<N>),
22    TableError(TableError)
23}
24
25impl<const N: usize> TablePartial<N> {
26    /// Example:
27    /// let Table::<3>::new().title("My table".red())
28    pub fn title(self, title: ColoredString) -> Self {
29        match self {
30            Self::Table(mut table) => {
31                table.title = title;
32                Self::Table(table)
33            }
34            Self::TableError(err) => Self::TableError(err),
35        }
36    }
37
38    pub fn headers(self, headers: [ColoredString; N]) -> TableResult<N> {
39        match self {
40            Self::Table(mut table) => {
41                for (i, header) in headers.into_iter().enumerate() {
42                    if header.width() > table.col_widths[i] {
43                        table.col_widths[i] = header.width();
44                    }
45                    table.headers[i] = header;
46                }
47                TableResult::Table(table)
48            }
49            Self::TableError(err) => TableResult::TableError(err),
50        }
51    }
52}
53
54pub enum TableResult<const N: usize> {
55    Table(Table<N>),
56    TableError(TableError),
57}
58
59
60impl<const N: usize> TableResult<N> {
61    /// Example:
62    /// let Table::<3>::new().headers(["my".blue(), "cool".blue(), "headers".blue()]).title("My table".red())
63    pub fn title(self, title: ColoredString) -> Self {
64        match self {
65            Self::Table(mut table) => {
66                table.title = title;
67                Self::Table(table)
68            }
69            Self::TableError(err) => Self::TableError(err),
70        }
71    }
72    pub fn row(self, row: [ColoredString; N]) -> Self {
73        match self {
74            Self::Table(mut table) => {
75                for (i, cell) in row.into_iter().enumerate() {
76                    if cell.width() > table.col_widths[i] {
77                        table.col_widths[i] = cell.width();
78                    }
79                    table.cells.push(cell);
80                }
81                table.rows += 1;
82                TableResult::Table(table)
83            }
84            err => err,
85        }
86    }
87
88    pub fn collect(self) -> Result<Table<N>, TableError> {
89        match self {
90            Self::Table(table) => Ok(table),
91            Self::TableError(err) => Err(err),
92        }
93    }
94}
95
96pub struct Table<const N: usize> {
97    title: ColoredString,
98    rows: usize,
99    col_widths: [usize; N],
100    headers: [ColoredString; N],
101    cells: Vec<ColoredString>,
102}
103
104impl<const N: usize> Table<N> {
105    /// Table is created with a fixed number of columns
106    #[allow(clippy::new_ret_no_self)]
107    pub fn new() -> TablePartial<N> {
108        TablePartial::Table(Self {
109            title: "".white(),
110            rows: 0,
111            col_widths: [0; N],
112            headers: std::array::from_fn(|_| "".white()),
113            cells: Vec::new(),
114        })
115    }
116    pub fn headers(&mut self, headers: [ColoredString; N]) -> Result<(), TableError> {
117        for (i, header) in headers.into_iter().enumerate() {
118            if header.width() > self.col_widths[i] {
119                self.col_widths[i] = header.width();
120            }
121            self.headers[i] = header;
122        }
123        Ok(())
124    }
125
126    pub fn push_row(&mut self, row: [ColoredString; N]) -> Result<(), TableError> {
127        for (i, cell) in row.into_iter().enumerate() {
128            if cell.width() > self.col_widths[i] {
129                self.col_widths[i] = cell.width();
130            }
131            self.cells.push(cell);
132        }
133        self.rows += 1;
134        Ok(())
135    }
136
137    pub fn get_title(&self) -> Option<&ColoredString> {
138        if self.title.is_empty() {
139            None
140        } else {
141            Some(&self.title)
142        }
143    }
144
145    pub fn get_row_count(&self) -> usize {
146        self.rows
147    }
148
149    pub fn get_column_count(&self) -> usize {
150        N
151    }
152
153    pub fn get_headers(&self) -> &[ColoredString] {
154        &self.headers
155    }
156
157    pub fn get_col_widths(&self) -> &[usize] {
158        &self.col_widths
159    }
160
161    pub fn get_row(&self, index: usize) -> Option<&[ColoredString]> {
162        if index >= self.rows {
163            return None;
164        }
165        Some(&self.cells[index * N..(index * N) + N])
166    }
167
168    pub fn padding(&self, spacing: usize) -> &str {
169        &SPACES[..std::cmp::min(spacing, SPACES.len())]
170    }
171
172    pub fn content_width(&self) -> usize {
173        self.get_col_widths().iter().sum()
174    }
175
176    fn safe_truncate(&self, input: &str, max_width: usize) -> String {
177        let mut width = 0;
178        let mut result = String::new();
179
180        for c in input.chars() {
181            let w = c.width().unwrap_or(0);
182            if width + w > max_width {
183                break;
184            }
185            width += w;
186            result.push(c);
187        }
188        result
189    }
190
191    fn _render_top<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
192        let table_width = self.content_width() + self.get_column_count() - 1;
193
194        writeln!(
195            w,
196            "\n{}{}{}",
197            TOP_LEFT.blue(),
198            HORIZONTAL.repeat(table_width).blue(),
199            TOP_RIGHT.blue()
200        )?;
201
202        Ok(())
203    }
204
205    fn _render_title<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
206        let table_width = self.content_width() + self.get_column_count() - 1;
207
208        if let Some(title) = self.get_title() {
209            write!(w, "{}", VERTICAL.blue())?;
210            // if title is longer than table width, truncate to be 5 chars shorter
211            // than the table, to account for the border chars and the '...'
212            if title.width() > table_width - 2 {
213                writeln!(
214                    w,
215                    "{}...{}",
216                    /*self.title[..table_width - 3]*/
217                    self.safe_truncate(title, table_width - 3)
218                        .color(self.title.fgcolor.unwrap_or(colored::Color::White)),
219                    VERTICAL.blue(),
220                )?;
221            } else {
222                let pad_l = self.padding((table_width - title.width()) / 2);
223                let pad_r =
224                    self.padding((table_width - title.width()) - (table_width - title.width()) / 2);
225
226                writeln!(w, "{}{}{}{}", pad_l, self.title, pad_r, VERTICAL.blue(),)?;
227            }
228        }
229        Ok(())
230    }
231
232    fn _render_headers<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
233        write!(
234            w,
235            "{}{}",
236            LEFT_T.blue(),
237            HORIZONTAL.repeat(self.get_col_widths()[0]).blue()
238        )?;
239        for i in 1..self.get_column_count() {
240            write!(
241                w,
242                "{}{}",
243                TOP_T.blue(),
244                HORIZONTAL.repeat(self.get_col_widths()[i]).blue()
245            )?;
246        }
247        writeln!(w, "{}", RIGHT_T.blue())?;
248        write!(w, "{}", VERTICAL.blue())?;
249        for (i, header) in self.get_headers().iter().enumerate() {
250            write!(
251                w,
252                "{}{}{}",
253                header,
254                self.padding(self.get_col_widths()[i] - header.width()),
255                VERTICAL.blue()
256            )?;
257        }
258        write!(
259            w,
260            "\n{}{}",
261            LEFT_T.blue(),
262            HORIZONTAL.repeat(self.get_col_widths()[0]).blue()
263        )?;
264        for i in 1..self.get_column_count() {
265            write!(
266                w,
267                "{}{}",
268                CROSS.blue(),
269                HORIZONTAL.repeat(self.get_col_widths()[i]).blue()
270            )?;
271        }
272        writeln!(w, "{}", RIGHT_T.blue())?;
273
274        Ok(())
275    }
276
277    fn _render_row<W: std::io::Write>(&self, w: &mut W, row: usize) -> Result<(), TableError> {
278        write!(w, "{}", VERTICAL.blue())?;
279        let Some(row_data) = self.get_row(row) else {
280            return Err(TableError::InputError(
281                "invalid row index given".to_string(),
282            ));
283        };
284        for (i, cell) in row_data.iter().enumerate() {
285            write!(
286                w,
287                "{}{}{}",
288                cell,
289                self.padding(self.get_col_widths()[i] - cell.width()),
290                VERTICAL.blue()
291            )?;
292        }
293        writeln!(w)?;
294        Ok(())
295    }
296
297    fn _render_bottom<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
298        write!(
299            w,
300            "{}{}",
301            BOTTOM_LEFT.blue(),
302            HORIZONTAL.repeat(self.get_col_widths()[0]).blue()
303        )?;
304        for i in 1..self.get_column_count() {
305            write!(
306                w,
307                "{}{}",
308                BOTTOM_T.blue(),
309                HORIZONTAL.repeat(self.get_col_widths()[i]).blue()
310            )?;
311        }
312        writeln!(w, "{}", BOTTOM_RIGHT.blue())?;
313        Ok(())
314    }
315
316    pub fn render<W: std::io::Write>(&self, w: &mut W) -> Result<(), TableError> {
317        self._render_top(w)?;
318        self._render_title(w)?;
319        self._render_headers(w)?;
320        for row in 0..self.get_row_count() {
321            self._render_row(w, row)?;
322        }
323        self._render_bottom(w)?;
324        Ok(())
325    }
326}