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