Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,8 +1,8 @@ [package] name = "qpprint" -version = "0.3.1" +version = "0.4.0" edition = "2024" license = "0BSD" # https://crates.io/category_slugs categories = [ "text-processing" ] keywords = [ "cli", "console", "terminal", "format", "print" ] Index: examples/newtbl.rs ================================================================== --- examples/newtbl.rs +++ examples/newtbl.rs @@ -1,6 +1,6 @@ -use qpprint::tbl::{Align, CellValue, Column, Data, Renderer}; +use qpprint::tbl::{Align, CellData, CellValue, Column, Data, Renderer}; use yansi::{Color, Painted}; fn main() { table1(); @@ -8,20 +8,22 @@ table2(); println!(); table3(); println!(); table4(); + println!(); + table5(); } fn table1() { // // Define table columns // let columns = [ - Column::new("Id", ToString::to_string), - Column::new("Name", ToString::to_string) + Column::<(), ()>::new("Id", |_cm, cd| cd.to_string()), + Column::<(), ()>::new("Name", |_cm, cd| cd.to_string()) ]; // // Create raw table data representation, with the number of columns defined // by `columns`. @@ -50,47 +52,51 @@ /// Print `Id` as blue if it is greater than `20`. Print `Name` as red if it /// is equal to `world`. fn table2() { - let idcol = Column::new("Id", ToString::to_string).stylize(|cv, s| { - let cs = Painted::new(s.to_string()); - if let CellValue::U64(id) = cv { - if *id > 20 { - cs.fg(Color::Blue) + let idcol = Column::<(), ()>::new("Id", |_cm, cd| cd.to_string()).stylize( + |_cm, cd, s| { + let cs = Painted::new(s.to_string()); + if let CellValue::U64(id) = cd.val { + if id > 20 { + cs.fg(Color::Blue) + } else { + cs + } + } else { + cs + } + } + ); + let namecol = + Column::new("Name", |_cm, cd| cd.to_string()).stylize(|_cm, _cv, s| { + let cs = Painted::new(s.to_string()); + if s == "world" { + cs.fg(Color::Red) } else { cs } - } else { - cs - } - }); - let namecol = Column::new("Name", ToString::to_string).stylize(|_cv, s| { - let cs = Painted::new(s.to_string()); - if s == "world" { - cs.fg(Color::Red) - } else { - cs - } - }); + }); let columns = [idcol, namecol]; let mut data = Data::new(columns.len()); - data.add_row(vec![CellValue::U64(42), CellValue::Str("hello".into())]); - data.add_row(vec![CellValue::U64(11), CellValue::Str("world".into())]); + data.add_row(vec![CellData::u64(42), CellData::str("hello")]); + data.add_row(vec![CellData::u64(11), CellData::str("world")]); let renderer = Renderer::new(&columns, &data).header(Some('-')); renderer.print(); } fn table3() { - let idcol = Column::new("Id", ToString::to_string).cell_align(Align::Right); - let namecol = Column::new("Name", ToString::to_string) + let idcol = Column::<(), ()>::new("Id", |_cm, cd| cd.to_string()) + .cell_align(Align::Right); + let namecol = Column::new("Name", |_cm, cd| cd.to_string()) .max_width(4) .trunc_style(2, '.'); - let namecol2 = Column::new("Title", ToString::to_string) + let namecol2 = Column::new("Title", |_cm, cd| cd.to_string()) .max_width(4) .trunc_style(1, '…'); let columns = [idcol, namecol, namecol2]; @@ -104,21 +110,72 @@ renderer.print(); } fn table4() { - let keycol = Column::new("Key", ToString::to_string); - let valcol = Column::new("Value", ToString::to_string); + let keycol = Column::<(), ()>::new("Key", |_cm, cd| cd.to_string()); + let valcol = Column::new("Value", |_cm, cd| cd.to_string()); let columns = [keycol, valcol]; let mut data = Data::new(columns.len()); data.add_row(vec!["hell".into(), "hell".into()]); data.add_row(vec!["hello🔒".into(), "hello".into()]); data.add_row(vec!["world".into(), "world".into()]); + + let renderer = Renderer::new(&columns, &data).header(Some('=')); + + renderer.print(); +} + + +struct ColMeta {} + +struct CellMeta { + default: String +} + +fn table5() { + let keycol = + Column::::new("Key", |_cm, cd| cd.to_string()); + let valcol = + Column::::new("Value", |_cm, cd| cd.to_string()) + .stylize(|_cm, cd, s| { + let cs = Painted::new(s.to_string()); + if let Some(ref meta) = cd.meta { + if meta.default == s { + // cell value == cell meta default + cs.fg(Color::BrightBlue) + } else { + cs + } + } else { + cs + } + }); + + let columns = [keycol, valcol]; + + let mut data = Data::new(columns.len()); + + let cd = CellData { + val: "some value".into(), + meta: Some(CellMeta { + default: "another value".into() + }) + }; + data.add_row(vec!["foo".into(), cd]); + + let cd = CellData { + val: "another value".into(), + meta: Some(CellMeta { + default: "another value".into() + }) + }; + data.add_row(vec!["bar".into(), cd]); let renderer = Renderer::new(&columns, &data).header(Some('=')); renderer.print(); } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/tbl.rs ================================================================== --- src/tbl.rs +++ src/tbl.rs @@ -10,27 +10,29 @@ pub use super::Align; #[allow(clippy::type_complexity)] -pub struct Column { +pub struct Column { title: String, min_width: Option, max_width: Option, trunc_len: usize, trunc_ch: char, - renderer: Box String>, - stylize: Option Painted>>, + renderer: Box, &CellData) -> String>, + stylize: + Option, &CellData, &str) -> Painted>>, title_align: Align, - cell_align: Align + cell_align: Align, + colmeta: Option } -impl Column { +impl Column { #[allow(clippy::needless_pass_by_value)] pub fn new( heading: impl ToString, - renderer: impl Fn(&CellValue) -> String + 'static + renderer: impl Fn(Option<&CM>, &CellData) -> String + 'static ) -> Self { Self { title: heading.to_string(), min_width: None, max_width: None, @@ -37,11 +39,12 @@ trunc_len: 0, trunc_ch: '…', renderer: Box::new(renderer), stylize: None, title_align: Align::Center, - cell_align: Align::Left + cell_align: Align::Left, + colmeta: None } } #[must_use] pub fn min_width(mut self, min: usize) -> Self { @@ -91,11 +94,11 @@ } #[must_use] pub fn stylize( mut self, - f: impl Fn(&CellValue, &str) -> Painted + 'static + f: impl Fn(Option<&CM>, &CellData, &str) -> Painted + 'static ) -> Self { self.stylize = Some(Box::new(f)); self } @@ -118,10 +121,21 @@ 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), @@ -154,16 +168,83 @@ } } } -pub struct Data { +pub struct CellData { + pub val: CellValue, + pub meta: Option +} + +impl CellData { + #[must_use] + pub fn str(val: impl Into) -> 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 From for CellData { + fn from(val: String) -> Self { + Self { + val: CellValue::from(val), + meta: None + } + } +} + +impl From<&str> for CellData { + fn from(val: &str) -> Self { + Self { + val: CellValue::from(val), + meta: None + } + } +} + +impl From for CellData { + fn from(val: u64) -> Self { + Self { + val: CellValue::from(val), + meta: None + } + } +} + +impl fmt::Display for CellData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.val { + CellValue::Str(s) => write!(f, "{s}"), + CellValue::U64(v) => write!(f, "{v}") + } + } +} + + +// Rename to TableData? +pub struct Data { num_cols: usize, - cells: Vec> + cells: Vec>> } -impl Data { +impl Data { #[must_use] pub const fn new(cols: usize) -> Self { Self { num_cols: cols, cells: Vec::new() @@ -170,11 +251,11 @@ } } /// # Panics /// The row length must equal to the number of columns. - pub fn add_row(&mut self, row: Vec) { + pub fn add_row(&mut self, row: Vec>) { assert_eq!(row.len(), self.num_cols); self.cells.push(row); } } @@ -183,21 +264,21 @@ fn print(); } -pub struct Renderer<'a> { +pub struct Renderer<'a, CM, CDM> { show_header: bool, header_underline: Option, col_spacing: usize, - cols: &'a [Column], - data: &'a [Vec] + cols: &'a [Column], + data: &'a [Vec>] } -impl<'a> Renderer<'a> { +impl<'a, CM, CDM> Renderer<'a, CM, CDM> { #[must_use] - pub fn new(cols: &'a [Column], data: &'a Data) -> Self { + pub fn new(cols: &'a [Column], data: &'a Data) -> Self { Self { show_header: false, header_underline: None, col_spacing: 2, cols, @@ -242,11 +323,11 @@ fn strlen(s: &str) -> usize { //c.title.chars().count() s.width() } -impl fmt::Display for Renderer<'_> { +impl fmt::Display for Renderer<'_, CM, CDM> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // // Iterate over cells and generate a new rendered table. // Every cell here is a String // @@ -288,11 +369,11 @@ */ for (cw, (col, cell)) in col_widths.iter_mut().zip(iter::zip(self.cols, row)) { - let cell_str = (col.renderer)(cell); + let cell_str = (col.renderer)(col.colmeta.as_ref(), cell); // ToDo: Cut down to size if cell_str.len() exceeds column's max_width //if cell_str.len() > col_width {} @@ -353,11 +434,11 @@ 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(cv, &cell); + 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); @@ -371,11 +452,10 @@ } fn format_cell(s: &str, cell_width: usize, align: Align) -> String { match align { Align::Left => { - //println!("cell_width={cell_width}, strlen(s)={}", strlen(s)); let pad = cell_width - strlen(s); format!("{s}{:pad$}", "") } Align::Center => { let pad = cell_width - strlen(s); Index: tests/expect.rs ================================================================== --- tests/expect.rs +++ tests/expect.rs @@ -6,11 +6,13 @@ // A long title // ~~~~~~~~~~~~ // short #[test] fn title_constrains_width() { - let columns = [Column::new("A long title", ToString::to_string)]; + let columns = [Column::<(), ()>::new("A long title", |_cm, cd| { + cd.to_string() + })]; let mut data = Data::new(columns.len()); data.add_row(vec!["short".into()]); let renderer = Renderer::new(&columns, &data).header(Some('~')); let res = renderer.to_string(); assert_eq!(res, "A long title\n~~~~~~~~~~~~\nshort \n"); @@ -22,22 +24,22 @@ // Title // ~~~~~~~~~~~ // A long cell #[test] fn cell_constrains_width() { - let columns = - [Column::new("Title", ToString::to_string).title_align(Align::Left)]; + let columns = [Column::<(), ()>::new("Title", |_cm, cd| cd.to_string()) + .title_align(Align::Left)]; let mut data = Data::new(columns.len()); data.add_row(vec!["A long cell".into()]); let renderer = Renderer::new(&columns, &data).header(Some('~')); let res = renderer.to_string(); assert_eq!(res, "Title \n~~~~~~~~~~~\nA long cell\n"); } #[test] fn left_align_title() { - let columns = [Column::new("Id", ToString::to_string) + let columns = [Column::<(), ()>::new("Id", |_cm, cd| cd.to_string()) .min_width(5) .title_align(Align::Left)]; let data = Data::new(columns.len()); let renderer = Renderer::new(&columns, &data).header(Some('~')); let res = renderer.to_string(); @@ -44,11 +46,11 @@ assert_eq!(res, "Id \n~~~~~\n"); } #[test] fn center_align_title() { - let columns = [Column::new("I", ToString::to_string) + let columns = [Column::<(), ()>::new("I", |_cm, cd| cd.to_string()) .min_width(5) .title_align(Align::Center)]; let data = Data::new(columns.len()); let renderer = Renderer::new(&columns, &data).header(Some('~')); let res = renderer.to_string(); @@ -55,11 +57,11 @@ assert_eq!(res, " I \n~~~~~\n"); } #[test] fn right_align_title() { - let columns = [Column::new("I", ToString::to_string) + let columns = [Column::<(), ()>::new("I", |_cm, cd| cd.to_string()) .min_width(5) .title_align(Align::Right)]; let data = Data::new(columns.len()); let renderer = Renderer::new(&columns, &data).header(Some('~')); let res = renderer.to_string(); @@ -67,15 +69,16 @@ } #[test] fn trunc_cells() { - let idcol = Column::new("Id", ToString::to_string).cell_align(Align::Right); - let namecol = Column::new("Name", ToString::to_string) + let idcol = Column::<(), ()>::new("Id", |_cm, cd| cd.to_string()) + .cell_align(Align::Right); + let namecol = Column::new("Name", |_cm, cd| cd.to_string()) .max_width(4) .trunc_style(2, '.'); - let namecol2 = Column::new("Title", ToString::to_string) + let namecol2 = Column::new("Title", |_cm, cd| cd.to_string()) .max_width(4) .trunc_style(1, '…'); let columns = [idcol, namecol, namecol2]; Index: www/changelog.md ================================================================== --- www/changelog.md +++ www/changelog.md @@ -7,10 +7,13 @@ [Details](/vdiff?from=qpprint-0.3.1&to=trunk) ### Added ### Changed + +- ⚠️ Add generic types that applications can use to add metadata to columns and + cells. ### Removed ---