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_even_attr: Option<Attributes>,
col_odd_attr: Option<Attributes>,
col_attr: HashMap<usize, Attributes>,
row_even_attr: Option<Attributes>,
row_odd_attr: Option<Attributes>,
row_attr: HashMap<usize, Attributes>,
cell_attr: HashMap<(usize, usize), Attributes>,
}
#[derive(Clone, Debug)]
pub struct Row {
cells: Vec<String>,
}
const LEFT: usize = 0;
const PAD: usize = 1;
const RIGHT: usize = 2;
const TOP: usize = 3;
const INTER: usize = 4;
const BOT: usize = 5;
#[derive(Clone, Debug)]
pub struct Style {
pub top: [&'static str; 6],
pub row: [&'static str; 6],
pub sep: [&'static str; 6],
pub bot: [&'static str; 6],
}
#[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)
}
}
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 {
pub fn empty() -> Self {
Self { cells: Vec::new() }
}
pub 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_even_attr: None,
col_odd_attr: None,
col_attr: HashMap::new(),
row_even_attr: None,
row_odd_attr: None,
row_attr: HashMap::new(),
cell_attr: 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: Attributes, odd: Attributes) -> Self {
self.col_even_attr = Some(even);
self.col_odd_attr = Some(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: Attributes, odd: Attributes) -> Self {
self.row_even_attr = Some(even);
self.row_odd_attr = Some(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 row % 2 == 0 {
self.row_even_attr.as_ref()
} else {
self.row_odd_attr.as_ref()
}
})
.or_else(|| self.col_attr.get(&col))
.or_else(|| {
if col % 2 == 0 {
self.col_even_attr.as_ref()
} else {
self.col_odd_attr.as_ref()
}
})
.or(self.table_attr.as_ref())
}
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 ),* ])
};
}
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[LEFT])?;
while off < end {
let width;
let glyph;
if ante[ain] == post[pin] {
width = post[pin] - off;
glyph = style[INTER];
ain += 1;
pin += 1;
} else if ante[ain] < post[pin] {
width = ante[ain] - off;
glyph = style[BOT];
ain += 1;
} else {
width = post[pin] - off;
glyph = style[TOP];
pin += 1;
}
off += width;
cols -= 1;
if off < end {
write!(f, "{}", style[PAD].repeat(width))?;
write!(f, "{}", glyph)?;
} else {
let width = width + cols * display_width(glyph);
write!(f, "{}", style[PAD].repeat(width))?;
}
}
writeln!(f, "{}{}", style[RIGHT], self.border_attr.off)?;
}
Ok(())
};
let full_width: usize = table.widths.iter().sum();
let mut style = &table.config.style.top;
let csw = display_width(table.config.style.row[INTER]);
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;
for (col, cell) in row.cells.iter().enumerate() {
let pos = if col == 0 { LEFT } else { INTER };
write!(f, "{}{}{}", on, table.config.style.row[pos], off)?;
if let Some(attr) = table.config.resolve_attr(index, col) {
write!(f, "{}", attr.on)?;
}
if index == 0 && table.config.invert_header {
write!(f, "{}", ansi::INVERT)?;
}
let mut displayed = 0;
if i < cell.lines.len() {
write!(f, "{}", cell.lines[i].line)?;
displayed += cell.lines[i].width;
}
if row.cells.len() == table.widths.len() || col < row.cells.len() - 1 {
let padding = table.widths[col] - displayed;
write!(f, "{}{}", " ".repeat(padding), ansi::INVERT_OFF)?;
remaining -= table.widths[col];
if let Some(attr) = table.config.resolve_attr(index, col) {
write!(f, "{}", attr.off)?;
}
} else {
remaining -= displayed;
}
}
let width = remaining + csw * (table.widths.len() - row.cells.len());
let pad = " ".repeat(width);
write!(f, "{}{}", pad, ansi::RESET)?;
writeln!(f, "{}{}{}", on, table.config.style.row[RIGHT], off)?;
}
ante = post;
post = table.offsets(index + 1, &table.widths, full_width);
}
draw(f, &table.config.style.bot, table.widths.len(), &ante, &post)?;
}
Ok(())
}
}