1use std::sync::Arc;
20
21use crate::console::{Console, ConsoleOptions, Justify, Overflow};
22use crate::measure::Measurement;
23use crate::protocol::{LineRenderable, Renderable};
24use crate::r#box::{Box as BoxSet, RowLevel, HEAVY_HEAD};
25use crate::segment::Segment;
26use crate::style::Style;
27use crate::text::{Text, DEFAULT_TAB_SIZE};
28
29struct Column {
31 header: Cell,
32 highlight: Option<bool>,
35 justify: Justify,
36 width: Option<usize>,
38 style: Style,
40 header_content_style: Option<Style>,
44 header_fill: Option<Style>,
48 ratio: Option<usize>,
51 min_width: Option<usize>,
53 max_width: Option<usize>,
56 no_wrap: bool,
58 overflow: Overflow,
61}
62
63#[derive(Clone)]
66pub enum Cell {
67 Text(Text),
70 Markup(String),
75 Renderable(Arc<dyn Renderable + Send + Sync>),
78}
79
80impl From<Text> for Cell {
81 fn from(text: Text) -> Self {
82 Cell::Text(text)
83 }
84}
85
86impl From<&str> for Cell {
88 fn from(text: &str) -> Self {
89 Cell::Markup(text.to_string())
90 }
91}
92
93impl From<String> for Cell {
94 fn from(text: String) -> Self {
95 Cell::Markup(text)
96 }
97}
98
99impl From<&String> for Cell {
100 fn from(text: &String) -> Self {
101 Cell::Markup(text.clone())
102 }
103}
104
105impl Cell {
106 pub(crate) fn to_text(&self, console: &Console, highlight: Option<bool>) -> Option<Text> {
109 match self {
110 Cell::Text(text) => Some(text.clone()),
111 Cell::Markup(markup) => Some(console.render_str(markup, highlight)),
112 Cell::Renderable(_) => None,
113 }
114 }
115
116 pub(crate) fn measure_cell(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
120 match self {
121 Cell::Text(text) => Measurement::get(console, options, text),
122 Cell::Markup(markup) if !markup.contains(['[', ':']) => {
125 if options.max_width < 1 {
126 return Measurement::new(0, 0);
127 }
128 let (minimum, maximum) = crate::text::measure_plain(markup);
129 let width = Measurement::new(minimum, maximum)
130 .normalize()
131 .with_maximum(options.max_width);
132 if width.maximum < 1 {
133 Measurement::new(0, 0)
134 } else {
135 width.normalize()
136 }
137 }
138 Cell::Markup(markup) => {
139 Measurement::get(console, options, &console.render_str(markup, Some(false)))
140 }
141 Cell::Renderable(renderable) => Measurement::get(console, options, renderable.as_ref()),
142 }
143 }
144}
145
146impl Default for Cell {
147 fn default() -> Self {
148 Cell::Text(Text::default())
149 }
150}
151
152#[derive(Clone, Debug)]
155pub struct ColumnOptions {
156 pub justify: Justify,
158 pub width: Option<usize>,
160 pub min_width: Option<usize>,
162 pub max_width: Option<usize>,
164 pub ratio: Option<usize>,
166 pub no_wrap: bool,
168 pub overflow: Overflow,
170 pub style: Style,
172}
173
174impl Default for ColumnOptions {
175 fn default() -> Self {
176 ColumnOptions {
177 justify: Justify::Left,
178 width: None,
179 min_width: None,
180 max_width: None,
181 ratio: None,
182 no_wrap: false,
183 overflow: Overflow::Ellipsis,
184 style: Style::new(),
185 }
186 }
187}
188
189pub struct Table {
191 columns: Vec<Column>,
192 rows: Vec<Vec<Cell>>,
193 box_set: BoxSet,
194 no_box: bool,
196 show_header: bool,
197 show_lines: bool,
198 show_edge: bool,
199 pad_edge: bool,
200 collapse_padding: bool,
201 expand: bool,
202 title: Option<String>,
203 caption: Option<String>,
204 padding: (usize, usize, usize, usize),
205 header_style: Style,
206 border_style: Style,
207 style: Style,
208 highlight: bool,
210}
211
212impl Default for Table {
213 fn default() -> Self {
214 Table {
215 columns: Vec::new(),
216 rows: Vec::new(),
217 box_set: HEAVY_HEAD,
218 no_box: false,
219 show_header: true,
220 show_lines: false,
221 show_edge: true,
222 pad_edge: true,
223 collapse_padding: false,
224 expand: false,
225 title: None,
226 caption: None,
227 padding: (0, 1, 0, 1),
228 header_style: Style::parse("bold").expect("valid built-in style"),
229 border_style: Style::new(),
230 style: Style::new(),
231 highlight: false,
232 }
233 }
234}
235
236impl Table {
237 pub fn new() -> Self {
238 Table::default()
239 }
240
241 pub fn grid() -> Self {
245 Table {
246 no_box: true,
247 show_header: false,
248 show_edge: false,
249 pad_edge: false,
250 collapse_padding: true,
251 padding: (0, 0, 0, 0),
252 ..Table::default()
253 }
254 }
255
256 pub fn without_box(mut self) -> Self {
258 self.no_box = true;
259 self
260 }
261
262 pub fn padding(mut self, top: usize, right: usize, bottom: usize, left: usize) -> Self {
264 self.padding = (top, right, bottom, left);
265 self
266 }
267
268 pub fn box_set(mut self, box_set: BoxSet) -> Self {
270 self.box_set = box_set;
271 self
272 }
273
274 pub fn border_style(mut self, style: Style) -> Self {
277 self.border_style = style;
278 self
279 }
280
281 pub fn show_header(mut self, show: bool) -> Self {
283 self.show_header = show;
284 self
285 }
286
287 pub fn expand(mut self, expand: bool) -> Self {
289 self.expand = expand;
290 self
291 }
292
293 pub fn show_lines(mut self, show: bool) -> Self {
295 self.show_lines = show;
296 self
297 }
298
299 pub fn show_edge(mut self, show: bool) -> Self {
302 self.show_edge = show;
303 self
304 }
305
306 pub fn pad_edge(mut self, pad: bool) -> Self {
309 self.pad_edge = pad;
310 self
311 }
312
313 pub fn collapse_padding(mut self, collapse: bool) -> Self {
316 self.collapse_padding = collapse;
317 self
318 }
319
320 pub fn style(mut self, style: Style) -> Self {
324 self.style = style;
325 self
326 }
327
328 pub fn highlight(mut self, highlight: bool) -> Self {
332 self.highlight = highlight;
333 self
334 }
335
336 fn cell_padding(&self, index: usize, ncols: usize) -> (usize, usize) {
339 let (_, pr, _, pl) = self.padding;
340 let mut left = if self.collapse_padding && index > 0 {
343 pl.saturating_sub(pr)
344 } else {
345 pl
346 };
347 let mut right = pr;
348 if !self.pad_edge && index == 0 {
350 left = 0;
351 }
352 if !self.pad_edge && index + 1 == ncols {
353 right = 0;
354 }
355 (left, right)
356 }
357
358 pub fn title(mut self, title: impl Into<String>) -> Self {
360 self.title = Some(title.into());
361 self
362 }
363
364 pub fn caption(mut self, caption: impl Into<String>) -> Self {
366 self.caption = Some(caption.into());
367 self
368 }
369
370 pub fn add_column(&mut self, header: impl Into<String>) -> &mut Self {
373 self.add_column_justify(header, Justify::Left)
374 }
375
376 pub fn add_column_justify(&mut self, header: impl Into<String>, justify: Justify) -> &mut Self {
379 self.add_column_text(Text::default(), justify);
380 if let Some(column) = self.columns.last_mut() {
381 column.header = Cell::Markup(header.into());
382 }
383 self
384 }
385
386 pub fn add_column_text(&mut self, header: Text, justify: Justify) -> &mut Self {
391 self.columns.push(Column {
392 header: Cell::Text(header),
393 highlight: None,
394 justify,
395 width: None,
396 style: Style::new(),
397 header_content_style: None,
398 header_fill: None,
399 ratio: None,
400 min_width: None,
401 max_width: None,
402 no_wrap: false,
403 overflow: Overflow::Ellipsis,
404 });
405 self
406 }
407
408 pub fn add_column_with(&mut self, header: Text, options: ColumnOptions) -> &mut Self {
411 self.columns.push(Column {
412 header: Cell::Text(header),
413 highlight: None,
414 justify: options.justify,
415 width: options.width,
416 style: options.style,
417 header_content_style: None,
418 header_fill: None,
419 ratio: options.ratio,
420 min_width: options.min_width,
421 max_width: options.max_width,
422 no_wrap: options.no_wrap,
423 overflow: options.overflow,
424 });
425 self
426 }
427
428 pub fn column_width(&mut self, width: usize) -> &mut Self {
432 if let Some(column) = self.columns.last_mut() {
433 column.width = Some(width);
434 }
435 self
436 }
437
438 pub fn column_ratio(&mut self, ratio: usize) -> &mut Self {
442 if let Some(column) = self.columns.last_mut() {
443 column.ratio = Some(ratio);
444 }
445 self
446 }
447
448 pub fn column_min_width(&mut self, min_width: usize) -> &mut Self {
451 if let Some(column) = self.columns.last_mut() {
452 column.min_width = Some(min_width);
453 }
454 self
455 }
456
457 pub fn column_max_width(&mut self, max_width: usize) -> &mut Self {
460 if let Some(column) = self.columns.last_mut() {
461 column.max_width = Some(max_width);
462 }
463 self
464 }
465
466 pub fn column_style(&mut self, style: Style) -> &mut Self {
469 if let Some(column) = self.columns.last_mut() {
470 column.style = style;
471 }
472 self
473 }
474
475 pub fn column_header_style(&mut self, style: Style) -> &mut Self {
479 if let Some(column) = self.columns.last_mut() {
480 column.header_content_style = Some(style);
481 }
482 self
483 }
484
485 pub fn column_header_fill(&mut self, style: Style) -> &mut Self {
489 if let Some(column) = self.columns.last_mut() {
490 column.header_fill = Some(style);
491 }
492 self
493 }
494
495 pub fn column_overflow(&mut self, overflow: Overflow) -> &mut Self {
498 if let Some(column) = self.columns.last_mut() {
499 column.overflow = overflow;
500 }
501 self
502 }
503
504 pub fn column_highlight(&mut self, highlight: bool) -> &mut Self {
507 if let Some(column) = self.columns.last_mut() {
508 column.highlight = Some(highlight);
509 }
510 self
511 }
512
513 pub fn column_no_wrap(&mut self) -> &mut Self {
516 if let Some(column) = self.columns.last_mut() {
517 column.no_wrap = true;
518 }
519 self
520 }
521
522 pub fn add_row(&mut self, cells: &[&str]) -> &mut Self {
526 self.rows.push(
527 cells
528 .iter()
529 .map(|s| Cell::Markup((*s).to_string()))
530 .collect(),
531 );
532 self
533 }
534
535 pub fn add_row_text(&mut self, cells: Vec<Text>) -> &mut Self {
539 self.rows.push(cells.into_iter().map(Cell::Text).collect());
540 self
541 }
542
543 pub fn add_row_cells(&mut self, cells: Vec<Cell>) -> &mut Self {
545 self.rows.push(cells);
546 self
547 }
548
549 fn extra_width(&self) -> usize {
552 if self.no_box {
553 0
554 } else {
555 (if self.show_edge { 2 } else { 0 }) + self.columns.len().saturating_sub(1)
556 }
557 }
558
559 fn padding_width(&self, index: usize) -> usize {
563 let (_, mut pad_right, _, mut pad_left) = self.padding;
564 if self.collapse_padding {
565 pad_left = 0;
566 }
567 if !self.pad_edge {
568 if index == 0 {
569 pad_left = 0;
570 }
571 if index + 1 == self.columns.len() {
572 pad_right = 0;
573 }
574 }
575 pad_left + pad_right
576 }
577
578 fn measure_padded_cell(
582 &self,
583 console: &Console,
584 options: &ConsoleOptions,
585 cell: &Cell,
586 (left, right): (usize, usize),
587 ) -> Measurement {
588 let max_width = options.max_width;
589 if max_width < 1 {
590 return Measurement::new(0, 0);
591 }
592 let (top, pr, bottom, pl) = self.padding;
593 if top == 0 && pr == 0 && bottom == 0 && pl == 0 {
594 return cell.measure_cell(console, options);
595 }
596 let extra_width = left + right;
597 let width = if max_width < extra_width + 1 {
598 Measurement::new(max_width, max_width)
599 } else {
600 let inner = cell.measure_cell(console, options);
601 Measurement::new(inner.minimum + extra_width, inner.maximum + extra_width)
602 .with_maximum(max_width)
603 };
604 let width = width.normalize().with_maximum(max_width);
606 if width.maximum < 1 {
607 Measurement::new(0, 0)
608 } else {
609 width.normalize()
610 }
611 }
612
613 fn measure_column(
618 &self,
619 console: &Console,
620 options: &ConsoleOptions,
621 index: usize,
622 ) -> Measurement {
623 let max_width = options.max_width;
624 if max_width < 1 {
625 return Measurement::new(0, 0);
626 }
627 let column = &self.columns[index];
628 let padding_width = self.padding_width(index);
629 if let Some(width) = column.width {
630 return Measurement::new(width + padding_width, width + padding_width)
632 .with_maximum(max_width);
633 }
634 let padding = self.cell_padding(index, self.columns.len());
637 let empty = Cell::Markup(String::new());
638 let header = self.show_header.then_some(&column.header);
639 let body = self.rows.iter().map(|row| row.get(index).unwrap_or(&empty));
640 let mut measured = false;
641 let (mut minimum, mut maximum) = (0, 0);
642 for cell in header.into_iter().chain(body) {
643 let width = self.measure_padded_cell(console, options, cell, padding);
644 minimum = minimum.max(width.minimum);
645 maximum = maximum.max(width.maximum);
646 measured = true;
647 }
648 let measurement = if measured {
649 Measurement::new(minimum, maximum)
650 } else {
651 Measurement::new(1, max_width)
652 }
653 .with_maximum(max_width);
654 measurement.clamp(
655 column.min_width.map(|width| width + padding_width),
656 column.max_width.map(|width| width + padding_width),
657 )
658 }
659
660 fn column_widths(
664 &self,
665 console: &Console,
666 options: &ConsoleOptions,
667 available: usize,
668 ) -> Vec<usize> {
669 let options = &options.update_width(available);
670 let maximums: Vec<i64> = (0..self.columns.len())
673 .map(|index| self.measure_column(console, options, index).maximum as i64)
674 .collect();
675 let mut widths: Vec<i64> = maximums.iter().map(|&width| width.max(1)).collect();
676
677 if self.expand {
681 let ratios: Vec<i64> = self
682 .columns
683 .iter()
684 .filter(|c| c.ratio.is_some())
685 .map(|c| c.ratio.unwrap() as i64)
686 .collect();
687 if ratios.iter().any(|&r| r > 0) {
688 let fixed_widths: Vec<i64> = maximums
689 .iter()
690 .zip(&self.columns)
691 .map(|(&w, c)| if c.ratio.is_some() { 0 } else { w })
692 .collect();
693 let flex_minimum: Vec<i64> = self
694 .columns
695 .iter()
696 .enumerate()
697 .filter(|(_, c)| c.ratio.is_some())
698 .map(|(index, c)| (c.width.unwrap_or(1) + self.padding_width(index)) as i64)
699 .collect();
700 let flexible_width = available as i64 - fixed_widths.iter().sum::<i64>();
701 let flex_widths = ratio_distribute(flexible_width, &ratios, Some(&flex_minimum));
702 let mut iter_flex = flex_widths.into_iter();
703 for (index, column) in self.columns.iter().enumerate() {
704 if column.ratio.is_some() {
705 widths[index] = fixed_widths[index] + iter_flex.next().unwrap_or(0);
706 }
707 }
708 }
709 }
710
711 let table_width: i64 = widths.iter().sum();
712 let collapsed = table_width > available as i64;
713 if collapsed {
714 let wrapable: Vec<bool> = self
717 .columns
718 .iter()
719 .map(|c| c.width.is_none() && !c.no_wrap)
720 .collect();
721 widths = collapse_widths(widths, &wrapable, available as i64);
722 let table_width: i64 = widths.iter().sum();
725 if table_width > available as i64 {
726 let excess = table_width - available as i64;
727 let ratios = vec![1i64; widths.len()];
728 widths = ratio_reduce(excess, &ratios, &widths, &widths);
729 }
730 widths = widths
734 .iter()
735 .enumerate()
736 .map(|(index, &width)| {
737 self.measure_column(
738 console,
739 &options.update_width(width.max(0) as usize),
740 index,
741 )
742 .maximum as i64
743 })
744 .collect();
745 }
746
747 let table_width: i64 = widths.iter().sum();
751 if !collapsed && self.expand && table_width < available as i64 && table_width > 0 {
752 let pad = ratio_distribute(available as i64 - table_width, &widths, None);
753 for (width, extra) in widths.iter_mut().zip(pad) {
754 *width += extra;
755 }
756 }
757 widths.into_iter().map(|w| w.max(0) as usize).collect()
758 }
759
760 fn cell_padding_fitted(&self, index: usize, ncols: usize, rendered: usize) -> (usize, usize) {
770 let (mut pl, mut pr) = self.cell_padding(index, ncols);
771 while pl + pr > rendered {
772 if pr > pl {
773 pr -= 1;
774 } else if pl > 0 {
775 pl -= 1;
776 } else {
777 break;
778 }
779 }
780 (pl, pr)
781 }
782
783 fn cell_style(&self, index: usize, is_header: bool) -> Style {
786 if is_header {
787 match self.columns.get(index).and_then(|c| c.header_fill.as_ref()) {
789 Some(fill) => self.header_style.combine(fill),
790 None => self.header_style.clone(),
791 }
792 } else {
793 self.columns
794 .get(index)
795 .map(|c| c.style.clone())
796 .unwrap_or_default()
797 }
798 }
799
800 fn vertical_padding(&self, first_row: bool, last_row: bool) -> (usize, usize) {
809 let (mut top, _, mut bottom, _) = self.padding;
810 if self.collapse_padding && !last_row {
811 bottom = top.saturating_sub(bottom);
812 }
813 if !self.pad_edge {
814 if first_row {
815 top = 0;
816 }
817 if last_row {
818 bottom = 0;
819 }
820 }
821 (top, bottom)
822 }
823
824 fn pad_cell_lines(
825 &self,
826 lines: Vec<Vec<Segment>>,
827 width: usize,
828 (cpl, cpr): (usize, usize),
829 (pt, pb): (usize, usize),
830 style: &Style,
831 ) -> Vec<Vec<Segment>> {
832 let cell_fill = Some(style.clone());
833 let cell_width = cpl + width + cpr;
834 if cell_width == 0 {
838 return Vec::new();
839 }
840 let blank = || vec![Segment::new(" ".repeat(cell_width), cell_fill.clone())];
841 let mut padded_lines: Vec<Vec<Segment>> = Vec::new();
842 for _ in 0..pt {
843 padded_lines.push(blank());
844 }
845 for line in &lines {
846 let mut row = Vec::new();
847 if cpl > 0 {
848 row.push(Segment::new(" ".repeat(cpl), cell_fill.clone()));
849 }
850 row.extend(Segment::adjust_line_length(line, width, cell_fill.clone()));
852 if cpr > 0 {
853 row.push(Segment::new(" ".repeat(cpr), cell_fill.clone()));
854 }
855 padded_lines.push(row);
856 }
857 for _ in 0..pb {
858 padded_lines.push(blank());
859 }
860 padded_lines
861 }
862
863 #[allow(clippy::too_many_arguments)]
865 fn render_row(
866 &self,
867 console: &Console,
868 options: &ConsoleOptions,
869 cells: &[Cell],
870 rendered_widths: &[usize],
871 is_header: bool,
872 (first_row, last_row): (bool, bool),
873 edges: Option<(char, char, char)>,
874 ) -> Vec<Vec<Segment>> {
875 let vertical = self.vertical_padding(first_row, last_row);
878 let border = Some(self.style.combine(&self.border_style));
879 let ncols = self.columns.len();
880 let paddings: Vec<(usize, usize)> = (0..ncols)
883 .map(|index| {
884 let rendered = rendered_widths.get(index).copied().unwrap_or(0);
885 self.cell_padding_fitted(index, ncols, rendered)
886 })
887 .collect();
888 let content_widths: Vec<usize> = rendered_widths
889 .iter()
890 .zip(&paddings)
891 .map(|(w, (pl, pr))| w.saturating_sub(pl + pr))
892 .collect();
893
894 let mut cell_lines: Vec<Vec<Vec<Segment>>> = Vec::with_capacity(ncols);
896 let mut height = 1;
897 for (index, width) in content_widths.iter().enumerate() {
898 let style = self.cell_style(index, is_header);
899 let column = self.columns.get(index);
900 let mut text = match cells.get(index) {
901 Some(Cell::Text(text)) => text.clone(),
902 Some(Cell::Markup(markup)) => console.render_str(
905 markup,
906 Some(column.and_then(|c| c.highlight).unwrap_or(self.highlight)),
907 ),
908 None => Text::default(),
909 Some(Cell::Renderable(renderable)) => {
910 let mut cell_options = options.update_width(*width);
914 cell_options.justify = column.map_or(Justify::Left, |c| c.justify);
915 cell_options.no_wrap = Some(column.is_some_and(|c| c.no_wrap));
916 cell_options.overflow = Some(column.map_or(Overflow::Ellipsis, |c| c.overflow));
917 let lines = if *width == 0 {
918 Vec::new()
919 } else {
920 console.render_lines_styled(
921 renderable.as_ref(),
922 &cell_options,
923 Some(&style),
924 true,
925 )
926 };
927 cell_lines.push(self.pad_cell_lines(
928 lines,
929 *width,
930 paddings[index],
931 vertical,
932 &style,
933 ));
934 height = height.max(cell_lines.last().map_or(0, Vec::len));
935 continue;
936 }
937 };
938 let justify = match text.get_justify() {
943 Justify::Default => column.map(|c| c.justify).unwrap_or(Justify::Left),
944 own => own,
945 };
946 let overflow = text
947 .get_overflow()
948 .unwrap_or_else(|| column.map_or(Overflow::Ellipsis, |c| c.overflow));
949 let no_wrap = text
950 .get_no_wrap()
951 .unwrap_or_else(|| column.map(|c| c.no_wrap).unwrap_or(false));
952 if is_header {
955 if let Some(span) = column.and_then(|c| c.header_content_style.clone()) {
956 let len = text.plain().len();
957 text.stylize(span, 0, len);
958 }
959 }
960 let mut lines: Vec<Vec<Segment>> = if *width == 0 {
972 Vec::new()
973 } else {
974 text.render_lines_wrapped(
975 console.theme(),
976 &Style::new(),
977 Some(*width),
978 justify,
979 overflow,
980 no_wrap,
981 )
982 .iter()
983 .map(|line| Segment::apply_style(&Segment::simplify(line), &style))
984 .collect()
985 };
986 if lines.is_empty() && *width > 0 {
987 lines.push(Vec::new());
988 }
989 let padded_lines =
990 self.pad_cell_lines(lines, *width, paddings[index], vertical, &style);
991 height = height.max(padded_lines.len());
992 cell_lines.push(padded_lines);
993 }
994
995 let row_height = cell_lines.iter().map(Vec::len).max().unwrap_or(0);
1001 for (index, lines) in cell_lines.iter_mut().enumerate() {
1002 let (cpl, cpr) = paddings[index];
1003 let blank = " ".repeat(cpl + content_widths[index] + cpr);
1004 let filler = vec![Segment::new(
1005 blank.clone(),
1006 Some(self.cell_style(index, is_header)),
1007 )];
1008 let missing = row_height.saturating_sub(lines.len());
1009 if is_header {
1010 lines.splice(0..0, std::iter::repeat_n(filler, missing));
1011 } else {
1012 lines.extend(std::iter::repeat_n(filler, missing));
1013 }
1014 while lines.len() < height {
1015 lines.push(vec![Segment::new(blank.clone(), None)]);
1016 }
1017 }
1018
1019 let last = ncols.saturating_sub(1);
1020 let mut rows_out: Vec<Vec<Segment>> = Vec::with_capacity(height);
1021 #[allow(clippy::needless_range_loop)]
1024 for r in 0..height {
1025 let mut row = Vec::new();
1026 if let (Some((edge_left, _, _)), true) = (edges, self.show_edge) {
1027 row.push(Segment::new(edge_left.to_string(), border.clone()));
1028 }
1029 for (c, column_lines) in cell_lines.iter().enumerate() {
1030 row.extend(column_lines[r].clone());
1031 let Some((_, edge_vertical, edge_right)) = edges else {
1032 continue;
1033 };
1034 if c != last {
1035 row.push(Segment::new(edge_vertical.to_string(), border.clone()));
1036 } else if self.show_edge {
1037 row.push(Segment::new(edge_right.to_string(), border.clone()));
1038 }
1039 }
1040 rows_out.push(row);
1041 }
1042 rows_out
1043 }
1044}
1045
1046impl LineRenderable for Table {
1047 fn try_for_each_line<E>(
1055 &self,
1056 console: &Console,
1057 options: &ConsoleOptions,
1058 mut emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
1059 ) -> Result<(), E> {
1060 if self.columns.is_empty() {
1061 return emit(vec![Segment::new("", None)]);
1062 }
1063 let box_set = self.box_set.substitute(
1065 console.legacy_windows(),
1066 console.safe_box(),
1067 console.ascii_only(),
1068 );
1069 let extra_width = self.extra_width();
1070 let available = options.max_width.saturating_sub(extra_width);
1071
1072 let rendered_widths = self.column_widths(console, options, available);
1073 let border = Some(self.style.combine(&self.border_style));
1074
1075 let table_width: usize = rendered_widths.iter().sum::<usize>() + extra_width;
1077
1078 if let Some(title) = self.title.as_ref().filter(|title| !title.is_empty()) {
1080 for line in render_annotation(console, options, title, "table.title", table_width) {
1081 emit(line)?;
1082 }
1083 }
1084
1085 let edge = self.show_edge;
1086 let boxed = !self.no_box;
1087 if boxed && edge {
1088 emit(vec![Segment::new(
1089 box_set.get_top(&rendered_widths, edge),
1090 border.clone(),
1091 )])?;
1092 }
1093
1094 let head_edges =
1095 boxed.then_some((box_set.head_left, box_set.head_vertical, box_set.head_right));
1096 let body_edges =
1097 boxed.then_some((box_set.mid_left, box_set.mid_vertical, box_set.mid_right));
1098
1099 if self.show_header {
1100 let headers: Vec<Cell> = self.columns.iter().map(|c| c.header.clone()).collect();
1101 for line in self.render_row(
1102 console,
1103 options,
1104 &headers,
1105 &rendered_widths,
1106 true,
1107 (true, self.rows.is_empty()),
1108 head_edges,
1109 ) {
1110 emit(line)?;
1111 }
1112 if boxed {
1113 emit(vec![Segment::new(
1114 box_set.get_row(&rendered_widths, RowLevel::Head, edge),
1115 border.clone(),
1116 )])?;
1117 }
1118 }
1119
1120 let row_last = self.rows.len().saturating_sub(1);
1121 for (index, row) in self.rows.iter().enumerate() {
1122 let place = (!self.show_header && index == 0, index == row_last);
1123 for line in self.render_row(
1124 console,
1125 options,
1126 row,
1127 &rendered_widths,
1128 false,
1129 place,
1130 body_edges,
1131 ) {
1132 emit(line)?;
1133 }
1134 if boxed && self.show_lines && index != row_last {
1135 emit(vec![Segment::new(
1136 box_set.get_row(&rendered_widths, RowLevel::Row, edge),
1137 border.clone(),
1138 )])?;
1139 }
1140 }
1141
1142 if boxed && edge {
1143 emit(vec![Segment::new(
1144 box_set.get_bottom(&rendered_widths, edge),
1145 border.clone(),
1146 )])?;
1147 }
1148
1149 if let Some(caption) = self.caption.as_ref().filter(|caption| !caption.is_empty()) {
1151 for line in render_annotation(console, options, caption, "table.caption", table_width) {
1152 emit(line)?;
1153 }
1154 }
1155
1156 Ok(())
1157 }
1158}
1159
1160impl crate::protocol::OwnedTableRows for Table {
1161 fn extend_owned_rows(&mut self, rows: Vec<Vec<String>>) -> &mut Self {
1162 self.rows.extend(
1163 rows.into_iter()
1164 .map(|row| row.into_iter().map(Cell::Markup).collect::<Vec<_>>()),
1165 );
1166 self
1167 }
1168}
1169
1170impl Renderable for Table {
1171 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
1174 if self.columns.is_empty() {
1175 let width = usize::from(!self.no_box && self.show_edge);
1179 return Measurement::new(width, width);
1180 }
1181 let extra_width = self.extra_width();
1182 let max_width: usize = self
1183 .column_widths(
1184 console,
1185 options,
1186 options.max_width.saturating_sub(extra_width),
1187 )
1188 .iter()
1189 .sum();
1190 let options = options.update_width(max_width);
1191 let (minimum, maximum) = (0..self.columns.len())
1192 .map(|index| self.measure_column(console, &options, index))
1193 .fold((0, 0), |(minimum, maximum), width| {
1194 (minimum + width.minimum, maximum + width.maximum)
1195 });
1196 Measurement::new(minimum + extra_width, maximum + extra_width)
1197 }
1198
1199 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1200 let mut segments = Vec::new();
1201 let mut first = true;
1202 let result: Result<(), std::convert::Infallible> =
1203 self.try_for_each_line(console, options, |line| {
1204 if !first {
1205 segments.push(Segment::line());
1206 }
1207 first = false;
1208 segments.extend(line);
1209 Ok(())
1210 });
1211 match result {
1212 Ok(()) => segments,
1213 Err(never) => match never {},
1214 }
1215 }
1216}
1217
1218fn render_annotation(
1221 console: &Console,
1222 options: &ConsoleOptions,
1223 annotation: &str,
1224 style: &str,
1225 width: usize,
1226) -> Vec<Vec<Segment>> {
1227 let expanded = console.expand_emoji(annotation);
1228 let mut text = Text::from_markup(&expanded).unwrap_or_else(|_| Text::new(expanded));
1229 text.set_base_style(style);
1230 let overflow = options.overflow.unwrap_or(Overflow::Fold);
1231 let no_wrap = options.no_wrap.unwrap_or(false) || overflow == Overflow::Ignore;
1232 let mut lines = Vec::new();
1233 for mut hard_line in text.split("\n", false, true) {
1234 hard_line.expand_tabs(DEFAULT_TAB_SIZE);
1235 let wrapped = if no_wrap {
1236 vec![hard_line]
1237 } else {
1238 let char_offsets: Vec<usize> = hard_line
1239 .plain()
1240 .char_indices()
1241 .map(|(i, _)| i)
1242 .chain(std::iter::once(hard_line.plain().len()))
1243 .collect();
1244 let breaks: Vec<usize> =
1245 crate::wrap::divide_line(hard_line.plain(), width, overflow == Overflow::Fold)
1246 .into_iter()
1247 .map(|i| char_offsets[i])
1248 .collect();
1249 hard_line.divide(&breaks)
1250 };
1251 for mut line in wrapped {
1252 if overflow != Overflow::Ignore {
1253 line.rstrip();
1257 line.truncate(width, Some(overflow), false);
1258 line.pad_left(width.saturating_sub(line.cell_len()) / 2, ' ');
1259 line.pad_right(width.saturating_sub(line.cell_len()), ' ');
1260 line.truncate(width, Some(overflow), false);
1261 }
1262 lines.push(line.render(console.theme(), console.base_style()));
1263 }
1264 }
1265 lines
1266}
1267
1268fn round_half_even(value: f64) -> i64 {
1270 let floor = value.floor();
1271 let diff = value - floor;
1272 if (diff - 0.5).abs() < 1e-9 {
1273 let f = floor as i64;
1274 if f % 2 == 0 {
1275 f
1276 } else {
1277 f + 1
1278 }
1279 } else {
1280 value.round() as i64
1281 }
1282}
1283
1284fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
1287 let ratios: Vec<i64> = ratios
1288 .iter()
1289 .zip(maximums)
1290 .map(|(&r, &m)| if m != 0 { r } else { 0 })
1291 .collect();
1292 let mut total_ratio: i64 = ratios.iter().sum();
1293 if total_ratio == 0 {
1294 return values.to_vec();
1295 }
1296 let mut total_remaining = total;
1297 let mut result = Vec::with_capacity(values.len());
1298 for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
1299 if ratio != 0 && total_ratio > 0 {
1300 let distributed = maximum.min(round_half_even(
1301 ratio as f64 * total_remaining as f64 / total_ratio as f64,
1302 ));
1303 result.push(value - distributed);
1304 total_remaining -= distributed;
1305 total_ratio -= ratio;
1306 } else {
1307 result.push(value);
1308 }
1309 }
1310 result
1311}
1312
1313fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
1317 let ratios: Vec<i64> = match minimums {
1319 Some(mins) => ratios
1320 .iter()
1321 .zip(mins)
1322 .map(|(&r, &m)| if m != 0 { r } else { 0 })
1323 .collect(),
1324 None => ratios.to_vec(),
1325 };
1326 let mut total_ratio: i64 = ratios.iter().sum();
1327 let mut total_remaining = total;
1328 let mut result = Vec::with_capacity(ratios.len());
1329 for (index, &ratio) in ratios.iter().enumerate() {
1330 let minimum = minimums.map_or(0, |m| m[index]);
1331 let distributed = if total_ratio > 0 {
1332 let numerator = ratio * total_remaining;
1335 let ceil_div = (numerator + total_ratio - 1) / total_ratio;
1336 minimum.max(ceil_div)
1337 } else {
1338 total_remaining
1339 };
1340 result.push(distributed);
1341 total_ratio -= ratio;
1342 total_remaining -= distributed;
1343 }
1344 result
1345}
1346
1347fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
1350 let mut total_width: i64 = widths.iter().sum();
1351 let mut excess_width = total_width - max_width;
1352 if wrapable.iter().any(|&w| w) {
1353 while total_width != 0 && excess_width > 0 {
1354 let max_column = widths
1355 .iter()
1356 .zip(wrapable)
1357 .filter(|(_, &w)| w)
1358 .map(|(&x, _)| x)
1359 .max()
1360 .unwrap_or(0);
1361 let second_max_column = widths
1362 .iter()
1363 .zip(wrapable)
1364 .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
1365 .max()
1366 .unwrap_or(0);
1367 let column_difference = max_column - second_max_column;
1368 let ratios: Vec<i64> = widths
1369 .iter()
1370 .zip(wrapable)
1371 .map(|(&x, &w)| i64::from(x == max_column && w))
1372 .collect();
1373 if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
1374 break;
1375 }
1376 let max_reduce = vec![excess_width.min(column_difference); widths.len()];
1377 widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
1378 total_width = widths.iter().sum();
1379 excess_width = total_width - max_width;
1380 }
1381 }
1382 widths
1383}
1384
1385#[cfg(test)]
1386mod tests {
1387 use super::*;
1388 use crate::color::ColorSystem;
1389 use crate::r#box::SQUARE;
1390
1391 fn console() -> Console {
1392 Console::builder()
1393 .force_terminal(true)
1394 .color_system(Some(ColorSystem::Truecolor))
1395 .width(40)
1396 .no_color(false)
1397 .build()
1398 }
1399
1400 #[test]
1401 fn owned_rows_preserve_measurement_styles_and_missing_cells() {
1402 use crate::protocol::OwnedTableRows;
1403 for width in [1, 12, 40, 80] {
1404 let console = Console::builder().width(width).force_terminal(true).build();
1405 let build = || {
1406 let mut table = Table::new()
1407 .title("Rows")
1408 .caption("owned or borrowed")
1409 .show_lines(true);
1410 table.add_column("Name");
1411 table.add_column_justify("Value", Justify::Right);
1412 table
1413 };
1414 let mut borrowed = build();
1415 let mut owned = build();
1416 for row in [
1417 vec!["漢字\n🙂", "123"],
1418 vec!["short"],
1419 vec!["extra", "4", "ignored"],
1420 ] {
1421 borrowed.add_row(&row);
1422 owned.extend_owned_rows(vec![row.into_iter().map(str::to_owned).collect()]);
1423 }
1424 assert_eq!(
1425 console.render_to_string(&borrowed),
1426 console.render_to_string(&owned)
1427 );
1428 }
1429 }
1430
1431 #[test]
1432 fn simple_square_table() {
1433 let mut table = Table::new().box_set(SQUARE);
1434 table.add_column("Name");
1435 table.add_column("Age");
1436 table.add_row(&["Alice", "30"]);
1437 table.add_row(&["Bob", "7"]);
1438 let out = console().render_export(&table);
1439 let expected = concat!(
1440 "┌───────┬─────┐\n",
1441 "│\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",
1442 "├───────┼─────┤\n",
1443 "│ Alice │ 30 │\n",
1444 "│ Bob │ 7 │\n",
1445 "└───────┴─────┘\n",
1446 );
1447 assert_eq!(out, expected);
1448 }
1449
1450 #[test]
1451 fn streamed_lines_match_styled_table_output() {
1452 let mut table = Table::new().box_set(SQUARE);
1453 table.add_column("Name");
1454 table.add_column("Age");
1455 table.add_row(&["Alice", "30"]);
1456 table.add_row(&["Bob", "7"]);
1457 let console = console();
1458 let mut streamed = String::new();
1459 table
1460 .try_for_each_line(&console, &console.options(), |line| {
1461 assert!(line.iter().all(|segment| !segment.text.contains('\n')));
1462 streamed.push_str(&console.segments_to_string(&line));
1463 streamed.push('\n');
1464 Ok::<_, std::convert::Infallible>(())
1465 })
1466 .unwrap();
1467 assert_eq!(streamed, console.render_export(&table));
1470 assert_eq!(streamed.lines().count(), 6);
1471 }
1472
1473 #[test]
1474 fn streamed_lines_stop_at_the_first_writer_error() {
1475 let mut table = Table::new()
1476 .box_set(SQUARE)
1477 .title("People")
1478 .caption("End")
1479 .show_lines(true);
1480 table.add_column("Name");
1481 table.add_row(&["Alice\nBob"]);
1482 table.add_row(&["Carol"]);
1483 let console = console();
1484 let mut visits = 0;
1485 let result = table.try_for_each_line(&console, &console.options(), |_| {
1486 visits += 1;
1487 if visits == 5 {
1488 Err("writer failed")
1489 } else {
1490 Ok(())
1491 }
1492 });
1493 assert_eq!(result, Err("writer failed"));
1494 assert_eq!(visits, 5);
1495 }
1496
1497 #[test]
1502 fn a_column_narrower_than_its_padding_stays_inside_the_border() {
1503 for ncols in [20usize, 29, 40] {
1504 let mut table = Table::new().box_set(SQUARE);
1505 for i in 0..ncols {
1506 table.add_column(format!("c{i}"));
1507 }
1508 let row: Vec<String> = (0..ncols).map(|i| i.to_string()).collect();
1509 table.add_row(&row.iter().map(String::as_str).collect::<Vec<_>>());
1510 let console = Console::builder().width(80).color_system(None).build();
1511 let out = console.render_to_string(&table);
1512 let rows: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
1513 let widths: Vec<usize> = rows.iter().map(|r| r.chars().count()).collect();
1514 assert!(
1515 widths.iter().all(|w| *w == widths[0]),
1516 "{ncols} columns produced ragged rows: {widths:?}"
1517 );
1518 for (index, row) in rows.iter().enumerate() {
1519 let last = row.chars().last().expect("non-empty row");
1520 assert!(
1521 !last.is_whitespace(),
1522 "{ncols} columns: row {index} lost its right border: {row:?}"
1523 );
1524 }
1525 }
1526 }
1527
1528 #[test]
1533 fn a_multi_line_cell_is_measured_by_its_widest_line() {
1534 let mut table = Table::new().box_set(SQUARE);
1535 table.add_column("name");
1536 table.add_column("bio");
1537 table.add_row(&["Alice", "line one\nline two is much longer"]);
1538 table.add_row(&["Bob", "short"]);
1539 let console = Console::builder().width(60).color_system(None).build();
1540 let out = console.render_to_string(&table);
1541 let top = out.lines().next().expect("a top border");
1542 let width = top.chars().count();
1543 assert!(
1545 width < 40,
1546 "the multi-line cell was measured as the sum of its lines: {width} wide"
1547 );
1548 assert!(
1549 out.contains("line two is much longer"),
1550 "content lost: {out:?}"
1551 );
1552 }
1553}