1use 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#[derive(Debug, Clone)]
59pub struct Cell {
60 pub content: String,
62 pub style: Option<Style>,
64 pub colspan: usize,
66 pub rowspan: usize,
68}
69
70impl Cell {
71 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 pub fn style(mut self, s: Style) -> Self { self.style = Some(s); self }
83 pub fn colspan(mut self, c: usize) -> Self { self.colspan = c; self }
85 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#[derive(Debug, Clone)]
103pub struct Column {
104 pub header: String,
106 pub footer: String,
108 pub header_style: Style,
110 pub footer_style: Style,
112 pub style: Style,
114 pub justify: AlignMethod,
116 pub vertical: VerticalAlignMethod,
118 pub overflow: OverflowMethod,
120 pub width: Option<usize>,
122 pub min_width: Option<usize>,
124 pub max_width: Option<usize>,
126 pub ratio: Option<usize>,
128 pub colspan: usize,
130}
131
132impl Column {
133 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 pub fn justify(mut self, j: AlignMethod) -> Self { self.justify = j; self }
154 pub fn width(mut self, w: usize) -> Self { self.width = Some(w); self }
156 pub fn min_width(mut self, w: usize) -> Self { self.min_width = Some(w); self }
158 pub fn max_width(mut self, w: usize) -> Self { self.max_width = Some(w); self }
160 pub fn style(mut self, s: Style) -> Self { self.style = s; self }
162 pub fn header_style(mut self, s: Style) -> Self { self.header_style = s; self }
164 pub fn ratio(mut self, r: usize) -> Self { self.ratio = Some(r); self }
166 pub fn overflow(mut self, o: OverflowMethod) -> Self { self.overflow = o; self }
168}
169
170#[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 pub fn new(cells: Vec<Cell>) -> Self {
185 Self { cells, style: None, end_section: false }
186 }
187
188 pub fn style(mut self, style: Style) -> Self {
190 self.style = Some(style);
191 self
192 }
193
194 pub fn end_section(mut self, value: bool) -> Self {
197 self.end_section = value;
198 self
199 }
200}
201
202#[derive(Debug, Clone)]
208pub struct Table {
209 columns: Vec<Column>,
210 rows: Vec<Vec<Cell>>,
211 pub title: Option<String>,
213 pub caption: Option<String>,
215 pub box_style: BoxStyle,
217 pub show_header: bool,
219 pub show_footer: bool,
221 pub show_edge: bool,
223 pub show_lines: bool,
225 pub padding: (usize, usize, usize, usize),
227 pub collapse_padding: bool,
229 pub style: Style,
231 pub border_style: Style,
233 pub title_style: Style,
235 pub caption_style: Style,
237 pub title_justify: AlignMethod,
239 pub caption_justify: AlignMethod,
241 pub highlight: bool,
243 pub width: Option<usize>,
245 pub row_styles: Vec<Style>,
247 pub leading: usize,
249 pub rowspans: Vec<usize>,
251 pub section_rows: HashSet<usize>,
253 pub pad_edge: bool,
255 pub sections: Vec<usize>,
257}
258
259impl Table {
260 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 pub fn add_column(&mut self, column: Column) {
305 self.columns.push(column);
306 }
307
308 pub fn add_row(&mut self, row: Vec<Cell>) {
321 self.rows.push(row);
322 }
323
324 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 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 pub fn column(mut self, col: Column) -> Self { self.add_column(col); self }
357
358 pub fn row(mut self, row: Vec<Cell>) -> Self { self.add_row(row); self }
360
361 pub fn row_str(mut self, row: Vec<String>) -> Self { self.add_row_str(row); self }
363
364 pub fn row_explicit(mut self, row: Row) -> Self { self.add_row_explicit(row); self }
366
367 pub fn title(mut self, t: impl Into<String>) -> Self { self.title = Some(t.into()); self }
369
370 pub fn caption(mut self, t: impl Into<String>) -> Self { self.caption = Some(t.into()); self }
372
373 pub fn box_style(mut self, bs: BoxStyle) -> Self { self.box_style = bs; self }
375
376 pub fn border_style(mut self, s: Style) -> Self { self.border_style = s; self }
378
379 pub fn hide_header(mut self) -> Self { self.show_header = false; self }
381
382 pub fn show_lines(mut self) -> Self { self.show_lines = true; self }
384
385 pub fn leading(mut self, l: usize) -> Self { self.leading = l; self }
387
388 pub fn highlight(mut self, value: bool) -> Self { self.highlight = value; self }
390
391 pub fn title_justify(mut self, justify: AlignMethod) -> Self { self.title_justify = justify; self }
393
394 pub fn caption_justify(mut self, justify: AlignMethod) -> Self { self.caption_justify = justify; self }
396
397 pub fn row_styles(mut self, styles: Vec<Style>) -> Self { self.row_styles = styles; self }
399
400 pub fn show_edge(mut self, value: bool) -> Self { self.show_edge = value; self }
402
403 pub fn collapse_padding(mut self, value: bool) -> Self { self.collapse_padding = value; self }
405
406 pub fn pad_edge(mut self, value: bool) -> Self { self.pad_edge = value; self }
408
409 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 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 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 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 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 let border_ansi = self.border_style.to_ansi();
481 let border_reset = if border_ansi.is_empty() { "" } else { "\x1b[0m" };
482 let bs = |ch: char| -> Segment {
483 Segment::new(format!("{border_ansi}{ch}{border_reset}"))
484 };
485 let bs_repeat = |ch: char, n: usize| -> Segment {
487 if border_ansi.is_empty() || n == 0 {
488 Segment::new(ch.to_string().repeat(n))
489 } else {
490 Segment::new(format!("{border_ansi}{}{border_reset}", ch.to_string().repeat(n)))
491 }
492 };
493
494 if let Some(ref title) = self.title {
496 let _tw = UnicodeWidthStr::width(title.as_str());
497 let centered = self.title_justify.align_text(title, available_width.saturating_sub(2));
498 lines.push(vec![bs(b.top_left), Segment::new(¢ered[1..centered.len()-1]), bs(b.top_right), Segment::line()]);
499 }
500
501 if self.show_edge {
503 let mut top_line = vec![bs(b.top_left)];
504 for (i, w) in col_widths.iter().enumerate() {
505 top_line.push(bs_repeat(b.top, *w));
506 if i < col_count - 1 {
507 top_line.push(bs(b.top_divider));
508 }
509 }
510 top_line.push(bs(b.top_right));
511 top_line.push(Segment::line());
512 lines.push(top_line);
513 }
514
515 if self.show_header && self.columns.iter().any(|c| !c.header.is_empty()) {
517 let (pt, _pr, _pb, _pl) = self.padding;
519 for _ in 0..pt {
520 lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
521 }
522
523 let header_cells: Vec<String> = self.columns.iter()
524 .map(|c| c.header.clone())
525 .collect();
526 lines.push(self.render_cell_line(&col_widths, &header_cells, &b, true));
527
528 let mut sep = vec![bs(b.head_row_left)];
530 for (i, w) in col_widths.iter().enumerate() {
531 sep.push(bs_repeat(b.head_row_horizontal, *w));
532 if i < col_count - 1 {
533 sep.push(bs(b.head_row_cross));
534 }
535 }
536 sep.push(bs(b.head_row_right));
537 sep.push(Segment::line());
538 lines.push(sep);
539 }
540
541 let mut rowspan_remaining: Vec<usize> = vec![0; col_count];
543 for (row_idx, row) in self.rows.iter().enumerate() {
544 if self.section_rows.contains(&row_idx) {
546 let sep_widths = Self::compute_span_widths(row, &col_widths);
547 let sc = sep_widths.len();
548 let mut sep = vec![bs(b.head_row_left)];
549 for (i, w) in sep_widths.iter().enumerate() {
550 sep.push(bs_repeat(b.head_row_horizontal, *w));
551 if i < sc - 1 {
552 sep.push(bs(b.head_row_cross));
553 }
554 }
555 sep.push(bs(b.head_row_right));
556 sep.push(Segment::line());
557 lines.push(sep);
558 }
559
560 if row_idx > 0 {
562 for _ in 0..self.leading {
563 lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
564 }
565 }
566
567 let (pt, _pr, _pb, _pl) = self.padding;
568 for _ in 0..pt {
569 lines.push(self.render_row_line(&col_widths, &[], &b, available_width, false));
570 }
571
572 let _style = if row_idx < self.row_styles.len() {
573 Some(&self.row_styles[row_idx])
574 } else if self.row_styles.len() == 2 {
575 Some(&self.row_styles[row_idx % 2])
576 } else {
577 None
578 };
579
580 lines.push(self.render_cell_line_with_rowspan(
581 &col_widths, row, &b, false, &mut rowspan_remaining,
582 ));
583
584 if self.show_lines && row_idx < self.rows.len() - 1 {
586 let sep_widths = Self::compute_span_widths(row, &col_widths);
587 let sc = sep_widths.len();
588 let mut sep = vec![bs(b.row_left)];
589 for (i, w) in sep_widths.iter().enumerate() {
590 sep.push(bs_repeat(b.row_horizontal, *w));
591 if i < sc - 1 {
592 sep.push(bs(b.row_cross));
593 }
594 }
595 sep.push(bs(b.row_right));
596 sep.push(Segment::line());
597 lines.push(sep);
598 }
599 }
600
601 if self.show_footer && self.columns.iter().any(|c| !c.footer.is_empty()) {
603 let mut sep = vec![bs(b.foot_row_left)];
604 for (i, w) in col_widths.iter().enumerate() {
605 sep.push(bs_repeat(b.foot_row_horizontal, *w));
606 if i < col_count - 1 {
607 sep.push(bs(b.foot_row_cross));
608 }
609 }
610 sep.push(bs(b.foot_row_right));
611 sep.push(Segment::line());
612 lines.push(sep);
613
614 let footer_cells: Vec<String> = self.columns.iter()
615 .map(|c| c.footer.clone())
616 .collect();
617 lines.push(self.render_cell_line(&col_widths, &footer_cells, &b, false));
618 }
619
620 if self.show_edge {
622 let bottom_widths = self.compute_bottom_widths(&col_widths);
623 let mut bot_line = vec![bs(b.bottom_left)];
624 let bc = bottom_widths.len();
625 for (i, w) in bottom_widths.iter().enumerate() {
626 bot_line.push(bs_repeat(b.bottom, *w));
627 if i < bc - 1 {
628 bot_line.push(bs(b.bottom_divider));
629 }
630 }
631 bot_line.push(bs(b.bottom_right));
632 bot_line.push(Segment::line());
633 lines.push(bot_line);
634 }
635
636 if let Some(ref caption) = self.caption {
638 let centered = self.caption_justify.align_text(caption, available_width.saturating_sub(2));
639 lines.push(vec![Segment::new(¢ered), Segment::line()]);
640 }
641
642 if options.ascii_only {
645 for line in &mut lines {
646 for seg in line.iter_mut() {
647 if seg.text.contains('\x1b') {
648 seg.text = crate::export::strip_ansi_escapes(&seg.text);
649 }
650 }
651 }
652 }
653
654 RenderResult { lines, items: Vec::new() }
655 }
656}
657
658impl Table {
659 fn calculate_column_widths(&self, available: usize) -> Vec<usize> {
660 let col_count = self.columns.len();
661 let total_pad = col_count.saturating_sub(1) + 2; let content_width = available.saturating_sub(total_pad);
663
664 let mut widths: Vec<usize> = vec![0; col_count];
666 let mut flex_cols: Vec<usize> = Vec::new();
667 let mut used = 0usize;
668
669 for (i, col) in self.columns.iter().enumerate() {
670 if let Some(w) = col.width {
671 widths[i] = w;
672 used += w;
673 } else {
674 flex_cols.push(i);
675 }
676 }
677
678 if flex_cols.is_empty() {
679 return widths;
680 }
681
682 let remaining = content_width.saturating_sub(used);
683 let _flex_count = flex_cols.len();
684
685 let total_ratio: usize = flex_cols.iter()
687 .map(|&i| self.columns[i].ratio.unwrap_or(1))
688 .sum();
689
690 for &i in &flex_cols {
691 let col = &self.columns[i];
692 let ratio = col.ratio.unwrap_or(1);
693 let mut w = (remaining * ratio) / total_ratio;
694 if let Some(min_w) = col.min_width {
695 w = w.max(min_w);
696 }
697 if let Some(max_w) = col.max_width {
698 w = w.min(max_w);
699 }
700 w = w.max(3); widths[i] = w;
702 }
703
704 let total: usize = widths.iter().sum();
706 if total < content_width && !flex_cols.is_empty() {
707 let extra = content_width - total;
708 widths[flex_cols[flex_cols.len() - 1]] += extra;
709 }
710
711 widths
712 }
713
714 fn render_cell_line(
715 &self,
716 widths: &[usize],
717 values: &[String],
718 b: &BoxStyle,
719 is_header: bool,
720 ) -> Vec<Segment> {
721 let mut line = Vec::new();
722 let col_count = widths.len();
723 let bs = |ch: char| -> Segment {
724 let ansi = self.border_style.to_ansi();
725 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
726 Segment::new(format!("{ansi}{ch}{reset}"))
727 };
728
729 line.push(bs(b.mid_left));
730
731 for (i, w) in widths.iter().enumerate() {
732 let val = values.get(i).map(|s| s.as_str()).unwrap_or("");
733 let col = self.columns.get(i);
734 let justify = col.map(|c| c.justify).unwrap_or(AlignMethod::Left);
735 let (_pt, pr, _pb, pl) = self.padding;
736
737 let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
739 let right_pad = if i == col_count - 1 && !self.pad_edge { 0 } else { pr };
740
741 line.push(Segment::new(" ".repeat(left_pad)));
743
744 let content_w = w.saturating_sub(left_pad + right_pad);
746 let disp = justify.align_text(val, content_w);
747 let disp_trunc = if UnicodeWidthStr::width(disp.as_str()) > content_w {
749 let mut truncated = disp.chars().take(
750 content_w.saturating_sub(1) ).collect::<String>();
752 truncated.push('…');
753 truncated
754 } else {
755 disp
756 };
757
758 if is_header {
760 let header_style = col.map(|c| &c.header_style);
761 if let Some(hs) = header_style {
762 let ansi = hs.to_ansi();
763 let reset = hs.reset_ansi();
764 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
765 } else {
766 line.push(Segment::new(disp_trunc));
767 }
768 } else {
769 line.push(Segment::new(disp_trunc));
770 }
771
772 line.push(Segment::new(" ".repeat(right_pad)));
774
775 if i < col_count - 1 {
776 line.push(bs(b.mid_vertical));
777 }
778 }
779
780 line.push(bs(b.mid_right));
781 line.push(Segment::line());
782 line
783 }
784
785 fn render_cell_line_with_rowspan(
788 &self,
789 widths: &[usize],
790 cells: &[Cell],
791 b: &BoxStyle,
792 is_header: bool,
793 rowspan_remaining: &mut [usize],
794 ) -> Vec<Segment> {
795 let mut line = Vec::new();
796 let col_count = widths.len();
797 let bs = |ch: char| -> Segment {
798 let ansi = self.border_style.to_ansi();
799 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
800 Segment::new(format!("{ansi}{ch}{reset}"))
801 };
802
803 line.push(bs(b.mid_left));
804
805 let mut cell_idx = 0;
806 let mut col: usize = 0;
807
808 while col < col_count {
809 if rowspan_remaining[col] > 0 {
811 let span_start = col;
816 let mut span_total_w = 0usize;
817 while col < col_count && rowspan_remaining[col] > 0 {
818 rowspan_remaining[col] -= 1;
819 span_total_w += widths[col];
820 col += 1;
821 }
822 let num_spanned = col - span_start;
825 span_total_w += num_spanned.saturating_sub(1);
826 line.push(Segment::new(" ".repeat(span_total_w)));
827 if col < col_count {
829 line.push(bs(b.mid_vertical));
830 }
831 continue;
832 }
833
834 if cell_idx >= cells.len() {
836 let w = widths[col];
837 let (_pt, pr, _pb, pl) = self.padding;
838 let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
839 let right_pad = if col == col_count - 1 && !self.pad_edge { 0 } else { pr };
840 line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
841 if col < col_count - 1 {
842 line.push(bs(b.mid_vertical));
843 }
844 col += 1;
845 continue;
846 }
847
848 let cell = &cells[cell_idx];
849 cell_idx += 1;
850
851 let span_end = (col + cell.colspan).min(col_count);
852 let num_spanned = span_end - col;
853 let span_width: usize = widths[col..span_end].iter().sum::<usize>()
856 + num_spanned.saturating_sub(1);
857 let (_pt, pr, _pb, pl) = self.padding;
858 let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
859 let right_pad = if span_end >= col_count && !self.pad_edge { 0 } else { pr };
860 let content_width = span_width.saturating_sub(left_pad + right_pad);
861
862 let col_def = self.columns.get(col);
863 let justify = col_def.map(|c| c.justify).unwrap_or(AlignMethod::Left);
864
865 let disp_text = justify.align_text(&cell.content, content_width);
867 let disp_trunc = if UnicodeWidthStr::width(disp_text.as_str()) > content_width {
868 let mut truncated: String = disp_text.chars()
869 .take(content_width.saturating_sub(1))
870 .collect();
871 truncated.push('…');
872 truncated
873 } else {
874 disp_text
875 };
876
877 line.push(Segment::new(" ".repeat(left_pad)));
879
880 if let Some(ref cell_style) = cell.style {
882 let ansi = cell_style.to_ansi();
883 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
884 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
885 } else if is_header {
886 if let Some(hs) = col_def.map(|c| &c.header_style) {
887 let ansi = hs.to_ansi();
888 let reset = hs.reset_ansi();
889 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
890 } else {
891 line.push(Segment::new(disp_trunc));
892 }
893 } else {
894 let col_ansi = col_def.map(|c| c.style.to_ansi()).unwrap_or_default();
896 if col_ansi.is_empty() {
897 line.push(Segment::new(disp_trunc));
898 } else {
899 line.push(Segment::new(format!("{col_ansi}{disp_trunc}\x1b[0m")));
900 }
901 }
902
903 line.push(Segment::new(" ".repeat(right_pad)));
905
906 if cell.rowspan > 1 {
908 for rc in col..span_end {
909 rowspan_remaining[rc] = cell.rowspan - 1;
910 }
911 }
912
913 col = span_end;
914
915 if col < col_count {
917 line.push(bs(b.mid_vertical));
918 }
919 }
920
921 line.push(bs(b.mid_right));
922 line.push(Segment::line());
923 line
924 }
925
926 fn compute_span_widths(cells: &[Cell], col_widths: &[usize]) -> Vec<usize> {
930 let col_count = col_widths.len();
931 if col_count == 0 {
932 return vec![];
933 }
934
935 let mut widths = Vec::new();
936 let mut col = 0usize;
937 for cell in cells {
938 if col >= col_count {
939 break;
940 }
941 let span = cell.colspan.min(col_count - col);
942 let w: usize = col_widths[col..col + span].iter().sum::<usize>()
944 + span.saturating_sub(1);
945 widths.push(w);
946 col += span;
947 }
948 while col < col_count {
950 widths.push(col_widths[col]);
951 col += 1;
952 }
953 widths
954 }
955
956 fn compute_bottom_widths(&self, col_widths: &[usize]) -> Vec<usize> {
959 if self.rows.is_empty() {
960 return col_widths.to_vec();
961 }
962 Self::compute_span_widths(&self.rows[self.rows.len() - 1], col_widths)
963 }
964
965 fn render_row_line(
966 &self,
967 widths: &[usize],
968 _values: &[String],
969 b: &BoxStyle,
970 _available_width: usize,
971 _is_header: bool,
972 ) -> Vec<Segment> {
973 let mut line = Vec::new();
974 let col_count = widths.len();
975 let bs = |ch: char| -> Segment {
976 let ansi = self.border_style.to_ansi();
977 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
978 Segment::new(format!("{ansi}{ch}{reset}"))
979 };
980
981 line.push(bs(b.mid_left));
984 for (i, w) in widths.iter().enumerate() {
985 let (_pt, pr, _pb, pl) = self.padding;
986 let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
987 let right_pad = if i == col_count - 1 && !self.pad_edge { 0 } else { pr };
988 line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
989 if i < col_count - 1 {
990 line.push(bs(b.mid_vertical));
991 }
992 }
993 line.push(bs(b.mid_right));
994 line.push(Segment::line());
995 line
996 }
997}
998
999impl Default for Table {
1000 fn default() -> Self {
1001 Self::new()
1002 }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008
1009 #[test]
1010 fn test_empty_table() {
1011 let table = Table::new();
1012 let opts = ConsoleOptions::default();
1013 let result = table.render(&opts);
1014 assert!(result.lines.is_empty());
1015 }
1016
1017 #[test]
1018 fn test_table_with_one_column() {
1019 let mut table = Table::new();
1020 table.add_column(Column::new("Name"));
1021 table.add_row_str(vec!["Alice".into()]);
1022 table.add_row_str(vec!["Bob".into()]);
1023
1024 let opts = ConsoleOptions::default();
1025 let result = table.render(&opts);
1026 let ansi = result.to_ansi();
1027 assert!(ansi.contains("Name"));
1028 assert!(ansi.contains("Alice"));
1029 }
1030
1031 #[test]
1032 fn test_cell_creation() {
1033 let cell = Cell::new("hello");
1034 assert_eq!(cell.content, "hello");
1035 assert_eq!(cell.colspan, 1);
1036 assert_eq!(cell.rowspan, 1);
1037 assert!(cell.style.is_none());
1038
1039 let cell2 = Cell::new("world").colspan(2).rowspan(3);
1040 assert_eq!(cell2.content, "world");
1041 assert_eq!(cell2.colspan, 2);
1042 assert_eq!(cell2.rowspan, 3);
1043 }
1044
1045 #[test]
1046 fn test_cell_from_string() {
1047 let cell: Cell = "test".into();
1048 assert_eq!(cell.content, "test");
1049 }
1050
1051 #[test]
1052 fn test_column_colspan() {
1053 let col = Column::new("Header");
1054 assert_eq!(col.colspan, 1);
1055 }
1056
1057 #[test]
1058 fn test_add_row_str() {
1059 let mut table = Table::new();
1060 table.add_column(Column::new("A"));
1061 table.add_column(Column::new("B"));
1062 table.add_row_str(vec!["x".into(), "y".into()]);
1063 assert_eq!(table.row_count(), 1);
1064 }
1065
1066 #[test]
1067 fn test_add_section() {
1068 let mut table = Table::new();
1069 table.add_column(Column::new("A"));
1070 table.add_row_str(vec!["r1".into()]);
1071 table.add_section();
1072 table.add_row_str(vec!["r2".into()]);
1073 assert_eq!(table.row_count(), 2);
1074 assert!(table.section_rows.contains(&1));
1075
1076 let opts = ConsoleOptions::default();
1077 let result = table.render(&opts);
1078 let ansi = result.to_ansi();
1079 assert!(ansi.contains("r1"));
1080 assert!(ansi.contains("r2"));
1081 }
1082
1083 #[test]
1084 fn test_leading() {
1085 let table = Table::new()
1086 .column(Column::new("X"))
1087 .row_str(vec!["a".into()])
1088 .row_str(vec!["b".into()])
1089 .leading(1);
1090 assert_eq!(table.leading, 1);
1091 }
1092
1093 #[test]
1094 fn test_cell_rowspan() {
1095 let mut table = Table::new();
1096 table.add_column(Column::new("A"));
1097 table.add_column(Column::new("B"));
1098 let cell_a = Cell::new("span").rowspan(2);
1099 let cell_b = Cell::new("single");
1100 table.add_row(vec![cell_a, cell_b]);
1101 table.add_row_str(vec!["row2col2".into()]);
1102
1103 let opts = ConsoleOptions::default();
1104 let result = table.render(&opts);
1105 let ansi = result.to_ansi();
1106 assert!(ansi.contains("span"));
1107 }
1108
1109 #[test]
1110 fn test_cell_colspan() {
1111 let mut table = Table::new();
1112 table.add_column(Column::new("A"));
1113 table.add_column(Column::new("B"));
1114 table.add_column(Column::new("C"));
1115 let cell = Cell::new("wide").colspan(2);
1116 table.add_row(vec![cell, Cell::new("c")]);
1117 table.add_row_str(vec!["a".into(), "b".into(), "c".into()]);
1118
1119 let opts = ConsoleOptions::default();
1120 let result = table.render(&opts);
1121 let ansi = result.to_ansi();
1122 assert!(ansi.contains("wide"));
1123 }
1124
1125 #[test]
1128 fn test_row_struct() {
1129 let cells = vec![Cell::new("a"), Cell::new("b")];
1130 let row = Row::new(cells)
1131 .style(Style::new().bold(true))
1132 .end_section(true);
1133 assert_eq!(row.cells.len(), 2);
1134 assert!(row.style.is_some());
1135 assert!(row.end_section);
1136 }
1137
1138 #[test]
1139 fn test_add_row_explicit() {
1140 let mut table = Table::new();
1141 table.add_column(Column::new("A"));
1142 table.add_column(Column::new("B"));
1143 let row = Row::new(vec![Cell::new("x"), Cell::new("y")]);
1144 table.add_row_explicit(row);
1145 assert_eq!(table.row_count(), 1);
1146
1147 let opts = ConsoleOptions::default();
1148 let result = table.render(&opts);
1149 let ansi = result.to_ansi();
1150 assert!(ansi.contains("x"));
1151 assert!(ansi.contains("y"));
1152 }
1153
1154 #[test]
1155 fn test_add_row_explicit_with_section() {
1156 let mut table = Table::new();
1157 table.add_column(Column::new("A"));
1158 table.add_row_str(vec!["before".into()]);
1159 let row = Row::new(vec![Cell::new("after")]).end_section(true);
1160 table.add_row_explicit(row);
1161 assert!(table.section_rows.contains(&1));
1162 }
1163
1164 #[test]
1165 fn test_builder_highlight() {
1166 let table = Table::new().highlight(true);
1167 assert!(table.highlight);
1168 }
1169
1170 #[test]
1171 fn test_builder_title_justify() {
1172 let table = Table::new().title_justify(AlignMethod::Right);
1173 assert_eq!(table.title_justify, AlignMethod::Right);
1174 }
1175
1176 #[test]
1177 fn test_builder_caption_justify() {
1178 let table = Table::new().caption_justify(AlignMethod::Left);
1179 assert_eq!(table.caption_justify, AlignMethod::Left);
1180 }
1181
1182 #[test]
1183 fn test_builder_row_styles() {
1184 let s1 = Style::new().bold(true);
1185 let s2 = Style::new().dim(true);
1186 let table = Table::new().row_styles(vec![s1.clone(), s2.clone()]);
1187 assert_eq!(table.row_styles.len(), 2);
1188 }
1189
1190 #[test]
1191 fn test_builder_show_edge() {
1192 let table = Table::new().show_edge(false);
1193 assert!(!table.show_edge);
1194 }
1195
1196 #[test]
1197 fn test_builder_collapse_padding() {
1198 let table = Table::new().collapse_padding(true);
1199 assert!(table.collapse_padding);
1200 }
1201
1202 #[test]
1203 fn test_builder_pad_edge() {
1204 let table = Table::new().pad_edge(false);
1205 assert!(!table.pad_edge);
1206 }
1207
1208 #[test]
1209 fn test_get_row_style_empty() {
1210 let table = Table::new();
1211 assert_eq!(table.get_row_style(0), None);
1212 }
1213
1214 #[test]
1215 fn test_get_row_style_with_styles() {
1216 let s1 = Style::new().bold(true);
1217 let s2 = Style::new().dim(true);
1218 let table = Table::new().row_styles(vec![s1, s2]);
1219 assert!(table.get_row_style(0).is_some());
1220 assert!(table.get_row_style(1).is_some());
1221 assert!(table.get_row_style(2).is_some());
1223 assert!(table.get_row_style(3).is_some());
1224 }
1225
1226 #[test]
1227 fn test_add_section_returns_self() {
1228 let mut table = Table::new();
1229 table.add_column(Column::new("A"));
1230 table.add_row_str(vec!["r1".into()]);
1231 let ret = table.add_section();
1232 ret.add_row_str(vec!["r2".into()]);
1234 assert_eq!(table.row_count(), 2);
1235 }
1236
1237 #[test]
1238 fn test_sections_field() {
1239 let mut table = Table::new();
1240 table.add_column(Column::new("A"));
1241 table.add_row_str(vec!["r1".into()]);
1242 table.add_section();
1243 table.add_row_str(vec!["r2".into()]);
1244 assert_eq!(table.sections.len(), 1);
1245 assert_eq!(table.sections[0], 1);
1246 }
1247
1248 #[test]
1249 fn test_pad_edge_default() {
1250 let table = Table::new();
1251 assert!(table.pad_edge);
1252 }
1253
1254 #[test]
1255 fn test_grid_method() {
1256 let table = Table::grid();
1257 assert!(!table.show_edge);
1258 assert!(!table.show_header);
1259 }
1260}