use std::{
borrow::Cow,
fmt,
iter::{self, zip}
};
use unicode_width::UnicodeWidthStr;
use yansi::Painted;
pub use super::Align;
#[allow(clippy::type_complexity)]
pub struct Column<CM, CDM> {
title: String,
min_width: Option<usize>,
max_width: Option<usize>,
trunc_len: usize,
trunc_ch: char,
renderer: Box<dyn Fn(Option<&CM>, &CellData<CDM>) -> String>,
stylize:
Option<Box<dyn Fn(Option<&CM>, &CellData<CDM>, &str) -> Painted<String>>>,
title_align: Align,
cell_align: Align,
colmeta: Option<CM>
}
impl<CM, CDM> Column<CM, CDM> {
#[allow(clippy::needless_pass_by_value)]
pub fn new(
heading: impl ToString,
renderer: impl Fn(Option<&CM>, &CellData<CDM>) -> String + 'static
) -> Self {
Self {
title: heading.to_string(),
min_width: None,
max_width: None,
trunc_len: 0,
trunc_ch: '…',
renderer: Box::new(renderer),
stylize: None,
title_align: Align::Center,
cell_align: Align::Left,
colmeta: None
}
}
#[must_use]
pub fn min_width(mut self, min: usize) -> Self {
self.min_width_ref(min);
self
}
pub fn min_width_ref(&mut self, min: usize) -> &mut Self {
if let Some(max) = self.max_width {
assert!(min <= max);
}
self.min_width = Some(min);
self
}
#[must_use]
pub fn max_width(mut self, max: usize) -> Self {
self.max_width_ref(max);
self
}
pub fn max_width_ref(&mut self, max: usize) -> &mut Self {
if let Some(min) = self.min_width {
assert!(max >= min);
}
self.max_width = Some(max);
self
}
#[must_use]
pub const fn trunc_style(mut self, len: usize, ch: char) -> Self {
self.trunc_style_ref(len, ch);
self
}
pub const fn trunc_style_ref(&mut self, len: usize, ch: char) -> &mut Self {
self.trunc_len = len;
self.trunc_ch = ch;
self
}
#[must_use]
pub fn stylize(
mut self,
f: impl Fn(Option<&CM>, &CellData<CDM>, &str) -> Painted<String> + 'static
) -> Self {
self.stylize = Some(Box::new(f));
self
}
#[must_use]
pub const fn title_align(mut self, align: Align) -> Self {
self.title_align_ref(align);
self
}
pub const fn title_align_ref(&mut self, align: Align) -> &mut Self {
self.title_align = align;
self
}
#[must_use]
pub const fn cell_align(mut self, align: Align) -> Self {
self.cell_align_ref(align);
self
}
pub const fn cell_align_ref(&mut self, align: Align) -> &mut Self {
self.cell_align = align;
self
}
#[must_use]
pub fn meta(mut self, m: CM) -> Self {
self.colmeta = Some(m);
self
}
pub fn meta_r(&mut self, m: CM) -> &mut Self {
self.colmeta = Some(m);
self
}
}
pub enum CellValue {
Str(String),
U64(u64)
}
impl From<String> for CellValue {
fn from(val: String) -> Self {
Self::Str(val)
}
}
impl From<&str> for CellValue {
fn from(val: &str) -> Self {
Self::Str(val.to_string())
}
}
impl From<u64> for CellValue {
fn from(val: u64) -> Self {
Self::U64(val)
}
}
impl fmt::Display for CellValue {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Str(s) => write!(f, "{s}"),
Self::U64(v) => write!(f, "{v}")
}
}
}
pub struct CellData<CDM> {
pub val: CellValue,
pub meta: Option<CDM>
}
impl<CDM> CellData<CDM> {
#[must_use]
pub fn str(val: impl Into<String>) -> Self {
Self {
val: CellValue::Str(val.into()),
meta: None
}
}
#[must_use]
pub const fn u64(val: u64) -> Self {
Self {
val: CellValue::U64(val),
meta: None
}
}
#[must_use]
pub fn meta(mut self, md: CDM) -> Self {
self.meta = Some(md);
self
}
}
impl<CDM> From<String> for CellData<CDM> {
fn from(val: String) -> Self {
Self {
val: CellValue::from(val),
meta: None
}
}
}
impl<CDM> From<&str> for CellData<CDM> {
fn from(val: &str) -> Self {
Self {
val: CellValue::from(val),
meta: None
}
}
}
impl<CDM> From<u64> for CellData<CDM> {
fn from(val: u64) -> Self {
Self {
val: CellValue::from(val),
meta: None
}
}
}
impl<CDM> fmt::Display for CellData<CDM> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.val {
CellValue::Str(s) => write!(f, "{s}"),
CellValue::U64(v) => write!(f, "{v}")
}
}
}
pub struct Data<CDM> {
num_cols: usize,
cells: Vec<Vec<CellData<CDM>>>
}
impl<CDM> Data<CDM> {
#[must_use]
pub const fn new(cols: usize) -> Self {
Self {
num_cols: cols,
cells: Vec::new()
}
}
pub fn add_row(&mut self, row: Vec<CellData<CDM>>) {
assert_eq!(row.len(), self.num_cols);
self.cells.push(row);
}
}
pub trait CellRender {
fn stringify();
fn print();
}
pub struct Renderer<'a, CM, CDM> {
show_header: bool,
header_underline: Option<char>,
col_spacing: usize,
cols: &'a [Column<CM, CDM>],
data: &'a [Vec<CellData<CDM>>]
}
impl<'a, CM, CDM> Renderer<'a, CM, CDM> {
#[must_use]
pub fn new(cols: &'a [Column<CM, CDM>], data: &'a Data<CDM>) -> Self {
Self {
show_header: false,
header_underline: None,
col_spacing: 2,
cols,
data: &data.cells
}
}
#[must_use]
pub const fn header(mut self, underline: Option<char>) -> Self {
self.header_ref(underline);
self
}
pub const fn header_ref(&mut self, underline: Option<char>) -> &mut Self {
self.show_header = true;
self.header_underline = underline;
self
}
#[must_use]
pub const fn column_spacing(mut self, n: usize) -> Self {
self.column_spacing_ref(n);
self
}
pub const fn column_spacing_ref(&mut self, n: usize) -> &mut Self {
self.col_spacing = n;
self
}
pub fn print(&self) {
println!("{self}");
}
}
#[inline]
fn strlen(s: &str) -> usize {
s.width()
}
impl<CM, CDM> fmt::Display for Renderer<'_, CM, CDM> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut rendered: Vec<Vec<String>> = Vec::with_capacity(self.data.len());
let mut col_widths: Vec<usize> = if self.show_header {
self.cols.iter().map(|c| strlen(&c.title)).collect()
} else {
vec![0; self.cols.len()]
};
for row in self.data {
let mut rrow = Vec::with_capacity(self.cols.len());
for (cw, (col, cell)) in
col_widths.iter_mut().zip(iter::zip(self.cols, row))
{
let cell_str = (col.renderer)(col.colmeta.as_ref(), cell);
*cw = std::cmp::max(*cw, strlen(&cell_str));
rrow.push(cell_str);
}
rendered.push(rrow);
}
let col_widths: Vec<usize> = zip(col_widths, self.cols)
.map(|(cw, col)| clamp_width(cw, col.min_width, col.max_width))
.collect();
let colspace = " ".repeat(self.col_spacing);
let mut fields = Vec::with_capacity(self.cols.len());
if self.show_header {
for (col, cw) in std::iter::zip(self.cols, &col_widths) {
let title = trunc_str(&col.title, *cw, col.trunc_len, col.trunc_ch);
let cc = format_cell(&title, *cw, col.title_align);
fields.push(cc);
}
writeln!(f, "{}", fields.join(&colspace))?;
if let Some(ch) = self.header_underline {
fields.clear();
for cw in &col_widths {
let line = std::iter::repeat_n(ch, *cw).collect::<String>();
fields.push(line);
}
writeln!(f, "{}", fields.join(&colspace))?;
}
}
for (cvs, row) in iter::zip(self.data, rendered) {
fields.clear();
for ((col, cw), (cv, cell)) in
iter::zip(iter::zip(self.cols, &col_widths), iter::zip(cvs, row))
{
let cell = trunc_str(&cell, *cw, col.trunc_len, col.trunc_ch);
if let Some(ref stylize) = col.stylize {
let cell = stylize(col.colmeta.as_ref(), cv, &cell);
let cc = format_painted_cell(&cell, *cw, col.cell_align);
fields.push(cc);
} else {
let cc = format_cell(&cell, *cw, col.cell_align);
fields.push(cc);
}
}
writeln!(f, "{}", fields.join(&colspace))?;
}
Ok(())
}
}
fn format_cell(s: &str, cell_width: usize, align: Align) -> String {
match align {
Align::Left => {
let pad = cell_width - strlen(s);
format!("{s}{:pad$}", "")
}
Align::Center => {
let pad = cell_width - strlen(s);
let (lpad, rpad) = split_len(pad);
format!("{:lpad$}{s}{:rpad$}", "", "")
}
Align::Right => {
let pad = cell_width - strlen(s);
format!("{:pad$}{s}", "")
}
}
}
fn format_painted_cell(
s: &Painted<String>,
cell_width: usize,
align: Align
) -> String {
match align {
Align::Left => {
let pad = cell_width - strlen(&s.value);
format!("{s}{:pad$}", "")
}
Align::Center => {
let pad = cell_width - strlen(&s.value);
let (lpad, rpad) = split_len(pad);
format!("{:lpad$}{s}{:rpad$}", "", "")
}
Align::Right => {
let pad = cell_width - strlen(&s.value);
format!("{:pad$}{s}", "")
}
}
}
#[inline]
const fn split_len(len: usize) -> (usize, usize) {
let left = len / 2;
let right = len - left;
(left, right)
}
fn clamp_width(w: usize, min: Option<usize>, max: Option<usize>) -> usize {
let w = min.map_or(w, |min| if w < min { min } else { w });
max.map_or(w, |max| if w > max { max } else { w })
}
fn trunc_str(
s: &str,
width: usize,
trunc_len: usize,
trunc_ch: char
) -> Cow<'_, str> {
let slen = strlen(s);
if slen > width {
let trunc = s[..width - trunc_len].to_string();
let cont = std::iter::repeat_n(trunc_ch, trunc_len).collect::<String>();
let s = format!("{trunc}{cont}");
Cow::from(s)
} else {
Cow::from(s)
}
}
#[cfg(test)]
mod tests {
use super::{clamp_width, trunc_str};
#[test]
fn truncing() {
assert_eq!(trunc_str("hello", 4, 1, '.').into_owned(), "hel.");
assert_eq!(trunc_str("hello", 4, 2, '.').into_owned(), "he..");
}
#[test]
fn clamping() {
assert_eq!(clamp_width(0, None, None), 0);
assert_eq!(clamp_width(0, Some(2), None), 2);
assert_eq!(clamp_width(8, Some(2), None), 8);
assert_eq!(clamp_width(0, None, Some(8)), 0);
assert_eq!(clamp_width(10, None, Some(8)), 8);
assert_eq!(clamp_width(0, Some(2), Some(8)), 2);
assert_eq!(clamp_width(4, Some(2), Some(8)), 4);
assert_eq!(clamp_width(10, Some(2), Some(8)), 8);
}
}