tablestream/
lib.rs

1//! TableStream is a tool for streaming tables out to the terminal.
2//! It will buffer some number of rows before first output and try to automatically
3//! determine appropriate widths for each column.
4//!
5//! ```
6//! # use std::io;
7//! # use tablestream::*;
8//! // Some sample data we want to show:
9//! struct City {
10//!     name: String,
11//!     country: String,
12//!     population: u32,
13//! }
14//! 
15//! impl City {
16//!     fn new(name: &str, country: &str, population: u32) -> Self {
17//!         Self { name: name.to_string(), country: country.to_string(), population, }
18//!     }
19//! }
20//! 
21//! fn largest_cities() -> Vec<City> {
22//!    vec![
23//!        City::new("Shanghai", "China", 24_150_000),
24//!        City::new("Beijing", "China", 21_700_000),
25//!        City::new("Lagos", "Nigeria", 21_320_000),
26//!    ]
27//! }
28//!
29//! let mut out = io::stdout();
30//! let mut stream = Stream::new(&mut out, vec![
31//!     // There are three different ways to specify which data to show in each column.
32//!     // 1. A closure that takes a formatter, and a reference to your type, and writes it out.
33//!     Column::new(|f, c: &City| write!(f, "{}", &c.name)).header("City"),
34//!      
35//!     // 2. Or we can use a shortcut macro to just show a single field:
36//!     // (It must implement fmt::Display)
37//!     col!(City: .country).header("Country"),
38//!
39//!     // 3. You can optionally specify a formatter:
40//!     // (Note: don't use padding/alignment in your formatters. TableStream will do that for you.)
41//!     col!(City: "{:.2e}", .population).header("Population"),
42//! ]);
43//!
44//! // Stream our data:
45//! for city in largest_cities() {
46//!    stream.row(city)?;
47//! }
48//! 
49//! stream.finish()?;
50//!  
51//! # Ok::<(), io::Error>(())
52//! ```
53
54use std::{
55    cmp::max,
56    fmt::{self, Write as FmtWrite},
57    io::{self, Write},
58    marker::PhantomData,
59    mem
60};
61
62use unicode_truncate::UnicodeTruncateStr;
63use unicode_width::UnicodeWidthStr;
64
65#[cfg(test)]
66mod tests;
67
68/// Allows printing rows of data to some io::Write.
69pub struct Stream<T, Out: Write> {
70    // User options:
71    columns: Vec<Column<T>>,
72    max_width: usize,
73    grow: Option<bool>,
74    output: Out,
75    borders: bool,
76    padding: bool,
77    title: Option<String>,
78
79    #[allow(dead_code)] // TODO
80    wrap: bool,
81
82    sizes_calculated: bool,
83    width: usize, // calculated.
84    buffer: Vec<T>,
85
86    // It's handy to have a long-lived string buffer so we don't have to continue to reallocate.
87    str_buf: String,
88
89    _pd: PhantomData<T>,
90}
91
92
93impl <T, Out: Write> Stream<T, Out> {
94    /// Create a new table streamer.
95    pub fn new(output: Out, columns: Vec<Column<T>>) -> Self {
96        let mut term_width = crossterm::terminal::size().map(|(w,_)| w as usize);
97        if cfg!(windows) {
98            // Windows Terminal has a weird bug. It seems to try to re-wrap text on resize. It does so if the
99            // text goes all the way to the edge of the terminal.  If we leave 1 colum extra, the behavior stops.
100            // 🤦‍♂️
101            // See: https://github.com/microsoft/terminal/issues/3088
102            term_width = term_width.map(|w| w - 1);
103        }
104
105        Self{
106            columns,
107            max_width: 0,
108            width: 0, // calculated later.
109            grow: None,
110            output,
111            wrap: false,
112            borders: false,
113            padding: true,
114            title: None,
115
116            sizes_calculated: false,
117            buffer: vec![],
118
119            str_buf: String::new(),
120
121            _pd: Default::default(),
122        }.max_width(
123            term_width.unwrap_or(80)
124        )
125    }
126
127    /// Enable right/left borders? (default: false)
128    pub fn borders(mut self, borders: bool) -> Self {
129        self.borders = borders;
130        let width = self.max_width;
131        self.max_width(width)
132    }
133
134    /// Set the maximum width for the table.
135    /// Note: this may be increased automatically for you if you've
136    /// specified columns, borders, dividers, and paddings with sizes
137    /// that require a larger max_width.
138    pub fn max_width(mut self, max_width: usize) -> Self {
139        let num_cols = self.columns.len();
140        let padding = if self.padding { 1 } else { 0 };
141        let dividers = (num_cols - 1) * (1 + 2*padding);
142        let border = if self.borders { 1 } else { 0 };
143        let borders = border * (border + padding) * 2;
144
145        let col_widths = self.columns.iter().map(|c| c.min_width).sum::<usize>();
146        let min_width = col_widths + borders + dividers;
147        self.max_width = max(max_width, min_width);
148
149        // If the user sets a long title, that likewise bumps up our max-width.
150        let title_width = self.title.as_ref().map(|t| t.width()).unwrap_or(0) + borders;
151        self.max_width = max(self.max_width, title_width);
152        
153        self
154    }
155
156    /// Enable horizontal padding around `|` dividers and inside external borders. (default: true)
157    pub fn padding(mut self, padding: bool) -> Self {
158        self.padding = padding;
159        let width = self.max_width;
160        self.max_width(width)
161    }
162
163    /// Should the table grow to fit its max_size?
164    /// 
165    /// Default behavior is determined by how much data we send to Stream.
166    /// 1. If we `.finish()` before the buffer is full, and determine that the table
167    ///    can be rendered smaller, then we will do so. (grow=false)
168    /// 2. If we fill the buffer and begin streaming mode, we'll grow the table so that
169    ///    there will be spare width later if we need it. (grow=true)
170    pub fn grow(mut self, grow: bool) -> Self {
171        self.grow = Some(grow);
172        self
173    }
174
175    /// Set a table title, to be displayed centered above the table.
176    pub fn title(mut self, title: &str) -> Self {
177        self.title = Some(title.to_string());
178        let width = self.max_width;
179        self.max_width(width)
180    }
181
182    /// Print a single row.
183    /// Note: Stream may buffer some rows before it begins output to calculate 
184    /// column sizes.
185    pub fn row(&mut self, data: T) -> io::Result<()> {
186
187        if self.sizes_calculated {
188            return self.print_row(data);
189        }
190        
191        self.buffer.push(data); 
192        if self.buffer.len() > 100 {
193            // Prefer to grow if unspecified, to allow extra space for rows to come:
194            self.grow = self.grow.or(Some(true));
195            self.write_buffer()?;
196        }
197        
198        Ok(())
199    }
200
201    fn write_buffer(&mut self) -> io::Result<()> {
202        self.calc_sizes()?;
203
204        self.print_headers()?;
205
206        let buffer = mem::replace(&mut self.buffer, vec![]);
207        for row in buffer {
208            self.print_row(row)?;
209        }
210
211        Ok(())
212    }
213
214    fn print_headers(&mut self) -> io::Result<()> {
215        self.hr()?;
216
217        let border_width = if self.borders { 1 } else { 0 } + if self.padding { 1 } else { 0 };
218        let title_width = self.width - (border_width * 2);
219
220        if let Some(title) = &self.title {
221            let title = title.clone();
222            self.border_left()?;
223            Alignment::Center.write(&mut self.output, title_width, &title)?;
224            self.border_right()?;
225            self.hr()?;
226        }
227
228        let has_headers = self.columns.iter().any(|c| c.header.is_some());
229        if has_headers {
230            let divider = if self.padding { " | " } else { "|" };
231            self.border_left()?;
232            for (i, col) in self.columns.iter().enumerate() {
233                if i > 0 {
234                    write!(&mut self.output, "{}", divider)?;
235                }
236                let name = col.header.as_ref().map(|h| h.as_str()).unwrap_or("");
237                Alignment::Center.write(&mut self.output, col.width, name)?;
238            }
239            self.border_right()?;
240            self.hr()?;
241        }
242
243        Ok(())
244    }
245
246    fn hr(&mut self) -> io::Result<()> {
247        writeln!(&mut self.output, "{1:-<0$}", self.width, "")
248    }
249
250    fn border_left(&mut self) -> io::Result<()> {
251        if self.borders {
252            let border = if self.padding { "| " } else { "|" };
253            write!(&mut self.output, "{}", border)?;
254        }
255        Ok(())
256    }
257    fn border_right(&mut self) -> io::Result<()> {
258        if self.borders {
259            let border = if self.padding { " |" } else { "|" };
260            writeln!(&mut self.output, "{}", border)
261        } else {
262            writeln!(&mut self.output, "")
263        }
264    }
265
266    fn print_row(&mut self, row: T) -> io::Result<()> {
267
268        let buf = &mut self.str_buf;
269        let out = &mut self.output;
270
271        if self.borders {
272            write!(out, "|")?;
273            if self.padding {
274                write!(out, " ")?;
275            }
276        }
277
278        for (i, col) in self.columns.iter().enumerate() {
279            if i > 0 {
280                if self.padding {
281                    write!(out, " | ")?;
282                } else {
283                    write!(out, "|")?;
284                }
285            }
286
287            buf.clear();
288            write!(
289                buf,
290                "{}", 
291                Displayer{ row: &row, writer: col.writer.as_ref() }
292            ).to_io()?;
293
294            col.alignment.write(out, col.width, buf.as_str())?;
295        }
296
297        if self.borders {
298            if self.padding {
299                write!(out, " ")?;
300            }
301            write!(out, "|")?;
302        }
303
304        writeln!(out, "")?;
305
306        Ok(())
307    }
308
309    fn calc_sizes(&mut self) -> io::Result<()> {
310        if self.sizes_calculated { return Ok(()); }
311        self.sizes_calculated = true; // or will be very soon. :p
312
313
314        for row in &self.buffer {
315            for col in self.columns.iter_mut() {
316                self.str_buf.clear();
317                write!(
318                    &mut self.str_buf,
319                    "{}",
320                    Displayer{ row, writer: col.writer.as_ref() }
321                ).to_io()?;
322                let width = self.str_buf.width();
323                col.max_width = max(col.max_width, width);
324                col.width_sum += width;
325            }
326        }
327
328        let num_cols = self.columns.len();
329        let padding = if self.padding { 1 } else { 0 };
330        let dividers = (num_cols - 1) * (1 + 2*padding);
331        let border = if self.borders { 1 } else { 0 };
332        let borders = border * (border + padding) * 2;
333        let available_width = self.max_width - borders - dividers;
334
335
336        // First attempt:
337        // Simple calculation: Just give every column its max width.
338        let col_width = |c: &Column<T>| { 
339            let mut width = max(
340                c.max_width,
341                c.header.as_ref().map(|h| h.len()).unwrap_or(0)
342            );
343            width = max(width, c.min_width);
344            width
345        };
346
347        let all_max: usize = self.columns.iter().map(col_width).sum();
348        if all_max < available_width {
349            // easy mode, just give everyone their max.
350
351            // also: distribute extra width to each column, if we want to grow:
352            let extra_width = if self.grow.unwrap_or(false) {
353                self.width = self.max_width;
354                available_width - all_max
355            } else {
356                self.width = all_max + dividers + borders;
357                0
358            };
359
360            let extra_per_col = extra_width / num_cols;
361            let mut extra_last_col = extra_width % num_cols;
362            for col in self.columns.iter_mut().rev() {
363                col.width = col_width(col) + extra_per_col + extra_last_col;
364                extra_last_col = 0;
365            }
366
367            return Ok(());
368        }
369
370        // What we have doesn't fit in the given width.
371        // BAD IDEA: Allocate cols according to how much we're trying to shove into them.
372        // This fails because one verbose column eats all the width, "penalizing" other columns
373        // that are displaying nice terse data.
374        // INSTEAD: "penalize" the verbose columns, by giving them the remainder after
375        // allowing the less verbose columns to use their max_width.
376
377        for big_cols in 1..=self.columns.len() {
378            // We expect that when verbose_cols=self.columns.len(), we'll just divide 
379            // the available columns among the columns. This should only fail in
380            // pathological cases where there are just too many cols to display period.
381            if self.penalize_big_cols(big_cols) {
382                self.width = self.max_width;
383                return Ok(())
384            }
385        }
386
387        // Should be guarded by the fact that we bump up max_width if user specifies wider columns.
388        panic!("Couldn't display {} columns worth of data in {} columns of text", self.columns.len(), self.max_width);
389    }
390
391    /// If we can get away w/ shrinking N biggest columns, do so
392    /// and return true.
393    fn penalize_big_cols(&mut self, num_big_cols: usize) -> bool {
394        let num_cols = self.columns.len();
395        let padding = if self.padding { 1 } else { 0 };
396        let dividers = (num_cols - 1) * (1 + 2*padding);
397        let border = if self.borders { 1 } else { 0 };
398        let borders = border * (border + padding) * 2;
399        let available_width = self.max_width - borders - dividers;
400
401        let mut col_refs: Vec<_> = self.columns.iter_mut().collect();
402        col_refs.sort_by_key(|c| c.width_sum); // sort "big" cols to the end:
403        let (small_cols, big_cols) = col_refs.split_at_mut(num_cols - num_big_cols);
404
405        let needed_width: usize = 
406            small_cols.iter().map(|c| max(c.min_width, c.max_width)).sum::<usize>()
407            + big_cols.iter().map(|c| c.min_width).sum::<usize>();
408
409        if needed_width > available_width {
410            return false
411        }
412
413        // Small cols all get their max width. Yay!
414        let mut remaining_width = available_width;
415        for col in small_cols.iter_mut() {
416            col.width = max(col.min_width, col.max_width);
417            remaining_width -= col.width;
418        }
419
420        // Big cols get assigned the remaining sizes.
421
422        // First pass, try assigning widths w/ simple algorithm.  If the column has a min_width that is
423        // larger, subtract the width from available cols, which we'll reallocate on the 2nd pass.
424        let mut big_cols_left = num_big_cols;
425        for col in big_cols.iter_mut() {
426            let cols_per_big_col = remaining_width / big_cols_left;
427            if cols_per_big_col < col.min_width {
428                col.width = col.min_width;
429                remaining_width -= col.width;
430                big_cols_left -= 1;
431            }
432        }
433
434        // Second pass: allocate remaining cols:
435        if big_cols_left > 0 {
436            let cols_per_big_col = remaining_width / big_cols_left;
437            for col in big_cols.iter_mut() {
438                if col.width > 0 { continue; } // already calculated.
439                col.width = cols_per_big_col;
440            }   
441            
442            remaining_width -= big_cols_left * cols_per_big_col;
443
444            // If we have any left, put it in the biggest column:
445            if remaining_width > 0 {
446                for col in big_cols.iter_mut().rev().take(1) {
447                    col.width += remaining_width;
448                }
449            }
450        }
451
452        true
453    }
454
455    /// Finish writing output.
456    /// This may write any items still in the buffer,
457    /// as well as a trailing horizontal line and footer.
458    pub fn finish(mut self) -> io::Result<()> {
459        if !self.buffer.is_empty() {
460            self.write_buffer()?;
461        }
462        self.hr()?;
463
464       
465        Ok(())
466    }
467
468    /// Like [`finish`], but adds a footer at the end as well.
469    pub fn footer(mut self, footer: &str) -> io::Result<()> {
470        if !self.buffer.is_empty() {
471            self.write_buffer()?;
472        }
473        
474        let border_width = if self.borders { 1 } else { 0 } + if self.padding { 1 } else { 0 };
475        let foot_width = self.width - (border_width * 2);
476
477        self.hr()?;
478        self.border_left()?;
479        Alignment::Center.write(&mut self.output, foot_width, &footer)?;
480        self.border_right()?;
481        self.hr()?;
482
483        Ok(())
484    }
485}
486
487/// Configure how we want to display a single column.
488pub struct Column<T> {
489    header: Option<String>,
490    writer: Box<dyn Fn(&mut fmt::Formatter, &T) -> fmt::Result>,
491
492    alignment: Alignment,
493
494    // Min size specified by user
495    min_width: usize,
496
497    // calculated size.
498    width: usize,
499
500    // Temp vars used while calculating the width:
501
502    max_width: usize, // max size encountered in buffer data.
503    width_sum: usize, // sum of widths of all rows. Used to weigh column widths.
504
505    _pd: PhantomData<T>,
506}
507
508impl <T> Column<T> {
509
510    /// Create a new Column with a Fn that knows how to extract one column of data from T.
511    pub fn new<F>(func: F) -> Self 
512    where F: (Fn(&mut fmt::Formatter, &T) -> fmt::Result) + 'static
513    {
514        Self {
515            header: None,
516            writer: Box::new(func),
517            alignment: Alignment::Left,
518
519            // a min-width of 1 means we'll always at least show there was *some* data in a col,
520            // even if it's truncated.
521            min_width: 1,
522            width: 0,
523            max_width: 0,
524            width_sum: 0,
525
526
527            _pd: Default::default(),
528        }
529    }
530
531    /// Set a column header.
532    ///
533    /// Note: This will increase the min_width of your column to the size of the header.
534    pub fn header(mut self, name: &str) -> Self {
535        self.header = Some(name.to_string());
536        self.min_width = max(self.min_width, name.len());
537        self
538    }
539
540    /// Set the minimum width of the column. (Default: 1)
541    ///
542    /// Note that setting widths of columns larger than the Stream.max_width will cause the
543    /// stream to expand its max_width to accomodate them.
544    pub fn min_width(mut self, min_width: usize) -> Self {
545        self.min_width = min_width;
546        self
547    }
548
549    /// Align left. (This is the default.)
550    pub fn left(mut self) -> Self {
551        self.alignment = Alignment::Left;
552        self
553    }
554
555    /// Align right.
556    pub fn right(mut self) -> Self {
557        self.alignment = Alignment::Right;
558        self
559    }
560
561    /// Center-align.
562    pub fn center(mut self) -> Self {
563        self.alignment = Alignment::Center;
564        self
565    }
566}
567
568enum Alignment {
569    Left,
570    Center,
571    Right,
572}
573
574impl Alignment {
575    // Write into a column of some width.
576    // Truncates to be no more than that size. 
577    // pads to be exactly that size.
578    fn write<W: io::Write>(&self, out: &mut W, col_width: usize, value: &str) -> io::Result<()> {
579        let (value, width) = value.unicode_truncate(col_width);
580        let (lpad, rpad) = match self {
581            Alignment::Left => (0, col_width - width),
582            Alignment::Right => (col_width - width, 0),
583            Alignment::Center => {
584                let padding = col_width - width;
585                let half = padding / 2;
586                let remainder = padding % 2;
587                (half, half + remainder)
588            }
589        };
590        // Note: We don't use Rust's built-in width formatter because
591        // it just counts chars. Do our own padding:
592        write!(out, "{0:1$}{3}{0:2$}", "", lpad, rpad, value)
593    }
594}
595
596trait ToIOResult {
597    fn to_io(self) -> io::Result<()>;
598}
599
600impl ToIOResult for fmt::Result {
601    fn to_io(self) -> io::Result<()> {
602        self.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
603    }
604}
605
606struct Displayer<'a, T> {
607    row: &'a T,
608    writer: &'a dyn Fn(&mut fmt::Formatter, &T) -> fmt::Result,
609}
610
611impl <'a, T> fmt::Display for Displayer<'a, T> {
612    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613        (self.writer)(f, self.row)
614    }
615}
616
617/// Create a new column. Saves some boilerplate vs. `Column::new(...)`.
618///
619/// See top-level docs for examples.
620// I wish I could use column!(), but that's already taken by Rust. 🤦‍♂️
621#[macro_export]
622macro_rules! col {
623    ($t:ty : .$field:ident) => {
624        $crate::Column::new(|f, row: &$t| write!(f, "{}", row.$field))
625    };
626    ($t:ty : $s:literal, $(.$field:ident),*) => {
627        $crate::Column::new(|f, row: &$t| write!(f, $s, $(row.$field)*,))
628    };
629}