qpprint

Check-in Differences
Login

Check-in Differences

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

Difference From qpprint-0.3.1 To trunk

2025-05-30
14:43
Begin brainstorming a redesign using a trait for renderer instead of closures. Leaf check-in: bb24ce375f user: jan tags: renderer-trait
14:26
Pass colmeta to callbacks. Leaf check-in: 6db8cf1b39 user: jan tags: trunk
13:28
Up version and changelog update. check-in: edbbe960cd user: jan tags: trunk
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

Changes to Cargo.toml.
1
2
3
4
5
6
7
8
9
10
[package]
name = "qpprint"
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."


|







1
2
3
4
5
6
7
8
9
10
[package]
name = "qpprint"
version = "0.4.0"
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."
Changes to examples/newtbl.rs.
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
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
  //
  let columns = [
    Column::new("Id", ToString::to_string),
    Column::new("Name", ToString::to_string)
  ];

  //
  // Create raw table data representation, with the number of columns defined
  // by `columns`.
  //
  let mut data = Data::new(columns.len());
|











>
>








|
|







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
use qpprint::tbl::{Align, CellData, CellValue, Column, Data, Renderer};

use yansi::{Color, Painted};

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


fn table1() {
  //
  // Define table columns
  //
  let columns = [
    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`.
  //
  let mut data = Data::new(columns.len());
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
64
65

66
67

68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

87
88
89
90
91
92
93
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
  renderer.print();
}


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

  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)
    .max_width(4)
    .trunc_style(2, '.');
  let namecol2 = Column::new("Title", ToString::to_string)
    .max_width(4)
    .trunc_style(1, '…');

  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 :







|
>
|
|
|
|
|
|
|
|
|
|
>
|
|
>
|
|
|
|
|
|
|



|
|







>
|
|


|

















|
|







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
  renderer.print();
}


/// 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", |_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
      }
    });
  let columns = [idcol, namecol];

  let mut data = Data::new(columns.len());
  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", |_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", |_cm, cd| cd.to_string())
    .max_width(4)
    .trunc_style(1, '…');

  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", |_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::<ColMeta, CellMeta>::new("Key", |_cm, cd| cd.to_string());
  let valcol =
    Column::<ColMeta, CellMeta>::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 :
Changes to src/tbl.rs.
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
39
40
41
42

43
44
45
46
47
48
49

use yansi::Painted;

pub use super::Align;


#[allow(clippy::type_complexity)]
pub struct Column {
  title: String,
  min_width: Option<usize>,
  max_width: Option<usize>,
  trunc_len: usize,
  trunc_ch: char,
  renderer: Box<dyn Fn(&CellValue) -> String>,

  stylize: Option<Box<dyn Fn(&CellValue, &str) -> Painted<String>>>,
  title_align: Align,
  cell_align: Align

}

impl Column {
  #[allow(clippy::needless_pass_by_value)]
  pub fn new(
    heading: impl ToString,
    renderer: impl Fn(&CellValue) -> 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

    }
  }

  #[must_use]
  pub fn min_width(mut self, min: usize) -> Self {
    self.min_width_ref(min);
    self







|





|
>
|

|
>


|



|










|
>







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
39
40
41
42
43
44
45
46
47
48
49
50
51
52

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
    self.trunc_ch = ch;
    self
  }

  #[must_use]
  pub fn stylize(
    mut self,
    f: impl Fn(&CellValue, &str) -> Painted<String> + 'static
  ) -> Self {
    self.stylize = Some(Box::new(f));
    self
  }

  #[must_use]
  pub const fn title_align(mut self, align: Align) -> Self {







|







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
    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 {
116
117
118
119
120
121
122











123
124
125
126
127
128
129
    self
  }

  pub const fn cell_align_ref(&mut self, align: Align) -> &mut Self {
    self.cell_align = align;
    self
  }











}


pub enum CellValue {
  Str(String),
  U64(u64)
}







>
>
>
>
>
>
>
>
>
>
>







119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
    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)
}
152
153
154
155
156
157
158



































































159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
      Self::Str(s) => write!(f, "{s}"),
      Self::U64(v) => write!(f, "{v}")
    }
  }
}





































































pub struct Data {
  num_cols: usize,
  cells: Vec<Vec<CellValue>>
}

impl Data {
  #[must_use]
  pub const fn new(cols: usize) -> Self {
    Self {
      num_cols: cols,
      cells: Vec::new()
    }
  }

  /// # Panics
  /// The row length must equal to the number of columns.
  pub fn add_row(&mut self, row: Vec<CellValue>) {
    assert_eq!(row.len(), self.num_cols);
    self.cells.push(row);
  }
}

pub trait CellRender {
  fn stringify();

  fn print();
}


pub struct Renderer<'a> {
  show_header: bool,
  header_underline: Option<char>,
  col_spacing: usize,
  cols: &'a [Column],
  data: &'a [Vec<CellValue>]
}

impl<'a> Renderer<'a> {
  #[must_use]
  pub fn new(cols: &'a [Column], data: &'a Data) -> Self {
    Self {
      show_header: false,
      header_underline: None,
      col_spacing: 2,
      cols,
      data: &data.cells
    }







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|

|


|










|












|



|
|


|

|







166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
      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}")
    }
  }
}


// Rename to TableData?
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()
    }
  }

  /// # Panics
  /// The row length must equal to the number of columns.
  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
    }
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/// 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());








|







321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
/// long as no one tries to be too creative.
#[inline]
fn strlen(s: &str) -> usize {
  //c.title.chars().count()
  s.width()
}

impl<CM, CDM> 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
    //
    let mut rendered: Vec<Vec<String>> = Vec::with_capacity(self.data.len());

286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
        rrow.push(cell_str);
      }
      */

      for (cw, (col, cell)) in
        col_widths.iter_mut().zip(iter::zip(self.cols, row))
      {
        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, strlen(&cell_str));
        rrow.push(cell_str);







|







367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
        rrow.push(cell_str);
      }
      */

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

        // 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, strlen(&cell_str));
        rrow.push(cell_str);
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

      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);
          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 => {
      //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$}", "", "")







|

















<







432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456

457
458
459
460
461
462
463

      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$}", "", "")
Changes to tests/expect.rs.
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

72
73
74
75
76
77
78
79
80
81
82
83
use qpprint::tbl::{Align, Column, Data, Renderer};

// With no explicit width constraints, a long title should set the column
// witdth:
//
// A long title
// ~~~~~~~~~~~~
// short
#[test]
fn title_constrains_width() {
  let columns = [Column::new("A long title", ToString::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");
}

// With no explicit width constraints, a cell should set the column
// witdth:
//
// Title
// ~~~~~~~~~~~
// A long cell
#[test]
fn cell_constrains_width() {
  let columns =
    [Column::new("Title", ToString::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)
    .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();
  assert_eq!(res, "Id   \n~~~~~\n");
}

#[test]
fn center_align_title() {
  let columns = [Column::new("I", ToString::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();
  assert_eq!(res, "  I  \n~~~~~\n");
}

#[test]
fn right_align_title() {
  let columns = [Column::new("I", ToString::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();
  assert_eq!(res, "    I\n~~~~~\n");
}


#[test]
fn trunc_cells() {

  let idcol = Column::new("Id", ToString::to_string).cell_align(Align::Right);
  let namecol = Column::new("Name", ToString::to_string)
    .max_width(4)
    .trunc_style(2, '.');
  let namecol2 = Column::new("Title", ToString::to_string)
    .max_width(4)
    .trunc_style(1, '…');

  let columns = [idcol, namecol, namecol2];

  let mut data = Data::new(columns.len());
  data.add_row(vec![4.into(), "hell".into(), "hell".into()]);










|
>
>















|
|









|










|










|











>
|
|


|







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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
use qpprint::tbl::{Align, Column, Data, Renderer};

// With no explicit width constraints, a long title should set the column
// witdth:
//
// A long title
// ~~~~~~~~~~~~
// short
#[test]
fn title_constrains_width() {
  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");
}

// With no explicit width constraints, a cell should set the column
// witdth:
//
// Title
// ~~~~~~~~~~~
// A long cell
#[test]
fn cell_constrains_width() {
  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", |_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();
  assert_eq!(res, "Id   \n~~~~~\n");
}

#[test]
fn center_align_title() {
  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();
  assert_eq!(res, "  I  \n~~~~~\n");
}

#[test]
fn right_align_title() {
  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();
  assert_eq!(res, "    I\n~~~~~\n");
}


#[test]
fn trunc_cells() {
  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", |_cm, cd| cd.to_string())
    .max_width(4)
    .trunc_style(1, '…');

  let columns = [idcol, namecol, namecol2];

  let mut data = Data::new(columns.len());
  data.add_row(vec![4.into(), "hell".into(), "hell".into()]);
Changes to www/changelog.md.
1
2
3
4
5
6
7
8
9
10
11



12
13
14
15
16
17
18
# Change Log

⚠️  indicates a breaking change.

## [Unreleased]

[Details](/vdiff?from=qpprint-0.3.1&to=trunk)

### Added

### Changed




### Removed

---

## [0.3.1] - 2025-05-30












>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Change Log

⚠️  indicates a breaking change.

## [Unreleased]

[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

---

## [0.3.1] - 2025-05-30