Skip to main content

nestable/
lib.rs

1//! Rust implementation of a nested table renderer for terminals and fixed
2//! font size displays
3
4use std::cmp::{max, min};
5use std::collections::HashMap;
6use std::fmt;
7use std::fmt::Formatter;
8use unicode_width::UnicodeWidthChar;
9
10/// The Table struct defines the tables contents, styling, attributes and
11/// alignment rules.
12///
13/// Applications create instances of Table, populate them using the add_rows
14/// and add_row method and optionally specify rendering attributes and
15/// alignment rules on the table, column, row or cell level.
16#[derive(Clone, Debug)]
17pub struct Table<'a> {
18    rows: Vec<Row>,
19    style: &'a Style,
20    invert_header: bool,
21    border_attr: Attributes,
22    table_attr: Option<Attributes>,
23    col_alt_attr: [Option<Attributes>; 2],
24    col_attr: HashMap<usize, Attributes>,
25    row_alt_attr: [Option<Attributes>; 2],
26    row_attr: HashMap<usize, Attributes>,
27    cell_attr: HashMap<(usize, usize), Attributes>,
28    table_align: (Horizontal, Vertical),
29    col_align: HashMap<usize, (Option<Horizontal>, Option<Vertical>)>,
30    row_align: HashMap<usize, (Option<Horizontal>, Option<Vertical>)>,
31    cell_align: HashMap<(usize, usize), (Option<Horizontal>, Option<Vertical>)>,
32}
33
34/// A Row is struct which just holds a vector of strings, each representing the
35/// content of each column.
36///
37/// Each individual string may contain line feeds and ansi escape sequences.
38#[derive(Clone, Debug)]
39struct Row {
40    cells: Vec<String>,
41}
42
43/// Index offsets in style arrays
44#[repr(usize)]
45pub enum StyleIdx {
46    Left = 0,
47    Pad = 1,
48    Right = 2,
49    Top = 3,
50    Inter = 4,
51    Bot = 5,
52}
53
54impl StyleIdx {
55    /// Provides the number of StyleIdx entries
56    pub const COUNT: usize = 6;
57}
58
59/// A Style is defined very loosely as 4 named arrays (top, row, sep, bot), each
60/// containing 6 strings, the meaning of which is indicated by their correcponding
61/// StyleIdx entry.
62///
63/// A typical example is shown here:
64///
65///  pub const GLYPHS_ROUNDED: Style = Style {
66///      top: ["╭", "─", "╮", "┬", "┼", "┴"],
67///      row: ["│", "─", "│", "│", "│", "│"],
68///      sep: ["├", "─", "┤", "┬", "┼", "┴"],
69///      bot: ["╰", "─", "╯", "┬", "┼", "┴"],
70///  };
71#[derive(Clone, Debug)]
72pub struct Style {
73    pub top: [&'static str; StyleIdx::COUNT],
74    pub row: [&'static str; StyleIdx::COUNT],
75    pub sep: [&'static str; StyleIdx::COUNT],
76    pub bot: [&'static str; StyleIdx::COUNT],
77}
78
79/// Attributes are a pair of strings which turn on and off specific ansi
80/// terminal rendering options.
81#[derive(Clone, Debug)]
82pub struct Attributes {
83    on: String,
84    off: String,
85}
86
87/// A LayoutLine holds the text of a line in a cell and its display width so that
88/// we only have to compute that width once
89#[derive(Clone, Debug)]
90struct LayoutLine {
91    line: String,
92    width: usize,
93}
94
95/// A LayoutCell holds 0 or more LayoutLines and the maximum display width of all
96/// the individual lines
97#[derive(Clone, Debug)]
98struct LayoutCell {
99    lines: Vec<LayoutLine>,
100    width: usize,
101}
102
103/// A LayoutRow holds a cell for each column which is used on the row
104#[derive(Clone, Debug)]
105struct LayoutRow {
106    cells: Vec<LayoutCell>,
107    height: usize,
108}
109
110/// A LayoutTable holds all the LayoutRows for a table and the tables stylistic
111/// configuration.
112///
113/// This is constructed prior to printing by converting an input Table.
114#[derive(Clone, Debug)]
115struct LayoutTable<'a> {
116    config: &'a Table<'a>,
117    rows: Vec<LayoutRow>,
118    widths: Vec<usize>,
119}
120
121/// The styles modules provides various ways to control how your table's borders
122/// are rendered.
123///
124/// It provides various implementations of the Style struct. Style implementations
125/// can also be provided by the hosting application and are free to include escape
126/// sequences and utf-8 characters if required.
127#[rustfmt::skip]
128pub mod styles {
129    use super::Style;
130
131    pub const ASCII_BORDER: Style = Style {
132        top: ["+", "-", "+", "+", "+", "+"],
133        row: ["|", "-", "|", "|", "|", "|"],
134        sep: ["+", "-", "+", "+", "+", "+"],
135        bot: ["+", "-", "+", "+", "+", "+"],
136    };
137
138    pub const ASCII_NO_BORDER: Style = Style {
139        top: ["", "",  "", "",  "",  "" ],
140        row: ["", " ", "", "|", "|", "|"],
141        sep: ["", "-", "", "+", "+", "+"],
142        bot: ["", "",  "", "",  "",  "" ],
143    };
144
145    pub const ASCII_NO_ROW_SEPARATOR: Style = Style {
146        top: ["", "",  "", "",  "",  "" ],
147        row: ["", " ", "", "|", "|", "|"],
148        sep: ["", "",  "", "",  "",  "" ],
149        bot: ["", "",  "", "",  "",  "" ],
150    };
151
152    pub const GLYPHS_SQUARE: Style = Style {
153        top: ["┌", "─", "┐", "┬", "┼", "┴"],
154        row: ["│", "─", "│", "│", "│", "│"],
155        sep: ["├", "─", "┤", "┬", "┼", "┴"],
156        bot: ["└", "─", "┘", "┬", "┼", "┴"],
157    };
158
159    pub const GLYPHS_ROUNDED: Style = Style {
160        top: ["╭", "─", "╮", "┬", "┼", "┴"],
161        row: ["│", "─", "│", "│", "│", "│"],
162        sep: ["├", "─", "┤", "┬", "┼", "┴"],
163        bot: ["╰", "─", "╯", "┬", "┼", "┴"],
164    };
165
166    pub const GLYPHS_ROUNDED_SPACED: Style = Style {
167        top: ["╭─", "─", "─╮", "─┬─", "─┼─", "─┴─"],
168        row: ["│ ", "─", " │", " │ ", " │ ", " │ "],
169        sep: ["├─", "─", "─┤", "─┬─", "─┼─", "─┴─"],
170        bot: ["╰─", "─", "─╯", "─┬─", "─┼─", "─┴─"],
171    };
172
173    pub const GLYPHS_NO_BORDER: Style = Style {
174        top: ["", "",  "", "",  "",  "" ],
175        row: ["", "─", "", "│", "│", "│"],
176        sep: ["", "─", "", "┬", "┼", "┴"],
177        bot: ["", "",  "", "",  "",  "" ],
178    };
179
180    pub const GLYPHS_NO_ROW_SEPARATOR: Style = Style {
181        top: ["", "",  "", "",  "",  "" ],
182        row: ["", " ", "", "│", "│", "│"],
183        sep: ["", "",  "", "",  "",  "" ],
184        bot: ["", "",  "", "",  "",  "" ],
185    };
186
187    pub const SPACE: Style = Style {
188        top: ["", "",  "", "", "",  ""],
189        row: ["", " ", "", "", " ", ""],
190        sep: ["", "",  "", "", "",  ""],
191        bot: ["", "",  "", "", "",  ""],
192    };
193
194    pub const HEAVY: Style = Style {
195        top: ["┏", "━", "┓", "┳", "╋", "┻"],
196        row: ["┃", "━", "┃", "┃", "┃", "┃"],
197        sep: ["┣", "━", "┫", "┳", "╋", "┻"],
198        bot: ["┗", "━", "┛", "┳", "╋", "┻"],
199    };
200
201    pub const DOUBLE: Style = Style {
202        top: ["╔", "═", "╗", "╦", "╬", "╩"],
203        row: ["║", "═", "║", "║", "║", "║"],
204        sep: ["╠", "═", "╣", "╦", "╬", "╩"],
205        bot: ["╚", "═", "╝", "╦", "╬", "╩"],
206    };
207
208    pub const HEAVY_MIXED: Style = Style {
209        top: ["┏", "─", "┓", "┳", "╋", "┻"],
210        row: ["│", "─", "│", "│", "│", "│"],
211        sep: ["┣", "─", "┫", "┳", "╋", "┻"],
212        bot: ["┗", "─", "┛", "┳", "╋", "┻"],
213    };
214
215    pub const DOUBLE_HEAVY: Style = Style {
216        top: ["╔", "━", "╗", "╦", "╬", "╩"],
217        row: ["┃", "━", "┃", "┃", "┃", "┃"],
218        sep: ["╠", "━", "╣", "╦", "╬", "╩"],
219        bot: ["╚", "━", "╝", "╦", "╬", "╩"],
220    };
221}
222
223/// Provides useful ansi escape sequences which can be used to decorate a table.
224#[rustfmt::skip]
225pub mod ansi {
226    pub const RESET:      &str = "\x1b[0;0m";
227
228    pub const BOLD:       &str = "\x1b[1m";
229    pub const DIM:        &str = "\x1b[2m";
230    pub const ITALIC:     &str = "\x1b[3m";
231    pub const UNDERLINE:  &str = "\x1b[4m";
232    pub const INVERT:     &str = "\x1b[7m";
233    pub const INVERT_OFF: &str = "\x1b[27m";
234
235    pub const BLACK:   &str = "\x1b[30m";
236    pub const RED:     &str = "\x1b[31m";
237    pub const GREEN:   &str = "\x1b[32m";
238    pub const YELLOW:  &str = "\x1b[33m";
239    pub const BLUE:    &str = "\x1b[34m";
240    pub const MAGENTA: &str = "\x1b[35m";
241    pub const CYAN:    &str = "\x1b[36m";
242    pub const WHITE:   &str = "\x1b[37m";
243
244    pub const BRIGHT_BLACK:   &str = "\x1b[90m";
245    pub const BRIGHT_RED:     &str = "\x1b[91m";
246    pub const BRIGHT_GREEN:   &str = "\x1b[92m";
247    pub const BRIGHT_YELLOW:  &str = "\x1b[93m";
248    pub const BRIGHT_BLUE:    &str = "\x1b[94m";
249    pub const BRIGHT_MAGENTA: &str = "\x1b[95m";
250    pub const BRIGHT_CYAN:    &str = "\x1b[96m";
251    pub const BRIGHT_WHITE:   &str = "\x1b[97m";
252
253    pub const BG_BLACK:   &str = "\x1b[40m";
254    pub const BG_RED:     &str = "\x1b[41m";
255    pub const BG_GREEN:   &str = "\x1b[42m";
256    pub const BG_YELLOW:  &str = "\x1b[43m";
257    pub const BG_BLUE:    &str = "\x1b[44m";
258    pub const BG_MAGENTA: &str = "\x1b[45m";
259    pub const BG_CYAN:    &str = "\x1b[46m";
260    pub const BG_WHITE:   &str = "\x1b[47m";
261
262    pub const BG_BRIGHT_BLACK:   &str = "\x1b[100m";
263    pub const BG_BRIGHT_RED:     &str = "\x1b[101m";
264    pub const BG_BRIGHT_GREEN:   &str = "\x1b[102m";
265    pub const BG_BRIGHT_YELLOW:  &str = "\x1b[103m";
266    pub const BG_BRIGHT_BLUE:    &str = "\x1b[104m";
267    pub const BG_BRIGHT_MAGENTA: &str = "\x1b[105m";
268    pub const BG_BRIGHT_CYAN:    &str = "\x1b[106m";
269    pub const BG_BRIGHT_WHITE:   &str = "\x1b[107m";
270
271    /// Provides the fg escape sequence for 256 colours.
272    pub fn fg_256(n: u8) -> String {
273        format!("\x1b[38;5;{}m", n)
274    }
275
276    /// Provides the fg escape sequence for 24 bit colours.
277    pub fn fg_rgb(r: u8, g: u8, b: u8) -> String {
278        format!("\x1b[38;2;{};{};{}m", r, g, b)
279    }
280
281    /// Provides the bg escape sequence for 256 colours.
282    pub fn bg_256(n: u8) -> String {
283        format!("\x1b[48;5;{}m", n)
284    }
285
286    /// Provides the bg escape sequence for 24 bit colours.
287    pub fn bg_rgb(r: u8, g: u8, b: u8) -> String {
288        format!("\x1b[48;2;{};{};{}m", r, g, b)
289    }
290}
291
292/// Specifies horizontal alignment options
293#[derive(Clone, Debug, Copy)]
294pub enum Horizontal {
295    Left,
296    Centre,
297    Right,
298}
299
300/// Specifies vertical alignment options
301#[derive(Clone, Debug, Copy)]
302pub enum Vertical {
303    Top,
304    Centre,
305    Bottom,
306}
307
308/// Determines the display width of a given utf8 string.
309fn display_width(input: &str) -> usize {
310    let mut chars = input.chars().peekable();
311    let mut width = 0;
312
313    while let Some(c) = chars.next() {
314        if c == '\x1b' {
315            // Try to parse CSI: ESC [
316            if let Some('[') = chars.peek().copied() {
317                chars.next(); // consume '['
318
319                // Consume until final byte (@ to ~)
320                let mut found_final = false;
321
322                while let Some(next) = chars.next() {
323                    if ('@'..='~').contains(&next) {
324                        found_final = true;
325                        break;
326                    }
327                }
328
329                if !found_final {
330                    eprintln!("Warning: unterminated ANSI escape sequence");
331                }
332
333                continue;
334            } else {
335                eprintln!("Warning: unsupported ANSI escape sequence");
336                continue;
337            }
338        }
339
340        // Normal character width
341        match UnicodeWidthChar::width(c) {
342            Some(w) => width += w,
343            None => {
344                // Fallback pictograph hack
345                let double = ('\u{1F300}'..='\u{1FAFF}').contains(&c);
346                width += if double { 2 } else { 1 };
347            }
348        }
349    }
350
351    width
352}
353
354/// Provides ctors for the Row
355impl Row {
356    /// Populates a Row from a list of str's
357    fn new<I, S>(input: I) -> Self
358    where
359        I: IntoIterator<Item = S>,
360        S: Into<String>,
361    {
362        let cells = input.into_iter().map(Into::into).collect();
363
364        Self { cells: cells }
365    }
366}
367
368/// Provides the Table implementation
369impl<'a> Table<'a> {
370    /// Provides an empty table
371    pub fn new() -> Self {
372        Self {
373            rows: Vec::new(),
374            invert_header: true,
375            style: &styles::GLYPHS_ROUNDED,
376            border_attr: Attributes::new(),
377            table_attr: None,
378            col_alt_attr: Default::default(),
379            col_attr: HashMap::new(),
380            row_alt_attr: Default::default(),
381            row_attr: HashMap::new(),
382            cell_attr: HashMap::new(),
383            table_align: (Horizontal::Left, Vertical::Top),
384            col_align: HashMap::new(),
385            row_align: HashMap::new(),
386            cell_align: HashMap::new(),
387        }
388    }
389
390    /// Sets the border style for rendering the table
391    pub fn set_table_style(mut self, style: &'a Style) -> Self {
392        self.style = style;
393        self
394    }
395
396    /// Sets header inversion on or off for first row
397    pub fn set_invert_header(mut self, value: bool) -> Self {
398        self.invert_header = value;
399        self
400    }
401
402    /// Sets the border attribute
403    pub fn set_border_attr(mut self, attr: Attributes) -> Self {
404        self.border_attr = attr;
405        self
406    }
407
408    /// Sets a default attribute for the entire table
409    pub fn set_table_attr(mut self, attr: Attributes) -> Self {
410        self.table_attr = Some(attr);
411        self
412    }
413
414    /// Sets attributes for alternating columns
415    pub fn set_column_alt_attr(mut self, even: Option<Attributes>, odd: Option<Attributes>) -> Self {
416        self.col_alt_attr = [even, odd];
417        self
418    }
419
420    /// Sets the attribute for a specific column
421    pub fn set_column_attr(mut self, index: usize, attr: Attributes) -> Self {
422        self.col_attr.insert(index, attr);
423        self
424    }
425
426    /// Sets attributes for alternating rows
427    pub fn set_row_alt_attr(mut self, even: Option<Attributes>, odd: Option<Attributes>) -> Self {
428        self.row_alt_attr = [even, odd];
429        self
430    }
431
432    /// Sets the attribute for a specific row
433    pub fn set_row_attr(mut self, index: usize, attr: Attributes) -> Self {
434        self.row_attr.insert(index, attr);
435        self
436    }
437
438    /// Sets the attribute for a specific cell
439    pub fn set_cell_attr(mut self, row: usize, col: usize, attr: Attributes) -> Self {
440        self.cell_attr.insert((row, col), attr);
441        self
442    }
443
444    /// Determines attribute to use for a given cell, giving priority to cell
445    /// over row, over column, over table
446    fn resolve_attr(&self, row: usize, col: usize) -> Option<&Attributes> {
447        self.cell_attr
448            .get(&(row, col))
449            .or_else(|| self.row_attr.get(&row))
450            .or_else(|| {
451                if self.invert_header && row == 0 {
452                    None
453                } else {
454                    let offset = self.invert_header as usize;
455                    self.row_alt_attr[(row + offset) % 2].as_ref()
456                }
457            })
458            .or_else(|| self.col_attr.get(&col))
459            .or_else(|| self.col_alt_attr[col % 2].as_ref())
460            .or(self.table_attr.as_ref())
461    }
462
463    /// Set default table alignment
464    pub fn set_table_align(mut self, horizontal: Horizontal, vertical: Vertical) -> Self {
465        self.table_align = (horizontal, vertical);
466        self
467    }
468
469    /// Set column alignment
470    pub fn set_col_align(mut self, col: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
471        self.col_align.insert(col, (h, v));
472        self
473    }
474
475    /// Set row alignment
476    pub fn set_row_align(mut self, row: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
477        self.row_align.insert(row, (h, v));
478        self
479    }
480
481    /// Set cell alignment
482    pub fn set_cell_align(mut self, r: usize, c: usize, h: Option<Horizontal>, v: Option<Vertical>) -> Self {
483        self.cell_align.insert((r, c), (h, v));
484        self
485    }
486
487    /// Determines horizontal and vertical alignment for a given cell
488    fn resolve_align(&self, row: usize, col: usize) -> (Horizontal, Vertical) {
489        let mut h = None;
490        let mut v = None;
491
492        if let Some((ch, cv)) = self.cell_align.get(&(row, col)) {
493            h = *ch;
494            v = *cv;
495        }
496
497        if h.is_none() || v.is_none() {
498            if let Some((rh, rv)) = self.row_align.get(&row) {
499                h = h.or(*rh);
500                v = v.or(*rv);
501            }
502        }
503
504        if h.is_none() || v.is_none() {
505            if let Some((ch, cv)) = self.col_align.get(&col) {
506                h = h.or(*ch);
507                v = v.or(*cv);
508            }
509        }
510
511        (h.unwrap_or(self.table_align.0), v.unwrap_or(self.table_align.1))
512    }
513
514    /// Add multiple rows to the table
515    pub fn add_rows<I, R, S>(mut self, input: I) -> Self
516    where
517        I: IntoIterator<Item = R>,
518        R: IntoIterator<Item = S>,
519        S: Into<String>,
520    {
521        self.rows.extend(input.into_iter().map(Row::new));
522        self
523    }
524
525    /// Add a single row to an immutable table
526    pub fn add_row<I, S>(mut self, input: I) -> Self
527    where
528        I: IntoIterator<Item = S>,
529        S: Into<String>,
530    {
531        self.rows.push(Row::new(input));
532        self
533    }
534
535    /// Push a single row to a mutable table
536    pub fn push_row<I, S>(&mut self, input: I)
537    where
538        I: IntoIterator<Item = S>,
539        S: Into<String>,
540    {
541        self.rows.push(Row::new(input));
542    }
543}
544
545impl Attributes {
546    /// Constructs an empty attribute
547    pub fn new() -> Self {
548        Self { on: "".to_string(), off: "".to_string() }
549    }
550
551    /// Constructs Attributes from a str
552    pub fn with(value: &str) -> Self {
553        Self { on: value.to_string(), off: ansi::RESET.to_string() }
554    }
555
556    /// Constructs Atrributes from a list of str's
557    pub fn as_styled<I, S>(input: I) -> Self
558    where
559        I: IntoIterator<Item = S>,
560        S: Into<String>,
561    {
562        Self { on: input.into_iter().map(|s| s.into()).collect::<String>(), off: ansi::RESET.to_string() }
563    }
564}
565
566/// Convenience macro to construct Attributes via attr![str, ...]
567#[macro_export]
568macro_rules! attr {
569    ( $( $x:expr ),* $(,)? ) => {
570        Attributes::as_styled([ $( $x ),* ])
571    };
572}
573
574/// Convenience macro to construct a list of (name, style) pairs.
575#[macro_export]
576macro_rules! styles {
577    ( $( $x:ident ),* ) => {
578        [ $( (stringify!($x), styles::$x) ),* ]
579    };
580}
581
582impl LayoutLine {
583    /// Creates a LayoutLine entry from an input string.
584    fn new(line: &str) -> Self {
585        let width = display_width(&line);
586        Self { line: line.to_string(), width: width }
587    }
588}
589
590impl LayoutCell {
591    /// Creates a LayoutCell entry from an input string.
592    ///
593    /// This also provides the splitting by line feeds and determines the
594    /// the widest entry in the cell.
595    pub fn new(line: &str) -> Self {
596        let mut max_width = 0;
597
598        let lines: Vec<LayoutLine> = line
599            .lines()
600            .map(|l| {
601                let layout_line = LayoutLine::new(&l);
602                max_width = max(layout_line.width, max_width);
603                layout_line
604            })
605            .collect();
606
607        Self { lines: lines, width: max_width }
608    }
609}
610
611impl LayoutRow {
612    /// This simply splits the incoming Row (containing cells with linefeed
613    /// delimited lines) into LayoutCells
614    pub fn new(row: &Row) -> Self {
615        let mut max_height = 0;
616
617        let cells: Vec<LayoutCell> = row
618            .cells
619            .iter()
620            .map(|c| {
621                let cell = LayoutCell::new(&c);
622                max_height = max(cell.lines.len(), max_height);
623                cell
624            })
625            .collect();
626
627        Self { cells: cells, height: max_height }
628    }
629
630    /// Determine the column offsets for a given row
631    pub fn offsets(&self, widths: &Vec<usize>, full_width: usize) -> Vec<usize> {
632        let mut result = Vec::<usize>::new();
633        let mut total = 0;
634        let count = self.cells.len();
635        let decrease = if count == widths.len() { 0 } else { 1 };
636        for index in 0..count - decrease {
637            total += widths[index];
638            result.push(total);
639        }
640        result.push(full_width);
641        result
642    }
643}
644
645impl<'a> LayoutTable<'a> {
646    /// This constructor converts a simple table to a layout table - it ensures
647    /// all column widths are correctly calculated to fit the widest cell, and
648    /// it additionally ensures that rows which do not have a uniform number of
649    /// columns only affect the width to the left of the last column unless their
650    /// combined width overflow the current width
651    fn new(table: &'a Table<'a>) -> Self {
652        let mut max_cols = 0;
653        let mut min_cols = usize::MAX;
654
655        // Convert table.row to layout.rows and determine the maximum number of columns on any row
656        let rows: Vec<LayoutRow> = table
657            .rows
658            .iter()
659            .map(|r| {
660                let row = LayoutRow::new(&r);
661                max_cols = max(max_cols, row.cells.len());
662                min_cols = min(min_cols, row.cells.len());
663                row
664            })
665            .collect();
666
667        // Allocate the column width vector and track rows which have spanning columns
668        let mut widths = Vec::with_capacity(max_cols);
669        widths.resize(max_cols, 0);
670
671        // Allocate a vec to track largest spanning item and which column its starts on
672        let mut map = Vec::with_capacity(max_cols);
673        map.resize(max_cols, 0);
674
675        // For each row which matches the number of entries in widths, populate with the maximum width
676        for row in &rows {
677            if row.cells.len() == max_cols {
678                for cell in 0..row.cells.len() {
679                    widths[cell] = max(widths[cell], row.cells[cell].width);
680                }
681            } else if row.cells.len() > 0 {
682                for cell in 0..row.cells.len() - 1 {
683                    widths[cell] = max(widths[cell], row.cells[cell].width);
684                }
685                let index = row.cells.len() - 1;
686                map[index] = max(map[index], row.cells[index].width);
687            }
688        }
689
690        // Extend all columns to the right of each overflowing spanning entry
691        let mut total = widths.iter().sum();
692        for index in 0..map.len() {
693            let span = map[index];
694            if span != 0 {
695                let width: usize = widths[0..index].iter().sum();
696                let required = width + span;
697                if required > total {
698                    let delta = required - total;
699                    let cols = max_cols - index;
700                    let extra_per_col = delta / cols;
701                    let remainder = delta % cols;
702                    for i in index..max_cols {
703                        widths[i] += extra_per_col;
704                    }
705                    for i in index..index + remainder {
706                        widths[i] += 1;
707                    }
708                    total = required;
709                }
710            }
711        }
712
713        Self { config: table, rows: rows, widths: widths }
714    }
715
716    /// Determine the column offsets for a given row index
717    fn offsets(&self, index: usize, widths: &Vec<usize>, full_width: usize) -> Vec<usize> {
718        self.rows.get(index).map(|r| r.offsets(widths, full_width)).unwrap_or_else(|| vec![full_width])
719    }
720}
721
722impl fmt::Display for Table<'_> {
723    /// This displays a Table by first converting to a LayoutTable and then
724    /// iterating through each row to render all the individual cells and
725    /// ensuring the broader style is applied
726    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
727        let table = LayoutTable::new(self);
728        if table.rows.len() > 0 && table.widths.len() > 0 {
729            // The draw lambda is responsible from drawing the borders and inter row separators.
730            // it's a no-op if all of the entries are empty strings
731            let draw =
732                |f: &mut Formatter<'_>, style: &[&str; 6], cols: usize, ante: &Vec<usize>, post: &Vec<usize>| -> fmt::Result {
733                    if style[0].len() + style[1].len() + style[2].len() + style[3].len() > 0 {
734                        let mut off = 0;
735                        let mut ain = 0;
736                        let mut pin = 0;
737                        let mut cols = cols;
738                        let end = *ante.last().unwrap();
739                        write!(f, "{}{}", self.border_attr.on, style[StyleIdx::Left as usize])?;
740                        while off < end {
741                            let width;
742                            let glyph;
743                            if ante[ain] == post[pin] {
744                                width = post[pin] - off;
745                                glyph = style[StyleIdx::Inter as usize];
746                                ain += 1;
747                                pin += 1;
748                            } else if ante[ain] < post[pin] {
749                                width = ante[ain] - off;
750                                glyph = style[StyleIdx::Bot as usize];
751                                ain += 1;
752                            } else {
753                                width = post[pin] - off;
754                                glyph = style[StyleIdx::Top as usize];
755                                pin += 1;
756                            }
757
758                            off += width;
759                            cols -= 1;
760
761                            if off < end {
762                                write!(f, "{}", style[StyleIdx::Pad as usize].repeat(width))?;
763                                write!(f, "{}", glyph)?;
764                            } else {
765                                let width = width + cols * display_width(glyph);
766                                write!(f, "{}", style[StyleIdx::Pad as usize].repeat(width))?;
767                            }
768                        }
769                        writeln!(f, "{}{}", style[StyleIdx::Right as usize], self.border_attr.off)?;
770                    }
771                    Ok(())
772                };
773
774            // Lambda for turning on invert
775            let invert_on = |row: usize| -> &str {
776                let active = row == 0 && table.config.invert_header;
777                if active { ansi::INVERT } else { "" }
778            };
779
780            let invert_off = |row: usize| -> &str {
781                let active = row == 0 && table.config.invert_header;
782                if active { ansi::INVERT_OFF } else { "" }
783            };
784
785            // Lambda for turning cell attributes on
786            let attr_on =
787                |row: usize, col: usize| -> &str { table.config.resolve_attr(row, col).map_or("", |attr| attr.on.as_str()) };
788
789            // Lambda for turning cell attributes off
790            let attr_off =
791                |row: usize, col: usize| -> &str { table.config.resolve_attr(row, col).map_or("", |attr| attr.off.as_str()) };
792
793            // This calculates the full width of every row
794            let full_width: usize = table.widths.iter().sum();
795
796            // This indicates the current style and sizes the column separator
797            let mut style = &table.config.style.top;
798            let csw = display_width(table.config.style.row[StyleIdx::Inter as usize]);
799
800            // Detemine column widths of previous and current rows
801            let mut ante = vec![full_width];
802            let mut post = table.offsets(0, &table.widths, full_width);
803
804            // Obtain ansi sequences requested for the border
805            let on = &table.config.border_attr.on;
806            let off = &table.config.border_attr.off;
807
808            // This loop renders the entire table
809            for (index, row) in table.rows.iter().enumerate() {
810                draw(f, style, table.widths.len(), &ante, &post)?;
811                style = &table.config.style.sep;
812                for i in 0..row.height {
813                    let mut remaining = full_width;
814                    write!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Left as usize], off)?;
815                    for (col, cell) in row.cells.iter().enumerate() {
816                        // Determine alignment for this cell
817                        let align = table.config.resolve_align(index, col);
818                        let horizontal = align.0;
819                        let vertical = align.1;
820
821                        // Handle vertical alignment
822                        let oob = cell.lines.len();
823                        let offset = match vertical {
824                            Vertical::Top => 0,
825                            Vertical::Centre => row.height / 2 - oob / 2,
826                            Vertical::Bottom => row.height - oob,
827                        };
828                        let i = (i >= offset).then(|| i - offset).unwrap_or(oob);
829
830                        // Determine line and width of line
831                        let line: (&str, usize) =
832                            if i < cell.lines.len() { (&cell.lines[i].line, cell.lines[i].width) } else { ("", 0) };
833
834                        // Determine the padding required
835                        let line_width = line.1;
836                        let is_last = col == row.cells.len() - 1;
837                        let span = table.widths.len() - row.cells.len();
838                        let is_spanning = span > 0 && is_last;
839
840                        let padding = if !is_spanning {
841                            let pad = table.widths[col] - line_width;
842                            remaining -= line_width + pad;
843                            pad
844                        } else {
845                            remaining - line_width + csw * span
846                        };
847
848                        // Horizontal alignment
849                        let left = padding / 2;
850                        let lr = match horizontal {
851                            Horizontal::Left => (0, padding),
852                            Horizontal::Centre => (left, padding - left),
853                            Horizontal::Right => (padding, 0),
854                        };
855
856                        // Render line in cell
857                        write!(f, "{}", invert_on(index))?;
858                        write!(f, "{}", attr_on(index, col))?;
859                        write!(f, "{:width$}", "", width = lr.0)?;
860                        write!(f, "{}", line.0)?;
861                        write!(f, "{:width$}", "", width = lr.1)?;
862                        write!(f, "{}", attr_off(index, col))?;
863                        write!(f, "{}", invert_off(index))?;
864
865                        // Handle inner and right hand borders
866                        if col < row.cells.len() - 1 {
867                            write!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Inter as usize], off)?;
868                        } else {
869                            writeln!(f, "{}{}{}", on, table.config.style.row[StyleIdx::Right as usize], off)?;
870                        }
871                    }
872                }
873
874                // Derive ante and post for following row
875                ante = post;
876                post = table.offsets(index + 1, &table.widths, full_width);
877            }
878
879            // Draw the outer lower border
880            draw(f, &table.config.style.bot, table.widths.len(), &ante, &post)?;
881        }
882        Ok(())
883    }
884}