qpprint

Check-in Differences
Login

Check-in Differences

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Difference From qpprint-0.3.0 To qpprint-0.3.1

2025-05-30
12:54
Merge meta. check-in: 7e55bb9041 user: jan tags: trunk
12:09
Merge from trunk to get latest unicode width updates. check-in: 8551ed53a3 user: jan tags: meta
11:13
Release maintenance. check-in: c838aa0bea user: jan tags: qpprint-0.3.1, trunk
11:07
Merge. check-in: be1d708848 user: jan tags: trunk
2025-05-16
22:15
Start work on fixing cell width when using multibyte utf-8 characters. check-in: 764f26c1b7 user: jan tags: unicode
21:08
Release maintenance. check-in: 735e2540e6 user: jan tags: qpprint-0.3.0, trunk
21:05
Update edition & msrv. Happy clippy. check-in: bd0251b533 user: jan tags: trunk

Changes to Cargo.toml.
1
2
3

4
5
6
7
8
9
10
1
2

3
4
5
6
7
8
9
10


-
+







[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" ]
repository = "https://repos.qrnch.tech/pub/qpprint"
description = "Simple console printing/formatting."
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35
36
37
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38







+











# https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section
[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 }
nursery = { level = "warn", priority = -1 }
cargo = { level = "warn", priority = -1 }

multiple_crate_versions = "allow"

Changes to examples/newtbl.rs.
1
2
3
4
5
6
7
8
9
10


11
12
13
14
15
16
17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19










+
+







use qpprint::tbl::{Align, CellValue, Column, Data, Renderer};

use yansi::{Color, Painted};

fn main() {
  table1();
  println!();
  table2();
  println!();
  table3();
  println!();
  table4();
}


fn table1() {
  //
  // Define table columns
  //
92
93
94
95
96
97
98

















99
100
101
102
103
104
105
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+








  let columns = [idcol, namecol, namecol2];

  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 :
Changes to src/lib.rs.
1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18










+







pub mod tbl;

use terminal_size::{terminal_size, Height, Width};

pub use yansi::{Color, Painted};

pub enum Types {
  Paragraph(String)
}

#[derive(Copy, Clone, Debug)]
pub enum Align {
  Left,
  Center,
  Right
}

pub struct Column {
Changes to src/tbl.rs.
1
2
3
4
5
6


7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15






+
+







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 {
227
228
229
230
231
232
233










234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253

254
255
256
257
258
259
260
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264

265
266
267
268
269
270
271
272







+
+
+
+
+
+
+
+
+
+



















-
+







  }

  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.
    // Every cell here is a String
    //
    let mut rendered: Vec<Vec<String>> = Vec::with_capacity(self.data.len());

    //
    // Used to keep track of auto-detected column widths.
    //
    // If the renderer is configured to show a header, then initialize to the
    // column titles.  Otherwise initialize to 0.
    //
    // If a column min and/or max widths have been configured, the column
    // widths will be clamped later.
    //
    let mut col_widths: Vec<usize> = 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()]
    };

    //
    // Convert table of CellValues into a table of Strings.
    //
280
281
282
283
284
285
286
287

288
289
290
291
292
293
294
292
293
294
295
296
297
298

299
300
301
302
303
304
305
306







-
+







      {
        let cell_str = (col.renderer)(cell);

        // 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);
    }

    //
306
307
308
309
310
311
312
313

314
315

316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
318
319
320
321
322
323
324

325


326








327
328
329
330
331
332
333







-
+
-
-
+
-
-
-
-
-
-
-
-







    if self.show_header {
      //
      // 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 {
        let cc = format_cell(&title, *cw, col.title_align);
          Align::Left => {
            fields.push(format!("{title:<cw$}"));
        fields.push(cc);
          }
          Align::Center => {
            fields.push(format!("{title:^cw$}"));
          }
          Align::Right => {
            fields.push(format!("{title:>cw$}"));
          }
        }
      }
      writeln!(f, "{}", fields.join(&colspace))?;


      //
      // Print heading underline
      //
349
350
351
352
353
354
355
356

357
358

359
360
361
362
363
364
365
366
367
368

369
370

371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386

















































387
388
389
390
391
392
393
394
395
396
397
398
399

400
401


402
403
404
405
406
407
408
409
352
353
354
355
356
357
358

359


360








361

362


363








364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434


435
436

437
438
439
440
441
442
443







-
+
-
-
+
-
-
-
-
-
-
-
-

-
+
-
-
+
-
-
-
-
-
-
-
-








+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+













+
-
-
+
+
-







      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(cv, &cell);
          match col.cell_align {
          let cc = format_painted_cell(&cell, *cw, col.cell_align);
            Align::Left => {
              fields.push(format!("{cell:<cw$}"));
          fields.push(cc);
            }
            Align::Center => {
              fields.push(format!("{cell:^cw$}"));
            }
            Align::Right => {
              fields.push(format!("{cell:>cw$}"));
            }
          }
        } else {
          match col.cell_align {
          let cc = format_cell(&cell, *cw, col.cell_align);
            Align::Left => {
              fields.push(format!("{cell:<cw$}"));
          fields.push(cc);
            }
            Align::Center => {
              fields.push(format!("{cell:^cw$}"));
            }
            Align::Right => {
              fields.push(format!("{cell:>cw$}"));
            }
          }
        }
      }

      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<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 s.len() > width {
    let trunc = s[..s.len() - trunc_len - 1].to_string();
  if slen > width {
    let trunc = s[..slen - trunc_len - 1].to_string();
    //let cont = iter::repeat(trunc_ch).take(trunc_len).collect::<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)
  }
}
Changes to www/changelog.md.
1
2
3
4
5
6
7

8
9
10
11
12
13
14

















15
16
17
18
19
20
21
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38






-
+







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







# Change Log

⚠️  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)

### Changed