tabular2 1.0.0

A simple library for displaying tabular data in the terminal
Documentation
use std::marker::PhantomData;

pub struct ModifyHeader;

pub struct ModifyRows;

pub struct Table<T = ModifyHeader, const N: usize = 0> {
    headers: Vec<Header>,
    column_widths: Vec<usize>,
    rows: Vec<Vec<String>>,
    skip_header: bool,
    _pd: PhantomData<T>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
    Left,
    Right,
    Center,
}

pub struct Header {
    pub text: String,
    pub alignment: Alignment,
}

impl Into<Header> for &str {
    fn into(self) -> Header {
        Header {
            text: self.to_string(),
            alignment: Alignment::Left,
        }
    }
}

fn width(s: &str) -> usize {
    let bytes = strip_ansi_escapes::strip(&s).expect("Failed to strip escape sequences");
    let s = unsafe { std::str::from_utf8_unchecked(&bytes) };
    unicode_width::UnicodeWidthStr::width(s)
}

impl<T, const N: usize> Table<T, N> {
    pub fn update_widths(&mut self, row: &Row<N>) {
        for (w, cell) in self.column_widths.iter_mut()
            .zip(row.cells.iter()) {
            *w = std::cmp::max(*w, width(&cell));
        }
    }
}

impl<const N: usize> Table<ModifyHeader, N> {
    pub fn new() -> Self {
        Table {
            headers: Vec::new(),
            column_widths: Vec::new(),
            rows: Vec::new(),
            skip_header: false,
            _pd: PhantomData,
        }
    }

    pub fn header<H: Into<Header>>(mut self, header: H) -> Table<ModifyHeader, N> {
        let header = header.into();
        let width = width(&header.text);
        self.headers.push(header);
        self.column_widths.push(width);
        Table {
            headers: self.headers,
            column_widths: self.column_widths,
            rows: self.rows,
            skip_header: self.skip_header,
            _pd: PhantomData,
        }
    }

    pub fn row(mut self, row: Row<N>) -> Table<ModifyRows, N> {
        self.update_widths(&row);
        Table {
            headers: self.headers,
            column_widths: self.column_widths,
            rows: vec![row.cells],
            skip_header: self.skip_header,
            _pd: PhantomData,
        }
    }

    pub fn end_header(self) -> Table<ModifyRows, N> {
        Table {
            headers: self.headers,
            column_widths: self.column_widths,
            rows: Vec::new(),
            skip_header: self.skip_header,
            _pd: PhantomData,
        }
    }
}

impl<const N: usize> Table<ModifyRows, N> {
    pub fn row(mut self, row: Row<N>) -> Self {
        self.update_widths(&row);
        self.rows.push(row.cells);
        self
    }
}

fn format(s: &str, target_width: usize, alignment: Alignment) -> String {
    let width = width(s);
    let target_width = std::cmp::max(target_width, 8);
    let padding = target_width - width;
    match alignment {
        Alignment::Left => s.to_string() + &" ".repeat(padding),
        Alignment::Right => " ".repeat(padding) + s,
        Alignment::Center => {
            let left = padding / 2;
            let right = padding - left;
            " ".repeat(left) + s + &" ".repeat(right)
        }
    }
}

impl std::fmt::Display for Table<ModifyRows> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        if !self.skip_header {
            for (header, width) in self.headers.iter()
                .zip(self.column_widths.iter()) {
                let header = format(&header.text, *width, header.alignment);
                write!(f, "{} ", header)?;
            }
            writeln!(f)?;
        }
        for row in self.rows.iter() {
            for (cell, width) in row.iter()
                .zip(self.column_widths.iter()) {
                let cell = format(cell, *width, Alignment::Left);
                write!(f, "{cell} ")?;
            }
            writeln!(f)?;
        }
        Ok(())
    }
}

pub struct Row<const N: usize> {
    cells: Vec<String>,
}

impl<const N: usize> Row<N> {
    pub fn new() -> Self {
        Row {
            cells: Vec::new(),
        }
    }

    pub fn cell(mut self, cell: &str) -> Row<N> {
        self.cells.push(cell.to_string());
        Row {
            cells: self.cells,
        }
    }
}

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

    #[test]
    fn it_works() {
        let table = Table::new()
            .header("Name")
            .header("Age")
            .row(Row::new().cell("Alice").cell("20"))
            .row(Row::new().cell("Bob").cell("30"));
        println!("{}", table);
        assert_eq!(table.to_string(),
                   "Name     Age      \n".to_owned() +
                       "Alice    20       \n" +
                       "Bob      30       \n");
    }
}