use anyhow::{Context, Result};
use log::trace;
use termcolor::{Color, ColorSpec};
use terminal_size;
use unicode_width::UnicodeWidthStr;
use crate::output::{Print, PrintTableOpts, WriteColor};
pub const DEFAULT_TERM_WIDTH: usize = 80;
pub const MAX_SHRINK_WIDTH: usize = 5;
#[derive(Debug, Default)]
pub struct Cell {
style: ColorSpec,
value: String,
shrinkable: bool,
}
impl Cell {
pub fn new<T: AsRef<str>>(value: T) -> Self {
Self {
value: String::from(value.as_ref()).replace(&['\r', '\n', '\t'][..], ""),
..Self::default()
}
}
pub fn unicode_width(&self) -> usize {
UnicodeWidthStr::width(self.value.as_str())
}
pub fn shrinkable(mut self) -> Self {
self.shrinkable = true;
self
}
pub fn is_shrinkable(&self) -> bool {
self.shrinkable
}
pub fn bold(mut self) -> Self {
self.style.set_bold(true);
self
}
pub fn bold_if(self, predicate: bool) -> Self {
if predicate {
self.bold()
} else {
self
}
}
pub fn underline(mut self) -> Self {
self.style.set_underline(true);
self
}
pub fn red(mut self) -> Self {
self.style.set_fg(Some(Color::Red));
self
}
pub fn green(mut self) -> Self {
self.style.set_fg(Some(Color::Green));
self
}
pub fn yellow(mut self) -> Self {
self.style.set_fg(Some(Color::Yellow));
self
}
pub fn blue(mut self) -> Self {
self.style.set_fg(Some(Color::Blue));
self
}
pub fn white(mut self) -> Self {
self.style.set_fg(Some(Color::White));
self
}
pub fn ansi_256(mut self, code: u8) -> Self {
self.style.set_fg(Some(Color::Ansi256(code)));
self
}
}
impl Print for Cell {
fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
writter
.set_color(&self.style)
.context(format!(r#"cannot apply colors to cell "{}""#, self.value))?;
write!(writter, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))
}
}
#[derive(Debug, Default)]
pub struct Row(
pub Vec<Cell>,
);
impl Row {
pub fn new() -> Self {
Self::default()
}
pub fn cell(mut self, cell: Cell) -> Self {
self.0.push(cell);
self
}
}
pub trait Table
where
Self: Sized,
{
fn head() -> Row;
fn row(&self) -> Row;
fn print(writter: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
let max_width = opts
.max_width
.or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize))
.unwrap_or(DEFAULT_TERM_WIDTH);
let mut table = vec![Self::head()];
let mut cell_widths: Vec<usize> =
table[0].0.iter().map(|cell| cell.unicode_width()).collect();
table.extend(
items
.iter()
.map(|item| {
let row = item.row();
row.0.iter().enumerate().for_each(|(i, cell)| {
cell_widths[i] = cell_widths[i].max(cell.unicode_width());
});
row
})
.collect::<Vec<_>>(),
);
trace!("cell widths: {:?}", cell_widths);
let spaces_plus_separators_len = cell_widths.len() * 2 - 1;
let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len;
trace!("table width: {}", table_width);
for row in table.iter_mut() {
let mut glue = Cell::default();
for (i, cell) in row.0.iter_mut().enumerate() {
glue.print(writter)?;
let table_is_overflowing = table_width > max_width;
if table_is_overflowing && cell.is_shrinkable() {
trace!("table is overflowing and cell is shrinkable");
let shrink_width = table_width - max_width;
trace!("shrink width: {}", shrink_width);
let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] {
cell_widths[i] - shrink_width
} else {
MAX_SHRINK_WIDTH
};
trace!("cell width: {}", cell_width);
trace!("cell unicode width: {}", cell.unicode_width());
let cell_is_overflowing = cell.unicode_width() > cell_width;
if cell_is_overflowing {
trace!("cell is overflowing");
let mut value = String::new();
let mut chars_width = 0;
for c in cell.value.chars() {
let char_width = UnicodeWidthStr::width(c.to_string().as_str());
if chars_width + char_width >= cell_width {
break;
}
chars_width += char_width;
value.push(c);
}
value.push_str("… ");
trace!("chars width: {}", chars_width);
trace!("shrinked value: {}", value);
let spaces_count = cell_width - chars_width - 1;
trace!("number of spaces added to shrinked value: {}", spaces_count);
value.push_str(&" ".repeat(spaces_count));
cell.value = value;
} else {
trace!("cell is not overflowing");
let spaces_count = cell_width - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
}
} else {
trace!("table is not overflowing or cell is not shrinkable");
trace!("cell width: {}", cell_widths[i]);
trace!("cell unicode width: {}", cell.unicode_width());
let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
}
cell.print(writter)?;
glue = Cell::new("│").ansi_256(8);
}
writeln!(writter)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::io;
use super::*;
#[derive(Debug, Default)]
struct StringWritter {
content: String,
}
impl io::Write for StringWritter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWritter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWritter {}
struct Item {
id: u16,
name: String,
desc: String,
}
impl<'a> Item {
pub fn new(id: u16, name: &'a str, desc: &'a str) -> Self {
Self {
id,
name: String::from(name),
desc: String::from(desc),
}
}
}
impl Table for Item {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID"))
.cell(Cell::new("NAME").shrinkable())
.cell(Cell::new("DESC"))
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(self.id.to_string()))
.cell(Cell::new(self.name.as_str()).shrinkable())
.cell(Cell::new(self.desc.as_str()))
}
}
macro_rules! write_items {
($writter:expr, $($item:expr),*) => {
Table::print($writter, &[$($item,)*], PrintTableOpts { max_width: Some(20) }).unwrap();
};
}
#[test]
fn row_smaller_than_head() {
let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1, "a", "aa"),
Item::new(2, "b", "bb"),
Item::new(3, "c", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2 │b │bb \n",
"3 │c │cc \n",
];
assert_eq!(expected, writter.content);
}
#[test]
fn row_bigger_than_head() {
let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "c", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2222 │bbbbb │bbbbb \n",
"3 │c │cc \n",
];
assert_eq!(expected, writter.content);
let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "cccccc", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2222 │bbbbb │bbbbb \n",
"3 │cccccc │cc \n",
];
assert_eq!(expected, writter.content);
}
#[test]
fn basic_shrink() {
let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1, "", "desc"),
Item::new(2, "short", "desc"),
Item::new(3, "loooooong", "desc"),
Item::new(4, "shriiiiink", "desc"),
Item::new(5, "shriiiiiiiiiink", "desc"),
Item::new(6, "😍😍😍😍", "desc"),
Item::new(7, "😍😍😍😍😍", "desc"),
Item::new(8, "!😍😍😍😍😍", "desc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │ │desc \n",
"2 │short │desc \n",
"3 │loooooong │desc \n",
"4 │shriiiii… │desc \n",
"5 │shriiiii… │desc \n",
"6 │😍😍😍😍 │desc \n",
"7 │😍😍😍😍… │desc \n",
"8 │!😍😍😍… │desc \n",
];
assert_eq!(expected, writter.content);
}
#[test]
fn max_shrink_width() {
let mut writter = StringWritter::default();
write_items![
&mut writter,
Item::new(1111, "shriiiiiiiink", "desc very looong"),
Item::new(2222, "shriiiiiiiink", "desc very loooooooooong")
];
let expected = concat![
"ID │NAME │DESC \n",
"1111 │shri… │desc very looong \n",
"2222 │shri… │desc very loooooooooong \n",
];
assert_eq!(expected, writter.content);
}
}