Skip to main content

rich/
table.rs

1//! Tables.
2//!
3//! Port of upstream `rich/table.py` (core subset). A [`Table`] lays out columns
4//! and rows inside a box, sizing each column to its widest cell.
5//!
6//! Scope: headers, rows, box choice (with legacy/ASCII substitution), per-cell
7//! padding, **`pad_edge`** + **`show_edge`** + **`collapse_padding`**, header
8//! styling (incl. a per-column header-content span and a per-column header-cell
9//! fill), a **table-level style** + **border style**,
10//! multi-line/wrapped cells (with **ellipsis overflow**), **shrink-to-fit** +
11//! **expand** column widths, per-column justify, **explicit width**, per-column
12//! **`ratio`/`min_width`/`max_width`**, **per-column style**, **`no_wrap`**,
13//! title, caption, and `show_lines`; the table-level `width` / `min_width`,
14//! footers (`show_footer`, column `footer` / `footer_style`), `leading`,
15//! alternating `row_styles`, per-row `style` and sections (`add_section`,
16//! `end_section`), `header_style` / `footer_style` / `title_style` /
17//! `caption_style`, `title_justify` / `caption_justify` and `safe_box`.
18//! A row with more cells than columns adds columns, as upstream's `add_row`
19//! does. Headers and cells may be styled [`Text`]
20//! (`add_column_text`, `add_row_text`), as upstream accepts renderables; plain
21//! strings (`add_column`, `add_row`) are console markup, as upstream's `str`
22//! cells are (see [`Cell::Markup`]).
23//! Deferred (tracked in the Table issue): the rare width-0 column padding edge.
24
25use std::sync::Arc;
26
27use crate::console::{Console, ConsoleOptions, Justify, Overflow};
28use crate::measure::Measurement;
29use crate::protocol::{LineRenderable, Renderable};
30use crate::r#box::{Box as BoxSet, RowLevel, HEAVY_HEAD};
31use crate::segment::Segment;
32use crate::style::{Style, StyleType};
33use crate::text::Text;
34
35/// A single column definition. Mirrors the used subset of `rich.table.Column`.
36struct Column {
37    header: Cell,
38    /// The footer cell (port of `Column.footer`), shown with `show_footer`.
39    footer: Cell,
40    /// A per-column footer *cell* style, combined over the table-level
41    /// `footer_style` (port of `Column.footer_style`).
42    footer_fill: Option<Style>,
43    /// Vertical alignment of body cells (port of `Column.vertical`).
44    vertical: crate::align::VerticalAlign,
45    /// Highlight `str` cells (port of `Column.highlight`); `None` takes the
46    /// table's `highlight`, as `add_column(highlight=None)` does.
47    highlight: Option<bool>,
48    justify: Justify,
49    /// An explicit content width; when set, the column doesn't shrink to fit.
50    width: Option<usize>,
51    /// A style applied to this column's body cells.
52    style: Style,
53    /// An extra style span applied to the header *content* only (over the base
54    /// `header_style`), leaving the header padding as `header_style`. Mirrors
55    /// upstream stylizing the heading `Text` (e.g. `markdown.table.header`).
56    header_content_style: Option<Style>,
57    /// A per-column header *cell* style — combined over the table-level
58    /// `header_style` to fill the whole header cell (content + padding). Port of
59    /// `Column.header_style` (as used by e.g. rich-cli's numeric columns).
60    header_fill: Option<Style>,
61    /// When set, the column flexes to this share of the free width when the table
62    /// is `expand`ed (port of `Column.ratio`; makes the column "flexible").
63    ratio: Option<usize>,
64    /// A floor on the column's content width (port of `Column.min_width`).
65    min_width: Option<usize>,
66    /// A cap on the column's content width — wider cells wrap (port of
67    /// `Column.max_width`).
68    max_width: Option<usize>,
69    /// When set, cells are never wrapped — they crop to one line (with ellipsis).
70    no_wrap: bool,
71    /// How over-long cell text is handled (upstream `Column.overflow`,
72    /// default `"ellipsis"`). A cell `Text`'s own overflow wins.
73    overflow: Overflow,
74}
75
76impl Column {
77    /// A column with every upstream default and the given header / justify.
78    fn new(header: Cell, justify: Justify) -> Self {
79        Column {
80            header,
81            footer: Cell::Markup(String::new()),
82            footer_fill: None,
83            highlight: None,
84            vertical: crate::align::VerticalAlign::Top,
85            justify,
86            width: None,
87            style: Style::new(),
88            header_content_style: None,
89            header_fill: None,
90            ratio: None,
91            min_width: None,
92            max_width: None,
93            no_wrap: false,
94            overflow: Overflow::Ellipsis,
95        }
96    }
97}
98
99/// A row's cells and the row's own options. Port of `rich.table.Row` (which
100/// upstream keeps apart from the cells, stored per column).
101struct Row {
102    cells: Vec<Cell>,
103    /// A style for the whole row (`add_row(..., style=…)`).
104    style: Option<StyleType>,
105    /// Draw a line beneath the row (`end_section`).
106    end_section: bool,
107}
108
109/// Which kind of row is being rendered: the header, a body row (with its
110/// resolved row style) or the footer.
111#[derive(Clone)]
112enum RowKind {
113    Header,
114    Body(Style),
115    Footer,
116}
117
118/// A table cell: a markup string, styled text, or any renderable (upstream
119/// accepts all three).
120#[derive(Clone)]
121pub enum Cell {
122    /// A text cell, rendered literally (a `Text` is never re-parsed); its own
123    /// `justify`, `overflow` and `no_wrap` override the column's.
124    Text(Text),
125    /// A plain string, which upstream renders through `Console.render_str`:
126    /// console markup and emoji codes are applied, and the column's
127    /// `highlight` decides whether it is highlighted. Pass [`Cell::Text`] for
128    /// data that must stay literal.
129    Markup(String),
130    /// A renderable cell, measured with `__rich_measure__` and rendered at the
131    /// column width, as upstream's `Padding(renderable)` cell is.
132    Renderable(Arc<dyn Renderable + Send + Sync>),
133}
134
135impl From<Text> for Cell {
136    fn from(text: Text) -> Self {
137        Cell::Text(text)
138    }
139}
140
141/// A string cell is console markup, as upstream's `str` renderables are:
142/// `"[b]x"` renders a bold `x`, and a stray `[` in data can restyle, link or
143/// hide what follows. For data you do not control (file names, model or
144/// column names, user input) pass a [`Text`] instead, as upstream's
145/// `add_row(Text(name))` does: `Cell::from(Text::new(name))`, which is never
146/// parsed.
147impl From<&str> for Cell {
148    fn from(text: &str) -> Self {
149        Cell::Markup(text.to_string())
150    }
151}
152
153/// Console markup, as for `&str`; see there, and use a [`Text`] for data.
154impl From<String> for Cell {
155    fn from(text: String) -> Self {
156        Cell::Markup(text)
157    }
158}
159
160/// Console markup, as for `&str`; see there, and use a [`Text`] for data.
161impl From<&String> for Cell {
162    fn from(text: &String) -> Self {
163        Cell::Markup(text.clone())
164    }
165}
166
167impl Cell {
168    /// The cell as `Text`, or `None` for a renderable. A markup string goes
169    /// through [`Console::render_str`] with `highlight`.
170    pub(crate) fn to_text(&self, console: &Console, highlight: Option<bool>) -> Option<Text> {
171        match self {
172            Cell::Text(text) => Some(text.clone()),
173            Cell::Markup(markup) => Some(console.render_str(markup, highlight)),
174            Cell::Renderable(_) => None,
175        }
176    }
177
178    /// `Measurement.get(console, options, cell)`. A string is measured as
179    /// upstream measures a `str`: `render_str(..., highlight=False)`, then the
180    /// resulting `Text`'s `__rich_measure__`.
181    pub(crate) fn measure_cell(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
182        match self {
183            Cell::Text(text) => Measurement::get(console, options, text),
184            // A string with no `[` or `:` is its own plain text (markup and
185            // emoji leave it alone); measure it without building a `Text`.
186            Cell::Markup(markup) if !markup.contains(['[', ':']) => {
187                if options.max_width < 1 {
188                    return Measurement::new(0, 0);
189                }
190                let (minimum, maximum) = crate::text::measure_plain(markup);
191                let width = Measurement::new(minimum, maximum)
192                    .normalize()
193                    .with_maximum(options.max_width);
194                if width.maximum < 1 {
195                    Measurement::new(0, 0)
196                } else {
197                    width.normalize()
198                }
199            }
200            Cell::Markup(markup) => {
201                Measurement::get(console, options, &console.render_str(markup, Some(false)))
202            }
203            Cell::Renderable(renderable) => Measurement::get(console, options, renderable.as_ref()),
204        }
205    }
206}
207
208impl Default for Cell {
209    fn default() -> Self {
210        Cell::Text(Text::default())
211    }
212}
213
214/// Options for a column, as upstream's `Column(...)` takes them. Pass to
215/// [`Table::add_column_with`].
216#[derive(Clone, Debug)]
217pub struct ColumnOptions {
218    /// Content justification (`justify`, default left).
219    pub justify: Justify,
220    /// A fixed content width (`width`).
221    pub width: Option<usize>,
222    /// A floor on the content width (`min_width`).
223    pub min_width: Option<usize>,
224    /// A cap on the content width (`max_width`).
225    pub max_width: Option<usize>,
226    /// A share of the free width when the table expands (`ratio`).
227    pub ratio: Option<usize>,
228    /// Never wrap cells (`no_wrap`).
229    pub no_wrap: bool,
230    /// How over-long text is handled (`overflow`, default ellipsis).
231    pub overflow: Overflow,
232    /// The style of the column's body cells (`style`).
233    pub style: Style,
234}
235
236impl Default for ColumnOptions {
237    fn default() -> Self {
238        ColumnOptions {
239            justify: Justify::Left,
240            width: None,
241            min_width: None,
242            max_width: None,
243            ratio: None,
244            no_wrap: false,
245            overflow: Overflow::Ellipsis,
246            style: Style::new(),
247        }
248    }
249}
250
251/// A grid of cells rendered inside a box. Mirrors `rich.table.Table`.
252pub struct Table {
253    columns: Vec<Column>,
254    rows: Vec<Row>,
255    box_set: BoxSet,
256    /// `box=None`: no borders and no column dividers (see [`Table::grid`]).
257    no_box: bool,
258    show_header: bool,
259    show_lines: bool,
260    show_edge: bool,
261    pad_edge: bool,
262    collapse_padding: bool,
263    expand: bool,
264    /// The title: a markup string or a [`Text`] (upstream `TextType`).
265    title: Option<Cell>,
266    caption: Option<Cell>,
267    padding: (usize, usize, usize, usize),
268    /// The header row's style (upstream default `"table.header"`).
269    header_style: StyleType,
270    /// The footer row's style (upstream default `"table.footer"`).
271    footer_style: StyleType,
272    border_style: Style,
273    style: Style,
274    /// Highlight `str` cells (port of `Table.highlight`, default `False`).
275    highlight: bool,
276    /// The table's width, borders included; setting it implies `expand`.
277    width: Option<usize>,
278    /// The table's minimum width, borders included.
279    min_width: Option<usize>,
280    show_footer: bool,
281    /// Blank lines between rows (drawn with the box's `mid` row).
282    leading: usize,
283    /// Styles that body rows cycle through.
284    row_styles: Vec<StyleType>,
285    /// `None` (the default) takes `table.title` / `table.caption`.
286    title_style: Option<StyleType>,
287    caption_style: Option<StyleType>,
288    title_justify: Justify,
289    caption_justify: Justify,
290    /// Substitute boxes a legacy Windows console cannot draw; `None` takes
291    /// the console's `safe_box`.
292    safe_box: Option<bool>,
293}
294
295impl Default for Table {
296    fn default() -> Self {
297        Table {
298            columns: Vec::new(),
299            rows: Vec::new(),
300            box_set: HEAVY_HEAD,
301            no_box: false,
302            show_header: true,
303            show_lines: false,
304            show_edge: true,
305            pad_edge: true,
306            collapse_padding: false,
307            expand: false,
308            title: None,
309            caption: None,
310            padding: (0, 1, 0, 1),
311            header_style: StyleType::Name("table.header".to_string()),
312            footer_style: StyleType::Name("table.footer".to_string()),
313            border_style: Style::new(),
314            style: Style::new(),
315            highlight: false,
316            width: None,
317            min_width: None,
318            show_footer: false,
319            leading: 0,
320            row_styles: Vec::new(),
321            title_style: None,
322            caption_style: None,
323            title_justify: Justify::Center,
324            caption_justify: Justify::Center,
325            safe_box: None,
326        }
327    }
328}
329
330impl Table {
331    pub fn new() -> Self {
332        Table::default()
333    }
334
335    /// A table with no borders, for laying out columns. Port of `Table.grid`:
336    /// `box=None`, no header or edge, `padding=0`, `collapse_padding=True` and
337    /// `pad_edge=False`.
338    pub fn grid() -> Self {
339        Table {
340            no_box: true,
341            show_header: false,
342            show_edge: false,
343            pad_edge: false,
344            collapse_padding: true,
345            padding: (0, 0, 0, 0),
346            ..Table::default()
347        }
348    }
349
350    /// Draw no borders and no column dividers (upstream `box=None`).
351    pub fn without_box(mut self) -> Self {
352        self.no_box = true;
353        self
354    }
355
356    /// Cell padding as `(top, right, bottom, left)` (upstream `padding`).
357    pub fn padding(mut self, top: usize, right: usize, bottom: usize, left: usize) -> Self {
358        self.padding = (top, right, bottom, left);
359        self
360    }
361
362    /// Choose the box-drawing set.
363    pub fn box_set(mut self, box_set: BoxSet) -> Self {
364        self.box_set = box_set;
365        self
366    }
367
368    /// Style the box border (edges + dividers). Composed over the table-level
369    /// style: `border = style + border_style`. Port of `Table(border_style=…)`.
370    pub fn border_style(mut self, style: Style) -> Self {
371        self.border_style = style;
372        self
373    }
374
375    /// Whether to render the header row.
376    pub fn show_header(mut self, show: bool) -> Self {
377        self.show_header = show;
378        self
379    }
380
381    /// Expand the table to fill the available width.
382    pub fn expand(mut self, expand: bool) -> Self {
383        self.expand = expand;
384        self
385    }
386
387    /// Draw a separator line between each body row.
388    pub fn show_lines(mut self, show: bool) -> Self {
389        self.show_lines = show;
390        self
391    }
392
393    /// Draw the outer box edges (top/bottom borders + left/right glyphs). When
394    /// off, only the internal dividers and content remain. Port of `show_edge`.
395    pub fn show_edge(mut self, show: bool) -> Self {
396        self.show_edge = show;
397        self
398    }
399
400    /// Pad the outer cell edges. When off, the first column drops its left pad
401    /// and the last column its right pad. Port of `pad_edge`.
402    pub fn pad_edge(mut self, pad: bool) -> Self {
403        self.pad_edge = pad;
404        self
405    }
406
407    /// Merge adjacent cell padding: an interior column's left pad is reduced by
408    /// the previous column's right pad. Port of `collapse_padding`.
409    pub fn collapse_padding(mut self, collapse: bool) -> Self {
410        self.collapse_padding = collapse;
411        self
412    }
413
414    /// Default style for the whole table. Upstream applies it as the base of the
415    /// border style (`border_style = style + border_style`); cell content keeps
416    /// its own styles. Port of `Table(style=…)`.
417    pub fn style(mut self, style: Style) -> Self {
418        self.style = style;
419        self
420    }
421
422    /// Highlight string cells with the console's highlighter (upstream
423    /// `Table(highlight=…)`, default off). Columns added without their own
424    /// setting use it.
425    pub fn highlight(mut self, highlight: bool) -> Self {
426        self.highlight = highlight;
427        self
428    }
429
430    /// The `(left, right)` padding for column `index` of `ncols`. Port of
431    /// `_get_padding_width` (collapse) combined with the `pad_edge` edge drops.
432    fn cell_padding(&self, index: usize, ncols: usize) -> (usize, usize) {
433        let (_, pr, _, pl) = self.padding;
434        // collapse_padding: interior columns shed the overlap with the previous
435        // column's right pad.
436        let mut left = if self.collapse_padding && index > 0 {
437            pl.saturating_sub(pr)
438        } else {
439            pl
440        };
441        let mut right = pr;
442        // pad_edge: the outer edges lose their padding.
443        if !self.pad_edge && index == 0 {
444            left = 0;
445        }
446        if !self.pad_edge && index + 1 == ncols {
447            right = 0;
448        }
449        (left, right)
450    }
451
452    /// A title rendered above the table (console markup, centered by
453    /// default).
454    pub fn title(mut self, title: impl Into<String>) -> Self {
455        self.title = Some(Cell::Markup(title.into()));
456        self
457    }
458
459    /// A caption rendered below the table (console markup).
460    pub fn caption(mut self, caption: impl Into<String>) -> Self {
461        self.caption = Some(Cell::Markup(caption.into()));
462        self
463    }
464
465    /// A [`Text`] title (upstream `title=Text(...)`): rendered as it is, in
466    /// its own style and justify, with no `title_style`.
467    pub fn title_text(mut self, title: Text) -> Self {
468        self.title = Some(Cell::Text(title));
469        self
470    }
471
472    /// A [`Text`] caption (upstream `caption=Text(...)`).
473    pub fn caption_text(mut self, caption: Text) -> Self {
474        self.caption = Some(Cell::Text(caption));
475        self
476    }
477
478    /// The style of a markup title (upstream `title_style`; default
479    /// `table.title`).
480    pub fn title_style(mut self, style: impl Into<StyleType>) -> Self {
481        self.title_style = Some(style.into());
482        self
483    }
484
485    /// The style of a markup caption (upstream `caption_style`; default
486    /// `table.caption`).
487    pub fn caption_style(mut self, style: impl Into<StyleType>) -> Self {
488        self.caption_style = Some(style.into());
489        self
490    }
491
492    /// How the title is justified (upstream `title_justify`, default center).
493    pub fn title_justify(mut self, justify: Justify) -> Self {
494        self.title_justify = justify;
495        self
496    }
497
498    /// How the caption is justified (upstream `caption_justify`).
499    pub fn caption_justify(mut self, justify: Justify) -> Self {
500        self.caption_justify = justify;
501        self
502    }
503
504    /// The header row's style (upstream `header_style`, default
505    /// `table.header`). A column's own header style combines over it.
506    pub fn header_style(mut self, style: impl Into<StyleType>) -> Self {
507        self.header_style = style.into();
508        self
509    }
510
511    /// The footer row's style (upstream `footer_style`, default
512    /// `table.footer`).
513    pub fn footer_style(mut self, style: impl Into<StyleType>) -> Self {
514        self.footer_style = style.into();
515        self
516    }
517
518    /// The table's width, borders included (upstream `width`). Setting it
519    /// expands the table to exactly that width.
520    pub fn width(mut self, width: Option<usize>) -> Self {
521        self.width = width;
522        self
523    }
524
525    /// The table's minimum width, borders included (upstream `min_width`).
526    pub fn min_width(mut self, min_width: Option<usize>) -> Self {
527        self.min_width = min_width;
528        self
529    }
530
531    /// Render a footer row from each column's `footer` (upstream
532    /// `show_footer`).
533    pub fn show_footer(mut self, show: bool) -> Self {
534        self.show_footer = show;
535        self
536    }
537
538    /// Blank lines between rows (upstream `leading`); takes precedence over
539    /// `show_lines`.
540    pub fn leading(mut self, leading: usize) -> Self {
541        self.leading = leading;
542        self
543    }
544
545    /// Styles body rows cycle through (upstream `row_styles`).
546    pub fn row_styles(mut self, styles: Vec<StyleType>) -> Self {
547        self.row_styles = styles;
548        self
549    }
550
551    /// Whether to substitute boxes a legacy Windows console cannot draw
552    /// (upstream `safe_box`; `None` takes the console's).
553    pub fn safe_box(mut self, safe_box: Option<bool>) -> Self {
554        self.safe_box = safe_box;
555        self
556    }
557
558    /// Whether the table expands: `expand`, or any explicit `width`. Port
559    /// of the `Table.expand` property.
560    pub fn is_expand(&self) -> bool {
561        self.expand || self.width.is_some()
562    }
563
564    /// The number of rows (upstream `row_count`).
565    pub fn row_count(&self) -> usize {
566        self.rows.len()
567    }
568
569    /// The number of columns.
570    pub fn column_count(&self) -> usize {
571        self.columns.len()
572    }
573
574    /// The style of body row `index`: its place in `row_styles`, then its
575    /// own style. Port of `Table.get_row_style`.
576    pub fn get_row_style(&self, console: &Console, index: usize) -> Style {
577        let theme = console.theme();
578        let mut style = Style::new();
579        if !self.row_styles.is_empty() {
580            style = style
581                .combine(&theme.get_style_or_null(&self.row_styles[index % self.row_styles.len()]));
582        }
583        if let Some(row_style) = self.rows.get(index).and_then(|row| row.style.as_ref()) {
584            style = style.combine(&theme.get_style_or_null(row_style));
585        }
586        style
587    }
588
589    /// End the current section: draw a line beneath the last row. Port of
590    /// `Table.add_section`.
591    pub fn add_section(&mut self) -> &mut Self {
592        if let Some(row) = self.rows.last_mut() {
593            row.end_section = true;
594        }
595        self
596    }
597
598    /// Add a left-justified column with the given header, which is console
599    /// markup as upstream's `add_column("[b]Name")` is.
600    pub fn add_column(&mut self, header: impl Into<String>) -> &mut Self {
601        self.add_column_justify(header, Justify::Left)
602    }
603
604    /// Add a column with an explicit justification. The header is console
605    /// markup; use [`add_column_text`](Self::add_column_text) for a literal one.
606    pub fn add_column_justify(&mut self, header: impl Into<String>, justify: Justify) -> &mut Self {
607        self.add_column_text(Text::default(), justify);
608        if let Some(column) = self.columns.last_mut() {
609            column.header = Cell::Markup(header.into());
610        }
611        self
612    }
613
614    /// Add a column whose header is a styled [`Text`], as upstream's
615    /// `add_column(header=Text(...))` does. The text's spans survive into the
616    /// header cell; its own `justify`, `overflow` and `no_wrap` override the
617    /// column's, as `Text.__rich_console__` prefers them over the options.
618    pub fn add_column_text(&mut self, header: Text, justify: Justify) -> &mut Self {
619        self.columns.push(Column::new(Cell::Text(header), justify));
620        self
621    }
622
623    /// Add a column whose header is any [`Cell`] (upstream's `header` is any
624    /// renderable).
625    pub fn add_column_cell(&mut self, header: Cell, justify: Justify) -> &mut Self {
626        self.columns.push(Column::new(header, justify));
627        self
628    }
629
630    /// Set the most-recently-added column's footer (upstream
631    /// `Column.footer`), shown with [`show_footer`](Self::show_footer).
632    pub fn column_footer(&mut self, footer: impl Into<Cell>) -> &mut Self {
633        if let Some(column) = self.columns.last_mut() {
634            column.footer = footer.into();
635        }
636        self
637    }
638
639    /// Style the most-recently-added column's whole footer cell, combined
640    /// over the table-level `footer_style` (upstream `Column.footer_style`).
641    pub fn column_footer_fill(&mut self, style: Style) -> &mut Self {
642        if let Some(column) = self.columns.last_mut() {
643            column.footer_fill = Some(style);
644        }
645        self
646    }
647
648    /// Add a column with every [`ColumnOptions`] set, as upstream's
649    /// `add_column(header, justify=…, width=…, ratio=…, …)` does.
650    pub fn add_column_with(&mut self, header: Text, options: ColumnOptions) -> &mut Self {
651        self.columns.push(Column {
652            width: options.width,
653            style: options.style,
654            ratio: options.ratio,
655            min_width: options.min_width,
656            max_width: options.max_width,
657            no_wrap: options.no_wrap,
658            overflow: options.overflow,
659            ..Column::new(Cell::Text(header), options.justify)
660        });
661        self
662    }
663
664    /// Set the vertical alignment of the most-recently-added column's body
665    /// cells (upstream `Column.vertical`, default top). A cell whose
666    /// renderable has its own [`vertical`](crate::protocol::Renderable::vertical)
667    /// uses that instead. Chain after `add_column`.
668    pub fn column_vertical(&mut self, vertical: crate::align::VerticalAlign) -> &mut Self {
669        if let Some(column) = self.columns.last_mut() {
670            column.vertical = vertical;
671        }
672        self
673    }
674
675    /// Pin the most-recently-added column to an explicit content width. Content
676    /// wider than this wraps (with ellipsis overflow) instead of shrinking the
677    /// column. Chain after `add_column`.
678    pub fn column_width(&mut self, width: usize) -> &mut Self {
679        if let Some(column) = self.columns.last_mut() {
680            column.width = Some(width);
681        }
682        self
683    }
684
685    /// Give the most-recently-added column a flex `ratio`: when the table is
686    /// `expand`ed, ratio columns share the free width in proportion. Chain after
687    /// `add_column`. Port of `Column.ratio`.
688    pub fn column_ratio(&mut self, ratio: usize) -> &mut Self {
689        if let Some(column) = self.columns.last_mut() {
690            column.ratio = Some(ratio);
691        }
692        self
693    }
694
695    /// Set a minimum content width on the most-recently-added column. Chain after
696    /// `add_column`. Port of `Column.min_width`.
697    pub fn column_min_width(&mut self, min_width: usize) -> &mut Self {
698        if let Some(column) = self.columns.last_mut() {
699            column.min_width = Some(min_width);
700        }
701        self
702    }
703
704    /// Set a maximum content width on the most-recently-added column — wider
705    /// cells wrap. Chain after `add_column`. Port of `Column.max_width`.
706    pub fn column_max_width(&mut self, max_width: usize) -> &mut Self {
707        if let Some(column) = self.columns.last_mut() {
708            column.max_width = Some(max_width);
709        }
710        self
711    }
712
713    /// Apply a style to the most-recently-added column's body cells. Chain after
714    /// `add_column`.
715    pub fn column_style(&mut self, style: Style) -> &mut Self {
716        if let Some(column) = self.columns.last_mut() {
717            column.style = style;
718        }
719        self
720    }
721
722    /// Style the most-recently-added column's header *content* (the visible
723    /// characters), leaving its padding as the base `header_style`. Chain after
724    /// `add_column`. Mirrors upstream stylizing the heading `Text`.
725    pub fn column_header_style(&mut self, style: Style) -> &mut Self {
726        if let Some(column) = self.columns.last_mut() {
727            column.header_content_style = Some(style);
728        }
729        self
730    }
731
732    /// Style the most-recently-added column's whole header *cell* (content +
733    /// padding), combined over the table-level `header_style`. Chain after
734    /// `add_column`. Port of `Column.header_style`.
735    pub fn column_header_fill(&mut self, style: Style) -> &mut Self {
736        if let Some(column) = self.columns.last_mut() {
737            column.header_fill = Some(style);
738        }
739        self
740    }
741
742    /// Set how the most-recently-added column handles over-long text (upstream
743    /// `Column.overflow`, default ellipsis). Chain after `add_column`.
744    pub fn column_overflow(&mut self, overflow: Overflow) -> &mut Self {
745        if let Some(column) = self.columns.last_mut() {
746            column.overflow = overflow;
747        }
748        self
749    }
750
751    /// Set whether the most-recently-added column highlights its string cells
752    /// (upstream `Column.highlight`). Chain after `add_column`.
753    pub fn column_highlight(&mut self, highlight: bool) -> &mut Self {
754        if let Some(column) = self.columns.last_mut() {
755            column.highlight = Some(highlight);
756        }
757        self
758    }
759
760    /// Mark the most-recently-added column `no_wrap`: its cells crop to a single
761    /// line (with ellipsis) instead of wrapping. Chain after `add_column`.
762    pub fn column_no_wrap(&mut self) -> &mut Self {
763        if let Some(column) = self.columns.last_mut() {
764            column.no_wrap = true;
765        }
766        self
767    }
768
769    /// Add a row of string cells (extra cells are ignored; missing cells render
770    /// empty). Each string is console markup, as upstream's `add_row("[b]x")`
771    /// is, so a `[` in data can restyle or hide the rest of the cell: for
772    /// data you do not control use [`add_row_text`](Self::add_row_text) with
773    /// `Text::new(..)` cells, which are never parsed.
774    pub fn add_row(&mut self, cells: &[&str]) -> &mut Self {
775        self.push_row(
776            cells
777                .iter()
778                .map(|s| Cell::Markup((*s).to_string()))
779                .collect(),
780            None,
781            false,
782        )
783    }
784
785    /// Add a row with upstream's keyword options: a `style` for the whole
786    /// row and `end_section` to draw a line beneath it. Port of
787    /// `add_row(*renderables, style=…, end_section=…)`.
788    pub fn add_row_with(
789        &mut self,
790        cells: Vec<Cell>,
791        style: Option<StyleType>,
792        end_section: bool,
793    ) -> &mut Self {
794        self.push_row(cells, style, end_section)
795    }
796
797    /// Port of `add_row`'s body: a row longer than the table adds a column
798    /// (with every default and the table's `highlight`) for each extra cell.
799    fn push_row(
800        &mut self,
801        cells: Vec<Cell>,
802        style: Option<StyleType>,
803        end_section: bool,
804    ) -> &mut Self {
805        while self.columns.len() < cells.len() {
806            let mut column = Column::new(Cell::Markup(String::new()), Justify::Left);
807            column.highlight = Some(self.highlight);
808            self.columns.push(column);
809        }
810        self.rows.push(Row {
811            cells,
812            style,
813            end_section,
814        });
815        self
816    }
817
818    /// Add a row of styled [`Text`] cells, as upstream's `add_row(Text(...))`.
819    /// Each cell keeps its spans, and its own `justify`, `overflow` and
820    /// `no_wrap` override the column's.
821    pub fn add_row_text(&mut self, cells: Vec<Text>) -> &mut Self {
822        self.push_row(cells.into_iter().map(Cell::Text).collect(), None, false)
823    }
824
825    /// Add a row of [`Cell`]s, which may be any renderable.
826    pub fn add_row_cells(&mut self, cells: Vec<Cell>) -> &mut Self {
827        self.push_row(cells, None, false)
828    }
829
830    /// The width of the borders: `ncols - 1` dividers, plus the two outer
831    /// edges when shown; no box, no border. Port of `_extra_width`.
832    fn extra_width(&self) -> usize {
833        if self.no_box {
834            0
835        } else {
836            (if self.show_edge { 2 } else { 0 }) + self.columns.len().saturating_sub(1)
837        }
838    }
839
840    /// The column's padding width. Port of `_get_padding_width`, which (unlike
841    /// the per-cell padding of `_get_cells`) drops the left pad entirely under
842    /// `collapse_padding`.
843    fn padding_width(&self, index: usize) -> usize {
844        let (_, mut pad_right, _, mut pad_left) = self.padding;
845        if self.collapse_padding {
846            pad_left = 0;
847        }
848        if !self.pad_edge {
849            if index == 0 {
850                pad_left = 0;
851            }
852            if index + 1 == self.columns.len() {
853                pad_right = 0;
854            }
855        }
856        pad_left + pad_right
857    }
858
859    /// `Measurement.get` of one of `_get_cells`' cells: the cell wrapped in
860    /// `Padding(renderable, (0, right, 0, left))` when the table has any
861    /// padding. Port of `Padding.__rich_measure__` over the cell.
862    fn measure_padded_cell(
863        &self,
864        console: &Console,
865        options: &ConsoleOptions,
866        cell: &Cell,
867        (left, right): (usize, usize),
868    ) -> Measurement {
869        let max_width = options.max_width;
870        if max_width < 1 {
871            return Measurement::new(0, 0);
872        }
873        let (top, pr, bottom, pl) = self.padding;
874        if top == 0 && pr == 0 && bottom == 0 && pl == 0 {
875            return cell.measure_cell(console, options);
876        }
877        let extra_width = left + right;
878        let width = if max_width < extra_width + 1 {
879            Measurement::new(max_width, max_width)
880        } else {
881            let inner = cell.measure_cell(console, options);
882            Measurement::new(inner.minimum + extra_width, inner.maximum + extra_width)
883                .with_maximum(max_width)
884        };
885        // `Measurement.get` around the `Padding`.
886        let width = width.normalize().with_maximum(max_width);
887        if width.maximum < 1 {
888            Measurement::new(0, 0)
889        } else {
890            width.normalize()
891        }
892    }
893
894    /// The minimum and maximum width of column `index` (content + padding).
895    /// Port of `Table._measure_column`: every cell, header included, is
896    /// measured with `Measurement.get`, so a nested renderable (a `Table`,
897    /// `Panel`, …) sizes its column by its own `__rich_measure__`.
898    fn measure_column(
899        &self,
900        console: &Console,
901        options: &ConsoleOptions,
902        index: usize,
903    ) -> Measurement {
904        let max_width = options.max_width;
905        if max_width < 1 {
906            return Measurement::new(0, 0);
907        }
908        let column = &self.columns[index];
909        let padding_width = self.padding_width(index);
910        if let Some(width) = column.width {
911            // Fixed width column.
912            return Measurement::new(width + padding_width, width + padding_width)
913                .with_maximum(max_width);
914        }
915        // Every cell of a column shares its left/right padding; only the
916        // vertical padding depends on the row.
917        let padding = self.cell_padding(index, self.columns.len());
918        let empty = Cell::Markup(String::new());
919        let header = self.show_header.then_some(&column.header);
920        let body = self
921            .rows
922            .iter()
923            .map(|row| row.cells.get(index).unwrap_or(&empty));
924        let footer = self.show_footer.then_some(&column.footer);
925        let mut measured = false;
926        let (mut minimum, mut maximum) = (0, 0);
927        for cell in header.into_iter().chain(body).chain(footer) {
928            let width = self.measure_padded_cell(console, options, cell, padding);
929            minimum = minimum.max(width.minimum);
930            maximum = maximum.max(width.maximum);
931            measured = true;
932        }
933        let measurement = if measured {
934            Measurement::new(minimum, maximum)
935        } else {
936            Measurement::new(1, max_width)
937        }
938        .with_maximum(max_width);
939        measurement.clamp(
940            column.min_width.map(|width| width + padding_width),
941            column.max_width.map(|width| width + padding_width),
942        )
943    }
944
945    /// The rendered width (content + padding) of each column, shrinking the
946    /// widest columns to fit `available` when necessary. Port of
947    /// `Table._calculate_column_widths` + `_collapse_widths`.
948    fn column_widths(
949        &self,
950        console: &Console,
951        options: &ConsoleOptions,
952        available: usize,
953    ) -> Vec<usize> {
954        let options = &options.update_width(available);
955        let max_width = available as i64;
956        let extra_width = self.extra_width() as i64;
957        // A fixed-width column uses its declared width; others measure content,
958        // clamped to the column's [min_width, max_width]. Port of `_measure_column`.
959        let maximums: Vec<i64> = (0..self.columns.len())
960            .map(|index| self.measure_column(console, options, index).maximum as i64)
961            .collect();
962        let mut widths: Vec<i64> = maximums.iter().map(|&width| width.max(1)).collect();
963
964        // Expand with explicit ratios: flexible (ratio) columns share the free
965        // width in proportion, fixed columns keep their measured width. Port of
966        // the `if self.expand: … if any(ratios)` block of `_calculate_column_widths`.
967        if self.is_expand() {
968            let ratios: Vec<i64> = self
969                .columns
970                .iter()
971                .filter(|c| c.ratio.is_some())
972                .map(|c| i64::try_from(c.ratio.unwrap()).unwrap_or(i64::MAX))
973                .collect();
974            if ratios.iter().any(|&r| r > 0) {
975                let fixed_widths: Vec<i64> = maximums
976                    .iter()
977                    .zip(&self.columns)
978                    .map(|(&w, c)| if c.ratio.is_some() { 0 } else { w })
979                    .collect();
980                let flex_minimum: Vec<i64> = self
981                    .columns
982                    .iter()
983                    .enumerate()
984                    .filter(|(_, c)| c.ratio.is_some())
985                    .map(|(index, c)| (c.width.unwrap_or(1) + self.padding_width(index)) as i64)
986                    .collect();
987                let flexible_width = max_width - fixed_widths.iter().sum::<i64>();
988                let flex_widths = ratio_distribute(flexible_width, &ratios, Some(&flex_minimum));
989                let mut iter_flex = flex_widths.into_iter();
990                for (index, column) in self.columns.iter().enumerate() {
991                    if column.ratio.is_some() {
992                        widths[index] = fixed_widths[index] + iter_flex.next().unwrap_or(0);
993                    }
994                }
995            }
996        }
997
998        let mut table_width: i64 = widths.iter().sum();
999        if table_width > max_width {
1000            // Only auto-width, wrapping columns may shrink; fixed and no_wrap
1001            // columns hold their width (no_wrap only yields via the last resort).
1002            let wrapable: Vec<bool> = self
1003                .columns
1004                .iter()
1005                .map(|c| c.width.is_none() && !c.no_wrap)
1006                .collect();
1007            widths = collapse_widths(widths, &wrapable, max_width);
1008            table_width = widths.iter().sum();
1009            // Last resort: if fixed columns still overflow, reduce every column
1010            // evenly. Port of `_calculate_column_widths`'s final `ratio_reduce`.
1011            if table_width > max_width {
1012                let excess = table_width - max_width;
1013                let ratios = vec![1i64; widths.len()];
1014                widths = ratio_reduce(excess, &ratios, &widths, &widths);
1015                table_width = widths.iter().sum();
1016            }
1017            // Upstream measures every column again at its reduced width, so a
1018            // `min_width` re-inflates its column and the table overflows (the
1019            // console crop then cuts it). `table_width` keeps the reduced sum,
1020            // as upstream's does.
1021            widths = widths
1022                .iter()
1023                .enumerate()
1024                .map(|(index, &width)| {
1025                    self.measure_column(
1026                        console,
1027                        &options.update_width(width.max(0) as usize),
1028                        index,
1029                    )
1030                    .maximum as i64
1031                })
1032                .collect();
1033        }
1034
1035        // Expand, or grow to the table's `min_width`: distribute the leftover
1036        // width proportionally (`ratio_distribute`, which asserts a positive
1037        // total ratio — a table of zero-width columns is left as it is).
1038        let min_width = self.min_width.map(|width| width as i64);
1039        if (table_width < max_width && self.is_expand())
1040            || min_width.is_some_and(|min_width| table_width < min_width - extra_width)
1041        {
1042            let target = match min_width {
1043                None => max_width,
1044                Some(min_width) => (min_width - extra_width).min(max_width),
1045            };
1046            if widths.iter().sum::<i64>() > 0 {
1047                let pad = ratio_distribute(target - table_width, &widths, None);
1048                for (width, extra) in widths.iter_mut().zip(pad) {
1049                    *width += extra;
1050                }
1051            }
1052        }
1053        widths.into_iter().map(|w| w.max(0) as usize).collect()
1054    }
1055
1056    /// `cell_padding` shrunk so that padding alone can never exceed the width
1057    /// the column was actually allotted.
1058    ///
1059    /// When many columns compete for a narrow terminal a column can be squeezed
1060    /// below its own padding. The cell then still emitted a full left and right
1061    /// pad, so every such column spent two cells where its border spent one and
1062    /// the content row grew wider than the table — at 29 columns in an 80-cell
1063    /// terminal the row overflowed by 15 cells and was cropped, taking the
1064    /// right-hand border with it while the border rows kept theirs.
1065    fn cell_padding_fitted(&self, index: usize, ncols: usize, rendered: usize) -> (usize, usize) {
1066        let (mut pl, mut pr) = self.cell_padding(index, ncols);
1067        while pl + pr > rendered {
1068            if pr > pl {
1069                pr -= 1;
1070            } else if pl > 0 {
1071                pl -= 1;
1072            } else {
1073                break;
1074            }
1075        }
1076        (pl, pr)
1077    }
1078
1079    /// The effective style for a cell in column `index`: `_get_cells`' cell
1080    /// style plus the row style. The header row takes `header_style` and the
1081    /// column's own header style, the footer likewise, and a body cell the
1082    /// column's style and then its row's.
1083    fn cell_style(&self, console: &Console, index: usize, kind: &RowKind) -> Style {
1084        let column = self.columns.get(index);
1085        match kind {
1086            RowKind::Header => {
1087                let base = console.theme().get_style_or_null(&self.header_style);
1088                match column.and_then(|c| c.header_fill.as_ref()) {
1089                    Some(fill) => base.combine(fill),
1090                    None => base,
1091                }
1092            }
1093            RowKind::Footer => {
1094                let base = console.theme().get_style_or_null(&self.footer_style);
1095                match column.and_then(|c| c.footer_fill.as_ref()) {
1096                    Some(fill) => base.combine(fill),
1097                    None => base,
1098                }
1099            }
1100            RowKind::Body(row_style) => column
1101                .map(|c| c.style.clone())
1102                .unwrap_or_default()
1103                .combine(row_style),
1104        }
1105    }
1106
1107    /// Pad a cell's rendered lines to its width, with the vertical padding
1108    /// above and below: upstream's `Padding` around the cell. Blank rows are
1109    /// one run across the whole cell, as `Padding`'s blank lines are.
1110    /// The `(top, bottom)` padding of a cell in the first and/or last of the
1111    /// rendered rows (header included). Port of `_get_cells`' `get_padding`:
1112    /// with `collapse_padding` every row but the last keeps only
1113    /// `max(0, top - bottom)` below it, and without `pad_edge` the first row
1114    /// loses its top and the last row its bottom.
1115    fn vertical_padding(&self, first_row: bool, last_row: bool) -> (usize, usize) {
1116        let (mut top, _, mut bottom, _) = self.padding;
1117        if self.collapse_padding && !last_row {
1118            bottom = top.saturating_sub(bottom);
1119        }
1120        if !self.pad_edge {
1121            if first_row {
1122                top = 0;
1123            }
1124            if last_row {
1125                bottom = 0;
1126            }
1127        }
1128        (top, bottom)
1129    }
1130
1131    fn pad_cell_lines(
1132        &self,
1133        lines: Vec<Vec<Segment>>,
1134        width: usize,
1135        (cpl, cpr): (usize, usize),
1136        (pt, pb): (usize, usize),
1137        style: &Style,
1138    ) -> Vec<Vec<Segment>> {
1139        let cell_fill = Some(style.clone());
1140        let cell_width = cpl + width + cpr;
1141        // The cell's `Padding` renders at the whole cell width, and
1142        // `Console.render` yields nothing at all below width 1: no content and
1143        // no vertical padding either (the row keeps its minimum height of 1).
1144        if cell_width == 0 {
1145            return Vec::new();
1146        }
1147        let blank = || vec![Segment::new(" ".repeat(cell_width), cell_fill.clone())];
1148        let mut padded_lines: Vec<Vec<Segment>> = Vec::new();
1149        for _ in 0..pt {
1150            padded_lines.push(blank());
1151        }
1152        for line in &lines {
1153            let mut row = Vec::new();
1154            if cpl > 0 {
1155                row.push(Segment::new(" ".repeat(cpl), cell_fill.clone()));
1156            }
1157            // The cell's segments pass through, as `Padding` yields them.
1158            row.extend(Segment::adjust_line_length(line, width, cell_fill.clone()));
1159            if cpr > 0 {
1160                row.push(Segment::new(" ".repeat(cpr), cell_fill.clone()));
1161            }
1162            padded_lines.push(row);
1163        }
1164        for _ in 0..pb {
1165            padded_lines.push(blank());
1166        }
1167        padded_lines
1168    }
1169
1170    /// Render one table row (a list of cell strings) into visual lines.
1171    #[allow(clippy::too_many_arguments)]
1172    fn render_row(
1173        &self,
1174        console: &Console,
1175        options: &ConsoleOptions,
1176        cells: &[Cell],
1177        rendered_widths: &[usize],
1178        kind: &RowKind,
1179        (first_row, last_row): (bool, bool),
1180        edges: Option<(Segment, Segment, Segment)>,
1181        row_index: usize,
1182    ) -> Vec<Vec<Segment>> {
1183        let is_header = matches!(kind, RowKind::Header);
1184        // Horizontal padding is per-column (see `cell_padding`); vertical
1185        // padding depends on the row's place (see `vertical_padding`).
1186        let vertical = self.vertical_padding(first_row, last_row);
1187        let ncols = self.columns.len();
1188        // Derived here rather than by the caller so the padding used to lay the
1189        // row out is the same padding the content width was reduced by.
1190        let paddings: Vec<(usize, usize)> = (0..ncols)
1191            .map(|index| {
1192                let rendered = rendered_widths.get(index).copied().unwrap_or(0);
1193                self.cell_padding_fitted(index, ncols, rendered)
1194            })
1195            .collect();
1196        let content_widths: Vec<usize> = rendered_widths
1197            .iter()
1198            .zip(&paddings)
1199            .map(|(w, (pl, pr))| w.saturating_sub(pl + pr))
1200            .collect();
1201
1202        // Render each cell into padded, simplified visual lines.
1203        let mut cell_lines: Vec<Vec<Vec<Segment>>> = Vec::with_capacity(ncols);
1204        let mut height = 1;
1205        // Not upstream: each cell's region, only when a sink is installed.
1206        let mut cell_regions = Vec::new();
1207        for (index, width) in content_widths.iter().enumerate() {
1208            let region = crate::protocol::enter_region(console, || {
1209                use crate::protocol::{RegionInfo, RegionRole};
1210                RegionInfo::new(match kind {
1211                    RowKind::Header => RegionRole::TableHeader { column: index },
1212                    RowKind::Footer => RegionRole::TableFooter { column: index },
1213                    RowKind::Body(_) => RegionRole::TableCell {
1214                        row: row_index,
1215                        column: index,
1216                    },
1217                })
1218            });
1219            // Nothing is allocated without a sink.
1220            if let Some(guard) = &region {
1221                cell_regions.push((index, guard.id()));
1222            }
1223            let style = self.cell_style(console, index, kind);
1224            let column = self.columns.get(index);
1225            let mut text = match cells.get(index) {
1226                Some(Cell::Text(text)) => text.clone(),
1227                // `render_options.update(highlight=column.highlight)`, then
1228                // `Console.render` of a `str` calls `render_str`.
1229                Some(Cell::Markup(markup)) => console.render_str(
1230                    markup,
1231                    Some(column.and_then(|c| c.highlight).unwrap_or(self.highlight)),
1232                ),
1233                None => Text::default(),
1234                Some(Cell::Renderable(renderable)) => {
1235                    // `console.render_lines(renderable, render_options, style)`
1236                    // at the content width, with the column's justify,
1237                    // no_wrap and overflow as options.
1238                    let mut cell_options = options.update_width(*width);
1239                    cell_options.highlight =
1240                        Some(column.and_then(|c| c.highlight).unwrap_or(self.highlight));
1241                    cell_options.justify = column.map_or(Justify::Left, |c| c.justify);
1242                    cell_options.no_wrap = Some(column.is_some_and(|c| c.no_wrap));
1243                    cell_options.overflow = Some(column.map_or(Overflow::Ellipsis, |c| c.overflow));
1244                    let lines = if *width == 0 {
1245                        Vec::new()
1246                    } else {
1247                        console.render_lines_styled(
1248                            renderable.as_ref(),
1249                            &cell_options,
1250                            Some(&style),
1251                            true,
1252                        )
1253                    };
1254                    cell_lines.push(self.pad_cell_lines(
1255                        lines,
1256                        *width,
1257                        paddings[index],
1258                        vertical,
1259                        &style,
1260                    ));
1261                    height = height.max(cell_lines.last().map_or(0, Vec::len));
1262                    continue;
1263                }
1264            };
1265            // Upstream renders the cell `Text` with the column's `justify`,
1266            // `no_wrap` and `overflow="ellipsis"` as options, which the text's own
1267            // settings override: wrap, then justify (which strips a right- or
1268            // center-justified line before measuring it), then truncate.
1269            let justify = text
1270                .get_justify_option()
1271                .unwrap_or_else(|| column.map(|c| c.justify).unwrap_or(Justify::Left));
1272            let overflow = text
1273                .get_overflow()
1274                .unwrap_or_else(|| column.map_or(Overflow::Ellipsis, |c| c.overflow));
1275            let no_wrap = text
1276                .get_no_wrap()
1277                .unwrap_or_else(|| column.map(|c| c.no_wrap).unwrap_or(false));
1278            // Header content carries its own style span over `header_style`; the
1279            // justify/edge padding stays `header_style` (matches upstream).
1280            if is_header {
1281                if let Some(span) = column.and_then(|c| c.header_content_style.clone()) {
1282                    let len = text.plain().len();
1283                    text.stylize(span, 0, len);
1284                }
1285            }
1286            // Upstream renders the cell as `Padding(renderable, …)` through
1287            // `render_lines`: a zero-width content area renders no lines
1288            // (`Console.render` returns nothing below width 1), while empty
1289            // text still renders one blank line.
1290            //
1291            // The text renders on its own and the cell style is applied to the
1292            // result (`render_lines(..., style=...)`), so a span keeps its own
1293            // segment even where it matches the cell style: `[b]Name` under a
1294            // bold header is `Name` + padding, as upstream prints it. The
1295            // justify padding joins the text's last run only where no span
1296            // ends there, as `Text.pad_right` on the plain string does.
1297            let tab_size = text.console_tab_size(console);
1298            let mut lines: Vec<Vec<Segment>> = if *width == 0 {
1299                Vec::new()
1300            } else {
1301                text.render_lines_wrapped_tabs(
1302                    console.theme(),
1303                    &Style::new(),
1304                    Some(*width),
1305                    justify,
1306                    overflow,
1307                    no_wrap,
1308                    tab_size,
1309                )
1310                .iter()
1311                .map(|line| Segment::apply_style(line, &style))
1312                .collect()
1313            };
1314            if lines.is_empty() && *width > 0 {
1315                lines.push(Vec::new());
1316            }
1317            let padded_lines =
1318                self.pad_cell_lines(lines, *width, paddings[index], vertical, &style);
1319            height = height.max(padded_lines.len());
1320            cell_lines.push(padded_lines);
1321        }
1322
1323        // Shape every cell to the row height (#445). Upstream aligns each cell
1324        // to `row_height` (the tallest cell, possibly 0) with the cell style:
1325        // header cells to the bottom, body cells (vertical "top") to the top.
1326        // `Segment.set_shape` then pads to `max_height` (at least 1) with an
1327        // unstyled blank.
1328        let row_height = cell_lines.iter().map(Vec::len).max().unwrap_or(0);
1329        for (index, lines) in cell_lines.iter_mut().enumerate() {
1330            // `getattr(renderable, "vertical", None) or column.vertical`; a
1331            // header row aligns to the bottom and a footer to the top,
1332            // whatever the cell says.
1333            let vertical = if is_header {
1334                crate::align::VerticalAlign::Bottom
1335            } else if matches!(kind, RowKind::Footer) {
1336                crate::align::VerticalAlign::Top
1337            } else {
1338                match cells.get(index) {
1339                    Some(Cell::Renderable(renderable)) => renderable.vertical(),
1340                    _ => None,
1341                }
1342                .unwrap_or_else(|| {
1343                    self.columns
1344                        .get(index)
1345                        .map_or(crate::align::VerticalAlign::Top, |c| c.vertical)
1346                })
1347            };
1348            let (cpl, cpr) = paddings[index];
1349            let blank = " ".repeat(cpl + content_widths[index] + cpr);
1350            let filler = vec![Segment::new(
1351                blank.clone(),
1352                Some(self.cell_style(console, index, kind)),
1353            )];
1354            let missing = row_height.saturating_sub(lines.len());
1355            let top = match vertical {
1356                crate::align::VerticalAlign::Top => 0,
1357                crate::align::VerticalAlign::Middle => missing / 2,
1358                crate::align::VerticalAlign::Bottom => missing,
1359            };
1360            lines.splice(0..0, std::iter::repeat_n(filler.clone(), top));
1361            lines.extend(std::iter::repeat_n(filler, missing - top));
1362            while lines.len() < height {
1363                lines.push(vec![Segment::new(blank.clone(), None)]);
1364            }
1365            if let Some((_, id)) = cell_regions.iter().find(|(cell, _)| *cell == index) {
1366                for line in lines.iter_mut() {
1367                    crate::protocol::tag_region(line, *id);
1368                }
1369            }
1370        }
1371
1372        let last = ncols.saturating_sub(1);
1373        let mut rows_out: Vec<Vec<Segment>> = Vec::with_capacity(height);
1374        // `r` indexes into each column's per-line vector, so a range loop is the
1375        // natural shape here (the columns are iterated with `enumerate`).
1376        #[allow(clippy::needless_range_loop)]
1377        for r in 0..height {
1378            let mut row = Vec::new();
1379            if let (Some((edge_left, _, _)), true) = (&edges, self.show_edge) {
1380                row.push(edge_left.clone());
1381            }
1382            for (c, column_lines) in cell_lines.iter().enumerate() {
1383                row.extend(column_lines[r].clone());
1384                let Some((_, divider, edge_right)) = &edges else {
1385                    continue;
1386                };
1387                if c != last {
1388                    row.push(divider.clone());
1389                } else if self.show_edge {
1390                    row.push(edge_right.clone());
1391                }
1392            }
1393            rows_out.push(row);
1394        }
1395        rows_out
1396    }
1397}
1398
1399impl LineRenderable for Table {
1400    /// Render visual lines in order without retaining the full rendered table.
1401    ///
1402    /// Like upstream's `Table.__rich_console__` / `_render` generators, this
1403    /// measures all columns first, then renders only one row block at a time.
1404    /// Lines contain styled segments without a trailing newline. The callback
1405    /// may write each line immediately; its first error stops rendering.
1406    /// The table still owns its source rows for column-width measurement.
1407    fn try_for_each_line<E>(
1408        &self,
1409        console: &Console,
1410        options: &ConsoleOptions,
1411        emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
1412    ) -> Result<(), E> {
1413        // Not upstream: a semantic region, only when a sink is installed.
1414        let region = crate::protocol::enter_region(console, || {
1415            let info = crate::protocol::RegionInfo::new(crate::protocol::RegionRole::Table);
1416            match &self.title {
1417                Some(title) => info.label(annotation_plain(console, title)),
1418                None => info,
1419            }
1420        });
1421        let mut emit = {
1422            let mut emit = emit;
1423            let region = region.as_ref().map(|guard| guard.id());
1424            move |mut line: Vec<Segment>| {
1425                if let Some(id) = region {
1426                    crate::protocol::tag_region(&mut line, id);
1427                }
1428                emit(line)
1429            }
1430        };
1431        if self.columns.is_empty() {
1432            return emit(vec![Segment::new("", None)]);
1433        }
1434        // Fall back to a terminal-safe box on legacy Windows / non-UTF-8, and
1435        // to a plain-headed box when there is no header to set apart.
1436        let box_set = self.box_set.substitute(
1437            console.legacy_windows(),
1438            self.safe_box.unwrap_or_else(|| console.safe_box()),
1439            console.ascii_only(),
1440        );
1441        let box_set = if self.show_header {
1442            box_set
1443        } else {
1444            box_set.get_plain_headed_box()
1445        };
1446        // `max_width = self.width` when the table has one.
1447        let max_width = self.width.unwrap_or(options.max_width);
1448        let extra_width = self.extra_width();
1449        let available = max_width.saturating_sub(extra_width);
1450
1451        let rendered_widths = self.column_widths(console, options, available);
1452        let border_style = self.style.combine(&self.border_style);
1453        let border = Some(border_style.clone());
1454
1455        // Full table width (for the title and caption): columns + borders.
1456        let table_width: usize = rendered_widths.iter().sum::<usize>() + extra_width;
1457
1458        if let Some(title) = &self.title {
1459            let style = self
1460                .title_style
1461                .clone()
1462                .unwrap_or_else(|| StyleType::Name("table.title".to_string()));
1463            for line in render_annotation(
1464                console,
1465                options,
1466                title,
1467                &style,
1468                self.title_justify,
1469                table_width,
1470            ) {
1471                emit(line)?;
1472            }
1473        }
1474
1475        let edge = self.show_edge;
1476        let boxed = !self.no_box;
1477        if boxed && edge {
1478            emit(vec![Segment::new(
1479                box_set.get_top(&rendered_widths, edge),
1480                border.clone(),
1481            )])?;
1482        }
1483
1484        // `box_segments[0 if first else (2 if last else 1)]`: the first row
1485        // drawn (header or not) takes the head glyphs and the last the foot's.
1486        let segments = |(left, vertical, right): (char, char, char)| {
1487            (
1488                Segment::new(left.to_string(), border.clone()),
1489                Segment::new(vertical.to_string(), border.clone()),
1490                Segment::new(right.to_string(), border.clone()),
1491            )
1492        };
1493        let head_edges = segments((box_set.head_left, box_set.head_vertical, box_set.head_right));
1494        let mid_edges = segments((box_set.mid_left, box_set.mid_vertical, box_set.mid_right));
1495        let foot_edges = segments((box_set.foot_left, box_set.foot_vertical, box_set.foot_right));
1496
1497        // Every row drawn, as upstream's `row_cells` holds them.
1498        let mut row_list: Vec<(RowKind, &[Cell], Option<&Row>)> = Vec::new();
1499        let header_cells: Vec<Cell> = self.columns.iter().map(|c| c.header.clone()).collect();
1500        let footer_cells: Vec<Cell> = self.columns.iter().map(|c| c.footer.clone()).collect();
1501        if self.show_header {
1502            row_list.push((RowKind::Header, &header_cells, None));
1503        }
1504        for (index, row) in self.rows.iter().enumerate() {
1505            row_list.push((
1506                RowKind::Body(self.get_row_style(console, index)),
1507                &row.cells,
1508                Some(row),
1509            ));
1510        }
1511        if self.show_footer {
1512            row_list.push((RowKind::Footer, &footer_cells, None));
1513        }
1514        let total = row_list.len();
1515        for (index, (kind, cells, row)) in row_list.iter().enumerate() {
1516            let first = index == 0;
1517            let last = index + 1 == total;
1518            let header_row = first && self.show_header;
1519            if boxed && last && self.show_footer {
1520                emit(vec![Segment::new(
1521                    box_set.get_row(&rendered_widths, RowLevel::Foot, edge),
1522                    border.clone(),
1523                )])?;
1524            }
1525            let edges = boxed.then(|| {
1526                let (left, divider, right) = if first {
1527                    head_edges.clone()
1528                } else if last {
1529                    foot_edges.clone()
1530                } else {
1531                    mid_edges.clone()
1532                };
1533                // A whitespace divider takes the row's background too.
1534                let divider = if divider.text.trim().is_empty() {
1535                    let background = match kind {
1536                        RowKind::Body(row_style) => {
1537                            Style::from_color(None, row_style.bgcolor().cloned())
1538                        }
1539                        _ => Style::new(),
1540                    };
1541                    Segment::new(
1542                        divider.text.clone(),
1543                        Some(background.combine(&border_style)),
1544                    )
1545                } else {
1546                    divider
1547                };
1548                (left, divider, right)
1549            });
1550            for line in self.render_row(
1551                console,
1552                options,
1553                cells,
1554                &rendered_widths,
1555                kind,
1556                (first, last),
1557                edges,
1558                index - usize::from(self.show_header && !matches!(kind, RowKind::Header)),
1559            ) {
1560                emit(line)?;
1561            }
1562            if boxed && header_row {
1563                emit(vec![Segment::new(
1564                    box_set.get_row(&rendered_widths, RowLevel::Head, edge),
1565                    border.clone(),
1566                )])?;
1567            }
1568            let end_section = row.is_some_and(|row| row.end_section);
1569            if boxed
1570                && (self.show_lines || self.leading > 0 || end_section)
1571                && !last
1572                && !(self.show_footer && index + 2 >= total)
1573                && !header_row
1574            {
1575                let line = if self.leading > 0 {
1576                    // Upstream repeats the row on one line, as it is here.
1577                    box_set
1578                        .get_row(&rendered_widths, RowLevel::Mid, edge)
1579                        .repeat(self.leading)
1580                } else {
1581                    box_set.get_row(&rendered_widths, RowLevel::Row, edge)
1582                };
1583                emit(vec![Segment::new(line, border.clone())])?;
1584            }
1585        }
1586
1587        if boxed && edge {
1588            emit(vec![Segment::new(
1589                box_set.get_bottom(&rendered_widths, edge),
1590                border.clone(),
1591            )])?;
1592        }
1593
1594        if let Some(caption) = &self.caption {
1595            let style = self
1596                .caption_style
1597                .clone()
1598                .unwrap_or_else(|| StyleType::Name("table.caption".to_string()));
1599            for line in render_annotation(
1600                console,
1601                options,
1602                caption,
1603                &style,
1604                self.caption_justify,
1605                table_width,
1606            ) {
1607                emit(line)?;
1608            }
1609        }
1610
1611        Ok(())
1612    }
1613}
1614
1615impl crate::protocol::OwnedTableRows for Table {
1616    fn extend_owned_rows(&mut self, rows: Vec<Vec<String>>) -> &mut Self {
1617        for row in rows {
1618            self.push_row(row.into_iter().map(Cell::Markup).collect(), None, false);
1619        }
1620        self
1621    }
1622}
1623
1624impl Renderable for Table {
1625    /// Port of `Table.__rich_measure__`: the column widths the table would
1626    /// render at, then each column measured within their total; an explicit
1627    /// `width` is the maximum, and `min_width` a floor.
1628    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
1629        let max_width = self.width.unwrap_or(options.max_width);
1630        if self.columns.is_empty() {
1631            // `_extra_width` counts `len(columns) - 1` dividers, so an empty
1632            // boxed table measures `2 - 1` with edges and `-1` (normalized to
1633            // 0) without.
1634            let width = usize::from(!self.no_box && self.show_edge);
1635            return Measurement::new(width, self.width.unwrap_or(width))
1636                .clamp(self.min_width, None);
1637        }
1638        let extra_width = self.extra_width();
1639        let columns_width: usize = self
1640            .column_widths(console, options, max_width.saturating_sub(extra_width))
1641            .iter()
1642            .sum();
1643        let options = options.update_width(columns_width);
1644        let (minimum, maximum) = (0..self.columns.len())
1645            .map(|index| self.measure_column(console, &options, index))
1646            .fold((0, 0), |(minimum, maximum), width| {
1647                (minimum + width.minimum, maximum + width.maximum)
1648            });
1649        let maximum = self.width.unwrap_or(maximum + extra_width);
1650        Measurement::new(minimum + extra_width, maximum).clamp(self.min_width, None)
1651    }
1652
1653    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1654        let mut segments = Vec::new();
1655        let mut first = true;
1656        let result: Result<(), std::convert::Infallible> =
1657            self.try_for_each_line(console, options, |line| {
1658                if !first {
1659                    segments.push(Segment::line());
1660                }
1661                first = false;
1662                segments.extend(line);
1663                Ok(())
1664            });
1665        match result {
1666            Ok(()) => segments,
1667            Err(never) => match never {},
1668        }
1669    }
1670}
1671
1672/// Port of `Table.__rich_console__.render_annotation`: a string title is
1673/// `console.render_str(text, style=style, highlight=False)` (markup and emoji
1674/// on, no highlighting); a `Text` renders as it is. Either renders at the
1675/// table's width with `justify` unless the text has its own.
1676/// A title or caption as plain text, for a region's label. Not upstream.
1677fn annotation_plain(console: &Console, annotation: &Cell) -> String {
1678    match annotation {
1679        Cell::Markup(markup) => console.render_str(markup, Some(false)).plain().to_string(),
1680        Cell::Text(text) => text.plain().to_string(),
1681        Cell::Renderable(_) => String::new(),
1682    }
1683}
1684
1685fn render_annotation(
1686    console: &Console,
1687    options: &ConsoleOptions,
1688    annotation: &Cell,
1689    style: &StyleType,
1690    justify: Justify,
1691    width: usize,
1692) -> Vec<Vec<Segment>> {
1693    let text = match annotation {
1694        Cell::Markup(markup) => {
1695            if markup.is_empty() {
1696                return Vec::new();
1697            }
1698            let mut text = console.render_str(markup, Some(false));
1699            text.set_base_style(style.clone());
1700            text
1701        }
1702        Cell::Text(text) => {
1703            if text.plain().is_empty() {
1704                return Vec::new();
1705            }
1706            text.clone()
1707        }
1708        Cell::Renderable(_) => return Vec::new(),
1709    };
1710    if width == 0 {
1711        return Vec::new();
1712    }
1713    let justify = text.get_justify_option().unwrap_or(justify);
1714    let overflow = text
1715        .get_overflow()
1716        .or(options.overflow)
1717        .unwrap_or(Overflow::Fold);
1718    let no_wrap = text.get_no_wrap().or(options.no_wrap).unwrap_or(false);
1719    let tab_size = text.console_tab_size(console);
1720    text.render_lines_wrapped_tabs(
1721        console.theme(),
1722        console.base_style(),
1723        Some(width),
1724        justify,
1725        overflow,
1726        no_wrap,
1727        tab_size,
1728    )
1729}
1730
1731/// Round half to even (banker's rounding), matching Python's `round`.
1732fn round_half_even(value: f64) -> i64 {
1733    let floor = value.floor();
1734    let diff = value - floor;
1735    if (diff - 0.5).abs() < 1e-9 {
1736        let f = floor as i64;
1737        if f % 2 == 0 {
1738            f
1739        } else {
1740            f + 1
1741        }
1742    } else {
1743        value.round() as i64
1744    }
1745}
1746
1747/// Reduce `values` by `total`, distributed across slots by `ratios` (capped by
1748/// `maximums`). Direct port of `rich._ratio.ratio_reduce`.
1749fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
1750    let ratios: Vec<i64> = ratios
1751        .iter()
1752        .zip(maximums)
1753        .map(|(&r, &m)| if m != 0 { r } else { 0 })
1754        .collect();
1755    let mut total_ratio: i128 = ratios.iter().map(|&r| i128::from(r)).sum();
1756    if total_ratio == 0 {
1757        return values.to_vec();
1758    }
1759    let mut total_remaining = total;
1760    let mut result = Vec::with_capacity(values.len());
1761    for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
1762        if ratio != 0 && total_ratio > 0 {
1763            let distributed = maximum.min(round_half_even(
1764                ratio as f64 * total_remaining as f64 / total_ratio as f64,
1765            ));
1766            result.push(value - distributed);
1767            total_remaining -= distributed;
1768            total_ratio -= i128::from(ratio);
1769        } else {
1770            result.push(value);
1771        }
1772    }
1773    result
1774}
1775
1776/// Divide `total` across slots proportionally to `ratios` (ceil each share),
1777/// each share floored at the matching `minimums` entry when given. Port of
1778/// `rich._ratio.ratio_distribute`.
1779fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
1780    // Upstream zeroes the ratio of any slot whose minimum is 0 (falsy).
1781    let ratios: Vec<i64> = match minimums {
1782        Some(mins) => ratios
1783            .iter()
1784            .zip(mins)
1785            .map(|(&r, &m)| if m != 0 { r } else { 0 })
1786            .collect(),
1787        None => ratios.to_vec(),
1788    };
1789    // Python ints never overflow; `ratio * total_remaining` can exceed i64
1790    // for a huge ratio, so the arithmetic runs in i128.
1791    let mut total_ratio: i128 = ratios.iter().map(|&r| i128::from(r)).sum();
1792    let mut total_remaining = i128::from(total);
1793    let mut result = Vec::with_capacity(ratios.len());
1794    for (index, &ratio) in ratios.iter().enumerate() {
1795        let ratio = i128::from(ratio);
1796        let minimum = i128::from(minimums.map_or(0, |m| m[index]));
1797        let distributed = if total_ratio > 0 {
1798            // ceil(ratio * total_remaining / total_ratio) for positive values,
1799            // then floored at `minimum`.
1800            let numerator = ratio * total_remaining;
1801            let ceil_div = (numerator + total_ratio - 1) / total_ratio;
1802            minimum.max(ceil_div)
1803        } else {
1804            total_remaining
1805        };
1806        result.push(i64::try_from(distributed).unwrap_or(if distributed < 0 {
1807            i64::MIN
1808        } else {
1809            i64::MAX
1810        }));
1811        total_ratio -= ratio;
1812        total_remaining -= distributed;
1813    }
1814    result
1815}
1816
1817/// Reduce `widths` so their total is under `max_width`, shrinking the widest
1818/// wrapable columns first. Direct port of `Table._collapse_widths`.
1819fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
1820    let mut total_width: i64 = widths.iter().sum();
1821    let mut excess_width = total_width - max_width;
1822    if wrapable.iter().any(|&w| w) {
1823        while total_width != 0 && excess_width > 0 {
1824            let max_column = widths
1825                .iter()
1826                .zip(wrapable)
1827                .filter(|(_, &w)| w)
1828                .map(|(&x, _)| x)
1829                .max()
1830                .unwrap_or(0);
1831            let second_max_column = widths
1832                .iter()
1833                .zip(wrapable)
1834                .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
1835                .max()
1836                .unwrap_or(0);
1837            let column_difference = max_column - second_max_column;
1838            let ratios: Vec<i64> = widths
1839                .iter()
1840                .zip(wrapable)
1841                .map(|(&x, &w)| i64::from(x == max_column && w))
1842                .collect();
1843            if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
1844                break;
1845            }
1846            let max_reduce = vec![excess_width.min(column_difference); widths.len()];
1847            widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
1848            total_width = widths.iter().sum();
1849            excess_width = total_width - max_width;
1850        }
1851    }
1852    widths
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857
1858    #[test]
1859    fn a_huge_column_ratio_does_not_overflow() {
1860        // Python ints never overflow. Expected output captured from rich 15.0.0
1861        // (`ratio=2**64 - 1`; the collapsed widths come out narrow there too).
1862        let console = crate::Console::builder()
1863            .width(40)
1864            .color_system(None)
1865            .build();
1866        let mut table = Table::new().expand(true);
1867        table.add_column("a").column_ratio(usize::MAX);
1868        table.add_column("b").column_ratio(1);
1869        table.add_row(&["x", "y"]);
1870        assert_eq!(
1871            console.render_to_string(&table) + "\n",
1872            "┏━━━┳━━━┓\n┃ a ┃ b ┃\n┡━━━╇━━━┩\n│ x │ y │\n└───┴───┘\n"
1873        );
1874    }
1875
1876    use super::*;
1877    use crate::color::ColorSystem;
1878    use crate::r#box::SQUARE;
1879
1880    #[test]
1881    fn text_cells_are_never_markup() {
1882        let console = Console::builder().width(80).color_system(None).build();
1883        // A column named like markup, a closing tag and an emoji code.
1884        let data = ["[bold]model[/]", "col[/b]", ":sparkles:"];
1885        let mut plain = Table::new();
1886        plain.add_column("a").add_column("b").add_column("c");
1887        plain.add_row_text(data.iter().map(|s| Text::new(*s)).collect());
1888        let rendered = console.render_to_string(&plain);
1889        for cell in data {
1890            assert!(rendered.contains(cell), "{cell}: {rendered}");
1891        }
1892        // A Text cell is the same; Cell::Markup and a &str parse.
1893        let mut cells = Table::new();
1894        cells.add_column("a").add_column("b").add_column("c");
1895        cells.add_row_cells(vec![
1896            Cell::from(Text::new("[bold]model[/]")),
1897            Cell::Markup("[bold]model[/]".into()),
1898            Cell::from("[bold]model[/]"),
1899        ]);
1900        let rendered = console.render_to_string(&cells);
1901        assert_eq!(rendered.matches("[bold]model[/]").count(), 1, "{rendered}");
1902        assert_eq!(rendered.matches("model").count(), 3, "{rendered}");
1903    }
1904
1905    fn console() -> Console {
1906        Console::builder()
1907            .force_terminal(true)
1908            .color_system(Some(ColorSystem::Truecolor))
1909            .width(40)
1910            .no_color(false)
1911            .build()
1912    }
1913
1914    #[test]
1915    fn owned_rows_preserve_measurement_styles_and_missing_cells() {
1916        use crate::protocol::OwnedTableRows;
1917        for width in [1, 12, 40, 80] {
1918            let console = Console::builder().width(width).force_terminal(true).build();
1919            let build = || {
1920                let mut table = Table::new()
1921                    .title("Rows")
1922                    .caption("owned or borrowed")
1923                    .show_lines(true);
1924                table.add_column("Name");
1925                table.add_column_justify("Value", Justify::Right);
1926                table
1927            };
1928            let mut borrowed = build();
1929            let mut owned = build();
1930            for row in [
1931                vec!["漢字\n🙂", "123"],
1932                vec!["short"],
1933                vec!["extra", "4", "ignored"],
1934            ] {
1935                borrowed.add_row(&row);
1936                owned.extend_owned_rows(vec![row.into_iter().map(str::to_owned).collect()]);
1937            }
1938            assert_eq!(
1939                console.render_to_string(&borrowed),
1940                console.render_to_string(&owned)
1941            );
1942        }
1943    }
1944
1945    #[test]
1946    fn simple_square_table() {
1947        let mut table = Table::new().box_set(SQUARE);
1948        table.add_column("Name");
1949        table.add_column("Age");
1950        table.add_row(&["Alice", "30"]);
1951        table.add_row(&["Bob", "7"]);
1952        let out = console().render_export(&table);
1953        let expected = concat!(
1954            "┌───────┬─────┐\n",
1955            "│\x1b[1m \x1b[0m\x1b[1mName \x1b[0m\x1b[1m \x1b[0m│\x1b[1m \x1b[0m\x1b[1mAge\x1b[0m\x1b[1m \x1b[0m│\n",
1956            "├───────┼─────┤\n",
1957            "│ Alice │ 30  │\n",
1958            "│ Bob   │ 7   │\n",
1959            "└───────┴─────┘\n",
1960        );
1961        assert_eq!(out, expected);
1962    }
1963
1964    #[test]
1965    fn streamed_lines_match_styled_table_output() {
1966        let mut table = Table::new().box_set(SQUARE);
1967        table.add_column("Name");
1968        table.add_column("Age");
1969        table.add_row(&["Alice", "30"]);
1970        table.add_row(&["Bob", "7"]);
1971        let console = console();
1972        let mut streamed = String::new();
1973        table
1974            .try_for_each_line(&console, &console.options(), |line| {
1975                assert!(line.iter().all(|segment| !segment.text.contains('\n')));
1976                streamed.push_str(&console.segments_to_string(&line));
1977                streamed.push('\n');
1978                Ok::<_, std::convert::Infallible>(())
1979            })
1980            .unwrap();
1981        // `simple_square_table` above fixes these bytes independently of the
1982        // collection path, including distinct header-style segments.
1983        assert_eq!(streamed, console.render_export(&table));
1984        assert_eq!(streamed.lines().count(), 6);
1985    }
1986
1987    #[test]
1988    fn streamed_lines_stop_at_the_first_writer_error() {
1989        let mut table = Table::new()
1990            .box_set(SQUARE)
1991            .title("People")
1992            .caption("End")
1993            .show_lines(true);
1994        table.add_column("Name");
1995        table.add_row(&["Alice\nBob"]);
1996        table.add_row(&["Carol"]);
1997        let console = console();
1998        let mut visits = 0;
1999        let result = table.try_for_each_line(&console, &console.options(), |_| {
2000            visits += 1;
2001            if visits == 5 {
2002                Err("writer failed")
2003            } else {
2004                Ok(())
2005            }
2006        });
2007        assert_eq!(result, Err("writer failed"));
2008        assert_eq!(visits, 5);
2009    }
2010
2011    /// A column squeezed below its own padding still emitted a full left and
2012    /// right pad, so each such column spent two cells where its border spent
2013    /// one. The content row then overflowed the table and was cropped, losing
2014    /// its right-hand border while the border rows kept theirs.
2015    #[test]
2016    fn a_column_narrower_than_its_padding_stays_inside_the_border() {
2017        for ncols in [20usize, 29, 40] {
2018            let mut table = Table::new().box_set(SQUARE);
2019            for i in 0..ncols {
2020                table.add_column(format!("c{i}"));
2021            }
2022            let row: Vec<String> = (0..ncols).map(|i| i.to_string()).collect();
2023            table.add_row(&row.iter().map(String::as_str).collect::<Vec<_>>());
2024            let console = Console::builder().width(80).color_system(None).build();
2025            let out = console.render_to_string(&table);
2026            let rows: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
2027            let widths: Vec<usize> = rows.iter().map(|r| r.chars().count()).collect();
2028            assert!(
2029                widths.iter().all(|w| *w == widths[0]),
2030                "{ncols} columns produced ragged rows: {widths:?}"
2031            );
2032            for (index, row) in rows.iter().enumerate() {
2033                let last = row.chars().last().expect("non-empty row");
2034                assert!(
2035                    !last.is_whitespace(),
2036                    "{ncols} columns: row {index} lost its right border: {row:?}"
2037                );
2038            }
2039        }
2040    }
2041
2042    /// A cell spanning several lines occupies its WIDEST line. Measuring the raw
2043    /// string made it as wide as all its lines summed — `\n` measures zero, so
2044    /// nothing capped it — and a quoted CSV cell holding two sentences blew its
2045    /// column out to 31 cells where upstream gives 23.
2046    #[test]
2047    fn a_multi_line_cell_is_measured_by_its_widest_line() {
2048        let mut table = Table::new().box_set(SQUARE);
2049        table.add_column("name");
2050        table.add_column("bio");
2051        table.add_row(&["Alice", "line one\nline two is much longer"]);
2052        table.add_row(&["Bob", "short"]);
2053        let console = Console::builder().width(60).color_system(None).build();
2054        let out = console.render_to_string(&table);
2055        let top = out.lines().next().expect("a top border");
2056        let width = top.chars().count();
2057        // "line two is much longer" is 23 cells; summing both lines would be 31.
2058        assert!(
2059            width < 40,
2060            "the multi-line cell was measured as the sum of its lines: {width} wide"
2061        );
2062        assert!(
2063            out.contains("line two is much longer"),
2064            "content lost: {out:?}"
2065        );
2066    }
2067}