#[macro_use]
extern crate lazy_static;
mod cell;
mod row;
mod ser;
mod style;
pub use crate::{
cell::{Alignment, Cell},
row::{IntoRow, Row},
style::TableStyle,
};
#[doc(inline)]
pub use term_data_table_derive::IntoRow;
use itertools::Itertools;
use serde::Serialize;
use std::{cell::RefCell, collections::HashMap, fmt};
use terminal_size::terminal_size;
thread_local! {
static MAX_WIDTHS_CELL_WIDTHS: RefCell<(ColumnWidths, HashMap<usize, usize>)>
= RefCell::new((ColumnWidths::new(), HashMap::new()));
}
#[derive(Eq, PartialEq, Copy, Clone)]
pub enum RowPosition {
First,
Mid,
Last,
}
#[derive(Clone, Debug)]
pub struct Table<'data> {
rows: Vec<Row<'data>>,
style: TableStyle,
pub has_separate_rows: bool,
pub has_top_border: bool,
pub has_bottom_border: bool,
column_widths: RefCell<ColumnWidths>,
row_lines: RefCell<Vec<usize>>,
}
impl<'data> Default for Table<'data> {
fn default() -> Self {
Self {
rows: Vec::new(),
style: TableStyle::EXTENDED,
has_separate_rows: true,
has_top_border: true,
has_bottom_border: true,
column_widths: RefCell::new(ColumnWidths::new()),
row_lines: RefCell::new(vec![]),
}
}
}
impl<'data> Table<'data> {
pub fn new() -> Table<'data> {
Default::default()
}
pub fn from_rows(rows: Vec<Row<'data>>) -> Table<'data> {
Self {
rows,
..Default::default()
}
}
pub fn from_serde(data: impl IntoIterator<Item = impl Serialize>) -> anyhow::Result<Self> {
let mut table = Table::new();
for row in data {
table.add_row(ser::serialize_row(row)?);
}
Ok(table)
}
pub fn with_row(mut self, row: Row<'data>) -> Self {
self.add_row(row);
self
}
pub fn add_row(&mut self, row: Row<'data>) -> &mut Self {
self.rows.push(row);
self
}
pub fn with_style(mut self, style: TableStyle) -> Self {
self.set_style(style);
self
}
pub fn set_style(&mut self, style: TableStyle) -> &mut Self {
self.style = style;
self
}
pub fn has_separate_rows(&self) -> bool {
self.has_separate_rows
}
pub fn with_separate_rows(mut self, has_separate_rows: bool) -> Self {
self.set_separate_rows(has_separate_rows);
self
}
pub fn set_separate_rows(&mut self, has_separate_rows: bool) -> &mut Self {
self.has_separate_rows = has_separate_rows;
self
}
fn layout(&self, width: Option<usize>) {
let cols = self.rows.iter().map(|row| row.columns()).max().unwrap_or(0);
let border_width = self.style.border_width();
let mut col_widths = self.column_widths.borrow_mut();
col_widths.reset(cols);
if cols == 0 {
return;
}
if let Some(width) = width {
let cell_width_total = width - (border_width + 1) * cols;
MAX_WIDTHS_CELL_WIDTHS.with(|lk| {
let (ref mut max_widths, ref mut cell_widths) = &mut *lk.borrow_mut();
max_widths.reset(cols);
cell_widths.clear();
for row in self.rows.iter() {
max_widths.fit_row_singleline(row, border_width);
}
let cell_width = cell_width_total / cols;
for (idx, max_width) in max_widths.iter().enumerate() {
if *max_width < cell_width {
cell_widths.insert(idx, *max_width);
}
}
let remaining_cells = cols - cell_widths.len();
let remaining_space =
cell_width_total - cell_widths.values().copied().sum::<usize>();
if remaining_cells > 0 {
let cell_width = remaining_space / remaining_cells;
for idx in 0..cols {
cell_widths.entry(idx).or_insert(cell_width);
}
}
col_widths.from_map(cell_widths);
});
} else {
for row in self.rows.iter() {
col_widths.fit_row_singleline(row, border_width);
}
}
for row in self.rows.iter() {
self.row_lines
.borrow_mut()
.push(row.layout(&*col_widths, border_width));
}
}
fn render(&self, view_width: Option<usize>, f: &mut fmt::Formatter) -> fmt::Result {
self.layout(view_width);
if self.rows.is_empty() {
return writeln!(f, "<empty table>");
}
let row_lines = self.row_lines.borrow();
if self.has_top_border {
self.rows[0].render_top_separator(&*self.column_widths.borrow(), &self.style, f)?;
}
self.rows[0].render_content(&*self.column_widths.borrow(), row_lines[0], &self.style, f)?;
for (idx, (prev_row, row)) in self.rows.iter().tuple_windows().enumerate() {
if self.has_separate_rows {
row.render_separator(prev_row, &*self.column_widths.borrow(), &self.style, f)?;
}
let row_lines = self.row_lines.borrow();
row.render_content(
&*self.column_widths.borrow(),
row_lines[idx + 1],
&self.style,
f,
)?;
}
if self.has_bottom_border {
self.rows[self.rows.len() - 1].render_bottom_separator(
&*self.column_widths.borrow(),
&self.style,
f,
)?;
}
Ok(())
}
pub fn for_terminal(&self) -> impl fmt::Display + '_ {
match terminal_size().and_then(|v| usize::try_from((v.0).0).ok()) {
Some(width) => FixedWidth { table: self, width },
None => FixedWidth {
table: self,
width: usize::MAX,
},
}
}
pub fn fixed_width(&self, width: usize) -> impl fmt::Display + '_ {
FixedWidth { table: self, width }
}
}
struct FixedWidth<'a> {
table: &'a Table<'a>,
width: usize,
}
impl fmt::Display for FixedWidth<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.table.render(Some(self.width), f)
}
}
impl<'data> fmt::Display for Table<'data> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.render(None, f)
}
}
pub fn data_table<'a, R: 'a>(input: impl IntoIterator<Item = &'a R>) -> Table<'a>
where
R: IntoRow,
{
let mut table = Table::new();
for row in input {
table.add_row(row.into_row());
}
table
}
#[derive(Debug, Clone)]
struct ColumnWidths(Vec<usize>);
impl ColumnWidths {
fn new() -> Self {
ColumnWidths(vec![])
}
fn reset(&mut self, num_cols: usize) {
self.0.clear();
self.0.resize(num_cols, 0);
}
fn fit_row_singleline(&mut self, row: &Row, border_width: usize) {
let mut idx = 0;
for cell in row.cells.iter() {
if cell.col_span == 1 {
self.0[idx] = self.0[idx].max(cell.min_width(true));
} else {
let required_width = cell.min_width(true) - border_width * (cell.col_span - 1);
let floor_per_cell = required_width / cell.col_span;
let mut to_fit = required_width % cell.col_span;
for i in 0..cell.col_span {
let extra = if to_fit > 0 { 1 } else { 0 };
to_fit = to_fit.saturating_sub(1);
self.0[idx + i] = self.0[idx + 1].max(floor_per_cell + extra);
}
}
idx += cell.col_span;
}
}
fn from_map(&mut self, map: &HashMap<usize, usize>) {
for (idx, slot) in self.0.iter_mut().enumerate() {
*slot = *map.get(&idx).unwrap();
}
}
}
impl std::ops::Deref for ColumnWidths {
type Target = [usize];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(test)]
mod test {
use crate::cell::{Alignment, Cell};
use crate::row::Row;
use crate::Table;
use crate::TableStyle;
use pretty_assertions::assert_eq;
#[test]
fn correct_default_padding() {
let table = Table::new()
.with_separate_rows(false)
.with_style(TableStyle::SIMPLE)
.with_row(
Row::new()
.with_cell(Cell::from("A").with_alignment(Alignment::Center))
.with_cell(Cell::from("B").with_alignment(Alignment::Center)),
)
.with_row(
Row::new()
.with_cell(Cell::from(1.to_string()))
.with_cell(Cell::from("1")),
)
.with_row(
Row::new()
.with_cell(Cell::from(2.to_string()))
.with_cell(Cell::from("10")),
)
.with_row(
Row::new()
.with_cell(Cell::from(3.to_string()))
.with_cell(Cell::from("100")),
);
let expected = r"+---+-----+
| A | B |
| 1 | 1 |
| 2 | 10 |
| 3 | 100 |
+---+-----+
";
println!("{}", table);
assert_eq!(expected, table.to_string());
}
#[test]
fn uneven_center_alignment() {
let table = Table::new()
.with_separate_rows(false)
.with_style(TableStyle::SIMPLE)
.with_row(Row::new().with_cell(Cell::from("A").with_alignment(Alignment::Center)))
.with_row(Row::new().with_cell(Cell::from(11.to_string())))
.with_row(Row::new().with_cell(Cell::from(2.to_string())))
.with_row(Row::new().with_cell(Cell::from(3.to_string())));
let expected = r"+----+
| A |
| 11 |
| 2 |
| 3 |
+----+
";
println!("{}", table);
assert_eq!(expected, table.to_string());
}
#[test]
fn uneven_center_alignment_2() {
let table = Table::new()
.with_separate_rows(false)
.with_style(TableStyle::SIMPLE)
.with_row(
Row::new()
.with_cell(Cell::from("A1").with_alignment(Alignment::Center))
.with_cell(Cell::from("B").with_alignment(Alignment::Center)),
);
println!("{}", table);
let expected = r"+----+---+
| A1 | B |
+----+---+
";
assert_eq!(expected, table.to_string());
}
#[test]
fn simple_table_style() {
let mut table = Table::new().with_style(TableStyle::SIMPLE);
add_data_to_test_table(&mut table);
let expected = r"+-----------------------------------------------------------------------------+
| This is some centered text |
+--------------------------------------+--------------------------------------+
| This is left aligned text | This is right aligned text |
+--------------------------------------+--------------------------------------+
| This is left aligned text | This is right aligned text |
+--------------------------------------+--------------------------------------+
| This is some really really really really really really really really |
| really that is going to wrap to the next line |
+-----------------------------------------------------------------------------+
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
+-----------------------------------------------------------------------------+
";
let table = table.fixed_width(80);
println!("{}", table.to_string());
assert_eq!(expected, table.to_string());
}
#[test]
#[ignore]
fn uneven_with_varying_col_span() {
let table = Table::new()
.with_separate_rows(true)
.with_style(TableStyle::SIMPLE)
.with_row(
Row::new()
.with_cell(Cell::from("A1111111").with_alignment(Alignment::Center))
.with_cell(Cell::from("B").with_alignment(Alignment::Center)),
)
.with_row(
Row::new()
.with_cell(Cell::from(1.to_string()))
.with_cell(Cell::from("1")),
)
.with_row(
Row::new()
.with_cell(Cell::from(2.to_string()))
.with_cell(Cell::from("10")),
)
.with_row(
Row::new()
.with_cell(
Cell::from(3.to_string())
.with_alignment(Alignment::Left)
.with_padding(false),
)
.with_cell(Cell::from("100")),
)
.with_row(Row::new().with_cell(Cell::from("S").with_alignment(Alignment::Center)));
let expected = "+----------+-----+
| A1111111 | B |
+----------+-----+
| 1 | 1 |
+----------+-----+
| 2 | 10 |
+----------+-----+
|\03\0 | 100 |
+----------+-----+
| S |
+----------------+
";
println!("{}", table);
assert_eq!(expected.trim(), table.to_string().trim());
}
#[test]
fn uneven_with_varying_col_span_2() {
let table = Table::new()
.with_separate_rows(false)
.with_style(TableStyle::SIMPLE)
.with_row(
Row::new()
.with_cell(Cell::from("A").with_alignment(Alignment::Center))
.with_cell(Cell::from("B").with_alignment(Alignment::Center)),
)
.with_row(
Row::new()
.with_cell(Cell::from(1.to_string()))
.with_cell(Cell::from("1")),
)
.with_row(
Row::new()
.with_cell(Cell::from(2.to_string()))
.with_cell(Cell::from("10")),
)
.with_row(
Row::new()
.with_cell(Cell::from(3.to_string()))
.with_cell(Cell::from("100")),
)
.with_row(
Row::new().with_cell(
Cell::from("Spanner")
.with_col_span(2)
.with_alignment(Alignment::Center),
),
);
let expected = "+-----+-----+
| A | B |
| 1 | 1 |
| 2 | 10 |
| 3 | 100 |
| Spanner |
+-----------+
";
println!("{}", table);
assert_eq!(expected.trim(), table.to_string().trim());
}
fn add_data_to_test_table(table: &mut Table) {
table.add_row(
Row::new().with_cell(
Cell::from("This is some centered text")
.with_col_span(2)
.with_alignment(Alignment::Center),
),
);
table.add_row(
Row::new()
.with_cell(Cell::from("This is left aligned text"))
.with_cell(
Cell::from("This is right aligned text").with_alignment(Alignment::Right),
),
);
table.add_row(
Row::new()
.with_cell(Cell::from("This is left aligned text"))
.with_cell(
Cell::from("This is right aligned text").with_alignment(Alignment::Right),
),
);
table.add_row(
Row::new().with_cell(
Cell::from(
"This is some really really really really really \
really really really really that is going to wrap to the next line",
)
.with_col_span(2),
),
);
table.add_row(
Row::new().with_cell(
Cell::from(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
.with_col_span(2),
),
);
}
}