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