1use std::sync::Arc;
26
27use crate::console::{Console, ConsoleOptions, Justify, Overflow};
28use crate::measure::Measurement;
29use crate::protocol::{LineRenderable, Renderable};
30use crate::r#box::{Box as BoxSet, RowLevel, HEAVY_HEAD};
31use crate::segment::Segment;
32use crate::style::{Style, StyleType};
33use crate::text::Text;
34
35struct Column {
37 header: Cell,
38 footer: Cell,
40 footer_fill: Option<Style>,
43 vertical: crate::align::VerticalAlign,
45 highlight: Option<bool>,
48 justify: Justify,
49 width: Option<usize>,
51 style: Style,
53 header_content_style: Option<Style>,
57 header_fill: Option<Style>,
61 ratio: Option<usize>,
64 min_width: Option<usize>,
66 max_width: Option<usize>,
69 no_wrap: bool,
71 overflow: Overflow,
74}
75
76impl Column {
77 fn new(header: Cell, justify: Justify) -> Self {
79 Column {
80 header,
81 footer: Cell::Markup(String::new()),
82 footer_fill: None,
83 highlight: None,
84 vertical: crate::align::VerticalAlign::Top,
85 justify,
86 width: None,
87 style: Style::new(),
88 header_content_style: None,
89 header_fill: None,
90 ratio: None,
91 min_width: None,
92 max_width: None,
93 no_wrap: false,
94 overflow: Overflow::Ellipsis,
95 }
96 }
97}
98
99struct Row {
102 cells: Vec<Cell>,
103 style: Option<StyleType>,
105 end_section: bool,
107}
108
109#[derive(Clone)]
112enum RowKind {
113 Header,
114 Body(Style),
115 Footer,
116}
117
118#[derive(Clone)]
121pub enum Cell {
122 Text(Text),
125 Markup(String),
130 Renderable(Arc<dyn Renderable + Send + Sync>),
133}
134
135impl From<Text> for Cell {
136 fn from(text: Text) -> Self {
137 Cell::Text(text)
138 }
139}
140
141impl From<&str> for Cell {
148 fn from(text: &str) -> Self {
149 Cell::Markup(text.to_string())
150 }
151}
152
153impl From<String> for Cell {
155 fn from(text: String) -> Self {
156 Cell::Markup(text)
157 }
158}
159
160impl From<&String> for Cell {
162 fn from(text: &String) -> Self {
163 Cell::Markup(text.clone())
164 }
165}
166
167impl Cell {
168 pub(crate) fn to_text(&self, console: &Console, highlight: Option<bool>) -> Option<Text> {
171 match self {
172 Cell::Text(text) => Some(text.clone()),
173 Cell::Markup(markup) => Some(console.render_str(markup, highlight)),
174 Cell::Renderable(_) => None,
175 }
176 }
177
178 pub(crate) fn measure_cell(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
182 match self {
183 Cell::Text(text) => Measurement::get(console, options, text),
184 Cell::Markup(markup) if !markup.contains(['[', ':']) => {
187 if options.max_width < 1 {
188 return Measurement::new(0, 0);
189 }
190 let (minimum, maximum) = crate::text::measure_plain(markup);
191 let width = Measurement::new(minimum, maximum)
192 .normalize()
193 .with_maximum(options.max_width);
194 if width.maximum < 1 {
195 Measurement::new(0, 0)
196 } else {
197 width.normalize()
198 }
199 }
200 Cell::Markup(markup) => {
201 Measurement::get(console, options, &console.render_str(markup, Some(false)))
202 }
203 Cell::Renderable(renderable) => Measurement::get(console, options, renderable.as_ref()),
204 }
205 }
206}
207
208impl Default for Cell {
209 fn default() -> Self {
210 Cell::Text(Text::default())
211 }
212}
213
214#[derive(Clone, Debug)]
217pub struct ColumnOptions {
218 pub justify: Justify,
220 pub width: Option<usize>,
222 pub min_width: Option<usize>,
224 pub max_width: Option<usize>,
226 pub ratio: Option<usize>,
228 pub no_wrap: bool,
230 pub overflow: Overflow,
232 pub style: Style,
234}
235
236impl Default for ColumnOptions {
237 fn default() -> Self {
238 ColumnOptions {
239 justify: Justify::Left,
240 width: None,
241 min_width: None,
242 max_width: None,
243 ratio: None,
244 no_wrap: false,
245 overflow: Overflow::Ellipsis,
246 style: Style::new(),
247 }
248 }
249}
250
251pub struct Table {
253 columns: Vec<Column>,
254 rows: Vec<Row>,
255 box_set: BoxSet,
256 no_box: bool,
258 show_header: bool,
259 show_lines: bool,
260 show_edge: bool,
261 pad_edge: bool,
262 collapse_padding: bool,
263 expand: bool,
264 title: Option<Cell>,
266 caption: Option<Cell>,
267 padding: (usize, usize, usize, usize),
268 header_style: StyleType,
270 footer_style: StyleType,
272 border_style: Style,
273 style: Style,
274 highlight: bool,
276 width: Option<usize>,
278 min_width: Option<usize>,
280 show_footer: bool,
281 leading: usize,
283 row_styles: Vec<StyleType>,
285 title_style: Option<StyleType>,
287 caption_style: Option<StyleType>,
288 title_justify: Justify,
289 caption_justify: Justify,
290 safe_box: Option<bool>,
293}
294
295impl Default for Table {
296 fn default() -> Self {
297 Table {
298 columns: Vec::new(),
299 rows: Vec::new(),
300 box_set: HEAVY_HEAD,
301 no_box: false,
302 show_header: true,
303 show_lines: false,
304 show_edge: true,
305 pad_edge: true,
306 collapse_padding: false,
307 expand: false,
308 title: None,
309 caption: None,
310 padding: (0, 1, 0, 1),
311 header_style: StyleType::Name("table.header".to_string()),
312 footer_style: StyleType::Name("table.footer".to_string()),
313 border_style: Style::new(),
314 style: Style::new(),
315 highlight: false,
316 width: None,
317 min_width: None,
318 show_footer: false,
319 leading: 0,
320 row_styles: Vec::new(),
321 title_style: None,
322 caption_style: None,
323 title_justify: Justify::Center,
324 caption_justify: Justify::Center,
325 safe_box: None,
326 }
327 }
328}
329
330impl Table {
331 pub fn new() -> Self {
332 Table::default()
333 }
334
335 pub fn grid() -> Self {
339 Table {
340 no_box: true,
341 show_header: false,
342 show_edge: false,
343 pad_edge: false,
344 collapse_padding: true,
345 padding: (0, 0, 0, 0),
346 ..Table::default()
347 }
348 }
349
350 pub fn without_box(mut self) -> Self {
352 self.no_box = true;
353 self
354 }
355
356 pub fn padding(mut self, top: usize, right: usize, bottom: usize, left: usize) -> Self {
358 self.padding = (top, right, bottom, left);
359 self
360 }
361
362 pub fn box_set(mut self, box_set: BoxSet) -> Self {
364 self.box_set = box_set;
365 self
366 }
367
368 pub fn border_style(mut self, style: Style) -> Self {
371 self.border_style = style;
372 self
373 }
374
375 pub fn show_header(mut self, show: bool) -> Self {
377 self.show_header = show;
378 self
379 }
380
381 pub fn expand(mut self, expand: bool) -> Self {
383 self.expand = expand;
384 self
385 }
386
387 pub fn show_lines(mut self, show: bool) -> Self {
389 self.show_lines = show;
390 self
391 }
392
393 pub fn show_edge(mut self, show: bool) -> Self {
396 self.show_edge = show;
397 self
398 }
399
400 pub fn pad_edge(mut self, pad: bool) -> Self {
403 self.pad_edge = pad;
404 self
405 }
406
407 pub fn collapse_padding(mut self, collapse: bool) -> Self {
410 self.collapse_padding = collapse;
411 self
412 }
413
414 pub fn style(mut self, style: Style) -> Self {
418 self.style = style;
419 self
420 }
421
422 pub fn highlight(mut self, highlight: bool) -> Self {
426 self.highlight = highlight;
427 self
428 }
429
430 fn cell_padding(&self, index: usize, ncols: usize) -> (usize, usize) {
433 let (_, pr, _, pl) = self.padding;
434 let mut left = if self.collapse_padding && index > 0 {
437 pl.saturating_sub(pr)
438 } else {
439 pl
440 };
441 let mut right = pr;
442 if !self.pad_edge && index == 0 {
444 left = 0;
445 }
446 if !self.pad_edge && index + 1 == ncols {
447 right = 0;
448 }
449 (left, right)
450 }
451
452 pub fn title(mut self, title: impl Into<String>) -> Self {
455 self.title = Some(Cell::Markup(title.into()));
456 self
457 }
458
459 pub fn caption(mut self, caption: impl Into<String>) -> Self {
461 self.caption = Some(Cell::Markup(caption.into()));
462 self
463 }
464
465 pub fn title_text(mut self, title: Text) -> Self {
468 self.title = Some(Cell::Text(title));
469 self
470 }
471
472 pub fn caption_text(mut self, caption: Text) -> Self {
474 self.caption = Some(Cell::Text(caption));
475 self
476 }
477
478 pub fn title_style(mut self, style: impl Into<StyleType>) -> Self {
481 self.title_style = Some(style.into());
482 self
483 }
484
485 pub fn caption_style(mut self, style: impl Into<StyleType>) -> Self {
488 self.caption_style = Some(style.into());
489 self
490 }
491
492 pub fn title_justify(mut self, justify: Justify) -> Self {
494 self.title_justify = justify;
495 self
496 }
497
498 pub fn caption_justify(mut self, justify: Justify) -> Self {
500 self.caption_justify = justify;
501 self
502 }
503
504 pub fn header_style(mut self, style: impl Into<StyleType>) -> Self {
507 self.header_style = style.into();
508 self
509 }
510
511 pub fn footer_style(mut self, style: impl Into<StyleType>) -> Self {
514 self.footer_style = style.into();
515 self
516 }
517
518 pub fn width(mut self, width: Option<usize>) -> Self {
521 self.width = width;
522 self
523 }
524
525 pub fn min_width(mut self, min_width: Option<usize>) -> Self {
527 self.min_width = min_width;
528 self
529 }
530
531 pub fn show_footer(mut self, show: bool) -> Self {
534 self.show_footer = show;
535 self
536 }
537
538 pub fn leading(mut self, leading: usize) -> Self {
541 self.leading = leading;
542 self
543 }
544
545 pub fn row_styles(mut self, styles: Vec<StyleType>) -> Self {
547 self.row_styles = styles;
548 self
549 }
550
551 pub fn safe_box(mut self, safe_box: Option<bool>) -> Self {
554 self.safe_box = safe_box;
555 self
556 }
557
558 pub fn is_expand(&self) -> bool {
561 self.expand || self.width.is_some()
562 }
563
564 pub fn row_count(&self) -> usize {
566 self.rows.len()
567 }
568
569 pub fn column_count(&self) -> usize {
571 self.columns.len()
572 }
573
574 pub fn get_row_style(&self, console: &Console, index: usize) -> Style {
577 let theme = console.theme();
578 let mut style = Style::new();
579 if !self.row_styles.is_empty() {
580 style = style
581 .combine(&theme.get_style_or_null(&self.row_styles[index % self.row_styles.len()]));
582 }
583 if let Some(row_style) = self.rows.get(index).and_then(|row| row.style.as_ref()) {
584 style = style.combine(&theme.get_style_or_null(row_style));
585 }
586 style
587 }
588
589 pub fn add_section(&mut self) -> &mut Self {
592 if let Some(row) = self.rows.last_mut() {
593 row.end_section = true;
594 }
595 self
596 }
597
598 pub fn add_column(&mut self, header: impl Into<String>) -> &mut Self {
601 self.add_column_justify(header, Justify::Left)
602 }
603
604 pub fn add_column_justify(&mut self, header: impl Into<String>, justify: Justify) -> &mut Self {
607 self.add_column_text(Text::default(), justify);
608 if let Some(column) = self.columns.last_mut() {
609 column.header = Cell::Markup(header.into());
610 }
611 self
612 }
613
614 pub fn add_column_text(&mut self, header: Text, justify: Justify) -> &mut Self {
619 self.columns.push(Column::new(Cell::Text(header), justify));
620 self
621 }
622
623 pub fn add_column_cell(&mut self, header: Cell, justify: Justify) -> &mut Self {
626 self.columns.push(Column::new(header, justify));
627 self
628 }
629
630 pub fn column_footer(&mut self, footer: impl Into<Cell>) -> &mut Self {
633 if let Some(column) = self.columns.last_mut() {
634 column.footer = footer.into();
635 }
636 self
637 }
638
639 pub fn column_footer_fill(&mut self, style: Style) -> &mut Self {
642 if let Some(column) = self.columns.last_mut() {
643 column.footer_fill = Some(style);
644 }
645 self
646 }
647
648 pub fn add_column_with(&mut self, header: Text, options: ColumnOptions) -> &mut Self {
651 self.columns.push(Column {
652 width: options.width,
653 style: options.style,
654 ratio: options.ratio,
655 min_width: options.min_width,
656 max_width: options.max_width,
657 no_wrap: options.no_wrap,
658 overflow: options.overflow,
659 ..Column::new(Cell::Text(header), options.justify)
660 });
661 self
662 }
663
664 pub fn column_vertical(&mut self, vertical: crate::align::VerticalAlign) -> &mut Self {
669 if let Some(column) = self.columns.last_mut() {
670 column.vertical = vertical;
671 }
672 self
673 }
674
675 pub fn column_width(&mut self, width: usize) -> &mut Self {
679 if let Some(column) = self.columns.last_mut() {
680 column.width = Some(width);
681 }
682 self
683 }
684
685 pub fn column_ratio(&mut self, ratio: usize) -> &mut Self {
689 if let Some(column) = self.columns.last_mut() {
690 column.ratio = Some(ratio);
691 }
692 self
693 }
694
695 pub fn column_min_width(&mut self, min_width: usize) -> &mut Self {
698 if let Some(column) = self.columns.last_mut() {
699 column.min_width = Some(min_width);
700 }
701 self
702 }
703
704 pub fn column_max_width(&mut self, max_width: usize) -> &mut Self {
707 if let Some(column) = self.columns.last_mut() {
708 column.max_width = Some(max_width);
709 }
710 self
711 }
712
713 pub fn column_style(&mut self, style: Style) -> &mut Self {
716 if let Some(column) = self.columns.last_mut() {
717 column.style = style;
718 }
719 self
720 }
721
722 pub fn column_header_style(&mut self, style: Style) -> &mut Self {
726 if let Some(column) = self.columns.last_mut() {
727 column.header_content_style = Some(style);
728 }
729 self
730 }
731
732 pub fn column_header_fill(&mut self, style: Style) -> &mut Self {
736 if let Some(column) = self.columns.last_mut() {
737 column.header_fill = Some(style);
738 }
739 self
740 }
741
742 pub fn column_overflow(&mut self, overflow: Overflow) -> &mut Self {
745 if let Some(column) = self.columns.last_mut() {
746 column.overflow = overflow;
747 }
748 self
749 }
750
751 pub fn column_highlight(&mut self, highlight: bool) -> &mut Self {
754 if let Some(column) = self.columns.last_mut() {
755 column.highlight = Some(highlight);
756 }
757 self
758 }
759
760 pub fn column_no_wrap(&mut self) -> &mut Self {
763 if let Some(column) = self.columns.last_mut() {
764 column.no_wrap = true;
765 }
766 self
767 }
768
769 pub fn add_row(&mut self, cells: &[&str]) -> &mut Self {
775 self.push_row(
776 cells
777 .iter()
778 .map(|s| Cell::Markup((*s).to_string()))
779 .collect(),
780 None,
781 false,
782 )
783 }
784
785 pub fn add_row_with(
789 &mut self,
790 cells: Vec<Cell>,
791 style: Option<StyleType>,
792 end_section: bool,
793 ) -> &mut Self {
794 self.push_row(cells, style, end_section)
795 }
796
797 fn push_row(
800 &mut self,
801 cells: Vec<Cell>,
802 style: Option<StyleType>,
803 end_section: bool,
804 ) -> &mut Self {
805 while self.columns.len() < cells.len() {
806 let mut column = Column::new(Cell::Markup(String::new()), Justify::Left);
807 column.highlight = Some(self.highlight);
808 self.columns.push(column);
809 }
810 self.rows.push(Row {
811 cells,
812 style,
813 end_section,
814 });
815 self
816 }
817
818 pub fn add_row_text(&mut self, cells: Vec<Text>) -> &mut Self {
822 self.push_row(cells.into_iter().map(Cell::Text).collect(), None, false)
823 }
824
825 pub fn add_row_cells(&mut self, cells: Vec<Cell>) -> &mut Self {
827 self.push_row(cells, None, false)
828 }
829
830 fn extra_width(&self) -> usize {
833 if self.no_box {
834 0
835 } else {
836 (if self.show_edge { 2 } else { 0 }) + self.columns.len().saturating_sub(1)
837 }
838 }
839
840 fn padding_width(&self, index: usize) -> usize {
844 let (_, mut pad_right, _, mut pad_left) = self.padding;
845 if self.collapse_padding {
846 pad_left = 0;
847 }
848 if !self.pad_edge {
849 if index == 0 {
850 pad_left = 0;
851 }
852 if index + 1 == self.columns.len() {
853 pad_right = 0;
854 }
855 }
856 pad_left + pad_right
857 }
858
859 fn measure_padded_cell(
863 &self,
864 console: &Console,
865 options: &ConsoleOptions,
866 cell: &Cell,
867 (left, right): (usize, usize),
868 ) -> Measurement {
869 let max_width = options.max_width;
870 if max_width < 1 {
871 return Measurement::new(0, 0);
872 }
873 let (top, pr, bottom, pl) = self.padding;
874 if top == 0 && pr == 0 && bottom == 0 && pl == 0 {
875 return cell.measure_cell(console, options);
876 }
877 let extra_width = left + right;
878 let width = if max_width < extra_width + 1 {
879 Measurement::new(max_width, max_width)
880 } else {
881 let inner = cell.measure_cell(console, options);
882 Measurement::new(inner.minimum + extra_width, inner.maximum + extra_width)
883 .with_maximum(max_width)
884 };
885 let width = width.normalize().with_maximum(max_width);
887 if width.maximum < 1 {
888 Measurement::new(0, 0)
889 } else {
890 width.normalize()
891 }
892 }
893
894 fn measure_column(
899 &self,
900 console: &Console,
901 options: &ConsoleOptions,
902 index: usize,
903 ) -> Measurement {
904 let max_width = options.max_width;
905 if max_width < 1 {
906 return Measurement::new(0, 0);
907 }
908 let column = &self.columns[index];
909 let padding_width = self.padding_width(index);
910 if let Some(width) = column.width {
911 return Measurement::new(width + padding_width, width + padding_width)
913 .with_maximum(max_width);
914 }
915 let padding = self.cell_padding(index, self.columns.len());
918 let empty = Cell::Markup(String::new());
919 let header = self.show_header.then_some(&column.header);
920 let body = self
921 .rows
922 .iter()
923 .map(|row| row.cells.get(index).unwrap_or(&empty));
924 let footer = self.show_footer.then_some(&column.footer);
925 let mut measured = false;
926 let (mut minimum, mut maximum) = (0, 0);
927 for cell in header.into_iter().chain(body).chain(footer) {
928 let width = self.measure_padded_cell(console, options, cell, padding);
929 minimum = minimum.max(width.minimum);
930 maximum = maximum.max(width.maximum);
931 measured = true;
932 }
933 let measurement = if measured {
934 Measurement::new(minimum, maximum)
935 } else {
936 Measurement::new(1, max_width)
937 }
938 .with_maximum(max_width);
939 measurement.clamp(
940 column.min_width.map(|width| width + padding_width),
941 column.max_width.map(|width| width + padding_width),
942 )
943 }
944
945 fn column_widths(
949 &self,
950 console: &Console,
951 options: &ConsoleOptions,
952 available: usize,
953 ) -> Vec<usize> {
954 let options = &options.update_width(available);
955 let max_width = available as i64;
956 let extra_width = self.extra_width() as i64;
957 let maximums: Vec<i64> = (0..self.columns.len())
960 .map(|index| self.measure_column(console, options, index).maximum as i64)
961 .collect();
962 let mut widths: Vec<i64> = maximums.iter().map(|&width| width.max(1)).collect();
963
964 if self.is_expand() {
968 let ratios: Vec<i64> = self
969 .columns
970 .iter()
971 .filter(|c| c.ratio.is_some())
972 .map(|c| i64::try_from(c.ratio.unwrap()).unwrap_or(i64::MAX))
973 .collect();
974 if ratios.iter().any(|&r| r > 0) {
975 let fixed_widths: Vec<i64> = maximums
976 .iter()
977 .zip(&self.columns)
978 .map(|(&w, c)| if c.ratio.is_some() { 0 } else { w })
979 .collect();
980 let flex_minimum: Vec<i64> = self
981 .columns
982 .iter()
983 .enumerate()
984 .filter(|(_, c)| c.ratio.is_some())
985 .map(|(index, c)| (c.width.unwrap_or(1) + self.padding_width(index)) as i64)
986 .collect();
987 let flexible_width = max_width - fixed_widths.iter().sum::<i64>();
988 let flex_widths = ratio_distribute(flexible_width, &ratios, Some(&flex_minimum));
989 let mut iter_flex = flex_widths.into_iter();
990 for (index, column) in self.columns.iter().enumerate() {
991 if column.ratio.is_some() {
992 widths[index] = fixed_widths[index] + iter_flex.next().unwrap_or(0);
993 }
994 }
995 }
996 }
997
998 let mut table_width: i64 = widths.iter().sum();
999 if table_width > max_width {
1000 let wrapable: Vec<bool> = self
1003 .columns
1004 .iter()
1005 .map(|c| c.width.is_none() && !c.no_wrap)
1006 .collect();
1007 widths = collapse_widths(widths, &wrapable, max_width);
1008 table_width = widths.iter().sum();
1009 if table_width > max_width {
1012 let excess = table_width - max_width;
1013 let ratios = vec![1i64; widths.len()];
1014 widths = ratio_reduce(excess, &ratios, &widths, &widths);
1015 table_width = widths.iter().sum();
1016 }
1017 widths = widths
1022 .iter()
1023 .enumerate()
1024 .map(|(index, &width)| {
1025 self.measure_column(
1026 console,
1027 &options.update_width(width.max(0) as usize),
1028 index,
1029 )
1030 .maximum as i64
1031 })
1032 .collect();
1033 }
1034
1035 let min_width = self.min_width.map(|width| width as i64);
1039 if (table_width < max_width && self.is_expand())
1040 || min_width.is_some_and(|min_width| table_width < min_width - extra_width)
1041 {
1042 let target = match min_width {
1043 None => max_width,
1044 Some(min_width) => (min_width - extra_width).min(max_width),
1045 };
1046 if widths.iter().sum::<i64>() > 0 {
1047 let pad = ratio_distribute(target - table_width, &widths, None);
1048 for (width, extra) in widths.iter_mut().zip(pad) {
1049 *width += extra;
1050 }
1051 }
1052 }
1053 widths.into_iter().map(|w| w.max(0) as usize).collect()
1054 }
1055
1056 fn cell_padding_fitted(&self, index: usize, ncols: usize, rendered: usize) -> (usize, usize) {
1066 let (mut pl, mut pr) = self.cell_padding(index, ncols);
1067 while pl + pr > rendered {
1068 if pr > pl {
1069 pr -= 1;
1070 } else if pl > 0 {
1071 pl -= 1;
1072 } else {
1073 break;
1074 }
1075 }
1076 (pl, pr)
1077 }
1078
1079 fn cell_style(&self, console: &Console, index: usize, kind: &RowKind) -> Style {
1084 let column = self.columns.get(index);
1085 match kind {
1086 RowKind::Header => {
1087 let base = console.theme().get_style_or_null(&self.header_style);
1088 match column.and_then(|c| c.header_fill.as_ref()) {
1089 Some(fill) => base.combine(fill),
1090 None => base,
1091 }
1092 }
1093 RowKind::Footer => {
1094 let base = console.theme().get_style_or_null(&self.footer_style);
1095 match column.and_then(|c| c.footer_fill.as_ref()) {
1096 Some(fill) => base.combine(fill),
1097 None => base,
1098 }
1099 }
1100 RowKind::Body(row_style) => column
1101 .map(|c| c.style.clone())
1102 .unwrap_or_default()
1103 .combine(row_style),
1104 }
1105 }
1106
1107 fn vertical_padding(&self, first_row: bool, last_row: bool) -> (usize, usize) {
1116 let (mut top, _, mut bottom, _) = self.padding;
1117 if self.collapse_padding && !last_row {
1118 bottom = top.saturating_sub(bottom);
1119 }
1120 if !self.pad_edge {
1121 if first_row {
1122 top = 0;
1123 }
1124 if last_row {
1125 bottom = 0;
1126 }
1127 }
1128 (top, bottom)
1129 }
1130
1131 fn pad_cell_lines(
1132 &self,
1133 lines: Vec<Vec<Segment>>,
1134 width: usize,
1135 (cpl, cpr): (usize, usize),
1136 (pt, pb): (usize, usize),
1137 style: &Style,
1138 ) -> Vec<Vec<Segment>> {
1139 let cell_fill = Some(style.clone());
1140 let cell_width = cpl + width + cpr;
1141 if cell_width == 0 {
1145 return Vec::new();
1146 }
1147 let blank = || vec![Segment::new(" ".repeat(cell_width), cell_fill.clone())];
1148 let mut padded_lines: Vec<Vec<Segment>> = Vec::new();
1149 for _ in 0..pt {
1150 padded_lines.push(blank());
1151 }
1152 for line in &lines {
1153 let mut row = Vec::new();
1154 if cpl > 0 {
1155 row.push(Segment::new(" ".repeat(cpl), cell_fill.clone()));
1156 }
1157 row.extend(Segment::adjust_line_length(line, width, cell_fill.clone()));
1159 if cpr > 0 {
1160 row.push(Segment::new(" ".repeat(cpr), cell_fill.clone()));
1161 }
1162 padded_lines.push(row);
1163 }
1164 for _ in 0..pb {
1165 padded_lines.push(blank());
1166 }
1167 padded_lines
1168 }
1169
1170 #[allow(clippy::too_many_arguments)]
1172 fn render_row(
1173 &self,
1174 console: &Console,
1175 options: &ConsoleOptions,
1176 cells: &[Cell],
1177 rendered_widths: &[usize],
1178 kind: &RowKind,
1179 (first_row, last_row): (bool, bool),
1180 edges: Option<(Segment, Segment, Segment)>,
1181 row_index: usize,
1182 ) -> Vec<Vec<Segment>> {
1183 let is_header = matches!(kind, RowKind::Header);
1184 let vertical = self.vertical_padding(first_row, last_row);
1187 let ncols = self.columns.len();
1188 let paddings: Vec<(usize, usize)> = (0..ncols)
1191 .map(|index| {
1192 let rendered = rendered_widths.get(index).copied().unwrap_or(0);
1193 self.cell_padding_fitted(index, ncols, rendered)
1194 })
1195 .collect();
1196 let content_widths: Vec<usize> = rendered_widths
1197 .iter()
1198 .zip(&paddings)
1199 .map(|(w, (pl, pr))| w.saturating_sub(pl + pr))
1200 .collect();
1201
1202 let mut cell_lines: Vec<Vec<Vec<Segment>>> = Vec::with_capacity(ncols);
1204 let mut height = 1;
1205 let mut cell_regions = Vec::new();
1207 for (index, width) in content_widths.iter().enumerate() {
1208 let region = crate::protocol::enter_region(console, || {
1209 use crate::protocol::{RegionInfo, RegionRole};
1210 RegionInfo::new(match kind {
1211 RowKind::Header => RegionRole::TableHeader { column: index },
1212 RowKind::Footer => RegionRole::TableFooter { column: index },
1213 RowKind::Body(_) => RegionRole::TableCell {
1214 row: row_index,
1215 column: index,
1216 },
1217 })
1218 });
1219 if let Some(guard) = ®ion {
1221 cell_regions.push((index, guard.id()));
1222 }
1223 let style = self.cell_style(console, index, kind);
1224 let column = self.columns.get(index);
1225 let mut text = match cells.get(index) {
1226 Some(Cell::Text(text)) => text.clone(),
1227 Some(Cell::Markup(markup)) => console.render_str(
1230 markup,
1231 Some(column.and_then(|c| c.highlight).unwrap_or(self.highlight)),
1232 ),
1233 None => Text::default(),
1234 Some(Cell::Renderable(renderable)) => {
1235 let mut cell_options = options.update_width(*width);
1239 cell_options.highlight =
1240 Some(column.and_then(|c| c.highlight).unwrap_or(self.highlight));
1241 cell_options.justify = column.map_or(Justify::Left, |c| c.justify);
1242 cell_options.no_wrap = Some(column.is_some_and(|c| c.no_wrap));
1243 cell_options.overflow = Some(column.map_or(Overflow::Ellipsis, |c| c.overflow));
1244 let lines = if *width == 0 {
1245 Vec::new()
1246 } else {
1247 console.render_lines_styled(
1248 renderable.as_ref(),
1249 &cell_options,
1250 Some(&style),
1251 true,
1252 )
1253 };
1254 cell_lines.push(self.pad_cell_lines(
1255 lines,
1256 *width,
1257 paddings[index],
1258 vertical,
1259 &style,
1260 ));
1261 height = height.max(cell_lines.last().map_or(0, Vec::len));
1262 continue;
1263 }
1264 };
1265 let justify = text
1270 .get_justify_option()
1271 .unwrap_or_else(|| column.map(|c| c.justify).unwrap_or(Justify::Left));
1272 let overflow = text
1273 .get_overflow()
1274 .unwrap_or_else(|| column.map_or(Overflow::Ellipsis, |c| c.overflow));
1275 let no_wrap = text
1276 .get_no_wrap()
1277 .unwrap_or_else(|| column.map(|c| c.no_wrap).unwrap_or(false));
1278 if is_header {
1281 if let Some(span) = column.and_then(|c| c.header_content_style.clone()) {
1282 let len = text.plain().len();
1283 text.stylize(span, 0, len);
1284 }
1285 }
1286 let tab_size = text.console_tab_size(console);
1298 let mut lines: Vec<Vec<Segment>> = if *width == 0 {
1299 Vec::new()
1300 } else {
1301 text.render_lines_wrapped_tabs(
1302 console.theme(),
1303 &Style::new(),
1304 Some(*width),
1305 justify,
1306 overflow,
1307 no_wrap,
1308 tab_size,
1309 )
1310 .iter()
1311 .map(|line| Segment::apply_style(line, &style))
1312 .collect()
1313 };
1314 if lines.is_empty() && *width > 0 {
1315 lines.push(Vec::new());
1316 }
1317 let padded_lines =
1318 self.pad_cell_lines(lines, *width, paddings[index], vertical, &style);
1319 height = height.max(padded_lines.len());
1320 cell_lines.push(padded_lines);
1321 }
1322
1323 let row_height = cell_lines.iter().map(Vec::len).max().unwrap_or(0);
1329 for (index, lines) in cell_lines.iter_mut().enumerate() {
1330 let vertical = if is_header {
1334 crate::align::VerticalAlign::Bottom
1335 } else if matches!(kind, RowKind::Footer) {
1336 crate::align::VerticalAlign::Top
1337 } else {
1338 match cells.get(index) {
1339 Some(Cell::Renderable(renderable)) => renderable.vertical(),
1340 _ => None,
1341 }
1342 .unwrap_or_else(|| {
1343 self.columns
1344 .get(index)
1345 .map_or(crate::align::VerticalAlign::Top, |c| c.vertical)
1346 })
1347 };
1348 let (cpl, cpr) = paddings[index];
1349 let blank = " ".repeat(cpl + content_widths[index] + cpr);
1350 let filler = vec![Segment::new(
1351 blank.clone(),
1352 Some(self.cell_style(console, index, kind)),
1353 )];
1354 let missing = row_height.saturating_sub(lines.len());
1355 let top = match vertical {
1356 crate::align::VerticalAlign::Top => 0,
1357 crate::align::VerticalAlign::Middle => missing / 2,
1358 crate::align::VerticalAlign::Bottom => missing,
1359 };
1360 lines.splice(0..0, std::iter::repeat_n(filler.clone(), top));
1361 lines.extend(std::iter::repeat_n(filler, missing - top));
1362 while lines.len() < height {
1363 lines.push(vec![Segment::new(blank.clone(), None)]);
1364 }
1365 if let Some((_, id)) = cell_regions.iter().find(|(cell, _)| *cell == index) {
1366 for line in lines.iter_mut() {
1367 crate::protocol::tag_region(line, *id);
1368 }
1369 }
1370 }
1371
1372 let last = ncols.saturating_sub(1);
1373 let mut rows_out: Vec<Vec<Segment>> = Vec::with_capacity(height);
1374 #[allow(clippy::needless_range_loop)]
1377 for r in 0..height {
1378 let mut row = Vec::new();
1379 if let (Some((edge_left, _, _)), true) = (&edges, self.show_edge) {
1380 row.push(edge_left.clone());
1381 }
1382 for (c, column_lines) in cell_lines.iter().enumerate() {
1383 row.extend(column_lines[r].clone());
1384 let Some((_, divider, edge_right)) = &edges else {
1385 continue;
1386 };
1387 if c != last {
1388 row.push(divider.clone());
1389 } else if self.show_edge {
1390 row.push(edge_right.clone());
1391 }
1392 }
1393 rows_out.push(row);
1394 }
1395 rows_out
1396 }
1397}
1398
1399impl LineRenderable for Table {
1400 fn try_for_each_line<E>(
1408 &self,
1409 console: &Console,
1410 options: &ConsoleOptions,
1411 emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
1412 ) -> Result<(), E> {
1413 let region = crate::protocol::enter_region(console, || {
1415 let info = crate::protocol::RegionInfo::new(crate::protocol::RegionRole::Table);
1416 match &self.title {
1417 Some(title) => info.label(annotation_plain(console, title)),
1418 None => info,
1419 }
1420 });
1421 let mut emit = {
1422 let mut emit = emit;
1423 let region = region.as_ref().map(|guard| guard.id());
1424 move |mut line: Vec<Segment>| {
1425 if let Some(id) = region {
1426 crate::protocol::tag_region(&mut line, id);
1427 }
1428 emit(line)
1429 }
1430 };
1431 if self.columns.is_empty() {
1432 return emit(vec![Segment::new("", None)]);
1433 }
1434 let box_set = self.box_set.substitute(
1437 console.legacy_windows(),
1438 self.safe_box.unwrap_or_else(|| console.safe_box()),
1439 console.ascii_only(),
1440 );
1441 let box_set = if self.show_header {
1442 box_set
1443 } else {
1444 box_set.get_plain_headed_box()
1445 };
1446 let max_width = self.width.unwrap_or(options.max_width);
1448 let extra_width = self.extra_width();
1449 let available = max_width.saturating_sub(extra_width);
1450
1451 let rendered_widths = self.column_widths(console, options, available);
1452 let border_style = self.style.combine(&self.border_style);
1453 let border = Some(border_style.clone());
1454
1455 let table_width: usize = rendered_widths.iter().sum::<usize>() + extra_width;
1457
1458 if let Some(title) = &self.title {
1459 let style = self
1460 .title_style
1461 .clone()
1462 .unwrap_or_else(|| StyleType::Name("table.title".to_string()));
1463 for line in render_annotation(
1464 console,
1465 options,
1466 title,
1467 &style,
1468 self.title_justify,
1469 table_width,
1470 ) {
1471 emit(line)?;
1472 }
1473 }
1474
1475 let edge = self.show_edge;
1476 let boxed = !self.no_box;
1477 if boxed && edge {
1478 emit(vec![Segment::new(
1479 box_set.get_top(&rendered_widths, edge),
1480 border.clone(),
1481 )])?;
1482 }
1483
1484 let segments = |(left, vertical, right): (char, char, char)| {
1487 (
1488 Segment::new(left.to_string(), border.clone()),
1489 Segment::new(vertical.to_string(), border.clone()),
1490 Segment::new(right.to_string(), border.clone()),
1491 )
1492 };
1493 let head_edges = segments((box_set.head_left, box_set.head_vertical, box_set.head_right));
1494 let mid_edges = segments((box_set.mid_left, box_set.mid_vertical, box_set.mid_right));
1495 let foot_edges = segments((box_set.foot_left, box_set.foot_vertical, box_set.foot_right));
1496
1497 let mut row_list: Vec<(RowKind, &[Cell], Option<&Row>)> = Vec::new();
1499 let header_cells: Vec<Cell> = self.columns.iter().map(|c| c.header.clone()).collect();
1500 let footer_cells: Vec<Cell> = self.columns.iter().map(|c| c.footer.clone()).collect();
1501 if self.show_header {
1502 row_list.push((RowKind::Header, &header_cells, None));
1503 }
1504 for (index, row) in self.rows.iter().enumerate() {
1505 row_list.push((
1506 RowKind::Body(self.get_row_style(console, index)),
1507 &row.cells,
1508 Some(row),
1509 ));
1510 }
1511 if self.show_footer {
1512 row_list.push((RowKind::Footer, &footer_cells, None));
1513 }
1514 let total = row_list.len();
1515 for (index, (kind, cells, row)) in row_list.iter().enumerate() {
1516 let first = index == 0;
1517 let last = index + 1 == total;
1518 let header_row = first && self.show_header;
1519 if boxed && last && self.show_footer {
1520 emit(vec![Segment::new(
1521 box_set.get_row(&rendered_widths, RowLevel::Foot, edge),
1522 border.clone(),
1523 )])?;
1524 }
1525 let edges = boxed.then(|| {
1526 let (left, divider, right) = if first {
1527 head_edges.clone()
1528 } else if last {
1529 foot_edges.clone()
1530 } else {
1531 mid_edges.clone()
1532 };
1533 let divider = if divider.text.trim().is_empty() {
1535 let background = match kind {
1536 RowKind::Body(row_style) => {
1537 Style::from_color(None, row_style.bgcolor().cloned())
1538 }
1539 _ => Style::new(),
1540 };
1541 Segment::new(
1542 divider.text.clone(),
1543 Some(background.combine(&border_style)),
1544 )
1545 } else {
1546 divider
1547 };
1548 (left, divider, right)
1549 });
1550 for line in self.render_row(
1551 console,
1552 options,
1553 cells,
1554 &rendered_widths,
1555 kind,
1556 (first, last),
1557 edges,
1558 index - usize::from(self.show_header && !matches!(kind, RowKind::Header)),
1559 ) {
1560 emit(line)?;
1561 }
1562 if boxed && header_row {
1563 emit(vec![Segment::new(
1564 box_set.get_row(&rendered_widths, RowLevel::Head, edge),
1565 border.clone(),
1566 )])?;
1567 }
1568 let end_section = row.is_some_and(|row| row.end_section);
1569 if boxed
1570 && (self.show_lines || self.leading > 0 || end_section)
1571 && !last
1572 && !(self.show_footer && index + 2 >= total)
1573 && !header_row
1574 {
1575 let line = if self.leading > 0 {
1576 box_set
1578 .get_row(&rendered_widths, RowLevel::Mid, edge)
1579 .repeat(self.leading)
1580 } else {
1581 box_set.get_row(&rendered_widths, RowLevel::Row, edge)
1582 };
1583 emit(vec![Segment::new(line, border.clone())])?;
1584 }
1585 }
1586
1587 if boxed && edge {
1588 emit(vec![Segment::new(
1589 box_set.get_bottom(&rendered_widths, edge),
1590 border.clone(),
1591 )])?;
1592 }
1593
1594 if let Some(caption) = &self.caption {
1595 let style = self
1596 .caption_style
1597 .clone()
1598 .unwrap_or_else(|| StyleType::Name("table.caption".to_string()));
1599 for line in render_annotation(
1600 console,
1601 options,
1602 caption,
1603 &style,
1604 self.caption_justify,
1605 table_width,
1606 ) {
1607 emit(line)?;
1608 }
1609 }
1610
1611 Ok(())
1612 }
1613}
1614
1615impl crate::protocol::OwnedTableRows for Table {
1616 fn extend_owned_rows(&mut self, rows: Vec<Vec<String>>) -> &mut Self {
1617 for row in rows {
1618 self.push_row(row.into_iter().map(Cell::Markup).collect(), None, false);
1619 }
1620 self
1621 }
1622}
1623
1624impl Renderable for Table {
1625 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
1629 let max_width = self.width.unwrap_or(options.max_width);
1630 if self.columns.is_empty() {
1631 let width = usize::from(!self.no_box && self.show_edge);
1635 return Measurement::new(width, self.width.unwrap_or(width))
1636 .clamp(self.min_width, None);
1637 }
1638 let extra_width = self.extra_width();
1639 let columns_width: usize = self
1640 .column_widths(console, options, max_width.saturating_sub(extra_width))
1641 .iter()
1642 .sum();
1643 let options = options.update_width(columns_width);
1644 let (minimum, maximum) = (0..self.columns.len())
1645 .map(|index| self.measure_column(console, &options, index))
1646 .fold((0, 0), |(minimum, maximum), width| {
1647 (minimum + width.minimum, maximum + width.maximum)
1648 });
1649 let maximum = self.width.unwrap_or(maximum + extra_width);
1650 Measurement::new(minimum + extra_width, maximum).clamp(self.min_width, None)
1651 }
1652
1653 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1654 let mut segments = Vec::new();
1655 let mut first = true;
1656 let result: Result<(), std::convert::Infallible> =
1657 self.try_for_each_line(console, options, |line| {
1658 if !first {
1659 segments.push(Segment::line());
1660 }
1661 first = false;
1662 segments.extend(line);
1663 Ok(())
1664 });
1665 match result {
1666 Ok(()) => segments,
1667 Err(never) => match never {},
1668 }
1669 }
1670}
1671
1672fn annotation_plain(console: &Console, annotation: &Cell) -> String {
1678 match annotation {
1679 Cell::Markup(markup) => console.render_str(markup, Some(false)).plain().to_string(),
1680 Cell::Text(text) => text.plain().to_string(),
1681 Cell::Renderable(_) => String::new(),
1682 }
1683}
1684
1685fn render_annotation(
1686 console: &Console,
1687 options: &ConsoleOptions,
1688 annotation: &Cell,
1689 style: &StyleType,
1690 justify: Justify,
1691 width: usize,
1692) -> Vec<Vec<Segment>> {
1693 let text = match annotation {
1694 Cell::Markup(markup) => {
1695 if markup.is_empty() {
1696 return Vec::new();
1697 }
1698 let mut text = console.render_str(markup, Some(false));
1699 text.set_base_style(style.clone());
1700 text
1701 }
1702 Cell::Text(text) => {
1703 if text.plain().is_empty() {
1704 return Vec::new();
1705 }
1706 text.clone()
1707 }
1708 Cell::Renderable(_) => return Vec::new(),
1709 };
1710 if width == 0 {
1711 return Vec::new();
1712 }
1713 let justify = text.get_justify_option().unwrap_or(justify);
1714 let overflow = text
1715 .get_overflow()
1716 .or(options.overflow)
1717 .unwrap_or(Overflow::Fold);
1718 let no_wrap = text.get_no_wrap().or(options.no_wrap).unwrap_or(false);
1719 let tab_size = text.console_tab_size(console);
1720 text.render_lines_wrapped_tabs(
1721 console.theme(),
1722 console.base_style(),
1723 Some(width),
1724 justify,
1725 overflow,
1726 no_wrap,
1727 tab_size,
1728 )
1729}
1730
1731fn round_half_even(value: f64) -> i64 {
1733 let floor = value.floor();
1734 let diff = value - floor;
1735 if (diff - 0.5).abs() < 1e-9 {
1736 let f = floor as i64;
1737 if f % 2 == 0 {
1738 f
1739 } else {
1740 f + 1
1741 }
1742 } else {
1743 value.round() as i64
1744 }
1745}
1746
1747fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
1750 let ratios: Vec<i64> = ratios
1751 .iter()
1752 .zip(maximums)
1753 .map(|(&r, &m)| if m != 0 { r } else { 0 })
1754 .collect();
1755 let mut total_ratio: i128 = ratios.iter().map(|&r| i128::from(r)).sum();
1756 if total_ratio == 0 {
1757 return values.to_vec();
1758 }
1759 let mut total_remaining = total;
1760 let mut result = Vec::with_capacity(values.len());
1761 for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
1762 if ratio != 0 && total_ratio > 0 {
1763 let distributed = maximum.min(round_half_even(
1764 ratio as f64 * total_remaining as f64 / total_ratio as f64,
1765 ));
1766 result.push(value - distributed);
1767 total_remaining -= distributed;
1768 total_ratio -= i128::from(ratio);
1769 } else {
1770 result.push(value);
1771 }
1772 }
1773 result
1774}
1775
1776fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
1780 let ratios: Vec<i64> = match minimums {
1782 Some(mins) => ratios
1783 .iter()
1784 .zip(mins)
1785 .map(|(&r, &m)| if m != 0 { r } else { 0 })
1786 .collect(),
1787 None => ratios.to_vec(),
1788 };
1789 let mut total_ratio: i128 = ratios.iter().map(|&r| i128::from(r)).sum();
1792 let mut total_remaining = i128::from(total);
1793 let mut result = Vec::with_capacity(ratios.len());
1794 for (index, &ratio) in ratios.iter().enumerate() {
1795 let ratio = i128::from(ratio);
1796 let minimum = i128::from(minimums.map_or(0, |m| m[index]));
1797 let distributed = if total_ratio > 0 {
1798 let numerator = ratio * total_remaining;
1801 let ceil_div = (numerator + total_ratio - 1) / total_ratio;
1802 minimum.max(ceil_div)
1803 } else {
1804 total_remaining
1805 };
1806 result.push(i64::try_from(distributed).unwrap_or(if distributed < 0 {
1807 i64::MIN
1808 } else {
1809 i64::MAX
1810 }));
1811 total_ratio -= ratio;
1812 total_remaining -= distributed;
1813 }
1814 result
1815}
1816
1817fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
1820 let mut total_width: i64 = widths.iter().sum();
1821 let mut excess_width = total_width - max_width;
1822 if wrapable.iter().any(|&w| w) {
1823 while total_width != 0 && excess_width > 0 {
1824 let max_column = widths
1825 .iter()
1826 .zip(wrapable)
1827 .filter(|(_, &w)| w)
1828 .map(|(&x, _)| x)
1829 .max()
1830 .unwrap_or(0);
1831 let second_max_column = widths
1832 .iter()
1833 .zip(wrapable)
1834 .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
1835 .max()
1836 .unwrap_or(0);
1837 let column_difference = max_column - second_max_column;
1838 let ratios: Vec<i64> = widths
1839 .iter()
1840 .zip(wrapable)
1841 .map(|(&x, &w)| i64::from(x == max_column && w))
1842 .collect();
1843 if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
1844 break;
1845 }
1846 let max_reduce = vec![excess_width.min(column_difference); widths.len()];
1847 widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
1848 total_width = widths.iter().sum();
1849 excess_width = total_width - max_width;
1850 }
1851 }
1852 widths
1853}
1854
1855#[cfg(test)]
1856mod tests {
1857
1858 #[test]
1859 fn a_huge_column_ratio_does_not_overflow() {
1860 let console = crate::Console::builder()
1863 .width(40)
1864 .color_system(None)
1865 .build();
1866 let mut table = Table::new().expand(true);
1867 table.add_column("a").column_ratio(usize::MAX);
1868 table.add_column("b").column_ratio(1);
1869 table.add_row(&["x", "y"]);
1870 assert_eq!(
1871 console.render_to_string(&table) + "\n",
1872 "┏━━━┳━━━┓\n┃ a ┃ b ┃\n┡━━━╇━━━┩\n│ x │ y │\n└───┴───┘\n"
1873 );
1874 }
1875
1876 use super::*;
1877 use crate::color::ColorSystem;
1878 use crate::r#box::SQUARE;
1879
1880 #[test]
1881 fn text_cells_are_never_markup() {
1882 let console = Console::builder().width(80).color_system(None).build();
1883 let data = ["[bold]model[/]", "col[/b]", ":sparkles:"];
1885 let mut plain = Table::new();
1886 plain.add_column("a").add_column("b").add_column("c");
1887 plain.add_row_text(data.iter().map(|s| Text::new(*s)).collect());
1888 let rendered = console.render_to_string(&plain);
1889 for cell in data {
1890 assert!(rendered.contains(cell), "{cell}: {rendered}");
1891 }
1892 let mut cells = Table::new();
1894 cells.add_column("a").add_column("b").add_column("c");
1895 cells.add_row_cells(vec![
1896 Cell::from(Text::new("[bold]model[/]")),
1897 Cell::Markup("[bold]model[/]".into()),
1898 Cell::from("[bold]model[/]"),
1899 ]);
1900 let rendered = console.render_to_string(&cells);
1901 assert_eq!(rendered.matches("[bold]model[/]").count(), 1, "{rendered}");
1902 assert_eq!(rendered.matches("model").count(), 3, "{rendered}");
1903 }
1904
1905 fn console() -> Console {
1906 Console::builder()
1907 .force_terminal(true)
1908 .color_system(Some(ColorSystem::Truecolor))
1909 .width(40)
1910 .no_color(false)
1911 .build()
1912 }
1913
1914 #[test]
1915 fn owned_rows_preserve_measurement_styles_and_missing_cells() {
1916 use crate::protocol::OwnedTableRows;
1917 for width in [1, 12, 40, 80] {
1918 let console = Console::builder().width(width).force_terminal(true).build();
1919 let build = || {
1920 let mut table = Table::new()
1921 .title("Rows")
1922 .caption("owned or borrowed")
1923 .show_lines(true);
1924 table.add_column("Name");
1925 table.add_column_justify("Value", Justify::Right);
1926 table
1927 };
1928 let mut borrowed = build();
1929 let mut owned = build();
1930 for row in [
1931 vec!["漢字\n🙂", "123"],
1932 vec!["short"],
1933 vec!["extra", "4", "ignored"],
1934 ] {
1935 borrowed.add_row(&row);
1936 owned.extend_owned_rows(vec![row.into_iter().map(str::to_owned).collect()]);
1937 }
1938 assert_eq!(
1939 console.render_to_string(&borrowed),
1940 console.render_to_string(&owned)
1941 );
1942 }
1943 }
1944
1945 #[test]
1946 fn simple_square_table() {
1947 let mut table = Table::new().box_set(SQUARE);
1948 table.add_column("Name");
1949 table.add_column("Age");
1950 table.add_row(&["Alice", "30"]);
1951 table.add_row(&["Bob", "7"]);
1952 let out = console().render_export(&table);
1953 let expected = concat!(
1954 "┌───────┬─────┐\n",
1955 "│\x1b[1m \x1b[0m\x1b[1mName \x1b[0m\x1b[1m \x1b[0m│\x1b[1m \x1b[0m\x1b[1mAge\x1b[0m\x1b[1m \x1b[0m│\n",
1956 "├───────┼─────┤\n",
1957 "│ Alice │ 30 │\n",
1958 "│ Bob │ 7 │\n",
1959 "└───────┴─────┘\n",
1960 );
1961 assert_eq!(out, expected);
1962 }
1963
1964 #[test]
1965 fn streamed_lines_match_styled_table_output() {
1966 let mut table = Table::new().box_set(SQUARE);
1967 table.add_column("Name");
1968 table.add_column("Age");
1969 table.add_row(&["Alice", "30"]);
1970 table.add_row(&["Bob", "7"]);
1971 let console = console();
1972 let mut streamed = String::new();
1973 table
1974 .try_for_each_line(&console, &console.options(), |line| {
1975 assert!(line.iter().all(|segment| !segment.text.contains('\n')));
1976 streamed.push_str(&console.segments_to_string(&line));
1977 streamed.push('\n');
1978 Ok::<_, std::convert::Infallible>(())
1979 })
1980 .unwrap();
1981 assert_eq!(streamed, console.render_export(&table));
1984 assert_eq!(streamed.lines().count(), 6);
1985 }
1986
1987 #[test]
1988 fn streamed_lines_stop_at_the_first_writer_error() {
1989 let mut table = Table::new()
1990 .box_set(SQUARE)
1991 .title("People")
1992 .caption("End")
1993 .show_lines(true);
1994 table.add_column("Name");
1995 table.add_row(&["Alice\nBob"]);
1996 table.add_row(&["Carol"]);
1997 let console = console();
1998 let mut visits = 0;
1999 let result = table.try_for_each_line(&console, &console.options(), |_| {
2000 visits += 1;
2001 if visits == 5 {
2002 Err("writer failed")
2003 } else {
2004 Ok(())
2005 }
2006 });
2007 assert_eq!(result, Err("writer failed"));
2008 assert_eq!(visits, 5);
2009 }
2010
2011 #[test]
2016 fn a_column_narrower_than_its_padding_stays_inside_the_border() {
2017 for ncols in [20usize, 29, 40] {
2018 let mut table = Table::new().box_set(SQUARE);
2019 for i in 0..ncols {
2020 table.add_column(format!("c{i}"));
2021 }
2022 let row: Vec<String> = (0..ncols).map(|i| i.to_string()).collect();
2023 table.add_row(&row.iter().map(String::as_str).collect::<Vec<_>>());
2024 let console = Console::builder().width(80).color_system(None).build();
2025 let out = console.render_to_string(&table);
2026 let rows: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
2027 let widths: Vec<usize> = rows.iter().map(|r| r.chars().count()).collect();
2028 assert!(
2029 widths.iter().all(|w| *w == widths[0]),
2030 "{ncols} columns produced ragged rows: {widths:?}"
2031 );
2032 for (index, row) in rows.iter().enumerate() {
2033 let last = row.chars().last().expect("non-empty row");
2034 assert!(
2035 !last.is_whitespace(),
2036 "{ncols} columns: row {index} lost its right border: {row:?}"
2037 );
2038 }
2039 }
2040 }
2041
2042 #[test]
2047 fn a_multi_line_cell_is_measured_by_its_widest_line() {
2048 let mut table = Table::new().box_set(SQUARE);
2049 table.add_column("name");
2050 table.add_column("bio");
2051 table.add_row(&["Alice", "line one\nline two is much longer"]);
2052 table.add_row(&["Bob", "short"]);
2053 let console = Console::builder().width(60).color_system(None).build();
2054 let out = console.render_to_string(&table);
2055 let top = out.lines().next().expect("a top border");
2056 let width = top.chars().count();
2057 assert!(
2059 width < 40,
2060 "the multi-line cell was measured as the sum of its lines: {width} wide"
2061 );
2062 assert!(
2063 out.contains("line two is much longer"),
2064 "content lost: {out:?}"
2065 );
2066 }
2067}