use std::cmp::{max, min};
use std::fmt;
use std::fmt::Formatter;
use unicode_width::UnicodeWidthChar;
#[derive(Debug)]
pub struct Style {
pub top: [&'static str; 4],
pub row: [&'static str; 4],
pub sep: [&'static str; 4],
pub bot: [&'static str; 4],
}
#[derive(Clone, Debug)]
pub struct Row {
cells: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct Table<'a> {
rows: Vec<Row>,
style: &'a Style,
invert_header: bool,
}
#[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,
}
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_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";
pub 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 => {
width += if ('\u{1F300}'..='\u{1FAFF}').contains(&c) {
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 }
}
}
fn empty_rows() -> impl Iterator<Item = Vec<String>> {
std::iter::empty()
}
impl<'a> Table<'a> {
pub fn empty() -> Self {
Self::with_style(&DEFAULT_STYLE, DEFAULT_INVERT_HEADER, empty_rows())
}
pub fn new<I, R, S>(input: I) -> Self
where
I: IntoIterator<Item = R>,
R: IntoIterator<Item = S>,
S: Into<String>,
{
Self::with_style(&DEFAULT_STYLE, DEFAULT_INVERT_HEADER, input)
}
pub fn with_style<I, R, S>(style: &'a Style, invert_header: bool, input: I) -> Self
where
I: IntoIterator<Item = R>,
R: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
rows: input.into_iter().map(|r| Row::new(r)).collect(),
style: style,
invert_header: invert_header,
}
}
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,
}
}
}
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);
for row in &rows {
if row.cells.len() == max_cols {
for cell_index in 0..row.cells.len() {
widths[cell_index] = max(widths[cell_index], row.cells[cell_index].width);
}
}
}
for index in 0..max_cols - min_cols {
for row in &rows {
if row.cells.len() == max_cols - index {
for cell_index in 0..row.cells.len() {
widths[cell_index] = max(widths[cell_index], row.cells[cell_index].width);
}
}
}
}
Self {
rows: rows,
widths: widths,
style: table.style,
invert_header: table.invert_header,
}
}
}
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; 4], widths: &Vec<usize>| -> fmt::Result {
if style[0].len() + style[1].len() + style[2].len() + style[3].len() > 0 {
for (index, width) in widths.iter().enumerate() {
let pos = if index == 0 { 0 } else { 2 };
write!(f, "{}", style[pos])?;
write!(f, "{}", style[1].repeat(*width))?;
}
writeln!(f, "{}", style[3])?;
}
Ok(())
};
let colsep_width = display_width(table.style.row[2]);
let full_width = |colsep_width: usize, widths: &Vec<usize>| -> usize {
let mut width = 0;
for w in widths {
width += w + colsep_width;
}
width
}(colsep_width, &table.widths);
for (index, row) in table.rows.iter().enumerate() {
if index == 0 {
draw(f, &table.style.top, &table.widths)?;
} else {
draw(f, &table.style.sep, &table.widths)?;
};
for i in 0..row.height {
let mut remaining = full_width;
for (col, cell) in row.cells.iter().enumerate() {
let pos = if col == 0 { 0 } else { 2 };
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] + colsep_width;
} else {
remaining -= displayed + colsep_width;
}
}
write!(
f,
"{}{}{}\n",
" ".repeat(remaining),
ANSI_REVERSE_OFF,
table.style.row[3]
)?;
}
}
draw(f, &table.style.bot, &table.widths)?;
}
Ok(())
}
}