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// Table
172// ---------------------------------------------------------------------------
173
174/// A renderable for tabular data.
175#[derive(Debug, Clone)]
176pub struct Table {
177    columns: Vec<Column>,
178    rows: Vec<Vec<Cell>>,
179    /// Title above the table.
180    pub title: Option<String>,
181    /// Caption below the table.
182    pub caption: Option<String>,
183    /// Box style.
184    pub box_style: BoxStyle,
185    /// Show the header row.
186    pub show_header: bool,
187    /// Show the footer row.
188    pub show_footer: bool,
189    /// Show outer edge border.
190    pub show_edge: bool,
191    /// Show lines between every row.
192    pub show_lines: bool,
193    /// Padding per cell (top, right, bottom, left).
194    pub padding: (usize, usize, usize, usize),
195    /// Collapse padding between rows.
196    pub collapse_padding: bool,
197    /// Default style for the table.
198    pub style: Style,
199    /// Border style.
200    pub border_style: Style,
201    /// Title style.
202    pub title_style: Style,
203    /// Caption style.
204    pub caption_style: Style,
205    /// Title justification.
206    pub title_justify: AlignMethod,
207    /// Caption justification.
208    pub caption_justify: AlignMethod,
209    /// If true, highlight cell strings.
210    pub highlight: bool,
211    /// Optional fixed width.
212    pub width: Option<usize>,
213    /// Row styles (alternating).
214    pub row_styles: Vec<Style>,
215    /// Number of blank lines between rows.
216    pub leading: usize,
217    /// Active rowspan counts per column (tracked during rendering).
218    pub rowspans: Vec<usize>,
219    /// Row indices that have a section separator before them.
220    pub section_rows: HashSet<usize>,
221}
222
223impl Table {
224    /// Create a new Table.
225    pub fn new() -> Self {
226        Self {
227            columns: Vec::new(),
228            rows: Vec::new(),
229            title: None,
230            caption: None,
231            box_style: BOX_HEAVY_HEAD.clone(),
232            show_header: true,
233            show_footer: false,
234            show_edge: true,
235            show_lines: false,
236            padding: (0, 1, 0, 1),
237            collapse_padding: false,
238            style: Style::new(),
239            border_style: Style::new(),
240            title_style: Style::new().bold(true),
241            caption_style: Style::new().dim(true),
242            title_justify: AlignMethod::Center,
243            caption_justify: AlignMethod::Center,
244            highlight: false,
245            width: None,
246            row_styles: Vec::new(),
247            leading: 0,
248            rowspans: Vec::new(),
249            section_rows: HashSet::new(),
250        }
251    }
252
253    /// Add a column definition to the table.
254    ///
255    /// Columns must be added before rows are populated.
256    ///
257    /// # Examples
258    ///
259    /// ```rust
260    /// use rusty_rich::{Table, Column};
261    ///
262    /// let mut table = Table::new();
263    /// table.add_column(Column::new("Name"));
264    /// table.add_column(Column::new("Age"));
265    /// ```
266    pub fn add_column(&mut self, column: Column) {
267        self.columns.push(column);
268    }
269
270    /// Add a row from [`Cell`] objects (supports colspan/rowspan).
271    ///
272    /// # Examples
273    ///
274    /// ```rust
275    /// use rusty_rich::{Table, Column, Cell};
276    ///
277    /// let mut table = Table::new();
278    /// table.add_column(Column::new("A"));
279    /// table.add_column(Column::new("B"));
280    /// table.add_row(vec![Cell::new("data").colspan(2)]);
281    /// ```
282    pub fn add_row(&mut self, row: Vec<Cell>) {
283        self.rows.push(row);
284    }
285
286    /// Add a row from plain strings (backward-compatible, converts to [`Cell`]s).
287    ///
288    /// # Examples
289    ///
290    /// ```rust
291    /// use rusty_rich::{Table, Column};
292    ///
293    /// let mut table = Table::new();
294    /// table.add_column(Column::new("Name"));
295    /// table.add_column(Column::new("Age"));
296    /// table.add_row_str(vec!["Alice".into(), "30".into()]);
297    /// ```
298    pub fn add_row_str(&mut self, row: Vec<String>) {
299        let cells: Vec<Cell> = row.into_iter().map(Cell::new).collect();
300        self.rows.push(cells);
301    }
302
303    /// Builder: add a column and return self.
304    pub fn column(mut self, col: Column) -> Self { self.add_column(col); self }
305
306    /// Builder: add a row of Cells and return self.
307    pub fn row(mut self, row: Vec<Cell>) -> Self { self.add_row(row); self }
308
309    /// Builder: add a row of strings and return self.
310    pub fn row_str(mut self, row: Vec<String>) -> Self { self.add_row_str(row); self }
311
312    /// Builder: set title.
313    pub fn title(mut self, t: impl Into<String>) -> Self { self.title = Some(t.into()); self }
314
315    /// Builder: set caption.
316    pub fn caption(mut self, t: impl Into<String>) -> Self { self.caption = Some(t.into()); self }
317
318    /// Builder: set box style.
319    pub fn box_style(mut self, bs: BoxStyle) -> Self { self.box_style = bs; self }
320
321    /// Builder: set border style.
322    pub fn border_style(mut self, s: Style) -> Self { self.border_style = s; self }
323
324    /// Builder: hide the header.
325    pub fn hide_header(mut self) -> Self { self.show_header = false; self }
326
327    /// Builder: show lines.
328    pub fn show_lines(mut self) -> Self { self.show_lines = true; self }
329
330    /// Builder: set leading (blank lines between rows).
331    pub fn leading(mut self, l: usize) -> Self { self.leading = l; self }
332
333    /// Create a grid table (no outer border, no header, no footer).
334    /// Equivalent to `Table.grid()`.
335    pub fn grid() -> Self {
336        Self {
337            columns: Vec::new(),
338            rows: Vec::new(),
339            title: None,
340            caption: None,
341            box_style: crate::box_drawing::BOX_SIMPLE.clone(),
342            show_header: false,
343            show_footer: false,
344            show_edge: false,
345            show_lines: false,
346            padding: (0, 1, 0, 1),
347            collapse_padding: false,
348            style: Style::new(),
349            border_style: Style::new(),
350            title_style: Style::new().bold(true),
351            caption_style: Style::new().dim(true),
352            title_justify: AlignMethod::Center,
353            caption_justify: AlignMethod::Center,
354            highlight: false,
355            width: None,
356            row_styles: Vec::new(),
357            leading: 0,
358            rowspans: Vec::new(),
359            section_rows: HashSet::new(),
360        }
361    }
362
363    /// Add a section separator before the next row.
364    /// The next row added will have a horizontal rule above it.
365    pub fn add_section(&mut self) {
366        self.section_rows.insert(self.rows.len());
367    }
368
369    /// Get the row count.
370    pub fn row_count(&self) -> usize { self.rows.len() }
371}
372
373impl Renderable for Table {
374    fn render(&self, options: &ConsoleOptions) -> RenderResult {
375        if self.columns.is_empty() {
376            return RenderResult::new();
377        }
378
379        let box_style = get_safe_box(&self.box_style, options.ascii_only);
380        let available_width = self.width.unwrap_or(options.max_width);
381        let col_count = self.columns.len();
382
383        // Calculate column widths
384        let col_widths = self.calculate_column_widths(available_width);
385
386        let mut lines: Vec<Vec<Segment>> = Vec::new();
387        let b = &box_style;
388
389        // Helper: make a border segment
390        let bs = |ch: char| -> Segment {
391            let ansi = self.border_style.to_ansi();
392            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
393            Segment::new(format!("{ansi}{ch}{reset}"))
394        };
395
396        // -- Title --
397        if let Some(ref title) = self.title {
398            let _tw = UnicodeWidthStr::width(title.as_str());
399            let centered = self.title_justify.align_text(title, available_width.saturating_sub(2));
400            lines.push(vec![bs(b.top_left), Segment::new(&centered[1..centered.len()-1]), bs(b.top_right), Segment::line()]);
401        }
402
403        // -- Top border --
404        if self.show_edge {
405            let mut top_line = vec![bs(b.top_left)];
406            for (i, w) in col_widths.iter().enumerate() {
407                top_line.push(Segment::new(b.top.to_string().repeat(*w)));
408                if i < col_count - 1 {
409                    top_line.push(bs(b.top_divider));
410                }
411            }
412            top_line.push(bs(b.top_right));
413            top_line.push(Segment::line());
414            lines.push(top_line);
415        }
416
417        // -- Header --
418        if self.show_header && self.columns.iter().any(|c| !c.header.is_empty()) {
419            // Top padding
420            let (pt, _pr, _pb, _pl) = self.padding;
421            for _ in 0..pt {
422                lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
423            }
424
425            let header_cells: Vec<String> = self.columns.iter()
426                .map(|c| c.header.clone())
427                .collect();
428            lines.push(self.render_cell_line(&col_widths, &header_cells, &b, true));
429
430            // Header separator
431            let mut sep = vec![bs(b.head_row_left)];
432            for (i, w) in col_widths.iter().enumerate() {
433                sep.push(Segment::new(b.head_row_horizontal.to_string().repeat(*w)));
434                if i < col_count - 1 {
435                    sep.push(bs(b.head_row_cross));
436                }
437            }
438            sep.push(bs(b.head_row_right));
439            sep.push(Segment::line());
440            lines.push(sep);
441        }
442
443        // -- Rows --
444        let mut rowspan_remaining: Vec<usize> = vec![0; col_count];
445        for (row_idx, row) in self.rows.iter().enumerate() {
446            // Section separator
447            if self.section_rows.contains(&row_idx) {
448                let mut sep = vec![bs(b.head_row_left)];
449                for (i, w) in col_widths.iter().enumerate() {
450                    sep.push(Segment::new(b.head_row_horizontal.to_string().repeat(*w)));
451                    if i < col_count - 1 {
452                        sep.push(bs(b.head_row_cross));
453                    }
454                }
455                sep.push(bs(b.head_row_right));
456                sep.push(Segment::line());
457                lines.push(sep);
458            }
459
460            // Leading blank lines between rows
461            if row_idx > 0 {
462                for _ in 0..self.leading {
463                    lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
464                }
465            }
466
467            let (pt, _pr, _pb, _pl) = self.padding;
468            for _ in 0..pt {
469                lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
470            }
471
472            let _style = if row_idx < self.row_styles.len() {
473                Some(&self.row_styles[row_idx])
474            } else if self.row_styles.len() == 2 {
475                Some(&self.row_styles[row_idx % 2])
476            } else {
477                None
478            };
479
480            lines.push(self.render_cell_line_with_rowspan(
481                &col_widths, row, &b, false, &mut rowspan_remaining,
482            ));
483
484            // Row separator
485            if self.show_lines && row_idx < self.rows.len() - 1 {
486                let mut sep = vec![bs(b.row_left)];
487                for (i, w) in col_widths.iter().enumerate() {
488                    sep.push(Segment::new(b.row_horizontal.to_string().repeat(*w)));
489                    if i < col_count - 1 {
490                        sep.push(bs(b.row_cross));
491                    }
492                }
493                sep.push(bs(b.row_right));
494                sep.push(Segment::line());
495                lines.push(sep);
496            }
497        }
498
499        // -- Footer --
500        if self.show_footer && self.columns.iter().any(|c| !c.footer.is_empty()) {
501            let mut sep = vec![bs(b.foot_row_left)];
502            for (i, w) in col_widths.iter().enumerate() {
503                sep.push(Segment::new(b.foot_row_horizontal.to_string().repeat(*w)));
504                if i < col_count - 1 {
505                    sep.push(bs(b.foot_row_cross));
506                }
507            }
508            sep.push(bs(b.foot_row_right));
509            sep.push(Segment::line());
510            lines.push(sep);
511
512            let footer_cells: Vec<String> = self.columns.iter()
513                .map(|c| c.footer.clone())
514                .collect();
515            lines.push(self.render_cell_line(&col_widths, &footer_cells, &b, false));
516        }
517
518        // -- Bottom border --
519        if self.show_edge {
520            let mut bot_line = vec![bs(b.bottom_left)];
521            for (i, w) in col_widths.iter().enumerate() {
522                bot_line.push(Segment::new(b.bottom.to_string().repeat(*w)));
523                if i < col_count - 1 {
524                    bot_line.push(bs(b.bottom_divider));
525                }
526            }
527            bot_line.push(bs(b.bottom_right));
528            bot_line.push(Segment::line());
529            lines.push(bot_line);
530        }
531
532        // -- Caption --
533        if let Some(ref caption) = self.caption {
534            let centered = self.caption_justify.align_text(caption, available_width.saturating_sub(2));
535            lines.push(vec![Segment::new(&centered), Segment::line()]);
536        }
537
538        RenderResult { lines, items: Vec::new() }
539    }
540}
541
542impl Table {
543    fn calculate_column_widths(&self, available: usize) -> Vec<usize> {
544        let col_count = self.columns.len();
545        let total_pad = col_count.saturating_sub(1) + 2; // separators + edges
546        let content_width = available.saturating_sub(total_pad);
547
548        // If any column has a fixed width, respect it
549        let mut widths: Vec<usize> = vec![0; col_count];
550        let mut flex_cols: Vec<usize> = Vec::new();
551        let mut used = 0usize;
552
553        for (i, col) in self.columns.iter().enumerate() {
554            if let Some(w) = col.width {
555                widths[i] = w;
556                used += w;
557            } else {
558                flex_cols.push(i);
559            }
560        }
561
562        if flex_cols.is_empty() {
563            return widths;
564        }
565
566        let remaining = content_width.saturating_sub(used);
567        let _flex_count = flex_cols.len();
568
569        // Distribute remaining width using ratios if available
570        let total_ratio: usize = flex_cols.iter()
571            .map(|&i| self.columns[i].ratio.unwrap_or(1))
572            .sum();
573
574        for &i in &flex_cols {
575            let col = &self.columns[i];
576            let ratio = col.ratio.unwrap_or(1);
577            let mut w = (remaining * ratio) / total_ratio;
578            if let Some(min_w) = col.min_width {
579                w = w.max(min_w);
580            }
581            if let Some(max_w) = col.max_width {
582                w = w.min(max_w);
583            }
584            w = w.max(3); // minimum usable width
585            widths[i] = w;
586        }
587
588        // Adjust for rounding
589        let total: usize = widths.iter().sum();
590        if total < content_width && !flex_cols.is_empty() {
591            let extra = content_width - total;
592            widths[flex_cols[flex_cols.len() - 1]] += extra;
593        }
594
595        widths
596    }
597
598    fn render_cell_line(
599        &self,
600        widths: &[usize],
601        values: &[String],
602        b: &BoxStyle,
603        is_header: bool,
604    ) -> Vec<Segment> {
605        let mut line = Vec::new();
606        let bs = |ch: char| -> Segment {
607            let ansi = self.border_style.to_ansi();
608            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
609            Segment::new(format!("{ansi}{ch}{reset}"))
610        };
611
612        line.push(bs(b.mid_vertical));
613
614        for (i, w) in widths.iter().enumerate() {
615            let val = values.get(i).map(|s| s.as_str()).unwrap_or("");
616            let col = self.columns.get(i);
617            let justify = col.map(|c| c.justify).unwrap_or(AlignMethod::Left);
618            let (_pt, pr, _pb, pl) = self.padding;
619
620            // Pad left
621            line.push(Segment::new(" ".repeat(pl)));
622
623            // Align the text
624            let disp = justify.align_text(val, w.saturating_sub(pl + pr));
625            // Truncate if needed
626            let disp_trunc = if UnicodeWidthStr::width(disp.as_str()) > *w {
627                let mut truncated = disp.chars().take(
628                    w.saturating_sub(1) // leave room for ellipsis
629                ).collect::<String>();
630                truncated.push('…');
631                truncated
632            } else {
633                disp
634            };
635
636            // Apply header style if needed
637            if is_header {
638                let header_style = col.map(|c| &c.header_style);
639                if let Some(hs) = header_style {
640                    let ansi = hs.to_ansi();
641                    let reset = hs.reset_ansi();
642                    line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
643                } else {
644                    line.push(Segment::new(disp_trunc));
645                }
646            } else {
647                line.push(Segment::new(disp_trunc));
648            }
649
650            // Pad right
651            line.push(Segment::new(" ".repeat(pr)));
652
653            if i < widths.len() - 1 {
654                line.push(bs(b.mid_vertical));
655            }
656        }
657
658        line.push(bs(b.mid_right));
659        line.push(Segment::line());
660        line
661    }
662
663    /// Render a row of Cells with colspan/rowspan support.
664    /// `rowspan_remaining` is updated to track active rowspans.
665    fn render_cell_line_with_rowspan(
666        &self,
667        widths: &[usize],
668        cells: &[Cell],
669        b: &BoxStyle,
670        is_header: bool,
671        rowspan_remaining: &mut [usize],
672    ) -> Vec<Segment> {
673        let mut line = Vec::new();
674        let bs = |ch: char| -> Segment {
675            let ansi = self.border_style.to_ansi();
676            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
677            Segment::new(format!("{ansi}{ch}{reset}"))
678        };
679
680        line.push(bs(b.mid_vertical));
681
682        let col_count = widths.len();
683        let mut cell_idx = 0;
684        let mut col: usize = 0;
685
686        while col < col_count {
687            // Check for active rowspan in this column
688            if rowspan_remaining[col] > 0 {
689                rowspan_remaining[col] -= 1;
690                // Render an empty spanned cell for this column
691                let w = widths[col];
692                let (_pt, pr, _pb, pl) = self.padding;
693                line.push(Segment::new(" ".repeat(pl + w + pr)));
694                if col < col_count - 1 {
695                    line.push(bs(b.mid_vertical));
696                }
697                col += 1;
698                continue;
699            }
700
701            // No more cells — fill remaining columns as empty
702            if cell_idx >= cells.len() {
703                let w = widths[col];
704                let (_pt, pr, _pb, pl) = self.padding;
705                line.push(Segment::new(" ".repeat(pl + w + pr)));
706                if col < col_count - 1 {
707                    line.push(bs(b.mid_vertical));
708                }
709                col += 1;
710                continue;
711            }
712
713            let cell = &cells[cell_idx];
714            cell_idx += 1;
715
716            let span_end = (col + cell.colspan).min(col_count);
717            let span_width: usize = widths[col..span_end].iter().sum();
718            let (_pt, pr, _pb, pl) = self.padding;
719            let content_width = span_width.saturating_sub(pl + pr);
720
721            let col_def = self.columns.get(col);
722            let justify = col_def.map(|c| c.justify).unwrap_or(AlignMethod::Left);
723
724            // Align and truncate content
725            let disp_text = justify.align_text(&cell.content, content_width);
726            let disp_trunc = if UnicodeWidthStr::width(disp_text.as_str()) > content_width {
727                let mut truncated: String = disp_text.chars()
728                    .take(content_width.saturating_sub(1))
729                    .collect();
730                truncated.push('…');
731                truncated
732            } else {
733                disp_text
734            };
735
736            // Pad left
737            line.push(Segment::new(" ".repeat(pl)));
738
739            // Apply cell style, header style, or column style
740            if let Some(ref cell_style) = cell.style {
741                let ansi = cell_style.to_ansi();
742                let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
743                line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
744            } else if is_header {
745                if let Some(hs) = col_def.map(|c| &c.header_style) {
746                    let ansi = hs.to_ansi();
747                    let reset = hs.reset_ansi();
748                    line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
749                } else {
750                    line.push(Segment::new(disp_trunc));
751                }
752            } else {
753                // Apply column default style if it has ANSI
754                let col_ansi = col_def.map(|c| c.style.to_ansi()).unwrap_or_default();
755                if col_ansi.is_empty() {
756                    line.push(Segment::new(disp_trunc));
757                } else {
758                    line.push(Segment::new(format!("{col_ansi}{disp_trunc}\x1b[0m")));
759                }
760            }
761
762            // Pad right
763            line.push(Segment::new(" ".repeat(pr)));
764
765            // Set rowspan for future rows
766            if cell.rowspan > 1 {
767                for rc in col..span_end {
768                    rowspan_remaining[rc] = cell.rowspan - 1;
769                }
770            }
771
772            col = span_end;
773
774            // Vertical separator after the span
775            if col < col_count {
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    fn render_row_line(
786        &self,
787        widths: &[usize],
788        _values: &[String],
789        b: &BoxStyle,
790        _available_width: usize,
791        _is_header: bool,
792    ) -> Vec<Segment> {
793        let mut line = Vec::new();
794        let bs = |ch: char| -> Segment {
795            let ansi = self.border_style.to_ansi();
796            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
797            Segment::new(format!("{ansi}{ch}{reset}"))
798        };
799
800        line.push(bs(b.mid_vertical));
801        for (i, w) in widths.iter().enumerate() {
802            line.push(Segment::new(" ".repeat(*w)));
803            if i < widths.len() - 1 {
804                line.push(bs(b.mid_vertical));
805            }
806        }
807        line.push(bs(b.mid_right));
808        line.push(Segment::line());
809        line
810    }
811}
812
813impl Default for Table {
814    fn default() -> Self {
815        Self::new()
816    }
817}
818
819#[cfg(test)]
820mod tests {
821    use super::*;
822
823    #[test]
824    fn test_empty_table() {
825        let table = Table::new();
826        let opts = ConsoleOptions::default();
827        let result = table.render(&opts);
828        assert!(result.lines.is_empty());
829    }
830
831    #[test]
832    fn test_table_with_one_column() {
833        let mut table = Table::new();
834        table.add_column(Column::new("Name"));
835        table.add_row_str(vec!["Alice".into()]);
836        table.add_row_str(vec!["Bob".into()]);
837
838        let opts = ConsoleOptions::default();
839        let result = table.render(&opts);
840        let ansi = result.to_ansi();
841        assert!(ansi.contains("Name"));
842        assert!(ansi.contains("Alice"));
843    }
844
845    #[test]
846    fn test_cell_creation() {
847        let cell = Cell::new("hello");
848        assert_eq!(cell.content, "hello");
849        assert_eq!(cell.colspan, 1);
850        assert_eq!(cell.rowspan, 1);
851        assert!(cell.style.is_none());
852
853        let cell2 = Cell::new("world").colspan(2).rowspan(3);
854        assert_eq!(cell2.content, "world");
855        assert_eq!(cell2.colspan, 2);
856        assert_eq!(cell2.rowspan, 3);
857    }
858
859    #[test]
860    fn test_cell_from_string() {
861        let cell: Cell = "test".into();
862        assert_eq!(cell.content, "test");
863    }
864
865    #[test]
866    fn test_column_colspan() {
867        let col = Column::new("Header");
868        assert_eq!(col.colspan, 1);
869    }
870
871    #[test]
872    fn test_add_row_str() {
873        let mut table = Table::new();
874        table.add_column(Column::new("A"));
875        table.add_column(Column::new("B"));
876        table.add_row_str(vec!["x".into(), "y".into()]);
877        assert_eq!(table.row_count(), 1);
878    }
879
880    #[test]
881    fn test_add_section() {
882        let mut table = Table::new();
883        table.add_column(Column::new("A"));
884        table.add_row_str(vec!["r1".into()]);
885        table.add_section();
886        table.add_row_str(vec!["r2".into()]);
887        assert_eq!(table.row_count(), 2);
888        assert!(table.section_rows.contains(&1));
889
890        let opts = ConsoleOptions::default();
891        let result = table.render(&opts);
892        let ansi = result.to_ansi();
893        assert!(ansi.contains("r1"));
894        assert!(ansi.contains("r2"));
895    }
896
897    #[test]
898    fn test_leading() {
899        let table = Table::new()
900            .column(Column::new("X"))
901            .row_str(vec!["a".into()])
902            .row_str(vec!["b".into()])
903            .leading(1);
904        assert_eq!(table.leading, 1);
905    }
906
907    #[test]
908    fn test_cell_rowspan() {
909        let mut table = Table::new();
910        table.add_column(Column::new("A"));
911        table.add_column(Column::new("B"));
912        let cell_a = Cell::new("span").rowspan(2);
913        let cell_b = Cell::new("single");
914        table.add_row(vec![cell_a, cell_b]);
915        table.add_row_str(vec!["row2col2".into()]);
916
917        let opts = ConsoleOptions::default();
918        let result = table.render(&opts);
919        let ansi = result.to_ansi();
920        assert!(ansi.contains("span"));
921    }
922
923    #[test]
924    fn test_cell_colspan() {
925        let mut table = Table::new();
926        table.add_column(Column::new("A"));
927        table.add_column(Column::new("B"));
928        table.add_column(Column::new("C"));
929        let cell = Cell::new("wide").colspan(2);
930        table.add_row(vec![cell, Cell::new("c")]);
931        table.add_row_str(vec!["a".into(), "b".into(), "c".into()]);
932
933        let opts = ConsoleOptions::default();
934        let result = table.render(&opts);
935        let ansi = result.to_ansi();
936        assert!(ansi.contains("wide"));
937    }
938}