Skip to main content

rusty_rich/
table.rs

1//! Table — tabular data with columns. Equivalent to Rich's `table.py`.
2//!
3//! # Overview
4//!
5//! The [`Table`] renderable displays data in rows and columns with rich
6//! styling. Each column is defined by a [`Column`] that specifies header,
7//! footer, alignment, width constraints, and ratio. Cells can optionally
8//! span multiple columns or rows via [`Cell::colspan`] and [`Cell::rowspan`].
9//!
10//! # Quick Example
11//!
12//! ```rust
13//! use rusty_rich::{Table, Column};
14//!
15//! let mut table = Table::new();
16//! table.add_column(Column::new("Name"));
17//! table.add_column(Column::new("Age"));
18//! table.add_row_str(vec!["Alice".into(), "30".into()]);
19//! table.add_row_str(vec!["Bob".into(), "25".into()]);
20//! ```
21//!
22//! # Colspan & Rowspan
23//!
24//! ```rust
25//! use rusty_rich::{Table, Column, Cell};
26//!
27//! let mut table = Table::new();
28//! table.add_column(Column::new("A"));
29//! table.add_column(Column::new("B"));
30//! table.add_row(vec![Cell::new("spans both").colspan(2)]);
31//! ```
32//!
33//! # Box Styles
34//!
35//! Tables support all 17 box styles from [`crate::box_drawing`]. The default
36//! is [`BOX_HEAVY_HEAD`](crate::box_drawing::BOX_HEAVY_HEAD). Change it with
37//! [`Table::box_style`](Table::box_style).
38//!
39//! # Sections
40//!
41//! Call [`Table::add_section`] to insert a section divider between groups of
42//! rows.
43
44use crate::align::{AlignMethod, VerticalAlignMethod};
45use crate::box_drawing::{get_safe_box, BoxStyle, BOX_HEAVY_HEAD};
46use crate::console::{ConsoleOptions, OverflowMethod, RenderResult, Renderable};
47use crate::segment::Segment;
48use crate::style::Style;
49use std::collections::HashSet;
50use unicode_width::UnicodeWidthStr;
51
52// ---------------------------------------------------------------------------
53// Cell
54// ---------------------------------------------------------------------------
55
56/// A single cell in a table row, with optional styling and spanning.
57#[derive(Debug, Clone)]
58pub struct Cell {
59    /// The text content of the cell.
60    pub content: String,
61    /// Optional per-cell style.
62    pub style: Option<Style>,
63    /// Number of columns this cell spans (default 1).
64    pub colspan: usize,
65    /// Number of rows this cell spans (default 1).
66    pub rowspan: usize,
67}
68
69impl Cell {
70    /// Create a new Cell with the given content.
71    pub fn new(content: impl Into<String>) -> Self {
72        Cell {
73            content: content.into(),
74            style: None,
75            colspan: 1,
76            rowspan: 1,
77        }
78    }
79
80    /// Builder: set style.
81    pub fn style(mut self, s: Style) -> Self {
82        self.style = Some(s);
83        self
84    }
85    /// Builder: set colspan.
86    pub fn colspan(mut self, c: usize) -> Self {
87        self.colspan = c;
88        self
89    }
90    /// Builder: set rowspan.
91    pub fn rowspan(mut self, r: usize) -> Self {
92        self.rowspan = r;
93        self
94    }
95}
96
97impl From<String> for Cell {
98    fn from(s: String) -> Self {
99        Cell::new(s)
100    }
101}
102
103impl From<&str> for Cell {
104    fn from(s: &str) -> Self {
105        Cell::new(s)
106    }
107}
108
109// ---------------------------------------------------------------------------
110// Column
111// ---------------------------------------------------------------------------
112
113/// Defines a column within a Table.
114#[derive(Debug, Clone)]
115pub struct Column {
116    /// The header text / renderable.
117    pub header: String,
118    /// The footer text / renderable.
119    pub footer: String,
120    /// Header style.
121    pub header_style: Style,
122    /// Footer style.
123    pub footer_style: Style,
124    /// Default style for cells in this column.
125    pub style: Style,
126    /// Horizontal justification.
127    pub justify: AlignMethod,
128    /// Vertical alignment.
129    pub vertical: VerticalAlignMethod,
130    /// Overflow method.
131    pub overflow: OverflowMethod,
132    /// Fixed width, if set.
133    pub width: Option<usize>,
134    /// Minimum width.
135    pub min_width: Option<usize>,
136    /// Maximum width.
137    pub max_width: Option<usize>,
138    /// Ratio weight for flexible distribution.
139    pub ratio: Option<usize>,
140    /// Number of columns this header spans (default 1).
141    pub colspan: usize,
142}
143
144impl Column {
145    /// Create a new column with the given header.
146    pub fn new(header: impl Into<String>) -> Self {
147        Self {
148            header: header.into(),
149            footer: String::new(),
150            header_style: Style::new().bold(true),
151            footer_style: Style::new(),
152            style: Style::new(),
153            justify: AlignMethod::Left,
154            vertical: VerticalAlignMethod::Top,
155            overflow: OverflowMethod::Ellipsis,
156            width: None,
157            min_width: None,
158            max_width: None,
159            ratio: None,
160            colspan: 1,
161        }
162    }
163
164    /// Builder: set justify.
165    pub fn justify(mut self, j: AlignMethod) -> Self {
166        self.justify = j;
167        self
168    }
169    /// Builder: set width.
170    pub fn width(mut self, w: usize) -> Self {
171        self.width = Some(w);
172        self
173    }
174    /// Builder: set min width.
175    pub fn min_width(mut self, w: usize) -> Self {
176        self.min_width = Some(w);
177        self
178    }
179    /// Builder: set max width.
180    pub fn max_width(mut self, w: usize) -> Self {
181        self.max_width = Some(w);
182        self
183    }
184    /// Builder: set style.
185    pub fn style(mut self, s: Style) -> Self {
186        self.style = s;
187        self
188    }
189    /// Builder: set header style.
190    pub fn header_style(mut self, s: Style) -> Self {
191        self.header_style = s;
192        self
193    }
194    /// Builder: set ratio.
195    pub fn ratio(mut self, r: usize) -> Self {
196        self.ratio = Some(r);
197        self
198    }
199    /// Builder: set overflow.
200    pub fn overflow(mut self, o: OverflowMethod) -> Self {
201        self.overflow = o;
202        self
203    }
204}
205
206// ---------------------------------------------------------------------------
207// Row
208// ---------------------------------------------------------------------------
209
210/// An explicit row in a table (header row or data row).
211#[derive(Debug, Clone)]
212pub struct Row {
213    pub cells: Vec<Cell>,
214    pub style: Option<Style>,
215    pub end_section: bool,
216}
217
218impl Row {
219    /// Create a new Row from a list of [`Cell`]s.
220    pub fn new(cells: Vec<Cell>) -> Self {
221        Self {
222            cells,
223            style: None,
224            end_section: false,
225        }
226    }
227
228    /// Builder: set the row style.
229    pub fn style(mut self, style: Style) -> Self {
230        self.style = Some(style);
231        self
232    }
233
234    /// Builder: signal that this row ends a section (a section divider
235    /// will be rendered after it).
236    pub fn end_section(mut self, value: bool) -> Self {
237        self.end_section = value;
238        self
239    }
240}
241
242// ---------------------------------------------------------------------------
243// Table
244// ---------------------------------------------------------------------------
245
246/// A renderable for tabular data.
247#[derive(Debug, Clone)]
248pub struct Table {
249    columns: Vec<Column>,
250    rows: Vec<Vec<Cell>>,
251    /// Title above the table.
252    pub title: Option<String>,
253    /// Caption below the table.
254    pub caption: Option<String>,
255    /// Box style.
256    pub box_style: BoxStyle,
257    /// Show the header row.
258    pub show_header: bool,
259    /// Show the footer row.
260    pub show_footer: bool,
261    /// Show outer edge border.
262    pub show_edge: bool,
263    /// Show lines between every row.
264    pub show_lines: bool,
265    /// Padding per cell (top, right, bottom, left).
266    pub padding: (usize, usize, usize, usize),
267    /// Collapse padding between rows.
268    pub collapse_padding: bool,
269    /// Default style for the table.
270    pub style: Style,
271    /// Border style.
272    pub border_style: Style,
273    /// Title style.
274    pub title_style: Style,
275    /// Caption style.
276    pub caption_style: Style,
277    /// Title justification.
278    pub title_justify: AlignMethod,
279    /// Caption justification.
280    pub caption_justify: AlignMethod,
281    /// If true, highlight cell strings.
282    pub highlight: bool,
283    /// Optional fixed width.
284    pub width: Option<usize>,
285    /// Row styles (alternating).
286    pub row_styles: Vec<Style>,
287    /// Number of blank lines between rows.
288    pub leading: usize,
289    /// Active rowspan counts per column (tracked during rendering).
290    pub rowspans: Vec<usize>,
291    /// Row indices that have a section separator before them.
292    pub section_rows: HashSet<usize>,
293    /// Pad the outer edge of the table (left of first column, right of last).
294    pub pad_edge: bool,
295    /// Row indices where sections end (ordered, in insertion order).
296    pub sections: Vec<usize>,
297}
298
299impl Table {
300    /// Create a new Table.
301    pub fn new() -> Self {
302        Self {
303            columns: Vec::new(),
304            rows: Vec::new(),
305            title: None,
306            caption: None,
307            box_style: BOX_HEAVY_HEAD.clone(),
308            show_header: true,
309            show_footer: false,
310            show_edge: true,
311            show_lines: false,
312            padding: (0, 1, 0, 1),
313            collapse_padding: false,
314            style: Style::new(),
315            border_style: Style::new(),
316            title_style: Style::new().bold(true),
317            caption_style: Style::new().dim(true),
318            title_justify: AlignMethod::Center,
319            caption_justify: AlignMethod::Center,
320            highlight: false,
321            width: None,
322            row_styles: Vec::new(),
323            leading: 0,
324            rowspans: Vec::new(),
325            section_rows: HashSet::new(),
326            pad_edge: true,
327            sections: Vec::new(),
328        }
329    }
330
331    /// Add a column definition to the table.
332    ///
333    /// Columns must be added before rows are populated.
334    ///
335    /// # Examples
336    ///
337    /// ```rust
338    /// use rusty_rich::{Table, Column};
339    ///
340    /// let mut table = Table::new();
341    /// table.add_column(Column::new("Name"));
342    /// table.add_column(Column::new("Age"));
343    /// ```
344    pub fn add_column(&mut self, column: Column) {
345        self.columns.push(column);
346    }
347
348    /// Add a row from [`Cell`] objects (supports colspan/rowspan).
349    ///
350    /// # Examples
351    ///
352    /// ```rust
353    /// use rusty_rich::{Table, Column, Cell};
354    ///
355    /// let mut table = Table::new();
356    /// table.add_column(Column::new("A"));
357    /// table.add_column(Column::new("B"));
358    /// table.add_row(vec![Cell::new("data").colspan(2)]);
359    /// ```
360    pub fn add_row(&mut self, row: Vec<Cell>) {
361        self.rows.push(row);
362    }
363
364    /// Add a pre-built [`Row`] object, which may carry a style and section
365    /// information.
366    ///
367    /// If the row has `end_section` set to `true`, a section divider is
368    /// inserted before this row.
369    pub fn add_row_explicit(&mut self, row: Row) -> &mut Self {
370        if row.end_section {
371            self.section_rows.insert(self.rows.len());
372            self.sections.push(self.rows.len());
373        }
374        self.rows.push(row.cells);
375        self
376    }
377
378    /// Add a row from plain strings (backward-compatible, converts to [`Cell`]s).
379    ///
380    /// # Examples
381    ///
382    /// ```rust
383    /// use rusty_rich::{Table, Column};
384    ///
385    /// let mut table = Table::new();
386    /// table.add_column(Column::new("Name"));
387    /// table.add_column(Column::new("Age"));
388    /// table.add_row_str(vec!["Alice".into(), "30".into()]);
389    /// ```
390    pub fn add_row_str(&mut self, row: Vec<String>) {
391        let cells: Vec<Cell> = row.into_iter().map(Cell::new).collect();
392        self.rows.push(cells);
393    }
394
395    /// Builder: add a column and return self.
396    pub fn column(mut self, col: Column) -> Self {
397        self.add_column(col);
398        self
399    }
400
401    /// Builder: add a row of Cells and return self.
402    pub fn row(mut self, row: Vec<Cell>) -> Self {
403        self.add_row(row);
404        self
405    }
406
407    /// Builder: add a row of strings and return self.
408    pub fn row_str(mut self, row: Vec<String>) -> Self {
409        self.add_row_str(row);
410        self
411    }
412
413    /// Builder: add a pre-built [`Row`] and return self.
414    pub fn row_explicit(mut self, row: Row) -> Self {
415        self.add_row_explicit(row);
416        self
417    }
418
419    /// Builder: set title.
420    pub fn title(mut self, t: impl Into<String>) -> Self {
421        self.title = Some(t.into());
422        self
423    }
424
425    /// Builder: set caption.
426    pub fn caption(mut self, t: impl Into<String>) -> Self {
427        self.caption = Some(t.into());
428        self
429    }
430
431    /// Builder: set box style.
432    pub fn box_style(mut self, bs: BoxStyle) -> Self {
433        self.box_style = bs;
434        self
435    }
436
437    /// Builder: set border style.
438    pub fn border_style(mut self, s: Style) -> Self {
439        self.border_style = s;
440        self
441    }
442
443    /// Builder: hide the header.
444    pub fn hide_header(mut self) -> Self {
445        self.show_header = false;
446        self
447    }
448
449    /// Builder: show lines.
450    pub fn show_lines(mut self) -> Self {
451        self.show_lines = true;
452        self
453    }
454
455    /// Builder: set leading (blank lines between rows).
456    pub fn leading(mut self, l: usize) -> Self {
457        self.leading = l;
458        self
459    }
460
461    /// Builder: enable row highlighting.
462    pub fn highlight(mut self, value: bool) -> Self {
463        self.highlight = value;
464        self
465    }
466
467    /// Builder: set title alignment.
468    pub fn title_justify(mut self, justify: AlignMethod) -> Self {
469        self.title_justify = justify;
470        self
471    }
472
473    /// Builder: set caption alignment.
474    pub fn caption_justify(mut self, justify: AlignMethod) -> Self {
475        self.caption_justify = justify;
476        self
477    }
478
479    /// Builder: set alternating row styles.
480    pub fn row_styles(mut self, styles: Vec<Style>) -> Self {
481        self.row_styles = styles;
482        self
483    }
484
485    /// Builder: show/hide outer edge border.
486    pub fn show_edge(mut self, value: bool) -> Self {
487        self.show_edge = value;
488        self
489    }
490
491    /// Builder: collapse padding between cells.
492    pub fn collapse_padding(mut self, value: bool) -> Self {
493        self.collapse_padding = value;
494        self
495    }
496
497    /// Builder: pad the outer edge of the table.
498    pub fn pad_edge(mut self, value: bool) -> Self {
499        self.pad_edge = value;
500        self
501    }
502
503    /// Get the style for a specific row (cycling through `row_styles` if set).
504    pub fn get_row_style(&self, row_index: usize) -> Option<Style> {
505        if self.row_styles.is_empty() {
506            None
507        } else {
508            Some(self.row_styles[row_index % self.row_styles.len()].clone())
509        }
510    }
511
512    /// Create a grid table (no outer border, no header, no footer).
513    /// Equivalent to `Table.grid()`.
514    pub fn grid() -> Self {
515        Self {
516            columns: Vec::new(),
517            rows: Vec::new(),
518            title: None,
519            caption: None,
520            box_style: crate::box_drawing::BOX_SIMPLE.clone(),
521            show_header: false,
522            show_footer: false,
523            show_edge: false,
524            show_lines: false,
525            padding: (0, 1, 0, 1),
526            collapse_padding: false,
527            style: Style::new(),
528            border_style: Style::new(),
529            title_style: Style::new().bold(true),
530            caption_style: Style::new().dim(true),
531            title_justify: AlignMethod::Center,
532            caption_justify: AlignMethod::Center,
533            highlight: false,
534            width: None,
535            row_styles: Vec::new(),
536            leading: 0,
537            rowspans: Vec::new(),
538            section_rows: HashSet::new(),
539            pad_edge: true,
540            sections: Vec::new(),
541        }
542    }
543
544    /// Add a section separator before the next row.
545    /// The next row added will have a horizontal rule above it.
546    /// Returns `&mut Self` for chaining.
547    pub fn add_section(&mut self) -> &mut Self {
548        self.section_rows.insert(self.rows.len());
549        self.sections.push(self.rows.len());
550        self
551    }
552
553    /// Get the row count.
554    pub fn row_count(&self) -> usize {
555        self.rows.len()
556    }
557}
558
559impl Renderable for Table {
560    fn render(&self, options: &ConsoleOptions) -> RenderResult {
561        if self.columns.is_empty() {
562            return RenderResult::new();
563        }
564
565        let box_style = get_safe_box(&self.box_style, options.ascii_only);
566        let available_width = self.width.unwrap_or(options.max_width);
567        let col_count = self.columns.len();
568
569        // Calculate column widths
570        let col_widths = self.calculate_column_widths(available_width);
571
572        let mut lines: Vec<Vec<Segment>> = Vec::new();
573        let b = &box_style;
574
575        // Helper: make a border segment for a single char (corners, dividers)
576        let border_ansi = self.border_style.to_ansi();
577        let border_reset = if border_ansi.is_empty() {
578            ""
579        } else {
580            "\x1b[0m"
581        };
582        let bs = |ch: char| -> Segment { Segment::new(format!("{border_ansi}{ch}{border_reset}")) };
583        // Helper: repeated border character batched under one ANSI wrap
584        let bs_repeat = |ch: char, n: usize| -> Segment {
585            if border_ansi.is_empty() || n == 0 {
586                Segment::new(ch.to_string().repeat(n))
587            } else {
588                Segment::new(format!(
589                    "{border_ansi}{}{border_reset}",
590                    ch.to_string().repeat(n)
591                ))
592            }
593        };
594
595        // -- Title --
596        if let Some(ref title) = self.title {
597            let _tw = UnicodeWidthStr::width(title.as_str());
598            let centered = self
599                .title_justify
600                .align_text(title, available_width.saturating_sub(2));
601            lines.push(vec![
602                bs(b.top_left),
603                Segment::new(&centered[1..centered.len() - 1]),
604                bs(b.top_right),
605                Segment::line(),
606            ]);
607        }
608
609        // -- Top border --
610        if self.show_edge {
611            let mut top_line = vec![bs(b.top_left)];
612            for (i, w) in col_widths.iter().enumerate() {
613                top_line.push(bs_repeat(b.top, *w));
614                if i < col_count - 1 {
615                    top_line.push(bs(b.top_divider));
616                }
617            }
618            top_line.push(bs(b.top_right));
619            top_line.push(Segment::line());
620            lines.push(top_line);
621        }
622
623        // -- Header --
624        if self.show_header && self.columns.iter().any(|c| !c.header.is_empty()) {
625            // Top padding
626            let (pt, _pr, _pb, _pl) = self.padding;
627            for _ in 0..pt {
628                lines.push(self.render_row_line(&col_widths, &[], b, available_width, false));
629            }
630
631            let header_cells: Vec<String> = self.columns.iter().map(|c| c.header.clone()).collect();
632            lines.push(self.render_cell_line(&col_widths, &header_cells, b, true));
633
634            // Header separator
635            let mut sep = vec![bs(b.head_row_left)];
636            for (i, w) in col_widths.iter().enumerate() {
637                sep.push(bs_repeat(b.head_row_horizontal, *w));
638                if i < col_count - 1 {
639                    sep.push(bs(b.head_row_cross));
640                }
641            }
642            sep.push(bs(b.head_row_right));
643            sep.push(Segment::line());
644            lines.push(sep);
645        }
646
647        // -- Rows --
648        let mut rowspan_remaining: Vec<usize> = vec![0; col_count];
649        for (row_idx, row) in self.rows.iter().enumerate() {
650            // Section separator
651            if self.section_rows.contains(&row_idx) {
652                let sep_widths = Self::compute_span_widths(row, &col_widths);
653                let sc = sep_widths.len();
654                let mut sep = vec![bs(b.head_row_left)];
655                for (i, w) in sep_widths.iter().enumerate() {
656                    sep.push(bs_repeat(b.head_row_horizontal, *w));
657                    if i < sc - 1 {
658                        sep.push(bs(b.head_row_cross));
659                    }
660                }
661                sep.push(bs(b.head_row_right));
662                sep.push(Segment::line());
663                lines.push(sep);
664            }
665
666            // Leading blank lines between rows
667            if row_idx > 0 {
668                for _ in 0..self.leading {
669                    lines.push(self.render_row_line(&col_widths, &[], b, available_width, false));
670                }
671            }
672
673            let (pt, _pr, _pb, _pl) = self.padding;
674            for _ in 0..pt {
675                lines.push(self.render_row_line(&col_widths, &[], b, available_width, false));
676            }
677
678            let _style = if row_idx < self.row_styles.len() {
679                Some(&self.row_styles[row_idx])
680            } else if self.row_styles.len() == 2 {
681                Some(&self.row_styles[row_idx % 2])
682            } else {
683                None
684            };
685
686            lines.push(self.render_cell_line_with_rowspan(
687                &col_widths,
688                row,
689                b,
690                false,
691                &mut rowspan_remaining,
692            ));
693
694            // Row separator (respect colspan in current row)
695            if self.show_lines && row_idx < self.rows.len() - 1 {
696                let sep_widths = Self::compute_span_widths(row, &col_widths);
697                let sc = sep_widths.len();
698                let mut sep = vec![bs(b.row_left)];
699                for (i, w) in sep_widths.iter().enumerate() {
700                    sep.push(bs_repeat(b.row_horizontal, *w));
701                    if i < sc - 1 {
702                        sep.push(bs(b.row_cross));
703                    }
704                }
705                sep.push(bs(b.row_right));
706                sep.push(Segment::line());
707                lines.push(sep);
708            }
709        }
710
711        // -- Footer --
712        if self.show_footer && self.columns.iter().any(|c| !c.footer.is_empty()) {
713            let mut sep = vec![bs(b.foot_row_left)];
714            for (i, w) in col_widths.iter().enumerate() {
715                sep.push(bs_repeat(b.foot_row_horizontal, *w));
716                if i < col_count - 1 {
717                    sep.push(bs(b.foot_row_cross));
718                }
719            }
720            sep.push(bs(b.foot_row_right));
721            sep.push(Segment::line());
722            lines.push(sep);
723
724            let footer_cells: Vec<String> = self.columns.iter().map(|c| c.footer.clone()).collect();
725            lines.push(self.render_cell_line(&col_widths, &footer_cells, b, false));
726        }
727
728        // -- Bottom border --
729        if self.show_edge {
730            let bottom_widths = self.compute_bottom_widths(&col_widths);
731            let mut bot_line = vec![bs(b.bottom_left)];
732            let bc = bottom_widths.len();
733            for (i, w) in bottom_widths.iter().enumerate() {
734                bot_line.push(bs_repeat(b.bottom, *w));
735                if i < bc - 1 {
736                    bot_line.push(bs(b.bottom_divider));
737                }
738            }
739            bot_line.push(bs(b.bottom_right));
740            bot_line.push(Segment::line());
741            lines.push(bot_line);
742        }
743
744        // -- Caption --
745        if let Some(ref caption) = self.caption {
746            let centered = self
747                .caption_justify
748                .align_text(caption, available_width.saturating_sub(2));
749            lines.push(vec![Segment::new(&centered), Segment::line()]);
750        }
751
752        // Strip ANSI escapes when in ASCII-only mode so that raw escape
753        // sequences don't leak into the output (e.g. "[1m" instead of bold).
754        if options.ascii_only {
755            for line in &mut lines {
756                for seg in line.iter_mut() {
757                    if seg.text.contains('\x1b') {
758                        seg.text = crate::export::strip_ansi_escapes(&seg.text);
759                    }
760                }
761            }
762        }
763
764        RenderResult {
765            lines,
766            items: Vec::new(),
767        }
768    }
769}
770
771impl Table {
772    fn calculate_column_widths(&self, available: usize) -> Vec<usize> {
773        let col_count = self.columns.len();
774        let total_pad = col_count.saturating_sub(1) + 2; // separators + edges
775        let content_width = available.saturating_sub(total_pad);
776
777        // If any column has a fixed width, respect it
778        let mut widths: Vec<usize> = vec![0; col_count];
779        let mut flex_cols: Vec<usize> = Vec::new();
780        let mut used = 0usize;
781
782        for (i, col) in self.columns.iter().enumerate() {
783            if let Some(w) = col.width {
784                widths[i] = w;
785                used += w;
786            } else {
787                flex_cols.push(i);
788            }
789        }
790
791        if flex_cols.is_empty() {
792            return widths;
793        }
794
795        let remaining = content_width.saturating_sub(used);
796        let _flex_count = flex_cols.len();
797
798        // Distribute remaining width using ratios if available
799        let total_ratio: usize = flex_cols
800            .iter()
801            .map(|&i| self.columns[i].ratio.unwrap_or(1))
802            .sum();
803
804        for &i in &flex_cols {
805            let col = &self.columns[i];
806            let ratio = col.ratio.unwrap_or(1);
807            let mut w = (remaining * ratio) / total_ratio;
808            if let Some(min_w) = col.min_width {
809                w = w.max(min_w);
810            }
811            if let Some(max_w) = col.max_width {
812                w = w.min(max_w);
813            }
814            w = w.max(3); // minimum usable width
815            widths[i] = w;
816        }
817
818        // Adjust for rounding
819        let total: usize = widths.iter().sum();
820        if total < content_width && !flex_cols.is_empty() {
821            let extra = content_width - total;
822            widths[flex_cols[flex_cols.len() - 1]] += extra;
823        }
824
825        widths
826    }
827
828    fn render_cell_line(
829        &self,
830        widths: &[usize],
831        values: &[String],
832        b: &BoxStyle,
833        is_header: bool,
834    ) -> Vec<Segment> {
835        let mut line = Vec::new();
836        let col_count = widths.len();
837        let bs = |ch: char| -> Segment {
838            let ansi = self.border_style.to_ansi();
839            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
840            Segment::new(format!("{ansi}{ch}{reset}"))
841        };
842
843        line.push(bs(b.mid_left));
844
845        for (i, w) in widths.iter().enumerate() {
846            let val = values.get(i).map(|s| s.as_str()).unwrap_or("");
847            let col = self.columns.get(i);
848            let justify = col.map(|c| c.justify).unwrap_or(AlignMethod::Left);
849            let (_pt, pr, _pb, pl) = self.padding;
850
851            // Adjust edge padding based on pad_edge
852            let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
853            let right_pad = if i == col_count - 1 && !self.pad_edge {
854                0
855            } else {
856                pr
857            };
858
859            // Pad left
860            line.push(Segment::new(" ".repeat(left_pad)));
861
862            // Align the text
863            let content_w = w.saturating_sub(left_pad + right_pad);
864            let disp = justify.align_text(val, content_w);
865            // Truncate if needed
866            let disp_trunc = if UnicodeWidthStr::width(disp.as_str()) > content_w {
867                let mut truncated = disp
868                    .chars()
869                    .take(
870                        content_w.saturating_sub(1), // leave room for ellipsis
871                    )
872                    .collect::<String>();
873                truncated.push('…');
874                truncated
875            } else {
876                disp
877            };
878
879            // Apply header style if needed
880            if is_header {
881                let header_style = col.map(|c| &c.header_style);
882                if let Some(hs) = header_style {
883                    let ansi = hs.to_ansi();
884                    let reset = hs.reset_ansi();
885                    line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
886                } else {
887                    line.push(Segment::new(disp_trunc));
888                }
889            } else {
890                line.push(Segment::new(disp_trunc));
891            }
892
893            // Pad right
894            line.push(Segment::new(" ".repeat(right_pad)));
895
896            if i < col_count - 1 {
897                line.push(bs(b.mid_vertical));
898            }
899        }
900
901        line.push(bs(b.mid_right));
902        line.push(Segment::line());
903        line
904    }
905
906    /// Render a row of Cells with colspan/rowspan support.
907    /// `rowspan_remaining` is updated to track active rowspans.
908    fn render_cell_line_with_rowspan(
909        &self,
910        widths: &[usize],
911        cells: &[Cell],
912        b: &BoxStyle,
913        is_header: bool,
914        rowspan_remaining: &mut [usize],
915    ) -> Vec<Segment> {
916        let mut line = Vec::new();
917        let col_count = widths.len();
918        let bs = |ch: char| -> Segment {
919            let ansi = self.border_style.to_ansi();
920            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
921            Segment::new(format!("{ansi}{ch}{reset}"))
922        };
923
924        line.push(bs(b.mid_left));
925
926        let mut cell_idx = 0;
927        let mut col: usize = 0;
928
929        while col < col_count {
930            // Check for active rowspan in this column
931            if rowspan_remaining[col] > 0 {
932                // A spanned cell from a previous row covers this column.
933                // Accumulate ALL consecutive columns that belong to the same
934                // rowspan group (originating from one cell with colspan=N)
935                // to avoid drawing stray vertical dividers inside the span.
936                let span_start = col;
937                let mut span_total_w = 0usize;
938                while col < col_count && rowspan_remaining[col] > 0 {
939                    rowspan_remaining[col] -= 1;
940                    span_total_w += widths[col];
941                    col += 1;
942                }
943                // Add the width of the internal separators that were removed
944                // (1 char each) so the total span matches the original colspan cell.
945                let num_spanned = col - span_start;
946                span_total_w += num_spanned.saturating_sub(1);
947                line.push(Segment::new(" ".repeat(span_total_w)));
948                // Only one vertical separator after the whole spanned group
949                if col < col_count {
950                    line.push(bs(b.mid_vertical));
951                }
952                continue;
953            }
954
955            // No more cells — fill remaining columns as empty
956            if cell_idx >= cells.len() {
957                let w = widths[col];
958                let (_pt, pr, _pb, pl) = self.padding;
959                let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
960                let right_pad = if col == col_count - 1 && !self.pad_edge {
961                    0
962                } else {
963                    pr
964                };
965                line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
966                if col < col_count - 1 {
967                    line.push(bs(b.mid_vertical));
968                }
969                col += 1;
970                continue;
971            }
972
973            let cell = &cells[cell_idx];
974            cell_idx += 1;
975
976            let span_end = (col + cell.colspan).min(col_count);
977            let num_spanned = span_end - col;
978            // Include the width of internal separators that are removed by the
979            // colspan (1 char each) so the total row width stays consistent.
980            let span_width: usize =
981                widths[col..span_end].iter().sum::<usize>() + num_spanned.saturating_sub(1);
982            let (_pt, pr, _pb, pl) = self.padding;
983            let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
984            let right_pad = if span_end >= col_count && !self.pad_edge {
985                0
986            } else {
987                pr
988            };
989            let content_width = span_width.saturating_sub(left_pad + right_pad);
990
991            let col_def = self.columns.get(col);
992            let justify = col_def.map(|c| c.justify).unwrap_or(AlignMethod::Left);
993
994            // Align and truncate content
995            let disp_text = justify.align_text(&cell.content, content_width);
996            let disp_trunc = if UnicodeWidthStr::width(disp_text.as_str()) > content_width {
997                let mut truncated: String = disp_text
998                    .chars()
999                    .take(content_width.saturating_sub(1))
1000                    .collect();
1001                truncated.push('…');
1002                truncated
1003            } else {
1004                disp_text
1005            };
1006
1007            // Pad left
1008            line.push(Segment::new(" ".repeat(left_pad)));
1009
1010            // Apply cell style, header style, or column style
1011            if let Some(ref cell_style) = cell.style {
1012                let ansi = cell_style.to_ansi();
1013                let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
1014                line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
1015            } else if is_header {
1016                if let Some(hs) = col_def.map(|c| &c.header_style) {
1017                    let ansi = hs.to_ansi();
1018                    let reset = hs.reset_ansi();
1019                    line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
1020                } else {
1021                    line.push(Segment::new(disp_trunc));
1022                }
1023            } else {
1024                // Apply column default style if it has ANSI
1025                let col_ansi = col_def.map(|c| c.style.to_ansi()).unwrap_or_default();
1026                if col_ansi.is_empty() {
1027                    line.push(Segment::new(disp_trunc));
1028                } else {
1029                    line.push(Segment::new(format!("{col_ansi}{disp_trunc}\x1b[0m")));
1030                }
1031            }
1032
1033            // Pad right
1034            line.push(Segment::new(" ".repeat(right_pad)));
1035
1036            // Set rowspan for future rows
1037            if cell.rowspan > 1 {
1038                for item in &mut rowspan_remaining[col..span_end] {
1039                    *item = cell.rowspan - 1;
1040                }
1041            }
1042
1043            col = span_end;
1044
1045            // Vertical separator after the span
1046            if col < col_count {
1047                line.push(bs(b.mid_vertical));
1048            }
1049        }
1050
1051        line.push(bs(b.mid_right));
1052        line.push(Segment::line());
1053        line
1054    }
1055
1056    /// Compute the effective column widths for a row, respecting colspan so
1057    /// that border/separator divider characters only appear at real column
1058    /// boundaries rather than mid-span.
1059    fn compute_span_widths(cells: &[Cell], col_widths: &[usize]) -> Vec<usize> {
1060        let col_count = col_widths.len();
1061        if col_count == 0 {
1062            return vec![];
1063        }
1064
1065        let mut widths = Vec::new();
1066        let mut col = 0usize;
1067        for cell in cells {
1068            if col >= col_count {
1069                break;
1070            }
1071            let span = cell.colspan.min(col_count - col);
1072            // Include internal separator widths (1 char each) removed by colspan
1073            let w: usize =
1074                col_widths[col..col + span].iter().sum::<usize>() + span.saturating_sub(1);
1075            widths.push(w);
1076            col += span;
1077        }
1078        // Fill remaining columns with original widths
1079        while col < col_count {
1080            widths.push(col_widths[col]);
1081            col += 1;
1082        }
1083        widths
1084    }
1085
1086    /// Compute the effective column widths for the bottom border, respecting
1087    /// colspan in the last row.
1088    fn compute_bottom_widths(&self, col_widths: &[usize]) -> Vec<usize> {
1089        if self.rows.is_empty() {
1090            return col_widths.to_vec();
1091        }
1092        Self::compute_span_widths(&self.rows[self.rows.len() - 1], col_widths)
1093    }
1094
1095    fn render_row_line(
1096        &self,
1097        widths: &[usize],
1098        _values: &[String],
1099        b: &BoxStyle,
1100        _available_width: usize,
1101        _is_header: bool,
1102    ) -> Vec<Segment> {
1103        let mut line = Vec::new();
1104        let col_count = widths.len();
1105        let bs = |ch: char| -> Segment {
1106            let ansi = self.border_style.to_ansi();
1107            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
1108            Segment::new(format!("{ansi}{ch}{reset}"))
1109        };
1110
1111        // Use mid_left for the outer left edge (not mid_vertical which is
1112        // the internal column separator — they differ for asymmetric boxes).
1113        line.push(bs(b.mid_left));
1114        for (i, w) in widths.iter().enumerate() {
1115            let (_pt, pr, _pb, pl) = self.padding;
1116            let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
1117            let right_pad = if i == col_count - 1 && !self.pad_edge {
1118                0
1119            } else {
1120                pr
1121            };
1122            line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
1123            if i < col_count - 1 {
1124                line.push(bs(b.mid_vertical));
1125            }
1126        }
1127        line.push(bs(b.mid_right));
1128        line.push(Segment::line());
1129        line
1130    }
1131}
1132
1133impl Default for Table {
1134    fn default() -> Self {
1135        Self::new()
1136    }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141    use super::*;
1142
1143    #[test]
1144    fn test_empty_table() {
1145        let table = Table::new();
1146        let opts = ConsoleOptions::default();
1147        let result = table.render(&opts);
1148        assert!(result.lines.is_empty());
1149    }
1150
1151    #[test]
1152    fn test_table_with_one_column() {
1153        let mut table = Table::new();
1154        table.add_column(Column::new("Name"));
1155        table.add_row_str(vec!["Alice".into()]);
1156        table.add_row_str(vec!["Bob".into()]);
1157
1158        let opts = ConsoleOptions::default();
1159        let result = table.render(&opts);
1160        let ansi = result.to_ansi();
1161        assert!(ansi.contains("Name"));
1162        assert!(ansi.contains("Alice"));
1163    }
1164
1165    #[test]
1166    fn test_cell_creation() {
1167        let cell = Cell::new("hello");
1168        assert_eq!(cell.content, "hello");
1169        assert_eq!(cell.colspan, 1);
1170        assert_eq!(cell.rowspan, 1);
1171        assert!(cell.style.is_none());
1172
1173        let cell2 = Cell::new("world").colspan(2).rowspan(3);
1174        assert_eq!(cell2.content, "world");
1175        assert_eq!(cell2.colspan, 2);
1176        assert_eq!(cell2.rowspan, 3);
1177    }
1178
1179    #[test]
1180    fn test_cell_from_string() {
1181        let cell: Cell = "test".into();
1182        assert_eq!(cell.content, "test");
1183    }
1184
1185    #[test]
1186    fn test_column_colspan() {
1187        let col = Column::new("Header");
1188        assert_eq!(col.colspan, 1);
1189    }
1190
1191    #[test]
1192    fn test_add_row_str() {
1193        let mut table = Table::new();
1194        table.add_column(Column::new("A"));
1195        table.add_column(Column::new("B"));
1196        table.add_row_str(vec!["x".into(), "y".into()]);
1197        assert_eq!(table.row_count(), 1);
1198    }
1199
1200    #[test]
1201    fn test_add_section() {
1202        let mut table = Table::new();
1203        table.add_column(Column::new("A"));
1204        table.add_row_str(vec!["r1".into()]);
1205        table.add_section();
1206        table.add_row_str(vec!["r2".into()]);
1207        assert_eq!(table.row_count(), 2);
1208        assert!(table.section_rows.contains(&1));
1209
1210        let opts = ConsoleOptions::default();
1211        let result = table.render(&opts);
1212        let ansi = result.to_ansi();
1213        assert!(ansi.contains("r1"));
1214        assert!(ansi.contains("r2"));
1215    }
1216
1217    #[test]
1218    fn test_leading() {
1219        let table = Table::new()
1220            .column(Column::new("X"))
1221            .row_str(vec!["a".into()])
1222            .row_str(vec!["b".into()])
1223            .leading(1);
1224        assert_eq!(table.leading, 1);
1225    }
1226
1227    #[test]
1228    fn test_cell_rowspan() {
1229        let mut table = Table::new();
1230        table.add_column(Column::new("A"));
1231        table.add_column(Column::new("B"));
1232        let cell_a = Cell::new("span").rowspan(2);
1233        let cell_b = Cell::new("single");
1234        table.add_row(vec![cell_a, cell_b]);
1235        table.add_row_str(vec!["row2col2".into()]);
1236
1237        let opts = ConsoleOptions::default();
1238        let result = table.render(&opts);
1239        let ansi = result.to_ansi();
1240        assert!(ansi.contains("span"));
1241    }
1242
1243    #[test]
1244    fn test_cell_colspan() {
1245        let mut table = Table::new();
1246        table.add_column(Column::new("A"));
1247        table.add_column(Column::new("B"));
1248        table.add_column(Column::new("C"));
1249        let cell = Cell::new("wide").colspan(2);
1250        table.add_row(vec![cell, Cell::new("c")]);
1251        table.add_row_str(vec!["a".into(), "b".into(), "c".into()]);
1252
1253        let opts = ConsoleOptions::default();
1254        let result = table.render(&opts);
1255        let ansi = result.to_ansi();
1256        assert!(ansi.contains("wide"));
1257    }
1258
1259    // --- New feature tests ---
1260
1261    #[test]
1262    fn test_row_struct() {
1263        let cells = vec![Cell::new("a"), Cell::new("b")];
1264        let row = Row::new(cells)
1265            .style(Style::new().bold(true))
1266            .end_section(true);
1267        assert_eq!(row.cells.len(), 2);
1268        assert!(row.style.is_some());
1269        assert!(row.end_section);
1270    }
1271
1272    #[test]
1273    fn test_add_row_explicit() {
1274        let mut table = Table::new();
1275        table.add_column(Column::new("A"));
1276        table.add_column(Column::new("B"));
1277        let row = Row::new(vec![Cell::new("x"), Cell::new("y")]);
1278        table.add_row_explicit(row);
1279        assert_eq!(table.row_count(), 1);
1280
1281        let opts = ConsoleOptions::default();
1282        let result = table.render(&opts);
1283        let ansi = result.to_ansi();
1284        assert!(ansi.contains("x"));
1285        assert!(ansi.contains("y"));
1286    }
1287
1288    #[test]
1289    fn test_add_row_explicit_with_section() {
1290        let mut table = Table::new();
1291        table.add_column(Column::new("A"));
1292        table.add_row_str(vec!["before".into()]);
1293        let row = Row::new(vec![Cell::new("after")]).end_section(true);
1294        table.add_row_explicit(row);
1295        assert!(table.section_rows.contains(&1));
1296    }
1297
1298    #[test]
1299    fn test_builder_highlight() {
1300        let table = Table::new().highlight(true);
1301        assert!(table.highlight);
1302    }
1303
1304    #[test]
1305    fn test_builder_title_justify() {
1306        let table = Table::new().title_justify(AlignMethod::Right);
1307        assert_eq!(table.title_justify, AlignMethod::Right);
1308    }
1309
1310    #[test]
1311    fn test_builder_caption_justify() {
1312        let table = Table::new().caption_justify(AlignMethod::Left);
1313        assert_eq!(table.caption_justify, AlignMethod::Left);
1314    }
1315
1316    #[test]
1317    fn test_builder_row_styles() {
1318        let s1 = Style::new().bold(true);
1319        let s2 = Style::new().dim(true);
1320        let table = Table::new().row_styles(vec![s1.clone(), s2.clone()]);
1321        assert_eq!(table.row_styles.len(), 2);
1322    }
1323
1324    #[test]
1325    fn test_builder_show_edge() {
1326        let table = Table::new().show_edge(false);
1327        assert!(!table.show_edge);
1328    }
1329
1330    #[test]
1331    fn test_builder_collapse_padding() {
1332        let table = Table::new().collapse_padding(true);
1333        assert!(table.collapse_padding);
1334    }
1335
1336    #[test]
1337    fn test_builder_pad_edge() {
1338        let table = Table::new().pad_edge(false);
1339        assert!(!table.pad_edge);
1340    }
1341
1342    #[test]
1343    fn test_get_row_style_empty() {
1344        let table = Table::new();
1345        assert_eq!(table.get_row_style(0), None);
1346    }
1347
1348    #[test]
1349    fn test_get_row_style_with_styles() {
1350        let s1 = Style::new().bold(true);
1351        let s2 = Style::new().dim(true);
1352        let table = Table::new().row_styles(vec![s1, s2]);
1353        assert!(table.get_row_style(0).is_some());
1354        assert!(table.get_row_style(1).is_some());
1355        // Cycles
1356        assert!(table.get_row_style(2).is_some());
1357        assert!(table.get_row_style(3).is_some());
1358    }
1359
1360    #[test]
1361    fn test_add_section_returns_self() {
1362        let mut table = Table::new();
1363        table.add_column(Column::new("A"));
1364        table.add_row_str(vec!["r1".into()]);
1365        let ret = table.add_section();
1366        // Verify the return value is &mut Self (can chain)
1367        ret.add_row_str(vec!["r2".into()]);
1368        assert_eq!(table.row_count(), 2);
1369    }
1370
1371    #[test]
1372    fn test_sections_field() {
1373        let mut table = Table::new();
1374        table.add_column(Column::new("A"));
1375        table.add_row_str(vec!["r1".into()]);
1376        table.add_section();
1377        table.add_row_str(vec!["r2".into()]);
1378        assert_eq!(table.sections.len(), 1);
1379        assert_eq!(table.sections[0], 1);
1380    }
1381
1382    #[test]
1383    fn test_pad_edge_default() {
1384        let table = Table::new();
1385        assert!(table.pad_edge);
1386    }
1387
1388    #[test]
1389    fn test_grid_method() {
1390        let table = Table::grid();
1391        assert!(!table.show_edge);
1392        assert!(!table.show_header);
1393    }
1394}