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("Alice", "30");
19//! table.add_row_str("Bob", "25");
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
480        let bs = |ch: char| -> Segment {
481            let ansi = self.border_style.to_ansi();
482            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
483            Segment::new(format!("{ansi}{ch}{reset}"))
484        };
485
486        // -- Title --
487        if let Some(ref title) = self.title {
488            let _tw = UnicodeWidthStr::width(title.as_str());
489            let centered = self.title_justify.align_text(title, available_width.saturating_sub(2));
490            lines.push(vec![bs(b.top_left), Segment::new(&centered[1..centered.len()-1]), bs(b.top_right), Segment::line()]);
491        }
492
493        // -- Top border --
494        if self.show_edge {
495            let mut top_line = vec![bs(b.top_left)];
496            for (i, w) in col_widths.iter().enumerate() {
497                top_line.push(Segment::new(b.top.to_string().repeat(*w)));
498                if i < col_count - 1 {
499                    top_line.push(bs(b.top_divider));
500                }
501            }
502            top_line.push(bs(b.top_right));
503            top_line.push(Segment::line());
504            lines.push(top_line);
505        }
506
507        // -- Header --
508        if self.show_header && self.columns.iter().any(|c| !c.header.is_empty()) {
509            // Top padding
510            let (pt, _pr, _pb, _pl) = self.padding;
511            for _ in 0..pt {
512                lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
513            }
514
515            let header_cells: Vec<String> = self.columns.iter()
516                .map(|c| c.header.clone())
517                .collect();
518            lines.push(self.render_cell_line(&col_widths, &header_cells, &b, true));
519
520            // Header separator
521            let mut sep = vec![bs(b.head_row_left)];
522            for (i, w) in col_widths.iter().enumerate() {
523                sep.push(Segment::new(b.head_row_horizontal.to_string().repeat(*w)));
524                if i < col_count - 1 {
525                    sep.push(bs(b.head_row_cross));
526                }
527            }
528            sep.push(bs(b.head_row_right));
529            sep.push(Segment::line());
530            lines.push(sep);
531        }
532
533        // -- Rows --
534        let mut rowspan_remaining: Vec<usize> = vec![0; col_count];
535        for (row_idx, row) in self.rows.iter().enumerate() {
536            // Section separator
537            if self.section_rows.contains(&row_idx) {
538                let mut sep = vec![bs(b.head_row_left)];
539                for (i, w) in col_widths.iter().enumerate() {
540                    sep.push(Segment::new(b.head_row_horizontal.to_string().repeat(*w)));
541                    if i < col_count - 1 {
542                        sep.push(bs(b.head_row_cross));
543                    }
544                }
545                sep.push(bs(b.head_row_right));
546                sep.push(Segment::line());
547                lines.push(sep);
548            }
549
550            // Leading blank lines between rows
551            if row_idx > 0 {
552                for _ in 0..self.leading {
553                    lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
554                }
555            }
556
557            let (pt, _pr, _pb, _pl) = self.padding;
558            for _ in 0..pt {
559                lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
560            }
561
562            let _style = if row_idx < self.row_styles.len() {
563                Some(&self.row_styles[row_idx])
564            } else if self.row_styles.len() == 2 {
565                Some(&self.row_styles[row_idx % 2])
566            } else {
567                None
568            };
569
570            lines.push(self.render_cell_line_with_rowspan(
571                &col_widths, row, &b, false, &mut rowspan_remaining,
572            ));
573
574            // Row separator
575            if self.show_lines && row_idx < self.rows.len() - 1 {
576                let mut sep = vec![bs(b.row_left)];
577                for (i, w) in col_widths.iter().enumerate() {
578                    sep.push(Segment::new(b.row_horizontal.to_string().repeat(*w)));
579                    if i < col_count - 1 {
580                        sep.push(bs(b.row_cross));
581                    }
582                }
583                sep.push(bs(b.row_right));
584                sep.push(Segment::line());
585                lines.push(sep);
586            }
587        }
588
589        // -- Footer --
590        if self.show_footer && self.columns.iter().any(|c| !c.footer.is_empty()) {
591            let mut sep = vec![bs(b.foot_row_left)];
592            for (i, w) in col_widths.iter().enumerate() {
593                sep.push(Segment::new(b.foot_row_horizontal.to_string().repeat(*w)));
594                if i < col_count - 1 {
595                    sep.push(bs(b.foot_row_cross));
596                }
597            }
598            sep.push(bs(b.foot_row_right));
599            sep.push(Segment::line());
600            lines.push(sep);
601
602            let footer_cells: Vec<String> = self.columns.iter()
603                .map(|c| c.footer.clone())
604                .collect();
605            lines.push(self.render_cell_line(&col_widths, &footer_cells, &b, false));
606        }
607
608        // -- Bottom border --
609        if self.show_edge {
610            let mut bot_line = vec![bs(b.bottom_left)];
611            for (i, w) in col_widths.iter().enumerate() {
612                bot_line.push(Segment::new(b.bottom.to_string().repeat(*w)));
613                if i < col_count - 1 {
614                    bot_line.push(bs(b.bottom_divider));
615                }
616            }
617            bot_line.push(bs(b.bottom_right));
618            bot_line.push(Segment::line());
619            lines.push(bot_line);
620        }
621
622        // -- Caption --
623        if let Some(ref caption) = self.caption {
624            let centered = self.caption_justify.align_text(caption, available_width.saturating_sub(2));
625            lines.push(vec![Segment::new(&centered), Segment::line()]);
626        }
627
628        RenderResult { lines, items: Vec::new() }
629    }
630}
631
632impl Table {
633    fn calculate_column_widths(&self, available: usize) -> Vec<usize> {
634        let col_count = self.columns.len();
635        let total_pad = col_count.saturating_sub(1) + 2; // separators + edges
636        let content_width = available.saturating_sub(total_pad);
637
638        // If any column has a fixed width, respect it
639        let mut widths: Vec<usize> = vec![0; col_count];
640        let mut flex_cols: Vec<usize> = Vec::new();
641        let mut used = 0usize;
642
643        for (i, col) in self.columns.iter().enumerate() {
644            if let Some(w) = col.width {
645                widths[i] = w;
646                used += w;
647            } else {
648                flex_cols.push(i);
649            }
650        }
651
652        if flex_cols.is_empty() {
653            return widths;
654        }
655
656        let remaining = content_width.saturating_sub(used);
657        let _flex_count = flex_cols.len();
658
659        // Distribute remaining width using ratios if available
660        let total_ratio: usize = flex_cols.iter()
661            .map(|&i| self.columns[i].ratio.unwrap_or(1))
662            .sum();
663
664        for &i in &flex_cols {
665            let col = &self.columns[i];
666            let ratio = col.ratio.unwrap_or(1);
667            let mut w = (remaining * ratio) / total_ratio;
668            if let Some(min_w) = col.min_width {
669                w = w.max(min_w);
670            }
671            if let Some(max_w) = col.max_width {
672                w = w.min(max_w);
673            }
674            w = w.max(3); // minimum usable width
675            widths[i] = w;
676        }
677
678        // Adjust for rounding
679        let total: usize = widths.iter().sum();
680        if total < content_width && !flex_cols.is_empty() {
681            let extra = content_width - total;
682            widths[flex_cols[flex_cols.len() - 1]] += extra;
683        }
684
685        widths
686    }
687
688    fn render_cell_line(
689        &self,
690        widths: &[usize],
691        values: &[String],
692        b: &BoxStyle,
693        is_header: bool,
694    ) -> Vec<Segment> {
695        let mut line = Vec::new();
696        let col_count = widths.len();
697        let bs = |ch: char| -> Segment {
698            let ansi = self.border_style.to_ansi();
699            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
700            Segment::new(format!("{ansi}{ch}{reset}"))
701        };
702
703        line.push(bs(b.mid_vertical));
704
705        for (i, w) in widths.iter().enumerate() {
706            let val = values.get(i).map(|s| s.as_str()).unwrap_or("");
707            let col = self.columns.get(i);
708            let justify = col.map(|c| c.justify).unwrap_or(AlignMethod::Left);
709            let (_pt, pr, _pb, pl) = self.padding;
710
711            // Adjust edge padding based on pad_edge
712            let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
713            let right_pad = if i == col_count - 1 && !self.pad_edge { 0 } else { pr };
714
715            // Pad left
716            line.push(Segment::new(" ".repeat(left_pad)));
717
718            // Align the text
719            let content_w = w.saturating_sub(left_pad + right_pad);
720            let disp = justify.align_text(val, content_w);
721            // Truncate if needed
722            let disp_trunc = if UnicodeWidthStr::width(disp.as_str()) > content_w {
723                let mut truncated = disp.chars().take(
724                    content_w.saturating_sub(1) // leave room for ellipsis
725                ).collect::<String>();
726                truncated.push('…');
727                truncated
728            } else {
729                disp
730            };
731
732            // Apply header style if needed
733            if is_header {
734                let header_style = col.map(|c| &c.header_style);
735                if let Some(hs) = header_style {
736                    let ansi = hs.to_ansi();
737                    let reset = hs.reset_ansi();
738                    line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
739                } else {
740                    line.push(Segment::new(disp_trunc));
741                }
742            } else {
743                line.push(Segment::new(disp_trunc));
744            }
745
746            // Pad right
747            line.push(Segment::new(" ".repeat(right_pad)));
748
749            if i < col_count - 1 {
750                line.push(bs(b.mid_vertical));
751            }
752        }
753
754        line.push(bs(b.mid_right));
755        line.push(Segment::line());
756        line
757    }
758
759    /// Render a row of Cells with colspan/rowspan support.
760    /// `rowspan_remaining` is updated to track active rowspans.
761    fn render_cell_line_with_rowspan(
762        &self,
763        widths: &[usize],
764        cells: &[Cell],
765        b: &BoxStyle,
766        is_header: bool,
767        rowspan_remaining: &mut [usize],
768    ) -> Vec<Segment> {
769        let mut line = Vec::new();
770        let col_count = widths.len();
771        let bs = |ch: char| -> Segment {
772            let ansi = self.border_style.to_ansi();
773            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
774            Segment::new(format!("{ansi}{ch}{reset}"))
775        };
776
777        line.push(bs(b.mid_vertical));
778
779        let mut cell_idx = 0;
780        let mut col: usize = 0;
781
782        while col < col_count {
783            // Check for active rowspan in this column
784            if rowspan_remaining[col] > 0 {
785                rowspan_remaining[col] -= 1;
786                // Render an empty spanned cell for this column
787                let w = widths[col];
788                let (_pt, pr, _pb, pl) = self.padding;
789                let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
790                let right_pad = if col == col_count - 1 && !self.pad_edge { 0 } else { pr };
791                line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
792                if col < col_count - 1 {
793                    line.push(bs(b.mid_vertical));
794                }
795                col += 1;
796                continue;
797            }
798
799            // No more cells — fill remaining columns as empty
800            if cell_idx >= cells.len() {
801                let w = widths[col];
802                let (_pt, pr, _pb, pl) = self.padding;
803                let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
804                let right_pad = if col == col_count - 1 && !self.pad_edge { 0 } else { pr };
805                line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
806                if col < col_count - 1 {
807                    line.push(bs(b.mid_vertical));
808                }
809                col += 1;
810                continue;
811            }
812
813            let cell = &cells[cell_idx];
814            cell_idx += 1;
815
816            let span_end = (col + cell.colspan).min(col_count);
817            let span_width: usize = widths[col..span_end].iter().sum();
818            let (_pt, pr, _pb, pl) = self.padding;
819            let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
820            let right_pad = if span_end >= col_count && !self.pad_edge { 0 } else { pr };
821            let content_width = span_width.saturating_sub(left_pad + right_pad);
822
823            let col_def = self.columns.get(col);
824            let justify = col_def.map(|c| c.justify).unwrap_or(AlignMethod::Left);
825
826            // Align and truncate content
827            let disp_text = justify.align_text(&cell.content, content_width);
828            let disp_trunc = if UnicodeWidthStr::width(disp_text.as_str()) > content_width {
829                let mut truncated: String = disp_text.chars()
830                    .take(content_width.saturating_sub(1))
831                    .collect();
832                truncated.push('…');
833                truncated
834            } else {
835                disp_text
836            };
837
838            // Pad left
839            line.push(Segment::new(" ".repeat(left_pad)));
840
841            // Apply cell style, header style, or column style
842            if let Some(ref cell_style) = cell.style {
843                let ansi = cell_style.to_ansi();
844                let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
845                line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
846            } else if is_header {
847                if let Some(hs) = col_def.map(|c| &c.header_style) {
848                    let ansi = hs.to_ansi();
849                    let reset = hs.reset_ansi();
850                    line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
851                } else {
852                    line.push(Segment::new(disp_trunc));
853                }
854            } else {
855                // Apply column default style if it has ANSI
856                let col_ansi = col_def.map(|c| c.style.to_ansi()).unwrap_or_default();
857                if col_ansi.is_empty() {
858                    line.push(Segment::new(disp_trunc));
859                } else {
860                    line.push(Segment::new(format!("{col_ansi}{disp_trunc}\x1b[0m")));
861                }
862            }
863
864            // Pad right
865            line.push(Segment::new(" ".repeat(right_pad)));
866
867            // Set rowspan for future rows
868            if cell.rowspan > 1 {
869                for rc in col..span_end {
870                    rowspan_remaining[rc] = cell.rowspan - 1;
871                }
872            }
873
874            col = span_end;
875
876            // Vertical separator after the span
877            if col < col_count {
878                line.push(bs(b.mid_vertical));
879            }
880        }
881
882        line.push(bs(b.mid_right));
883        line.push(Segment::line());
884        line
885    }
886
887    fn render_row_line(
888        &self,
889        widths: &[usize],
890        _values: &[String],
891        b: &BoxStyle,
892        _available_width: usize,
893        _is_header: bool,
894    ) -> Vec<Segment> {
895        let mut line = Vec::new();
896        let col_count = widths.len();
897        let bs = |ch: char| -> Segment {
898            let ansi = self.border_style.to_ansi();
899            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
900            Segment::new(format!("{ansi}{ch}{reset}"))
901        };
902
903        line.push(bs(b.mid_vertical));
904        for (i, w) in widths.iter().enumerate() {
905            let (_pt, pr, _pb, pl) = self.padding;
906            let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
907            let right_pad = if i == col_count - 1 && !self.pad_edge { 0 } else { pr };
908            line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
909            if i < col_count - 1 {
910                line.push(bs(b.mid_vertical));
911            }
912        }
913        line.push(bs(b.mid_right));
914        line.push(Segment::line());
915        line
916    }
917}
918
919impl Default for Table {
920    fn default() -> Self {
921        Self::new()
922    }
923}
924
925#[cfg(test)]
926mod tests {
927    use super::*;
928
929    #[test]
930    fn test_empty_table() {
931        let table = Table::new();
932        let opts = ConsoleOptions::default();
933        let result = table.render(&opts);
934        assert!(result.lines.is_empty());
935    }
936
937    #[test]
938    fn test_table_with_one_column() {
939        let mut table = Table::new();
940        table.add_column(Column::new("Name"));
941        table.add_row_str(vec!["Alice".into()]);
942        table.add_row_str(vec!["Bob".into()]);
943
944        let opts = ConsoleOptions::default();
945        let result = table.render(&opts);
946        let ansi = result.to_ansi();
947        assert!(ansi.contains("Name"));
948        assert!(ansi.contains("Alice"));
949    }
950
951    #[test]
952    fn test_cell_creation() {
953        let cell = Cell::new("hello");
954        assert_eq!(cell.content, "hello");
955        assert_eq!(cell.colspan, 1);
956        assert_eq!(cell.rowspan, 1);
957        assert!(cell.style.is_none());
958
959        let cell2 = Cell::new("world").colspan(2).rowspan(3);
960        assert_eq!(cell2.content, "world");
961        assert_eq!(cell2.colspan, 2);
962        assert_eq!(cell2.rowspan, 3);
963    }
964
965    #[test]
966    fn test_cell_from_string() {
967        let cell: Cell = "test".into();
968        assert_eq!(cell.content, "test");
969    }
970
971    #[test]
972    fn test_column_colspan() {
973        let col = Column::new("Header");
974        assert_eq!(col.colspan, 1);
975    }
976
977    #[test]
978    fn test_add_row_str() {
979        let mut table = Table::new();
980        table.add_column(Column::new("A"));
981        table.add_column(Column::new("B"));
982        table.add_row_str(vec!["x".into(), "y".into()]);
983        assert_eq!(table.row_count(), 1);
984    }
985
986    #[test]
987    fn test_add_section() {
988        let mut table = Table::new();
989        table.add_column(Column::new("A"));
990        table.add_row_str(vec!["r1".into()]);
991        table.add_section();
992        table.add_row_str(vec!["r2".into()]);
993        assert_eq!(table.row_count(), 2);
994        assert!(table.section_rows.contains(&1));
995
996        let opts = ConsoleOptions::default();
997        let result = table.render(&opts);
998        let ansi = result.to_ansi();
999        assert!(ansi.contains("r1"));
1000        assert!(ansi.contains("r2"));
1001    }
1002
1003    #[test]
1004    fn test_leading() {
1005        let table = Table::new()
1006            .column(Column::new("X"))
1007            .row_str(vec!["a".into()])
1008            .row_str(vec!["b".into()])
1009            .leading(1);
1010        assert_eq!(table.leading, 1);
1011    }
1012
1013    #[test]
1014    fn test_cell_rowspan() {
1015        let mut table = Table::new();
1016        table.add_column(Column::new("A"));
1017        table.add_column(Column::new("B"));
1018        let cell_a = Cell::new("span").rowspan(2);
1019        let cell_b = Cell::new("single");
1020        table.add_row(vec![cell_a, cell_b]);
1021        table.add_row_str(vec!["row2col2".into()]);
1022
1023        let opts = ConsoleOptions::default();
1024        let result = table.render(&opts);
1025        let ansi = result.to_ansi();
1026        assert!(ansi.contains("span"));
1027    }
1028
1029    #[test]
1030    fn test_cell_colspan() {
1031        let mut table = Table::new();
1032        table.add_column(Column::new("A"));
1033        table.add_column(Column::new("B"));
1034        table.add_column(Column::new("C"));
1035        let cell = Cell::new("wide").colspan(2);
1036        table.add_row(vec![cell, Cell::new("c")]);
1037        table.add_row_str(vec!["a".into(), "b".into(), "c".into()]);
1038
1039        let opts = ConsoleOptions::default();
1040        let result = table.render(&opts);
1041        let ansi = result.to_ansi();
1042        assert!(ansi.contains("wide"));
1043    }
1044
1045    // --- New feature tests ---
1046
1047    #[test]
1048    fn test_row_struct() {
1049        let cells = vec![Cell::new("a"), Cell::new("b")];
1050        let row = Row::new(cells)
1051            .style(Style::new().bold(true))
1052            .end_section(true);
1053        assert_eq!(row.cells.len(), 2);
1054        assert!(row.style.is_some());
1055        assert!(row.end_section);
1056    }
1057
1058    #[test]
1059    fn test_add_row_explicit() {
1060        let mut table = Table::new();
1061        table.add_column(Column::new("A"));
1062        table.add_column(Column::new("B"));
1063        let row = Row::new(vec![Cell::new("x"), Cell::new("y")]);
1064        table.add_row_explicit(row);
1065        assert_eq!(table.row_count(), 1);
1066
1067        let opts = ConsoleOptions::default();
1068        let result = table.render(&opts);
1069        let ansi = result.to_ansi();
1070        assert!(ansi.contains("x"));
1071        assert!(ansi.contains("y"));
1072    }
1073
1074    #[test]
1075    fn test_add_row_explicit_with_section() {
1076        let mut table = Table::new();
1077        table.add_column(Column::new("A"));
1078        table.add_row_str(vec!["before".into()]);
1079        let row = Row::new(vec![Cell::new("after")]).end_section(true);
1080        table.add_row_explicit(row);
1081        assert!(table.section_rows.contains(&1));
1082    }
1083
1084    #[test]
1085    fn test_builder_highlight() {
1086        let table = Table::new().highlight(true);
1087        assert!(table.highlight);
1088    }
1089
1090    #[test]
1091    fn test_builder_title_justify() {
1092        let table = Table::new().title_justify(AlignMethod::Right);
1093        assert_eq!(table.title_justify, AlignMethod::Right);
1094    }
1095
1096    #[test]
1097    fn test_builder_caption_justify() {
1098        let table = Table::new().caption_justify(AlignMethod::Left);
1099        assert_eq!(table.caption_justify, AlignMethod::Left);
1100    }
1101
1102    #[test]
1103    fn test_builder_row_styles() {
1104        let s1 = Style::new().bold(true);
1105        let s2 = Style::new().dim(true);
1106        let table = Table::new().row_styles(vec![s1.clone(), s2.clone()]);
1107        assert_eq!(table.row_styles.len(), 2);
1108    }
1109
1110    #[test]
1111    fn test_builder_show_edge() {
1112        let table = Table::new().show_edge(false);
1113        assert!(!table.show_edge);
1114    }
1115
1116    #[test]
1117    fn test_builder_collapse_padding() {
1118        let table = Table::new().collapse_padding(true);
1119        assert!(table.collapse_padding);
1120    }
1121
1122    #[test]
1123    fn test_builder_pad_edge() {
1124        let table = Table::new().pad_edge(false);
1125        assert!(!table.pad_edge);
1126    }
1127
1128    #[test]
1129    fn test_get_row_style_empty() {
1130        let table = Table::new();
1131        assert_eq!(table.get_row_style(0), None);
1132    }
1133
1134    #[test]
1135    fn test_get_row_style_with_styles() {
1136        let s1 = Style::new().bold(true);
1137        let s2 = Style::new().dim(true);
1138        let table = Table::new().row_styles(vec![s1, s2]);
1139        assert!(table.get_row_style(0).is_some());
1140        assert!(table.get_row_style(1).is_some());
1141        // Cycles
1142        assert!(table.get_row_style(2).is_some());
1143        assert!(table.get_row_style(3).is_some());
1144    }
1145
1146    #[test]
1147    fn test_add_section_returns_self() {
1148        let mut table = Table::new();
1149        table.add_column(Column::new("A"));
1150        table.add_row_str(vec!["r1".into()]);
1151        let ret = table.add_section();
1152        // Verify the return value is &mut Self (can chain)
1153        ret.add_row_str(vec!["r2".into()]);
1154        assert_eq!(table.row_count(), 2);
1155    }
1156
1157    #[test]
1158    fn test_sections_field() {
1159        let mut table = Table::new();
1160        table.add_column(Column::new("A"));
1161        table.add_row_str(vec!["r1".into()]);
1162        table.add_section();
1163        table.add_row_str(vec!["r2".into()]);
1164        assert_eq!(table.sections.len(), 1);
1165        assert_eq!(table.sections[0], 1);
1166    }
1167
1168    #[test]
1169    fn test_pad_edge_default() {
1170        let table = Table::new();
1171        assert!(table.pad_edge);
1172    }
1173
1174    #[test]
1175    fn test_grid_method() {
1176        let table = Table::grid();
1177        assert!(!table.show_edge);
1178        assert!(!table.show_header);
1179    }
1180}