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