Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,8 +1,8 @@ [package] name = "qpprint" -version = "0.3.0" +version = "0.3.1" edition = "2024" license = "0BSD" # https://crates.io/category_slugs categories = [ "text-processing" ] keywords = [ "cli", "console", "terminal", "format", "print" ] @@ -23,10 +23,11 @@ [badges] maintenance = { status = "experimental" } [dependencies] terminal_size = { version = "0.4.2" } +unicode-width = { version = "0.2.0" } yansi = { version = "1.0.1" } [lints.clippy] all = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } Index: examples/newtbl.rs ================================================================== --- examples/newtbl.rs +++ examples/newtbl.rs @@ -6,10 +6,12 @@ table1(); println!(); table2(); println!(); table3(); + println!(); + table4(); } fn table1() { // @@ -94,12 +96,29 @@ let mut data = Data::new(columns.len()); data.add_row(vec![4.into(), "hell".into(), "hell".into()]); data.add_row(vec![42.into(), "hello".into(), "hello".into()]); data.add_row(vec![11.into(), "world".into(), "world".into()]); + + let renderer = Renderer::new(&columns, &data).header(Some('=')); + + renderer.print(); +} + + +fn table4() { + let keycol = Column::new("Key", ToString::to_string); + let valcol = Column::new("Value", ToString::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(); } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -6,10 +6,11 @@ pub enum Types { Paragraph(String) } +#[derive(Copy, Clone, Debug)] pub enum Align { Left, Center, Right } Index: src/tbl.rs ================================================================== --- src/tbl.rs +++ src/tbl.rs @@ -2,10 +2,12 @@ borrow::Cow, fmt, iter::{self, zip} }; +use unicode_width::UnicodeWidthStr; + use yansi::Painted; pub use super::Align; @@ -229,10 +231,20 @@ pub fn print(&self) { println!("{self}"); } } +/// Calculate the width (in units of number of terminal character cells) of a +/// unicode string. +/// +/// Apparently this is not an exact science. It should work well enough as +/// long as no one tries to be too creative. +#[inline] +fn strlen(s: &str) -> usize { + //c.title.chars().count() + s.width() +} impl fmt::Display for Renderer<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // // Iterate over cells and generate a new rendered table. @@ -248,11 +260,11 @@ // // If a column min and/or max widths have been configured, the column // widths will be clamped later. // let mut col_widths: Vec = if self.show_header { - self.cols.iter().map(|c| c.title.len()).collect() + self.cols.iter().map(|c| strlen(&c.title)).collect() } else { vec![0; self.cols.len()] }; // @@ -282,11 +294,11 @@ // ToDo: Cut down to size if cell_str.len() exceeds column's max_width //if cell_str.len() > col_width {} - *cw = std::cmp::max(*cw, cell_str.len()); + *cw = std::cmp::max(*cw, strlen(&cell_str)); rrow.push(cell_str); } rendered.push(rrow); } @@ -308,21 +320,12 @@ // Print column headers // for (col, cw) in std::iter::zip(self.cols, &col_widths) { let title = trunc_str(&col.title, *cw, col.trunc_len, col.trunc_ch); - match col.title_align { - Align::Left => { - fields.push(format!("{title: { - fields.push(format!("{title:^cw$}")); - } - Align::Right => { - fields.push(format!("{title:>cw$}")); - } - } + let cc = format_cell(&title, *cw, col.title_align); + fields.push(cc); } writeln!(f, "{}", fields.join(&colspace))?; // @@ -351,41 +354,72 @@ { let cell = trunc_str(&cell, *cw, col.trunc_len, col.trunc_ch); if let Some(ref stylize) = col.stylize { let cell = stylize(cv, &cell); - match col.cell_align { - Align::Left => { - fields.push(format!("{cell: { - fields.push(format!("{cell:^cw$}")); - } - Align::Right => { - fields.push(format!("{cell:>cw$}")); - } - } + let cc = format_painted_cell(&cell, *cw, col.cell_align); + fields.push(cc); } else { - match col.cell_align { - Align::Left => { - fields.push(format!("{cell: { - fields.push(format!("{cell:^cw$}")); - } - Align::Right => { - fields.push(format!("{cell:>cw$}")); - } - } + 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 => { + //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); + 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, + 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, max: Option) -> 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 }) @@ -395,13 +429,13 @@ s: &str, width: usize, trunc_len: usize, trunc_ch: char ) -> Cow { - if s.len() > width { - let trunc = s[..s.len() - trunc_len - 1].to_string(); - //let cont = iter::repeat(trunc_ch).take(trunc_len).collect::(); + let slen = strlen(s); + if slen > width { + let trunc = s[..slen - trunc_len - 1].to_string(); let cont = std::iter::repeat_n(trunc_ch, trunc_len).collect::(); let s = format!("{trunc}{cont}"); Cow::from(s) } else { Cow::from(s) Index: www/changelog.md ================================================================== --- www/changelog.md +++ www/changelog.md @@ -2,18 +2,35 @@ ⚠️ indicates a breaking change. ## [Unreleased] -[Details](/vdiff?from=qpprint-0.3.0&to=trunk) +[Details](/vdiff?from=qpprint-0.3.1&to=trunk) ### Added ### Changed ### Removed +--- + +## [0.3.1] - 2025-05-30 + +[Details](/vdiff?from=qpprint-0.3.0&to=qpprint-0.3.1) + +### Added + +- Derive `Copy`, `Clone` and `Debug` for `Align`. + +### Changed + +- Fix cell width calculation when it contains unicode characters that are wider + than a single character cell when drawn to the terminal. Apparently this + isn't an exact science, but should work as long as one doesn't try to be too + creative. + --- ## [0.3.0] - 2025-05-16 [Details](/vdiff?from=qpprint-0.2.1&to=qpprint-0.3.0)