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)]
191pub enum ColumnWidth {
192 #[default]
194 Auto,
195 Fixed(usize),
197 Fraction(f64),
199}
200
201impl ColumnWidth {
202 pub fn fixed(width: usize) -> Self {
204 Self::Fixed(width)
205 }
206
207 pub fn fraction(fraction: f64) -> Self {
209 Self::Fraction(fraction)
210 }
211}
212
213macro_rules! impl_column_width_from_int {
214 ($($ty:ty),* $(,)?) => {
215 $(impl From<$ty> for ColumnWidth {
216 fn from(width: $ty) -> Self {
217 Self::Fixed(width.max(0) as usize)
218 }
219 })*
220 };
221}
222
223impl_column_width_from_int!(
224 usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128
225);
226
227impl From<f32> for ColumnWidth {
228 fn from(fraction: f32) -> Self {
229 Self::Fraction(fraction as f64)
230 }
231}
232
233impl From<f64> for ColumnWidth {
234 fn from(fraction: f64) -> Self {
235 Self::Fraction(fraction)
236 }
237}
238
239#[derive(Clone, Debug, Eq, PartialEq, Hash)]
253pub enum ColumnTarget {
254 Index(usize),
256 Header(String),
258}
259
260impl From<usize> for ColumnTarget {
261 fn from(index: usize) -> Self {
262 Self::Index(index)
263 }
264}
265
266impl From<&str> for ColumnTarget {
267 fn from(header: &str) -> Self {
268 Self::Header(header.to_string())
269 }
270}
271
272impl From<String> for ColumnTarget {
273 fn from(header: String) -> Self {
274 Self::Header(header)
275 }
276}
277
278pub struct Column {
294 header: Cell,
295 style: ColumnStyle,
296}
297
298impl Column {
299 pub fn new(header: impl Into<Cell>) -> Self {
301 Self {
302 header: header.into(),
303 style: ColumnStyle::default(),
304 }
305 }
306
307 pub fn width(mut self, width: impl Into<ColumnWidth>) -> Self {
309 self.style.width = Some(width.into());
310 self
311 }
312
313 pub fn max_width(self, width: impl Into<ColumnWidth>) -> Self {
315 self.width(width)
316 }
317
318 pub fn truncate(mut self, truncation: Trunc) -> Self {
320 self.style.truncation = Some(truncation);
321 self
322 }
323
324 pub fn align(mut self, align: Align) -> Self {
326 self.style.align = Some(align);
327 self
328 }
329}
330
331#[cfg(feature = "style")]
332impl_style_methods!(Column, |mut column: Column, action| {
333 column.style.styles.push(action);
334 column
335});
336
337#[derive(Clone, Debug, Default)]
338struct ColumnStyle {
339 #[cfg(feature = "style")]
340 styles: Vec<StyleAction>,
341 width: Option<ColumnWidth>,
342 truncation: Option<Trunc>,
343 align: Option<Align>,
344}
345
346impl ColumnStyle {
347 fn merge(&mut self, other: &ColumnStyle) {
348 #[cfg(feature = "style")]
349 self.styles.extend_from_slice(&other.styles);
350
351 if other.width.is_some() {
352 self.width = other.width;
353 }
354
355 if other.truncation.is_some() {
356 self.truncation = other.truncation;
357 }
358
359 if other.align.is_some() {
360 self.align = other.align;
361 }
362 }
363}
364
365pub struct Cell {
381 content: String,
382 #[cfg(feature = "style")]
383 styles: Vec<StyleAction>,
384 truncation: Option<Trunc>,
385 align: Option<Align>,
386}
387
388struct PreparedCell {
389 lines: Vec<String>,
390 align: Align,
391}
392
393enum TableRow {
394 Cells(Vec<Cell>),
395 Section(SectionRow),
396}
397
398enum PreparedRow {
399 Cells(Vec<PreparedCell>),
400 Section(SectionRow),
401}
402
403#[derive(Clone, Debug)]
404struct SectionRow {
405 title: String,
406 align: Align,
407 style: TableStyle,
408}
409
410pub struct SectionBuilder<'a> {
412 table: &'a mut Table,
413 row_index: usize,
414}
415
416pub struct ColumnBuilder<'a> {
421 table: &'a mut Table,
422 target: ColumnTarget,
423}
424
425impl Cell {
426 pub fn new(content: impl ToString) -> Self {
428 Self {
429 content: content.to_string(),
430 #[cfg(feature = "style")]
431 styles: Vec::new(),
432 truncation: None,
433 align: None,
434 }
435 }
436
437 #[must_use]
439 pub fn truncate(mut self, truncation: Trunc) -> Self {
440 self.truncation = Some(truncation);
441 self
442 }
443
444 #[must_use]
446 pub fn align(mut self, align: Align) -> Self {
447 self.align = Some(align);
448 self
449 }
450}
451
452#[cfg(feature = "style")]
453impl_style_methods!(Cell, |mut cell: Cell, action| {
454 cell.styles.push(action);
455 cell
456});
457
458impl From<&str> for Cell {
459 fn from(content: &str) -> Self {
460 Self::new(content)
461 }
462}
463
464impl From<String> for Cell {
465 fn from(content: String) -> Self {
466 Self::new(content)
467 }
468}
469
470pub struct Table {
506 headers: Vec<Cell>,
507 rows: Vec<TableRow>,
508 column_defaults: Vec<ColumnStyle>,
509 column_overrides: HashMap<ColumnTarget, ColumnStyle>,
510 style: TableStyle,
511 section_style: Option<SectionStyle>,
512 separator_style: Option<SectionStyle>,
513}
514
515impl Table {
516 pub fn new() -> Self {
518 Self {
519 headers: Vec::new(),
520 rows: Vec::new(),
521 column_defaults: Vec::new(),
522 column_overrides: HashMap::new(),
523 style: TableStyle::unicode(),
524 section_style: None,
525 separator_style: None,
526 }
527 }
528
529 pub fn with_columns(columns: impl IntoIterator<Item = Column>) -> Self {
534 let mut table = Self::new();
535 table.set_columns(columns);
536 table
537 }
538
539 pub fn with_style(mut self, style: TableStyle) -> Self {
541 self.style = style;
542 self
543 }
544
545 pub fn with_section_style(mut self, style: SectionStyle) -> Self {
550 self.section_style = Some(style);
551 self
552 }
553
554 pub fn with_separator_style(mut self, style: SectionStyle) -> Self {
559 self.separator_style = Some(style);
560 self
561 }
562
563 pub fn set_columns(&mut self, columns: impl IntoIterator<Item = Column>) {
568 let (headers, column_defaults): (Vec<_>, Vec<_>) = columns
569 .into_iter()
570 .map(|Column { header, style }| (header, style))
571 .unzip();
572
573 self.headers = headers;
574 self.column_defaults = column_defaults;
575 self.column_overrides.clear();
576 }
577
578 pub fn add_row(&mut self, row: Vec<Cell>) {
583 self.rows.push(TableRow::Cells(row));
584 }
585
586 pub fn add_section(&mut self, title: impl ToString) -> SectionBuilder<'_> {
595 let row_index = self.rows.len();
596 let style = match self.section_style {
597 Some(s) => TableStyle::from_section_style(s),
598 None => self.style,
599 };
600 self.rows.push(TableRow::Section(SectionRow {
601 title: title.to_string(),
602 align: Align::Center,
603 style,
604 }));
605
606 SectionBuilder {
607 table: self,
608 row_index,
609 }
610 }
611
612 pub fn add_separator(&mut self) -> SectionBuilder<'_> {
617 let row_index = self.rows.len();
618 let style = match self.separator_style {
619 Some(s) => TableStyle::from_section_style(s),
620 None => self.style,
621 };
622 self.rows.push(TableRow::Section(SectionRow {
623 title: String::new(),
624 align: Align::Center,
625 style,
626 }));
627
628 SectionBuilder {
629 table: self,
630 row_index,
631 }
632 }
633
634 pub fn column<T: Into<ColumnTarget>>(&mut self, target: T) -> ColumnBuilder<'_> {
636 ColumnBuilder {
637 table: self,
638 target: target.into(),
639 }
640 }
641
642 #[allow(dead_code)]
644 pub fn print(&self) {
645 for line in self.render_lines() {
646 println!("{line}");
647 }
648 }
649
650 pub fn render(&self) -> String {
656 self.render_lines().join("\n")
657 }
658
659 fn column_style_mut(&mut self, target: ColumnTarget) -> &mut ColumnStyle {
660 self.column_overrides.entry(target).or_default()
661 }
662
663 fn column_style(&self, col: usize) -> ColumnStyle {
664 let mut style = self.column_defaults.get(col).cloned().unwrap_or_default();
665
666 if let Some(header) = self.headers.get(col)
667 && let Some(header_style) = self
668 .column_overrides
669 .get(&ColumnTarget::Header(strip_ansi(&header.content)))
670 {
671 style.merge(header_style);
672 }
673
674 if let Some(index_style) = self.column_overrides.get(&ColumnTarget::Index(col)) {
675 style.merge(index_style);
676 }
677
678 style
679 }
680
681 fn prepare_cell(
682 &self,
683 cell: Option<&Cell>,
684 column_style: &ColumnStyle,
685 width: usize,
686 #[cfg_attr(not(feature = "style"), allow(unused_variables))] is_header: bool,
687 ) -> PreparedCell {
688 let raw = cell.map(|c| c.content.as_str()).unwrap_or("");
689 let truncation = cell
690 .and_then(|c| c.truncation)
691 .or(column_style.truncation)
692 .unwrap_or(Trunc::End);
693 let align = cell
694 .and_then(|c| c.align)
695 .or(column_style.align)
696 .unwrap_or(Align::Left);
697
698 #[cfg(feature = "style")]
699 let styled = {
700 let mut all_styles = column_style.styles.clone();
703 if let Some(cell) = cell {
704 all_styles.extend_from_slice(&cell.styles);
705 }
706 if is_header {
707 all_styles.push(StyleAction::Bold);
708 }
709 apply_style_actions(raw, &all_styles)
710 };
711 #[cfg(not(feature = "style"))]
712 let styled = raw.to_string();
713
714 let lines = split_lines(&styled)
715 .into_iter()
716 .flat_map(|line| layout_line(&line, Some(width), truncation))
717 .collect();
718
719 PreparedCell { lines, align }
720 }
721
722 fn prepare_row(
723 &self,
724 row: &[Cell],
725 col_widths: &[usize],
726 column_styles: &[ColumnStyle],
727 is_header: bool,
728 ) -> Vec<PreparedCell> {
729 col_widths
730 .iter()
731 .zip(column_styles)
732 .enumerate()
733 .map(|(col, (&width, style))| self.prepare_cell(row.get(col), style, width, is_header))
734 .collect()
735 }
736
737 fn collect_content_widths(&self, col_count: usize) -> Vec<usize> {
738 let mut widths = vec![0usize; col_count];
739
740 let all_rows =
741 std::iter::once(self.headers.as_slice()).chain(self.rows.iter().filter_map(|row| {
742 match row {
743 TableRow::Cells(cells) => Some(cells.as_slice()),
744 TableRow::Section(_) => None,
745 }
746 }));
747
748 for row in all_rows {
749 for (col, cell) in row.iter().enumerate() {
750 for line in split_lines(&cell.content) {
751 widths[col] = widths[col].max(visible_len(&line));
752 }
753 }
754 }
755
756 widths
757 }
758
759 fn resolve_column_widths(
760 &self,
761 content_widths: &[usize],
762 column_styles: &[ColumnStyle],
763 terminal_width: Option<usize>,
764 ) -> Vec<usize> {
765 let mut widths = content_widths.to_vec();
766 let mut fraction_columns = Vec::new();
767 let mut reserved_width = 0usize;
768
769 for (col, style) in column_styles.iter().enumerate() {
770 match style.width.unwrap_or_default() {
771 ColumnWidth::Auto => {
772 reserved_width += widths[col];
773 }
774 ColumnWidth::Fixed(width) => {
775 widths[col] = width;
776 reserved_width += width;
777 }
778 ColumnWidth::Fraction(fraction) => {
779 fraction_columns.push((col, fraction.max(0.0)));
780 }
781 }
782 }
783
784 let Some(terminal_width) = terminal_width else {
785 return widths;
786 };
787
788 let table_overhead = (3 * widths.len()) + 1;
789 let available_content_width = terminal_width.saturating_sub(table_overhead);
790 let remaining_width = available_content_width.saturating_sub(reserved_width);
791
792 if fraction_columns.is_empty() {
793 return widths;
794 }
795
796 let total_fraction: f64 = fraction_columns.iter().map(|(_, fraction)| *fraction).sum();
797 if total_fraction <= f64::EPSILON {
798 for (col, _) in fraction_columns {
799 widths[col] = 0;
800 }
801
802 return widths;
803 }
804
805 let mut remainders = Vec::with_capacity(fraction_columns.len());
806 let mut assigned = 0usize;
807
808 for (col, fraction) in fraction_columns {
809 let exact = (remaining_width as f64) * fraction / total_fraction;
810 let width = exact.floor() as usize;
811 widths[col] = width;
812 assigned += width;
813 remainders.push((col, exact - width as f64));
814 }
815
816 let mut leftover = remaining_width.saturating_sub(assigned);
817 remainders.sort_by(|left, right| right.1.partial_cmp(&left.1).unwrap_or(Ordering::Equal));
818
819 for (col, _) in remainders {
820 if leftover == 0 {
821 break;
822 }
823
824 widths[col] += 1;
825 leftover -= 1;
826 }
827
828 widths
829 }
830
831 fn column_count(&self) -> usize {
832 let max_row_len = self
833 .rows
834 .iter()
835 .filter_map(|row| match row {
836 TableRow::Cells(cells) => Some(cells.len()),
837 TableRow::Section(_) => None,
838 })
839 .max()
840 .unwrap_or(0);
841
842 self.headers.len().max(max_row_len)
843 }
844
845 fn row_height(cells: &[PreparedCell]) -> usize {
846 cells.iter().map(|cell| cell.lines.len()).max().unwrap_or(1)
847 }
848
849 fn rule_line(
850 &self,
851 style: &TableStyle,
852 left: &str,
853 joint: &str,
854 right: &str,
855 col_widths: &[usize],
856 ) -> String {
857 let h = style.horiz;
858 let join = format!("{}{}{}", h, joint, h);
859 let inner = col_widths
860 .iter()
861 .map(|&width| h.repeat(width))
862 .collect::<Vec<_>>()
863 .join(&join);
864
865 format!("{}{}{}{}{}", left, h, inner, h, right)
866 }
867
868 fn push_row_lines(
869 &self,
870 lines: &mut Vec<String>,
871 cells: &[PreparedCell],
872 col_widths: &[usize],
873 ) {
874 for line_idx in 0..Self::row_height(cells) {
875 lines.push(self.render_row_line(cells, line_idx, col_widths));
876 }
877 }
878
879 fn render_row_line(
880 &self,
881 row: &[PreparedCell],
882 line_idx: usize,
883 col_widths: &[usize],
884 ) -> String {
885 let vertical = self.style.vert;
886 let rendered_cells: Vec<String> = row
887 .iter()
888 .enumerate()
889 .map(|(col, cell)| {
890 let raw = cell.lines.get(line_idx).map(String::as_str).unwrap_or("");
891 let padding = col_widths[col].saturating_sub(visible_len(raw));
892 match cell.align {
893 Align::Left => format!("{}{}", raw, " ".repeat(padding)),
894 Align::Right => format!("{}{}", " ".repeat(padding), raw),
895 Align::Center => {
896 let left_pad = padding / 2;
897 let right_pad = padding - left_pad;
898 format!("{}{}{}", " ".repeat(left_pad), raw, " ".repeat(right_pad))
899 }
900 }
901 })
902 .collect();
903
904 format!(
905 "{} {} {}",
906 vertical,
907 rendered_cells.join(&format!(" {} ", vertical)),
908 vertical
909 )
910 }
911
912 fn render_section_line(&self, section: &SectionRow, col_widths: &[usize]) -> String {
913 let style = §ion.style;
914
915 if section.title.trim().is_empty() {
916 return self.rule_line(
917 style,
918 style.mid_left,
919 style.mid_joint,
920 style.mid_right,
921 col_widths,
922 );
923 }
924
925 let total_inner = col_widths.iter().sum::<usize>() + 3 * col_widths.len() - 1;
926 let label = truncate_line(
927 &format!(" {} ", section.title),
928 Some(total_inner),
929 Trunc::End,
930 );
931 let label_len = label.chars().count();
932 let remaining = total_inner.saturating_sub(label_len);
933
934 let left_fill = match section.align {
935 Align::Left => 1,
936 Align::Center => remaining / 2,
937 Align::Right => remaining.saturating_sub(1),
938 };
939
940 let mut inner: Vec<char> = style.horiz.repeat(total_inner).chars().collect();
941 let joint = style.mid_joint.chars().next().unwrap_or('┼');
942 let mut cursor = 1;
943
944 for &w in col_widths.iter().take(col_widths.len().saturating_sub(1)) {
945 cursor += w + 1;
946 if cursor < inner.len() {
947 inner[cursor] = joint;
948 }
949 cursor += 2;
950 }
951
952 let prefix: String = inner[..left_fill].iter().collect();
953 let suffix: String = inner[left_fill + label_len..].iter().collect();
954
955 #[cfg(feature = "style")]
956 let bold_label = format!("\x1b[1m{}{}", label, ANSI_RESET);
957 #[cfg(not(feature = "style"))]
958 let bold_label = label;
959
960 format!(
961 "{}{}{}{}{}",
962 style.mid_left, prefix, bold_label, suffix, style.mid_right
963 )
964 }
965
966 fn render_lines_with_terminal_width(&self, terminal_width: Option<usize>) -> Vec<String> {
967 let col_count = self.column_count();
968 if col_count == 0 {
969 return Vec::new();
970 }
971
972 let column_styles: Vec<ColumnStyle> =
973 (0..col_count).map(|col| self.column_style(col)).collect();
974 let content_widths = self.collect_content_widths(col_count);
975 let col_widths =
976 self.resolve_column_widths(&content_widths, &column_styles, terminal_width);
977
978 let prepared_header = (!self.headers.is_empty())
979 .then(|| self.prepare_row(&self.headers, &col_widths, &column_styles, true));
980 let prepared_rows: Vec<PreparedRow> = self
981 .rows
982 .iter()
983 .map(|row| match row {
984 TableRow::Cells(cells) => {
985 PreparedRow::Cells(self.prepare_row(cells, &col_widths, &column_styles, false))
986 }
987 TableRow::Section(section) => PreparedRow::Section(section.clone()),
988 })
989 .collect();
990
991 let mut lines = Vec::new();
992
993 lines.push(self.rule_line(
994 &self.style,
995 self.style.top_left,
996 self.style.top_joint,
997 self.style.top_right,
998 &col_widths,
999 ));
1000
1001 if let Some(header) = prepared_header.as_ref() {
1002 self.push_row_lines(&mut lines, header, &col_widths);
1003
1004 if prepared_rows.is_empty()
1005 || !matches!(prepared_rows.first(), Some(PreparedRow::Section(_)))
1006 {
1007 lines.push(self.rule_line(
1008 &self.style,
1009 self.style.mid_left,
1010 self.style.mid_joint,
1011 self.style.mid_right,
1012 &col_widths,
1013 ));
1014 }
1015 }
1016
1017 for row in &prepared_rows {
1018 match row {
1019 PreparedRow::Cells(cells) => self.push_row_lines(&mut lines, cells, &col_widths),
1020 PreparedRow::Section(section) => {
1021 lines.push(self.render_section_line(section, &col_widths))
1022 }
1023 }
1024 }
1025
1026 lines.push(self.rule_line(
1027 &self.style,
1028 self.style.bottom_left,
1029 self.style.bottom_joint,
1030 self.style.bottom_right,
1031 &col_widths,
1032 ));
1033
1034 lines
1035 }
1036
1037 fn render_lines(&self) -> Vec<String> {
1038 self.render_lines_with_terminal_width(
1039 terminal_size().map(|(Width(width), _)| width as usize),
1040 )
1041 }
1042}
1043
1044impl Default for Table {
1045 fn default() -> Self {
1046 Self::new()
1047 }
1048}
1049
1050impl std::fmt::Display for Table {
1051 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1052 for line in self.render_lines() {
1053 writeln!(f, "{line}")?;
1054 }
1055 Ok(())
1056 }
1057}
1058
1059impl<'a> SectionBuilder<'a> {
1060 pub fn align(self, align: Align) -> Self {
1062 if let Some(TableRow::Section(section)) = self.table.rows.get_mut(self.row_index) {
1063 section.align = align;
1064 }
1065
1066 self
1067 }
1068
1069 pub fn style(self, style: SectionStyle) -> Self {
1071 if let Some(TableRow::Section(section)) = self.table.rows.get_mut(self.row_index) {
1072 section.style = TableStyle::from_section_style(style);
1073 }
1074
1075 self
1076 }
1077}
1078
1079impl<'a> ColumnBuilder<'a> {
1080 #[cfg(feature = "style")]
1082 pub fn color(self, color: Color) -> Self {
1083 self.table
1084 .column_style_mut(self.target.clone())
1085 .styles
1086 .push(StyleAction::Color(color));
1087 self
1088 }
1089
1090 pub fn width(self, width: impl Into<ColumnWidth>) -> Self {
1092 self.table.column_style_mut(self.target.clone()).width = Some(width.into());
1093 self
1094 }
1095
1096 pub fn max_width(self, width: impl Into<ColumnWidth>) -> Self {
1098 self.width(width)
1099 }
1100
1101 pub fn truncate(self, truncation: Trunc) -> Self {
1103 self.table.column_style_mut(self.target.clone()).truncation = Some(truncation);
1104 self
1105 }
1106
1107 pub fn align(self, align: Align) -> Self {
1109 self.table.column_style_mut(self.target.clone()).align = Some(align);
1110 self
1111 }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::text::strip_ansi;
1117 use super::*;
1118 #[cfg(feature = "style")]
1119 use crate::Color::BrightBlack;
1120
1121 impl Table {
1122 fn render_lines_for_test(&self, terminal_width: Option<usize>) -> Vec<String> {
1123 self.render_lines_with_terminal_width(terminal_width)
1124 }
1125 }
1126
1127 #[cfg(feature = "style")]
1128 #[test]
1129 fn cell_builders_are_chainable() {
1130 let cell = Cell::new("value")
1131 .color(BrightBlack)
1132 .truncate(Trunc::Middle);
1133
1134 assert!(matches!(
1135 cell.styles.as_slice(),
1136 [StyleAction::Color(BrightBlack)]
1137 ));
1138 assert_eq!(cell.truncation, Some(Trunc::Middle));
1139 }
1140
1141 #[cfg(feature = "style")]
1142 #[test]
1143 fn accepts_colorize_values_for_cells_and_headers() {
1144 let mut table = Table::with_columns(vec![
1145 Column::new("Status").bright_green().bold(),
1146 Column::new("Notes"),
1147 ]);
1148
1149 table.column("Status").width(10);
1150 table.add_row(vec![
1151 Cell::new("DefinitelyActive").bright_red().underline(),
1152 Cell::new("Ready"),
1153 ]);
1154
1155 let plain = plain_lines(&table);
1156
1157 assert!(plain[1].contains("Status"));
1158 assert!(plain[3].contains("Definitel…"));
1159 }
1160
1161 #[test]
1162 fn renders_multiline_headers_and_rows() {
1163 let mut table = Table::with_columns(vec![Column::new("Name\nAlias"), Column::new("Value")]);
1164 table.add_row(vec![Cell::new("alpha\nbeta"), Cell::new("1")]);
1165
1166 assert_eq!(
1167 plain_lines(&table),
1168 vec![
1169 "┌───────┬───────┐",
1170 "│ Name │ Value │",
1171 "│ Alias │ │",
1172 "├───────┼───────┤",
1173 "│ alpha │ 1 │",
1174 "│ beta │ │",
1175 "└───────┴───────┘",
1176 ]
1177 );
1178 }
1179
1180 #[test]
1181 fn renders_center_aligned_sections_inside_a_single_table() {
1182 assert_eq!(
1183 section_table_lines(Align::Center),
1184 expected_section_lines("├─── Alpha ────┤")
1185 );
1186 }
1187
1188 #[test]
1189 fn renders_left_aligned_sections_inside_a_single_table() {
1190 assert_eq!(
1191 section_table_lines(Align::Left),
1192 expected_section_lines("├─ Alpha ──────┤")
1193 );
1194 }
1195
1196 #[test]
1197 fn renders_right_aligned_sections_inside_a_single_table() {
1198 assert_eq!(
1199 section_table_lines(Align::Right),
1200 expected_section_lines("├────── Alpha ─┤")
1201 );
1202 }
1203
1204 #[test]
1205 fn renders_mid_joints_when_a_section_label_leaves_room() {
1206 let mut table =
1207 Table::with_columns(vec![Column::new("A"), Column::new("B"), Column::new("C")]);
1208 table.add_section("X");
1209 table.add_row(vec![Cell::new("1"), Cell::new("2"), Cell::new("3")]);
1210
1211 assert_eq!(
1212 plain_lines(&table),
1213 vec![
1214 "┌───┬───┬───┐",
1215 "│ A │ B │ C │",
1216 "├───┼ X ┼───┤",
1217 "│ 1 │ 2 │ 3 │",
1218 "└───┴───┴───┘",
1219 ]
1220 );
1221 }
1222
1223 #[test]
1224 fn sections_and_separators_can_use_their_own_styles() {
1225 let table_style = TableStyle {
1226 top_left: "╔",
1227 top_right: "╗",
1228 bottom_left: "╚",
1229 bottom_right: "╝",
1230 horiz: "═",
1231 vert: "║",
1232 top_joint: "╦",
1233 mid_left: "╠",
1234 mid_right: "╣",
1235 mid_joint: "╬",
1236 bottom_joint: "╩",
1237 };
1238 let section_style = SectionStyle::unicode();
1239 let separator_style = SectionStyle {
1240 horiz: "-",
1241 mid_left: "-",
1242 mid_right: "-",
1243 mid_joint: "-",
1244 };
1245
1246 let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")])
1247 .with_style(table_style);
1248
1249 table.add_section("Alpha").style(section_style);
1250 table.add_row(vec![Cell::new("a"), Cell::new("1")]);
1251 table.add_separator().style(separator_style);
1252 table.add_row(vec![Cell::new("b"), Cell::new("2")]);
1253
1254 let plain = plain_lines(&table);
1255
1256 assert!(plain[0].starts_with("╔"));
1257 assert!(plain[0].ends_with("╗"));
1258 assert_eq!(plain[2], "├─── Alpha ────┤");
1259 assert_eq!(plain[4], "----------------");
1260 assert!(plain[6].starts_with("╚"));
1261 assert!(plain[6].ends_with("╝"));
1262 }
1263
1264 #[test]
1265 fn applies_column_and_cell_truncation() {
1266 let mut table = Table::with_columns(vec![Column::new("Value"), Column::new("Other")]);
1267 table.column("Value").max_width(5).truncate(Trunc::Start);
1268 table.add_row(vec![Cell::new("abcdefghij"), Cell::new("z")]);
1269 table.add_row(vec![
1270 Cell::new("abcdefghij").truncate(Trunc::Middle),
1271 Cell::new("z"),
1272 ]);
1273
1274 assert_eq!(
1275 plain_lines(&table),
1276 vec![
1277 "┌───────┬───────┐",
1278 "│ Value │ Other │",
1279 "├───────┼───────┤",
1280 "│ …ghij │ z │",
1281 "│ ab…ij │ z │",
1282 "└───────┴───────┘",
1283 ]
1284 );
1285 }
1286
1287 #[cfg(feature = "style")]
1288 #[test]
1289 fn truncation_keeps_ellipsis_tight_and_colored() {
1290 let mut table = Table::with_columns(vec![Column::new("Name")]);
1291 table.column(0).max_width(14);
1292 table.add_row(vec![Cell::new("Cynthia \"CJ\" Lee").bright_red()]);
1293
1294 let rendered = table.render_lines_for_test(Some(40)).join("\n");
1295 let plain = strip_ansi(&rendered);
1296
1297 assert!(plain.contains("Cynthia \"CJ\"…"));
1298 assert!(!plain.contains("Cynthia \"CJ\" …"));
1299 assert!(rendered.contains("\x1b[91mCynthia \"CJ\"…\x1b[0m"));
1300 }
1301
1302 #[test]
1303 fn builds_columns_in_one_step() {
1304 let mut table = Table::with_columns(vec![
1305 Column::new("Name").width(0.3),
1306 Column::new("Age").width(0.15),
1307 Column::new("City").width(0.55),
1308 ]);
1309
1310 table.add_row(vec![
1311 Cell::new("Alice"),
1312 Cell::new("30"),
1313 Cell::new("New York"),
1314 ]);
1315
1316 let plain = table
1317 .render_lines_for_test(Some(40))
1318 .into_iter()
1319 .map(|line| strip_ansi(&line))
1320 .collect::<Vec<_>>();
1321
1322 assert_eq!(plain[0].chars().count(), 40);
1323 assert!(plain[1].contains("Name"));
1324 assert!(plain[1].contains("Age"));
1325 assert!(plain[1].contains("City"));
1326 assert!(plain[3].contains("Alice"));
1327 }
1328
1329 #[test]
1330 fn renders_fractional_columns_against_terminal_width() {
1331 let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")]);
1332 table.column("Name").max_width(0.5);
1333 table.column("Value").max_width(0.5);
1334 table.add_row(vec![Cell::new("Alice"), Cell::new("123")]);
1335
1336 let lines = table.render_lines_for_test(Some(40));
1337 let plain = lines
1338 .iter()
1339 .map(|line| strip_ansi(line))
1340 .collect::<Vec<_>>();
1341
1342 assert_eq!(plain[0].chars().count(), 40);
1343 assert_eq!(plain.last().unwrap().chars().count(), 40);
1344 assert!(plain[1].contains("Name"));
1345 assert!(plain[3].contains("Alice"));
1346 }
1347
1348 #[test]
1349 fn newline_truncation_wraps_at_spaces_and_hard_breaks_when_needed() {
1350 let mut table = Table::with_columns(vec![Column::new("Value")]);
1351 table.column(0).max_width(8);
1352 table.add_row(vec![Cell::new("one two three").truncate(Trunc::NewLine)]);
1353 table.add_row(vec![Cell::new("abcdefghij").truncate(Trunc::NewLine)]);
1354
1355 assert_eq!(
1356 plain_lines(&table),
1357 vec![
1358 "┌──────────┐",
1359 "│ Value │",
1360 "├──────────┤",
1361 "│ one two │",
1362 "│ three │",
1363 "│ abcdefgh │",
1364 "│ ij │",
1365 "└──────────┘",
1366 ]
1367 );
1368 }
1369
1370 fn plain_lines(table: &Table) -> Vec<String> {
1371 table
1372 .render_lines()
1373 .into_iter()
1374 .map(|line| strip_ansi(&line))
1375 .collect()
1376 }
1377
1378 fn section_table_lines(align: Align) -> Vec<String> {
1379 let mut table = Table::with_columns(vec![Column::new("Name"), Column::new("Value")]);
1380 table.add_section("Alpha").align(align);
1381 table.add_row(vec![Cell::new("a"), Cell::new("1")]);
1382
1383 plain_lines(&table)
1384 }
1385
1386 fn expected_section_lines(section_line: &str) -> Vec<String> {
1387 vec![
1388 "┌──────┬───────┐".to_string(),
1389 "│ Name │ Value │".to_string(),
1390 section_line.to_string(),
1391 "│ a │ 1 │".to_string(),
1392 "└──────┴───────┘".to_string(),
1393 ]
1394 }
1395}