Skip to main content

rusty_rich/
table.rs

1//! Table — tabular data with columns. Equivalent to Rich's `table.py`.
2
3
4use crate::align::{AlignMethod, VerticalAlignMethod};
5use crate::box_drawing::{get_safe_box, BoxStyle, BOX_HEAVY_HEAD};
6use crate::console::{ConsoleOptions, OverflowMethod, RenderResult, Renderable};
7use crate::segment::Segment;
8use crate::style::Style;
9use std::collections::HashSet;
10use unicode_width::UnicodeWidthStr;
11
12// ---------------------------------------------------------------------------
13// Cell
14// ---------------------------------------------------------------------------
15
16/// A single cell in a table row, with optional styling and spanning.
17#[derive(Debug, Clone)]
18pub struct Cell {
19    /// The text content of the cell.
20    pub content: String,
21    /// Optional per-cell style.
22    pub style: Option<Style>,
23    /// Number of columns this cell spans (default 1).
24    pub colspan: usize,
25    /// Number of rows this cell spans (default 1).
26    pub rowspan: usize,
27}
28
29impl Cell {
30    /// Create a new Cell with the given content.
31    pub fn new(content: impl Into<String>) -> Self {
32        Cell {
33            content: content.into(),
34            style: None,
35            colspan: 1,
36            rowspan: 1,
37        }
38    }
39
40    /// Builder: set style.
41    pub fn style(mut self, s: Style) -> Self { self.style = Some(s); self }
42    /// Builder: set colspan.
43    pub fn colspan(mut self, c: usize) -> Self { self.colspan = c; self }
44    /// Builder: set rowspan.
45    pub fn rowspan(mut self, r: usize) -> Self { self.rowspan = r; self }
46}
47
48impl From<String> for Cell {
49    fn from(s: String) -> Self { Cell::new(s) }
50}
51
52impl From<&str> for Cell {
53    fn from(s: &str) -> Self { Cell::new(s) }
54}
55
56// ---------------------------------------------------------------------------
57// Column
58// ---------------------------------------------------------------------------
59
60/// Defines a column within a Table.
61#[derive(Debug, Clone)]
62pub struct Column {
63    /// The header text / renderable.
64    pub header: String,
65    /// The footer text / renderable.
66    pub footer: String,
67    /// Header style.
68    pub header_style: Style,
69    /// Footer style.
70    pub footer_style: Style,
71    /// Default style for cells in this column.
72    pub style: Style,
73    /// Horizontal justification.
74    pub justify: AlignMethod,
75    /// Vertical alignment.
76    pub vertical: VerticalAlignMethod,
77    /// Overflow method.
78    pub overflow: OverflowMethod,
79    /// Fixed width, if set.
80    pub width: Option<usize>,
81    /// Minimum width.
82    pub min_width: Option<usize>,
83    /// Maximum width.
84    pub max_width: Option<usize>,
85    /// Ratio weight for flexible distribution.
86    pub ratio: Option<usize>,
87    /// Number of columns this header spans (default 1).
88    pub colspan: usize,
89}
90
91impl Column {
92    /// Create a new column with the given header.
93    pub fn new(header: impl Into<String>) -> Self {
94        Self {
95            header: header.into(),
96            footer: String::new(),
97            header_style: Style::new().bold(true),
98            footer_style: Style::new(),
99            style: Style::new(),
100            justify: AlignMethod::Left,
101            vertical: VerticalAlignMethod::Top,
102            overflow: OverflowMethod::Ellipsis,
103            width: None,
104            min_width: None,
105            max_width: None,
106            ratio: None,
107            colspan: 1,
108        }
109    }
110
111    /// Builder: set justify.
112    pub fn justify(mut self, j: AlignMethod) -> Self { self.justify = j; self }
113    /// Builder: set width.
114    pub fn width(mut self, w: usize) -> Self { self.width = Some(w); self }
115    /// Builder: set min width.
116    pub fn min_width(mut self, w: usize) -> Self { self.min_width = Some(w); self }
117    /// Builder: set max width.
118    pub fn max_width(mut self, w: usize) -> Self { self.max_width = Some(w); self }
119    /// Builder: set style.
120    pub fn style(mut self, s: Style) -> Self { self.style = s; self }
121    /// Builder: set header style.
122    pub fn header_style(mut self, s: Style) -> Self { self.header_style = s; self }
123    /// Builder: set ratio.
124    pub fn ratio(mut self, r: usize) -> Self { self.ratio = Some(r); self }
125    /// Builder: set overflow.
126    pub fn overflow(mut self, o: OverflowMethod) -> Self { self.overflow = o; self }
127}
128
129// ---------------------------------------------------------------------------
130// Table
131// ---------------------------------------------------------------------------
132
133/// A renderable for tabular data.
134#[derive(Debug, Clone)]
135pub struct Table {
136    columns: Vec<Column>,
137    rows: Vec<Vec<Cell>>,
138    /// Title above the table.
139    pub title: Option<String>,
140    /// Caption below the table.
141    pub caption: Option<String>,
142    /// Box style.
143    pub box_style: BoxStyle,
144    /// Show the header row.
145    pub show_header: bool,
146    /// Show the footer row.
147    pub show_footer: bool,
148    /// Show outer edge border.
149    pub show_edge: bool,
150    /// Show lines between every row.
151    pub show_lines: bool,
152    /// Padding per cell (top, right, bottom, left).
153    pub padding: (usize, usize, usize, usize),
154    /// Collapse padding between rows.
155    pub collapse_padding: bool,
156    /// Default style for the table.
157    pub style: Style,
158    /// Border style.
159    pub border_style: Style,
160    /// Title style.
161    pub title_style: Style,
162    /// Caption style.
163    pub caption_style: Style,
164    /// Title justification.
165    pub title_justify: AlignMethod,
166    /// Caption justification.
167    pub caption_justify: AlignMethod,
168    /// If true, highlight cell strings.
169    pub highlight: bool,
170    /// Optional fixed width.
171    pub width: Option<usize>,
172    /// Row styles (alternating).
173    pub row_styles: Vec<Style>,
174    /// Number of blank lines between rows.
175    pub leading: usize,
176    /// Active rowspan counts per column (tracked during rendering).
177    pub rowspans: Vec<usize>,
178    /// Row indices that have a section separator before them.
179    pub section_rows: HashSet<usize>,
180}
181
182impl Table {
183    /// Create a new Table.
184    pub fn new() -> Self {
185        Self {
186            columns: Vec::new(),
187            rows: Vec::new(),
188            title: None,
189            caption: None,
190            box_style: BOX_HEAVY_HEAD.clone(),
191            show_header: true,
192            show_footer: false,
193            show_edge: true,
194            show_lines: false,
195            padding: (0, 1, 0, 1),
196            collapse_padding: false,
197            style: Style::new(),
198            border_style: Style::new(),
199            title_style: Style::new().bold(true),
200            caption_style: Style::new().dim(true),
201            title_justify: AlignMethod::Center,
202            caption_justify: AlignMethod::Center,
203            highlight: false,
204            width: None,
205            row_styles: Vec::new(),
206            leading: 0,
207            rowspans: Vec::new(),
208            section_rows: HashSet::new(),
209        }
210    }
211
212    /// Add a column.
213    pub fn add_column(&mut self, column: Column) {
214        self.columns.push(column);
215    }
216
217    /// Add a row from Cell objects (supports colspan/rowspan).
218    pub fn add_row(&mut self, row: Vec<Cell>) {
219        self.rows.push(row);
220    }
221
222    /// Add a row from strings (backward-compatible, converts to Cells).
223    pub fn add_row_str(&mut self, row: Vec<String>) {
224        let cells: Vec<Cell> = row.into_iter().map(Cell::new).collect();
225        self.rows.push(cells);
226    }
227
228    /// Builder: add a column and return self.
229    pub fn column(mut self, col: Column) -> Self { self.add_column(col); self }
230
231    /// Builder: add a row of Cells and return self.
232    pub fn row(mut self, row: Vec<Cell>) -> Self { self.add_row(row); self }
233
234    /// Builder: add a row of strings and return self.
235    pub fn row_str(mut self, row: Vec<String>) -> Self { self.add_row_str(row); self }
236
237    /// Builder: set title.
238    pub fn title(mut self, t: impl Into<String>) -> Self { self.title = Some(t.into()); self }
239
240    /// Builder: set caption.
241    pub fn caption(mut self, t: impl Into<String>) -> Self { self.caption = Some(t.into()); self }
242
243    /// Builder: set box style.
244    pub fn box_style(mut self, bs: BoxStyle) -> Self { self.box_style = bs; self }
245
246    /// Builder: set border style.
247    pub fn border_style(mut self, s: Style) -> Self { self.border_style = s; self }
248
249    /// Builder: hide the header.
250    pub fn hide_header(mut self) -> Self { self.show_header = false; self }
251
252    /// Builder: show lines.
253    pub fn show_lines(mut self) -> Self { self.show_lines = true; self }
254
255    /// Builder: set leading (blank lines between rows).
256    pub fn leading(mut self, l: usize) -> Self { self.leading = l; self }
257
258    /// Create a grid table (no outer border, no header, no footer).
259    /// Equivalent to `Table.grid()`.
260    pub fn grid() -> Self {
261        Self {
262            columns: Vec::new(),
263            rows: Vec::new(),
264            title: None,
265            caption: None,
266            box_style: crate::box_drawing::BOX_SIMPLE.clone(),
267            show_header: false,
268            show_footer: false,
269            show_edge: false,
270            show_lines: false,
271            padding: (0, 1, 0, 1),
272            collapse_padding: false,
273            style: Style::new(),
274            border_style: Style::new(),
275            title_style: Style::new().bold(true),
276            caption_style: Style::new().dim(true),
277            title_justify: AlignMethod::Center,
278            caption_justify: AlignMethod::Center,
279            highlight: false,
280            width: None,
281            row_styles: Vec::new(),
282            leading: 0,
283            rowspans: Vec::new(),
284            section_rows: HashSet::new(),
285        }
286    }
287
288    /// Add a section separator before the next row.
289    /// The next row added will have a horizontal rule above it.
290    pub fn add_section(&mut self) {
291        self.section_rows.insert(self.rows.len());
292    }
293
294    /// Get the row count.
295    pub fn row_count(&self) -> usize { self.rows.len() }
296}
297
298impl Renderable for Table {
299    fn render(&self, options: &ConsoleOptions) -> RenderResult {
300        if self.columns.is_empty() {
301            return RenderResult::new();
302        }
303
304        let box_style = get_safe_box(&self.box_style, options.ascii_only);
305        let available_width = self.width.unwrap_or(options.max_width);
306        let col_count = self.columns.len();
307
308        // Calculate column widths
309        let col_widths = self.calculate_column_widths(available_width);
310
311        let mut lines: Vec<Vec<Segment>> = Vec::new();
312        let b = &box_style;
313
314        // Helper: make a border segment
315        let bs = |ch: char| -> Segment {
316            let ansi = self.border_style.to_ansi();
317            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
318            Segment::new(format!("{ansi}{ch}{reset}"))
319        };
320
321        // -- Title --
322        if let Some(ref title) = self.title {
323            let _tw = UnicodeWidthStr::width(title.as_str());
324            let centered = self.title_justify.align_text(title, available_width.saturating_sub(2));
325            lines.push(vec![bs(b.top_left), Segment::new(&centered[1..centered.len()-1]), bs(b.top_right), Segment::line()]);
326        }
327
328        // -- Top border --
329        if self.show_edge {
330            let mut top_line = vec![bs(b.top_left)];
331            for (i, w) in col_widths.iter().enumerate() {
332                top_line.push(Segment::new(b.top.to_string().repeat(*w)));
333                if i < col_count - 1 {
334                    top_line.push(bs(b.top_divider));
335                }
336            }
337            top_line.push(bs(b.top_right));
338            top_line.push(Segment::line());
339            lines.push(top_line);
340        }
341
342        // -- Header --
343        if self.show_header && self.columns.iter().any(|c| !c.header.is_empty()) {
344            // Top padding
345            let (pt, _pr, _pb, _pl) = self.padding;
346            for _ in 0..pt {
347                lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
348            }
349
350            let header_cells: Vec<String> = self.columns.iter()
351                .map(|c| c.header.clone())
352                .collect();
353            lines.push(self.render_cell_line(&col_widths, &header_cells, &b, true));
354
355            // Header separator
356            let mut sep = vec![bs(b.head_row_left)];
357            for (i, w) in col_widths.iter().enumerate() {
358                sep.push(Segment::new(b.head_row_horizontal.to_string().repeat(*w)));
359                if i < col_count - 1 {
360                    sep.push(bs(b.head_row_cross));
361                }
362            }
363            sep.push(bs(b.head_row_right));
364            sep.push(Segment::line());
365            lines.push(sep);
366        }
367
368        // -- Rows --
369        let mut rowspan_remaining: Vec<usize> = vec![0; col_count];
370        for (row_idx, row) in self.rows.iter().enumerate() {
371            // Section separator
372            if self.section_rows.contains(&row_idx) {
373                let mut sep = vec![bs(b.head_row_left)];
374                for (i, w) in col_widths.iter().enumerate() {
375                    sep.push(Segment::new(b.head_row_horizontal.to_string().repeat(*w)));
376                    if i < col_count - 1 {
377                        sep.push(bs(b.head_row_cross));
378                    }
379                }
380                sep.push(bs(b.head_row_right));
381                sep.push(Segment::line());
382                lines.push(sep);
383            }
384
385            // Leading blank lines between rows
386            if row_idx > 0 {
387                for _ in 0..self.leading {
388                    lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
389                }
390            }
391
392            let (pt, _pr, _pb, _pl) = self.padding;
393            for _ in 0..pt {
394                lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
395            }
396
397            let _style = if row_idx < self.row_styles.len() {
398                Some(&self.row_styles[row_idx])
399            } else if self.row_styles.len() == 2 {
400                Some(&self.row_styles[row_idx % 2])
401            } else {
402                None
403            };
404
405            lines.push(self.render_cell_line_with_rowspan(
406                &col_widths, row, &b, false, &mut rowspan_remaining,
407            ));
408
409            // Row separator
410            if self.show_lines && row_idx < self.rows.len() - 1 {
411                let mut sep = vec![bs(b.row_left)];
412                for (i, w) in col_widths.iter().enumerate() {
413                    sep.push(Segment::new(b.row_horizontal.to_string().repeat(*w)));
414                    if i < col_count - 1 {
415                        sep.push(bs(b.row_cross));
416                    }
417                }
418                sep.push(bs(b.row_right));
419                sep.push(Segment::line());
420                lines.push(sep);
421            }
422        }
423
424        // -- Footer --
425        if self.show_footer && self.columns.iter().any(|c| !c.footer.is_empty()) {
426            let mut sep = vec![bs(b.foot_row_left)];
427            for (i, w) in col_widths.iter().enumerate() {
428                sep.push(Segment::new(b.foot_row_horizontal.to_string().repeat(*w)));
429                if i < col_count - 1 {
430                    sep.push(bs(b.foot_row_cross));
431                }
432            }
433            sep.push(bs(b.foot_row_right));
434            sep.push(Segment::line());
435            lines.push(sep);
436
437            let footer_cells: Vec<String> = self.columns.iter()
438                .map(|c| c.footer.clone())
439                .collect();
440            lines.push(self.render_cell_line(&col_widths, &footer_cells, &b, false));
441        }
442
443        // -- Bottom border --
444        if self.show_edge {
445            let mut bot_line = vec![bs(b.bottom_left)];
446            for (i, w) in col_widths.iter().enumerate() {
447                bot_line.push(Segment::new(b.bottom.to_string().repeat(*w)));
448                if i < col_count - 1 {
449                    bot_line.push(bs(b.bottom_divider));
450                }
451            }
452            bot_line.push(bs(b.bottom_right));
453            bot_line.push(Segment::line());
454            lines.push(bot_line);
455        }
456
457        // -- Caption --
458        if let Some(ref caption) = self.caption {
459            let centered = self.caption_justify.align_text(caption, available_width.saturating_sub(2));
460            lines.push(vec![Segment::new(&centered), Segment::line()]);
461        }
462
463        RenderResult { lines, items: Vec::new() }
464    }
465}
466
467impl Table {
468    fn calculate_column_widths(&self, available: usize) -> Vec<usize> {
469        let col_count = self.columns.len();
470        let total_pad = col_count.saturating_sub(1) + 2; // separators + edges
471        let content_width = available.saturating_sub(total_pad);
472
473        // If any column has a fixed width, respect it
474        let mut widths: Vec<usize> = vec![0; col_count];
475        let mut flex_cols: Vec<usize> = Vec::new();
476        let mut used = 0usize;
477
478        for (i, col) in self.columns.iter().enumerate() {
479            if let Some(w) = col.width {
480                widths[i] = w;
481                used += w;
482            } else {
483                flex_cols.push(i);
484            }
485        }
486
487        if flex_cols.is_empty() {
488            return widths;
489        }
490
491        let remaining = content_width.saturating_sub(used);
492        let _flex_count = flex_cols.len();
493
494        // Distribute remaining width using ratios if available
495        let total_ratio: usize = flex_cols.iter()
496            .map(|&i| self.columns[i].ratio.unwrap_or(1))
497            .sum();
498
499        for &i in &flex_cols {
500            let col = &self.columns[i];
501            let ratio = col.ratio.unwrap_or(1);
502            let mut w = (remaining * ratio) / total_ratio;
503            if let Some(min_w) = col.min_width {
504                w = w.max(min_w);
505            }
506            if let Some(max_w) = col.max_width {
507                w = w.min(max_w);
508            }
509            w = w.max(3); // minimum usable width
510            widths[i] = w;
511        }
512
513        // Adjust for rounding
514        let total: usize = widths.iter().sum();
515        if total < content_width && !flex_cols.is_empty() {
516            let extra = content_width - total;
517            widths[flex_cols[flex_cols.len() - 1]] += extra;
518        }
519
520        widths
521    }
522
523    fn render_cell_line(
524        &self,
525        widths: &[usize],
526        values: &[String],
527        b: &BoxStyle,
528        is_header: bool,
529    ) -> Vec<Segment> {
530        let mut line = Vec::new();
531        let bs = |ch: char| -> Segment {
532            let ansi = self.border_style.to_ansi();
533            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
534            Segment::new(format!("{ansi}{ch}{reset}"))
535        };
536
537        line.push(bs(b.mid_vertical));
538
539        for (i, w) in widths.iter().enumerate() {
540            let val = values.get(i).map(|s| s.as_str()).unwrap_or("");
541            let col = self.columns.get(i);
542            let justify = col.map(|c| c.justify).unwrap_or(AlignMethod::Left);
543            let (_pt, pr, _pb, pl) = self.padding;
544
545            // Pad left
546            line.push(Segment::new(" ".repeat(pl)));
547
548            // Align the text
549            let disp = justify.align_text(val, w.saturating_sub(pl + pr));
550            // Truncate if needed
551            let disp_trunc = if UnicodeWidthStr::width(disp.as_str()) > *w {
552                let mut truncated = disp.chars().take(
553                    w.saturating_sub(1) // leave room for ellipsis
554                ).collect::<String>();
555                truncated.push('…');
556                truncated
557            } else {
558                disp
559            };
560
561            // Apply header style if needed
562            if is_header {
563                let header_style = col.map(|c| &c.header_style);
564                if let Some(hs) = header_style {
565                    let ansi = hs.to_ansi();
566                    let reset = hs.reset_ansi();
567                    line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
568                } else {
569                    line.push(Segment::new(disp_trunc));
570                }
571            } else {
572                line.push(Segment::new(disp_trunc));
573            }
574
575            // Pad right
576            line.push(Segment::new(" ".repeat(pr)));
577
578            if i < widths.len() - 1 {
579                line.push(bs(b.mid_vertical));
580            }
581        }
582
583        line.push(bs(b.mid_right));
584        line.push(Segment::line());
585        line
586    }
587
588    /// Render a row of Cells with colspan/rowspan support.
589    /// `rowspan_remaining` is updated to track active rowspans.
590    fn render_cell_line_with_rowspan(
591        &self,
592        widths: &[usize],
593        cells: &[Cell],
594        b: &BoxStyle,
595        is_header: bool,
596        rowspan_remaining: &mut [usize],
597    ) -> Vec<Segment> {
598        let mut line = Vec::new();
599        let bs = |ch: char| -> Segment {
600            let ansi = self.border_style.to_ansi();
601            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
602            Segment::new(format!("{ansi}{ch}{reset}"))
603        };
604
605        line.push(bs(b.mid_vertical));
606
607        let col_count = widths.len();
608        let mut cell_idx = 0;
609        let mut col: usize = 0;
610
611        while col < col_count {
612            // Check for active rowspan in this column
613            if rowspan_remaining[col] > 0 {
614                rowspan_remaining[col] -= 1;
615                // Render an empty spanned cell for this column
616                let w = widths[col];
617                let (_pt, pr, _pb, pl) = self.padding;
618                line.push(Segment::new(" ".repeat(pl + w + pr)));
619                if col < col_count - 1 {
620                    line.push(bs(b.mid_vertical));
621                }
622                col += 1;
623                continue;
624            }
625
626            // No more cells — fill remaining columns as empty
627            if cell_idx >= cells.len() {
628                let w = widths[col];
629                let (_pt, pr, _pb, pl) = self.padding;
630                line.push(Segment::new(" ".repeat(pl + w + pr)));
631                if col < col_count - 1 {
632                    line.push(bs(b.mid_vertical));
633                }
634                col += 1;
635                continue;
636            }
637
638            let cell = &cells[cell_idx];
639            cell_idx += 1;
640
641            let span_end = (col + cell.colspan).min(col_count);
642            let span_width: usize = widths[col..span_end].iter().sum();
643            let (_pt, pr, _pb, pl) = self.padding;
644            let content_width = span_width.saturating_sub(pl + pr);
645
646            let col_def = self.columns.get(col);
647            let justify = col_def.map(|c| c.justify).unwrap_or(AlignMethod::Left);
648
649            // Align and truncate content
650            let disp_text = justify.align_text(&cell.content, content_width);
651            let disp_trunc = if UnicodeWidthStr::width(disp_text.as_str()) > content_width {
652                let mut truncated: String = disp_text.chars()
653                    .take(content_width.saturating_sub(1))
654                    .collect();
655                truncated.push('…');
656                truncated
657            } else {
658                disp_text
659            };
660
661            // Pad left
662            line.push(Segment::new(" ".repeat(pl)));
663
664            // Apply cell style, header style, or column style
665            if let Some(ref cell_style) = cell.style {
666                let ansi = cell_style.to_ansi();
667                let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
668                line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
669            } else if is_header {
670                if let Some(hs) = col_def.map(|c| &c.header_style) {
671                    let ansi = hs.to_ansi();
672                    let reset = hs.reset_ansi();
673                    line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
674                } else {
675                    line.push(Segment::new(disp_trunc));
676                }
677            } else {
678                // Apply column default style if it has ANSI
679                let col_ansi = col_def.map(|c| c.style.to_ansi()).unwrap_or_default();
680                if col_ansi.is_empty() {
681                    line.push(Segment::new(disp_trunc));
682                } else {
683                    line.push(Segment::new(format!("{col_ansi}{disp_trunc}\x1b[0m")));
684                }
685            }
686
687            // Pad right
688            line.push(Segment::new(" ".repeat(pr)));
689
690            // Set rowspan for future rows
691            if cell.rowspan > 1 {
692                for rc in col..span_end {
693                    rowspan_remaining[rc] = cell.rowspan - 1;
694                }
695            }
696
697            col = span_end;
698
699            // Vertical separator after the span
700            if col < col_count {
701                line.push(bs(b.mid_vertical));
702            }
703        }
704
705        line.push(bs(b.mid_right));
706        line.push(Segment::line());
707        line
708    }
709
710    fn render_row_line(
711        &self,
712        widths: &[usize],
713        _values: &[String],
714        b: &BoxStyle,
715        _available_width: usize,
716        _is_header: bool,
717    ) -> Vec<Segment> {
718        let mut line = Vec::new();
719        let bs = |ch: char| -> Segment {
720            let ansi = self.border_style.to_ansi();
721            let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
722            Segment::new(format!("{ansi}{ch}{reset}"))
723        };
724
725        line.push(bs(b.mid_vertical));
726        for (i, w) in widths.iter().enumerate() {
727            line.push(Segment::new(" ".repeat(*w)));
728            if i < widths.len() - 1 {
729                line.push(bs(b.mid_vertical));
730            }
731        }
732        line.push(bs(b.mid_right));
733        line.push(Segment::line());
734        line
735    }
736}
737
738impl Default for Table {
739    fn default() -> Self {
740        Self::new()
741    }
742}
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747
748    #[test]
749    fn test_empty_table() {
750        let table = Table::new();
751        let opts = ConsoleOptions::default();
752        let result = table.render(&opts);
753        assert!(result.lines.is_empty());
754    }
755
756    #[test]
757    fn test_table_with_one_column() {
758        let mut table = Table::new();
759        table.add_column(Column::new("Name"));
760        table.add_row_str(vec!["Alice".into()]);
761        table.add_row_str(vec!["Bob".into()]);
762
763        let opts = ConsoleOptions::default();
764        let result = table.render(&opts);
765        let ansi = result.to_ansi();
766        assert!(ansi.contains("Name"));
767        assert!(ansi.contains("Alice"));
768    }
769
770    #[test]
771    fn test_cell_creation() {
772        let cell = Cell::new("hello");
773        assert_eq!(cell.content, "hello");
774        assert_eq!(cell.colspan, 1);
775        assert_eq!(cell.rowspan, 1);
776        assert!(cell.style.is_none());
777
778        let cell2 = Cell::new("world").colspan(2).rowspan(3);
779        assert_eq!(cell2.content, "world");
780        assert_eq!(cell2.colspan, 2);
781        assert_eq!(cell2.rowspan, 3);
782    }
783
784    #[test]
785    fn test_cell_from_string() {
786        let cell: Cell = "test".into();
787        assert_eq!(cell.content, "test");
788    }
789
790    #[test]
791    fn test_column_colspan() {
792        let col = Column::new("Header");
793        assert_eq!(col.colspan, 1);
794    }
795
796    #[test]
797    fn test_add_row_str() {
798        let mut table = Table::new();
799        table.add_column(Column::new("A"));
800        table.add_column(Column::new("B"));
801        table.add_row_str(vec!["x".into(), "y".into()]);
802        assert_eq!(table.row_count(), 1);
803    }
804
805    #[test]
806    fn test_add_section() {
807        let mut table = Table::new();
808        table.add_column(Column::new("A"));
809        table.add_row_str(vec!["r1".into()]);
810        table.add_section();
811        table.add_row_str(vec!["r2".into()]);
812        assert_eq!(table.row_count(), 2);
813        assert!(table.section_rows.contains(&1));
814
815        let opts = ConsoleOptions::default();
816        let result = table.render(&opts);
817        let ansi = result.to_ansi();
818        assert!(ansi.contains("r1"));
819        assert!(ansi.contains("r2"));
820    }
821
822    #[test]
823    fn test_leading() {
824        let table = Table::new()
825            .column(Column::new("X"))
826            .row_str(vec!["a".into()])
827            .row_str(vec!["b".into()])
828            .leading(1);
829        assert_eq!(table.leading, 1);
830    }
831
832    #[test]
833    fn test_cell_rowspan() {
834        let mut table = Table::new();
835        table.add_column(Column::new("A"));
836        table.add_column(Column::new("B"));
837        let cell_a = Cell::new("span").rowspan(2);
838        let cell_b = Cell::new("single");
839        table.add_row(vec![cell_a, cell_b]);
840        table.add_row_str(vec!["row2col2".into()]);
841
842        let opts = ConsoleOptions::default();
843        let result = table.render(&opts);
844        let ansi = result.to_ansi();
845        assert!(ansi.contains("span"));
846    }
847
848    #[test]
849    fn test_cell_colspan() {
850        let mut table = Table::new();
851        table.add_column(Column::new("A"));
852        table.add_column(Column::new("B"));
853        table.add_column(Column::new("C"));
854        let cell = Cell::new("wide").colspan(2);
855        table.add_row(vec![cell, Cell::new("c")]);
856        table.add_row_str(vec!["a".into(), "b".into(), "c".into()]);
857
858        let opts = ConsoleOptions::default();
859        let result = table.render(&opts);
860        let ansi = result.to_ansi();
861        assert!(ansi.contains("wide"));
862    }
863}