use std::cmp::{max, min};
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,
}
#[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(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)]
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> {
rows: Vec<LayoutRow>,
widths: Vec<usize>,
style: &'a Style,
invert_header: bool,
}
#[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: ["", "", "", "", "", ""],
};
}
const DEFAULT_STYLE: &Style = &styles::GLYPHS_ROUNDED;
const DEFAULT_INVERT_HEADER: bool = true;
const ANSI_REVERSE: &str = "\x1b[7m";
const ANSI_REVERSE_OFF: &str = "\x1b[27m";
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: DEFAULT_INVERT_HEADER,
style: &DEFAULT_STYLE
}
}
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 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 push_row<I, S>(&mut self, input: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.rows.push(Row::new(input));
}
}
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: &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 += delta;
}
}
}
Self {
rows: rows,
widths: widths,
style: table.style,
invert_header: table.invert_header,
}
}
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, "{}", 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])?;
}
Ok(())
};
let full_width: usize = table.widths.iter().sum();
let mut style = &table.style.top;
let csw = display_width(table.style.row[INTER]);
let mut ante = vec![full_width];
let mut post = table.offsets(0, &table.widths, full_width);
for (index, row) in table.rows.iter().enumerate() {
draw(f, style, table.widths.len(), &ante, &post)?;
style = &table.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, "{}", table.style.row[pos])?;
if index == 0 && table.invert_header {
write!(f, "{}", ANSI_REVERSE)?;
}
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_REVERSE_OFF)?;
remaining -= table.widths[col];
} else {
remaining -= displayed;
}
}
let width = remaining + csw * (table.widths.len() - row.cells.len());
let pad = " ".repeat(width);
writeln!(f, "{}{}{}", pad, ANSI_REVERSE_OFF, table.style.row[RIGHT])?;
}
ante = post;
post = table.offsets(index + 1, &table.widths, full_width);
}
draw(f, &table.style.bot, table.widths.len(), &ante, &post)?;
}
Ok(())
}
}