a3s-tui 0.1.7

TEA (The Elm Architecture) framework for terminal user interfaces
Documentation
use crate::element::{BoxElement, Element, FlexDirection, TextElement};
use crate::style::{pad_visible, visible_len, Color};

pub struct Table {
    headers: Vec<String>,
    rows: Vec<Vec<String>>,
    col_widths: Vec<usize>,
}

impl Table {
    pub fn new(headers: Vec<impl Into<String>>) -> Self {
        let headers: Vec<String> = headers.into_iter().map(|h| h.into()).collect();
        let col_widths = headers.iter().map(|h| visible_len(h)).collect();
        Self {
            headers,
            rows: Vec::new(),
            col_widths,
        }
    }

    pub fn row(mut self, cells: Vec<impl Into<String>>) -> Self {
        let row: Vec<String> = cells.into_iter().map(|c| c.into()).collect();
        self.update_col_widths(&row);
        self.rows.push(row);
        self
    }

    pub fn add_row(&mut self, cells: Vec<impl Into<String>>) {
        let row: Vec<String> = cells.into_iter().map(|c| c.into()).collect();
        self.update_col_widths(&row);
        self.rows.push(row);
    }

    pub fn element<Msg>(&self) -> Element<Msg> {
        let mut lines: Vec<Element<Msg>> = Vec::new();

        let header_text = self.format_row(&self.headers);
        lines.push(Element::Text(
            TextElement::new(header_text).bold().fg(Color::BrightWhite),
        ));

        let separator = self
            .col_widths
            .iter()
            .map(|w| "".repeat(w.saturating_add(2)))
            .collect::<Vec<_>>()
            .join("");
        lines.push(Element::Text(
            TextElement::new(separator).fg(Color::BrightBlack),
        ));

        for row in &self.rows {
            let row_text = self.format_row(row);
            lines.push(Element::Text(TextElement::new(row_text)));
        }

        Element::Box(
            BoxElement::new()
                .direction(FlexDirection::Column)
                .children(lines),
        )
    }

    fn update_col_widths(&mut self, row: &[String]) {
        if row.len() > self.col_widths.len() {
            self.col_widths.resize(row.len(), 0);
        }
        for (i, cell) in row.iter().enumerate() {
            self.col_widths[i] = self.col_widths[i].max(visible_len(cell));
        }
    }

    fn format_row(&self, cells: &[String]) -> String {
        let column_count = self.col_widths.len().max(cells.len());
        (0..column_count)
            .map(|i| {
                let cell = cells.get(i).map(String::as_str).unwrap_or("");
                let width = self
                    .col_widths
                    .get(i)
                    .copied()
                    .unwrap_or_else(|| visible_len(cell));
                format!(" {} ", pad_visible(cell, width))
            })
            .collect::<Vec<_>>()
            .join("")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::style::visible_len;

    #[test]
    fn empty_table() {
        let table = Table::new(vec!["A", "B"]);
        let el: Element<()> = table.element();
        match el {
            Element::Box(b) => {
                // header + separator = 2 lines
                assert_eq!(b.children.len(), 2);
            }
            _ => panic!("expected Box"),
        }
    }

    #[test]
    fn table_with_rows() {
        let table = Table::new(vec!["Name", "Age"])
            .row(vec!["Alice", "30"])
            .row(vec!["Bob", "25"]);
        let el: Element<()> = table.element();
        match el {
            Element::Box(b) => {
                // header + separator + 2 rows = 4
                assert_eq!(b.children.len(), 4);
            }
            _ => panic!("expected Box"),
        }
    }

    #[test]
    fn col_widths_expand() {
        let table = Table::new(vec!["X"]).row(vec!["longer text"]);
        assert_eq!(table.col_widths[0], 11);
    }

    #[test]
    fn columns_align_wide_cells_by_display_width() {
        let table = Table::new(vec!["文件", "State"]).row(vec!["", "ok"]);
        let header = table.format_row(&table.headers);
        let row = table.format_row(&table.rows[0]);
        let header_first_col = header.split('').next().unwrap();
        let row_first_col = row.split('').next().unwrap();

        assert_eq!(visible_len(header_first_col), visible_len(row_first_col));
    }

    #[test]
    fn rows_with_missing_cells_preserve_columns() {
        let table = Table::new(vec!["A", "B"]).row(vec!["x"]);
        let header = table.format_row(&table.headers);
        let row = table.format_row(&table.rows[0]);

        assert_eq!(visible_len(&row), visible_len(&header));
        assert_eq!(row.split('').count(), 2);
    }

    #[test]
    fn rows_with_extra_cells_expand_columns() {
        let table = Table::new(vec!["A"]).row(vec!["x", "extra"]);
        let Element::Box(box_el) = table.element::<()>() else {
            panic!("expected Box");
        };
        let Element::Text(header) = &box_el.children[0] else {
            panic!("expected header");
        };
        let Element::Text(separator) = &box_el.children[1] else {
            panic!("expected separator");
        };
        let Element::Text(row) = &box_el.children[2] else {
            panic!("expected row");
        };

        assert_eq!(table.col_widths, vec![1, 5]);
        assert_eq!(visible_len(&header.content), visible_len(&row.content));
        assert_eq!(visible_len(&separator.content), visible_len(&row.content));
        assert_eq!(row.content.split('').count(), 2);
    }

    #[test]
    fn add_row_method() {
        let mut table = Table::new(vec!["Col"]);
        table.add_row(vec!["val"]);
        let el: Element<()> = table.element();
        match el {
            Element::Box(b) => assert_eq!(b.children.len(), 3),
            _ => panic!("expected Box"),
        }
    }
}