#![forbid(unsafe_code)]
#![warn(clippy::pedantic, missing_debug_implementations)]
#![allow(clippy::doc_markdown)]
mod errors;
pub use colored::Color;
use colored::{ColoredString, Colorize};
pub use errors::{Result, TableError};
use std::fmt::{Display, Write as _};
use unicode_width::UnicodeWidthStr;
pub trait Row {
fn as_row(&self) -> Vec<Cell>;
}
#[derive(Debug)]
pub struct Cell {
pub value: String,
pub color: Option<Color>,
pub style: Option<CellStyle>,
pub alignment: Alignment,
}
impl Cell {
#[must_use]
pub fn new<V>(value: V) -> Self
where
V: Display,
{
Cell {
value: value.to_string(),
color: None,
style: None,
alignment: Alignment::Left,
}
}
#[must_use]
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
#[must_use]
pub fn with_color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn with_style(mut self, style: CellStyle) -> Self {
self.style = Some(style);
self
}
}
impl Display for Cell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = self.value.as_str();
let colored_value: ColoredString;
if let Some(color) = self.color {
colored_value = value.color(color);
} else {
colored_value = value.normal();
}
match self.style {
Some(CellStyle::Bold) => write!(f, "{}", colored_value.bold()),
Some(CellStyle::Dimmed) => write!(f, "{}", colored_value.dimmed()),
Some(CellStyle::Italic) => write!(f, "{}", colored_value.italic()),
None => write!(f, "{colored_value}"),
}
}
}
impl From<String> for Cell {
fn from(value: String) -> Self {
Cell {
value,
color: None,
style: None,
alignment: Alignment::Left,
}
}
}
impl From<&str> for Cell {
fn from(value: &str) -> Self {
Cell {
value: value.to_string(),
color: None,
style: None,
alignment: Alignment::Left,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CellStyle {
Bold,
Italic,
Dimmed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Alignment {
Left,
Center,
Right,
}
#[derive(Debug)]
pub struct Table<'a, R>
where
R: 'a,
{
pub header: Vec<Cell>,
pub rows: &'a [&'a R],
pub separator: String,
}
impl<'a, R> Table<'a, R> {
#[must_use]
pub fn new(rows: &'a [&'a R]) -> Self {
Table {
header: Vec::new(),
rows,
separator: String::from(" "),
}
}
#[must_use]
pub fn with_header(
mut self,
header: &[&str],
color: Option<Color>,
style: Option<CellStyle>,
alignment: Option<Alignment>,
) -> Self {
self.header = header
.iter()
.map(|&s| {
let mut c = Cell::from(s);
if let Some(col) = color {
c = c.with_color(col);
}
if let Some(st) = style {
c = c.with_style(st);
}
if let Some(al) = alignment {
c = c.with_alignment(al);
}
c
})
.collect();
self
}
#[must_use]
pub fn with_separator<S>(mut self, separator: S) -> Self
where
S: AsRef<str>,
{
self.separator = separator.as_ref().to_string();
self
}
}
impl<'a, R> Table<'a, R>
where
&'a R: Row,
{
pub fn format(&self) -> Result<String> {
let mut col_widths: Vec<usize> = Vec::new();
if !self.header.is_empty() {
col_widths = self
.header
.iter()
.map(|c| UnicodeWidthStr::width(c.value.as_str()))
.collect();
}
if !self.rows.is_empty() {
let first_row_len = self.rows[0].as_row().len();
if !self.header.is_empty() && self.header.len() != first_row_len {
return Err(TableError::HeaderLengthMismatch(
self.header.len(),
first_row_len,
));
}
if self.header.is_empty() && first_row_len > 0 {
col_widths = vec![0; first_row_len];
}
for row in self.rows {
let row_values = row.as_row();
if row_values.len() != col_widths.len() && !self.header.is_empty() {
return Err(TableError::HeaderLengthMismatch(
row_values.len(),
col_widths.len(),
));
}
if row_values.len() != first_row_len {
return Err(TableError::RowLengthMismatch(
row_values.len(),
first_row_len,
));
}
for (i, value) in row_values.iter().enumerate() {
let cell_content_width = UnicodeWidthStr::width(value.value.as_str());
if i < col_widths.len() {
col_widths[i] = col_widths[i].max(cell_content_width);
} else {
col_widths.push(cell_content_width);
}
}
}
}
let mut output = String::new();
if !self.header.is_empty() {
for (i, header_cell) in self.header.iter().enumerate() {
if i < col_widths.len() {
let header_display = format!("{header_cell}");
let header_content_width = UnicodeWidthStr::width(header_cell.value.as_str());
let required_width = col_widths[i];
let padding = required_width.saturating_sub(header_content_width);
format_cell(&mut output, header_cell.alignment, &header_display, padding);
if i < self.header.len() - 1 {
write!(output, "{}", self.separator).unwrap();
}
} else {
write!(output, "{header_cell}").unwrap();
if i < self.header.len() - 1 {
write!(output, "{}", self.separator).unwrap();
}
}
}
writeln!(output).unwrap();
}
for row in self.rows {
let row_values = row.as_row();
for (i, value_cell) in row_values.iter().enumerate() {
if i >= col_widths.len() {
write!(output, "{value_cell}").unwrap();
} else {
let value_display = format!("{value_cell}");
let value_content_width = UnicodeWidthStr::width(value_cell.value.as_str());
let required_width = col_widths[i];
let padding = required_width.saturating_sub(value_content_width);
format_cell(&mut output, value_cell.alignment, &value_display, padding);
}
if i < row_values.len() - 1 {
write!(output, "{}", self.separator).unwrap();
}
}
writeln!(output).unwrap();
}
Ok(output)
}
}
fn format_cell(output: &mut String, alignment: Alignment, value: &str, padding: usize) {
match alignment {
Alignment::Left => {
write!(output, "{value}{}", " ".repeat(padding)).unwrap();
}
Alignment::Center => {
let left_padding = padding / 2;
let right_padding = padding - left_padding;
write!(
output,
"{}{value}{}",
" ".repeat(left_padding),
" ".repeat(right_padding)
)
.unwrap();
}
Alignment::Right => {
write!(output, "{}{value}", " ".repeat(padding)).unwrap();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use handy::iter::IntoRefVec;
#[test]
fn test_table() {
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
impl Row for &Person {
fn as_row(&self) -> Vec<Cell> {
vec![self.name.clone().into(), Cell::new(self.age)]
}
}
let data = [
Person {
name: "Johnny".into(),
age: 30,
},
Person {
name: "Jane".into(),
age: 25,
},
];
let data_refs = data.as_ref_vec();
let table = Table::new(&data_refs)
.with_header(&["Name", "Age"], None, None, None)
.with_separator(" ");
let formatted = dbg!(table).format().unwrap();
assert_eq!(formatted, "Name Age\nJohnny 30 \nJane 25 \n");
}
#[test]
fn test_table_centered_header() {
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
impl Row for &Person {
fn as_row(&self) -> Vec<Cell> {
vec![
Cell::new(&self.name).with_alignment(Alignment::Right),
Cell::new(self.age).with_alignment(Alignment::Center),
]
}
}
let data = [
Person {
name: "Johnny".into(),
age: 30,
},
Person {
name: "Jane".into(),
age: 25,
},
];
let data_refs = data.as_ref_vec();
let table = Table::new(&data_refs)
.with_header(&["Name", "Some Age"], None, None, Some(Alignment::Center))
.with_separator(" ");
let formatted = dbg!(table).format().unwrap();
assert_eq!(
formatted,
" Name Some Age\nJohnny 30 \n Jane 25 \n"
);
}
#[test]
fn test_table_colored() {
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
impl Row for &Person {
fn as_row(&self) -> Vec<Cell> {
vec![
self.name.clone().into(),
Cell::new(self.age).with_color(Color::Cyan),
]
}
}
let data = [
Person {
name: "Johnny".into(),
age: 30,
},
Person {
name: "Jane".into(),
age: 25,
},
];
let data_refs = data.as_ref_vec();
let table = Table::new(&data_refs)
.with_header(&["Name", "Age"], None, Some(CellStyle::Bold), None)
.with_separator(" ");
let formatted = dbg!(table).format().unwrap();
assert_eq!(
formatted,
"\u{1b}[1mName\u{1b}[0m \u{1b}[1mAge\u{1b}[0m\nJohnny \u{1b}[36m30\u{1b}[0m \nJane \u{1b}[36m25\u{1b}[0m \n"
);
}
#[test]
fn test_table_empty_header() {
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
impl Row for &Person {
fn as_row(&self) -> Vec<Cell> {
vec![self.name.clone().into(), Cell::new(self.age)]
}
}
let data = [
Person {
name: "Johnny".into(),
age: 30,
},
Person {
name: "Jane".into(),
age: 25,
},
];
let data_refs = data.as_ref_vec();
let table = Table::new(&data_refs);
let formatted = dbg!(table).format().unwrap();
assert_eq!(formatted, "Johnny 30\nJane 25\n");
}
#[test]
fn test_table_empty_rows() {
#[derive(Debug)]
struct Person;
impl Row for &Person {
fn as_row(&self) -> Vec<Cell> {
vec![]
}
}
let data = [];
let data_refs = data.as_ref_vec();
let table: Table<'_, Person> = Table::new(&data_refs)
.with_header(&["Name", "Age"], None, None, None)
.with_separator(" | ");
let formatted = dbg!(table).format().unwrap();
assert_eq!(formatted, "Name | Age\n");
}
#[test]
fn test_table_with_alignment() {
#[derive(Debug)]
struct Person<'a> {
name: &'a str,
age: u8,
number: u32,
}
impl Row for &Person<'_> {
fn as_row(&self) -> Vec<Cell> {
vec![
Cell::new(self.name),
Cell::new(self.age).with_alignment(Alignment::Center),
Cell::new(self.number).with_alignment(Alignment::Right),
]
}
}
let data = [
Person {
name: "Johnny",
age: 30,
number: 1,
},
Person {
name: "Jane",
age: 25,
number: 2,
},
];
let data_refs = data.as_ref_vec();
let table = Table::new(&data_refs)
.with_header(
&["Person's name", "Person's Age", "Number"],
None,
None,
None,
)
.with_separator(" | ");
let formatted = dbg!(table).format().unwrap();
assert_eq!(formatted, "Person's name | Person's Age | Number\nJohnny | 30 | 1\nJane | 25 | 2\n");
}
#[test]
#[should_panic(expected = "HeaderLengthMismatch(1, 2)")]
fn test_table_wrong_header_length() {
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
impl Row for &Person {
fn as_row(&self) -> Vec<Cell> {
vec![self.name.clone().into(), Cell::new(self.age)]
}
}
let data = [
Person {
name: "Johnny".into(),
age: 30,
},
Person {
name: "Jane".into(),
age: 25,
},
];
let data_refs = data.as_ref_vec();
let table = Table::new(&data_refs).with_header(&["Name"], None, None, None);
dbg!(table).format().unwrap();
}
}