1use crate::align::{AlignMethod, VerticalAlignMethod};
45use crate::box_drawing::{get_safe_box, BoxStyle, BOX_HEAVY_HEAD};
46use crate::console::{ConsoleOptions, OverflowMethod, RenderResult, Renderable};
47use crate::segment::Segment;
48use crate::style::Style;
49use std::collections::HashSet;
50use unicode_width::UnicodeWidthStr;
51
52#[derive(Debug, Clone)]
58pub struct Cell {
59 pub content: String,
61 pub style: Option<Style>,
63 pub colspan: usize,
65 pub rowspan: usize,
67}
68
69impl Cell {
70 pub fn new(content: impl Into<String>) -> Self {
72 Cell {
73 content: content.into(),
74 style: None,
75 colspan: 1,
76 rowspan: 1,
77 }
78 }
79
80 pub fn style(mut self, s: Style) -> Self {
82 self.style = Some(s);
83 self
84 }
85 pub fn colspan(mut self, c: usize) -> Self {
87 self.colspan = c;
88 self
89 }
90 pub fn rowspan(mut self, r: usize) -> Self {
92 self.rowspan = r;
93 self
94 }
95}
96
97impl From<String> for Cell {
98 fn from(s: String) -> Self {
99 Cell::new(s)
100 }
101}
102
103impl From<&str> for Cell {
104 fn from(s: &str) -> Self {
105 Cell::new(s)
106 }
107}
108
109#[derive(Debug, Clone)]
115pub struct Column {
116 pub header: String,
118 pub footer: String,
120 pub header_style: Style,
122 pub footer_style: Style,
124 pub style: Style,
126 pub justify: AlignMethod,
128 pub vertical: VerticalAlignMethod,
130 pub overflow: OverflowMethod,
132 pub width: Option<usize>,
134 pub min_width: Option<usize>,
136 pub max_width: Option<usize>,
138 pub ratio: Option<usize>,
140 pub colspan: usize,
142}
143
144impl Column {
145 pub fn new(header: impl Into<String>) -> Self {
147 Self {
148 header: header.into(),
149 footer: String::new(),
150 header_style: Style::new().bold(true),
151 footer_style: Style::new(),
152 style: Style::new(),
153 justify: AlignMethod::Left,
154 vertical: VerticalAlignMethod::Top,
155 overflow: OverflowMethod::Ellipsis,
156 width: None,
157 min_width: None,
158 max_width: None,
159 ratio: None,
160 colspan: 1,
161 }
162 }
163
164 pub fn justify(mut self, j: AlignMethod) -> Self {
166 self.justify = j;
167 self
168 }
169 pub fn width(mut self, w: usize) -> Self {
171 self.width = Some(w);
172 self
173 }
174 pub fn min_width(mut self, w: usize) -> Self {
176 self.min_width = Some(w);
177 self
178 }
179 pub fn max_width(mut self, w: usize) -> Self {
181 self.max_width = Some(w);
182 self
183 }
184 pub fn style(mut self, s: Style) -> Self {
186 self.style = s;
187 self
188 }
189 pub fn header_style(mut self, s: Style) -> Self {
191 self.header_style = s;
192 self
193 }
194 pub fn ratio(mut self, r: usize) -> Self {
196 self.ratio = Some(r);
197 self
198 }
199 pub fn overflow(mut self, o: OverflowMethod) -> Self {
201 self.overflow = o;
202 self
203 }
204}
205
206#[derive(Debug, Clone)]
212pub struct Row {
213 pub cells: Vec<Cell>,
214 pub style: Option<Style>,
215 pub end_section: bool,
216}
217
218impl Row {
219 pub fn new(cells: Vec<Cell>) -> Self {
221 Self {
222 cells,
223 style: None,
224 end_section: false,
225 }
226 }
227
228 pub fn style(mut self, style: Style) -> Self {
230 self.style = Some(style);
231 self
232 }
233
234 pub fn end_section(mut self, value: bool) -> Self {
237 self.end_section = value;
238 self
239 }
240}
241
242#[derive(Debug, Clone)]
248pub struct Table {
249 columns: Vec<Column>,
250 rows: Vec<Vec<Cell>>,
251 pub title: Option<String>,
253 pub caption: Option<String>,
255 pub box_style: BoxStyle,
257 pub show_header: bool,
259 pub show_footer: bool,
261 pub show_edge: bool,
263 pub show_lines: bool,
265 pub padding: (usize, usize, usize, usize),
267 pub collapse_padding: bool,
269 pub style: Style,
271 pub border_style: Style,
273 pub title_style: Style,
275 pub caption_style: Style,
277 pub title_justify: AlignMethod,
279 pub caption_justify: AlignMethod,
281 pub highlight: bool,
283 pub width: Option<usize>,
285 pub row_styles: Vec<Style>,
287 pub leading: usize,
289 pub rowspans: Vec<usize>,
291 pub section_rows: HashSet<usize>,
293 pub pad_edge: bool,
295 pub sections: Vec<usize>,
297}
298
299impl Table {
300 pub fn new() -> Self {
302 Self {
303 columns: Vec::new(),
304 rows: Vec::new(),
305 title: None,
306 caption: None,
307 box_style: BOX_HEAVY_HEAD.clone(),
308 show_header: true,
309 show_footer: false,
310 show_edge: true,
311 show_lines: false,
312 padding: (0, 1, 0, 1),
313 collapse_padding: false,
314 style: Style::new(),
315 border_style: Style::new(),
316 title_style: Style::new().bold(true),
317 caption_style: Style::new().dim(true),
318 title_justify: AlignMethod::Center,
319 caption_justify: AlignMethod::Center,
320 highlight: false,
321 width: None,
322 row_styles: Vec::new(),
323 leading: 0,
324 rowspans: Vec::new(),
325 section_rows: HashSet::new(),
326 pad_edge: true,
327 sections: Vec::new(),
328 }
329 }
330
331 pub fn add_column(&mut self, column: Column) {
345 self.columns.push(column);
346 }
347
348 pub fn add_row(&mut self, row: Vec<Cell>) {
361 self.rows.push(row);
362 }
363
364 pub fn add_row_explicit(&mut self, row: Row) -> &mut Self {
370 if row.end_section {
371 self.section_rows.insert(self.rows.len());
372 self.sections.push(self.rows.len());
373 }
374 self.rows.push(row.cells);
375 self
376 }
377
378 pub fn add_row_str(&mut self, row: Vec<String>) {
391 let cells: Vec<Cell> = row.into_iter().map(Cell::new).collect();
392 self.rows.push(cells);
393 }
394
395 pub fn column(mut self, col: Column) -> Self {
397 self.add_column(col);
398 self
399 }
400
401 pub fn row(mut self, row: Vec<Cell>) -> Self {
403 self.add_row(row);
404 self
405 }
406
407 pub fn row_str(mut self, row: Vec<String>) -> Self {
409 self.add_row_str(row);
410 self
411 }
412
413 pub fn row_explicit(mut self, row: Row) -> Self {
415 self.add_row_explicit(row);
416 self
417 }
418
419 pub fn title(mut self, t: impl Into<String>) -> Self {
421 self.title = Some(t.into());
422 self
423 }
424
425 pub fn caption(mut self, t: impl Into<String>) -> Self {
427 self.caption = Some(t.into());
428 self
429 }
430
431 pub fn box_style(mut self, bs: BoxStyle) -> Self {
433 self.box_style = bs;
434 self
435 }
436
437 pub fn border_style(mut self, s: Style) -> Self {
439 self.border_style = s;
440 self
441 }
442
443 pub fn hide_header(mut self) -> Self {
445 self.show_header = false;
446 self
447 }
448
449 pub fn show_lines(mut self) -> Self {
451 self.show_lines = true;
452 self
453 }
454
455 pub fn leading(mut self, l: usize) -> Self {
457 self.leading = l;
458 self
459 }
460
461 pub fn highlight(mut self, value: bool) -> Self {
463 self.highlight = value;
464 self
465 }
466
467 pub fn title_justify(mut self, justify: AlignMethod) -> Self {
469 self.title_justify = justify;
470 self
471 }
472
473 pub fn caption_justify(mut self, justify: AlignMethod) -> Self {
475 self.caption_justify = justify;
476 self
477 }
478
479 pub fn row_styles(mut self, styles: Vec<Style>) -> Self {
481 self.row_styles = styles;
482 self
483 }
484
485 pub fn show_edge(mut self, value: bool) -> Self {
487 self.show_edge = value;
488 self
489 }
490
491 pub fn collapse_padding(mut self, value: bool) -> Self {
493 self.collapse_padding = value;
494 self
495 }
496
497 pub fn pad_edge(mut self, value: bool) -> Self {
499 self.pad_edge = value;
500 self
501 }
502
503 pub fn get_row_style(&self, row_index: usize) -> Option<Style> {
505 if self.row_styles.is_empty() {
506 None
507 } else {
508 Some(self.row_styles[row_index % self.row_styles.len()].clone())
509 }
510 }
511
512 pub fn grid() -> Self {
515 Self {
516 columns: Vec::new(),
517 rows: Vec::new(),
518 title: None,
519 caption: None,
520 box_style: crate::box_drawing::BOX_SIMPLE.clone(),
521 show_header: false,
522 show_footer: false,
523 show_edge: false,
524 show_lines: false,
525 padding: (0, 1, 0, 1),
526 collapse_padding: false,
527 style: Style::new(),
528 border_style: Style::new(),
529 title_style: Style::new().bold(true),
530 caption_style: Style::new().dim(true),
531 title_justify: AlignMethod::Center,
532 caption_justify: AlignMethod::Center,
533 highlight: false,
534 width: None,
535 row_styles: Vec::new(),
536 leading: 0,
537 rowspans: Vec::new(),
538 section_rows: HashSet::new(),
539 pad_edge: true,
540 sections: Vec::new(),
541 }
542 }
543
544 pub fn add_section(&mut self) -> &mut Self {
548 self.section_rows.insert(self.rows.len());
549 self.sections.push(self.rows.len());
550 self
551 }
552
553 pub fn row_count(&self) -> usize {
555 self.rows.len()
556 }
557}
558
559impl Renderable for Table {
560 fn render(&self, options: &ConsoleOptions) -> RenderResult {
561 if self.columns.is_empty() {
562 return RenderResult::new();
563 }
564
565 let box_style = get_safe_box(&self.box_style, options.ascii_only);
566 let available_width = self.width.unwrap_or(options.max_width);
567 let col_count = self.columns.len();
568
569 let col_widths = self.calculate_column_widths(available_width);
571
572 let mut lines: Vec<Vec<Segment>> = Vec::new();
573 let b = &box_style;
574
575 let border_ansi = self.border_style.to_ansi();
577 let border_reset = if border_ansi.is_empty() {
578 ""
579 } else {
580 "\x1b[0m"
581 };
582 let bs = |ch: char| -> Segment { Segment::new(format!("{border_ansi}{ch}{border_reset}")) };
583 let bs_repeat = |ch: char, n: usize| -> Segment {
585 if border_ansi.is_empty() || n == 0 {
586 Segment::new(ch.to_string().repeat(n))
587 } else {
588 Segment::new(format!(
589 "{border_ansi}{}{border_reset}",
590 ch.to_string().repeat(n)
591 ))
592 }
593 };
594
595 if let Some(ref title) = self.title {
597 let _tw = UnicodeWidthStr::width(title.as_str());
598 let centered = self
599 .title_justify
600 .align_text(title, available_width.saturating_sub(2));
601 lines.push(vec![
602 bs(b.top_left),
603 Segment::new(¢ered[1..centered.len() - 1]),
604 bs(b.top_right),
605 Segment::line(),
606 ]);
607 }
608
609 if self.show_edge {
611 let mut top_line = vec![bs(b.top_left)];
612 for (i, w) in col_widths.iter().enumerate() {
613 top_line.push(bs_repeat(b.top, *w));
614 if i < col_count - 1 {
615 top_line.push(bs(b.top_divider));
616 }
617 }
618 top_line.push(bs(b.top_right));
619 top_line.push(Segment::line());
620 lines.push(top_line);
621 }
622
623 if self.show_header && self.columns.iter().any(|c| !c.header.is_empty()) {
625 let (pt, _pr, _pb, _pl) = self.padding;
627 for _ in 0..pt {
628 lines.push(self.render_row_line(&col_widths, &[], b, available_width, false));
629 }
630
631 let header_cells: Vec<String> = self.columns.iter().map(|c| c.header.clone()).collect();
632 lines.push(self.render_cell_line(&col_widths, &header_cells, b, true));
633
634 let mut sep = vec![bs(b.head_row_left)];
636 for (i, w) in col_widths.iter().enumerate() {
637 sep.push(bs_repeat(b.head_row_horizontal, *w));
638 if i < col_count - 1 {
639 sep.push(bs(b.head_row_cross));
640 }
641 }
642 sep.push(bs(b.head_row_right));
643 sep.push(Segment::line());
644 lines.push(sep);
645 }
646
647 let mut rowspan_remaining: Vec<usize> = vec![0; col_count];
649 for (row_idx, row) in self.rows.iter().enumerate() {
650 if self.section_rows.contains(&row_idx) {
652 let sep_widths = Self::compute_span_widths(row, &col_widths);
653 let sc = sep_widths.len();
654 let mut sep = vec![bs(b.head_row_left)];
655 for (i, w) in sep_widths.iter().enumerate() {
656 sep.push(bs_repeat(b.head_row_horizontal, *w));
657 if i < sc - 1 {
658 sep.push(bs(b.head_row_cross));
659 }
660 }
661 sep.push(bs(b.head_row_right));
662 sep.push(Segment::line());
663 lines.push(sep);
664 }
665
666 if row_idx > 0 {
668 for _ in 0..self.leading {
669 lines.push(self.render_row_line(&col_widths, &[], b, available_width, false));
670 }
671 }
672
673 let (pt, _pr, _pb, _pl) = self.padding;
674 for _ in 0..pt {
675 lines.push(self.render_row_line(&col_widths, &[], b, available_width, false));
676 }
677
678 let _style = if row_idx < self.row_styles.len() {
679 Some(&self.row_styles[row_idx])
680 } else if self.row_styles.len() == 2 {
681 Some(&self.row_styles[row_idx % 2])
682 } else {
683 None
684 };
685
686 lines.push(self.render_cell_line_with_rowspan(
687 &col_widths,
688 row,
689 b,
690 false,
691 &mut rowspan_remaining,
692 ));
693
694 if self.show_lines && row_idx < self.rows.len() - 1 {
696 let sep_widths = Self::compute_span_widths(row, &col_widths);
697 let sc = sep_widths.len();
698 let mut sep = vec![bs(b.row_left)];
699 for (i, w) in sep_widths.iter().enumerate() {
700 sep.push(bs_repeat(b.row_horizontal, *w));
701 if i < sc - 1 {
702 sep.push(bs(b.row_cross));
703 }
704 }
705 sep.push(bs(b.row_right));
706 sep.push(Segment::line());
707 lines.push(sep);
708 }
709 }
710
711 if self.show_footer && self.columns.iter().any(|c| !c.footer.is_empty()) {
713 let mut sep = vec![bs(b.foot_row_left)];
714 for (i, w) in col_widths.iter().enumerate() {
715 sep.push(bs_repeat(b.foot_row_horizontal, *w));
716 if i < col_count - 1 {
717 sep.push(bs(b.foot_row_cross));
718 }
719 }
720 sep.push(bs(b.foot_row_right));
721 sep.push(Segment::line());
722 lines.push(sep);
723
724 let footer_cells: Vec<String> = self.columns.iter().map(|c| c.footer.clone()).collect();
725 lines.push(self.render_cell_line(&col_widths, &footer_cells, b, false));
726 }
727
728 if self.show_edge {
730 let bottom_widths = self.compute_bottom_widths(&col_widths);
731 let mut bot_line = vec![bs(b.bottom_left)];
732 let bc = bottom_widths.len();
733 for (i, w) in bottom_widths.iter().enumerate() {
734 bot_line.push(bs_repeat(b.bottom, *w));
735 if i < bc - 1 {
736 bot_line.push(bs(b.bottom_divider));
737 }
738 }
739 bot_line.push(bs(b.bottom_right));
740 bot_line.push(Segment::line());
741 lines.push(bot_line);
742 }
743
744 if let Some(ref caption) = self.caption {
746 let centered = self
747 .caption_justify
748 .align_text(caption, available_width.saturating_sub(2));
749 lines.push(vec![Segment::new(¢ered), Segment::line()]);
750 }
751
752 if options.ascii_only {
755 for line in &mut lines {
756 for seg in line.iter_mut() {
757 if seg.text.contains('\x1b') {
758 seg.text = crate::export::strip_ansi_escapes(&seg.text);
759 }
760 }
761 }
762 }
763
764 RenderResult {
765 lines,
766 items: Vec::new(),
767 }
768 }
769}
770
771impl Table {
772 fn calculate_column_widths(&self, available: usize) -> Vec<usize> {
773 let col_count = self.columns.len();
774 let total_pad = col_count.saturating_sub(1) + 2; let content_width = available.saturating_sub(total_pad);
776
777 let mut widths: Vec<usize> = vec![0; col_count];
779 let mut flex_cols: Vec<usize> = Vec::new();
780 let mut used = 0usize;
781
782 for (i, col) in self.columns.iter().enumerate() {
783 if let Some(w) = col.width {
784 widths[i] = w;
785 used += w;
786 } else {
787 flex_cols.push(i);
788 }
789 }
790
791 if flex_cols.is_empty() {
792 return widths;
793 }
794
795 let remaining = content_width.saturating_sub(used);
796 let _flex_count = flex_cols.len();
797
798 let total_ratio: usize = flex_cols
800 .iter()
801 .map(|&i| self.columns[i].ratio.unwrap_or(1))
802 .sum();
803
804 for &i in &flex_cols {
805 let col = &self.columns[i];
806 let ratio = col.ratio.unwrap_or(1);
807 let mut w = (remaining * ratio) / total_ratio;
808 if let Some(min_w) = col.min_width {
809 w = w.max(min_w);
810 }
811 if let Some(max_w) = col.max_width {
812 w = w.min(max_w);
813 }
814 w = w.max(3); widths[i] = w;
816 }
817
818 let total: usize = widths.iter().sum();
820 if total < content_width && !flex_cols.is_empty() {
821 let extra = content_width - total;
822 widths[flex_cols[flex_cols.len() - 1]] += extra;
823 }
824
825 widths
826 }
827
828 fn render_cell_line(
829 &self,
830 widths: &[usize],
831 values: &[String],
832 b: &BoxStyle,
833 is_header: bool,
834 ) -> Vec<Segment> {
835 let mut line = Vec::new();
836 let col_count = widths.len();
837 let bs = |ch: char| -> Segment {
838 let ansi = self.border_style.to_ansi();
839 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
840 Segment::new(format!("{ansi}{ch}{reset}"))
841 };
842
843 line.push(bs(b.mid_left));
844
845 for (i, w) in widths.iter().enumerate() {
846 let val = values.get(i).map(|s| s.as_str()).unwrap_or("");
847 let col = self.columns.get(i);
848 let justify = col.map(|c| c.justify).unwrap_or(AlignMethod::Left);
849 let (_pt, pr, _pb, pl) = self.padding;
850
851 let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
853 let right_pad = if i == col_count - 1 && !self.pad_edge {
854 0
855 } else {
856 pr
857 };
858
859 line.push(Segment::new(" ".repeat(left_pad)));
861
862 let content_w = w.saturating_sub(left_pad + right_pad);
864 let disp = justify.align_text(val, content_w);
865 let disp_trunc = if UnicodeWidthStr::width(disp.as_str()) > content_w {
867 let mut truncated = disp
868 .chars()
869 .take(
870 content_w.saturating_sub(1), )
872 .collect::<String>();
873 truncated.push('…');
874 truncated
875 } else {
876 disp
877 };
878
879 if is_header {
881 let header_style = col.map(|c| &c.header_style);
882 if let Some(hs) = header_style {
883 let ansi = hs.to_ansi();
884 let reset = hs.reset_ansi();
885 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
886 } else {
887 line.push(Segment::new(disp_trunc));
888 }
889 } else {
890 line.push(Segment::new(disp_trunc));
891 }
892
893 line.push(Segment::new(" ".repeat(right_pad)));
895
896 if i < col_count - 1 {
897 line.push(bs(b.mid_vertical));
898 }
899 }
900
901 line.push(bs(b.mid_right));
902 line.push(Segment::line());
903 line
904 }
905
906 fn render_cell_line_with_rowspan(
909 &self,
910 widths: &[usize],
911 cells: &[Cell],
912 b: &BoxStyle,
913 is_header: bool,
914 rowspan_remaining: &mut [usize],
915 ) -> Vec<Segment> {
916 let mut line = Vec::new();
917 let col_count = widths.len();
918 let bs = |ch: char| -> Segment {
919 let ansi = self.border_style.to_ansi();
920 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
921 Segment::new(format!("{ansi}{ch}{reset}"))
922 };
923
924 line.push(bs(b.mid_left));
925
926 let mut cell_idx = 0;
927 let mut col: usize = 0;
928
929 while col < col_count {
930 if rowspan_remaining[col] > 0 {
932 let span_start = col;
937 let mut span_total_w = 0usize;
938 while col < col_count && rowspan_remaining[col] > 0 {
939 rowspan_remaining[col] -= 1;
940 span_total_w += widths[col];
941 col += 1;
942 }
943 let num_spanned = col - span_start;
946 span_total_w += num_spanned.saturating_sub(1);
947 line.push(Segment::new(" ".repeat(span_total_w)));
948 if col < col_count {
950 line.push(bs(b.mid_vertical));
951 }
952 continue;
953 }
954
955 if cell_idx >= cells.len() {
957 let w = widths[col];
958 let (_pt, pr, _pb, pl) = self.padding;
959 let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
960 let right_pad = if col == col_count - 1 && !self.pad_edge {
961 0
962 } else {
963 pr
964 };
965 line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
966 if col < col_count - 1 {
967 line.push(bs(b.mid_vertical));
968 }
969 col += 1;
970 continue;
971 }
972
973 let cell = &cells[cell_idx];
974 cell_idx += 1;
975
976 let span_end = (col + cell.colspan).min(col_count);
977 let num_spanned = span_end - col;
978 let span_width: usize =
981 widths[col..span_end].iter().sum::<usize>() + num_spanned.saturating_sub(1);
982 let (_pt, pr, _pb, pl) = self.padding;
983 let left_pad = if col == 0 && !self.pad_edge { 0 } else { pl };
984 let right_pad = if span_end >= col_count && !self.pad_edge {
985 0
986 } else {
987 pr
988 };
989 let content_width = span_width.saturating_sub(left_pad + right_pad);
990
991 let col_def = self.columns.get(col);
992 let justify = col_def.map(|c| c.justify).unwrap_or(AlignMethod::Left);
993
994 let disp_text = justify.align_text(&cell.content, content_width);
996 let disp_trunc = if UnicodeWidthStr::width(disp_text.as_str()) > content_width {
997 let mut truncated: String = disp_text
998 .chars()
999 .take(content_width.saturating_sub(1))
1000 .collect();
1001 truncated.push('…');
1002 truncated
1003 } else {
1004 disp_text
1005 };
1006
1007 line.push(Segment::new(" ".repeat(left_pad)));
1009
1010 if let Some(ref cell_style) = cell.style {
1012 let ansi = cell_style.to_ansi();
1013 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
1014 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
1015 } else if is_header {
1016 if let Some(hs) = col_def.map(|c| &c.header_style) {
1017 let ansi = hs.to_ansi();
1018 let reset = hs.reset_ansi();
1019 line.push(Segment::new(format!("{ansi}{disp_trunc}{reset}")));
1020 } else {
1021 line.push(Segment::new(disp_trunc));
1022 }
1023 } else {
1024 let col_ansi = col_def.map(|c| c.style.to_ansi()).unwrap_or_default();
1026 if col_ansi.is_empty() {
1027 line.push(Segment::new(disp_trunc));
1028 } else {
1029 line.push(Segment::new(format!("{col_ansi}{disp_trunc}\x1b[0m")));
1030 }
1031 }
1032
1033 line.push(Segment::new(" ".repeat(right_pad)));
1035
1036 if cell.rowspan > 1 {
1038 for item in &mut rowspan_remaining[col..span_end] {
1039 *item = cell.rowspan - 1;
1040 }
1041 }
1042
1043 col = span_end;
1044
1045 if col < col_count {
1047 line.push(bs(b.mid_vertical));
1048 }
1049 }
1050
1051 line.push(bs(b.mid_right));
1052 line.push(Segment::line());
1053 line
1054 }
1055
1056 fn compute_span_widths(cells: &[Cell], col_widths: &[usize]) -> Vec<usize> {
1060 let col_count = col_widths.len();
1061 if col_count == 0 {
1062 return vec![];
1063 }
1064
1065 let mut widths = Vec::new();
1066 let mut col = 0usize;
1067 for cell in cells {
1068 if col >= col_count {
1069 break;
1070 }
1071 let span = cell.colspan.min(col_count - col);
1072 let w: usize =
1074 col_widths[col..col + span].iter().sum::<usize>() + span.saturating_sub(1);
1075 widths.push(w);
1076 col += span;
1077 }
1078 while col < col_count {
1080 widths.push(col_widths[col]);
1081 col += 1;
1082 }
1083 widths
1084 }
1085
1086 fn compute_bottom_widths(&self, col_widths: &[usize]) -> Vec<usize> {
1089 if self.rows.is_empty() {
1090 return col_widths.to_vec();
1091 }
1092 Self::compute_span_widths(&self.rows[self.rows.len() - 1], col_widths)
1093 }
1094
1095 fn render_row_line(
1096 &self,
1097 widths: &[usize],
1098 _values: &[String],
1099 b: &BoxStyle,
1100 _available_width: usize,
1101 _is_header: bool,
1102 ) -> Vec<Segment> {
1103 let mut line = Vec::new();
1104 let col_count = widths.len();
1105 let bs = |ch: char| -> Segment {
1106 let ansi = self.border_style.to_ansi();
1107 let reset = if ansi.is_empty() { "" } else { "\x1b[0m" };
1108 Segment::new(format!("{ansi}{ch}{reset}"))
1109 };
1110
1111 line.push(bs(b.mid_left));
1114 for (i, w) in widths.iter().enumerate() {
1115 let (_pt, pr, _pb, pl) = self.padding;
1116 let left_pad = if i == 0 && !self.pad_edge { 0 } else { pl };
1117 let right_pad = if i == col_count - 1 && !self.pad_edge {
1118 0
1119 } else {
1120 pr
1121 };
1122 line.push(Segment::new(" ".repeat(left_pad + w + right_pad)));
1123 if i < col_count - 1 {
1124 line.push(bs(b.mid_vertical));
1125 }
1126 }
1127 line.push(bs(b.mid_right));
1128 line.push(Segment::line());
1129 line
1130 }
1131}
1132
1133impl Default for Table {
1134 fn default() -> Self {
1135 Self::new()
1136 }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142
1143 #[test]
1144 fn test_empty_table() {
1145 let table = Table::new();
1146 let opts = ConsoleOptions::default();
1147 let result = table.render(&opts);
1148 assert!(result.lines.is_empty());
1149 }
1150
1151 #[test]
1152 fn test_table_with_one_column() {
1153 let mut table = Table::new();
1154 table.add_column(Column::new("Name"));
1155 table.add_row_str(vec!["Alice".into()]);
1156 table.add_row_str(vec!["Bob".into()]);
1157
1158 let opts = ConsoleOptions::default();
1159 let result = table.render(&opts);
1160 let ansi = result.to_ansi();
1161 assert!(ansi.contains("Name"));
1162 assert!(ansi.contains("Alice"));
1163 }
1164
1165 #[test]
1166 fn test_cell_creation() {
1167 let cell = Cell::new("hello");
1168 assert_eq!(cell.content, "hello");
1169 assert_eq!(cell.colspan, 1);
1170 assert_eq!(cell.rowspan, 1);
1171 assert!(cell.style.is_none());
1172
1173 let cell2 = Cell::new("world").colspan(2).rowspan(3);
1174 assert_eq!(cell2.content, "world");
1175 assert_eq!(cell2.colspan, 2);
1176 assert_eq!(cell2.rowspan, 3);
1177 }
1178
1179 #[test]
1180 fn test_cell_from_string() {
1181 let cell: Cell = "test".into();
1182 assert_eq!(cell.content, "test");
1183 }
1184
1185 #[test]
1186 fn test_column_colspan() {
1187 let col = Column::new("Header");
1188 assert_eq!(col.colspan, 1);
1189 }
1190
1191 #[test]
1192 fn test_add_row_str() {
1193 let mut table = Table::new();
1194 table.add_column(Column::new("A"));
1195 table.add_column(Column::new("B"));
1196 table.add_row_str(vec!["x".into(), "y".into()]);
1197 assert_eq!(table.row_count(), 1);
1198 }
1199
1200 #[test]
1201 fn test_add_section() {
1202 let mut table = Table::new();
1203 table.add_column(Column::new("A"));
1204 table.add_row_str(vec!["r1".into()]);
1205 table.add_section();
1206 table.add_row_str(vec!["r2".into()]);
1207 assert_eq!(table.row_count(), 2);
1208 assert!(table.section_rows.contains(&1));
1209
1210 let opts = ConsoleOptions::default();
1211 let result = table.render(&opts);
1212 let ansi = result.to_ansi();
1213 assert!(ansi.contains("r1"));
1214 assert!(ansi.contains("r2"));
1215 }
1216
1217 #[test]
1218 fn test_leading() {
1219 let table = Table::new()
1220 .column(Column::new("X"))
1221 .row_str(vec!["a".into()])
1222 .row_str(vec!["b".into()])
1223 .leading(1);
1224 assert_eq!(table.leading, 1);
1225 }
1226
1227 #[test]
1228 fn test_cell_rowspan() {
1229 let mut table = Table::new();
1230 table.add_column(Column::new("A"));
1231 table.add_column(Column::new("B"));
1232 let cell_a = Cell::new("span").rowspan(2);
1233 let cell_b = Cell::new("single");
1234 table.add_row(vec![cell_a, cell_b]);
1235 table.add_row_str(vec!["row2col2".into()]);
1236
1237 let opts = ConsoleOptions::default();
1238 let result = table.render(&opts);
1239 let ansi = result.to_ansi();
1240 assert!(ansi.contains("span"));
1241 }
1242
1243 #[test]
1244 fn test_cell_colspan() {
1245 let mut table = Table::new();
1246 table.add_column(Column::new("A"));
1247 table.add_column(Column::new("B"));
1248 table.add_column(Column::new("C"));
1249 let cell = Cell::new("wide").colspan(2);
1250 table.add_row(vec![cell, Cell::new("c")]);
1251 table.add_row_str(vec!["a".into(), "b".into(), "c".into()]);
1252
1253 let opts = ConsoleOptions::default();
1254 let result = table.render(&opts);
1255 let ansi = result.to_ansi();
1256 assert!(ansi.contains("wide"));
1257 }
1258
1259 #[test]
1262 fn test_row_struct() {
1263 let cells = vec![Cell::new("a"), Cell::new("b")];
1264 let row = Row::new(cells)
1265 .style(Style::new().bold(true))
1266 .end_section(true);
1267 assert_eq!(row.cells.len(), 2);
1268 assert!(row.style.is_some());
1269 assert!(row.end_section);
1270 }
1271
1272 #[test]
1273 fn test_add_row_explicit() {
1274 let mut table = Table::new();
1275 table.add_column(Column::new("A"));
1276 table.add_column(Column::new("B"));
1277 let row = Row::new(vec![Cell::new("x"), Cell::new("y")]);
1278 table.add_row_explicit(row);
1279 assert_eq!(table.row_count(), 1);
1280
1281 let opts = ConsoleOptions::default();
1282 let result = table.render(&opts);
1283 let ansi = result.to_ansi();
1284 assert!(ansi.contains("x"));
1285 assert!(ansi.contains("y"));
1286 }
1287
1288 #[test]
1289 fn test_add_row_explicit_with_section() {
1290 let mut table = Table::new();
1291 table.add_column(Column::new("A"));
1292 table.add_row_str(vec!["before".into()]);
1293 let row = Row::new(vec![Cell::new("after")]).end_section(true);
1294 table.add_row_explicit(row);
1295 assert!(table.section_rows.contains(&1));
1296 }
1297
1298 #[test]
1299 fn test_builder_highlight() {
1300 let table = Table::new().highlight(true);
1301 assert!(table.highlight);
1302 }
1303
1304 #[test]
1305 fn test_builder_title_justify() {
1306 let table = Table::new().title_justify(AlignMethod::Right);
1307 assert_eq!(table.title_justify, AlignMethod::Right);
1308 }
1309
1310 #[test]
1311 fn test_builder_caption_justify() {
1312 let table = Table::new().caption_justify(AlignMethod::Left);
1313 assert_eq!(table.caption_justify, AlignMethod::Left);
1314 }
1315
1316 #[test]
1317 fn test_builder_row_styles() {
1318 let s1 = Style::new().bold(true);
1319 let s2 = Style::new().dim(true);
1320 let table = Table::new().row_styles(vec![s1.clone(), s2.clone()]);
1321 assert_eq!(table.row_styles.len(), 2);
1322 }
1323
1324 #[test]
1325 fn test_builder_show_edge() {
1326 let table = Table::new().show_edge(false);
1327 assert!(!table.show_edge);
1328 }
1329
1330 #[test]
1331 fn test_builder_collapse_padding() {
1332 let table = Table::new().collapse_padding(true);
1333 assert!(table.collapse_padding);
1334 }
1335
1336 #[test]
1337 fn test_builder_pad_edge() {
1338 let table = Table::new().pad_edge(false);
1339 assert!(!table.pad_edge);
1340 }
1341
1342 #[test]
1343 fn test_get_row_style_empty() {
1344 let table = Table::new();
1345 assert_eq!(table.get_row_style(0), None);
1346 }
1347
1348 #[test]
1349 fn test_get_row_style_with_styles() {
1350 let s1 = Style::new().bold(true);
1351 let s2 = Style::new().dim(true);
1352 let table = Table::new().row_styles(vec![s1, s2]);
1353 assert!(table.get_row_style(0).is_some());
1354 assert!(table.get_row_style(1).is_some());
1355 assert!(table.get_row_style(2).is_some());
1357 assert!(table.get_row_style(3).is_some());
1358 }
1359
1360 #[test]
1361 fn test_add_section_returns_self() {
1362 let mut table = Table::new();
1363 table.add_column(Column::new("A"));
1364 table.add_row_str(vec!["r1".into()]);
1365 let ret = table.add_section();
1366 ret.add_row_str(vec!["r2".into()]);
1368 assert_eq!(table.row_count(), 2);
1369 }
1370
1371 #[test]
1372 fn test_sections_field() {
1373 let mut table = Table::new();
1374 table.add_column(Column::new("A"));
1375 table.add_row_str(vec!["r1".into()]);
1376 table.add_section();
1377 table.add_row_str(vec!["r2".into()]);
1378 assert_eq!(table.sections.len(), 1);
1379 assert_eq!(table.sections[0], 1);
1380 }
1381
1382 #[test]
1383 fn test_pad_edge_default() {
1384 let table = Table::new();
1385 assert!(table.pad_edge);
1386 }
1387
1388 #[test]
1389 fn test_grid_method() {
1390 let table = Table::grid();
1391 assert!(!table.show_edge);
1392 assert!(!table.show_header);
1393 }
1394}