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::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 {
1413                if let Some(span) = column.and_then(|c| c.header_content_style.clone()) {
1414                    let len = text.plain().len();
1415                    text.stylize(span, 0, len);
1416                }
1417            }
1418            // Upstream renders the cell as `Padding(renderable, …)` through
1419            // `render_lines`: a zero-width content area renders no lines
1420            // (`Console.render` returns nothing below width 1), while empty
1421            // text still renders one blank line.
1422            //
1423            // The text renders on its own and the cell style is applied to the
1424            // result (`render_lines(..., style=...)`), so a span keeps its own
1425            // segment even where it matches the cell style: `[b]Name` under a
1426            // bold header is `Name` + padding, as upstream prints it. The
1427            // justify padding joins the text's last run only where no span
1428            // ends there, as `Text.pad_right` on the plain string does.
1429            let tab_size = text.console_tab_size(console);
1430            let mut lines: Vec<Vec<Segment>> = if *width == 0 {
1431                Vec::new()
1432            } else {
1433                text.render_lines_wrapped_tabs(
1434                    console.theme(),
1435                    &Style::new(),
1436                    Some(*width),
1437                    justify,
1438                    overflow,
1439                    no_wrap,
1440                    tab_size,
1441                )
1442                .iter()
1443                .map(|line| Segment::apply_style(line, &style))
1444                .collect()
1445            };
1446            if lines.is_empty() && *width > 0 {
1447                lines.push(Vec::new());
1448            }
1449            let padded_lines =
1450                self.pad_cell_lines(lines, *width, paddings[index], vertical, &style);
1451            height = height.max(padded_lines.len());
1452            cell_lines.push(padded_lines);
1453        }
1454
1455        // Shape every cell to the row height (#445). Upstream aligns each cell
1456        // to `row_height` (the tallest cell, possibly 0) with the cell style:
1457        // header cells to the bottom, body cells (vertical "top") to the top.
1458        // `Segment.set_shape` then pads to `max_height` (at least 1) with an
1459        // unstyled blank.
1460        let row_height = cell_lines.iter().map(Vec::len).max().unwrap_or(0);
1461        for (index, lines) in cell_lines.iter_mut().enumerate() {
1462            // `getattr(renderable, "vertical", None) or column.vertical`; a
1463            // header row aligns to the bottom and a footer to the top,
1464            // whatever the cell says.
1465            let vertical = if is_header {
1466                crate::align::VerticalAlign::Bottom
1467            } else if matches!(kind, RowKind::Footer) {
1468                crate::align::VerticalAlign::Top
1469            } else {
1470                match cells.get(index) {
1471                    Some(Cell::Renderable(renderable)) => renderable.vertical(),
1472                    _ => None,
1473                }
1474                .unwrap_or_else(|| {
1475                    self.columns
1476                        .get(index)
1477                        .map_or(crate::align::VerticalAlign::Top, |c| c.vertical)
1478                })
1479            };
1480            let (cpl, cpr) = paddings[index];
1481            let blank = " ".repeat(cpl + content_widths[index] + cpr);
1482            let filler = vec![Segment::new(
1483                blank.clone(),
1484                Some(self.cell_style(console, index, kind)),
1485            )];
1486            let missing = row_height.saturating_sub(lines.len());
1487            let top = match vertical {
1488                crate::align::VerticalAlign::Top => 0,
1489                crate::align::VerticalAlign::Middle => missing / 2,
1490                crate::align::VerticalAlign::Bottom => missing,
1491            };
1492            lines.splice(0..0, std::iter::repeat_n(filler.clone(), top));
1493            lines.extend(std::iter::repeat_n(filler, missing - top));
1494            while lines.len() < height {
1495                lines.push(vec![Segment::new(blank.clone(), None)]);
1496            }
1497            if let Some((_, id)) = cell_regions.iter().find(|(cell, _)| *cell == index) {
1498                for line in lines.iter_mut() {
1499                    crate::protocol::tag_region(line, *id);
1500                }
1501            }
1502        }
1503
1504        let last = ncols.saturating_sub(1);
1505        let mut rows_out: Vec<Vec<Segment>> = Vec::with_capacity(height);
1506        // `r` indexes into each column's per-line vector, so a range loop is the
1507        // natural shape here (the columns are iterated with `enumerate`).
1508        #[allow(clippy::needless_range_loop)]
1509        for r in 0..height {
1510            let mut row = Vec::new();
1511            if let (Some((edge_left, _, _)), true) = (&edges, self.show_edge) {
1512                row.push(edge_left.clone());
1513            }
1514            for (c, column_lines) in cell_lines.iter().enumerate() {
1515                row.extend(column_lines[r].clone());
1516                let Some((_, divider, edge_right)) = &edges else {
1517                    continue;
1518                };
1519                if c != last {
1520                    row.push(divider.clone());
1521                } else if self.show_edge {
1522                    row.push(edge_right.clone());
1523                }
1524            }
1525            rows_out.push(row);
1526        }
1527        rows_out
1528    }
1529}
1530
1531impl LineRenderable for Table {
1532    /// Render visual lines in order without retaining the full rendered table.
1533    ///
1534    /// Like upstream's `Table.__rich_console__` / `_render` generators, this
1535    /// measures all columns first, then renders only one row block at a time.
1536    /// Lines contain styled segments without a trailing newline. The callback
1537    /// may write each line immediately; its first error stops rendering.
1538    /// The table still owns its source rows for column-width measurement.
1539    fn try_for_each_line<E>(
1540        &self,
1541        console: &Console,
1542        options: &ConsoleOptions,
1543        emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
1544    ) -> Result<(), E> {
1545        // Not upstream: a semantic region, only when a sink is installed.
1546        let region = crate::protocol::enter_region(console, || {
1547            let info = crate::protocol::RegionInfo::new(crate::protocol::RegionRole::Table);
1548            match &self.title {
1549                Some(title) => info.label(annotation_plain(console, title)),
1550                None => info,
1551            }
1552        });
1553        let mut emit = {
1554            let mut emit = emit;
1555            let region = region.as_ref().map(|guard| guard.id());
1556            move |mut line: Vec<Segment>| {
1557                if let Some(id) = region {
1558                    crate::protocol::tag_region(&mut line, id);
1559                }
1560                emit(line)
1561            }
1562        };
1563        if self.columns.is_empty() {
1564            return emit(vec![Segment::new("", None)]);
1565        }
1566        // Fall back to a terminal-safe box on legacy Windows / non-UTF-8, and
1567        // to a plain-headed box when there is no header to set apart.
1568        let box_set = self
1569            .box_set
1570            .substitute_for(options, self.safe_box.unwrap_or_else(|| console.safe_box()));
1571        let box_set = if self.show_header {
1572            box_set
1573        } else {
1574            box_set.get_plain_headed_box()
1575        };
1576        // `max_width = self.width` when the table has one.
1577        let max_width = self.width.unwrap_or(options.max_width);
1578        let extra_width = self.extra_width();
1579        let available = max_width.saturating_sub(extra_width);
1580
1581        let rendered_widths = self.column_widths(console, options, available);
1582        let border_style = self.style.combine(&self.border_style);
1583        let border = Some(border_style.clone());
1584
1585        // Full table width (for the title and caption): columns + borders.
1586        let table_width: usize = rendered_widths.iter().sum::<usize>() + extra_width;
1587
1588        if let Some(title) = &self.title {
1589            let style = self
1590                .title_style
1591                .clone()
1592                .unwrap_or_else(|| StyleType::Name("table.title".to_string()));
1593            for line in render_annotation(
1594                console,
1595                options,
1596                title,
1597                &style,
1598                self.title_justify,
1599                table_width,
1600            ) {
1601                emit(line)?;
1602            }
1603        }
1604
1605        let edge = self.show_edge;
1606        let boxed = !self.no_box;
1607        if boxed && edge {
1608            emit(vec![Segment::new(
1609                box_set.get_top(&rendered_widths, edge),
1610                border.clone(),
1611            )])?;
1612        }
1613
1614        // `box_segments[0 if first else (2 if last else 1)]`: the first row
1615        // drawn (header or not) takes the head glyphs and the last the foot's.
1616        let segments = |(left, vertical, right): (char, char, char)| {
1617            (
1618                Segment::new(left.to_string(), border.clone()),
1619                Segment::new(vertical.to_string(), border.clone()),
1620                Segment::new(right.to_string(), border.clone()),
1621            )
1622        };
1623        let head_edges = segments((box_set.head_left, box_set.head_vertical, box_set.head_right));
1624        let mid_edges = segments((box_set.mid_left, box_set.mid_vertical, box_set.mid_right));
1625        let foot_edges = segments((box_set.foot_left, box_set.foot_vertical, box_set.foot_right));
1626
1627        // Every row drawn, as upstream's `row_cells` holds them.
1628        let mut row_list: Vec<(RowKind, &[Cell], Option<&Row>)> = Vec::new();
1629        let header_cells: Vec<Cell> = self.columns.iter().map(|c| c.header.clone()).collect();
1630        let footer_cells: Vec<Cell> = self.columns.iter().map(|c| c.footer.clone()).collect();
1631        if self.show_header {
1632            row_list.push((RowKind::Header, &header_cells, None));
1633        }
1634        for (index, row) in self.rows.iter().enumerate() {
1635            row_list.push((
1636                RowKind::Body(self.get_row_style(console, index)),
1637                &row.cells,
1638                Some(row),
1639            ));
1640        }
1641        if self.show_footer {
1642            row_list.push((RowKind::Footer, &footer_cells, None));
1643        }
1644        let total = row_list.len();
1645        for (index, (kind, cells, row)) in row_list.iter().enumerate() {
1646            let first = index == 0;
1647            let last = index + 1 == total;
1648            let header_row = first && self.show_header;
1649            if boxed && last && self.show_footer {
1650                emit(vec![Segment::new(
1651                    box_set.get_row(&rendered_widths, RowLevel::Foot, edge),
1652                    border.clone(),
1653                )])?;
1654            }
1655            let edges = boxed.then(|| {
1656                let (left, divider, right) = if first {
1657                    head_edges.clone()
1658                } else if last {
1659                    foot_edges.clone()
1660                } else {
1661                    mid_edges.clone()
1662                };
1663                // A whitespace divider takes the row's background too.
1664                let divider = if divider.text.trim().is_empty() {
1665                    let background = match kind {
1666                        RowKind::Body(row_style) => {
1667                            Style::from_color(None, row_style.bgcolor().cloned())
1668                        }
1669                        _ => Style::new(),
1670                    };
1671                    Segment::new(
1672                        divider.text.clone(),
1673                        Some(background.combine(&border_style)),
1674                    )
1675                } else {
1676                    divider
1677                };
1678                (left, divider, right)
1679            });
1680            for line in self.render_row(
1681                console,
1682                options,
1683                cells,
1684                &rendered_widths,
1685                kind,
1686                (first, last),
1687                edges,
1688                index - usize::from(self.show_header && !matches!(kind, RowKind::Header)),
1689            ) {
1690                emit(line)?;
1691            }
1692            if boxed && header_row {
1693                emit(vec![Segment::new(
1694                    box_set.get_row(&rendered_widths, RowLevel::Head, edge),
1695                    border.clone(),
1696                )])?;
1697            }
1698            let end_section = row.is_some_and(|row| row.end_section);
1699            if boxed
1700                && (self.show_lines || self.leading > 0 || end_section)
1701                && !last
1702                && !(self.show_footer && index + 2 >= total)
1703                && !header_row
1704            {
1705                let line = if self.leading > 0 {
1706                    // Upstream repeats the row on one line, as it is here.
1707                    box_set
1708                        .get_row(&rendered_widths, RowLevel::Mid, edge)
1709                        .repeat(self.leading)
1710                } else {
1711                    box_set.get_row(&rendered_widths, RowLevel::Row, edge)
1712                };
1713                emit(vec![Segment::new(line, border.clone())])?;
1714            }
1715        }
1716
1717        if boxed && edge {
1718            emit(vec![Segment::new(
1719                box_set.get_bottom(&rendered_widths, edge),
1720                border.clone(),
1721            )])?;
1722        }
1723
1724        if let Some(caption) = &self.caption {
1725            let style = self
1726                .caption_style
1727                .clone()
1728                .unwrap_or_else(|| StyleType::Name("table.caption".to_string()));
1729            for line in render_annotation(
1730                console,
1731                options,
1732                caption,
1733                &style,
1734                self.caption_justify,
1735                table_width,
1736            ) {
1737                emit(line)?;
1738            }
1739        }
1740
1741        Ok(())
1742    }
1743}
1744
1745impl crate::protocol::OwnedTableRows for Table {
1746    fn extend_owned_rows(&mut self, rows: Vec<Vec<String>>) -> &mut Self {
1747        for row in rows {
1748            self.push_row(row.into_iter().map(Cell::Markup).collect(), None, false);
1749        }
1750        self
1751    }
1752}
1753
1754impl Renderable for Table {
1755    /// Port of `Table.__rich_measure__`: the column widths the table would
1756    /// render at, then each column measured within their total; an explicit
1757    /// `width` is the maximum, and `min_width` a floor.
1758    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
1759        let max_width = self.width.unwrap_or(options.max_width);
1760        if self.columns.is_empty() {
1761            // `_extra_width` counts `len(columns) - 1` dividers, so an empty
1762            // boxed table measures `2 - 1` with edges and `-1` (normalized to
1763            // 0) without.
1764            let width = usize::from(!self.no_box && self.show_edge);
1765            return Measurement::new(width, self.width.unwrap_or(width))
1766                .clamp(self.min_width, None);
1767        }
1768        let extra_width = self.extra_width();
1769        let columns_width: usize = self
1770            .column_widths(console, options, max_width.saturating_sub(extra_width))
1771            .iter()
1772            .sum();
1773        let options = options.update_width(columns_width);
1774        let (minimum, maximum) = (0..self.columns.len())
1775            .map(|index| self.measure_column(console, &options, index))
1776            .fold((0, 0), |(minimum, maximum), width| {
1777                (minimum + width.minimum, maximum + width.maximum)
1778            });
1779        let maximum = self.width.unwrap_or(maximum + extra_width);
1780        Measurement::new(minimum + extra_width, maximum).clamp(self.min_width, None)
1781    }
1782
1783    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1784        let mut segments = Vec::new();
1785        let mut first = true;
1786        let result: Result<(), std::convert::Infallible> =
1787            self.try_for_each_line(console, options, |line| {
1788                if !first {
1789                    segments.push(Segment::line());
1790                }
1791                first = false;
1792                segments.extend(line);
1793                Ok(())
1794            });
1795        match result {
1796            Ok(()) => segments,
1797            Err(never) => match never {},
1798        }
1799    }
1800}
1801
1802/// Port of `Table.__rich_console__.render_annotation`: a string title is
1803/// `console.render_str(text, style=style, highlight=False)` (markup and emoji
1804/// on, no highlighting); a `Text` renders as it is. Either renders at the
1805/// table's width with `justify` unless the text has its own.
1806/// A title or caption as plain text, for a region's label. Not upstream.
1807fn annotation_plain(console: &Console, annotation: &Cell) -> String {
1808    match annotation {
1809        Cell::Markup(markup) => console.render_str(markup, Some(false)).plain().to_string(),
1810        Cell::Text(text) => text.plain().to_string(),
1811        Cell::Renderable(_) => String::new(),
1812    }
1813}
1814
1815fn render_annotation(
1816    console: &Console,
1817    options: &ConsoleOptions,
1818    annotation: &Cell,
1819    style: &StyleType,
1820    justify: Justify,
1821    width: usize,
1822) -> Vec<Vec<Segment>> {
1823    let text = match annotation {
1824        Cell::Markup(markup) => {
1825            if markup.is_empty() {
1826                return Vec::new();
1827            }
1828            let mut text = console.render_str(markup, Some(false));
1829            text.set_base_style(style.clone());
1830            text
1831        }
1832        Cell::Text(text) => {
1833            if text.plain().is_empty() {
1834                return Vec::new();
1835            }
1836            text.clone()
1837        }
1838        Cell::Renderable(_) => return Vec::new(),
1839    };
1840    if width == 0 {
1841        return Vec::new();
1842    }
1843    let justify = text.get_justify_option().unwrap_or(justify);
1844    let overflow = text
1845        .get_overflow()
1846        .or(options.overflow)
1847        .unwrap_or(Overflow::Fold);
1848    let no_wrap = text.get_no_wrap().or(options.no_wrap).unwrap_or(false);
1849    let tab_size = text.console_tab_size(console);
1850    text.render_lines_wrapped_tabs(
1851        console.theme(),
1852        console.base_style(),
1853        Some(width),
1854        justify,
1855        overflow,
1856        no_wrap,
1857        tab_size,
1858    )
1859}
1860
1861/// Reduce `widths` so their total is under `max_width`, shrinking the widest
1862/// wrapable columns first. Direct port of `Table._collapse_widths`.
1863fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
1864    let mut total_width: i64 = widths.iter().sum();
1865    let mut excess_width = total_width - max_width;
1866    if wrapable.iter().any(|&w| w) {
1867        while total_width != 0 && excess_width > 0 {
1868            let max_column = widths
1869                .iter()
1870                .zip(wrapable)
1871                .filter(|(_, &w)| w)
1872                .map(|(&x, _)| x)
1873                .max()
1874                .unwrap_or(0);
1875            let second_max_column = widths
1876                .iter()
1877                .zip(wrapable)
1878                .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
1879                .max()
1880                .unwrap_or(0);
1881            let column_difference = max_column - second_max_column;
1882            let ratios: Vec<i64> = widths
1883                .iter()
1884                .zip(wrapable)
1885                .map(|(&x, &w)| i64::from(x == max_column && w))
1886                .collect();
1887            if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
1888                break;
1889            }
1890            let max_reduce = vec![excess_width.min(column_difference); widths.len()];
1891            widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
1892            total_width = widths.iter().sum();
1893            excess_width = total_width - max_width;
1894        }
1895    }
1896    widths
1897}
1898
1899#[cfg(test)]
1900mod tests {
1901
1902    #[test]
1903    fn a_huge_column_ratio_does_not_overflow() {
1904        // Python ints never overflow. Expected output captured from rich 15.0.0
1905        // (`ratio=2**64 - 1`; the collapsed widths come out narrow there too).
1906        let console = crate::Console::builder()
1907            .width(40)
1908            .color_system(None)
1909            .build();
1910        let mut table = Table::new().expand(true);
1911        table.add_column("a").column_ratio(usize::MAX);
1912        table.add_column("b").column_ratio(1);
1913        table.add_row(&["x", "y"]);
1914        assert_eq!(
1915            console.render_to_string(&table) + "\n",
1916            "┏━━━┳━━━┓\n┃ a ┃ b ┃\n┡━━━╇━━━┩\n│ x │ y │\n└───┴───┘\n"
1917        );
1918    }
1919
1920    use super::*;
1921    use crate::color::ColorSystem;
1922    use crate::r#box::SQUARE;
1923
1924    #[test]
1925    fn text_cells_are_never_markup() {
1926        let console = Console::builder().width(80).color_system(None).build();
1927        // A column named like markup, a closing tag and an emoji code.
1928        let data = ["[bold]model[/]", "col[/b]", ":sparkles:"];
1929        let mut plain = Table::new();
1930        plain.add_column("a").add_column("b").add_column("c");
1931        plain.add_row_text(data.iter().map(|s| Text::new(*s)).collect());
1932        let rendered = console.render_to_string(&plain);
1933        for cell in data {
1934            assert!(rendered.contains(cell), "{cell}: {rendered}");
1935        }
1936        // A Text cell is the same; Cell::Markup and a &str parse.
1937        let mut cells = Table::new();
1938        cells.add_column("a").add_column("b").add_column("c");
1939        cells.add_row_cells(vec![
1940            Cell::from(Text::new("[bold]model[/]")),
1941            Cell::Markup("[bold]model[/]".into()),
1942            Cell::from("[bold]model[/]"),
1943        ]);
1944        let rendered = console.render_to_string(&cells);
1945        assert_eq!(rendered.matches("[bold]model[/]").count(), 1, "{rendered}");
1946        assert_eq!(rendered.matches("model").count(), 3, "{rendered}");
1947    }
1948
1949    fn console() -> Console {
1950        Console::builder()
1951            .force_terminal(true)
1952            .color_system(Some(ColorSystem::Truecolor))
1953            .width(40)
1954            .no_color(false)
1955            .build()
1956    }
1957
1958    #[test]
1959    fn owned_rows_preserve_measurement_styles_and_missing_cells() {
1960        use crate::protocol::OwnedTableRows;
1961        for width in [1, 12, 40, 80] {
1962            let console = Console::builder().width(width).force_terminal(true).build();
1963            let build = || {
1964                let mut table = Table::new()
1965                    .title("Rows")
1966                    .caption("owned or borrowed")
1967                    .show_lines(true);
1968                table.add_column("Name");
1969                table.add_column_justify("Value", Justify::Right);
1970                table
1971            };
1972            let mut borrowed = build();
1973            let mut owned = build();
1974            for row in [
1975                vec!["漢字\n🙂", "123"],
1976                vec!["short"],
1977                vec!["extra", "4", "ignored"],
1978            ] {
1979                borrowed.add_row(&row);
1980                owned.extend_owned_rows(vec![row.into_iter().map(str::to_owned).collect()]);
1981            }
1982            assert_eq!(
1983                console.render_to_string(&borrowed),
1984                console.render_to_string(&owned)
1985            );
1986        }
1987    }
1988
1989    #[test]
1990    fn simple_square_table() {
1991        let mut table = Table::new().box_set(SQUARE);
1992        table.add_column("Name");
1993        table.add_column("Age");
1994        table.add_row(&["Alice", "30"]);
1995        table.add_row(&["Bob", "7"]);
1996        let out = console().render_export(&table);
1997        let expected = concat!(
1998            "┌───────┬─────┐\n",
1999            "│\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",
2000            "├───────┼─────┤\n",
2001            "│ Alice │ 30  │\n",
2002            "│ Bob   │ 7   │\n",
2003            "└───────┴─────┘\n",
2004        );
2005        assert_eq!(out, expected);
2006    }
2007
2008    #[test]
2009    fn streamed_lines_match_styled_table_output() {
2010        let mut table = Table::new().box_set(SQUARE);
2011        table.add_column("Name");
2012        table.add_column("Age");
2013        table.add_row(&["Alice", "30"]);
2014        table.add_row(&["Bob", "7"]);
2015        let console = console();
2016        let mut streamed = String::new();
2017        table
2018            .try_for_each_line(&console, &console.options(), |line| {
2019                assert!(line.iter().all(|segment| !segment.text.contains('\n')));
2020                streamed.push_str(&console.segments_to_string(&line));
2021                streamed.push('\n');
2022                Ok::<_, std::convert::Infallible>(())
2023            })
2024            .unwrap();
2025        // `simple_square_table` above fixes these bytes independently of the
2026        // collection path, including distinct header-style segments.
2027        assert_eq!(streamed, console.render_export(&table));
2028        assert_eq!(streamed.lines().count(), 6);
2029    }
2030
2031    #[test]
2032    fn streamed_lines_stop_at_the_first_writer_error() {
2033        let mut table = Table::new()
2034            .box_set(SQUARE)
2035            .title("People")
2036            .caption("End")
2037            .show_lines(true);
2038        table.add_column("Name");
2039        table.add_row(&["Alice\nBob"]);
2040        table.add_row(&["Carol"]);
2041        let console = console();
2042        let mut visits = 0;
2043        let result = table.try_for_each_line(&console, &console.options(), |_| {
2044            visits += 1;
2045            if visits == 5 {
2046                Err("writer failed")
2047            } else {
2048                Ok(())
2049            }
2050        });
2051        assert_eq!(result, Err("writer failed"));
2052        assert_eq!(visits, 5);
2053    }
2054
2055    /// A column squeezed below its own padding still emitted a full left and
2056    /// right pad, so each such column spent two cells where its border spent
2057    /// one. The content row then overflowed the table and was cropped, losing
2058    /// its right-hand border while the border rows kept theirs.
2059    #[test]
2060    fn a_column_narrower_than_its_padding_stays_inside_the_border() {
2061        for ncols in [20usize, 29, 40] {
2062            let mut table = Table::new().box_set(SQUARE);
2063            for i in 0..ncols {
2064                table.add_column(format!("c{i}"));
2065            }
2066            let row: Vec<String> = (0..ncols).map(|i| i.to_string()).collect();
2067            table.add_row(&row.iter().map(String::as_str).collect::<Vec<_>>());
2068            let console = Console::builder().width(80).color_system(None).build();
2069            let out = console.render_to_string(&table);
2070            let rows: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
2071            let widths: Vec<usize> = rows.iter().map(|r| r.chars().count()).collect();
2072            assert!(
2073                widths.iter().all(|w| *w == widths[0]),
2074                "{ncols} columns produced ragged rows: {widths:?}"
2075            );
2076            for (index, row) in rows.iter().enumerate() {
2077                let last = row.chars().last().expect("non-empty row");
2078                assert!(
2079                    !last.is_whitespace(),
2080                    "{ncols} columns: row {index} lost its right border: {row:?}"
2081                );
2082            }
2083        }
2084    }
2085
2086    /// A cell spanning several lines occupies its WIDEST line. Measuring the raw
2087    /// string made it as wide as all its lines summed — `\n` measures zero, so
2088    /// nothing capped it — and a quoted CSV cell holding two sentences blew its
2089    /// column out to 31 cells where upstream gives 23.
2090    #[test]
2091    fn a_multi_line_cell_is_measured_by_its_widest_line() {
2092        let mut table = Table::new().box_set(SQUARE);
2093        table.add_column("name");
2094        table.add_column("bio");
2095        table.add_row(&["Alice", "line one\nline two is much longer"]);
2096        table.add_row(&["Bob", "short"]);
2097        let console = Console::builder().width(60).color_system(None).build();
2098        let out = console.render_to_string(&table);
2099        let top = out.lines().next().expect("a top border");
2100        let width = top.chars().count();
2101        // "line two is much longer" is 23 cells; summing both lines would be 31.
2102        assert!(
2103            width < 40,
2104            "the multi-line cell was measured as the sum of its lines: {width} wide"
2105        );
2106        assert!(
2107            out.contains("line two is much longer"),
2108            "content lost: {out:?}"
2109        );
2110    }
2111}