use std::cmp::{max, min};
use std::collections::HashMap;
use std::fmt;
use std::fmt::Formatter;
use unicode_width::UnicodeWidthChar;
#[derive(Clone, Debug)]
pub struct Table<'a> {
rows: Vec<Row>,
style: &'a Style,
invert_header: bool,
border_attr: Attributes,
table_attr: Option<Attributes>,
col_alt_attr: [Option<Attributes>; 2],
col_attr: HashMap<usize, Attributes>,
row_alt_attr: [Option<Attributes>; 2],
row_attr: HashMap<usize, Attributes>,
cell_attr: HashMap<(usize, usize), Attributes>,
table_align: (Horizontal, Vertical),
col_align: HashMap<usize, (Option<Horizontal>, Option<Vertical>)>,
row_align: HashMap<usize, (Option<Horizontal>, Option<Vertical>)>,
cell_align: HashMap<(usize, usize), (Option<Horizontal>, Option<Vertical>)>,
}
#[derive(Clone, Debug)]
struct Row {
cells: Vec<String>,
}
#[repr(usize)]
pub enum StyleIdx {
Left = 0,
Pad = 1,
Right = 2,
Top = 3,
Inter = 4,
Bot = 5,
}
impl StyleIdx {
pub const COUNT: usize = 6;
}
#[derive(Clone, Debug)]
pub struct Style {
pub top: [&'static str; StyleIdx::COUNT],
pub row: [&'static str; StyleIdx::COUNT],
pub sep: [&'static str; StyleIdx::COUNT],
pub bot: [&'static str; StyleIdx::COUNT],
}
#[derive(Clone, Debug)]
pub struct Attributes {
on: String,
off: String,
}
#[derive(Clone, Debug)]
struct LayoutLine {
line: String,
width: usize,
}
#[derive(Clone, Debug)]
struct LayoutCell {
lines: Vec<LayoutLine>,
width: usize,
}
#[derive(Clone, Debug)]
struct LayoutRow {
cells: Vec<LayoutCell>,
height: usize,
}
#[derive(Clone, Debug)]
struct LayoutTable<'a> {
config: &'a Table<'a>,
rows: Vec<LayoutRow>,
widths: Vec<usize>,
}
#[rustfmt::skip]
pub mod styles {
use super::Style;
pub const ASCII_BORDER: Style = Style {
top: ["+", "-", "+", "+", "+", "+"],
row: ["|", "-", "|", "|", "|", "|"],
sep: ["+", "-", "+", "+", "+", "+"],
bot: ["+", "-", "+", "+", "+", "+"],
};
pub const ASCII_NO_BORDER: Style = Style {
top: ["", "", "", "", "", "" ],
row: ["", " ", "", "|", "|", "|"],
sep: ["", "-", "", "+", "+", "+"],
bot: ["", "", "", "", "", "" ],
};
pub const ASCII_NO_ROW_SEPARATOR: Style = Style {
top: ["", "", "", "", "", "" ],
row: ["", " ", "", "|", "|", "|"],
sep: ["", "", "", "", "", "" ],
bot: ["", "", "", "", "", "" ],
};
pub const GLYPHS_SQUARE: Style = Style {
top: ["┌", "─", "┐", "┬", "┼", "┴"],
row: ["│", "─", "│", "│", "│", "│"],
sep: ["├", "─", "┤", "┬", "┼", "┴"],
bot: ["└", "─", "┘", "┬", "┼", "┴"],
};
pub const GLYPHS_ROUNDED: Style = Style {
top: ["╭", "─", "╮", "┬", "┼", "┴"],
row: ["│", "─", "│", "│", "│", "│"],
sep: ["├", "─", "┤", "┬", "┼", "┴"],
bot: ["╰", "─", "╯", "┬", "┼", "┴"],
};
pub const GLYPHS_ROUNDED_SPACED: Style = Style {
top: ["╭─", "─", "─╮", "─┬─", "─┼─", "─┴─"],
row: ["│ ", "─", " │", " │ ", " │ ", " │ "],
sep: ["├─", "─", "─┤", "─┬─", "─┼─", "─┴─"],
bot: ["╰─", "─", "─╯", "─┬─", "─┼─", "─┴─"],
};
pub const GLYPHS_NO_BORDER: Style = Style {
top: ["", "", "", "", "", "" ],
row: ["", "─", "", "│", "│", "│"],
sep: ["", "─", "", "┬", "┼", "┴"],
bot: ["", "", "", "", "", "" ],
};
pub const GLYPHS_NO_ROW_SEPARATOR: Style = Style {
top: ["", "", "", "", "", "" ],
row: ["", " ", "", "│", "│", "│"],
sep: ["", "", "", "", "", "" ],
bot: ["", "", "", "", "", "" ],
};
pub const SPACE: Style = Style {
top: ["", "", "", "", "", ""],
row: ["", " ", "", "", " ", ""],
sep: ["", "", "", "", "", ""],
bot: ["", "", "", "", "", ""],
};
pub const HEAVY: Style = Style {
top: ["┏", "━", "┓", "┳", "╋", "┻"],
row: ["┃", "━", "┃", "┃", "┃", "┃"],
sep: ["┣", "━", "┫", "┳", "╋", "┻"],
bot: ["┗", "━", "┛", "┳", "╋", "┻"],
};
pub const DOUBLE: Style = Style {
top: ["╔", "═", "╗", "╦", "╬", "╩"],
row: ["║", "═", "║", "║", "║", "║"],
sep: ["╠", "═", "╣", "╦", "╬", "╩"],
bot: ["╚", "═", "╝", "╦", "╬", "╩"],
};
pub const HEAVY_MIXED: Style = Style {
top: ["┏", "─", "┓", "┳", "╋", "┻"],
row: ["│", "─", "│", "│", "│", "│"],
sep: ["┣", "─", "┫", "┳", "╋", "┻"],
bot: ["┗", "─", "┛", "┳", "╋", "┻"],
};
pub const DOUBLE_HEAVY: Style = Style {
top: ["╔", "━", "╗", "╦", "╬", "╩"],
row: ["┃", "━", "┃", "┃", "┃", "┃"],
sep: ["╠", "━", "╣", "╦", "╬", "╩"],
bot: ["╚", "━", "╝", "╦", "╬", "╩"],
};
}
#[rustfmt::skip]
pub mod ansi {
pub const RESET: &str = "\x1b[0;0m";
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const ITALIC: &str = "\x1b[3m";
pub const UNDERLINE: &str = "\x1b[4m";
pub const INVERT: &str = "\x1b[7m";
pub const INVERT_OFF: &str = "\x1b[27m";
pub const BLACK: &str = "\x1b[30m";
pub const RED: &str = "\x1b[31m";
pub const GREEN: &str = "\x1b[32m";
pub const YELLOW: &str = "\x1b[33m";
pub const BLUE: &str = "\x1b[34m";
pub const MAGENTA: &str = "\x1b[35m";
pub const CYAN: &str = "\x1b[36m";
pub const WHITE: &str = "\x1b[37m";
pub const BRIGHT_BLACK: &str = "\x1b[90m";
pub const BRIGHT_RED: &str = "\x1b[91m";
pub const BRIGHT_GREEN: &str = "\x1b[92m";
pub const BRIGHT_YELLOW: &str = "\x1b[93m";
pub const BRIGHT_BLUE: &str = "\x1b[94m";
pub const BRIGHT_MAGENTA: &str = "\x1b[95m";
pub const BRIGHT_CYAN: &str = "\x1b[96m";
pub const BRIGHT_WHITE: &str = "\x1b[97m";
pub const BG_BLACK: &str = "\x1b[40m";
pub const BG_RED: &str = "\x1b[41m";
pub const BG_GREEN: &str = "\x1b[42m";
pub const BG_YELLOW: &str = "\x1b[43m";
pub const BG_BLUE: &str = "\x1b[44m";
pub const BG_MAGENTA: &str = "\x1b[45m";
pub const BG_CYAN: &str = "\x1b[46m";
pub const BG_WHITE: &str = "\x1b[47m";
pub const BG_BRIGHT_BLACK: &str = "\x1b[100m";
pub const BG_BRIGHT_RED: &str = "\x1b[101m";
pub const BG_BRIGHT_GREEN: &str = "\x1b[102m";
pub const BG_BRIGHT_YELLOW: &str = "\x1b[103m";
pub const BG_BRIGHT_BLUE: &str = "\x1b[104m";
pub const BG_BRIGHT_MAGENTA: &str = "\x1b[105m";
pub const BG_BRIGHT_CYAN: &str = "\x1b[106m";
pub const BG_BRIGHT_WHITE: &str = "\x1b[107m";
pub fn fg_256(n: u8) -> String {
format!("\x1b[38;5;{}m", n)
}
pub fn fg_rgb(r: u8, g: u8, b: u8) -> String {
format!("\x1b[38;2;{};{};{}m", r, g, b)
}
pub fn bg_256(n: u8) -> String {
format!("\x1b[48;5;{}m", n)
}
pub fn bg_rgb(r: u8, g: u8, b: u8) -> String {
format!("\x1b[48;2;{};{};{}m", r, g, b)
}
}
#[derive(Clone, Debug, Copy)]
pub enum Horizontal {
Left,
Centre,
Right,
}
#[derive(Clone, Debug, Copy)]
pub enum Vertical {
Top,
Centre,
Bottom,
}
fn display_width(input: &str) -> usize {
let mut chars = input.chars().peekable();
let mut width = 0;
while let Some(c) = chars.next() {
if c == '\x1b' {
if let Some('[') = chars.peek().copied() {
chars.next();
let mut found_final = false;
while let Some(next) = chars.next() {
if ('@'..='~').contains(&next) {
found_final = true;
break;
}
}
if !found_final {
eprintln!("Warning: unterminated ANSI escape sequence");
}
continue;
} else {
eprintln!("Warning: unsupported ANSI escape sequence");
continue;
}
}
match UnicodeWidthChar::width(c) {
Some(w) => width += w,
None => {
let double = ('\u{1F300}'..='\u{1FAFF}').contains(&c);
width += if double { 2 } else { 1 };
}
}
}
width
}
impl Row {
fn new<I, S>(input: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let cells = input.into_iter().map(Into::into).collect();
Self { cells: cells }
}
}
impl<'a> Table<'a> {
pub fn new() -> Self {
Self {
rows: Vec::new(),
invert_header: true,
style: &styles::GLYPHS_ROUNDED,
border_attr: Attributes::new(),
table_attr: None,
col_alt_attr: Default::default(),
col_attr: HashMap::new(),
row_alt_attr: Default::default(),
row_attr: HashMap::new(),
cell_attr: HashMap::new(),
table_align: (Horizontal::Left, Vertical::Top),
col_align: HashMap::new(),
row_align: HashMap::new(),
cell_align: HashMap::new(),
}
}
pub fn set_table_style(mut self, style: &'a Style) -> Self {
self.style = style;
self
}
pub fn set_invert_header(mut self, value: bool) -> Self {
self.invert_header = value;
self
}
pub fn set_border_attr(mut self, attr: Attributes) -> Self {
self.border_attr = attr;
self
}
pub fn set_table_attr(mut self, attr: Attributes) -> Self {
self.table_attr = Some(attr);
self
}
pub fn set_column_alt_attr(mut self, even: Option<Attributes>, odd: Option<Attributes>) -> Self {
self.col_alt_attr = [even, odd];
self
}
pub fn set_column_attr(mut self, index: usize, attr: Attributes) -> Self {
self.col_attr.insert(index, attr);
self
}
pub fn set_row_alt_attr(mut self, even: Option<Attributes>, odd: Option<Attributes>) -> Self {
self.row_alt_attr = [even, odd];
self
}
pub fn set_row_attr(mut self, index: usize, attr: Attributes) -> Self {
self.row_attr.insert(index, attr);
self
}
pub fn set_cell_attr(mut self, row: usize, col: usize, attr: Attributes) -> Self {
self.cell_attr.insert((row, col), attr);
self
}
fn resolve_attr(&self, row: usize, col: usize) -> Option<&Attributes> {
self.cell_attr
.get(&(row, col))
.or_else(|| self.row_attr.get(&row))
.or_else(|| {
if self.invert_header && row == 0 {
None
} else {
let offset = self.invert_header as usize;
self.row_alt_attr[(row + offset) % 2].as_ref()
}
})
.or_else(|| self.col_attr.get(&col))
.or_else(|| self.col_alt_attr[col % 2].as_ref())
.or(self.table_attr.as_ref())
}
pub fn set_table_align(mut self, horizontal: Horizontal, vertical: Vertical) -> Self {
self.table_align = (horizontal, vertical);
self
}
pub fn set_col_align(mut self, col: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
self.col_align.insert(col, (h, v));
self
}
pub fn set_row_align(mut self, row: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
self.row_align.insert(row, (h, v));
self
}
pub fn set_cell_align(mut self, r: usize, c: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
self.cell_align.insert((r, c), (h, v));
self
}
fn resolve_align(&self, row: usize, col: usize) -> (Horizontal, Vertical) {
let mut h = None;
let mut v = None;
if let Some((ch, cv)) = self.cell_align.get(&(row, col)) {
h = *ch;
v = *cv;
}
if h.is_none() || v.is_none() {
if let Some((rh, rv)) = self.row_align.get(&row) {
h = h.or(*rh);
v = v.or(*rv);
}
}
if h.is_none() || v.is_none() {
if let Some((ch, cv)) = self.col_align.get(&col) {
h = h.or(*ch);
v = v.or(*cv);
}
}
(h.unwrap_or(self.table_align.0), v.unwrap_or(self.table_align.1))
}
pub fn add_rows<I, R, S>(mut self, input: I) -> Self
where
I: IntoIterator<Item = R>,
R: IntoIterator<Item = S>,
S: Into<String>,
{
self.rows.extend(input.into_iter().map(Row::new));
self
}
pub fn add_row<I, S>(mut self, input: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.rows.push(Row::new(input));
self
}
pub fn push_row<I, S>(&mut self, input: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.rows.push(Row::new(input));
}
}
impl Attributes {
pub fn new() -> Self {
Self { on: "".to_string(), off: "".to_string() }
}
pub fn with(value: &str) -> Self {
Self { on: value.to_string(), off: ansi::RESET.to_string() }
}
pub fn as_styled<I, S>(input: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self { on: input.into_iter().map(|s| s.into()).collect::<String>(), off: ansi::RESET.to_string() }
}
}
#[macro_export]
macro_rules! attr {
( $( $x:expr ),* $(,)? ) => {
Attributes::as_styled([ $( $x ),* ])
};
}
#[macro_export]
macro_rules! styles {
( $( $x:ident ),* ) => {
[ $( (stringify!($x), styles::$x) ),* ]
};
}
impl LayoutLine {
fn new(line: &str) -> Self {
let width = display_width(&line);
Self { line: line.to_string(), width: width }
}
}
impl LayoutCell {
pub fn new(line: &str) -> Self {
let mut max_width = 0;
let lines: Vec<LayoutLine> = line
.lines()
.map(|l| {
let layout_line = LayoutLine::new(&l);
max_width = max(layout_line.width, max_width);
layout_line
})
.collect();
Self { lines: lines, width: max_width }
}
}
impl LayoutRow {
pub fn new(row: &Row) -> Self {
let mut max_height = 0;
let cells: Vec<LayoutCell> = row
.cells
.iter()
.map(|c| {
let cell = LayoutCell::new(&c);
max_height = max(cell.lines.len(), max_height);
cell
})
.collect();
Self { cells: cells, height: max_height }
}
pub fn offsets(&self, widths: &Vec<usize>, full_width: usize) -> Vec<usize> {
let mut result = Vec::<usize>::new();
let mut total = 0;
let count = self.cells.len();
let decrease = if count == widths.len() { 0 } else { 1 };
for index in 0..count - decrease {
total += widths[index];
result.push(total);
}
result.push(full_width);
result
}
}
impl<'a> LayoutTable<'a> {
fn new(table: &'a Table<'a>) -> Self {
let mut max_cols = 0;
let mut min_cols = usize::MAX;
let rows: Vec<LayoutRow> = table
.rows
.iter()
.map(|r| {
let row = LayoutRow::new(&r);
max_cols = max(max_cols, row.cells.len());
min_cols = min(min_cols, row.cells.len());
row
})
.collect();
let mut widths = Vec::with_capacity(max_cols);
widths.resize(max_cols, 0);
let mut map = Vec::with_capacity(max_cols);
map.resize(max_cols, 0);
for row in &rows {
if row.cells.len() == max_cols {
for cell in 0..row.cells.len() {
widths[cell] = max(widths[cell], row.cells[cell].width);
}
} else if row.cells.len() > 0 {
for cell in 0..row.cells.len() - 1 {
widths[cell] = max(widths[cell], row.cells[cell].width);
}
let index = row.cells.len() - 1;
map[index] = max(map[index], row.cells[index].width);
}
}
let mut total = widths.iter().sum();
for index in 0..map.len() {
let span = map[index];
if span != 0 {
let width: usize = widths[0..index].iter().sum();
let required = width + span;
if required > total {
let delta = required - total;
let cols = max_cols - index;
let extra_per_col = delta / cols;
let remainder = delta % cols;
for i in index..max_cols {
widths[i] += extra_per_col;
}
for i in index..index + remainder {
widths[i] += 1;
}
total = required;
}
}
}
Self { config: table, rows: rows, widths: widths }
}
fn offsets(&self, index: usize, widths: &Vec<usize>, full_width: usize) -> Vec<usize> {
self.rows.get(index).map(|r| r.offsets(widths, full_width)).unwrap_or_else(|| vec![full_width])
}
}
impl fmt::Display for Table<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let table = LayoutTable::new(self);
if table.rows.len() > 0 && table.widths.len() > 0 {
let draw =
|f: &mut Formatter<'_>, style: &[&str; 6], cols: usize, ante: &Vec<usize>, post: &Vec<usize>| -> fmt::Result {
if style[0].len() + style[1].len() + style[2].len() + style[3].len() > 0 {
let mut off = 0;
let mut ain = 0;
let mut pin = 0;
let mut cols = cols;
let end = *ante.last().unwrap();
write!(f, "{}{}", self.border_attr.on, style[StyleIdx::Left as usize])?;
while off < end {
let width;
let glyph;
if ante[ain] == post[pin] {
width = post[pin] - off;
glyph = style[StyleIdx::Inter as usize];
ain += 1;
pin += 1;
} else if ante[ain] < post[pin] {
width = ante[ain] - off;
glyph = style[StyleIdx::Bot as usize];
ain += 1;
} else {
width = post[pin] - off;
glyph = style[StyleIdx::Top as usize];
pin += 1;
}
off += width;
cols -= 1;
if off < end {
write!(f, "{}", style[StyleIdx::Pad as usize].repeat(width))?;
write!(f, "{}", glyph)?;
} else {
let width = width + cols * display_width(glyph);
write!(f, "{}", style[StyleIdx::Pad as usize].repeat(width))?;
}
}
writeln!(f, "{}{}", style[StyleIdx::Right as usize], self.border_attr.off)?;
}
Ok(())
};
let invert_on = |row: usize| -> &str {
let active = row == 0 && table.config.invert_header;
if active { ansi::INVERT } else { "" }
};
let invert_off = |row: usize| -> &str {
let active = row == 0 && table.config.invert_header;
if active { ansi::INVERT_OFF } else { "" }
};
let attr_on =
|row: usize, col: usize| -> &str { table.config.resolve_attr(row, col).map_or("", |attr| attr.on.as_str()) };
let attr_off =
|row: usize, col: usize| -> &str { table.config.resolve_attr(row, col).map_or("", |attr| attr.off.as_str()) };
let full_width: usize = table.widths.iter().sum();
let mut style = &table.config.style.top;
let csw = display_width(table.config.style.row[StyleIdx::Inter as usize]);
let mut ante = vec![full_width];
let mut post = table.offsets(0, &table.widths, full_width);
let on = &table.config.border_attr.on;
let off = &table.config.border_attr.off;
for (index, row) in table.rows.iter().enumerate() {
draw(f, style, table.widths.len(), &ante, &post)?;
style = &table.config.style.sep;
for i in 0..row.height {
let mut remaining = full_width;
write!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Left as usize], off)?;
for (col, cell) in row.cells.iter().enumerate() {
let align = table.config.resolve_align(index, col);
let horizontal = align.0;
let vertical = align.1;
let oob = cell.lines.len();
let offset = match vertical {
Vertical::Top => 0,
Vertical::Centre => row.height / 2 - oob / 2,
Vertical::Bottom => row.height - oob,
};
let i = (i >= offset).then(|| i - offset).unwrap_or(oob);
let line: (&str, usize) =
if i < cell.lines.len() { (&cell.lines[i].line, cell.lines[i].width) } else { ("", 0) };
let line_width = line.1;
let is_last = col == row.cells.len() - 1;
let span = table.widths.len() - row.cells.len();
let is_spanning = span > 0 && is_last;
let padding = if !is_spanning {
let pad = table.widths[col] - line_width;
remaining -= line_width + pad;
pad
} else {
remaining - line_width + csw * span
};
let left = padding / 2;
let lr = match horizontal {
Horizontal::Left => (0, padding),
Horizontal::Centre => (left, padding - left),
Horizontal::Right => (padding, 0),
};
write!(f, "{}", invert_on(index))?;
write!(f, "{}", attr_on(index, col))?;
write!(f, "{:width$}", "", width = lr.0)?;
write!(f, "{}", line.0)?;
write!(f, "{:width$}", "", width = lr.1)?;
write!(f, "{}", attr_off(index, col))?;
write!(f, "{}", invert_off(index))?;
if col < row.cells.len() - 1 {
write!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Inter as usize], off)?;
} else {
writeln!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Right as usize], off)?;
}
}
}
ante = post;
post = table.offsets(index + 1, &table.widths, full_width);
}
draw(f, &table.config.style.bot, table.widths.len(), &ante, &post)?;
}
Ok(())
}
}