1#[cfg(feature = "style")]
17pub use crate::color::{Color, CustomColor};
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use terminal_size::{Width, terminal_size};
21
22#[cfg(feature = "style")]
23mod style;
24mod text;
25
26#[cfg(feature = "style")]
27use style::{StyleAction, apply_style_actions, impl_style_methods};
28use text::{layout_line, split_lines, strip_ansi, truncate_line, visible_len};
29
30const ANSI_RESET: &str = "\x1b[0m";
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub struct TableStyle {
41 pub top_left: &'static str,
43 pub top_right: &'static str,
45 pub bottom_left: &'static str,
47 pub bottom_right: &'static str,
49 pub horiz: &'static str,
51 pub vert: &'static str,
53 pub top_joint: &'static str,
55 pub mid_left: &'static str,
57 pub mid_right: &'static str,
59 pub mid_joint: &'static str,
61 pub bottom_joint: &'static str,
63}
64
65impl TableStyle {
66 pub fn unicode() -> Self {
68 TableStyle {
69 top_left: "┌",
70 top_right: "┐",
71 bottom_left: "└",
72 bottom_right: "┘",
73 horiz: "─",
74 vert: "│",
75 top_joint: "┬",
76 mid_left: "├",
77 mid_right: "┤",
78 mid_joint: "┼",
79 bottom_joint: "┴",
80 }
81 }
82
83 pub fn from_section_style(section_style: SectionStyle) -> Self {
85 TableStyle {
86 horiz: section_style.horiz,
87 mid_left: section_style.mid_left,
88 mid_right: section_style.mid_right,
89 mid_joint: section_style.mid_joint,
90 ..TableStyle::unicode()
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub struct SectionStyle {
98 pub horiz: &'static str,
100 pub mid_left: &'static str,
102 pub mid_right: &'static str,
104 pub mid_joint: &'static str,
106}
107
108impl SectionStyle {
109 pub fn unicode() -> Self {
111 SectionStyle::from_table_style(TableStyle::unicode())
112 }
113
114 pub fn from_table_style(table_style: TableStyle) -> Self {
116 SectionStyle {
117 horiz: table_style.horiz,
118 mid_left: table_style.mid_left,
119 mid_right: table_style.mid_right,
120 mid_joint: table_style.mid_joint,
121 }
122 }
123}
124
125#[allow(dead_code)]
139#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
140pub enum Trunc {
141 #[default]
143 End,
144 Start,
146 Middle,
148 NewLine,
150}
151
152#[allow(dead_code)]
163#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
164pub enum Align {
165 #[default]
167 Center,
168 Left,
170 Right,
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Default)]
192pub enum ColumnWidth {
193 #[default]
195 Auto,
196 Fixed(usize),
198 Fraction(f64),
200 Fill,
202}
203
204impl ColumnWidth {
205 pub fn fixed(width: usize) -> Self {
207 Self::Fixed(width)
208 }
209
210 pub fn fraction(fraction: f64) -> Self {
212 Self::Fraction(fraction)
213 }
214
215 pub fn fill() -> Self {
217 Self::Fill
218 }
219}
220
221macro_rules! impl_column_width_from_int {
222 ($($ty:ty),* $(,)?) => {
223 $(impl From<$ty> for ColumnWidth {
224 fn from(width: $ty) -> Self {
225 Self::Fixed(width.max(0) as usize)
226 }
227 })*
228 };
229}
230
231impl_column_width_from_int!(
232 usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128
233);
234
235impl From<f32> for ColumnWidth {
236 fn from(fraction: f32) -> Self {
237 Self::Fraction(fraction as f64)
238 }
239}
240
241impl From<f64> for ColumnWidth {
242 fn from(fraction: f64) -> Self {
243 Self::Fraction(fraction)
244 }
245}
246
247#[derive(Clone, Debug, Eq, PartialEq, Hash)]
261pub enum ColumnTarget {
262 Index(usize),
264 Header(String),
266}
267
268impl From<usize> for ColumnTarget {
269 fn from(index: usize) -> Self {
270 Self::Index(index)
271 }
272}
273
274impl From<&str> for ColumnTarget {
275 fn from(header: &str) -> Self {
276 Self::Header(header.to_string())
277 }
278}
279
280impl From<String> for ColumnTarget {
281 fn from(header: String) -> Self {
282 Self::Header(header)
283 }
284}
285
286pub struct Column {
302 header: Cell,
303 style: ColumnStyle,
304}
305
306impl Column {
307 pub fn new(header: impl Into<Cell>) -> Self {
309 Self {
310 header: header.into(),
311 style: ColumnStyle::default(),
312 }
313 }
314
315 pub fn width(mut self, width: impl Into<ColumnWidth>) -> Self {
317 self.style.width = Some(width.into());
318 self
319 }
320
321 pub fn max_width(self, width: impl Into<ColumnWidth>) -> Self {
323 self.width(width)
324 }
325
326 pub fn truncate(mut self, truncation: Trunc) -> Self {
328 self.style.truncation = Some(truncation);
329 self
330 }
331
332 pub fn align(mut self, align: Align) -> Self {
334 self.style.align = Some(align);
335 self
336 }
337}
338
339#[cfg(feature = "style")]
340impl_style_methods!(Column, |mut column: Column, action| {
341 column.style.styles.push(action);
342 column
343});
344
345#[derive(Clone, Debug, Default)]
346struct ColumnStyle {
347 #[cfg(feature = "style")]
348 styles: Vec<StyleAction>,
349 width: Option<ColumnWidth>,
350 truncation: Option<Trunc>,
351 align: Option<Align>,
352}
353
354impl ColumnStyle {
355 fn merge(&mut self, other: &ColumnStyle) {
356 #[cfg(feature = "style")]
357 self.styles.extend_from_slice(&other.styles);
358
359 if other.width.is_some() {
360 self.width = other.width;
361 }
362
363 if other.truncation.is_some() {
364 self.truncation = other.truncation;
365 }
366
367 if other.align.is_some() {
368 self.align = other.align;
369 }
370 }
371}
372
373pub struct Cell {
389 content: String,
390 #[cfg(feature = "style")]
391 styles: Vec<StyleAction>,
392 truncation: Option<Trunc>,
393 align: Option<Align>,
394}
395
396struct PreparedCell {
397 lines: Vec<String>,
398 align: Align,
399}
400
401enum TableRow {
402 Cells(Vec<Cell>),
403 Section(SectionRow),
404}
405
406enum PreparedRow {
407 Cells(Vec<PreparedCell>),
408 Section(SectionRow),
409}
410
411#[derive(Clone, Debug)]
412struct SectionRow {
413 title: String,
414 align: Align,
415 style: TableStyle,
416}
417
418pub struct SectionBuilder<'a> {
420 table: &'a mut Table,
421 row_index: usize,
422}
423
424pub struct ColumnBuilder<'a> {
429 table: &'a mut Table,
430 target: ColumnTarget,
431}
432
433impl Cell {
434 pub fn new(content: impl ToString) -> Self {
436 Self {
437 content: content.to_string(),
438 #[cfg(feature = "style")]
439 styles: Vec::new(),
440 truncation: None,
441 align: None,
442 }
443 }
444
445 #[must_use]
447 pub fn truncate(mut self, truncation: Trunc) -> Self {
448 self.truncation = Some(truncation);
449 self
450 }
451
452 #[must_use]
454 pub fn align(mut self, align: Align) -> Self {
455 self.align = Some(align);
456 self
457 }
458}
459
460#[cfg(feature = "style")]
461impl_style_methods!(Cell, |mut cell: Cell, action| {
462 cell.styles.push(action);
463 cell
464});
465
466impl From<&str> for Cell {
467 fn from(content: &str) -> Self {
468 Self::new(content)
469 }
470}
471
472impl From<String> for Cell {
473 fn from(content: String) -> Self {
474 Self::new(content)
475 }
476}
477
478pub struct Table {
514 headers: Vec<Cell>,
515 rows: Vec<TableRow>,
516 column_defaults: Vec<ColumnStyle>,
517 column_overrides: HashMap<ColumnTarget, ColumnStyle>,
518 style: TableStyle,
519 section_style: Option<SectionStyle>,
520 separator_style: Option<SectionStyle>,
521}
522
523impl Table {
524 pub fn new() -> Self {
526 Self {
527 headers: Vec::new(),
528 rows: Vec::new(),
529 column_defaults: Vec::new(),
530 column_overrides: HashMap::new(),
531 style: TableStyle::unicode(),
532 section_style: None,
533 separator_style: None,
534 }
535 }
536
537 pub fn with_columns(columns: impl IntoIterator<Item = Column>) -> Self {
542 let mut table = Self::new();
543 table.set_columns(columns);
544 table
545 }
546
547 pub fn with_style(mut self, style: TableStyle) -> Self {
549 self.style = style;
550 self
551 }
552
553 pub fn with_section_style(mut self, style: SectionStyle) -> Self {
558 self.section_style = Some(style);
559 self
560 }
561
562 pub fn with_separator_style(mut self, style: SectionStyle) -> Self {
567 self.separator_style = Some(style);
568 self
569 }
570
571 pub fn set_columns(&mut self, columns: impl IntoIterator<Item = Column>) {
576 let (headers, column_defaults): (Vec<_>, Vec<_>) = columns
577 .into_iter()
578 .map(|Column { header, style }| (header, style))
579 .unzip();
580
581 self.headers = headers;
582 self.column_defaults = column_defaults;
583 self.column_overrides.clear();
584 }
585
586 pub fn add_row(&mut self, row: Vec<Cell>) {
591 self.rows.push(TableRow::Cells(row));
592 }
593
594 pub fn add_section(&mut self, title: impl ToString) -> SectionBuilder<'_> {
603 let row_index = self.rows.len();
604 let style = match self.section_style {
605 Some(s) => TableStyle::from_section_style(s),
606 None => self.style,
607 };
608 self.rows.push(TableRow::Section(SectionRow {
609 title: title.to_string(),
610 align: Align::Center,
611 style,
612 }));
613
614 SectionBuilder {
615 table: self,
616 row_index,
617 }
618 }
619
620 pub fn add_separator(&mut self) -> SectionBuilder<'_> {
625 let row_index = self.rows.len();
626 let style = match self.separator_style {
627 Some(s) => TableStyle::from_section_style(s),
628 None => self.style,
629 };
630 self.rows.push(TableRow::Section(SectionRow {
631 title: String::new(),
632 align: Align::Center,
633 style,
634 }));
635
636 SectionBuilder {
637 table: self,
638 row_index,
639 }
640 }
641
642 pub fn column<T: Into<ColumnTarget>>(&mut self, target: T) -> ColumnBuilder<'_> {
644 ColumnBuilder {
645 table: self,
646 target: target.into(),
647 }
648 }
649
650 #[allow(dead_code)]
652 pub fn print(&self) {
653 for line in self.render_lines() {
654 println!("{line}");
655 }
656 }
657
658 pub fn render(&self) -> String {
664 self.render_lines().join("\n")
665 }
666
667 fn column_style_mut(&mut self, target: ColumnTarget) -> &mut ColumnStyle {
668 self.column_overrides.entry(target).or_default()
669 }
670
671 fn column_style(&self, col: usize) -> ColumnStyle {
672 let mut style = self.column_defaults.get(col).cloned().unwrap_or_default();
673
674 if let Some(header) = self.headers.get(col)
675 && let Some(header_style) = self
676 .column_overrides
677 .get(&ColumnTarget::Header(strip_ansi(&header.content)))
678 {
679 style.merge(header_style);
680 }
681
682 if let Some(index_style) = self.column_overrides.get(&ColumnTarget::Index(col)) {
683 style.merge(index_style);
684 }
685
686 style
687 }
688
689 fn prepare_cell(
690 &self,
691 cell: Option<&Cell>,
692 column_style: &ColumnStyle,
693 width: usize,
694 #[cfg_attr(not(feature = "style"), allow(unused_variables))] is_header: bool,
695 ) -> PreparedCell {
696 let raw = cell.map(|c| c.content.as_str()).unwrap_or("");
697 let truncation = cell
698 .and_then(|c| c.truncation)
699 .or(column_style.truncation)
700 .unwrap_or(Trunc::End);
701 let align = cell
702 .and_then(|c| c.align)
703 .or(column_style.align)
704 .unwrap_or(Align::Left);
705
706 #[cfg(feature = "style")]
707 let styled = {
708 let mut all_styles = column_style.styles.clone();
711 if let Some(cell) = cell {
712 all_styles.extend_from_slice(&cell.styles);
713 }
714 if is_header {
715 all_styles.push(StyleAction::Bold);
716 }
717 apply_style_actions(raw, &all_styles)
718 };
719 #[cfg(not(feature = "style"))]
720 let styled = raw.to_string();
721
722 let lines = split_lines(&styled)
723 .into_iter()
724 .flat_map(|line| layout_line(&line, Some(width), truncation))
725 .collect();
726
727 PreparedCell { lines, align }
728 }
729
730 fn prepare_row(
731 &self,
732 row: &[Cell],
733 col_widths: &[usize],
734 column_styles: &[ColumnStyle],
735 is_header: bool,
736 ) -> Vec<PreparedCell> {
737 col_widths
738 .iter()
739 .zip(column_styles)
740 .enumerate()
741 .map(|(col, (&width, style))| self.prepare_cell(row.get(col), style, width, is_header))
742 .collect()
743 }
744
745 fn collect_content_widths(&self, col_count: usize) -> Vec<usize> {
746 let mut widths = vec![0usize; col_count];
747
748 let all_rows =
749 std::iter::once(self.headers.as_slice()).chain(self.rows.iter().filter_map(|row| {
750 match row {
751 TableRow::Cells(cells) => Some(cells.as_slice()),
752 TableRow::Section(_) => None,
753 }
754 }));
755
756 for row in all_rows {
757 for (col, cell) in row.iter().enumerate() {
758 for line in split_lines(&cell.content) {
759 widths[col] = widths[col].max(visible_len(&line));
760 }
761 }
762 }
763
764 widths
765 }
766
767 fn resolve_column_widths(
768 &self,
769 content_widths: &[usize],
770 column_styles: &[ColumnStyle],
771 terminal_width: Option<usize>,
772 ) -> Vec<usize> {
773 let mut widths = content_widths.to_vec();
774 let mut fraction_columns = Vec::new();
775 let mut fill_columns = Vec::new();
776 let mut reserved_width = 0usize;
777
778 for (col, style) in column_styles.iter().enumerate() {
779 match style.width.unwrap_or_default() {
780 ColumnWidth::Auto => {
781 reserved_width += widths[col];
782 }
783 ColumnWidth::Fixed(width) => {
784 widths[col] = width;
785 reserved_width += width;
786 }
787 ColumnWidth::Fraction(fraction) => {
788 fraction_columns.push((col, fraction.max(0.0)));
789 }
790 ColumnWidth::Fill => {
791 fill_columns.push(col);
792 }
793 }
794 }
795
796 let Some(terminal_width) = terminal_width else {
797 return widths;
798 };
799
800 let table_overhead = (3 * widths.len()) + 1;
801 let available_content_width = terminal_width.saturating_sub(table_overhead);
802 let remaining_width = available_content_width.saturating_sub(reserved_width);
803
804 if fraction_columns.is_empty() && fill_columns.is_empty() {
805 return widths;
806 }
807
808 let mut leftover = remaining_width;
809
810 if !fraction_columns.is_empty() {
811 let total_fraction: f64 = fraction_columns.iter().map(|(_, fraction)| *fraction).sum();
812 if total_fraction <= f64::EPSILON {
813 for (col, _) in fraction_columns {
814 widths[col] = 0;
815 }
816 } else {
817 let fraction_budget = remaining_width;
818
819 let mut remainders = Vec::with_capacity(fraction_columns.len());
820 let mut assigned = 0usize;
821
822 for (col, fraction) in fraction_columns {
823 let exact = if total_fraction <= 1.0 {
824 (fraction_budget as f64) * fraction
825 } else {
826 (fraction_budget as f64) * fraction / total_fraction
827 };
828 let width = exact.floor() as usize;
829 widths[col] = width;
830 assigned += width;
831 remainders.push((col, exact - width as f64));
832 }
833
834 leftover = leftover.saturating_sub(assigned);
835
836 if fill_columns.is_empty() {
837 remainders.sort_by(|left, right| {
838 right.1.partial_cmp(&left.1).unwrap_or(Ordering::Equal)
839 });
840
841 for (col, _) in remainders {
842 if leftover == 0 {
843 break;
844 }
845
846 widths[col] += 1;
847 leftover -= 1;
848 }
849 }
850 }
851 }
852
853 if !fill_columns.is_empty() {
854 let fill_count = fill_columns.len();
855 let fill_width = leftover / fill_count;
856 let mut fill_remainder = leftover % fill_count;
857
858 for col in fill_columns {
859 widths[col] = fill_width + usize::from(fill_remainder > 0);
860 fill_remainder = fill_remainder.saturating_sub(1);
861 }
862 }
863
864 widths
865 }
866
867 fn column_count(&self) -> usize {
868 let max_row_len = self
869 .rows
870 .iter()
871 .filter_map(|row| match row {
872 TableRow::Cells(cells) => Some(cells.len()),
873 TableRow::Section(_) => None,
874 })
875 .max()
876 .unwrap_or(0);
877
878 self.headers.len().max(max_row_len)
879 }
880
881 fn row_height(cells: &[PreparedCell]) -> usize {
882 cells.iter().map(|cell| cell.lines.len()).max().unwrap_or(1)
883 }
884
885 fn rule_line(
886 &self,
887 style: &TableStyle,
888 left: &str,
889 joint: &str,
890 right: &str,
891 col_widths: &[usize],
892 ) -> String {
893 let h = style.horiz;
894 let join = format!("{}{}{}", h, joint, h);
895 let inner = col_widths
896 .iter()
897 .map(|&width| h.repeat(width))
898 .collect::<Vec<_>>()
899 .join(&join);
900
901 format!("{}{}{}{}{}", left, h, inner, h, right)
902 }
903
904 fn push_row_lines(
905 &self,
906 lines: &mut Vec<String>,
907 cells: &[PreparedCell],
908 col_widths: &[usize],
909 ) {
910 for line_idx in 0..Self::row_height(cells) {
911 lines.push(self.render_row_line(cells, line_idx, col_widths));
912 }
913 }
914
915 fn render_row_line(
916 &self,
917 row: &[PreparedCell],
918 line_idx: usize,
919 col_widths: &[usize],
920 ) -> String {
921 let vertical = self.style.vert;
922 let rendered_cells: Vec<String> = row
923 .iter()
924 .enumerate()
925 .map(|(col, cell)| {
926 let raw = cell.lines.get(line_idx).map(String::as_str).unwrap_or("");
927 let padding = col_widths[col].saturating_sub(visible_len(raw));
928 match cell.align {
929 Align::Left => format!("{}{}", raw, " ".repeat(padding)),
930 Align::Right => format!("{}{}", " ".repeat(padding), raw),
931 Align::Center => {
932 let left_pad = padding / 2;
933 let right_pad = padding - left_pad;
934 format!("{}{}{}", " ".repeat(left_pad), raw, " ".repeat(right_pad))
935 }
936 }
937 })
938 .collect();
939
940 format!(
941 "{} {} {}",
942 vertical,
943 rendered_cells.join(&format!(" {} ", vertical)),
944 vertical
945 )
946 }
947
948 fn render_section_line(&self, section: &SectionRow, col_widths: &[usize]) -> String {
949 let style = §ion.style;
950
951 if section.title.trim().is_empty() {
952 return self.rule_line(
953 style,
954 style.mid_left,
955 style.mid_joint,
956 style.mid_right,
957 col_widths,
958 );
959 }
960
961 let total_inner = col_widths.iter().sum::<usize>() + 3 * col_widths.len() - 1;
962 let label = truncate_line(
963 &format!(" {} ", section.title),
964 Some(total_inner),
965 Trunc::End,
966 );
967 let label_len = label.chars().count();
968 let remaining = total_inner.saturating_sub(label_len);
969
970 let left_fill = match section.align {
971 Align::Left => 1,
972 Align::Center => remaining / 2,
973 Align::Right => remaining.saturating_sub(1),
974 };
975
976 let mut inner: Vec<char> = style.horiz.repeat(total_inner).chars().collect();
977 let joint = style.mid_joint.chars().next().unwrap_or('┼');
978 let mut cursor = 1;
979
980 for &w in col_widths.iter().take(col_widths.len().saturating_sub(1)) {
981 cursor += w + 1;
982 if cursor < inner.len() {
983 inner[cursor] = joint;
984 }
985 cursor += 2;
986 }
987
988 let prefix: String = inner[..left_fill].iter().collect();
989 let suffix: String = inner[left_fill + label_len..].iter().collect();
990
991 #[cfg(feature = "style")]
992 let bold_label = format!("\x1b[1m{}{}", label, ANSI_RESET);
993 #[cfg(not(feature = "style"))]
994 let bold_label = label;
995
996 format!(
997 "{}{}{}{}{}",
998 style.mid_left, prefix, bold_label, suffix, style.mid_right
999 )
1000 }
1001
1002 fn render_lines_with_terminal_width(&self, terminal_width: Option<usize>) -> Vec<String> {
1003 let col_count = self.column_count();
1004 if col_count == 0 {
1005 return Vec::new();
1006 }
1007
1008 let column_styles: Vec<ColumnStyle> =
1009 (0..col_count).map(|col| self.column_style(col)).collect();
1010 let content_widths = self.collect_content_widths(col_count);
1011 let col_widths =
1012 self.resolve_column_widths(&content_widths, &column_styles, terminal_width);
1013
1014 let prepared_header = (!self.headers.is_empty())
1015 .then(|| self.prepare_row(&self.headers, &col_widths, &column_styles, true));
1016 let prepared_rows: Vec<PreparedRow> = self
1017 .rows
1018 .iter()
1019 .map(|row| match row {
1020 TableRow::Cells(cells) => {
1021 PreparedRow::Cells(self.prepare_row(cells, &col_widths, &column_styles, false))
1022 }
1023 TableRow::Section(section) => PreparedRow::Section(section.clone()),
1024 })
1025 .collect();
1026
1027 let mut lines = Vec::new();
1028
1029 lines.push(self.rule_line(
1030 &self.style,
1031 self.style.top_left,
1032 self.style.top_joint,
1033 self.style.top_right,
1034 &col_widths,
1035 ));
1036
1037 if let Some(header) = prepared_header.as_ref() {
1038 self.push_row_lines(&mut lines, header, &col_widths);
1039
1040 if prepared_rows.is_empty()
1041 || !matches!(prepared_rows.first(), Some(PreparedRow::Section(_)))
1042 {
1043 lines.push(self.rule_line(
1044 &self.style,
1045 self.style.mid_left,
1046 self.style.mid_joint,
1047 self.style.mid_right,
1048 &col_widths,
1049 ));
1050 }
1051 }
1052
1053 for row in &prepared_rows {
1054 match row {
1055 PreparedRow::Cells(cells) => self.push_row_lines(&mut lines, cells, &col_widths),
1056 PreparedRow::Section(section) => {
1057 lines.push(self.render_section_line(section, &col_widths))
1058 }
1059 }
1060 }
1061
1062 lines.push(self.rule_line(
1063 &self.style,
1064 self.style.bottom_left,
1065 self.style.bottom_joint,
1066 self.style.bottom_right,
1067 &col_widths,
1068 ));
1069
1070 lines
1071 }
1072
1073 fn render_lines(&self) -> Vec<String> {
1074 self.render_lines_with_terminal_width(
1075 terminal_size().map(|(Width(width), _)| width as usize),
1076 )
1077 }
1078}
1079
1080impl Default for Table {
1081 fn default() -> Self {
1082 Self::new()
1083 }
1084}
1085
1086impl std::fmt::Display for Table {
1087 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1088 for line in self.render_lines() {
1089 writeln!(f, "{line}")?;
1090 }
1091 Ok(())
1092 }
1093}
1094
1095impl<'a> SectionBuilder<'a> {
1096 pub fn align(self, align: Align) -> Self {
1098 if let Some(TableRow::Section(section)) = self.table.rows.get_mut(self.row_index) {
1099 section.align = align;
1100 }
1101
1102 self
1103 }
1104
1105 pub fn style(self, style: SectionStyle) -> Self {
1107 if let Some(TableRow::Section(section)) = self.table.rows.get_mut(self.row_index) {
1108 section.style = TableStyle::from_section_style(style);
1109 }
1110
1111 self
1112 }
1113}
1114
1115impl<'a> ColumnBuilder<'a> {
1116 #[cfg(feature = "style")]
1118 pub fn color(self, color: Color) -> Self {
1119 self.table
1120 .column_style_mut(self.target.clone())
1121 .styles
1122 .push(StyleAction::Color(color));
1123 self
1124 }
1125
1126 pub fn width(self, width: impl Into<ColumnWidth>) -> Self {
1128 self.table.column_style_mut(self.target.clone()).width = Some(width.into());
1129 self
1130 }
1131
1132 pub fn max_width(self, width: impl Into<ColumnWidth>) -> Self {
1134 self.width(width)
1135 }
1136
1137 pub fn truncate(self, truncation: Trunc) -> Self {
1139 self.table.column_style_mut(self.target.clone()).truncation = Some(truncation);
1140 self
1141 }
1142
1143 pub fn align(self, align: Align) -> Self {
1145 self.table.column_style_mut(self.target.clone()).align = Some(align);
1146 self
1147 }
1148}
1149
1150#[cfg(test)]
1151mod tests {
1152 use super::text::strip_ansi;
1153 use super::*;
1154 #[cfg(feature = "style")]
1155 use crate::Color::BrightBlack;
1156
1157 impl Table {
1158 fn render_lines_for_test(&self, terminal_width: Option<usize>) -> Vec<String> {
1159 self.render_lines_with_terminal_width(terminal_width)
1160 }
1161 }
1162
1163 #[cfg(feature = "style")]
1164 #[test]
1165 fn cell_builders_are_chainable() {
1166 let cell = Cell::new("value")
1167 .color(BrightBlack)
1168 .truncate(Trunc::Middle);
1169
1170 assert!(matches!(
1171 cell.styles.as_slice(),
1172 [StyleAction::Color(BrightBlack)]
1173 ));
1174 assert_eq!(cell.truncation, Some(Trunc::Middle));
1175 }
1176
1177 #[cfg(feature = "style")]
1178 #[test]
1179 fn accepts_colorize_values_for_cells_and_headers() {
1180 let mut table = Table::with_columns(vec![
1181 Column::new("Status").bright_green().bold(),
1182 Column::new("Notes"),
1183 ]);
1184
1185 table.column("Status").width(10);
1186 table.add_row(vec![
1187 Cell::new("DefinitelyActive").bright_red().underline(),
1188 Cell::new("Ready"),
1189 ]);
1190
1191 let plain = plain_lines(&table);
1192
1193 assert!(plain[1].contains("Status"));
1194 assert!(plain[3].contains("Definitel…"));
1195 }
1196
1197 #[test]
1198 fn renders_multiline_headers_and_rows() {
1199 let mut table = Table::with_columns(vec![Column::new("Name\nAlias"), Column::new("Value")]);
1200 table.add_row(vec![Cell::new("alpha\nbeta"), Cell::new("1")]);
1201
1202 assert_eq!(
1203 plain_lines(&table),
1204 vec![
1205 "┌───────┬───────┐",
1206 "│ Name │ Value │",
1207 "│ Alias │ │",
1208 "├───────┼───────┤",
1209 "│ alpha │ 1 │",
1210 "│ beta │ │",
1211 "└───────┴───────┘",
1212 ]
1213 );
1214 }
1215
1216 #[test]
1217 fn renders_center_aligned_sections_inside_a_single_table() {
1218 assert_eq!(
1219 section_table_lines(Align::Center),
1220 expected_section_lines("├─── Alpha ────┤")
1221 );
1222 }
1223
1224 #[test]
1225 fn renders_left_aligned_sections_inside_a_single_table() {
1226 assert_eq!(
1227 section_table_lines(Align::Left),
1228 expected_section_lines("├─ Alpha ──────┤")
1229 );
1230 }
1231
1232 #[test]
1233 fn renders_right_aligned_sections_inside_a_single_table() {
1234 assert_eq!(
1235 section_table_lines(Align::Right),
1236 expected_section_lines("├────── Alpha ─┤")
1237 );
1238 }
1239
1240 #[test]
1241 fn renders_mid_joints_when_a_section_label_leaves_room() {
1242 let mut table =
1243 Table::with_columns(vec![Column::new("A"), Column::new("B"), Column::new("C")]);
1244 table.add_section("X");
1245 table.add_row(vec![Cell::new("1"), Cell::new("2"), Cell::new("3")]);
1246
1247 assert_eq!(
1248 plain_lines(&table),
1249 vec![
1250 "┌───┬───┬───┐",
1251 "│ A │ B │ C │",
1252 "├───┼ X ┼───┤",
1253 "│ 1 │ 2 │ 3 │",
1254 "└───┴───┴───┘",
1255 ]
1256 );
1257 }
1258
1259 #[test]
1260 fn sections_and_separators_can_use_their_own_styles() {
1261 let table_style = TableStyle {
1262 top_left: "╔",
1263 top_right: "╗",
1264 bottom_left: "╚",
1265 bottom_right: "╝",
1266 horiz: "═",
1267 vert: "║",
1268 top_joint: "╦",
1269 mid_left: "╠",
1270 mid_right: "╣",
1271 mid_joint: "╬",
1272 bottom_joint: "╩",
1273 };
1274 let section_style = SectionStyle::unicode();
1275 let separator_style = SectionStyle {
1276 horiz: "-",
1277 mid_left: "-",
1278 mid_right: "-",
1279 mid_joint: "-",
1280 };
1281
1282 let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")])
1283 .with_style(table_style);
1284
1285 table.add_section("Alpha").style(section_style);
1286 table.add_row(vec![Cell::new("a"), Cell::new("1")]);
1287 table.add_separator().style(separator_style);
1288 table.add_row(vec![Cell::new("b"), Cell::new("2")]);
1289
1290 let plain = plain_lines(&table);
1291
1292 assert!(plain[0].starts_with("╔"));
1293 assert!(plain[0].ends_with("╗"));
1294 assert_eq!(plain[2], "├─── Alpha ────┤");
1295 assert_eq!(plain[4], "----------------");
1296 assert!(plain[6].starts_with("╚"));
1297 assert!(plain[6].ends_with("╝"));
1298 }
1299
1300 #[test]
1301 fn applies_column_and_cell_truncation() {
1302 let mut table = Table::with_columns(vec![Column::new("Value"), Column::new("Other")]);
1303 table.column("Value").max_width(5).truncate(Trunc::Start);
1304 table.add_row(vec![Cell::new("abcdefghij"), Cell::new("z")]);
1305 table.add_row(vec![
1306 Cell::new("abcdefghij").truncate(Trunc::Middle),
1307 Cell::new("z"),
1308 ]);
1309
1310 assert_eq!(
1311 plain_lines(&table),
1312 vec![
1313 "┌───────┬───────┐",
1314 "│ Value │ Other │",
1315 "├───────┼───────┤",
1316 "│ …ghij │ z │",
1317 "│ ab…ij │ z │",
1318 "└───────┴───────┘",
1319 ]
1320 );
1321 }
1322
1323 #[cfg(feature = "style")]
1324 #[test]
1325 fn truncation_keeps_ellipsis_tight_and_colored() {
1326 let mut table = Table::with_columns(vec![Column::new("Name")]);
1327 table.column(0).max_width(14);
1328 table.add_row(vec![Cell::new("Cynthia \"CJ\" Lee").bright_red()]);
1329
1330 let rendered = table.render_lines_for_test(Some(40)).join("\n");
1331 let plain = strip_ansi(&rendered);
1332
1333 assert!(plain.contains("Cynthia \"CJ\"…"));
1334 assert!(!plain.contains("Cynthia \"CJ\" …"));
1335 assert!(rendered.contains("\x1b[91mCynthia \"CJ\"…\x1b[0m"));
1336 }
1337
1338 #[test]
1339 fn builds_columns_in_one_step() {
1340 let mut table = Table::with_columns(vec![
1341 Column::new("Name").width(0.3),
1342 Column::new("Age").width(0.15),
1343 Column::new("City").width(0.55),
1344 ]);
1345
1346 table.add_row(vec![
1347 Cell::new("Alice"),
1348 Cell::new("30"),
1349 Cell::new("New York"),
1350 ]);
1351
1352 let plain = table
1353 .render_lines_for_test(Some(40))
1354 .into_iter()
1355 .map(|line| strip_ansi(&line))
1356 .collect::<Vec<_>>();
1357
1358 assert_eq!(plain[0].chars().count(), 40);
1359 assert!(plain[1].contains("Name"));
1360 assert!(plain[1].contains("Age"));
1361 assert!(plain[1].contains("City"));
1362 assert!(plain[3].contains("Alice"));
1363 }
1364
1365 #[test]
1366 fn fill_columns_take_the_remainder_after_fractional_columns() {
1367 let mut table = Table::with_columns(vec![
1368 Column::new("Name").width(ColumnWidth::fill()),
1369 Column::new("Role").width(0.6),
1370 Column::new("Status").width(0.3),
1371 ]);
1372
1373 table.add_row(vec![
1374 Cell::new("Ada Lovelace"),
1375 Cell::new("Principal Engineer"),
1376 Cell::new("Active"),
1377 ]);
1378
1379 let plain = table
1380 .render_lines_for_test(Some(70))
1381 .into_iter()
1382 .map(|line| strip_ansi(&line))
1383 .collect::<Vec<_>>();
1384
1385 assert_eq!(plain[0].chars().count(), 70);
1386 assert_eq!(cell_widths(&plain[1]), vec![6, 36, 18]);
1387 assert_eq!(cell_widths(&plain[3]), vec![6, 36, 18]);
1388 assert!(plain[1].contains("Name"));
1389 assert!(plain[1].contains("Role"));
1390 assert!(plain[1].contains("Status"));
1391 }
1392
1393 fn cell_widths(line: &str) -> Vec<usize> {
1394 line.split('│')
1395 .filter(|segment| !segment.is_empty())
1396 .map(|segment| segment.chars().count().saturating_sub(2))
1397 .collect()
1398 }
1399
1400 #[test]
1401 fn renders_fractional_columns_against_terminal_width() {
1402 let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")]);
1403 table.column("Name").max_width(0.5);
1404 table.column("Value").max_width(0.5);
1405 table.add_row(vec![Cell::new("Alice"), Cell::new("123")]);
1406
1407 let lines = table.render_lines_for_test(Some(40));
1408 let plain = lines
1409 .iter()
1410 .map(|line| strip_ansi(line))
1411 .collect::<Vec<_>>();
1412
1413 assert_eq!(plain[0].chars().count(), 40);
1414 assert_eq!(plain.last().unwrap().chars().count(), 40);
1415 assert!(plain[1].contains("Name"));
1416 assert!(plain[3].contains("Alice"));
1417 }
1418
1419 #[test]
1420 fn newline_truncation_wraps_at_spaces_and_hard_breaks_when_needed() {
1421 let mut table = Table::with_columns(vec![Column::new("Value")]);
1422 table.column(0).max_width(8);
1423 table.add_row(vec![Cell::new("one two three").truncate(Trunc::NewLine)]);
1424 table.add_row(vec![Cell::new("abcdefghij").truncate(Trunc::NewLine)]);
1425
1426 assert_eq!(
1427 plain_lines(&table),
1428 vec![
1429 "┌──────────┐",
1430 "│ Value │",
1431 "├──────────┤",
1432 "│ one two │",
1433 "│ three │",
1434 "│ abcdefgh │",
1435 "│ ij │",
1436 "└──────────┘",
1437 ]
1438 );
1439 }
1440
1441 fn plain_lines(table: &Table) -> Vec<String> {
1442 table
1443 .render_lines()
1444 .into_iter()
1445 .map(|line| strip_ansi(&line))
1446 .collect()
1447 }
1448
1449 fn section_table_lines(align: Align) -> Vec<String> {
1450 let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")]);
1451 table.add_section("Alpha").align(align);
1452 table.add_row(vec![Cell::new("a"), Cell::new("1")]);
1453
1454 plain_lines(&table)
1455 }
1456
1457 fn expected_section_lines(section_line: &str) -> Vec<String> {
1458 vec![
1459 "┌──────┬───────┐".to_string(),
1460 "│ Name │ Value │".to_string(),
1461 section_line.to_string(),
1462 "│ a │ 1 │".to_string(),
1463 "└──────┴───────┘".to_string(),
1464 ]
1465 }
1466}