1use crate::cells::{cell_len, set_cell_size};
17use crate::console::{Console, ConsoleOptions, Justify, Overflow};
18use crate::protocol::{LineRenderable, Renderable};
19use crate::r#box::{Box as BoxSet, RowLevel, HEAVY_HEAD};
20use crate::segment::Segment;
21use crate::style::Style;
22use crate::text::{Text, DEFAULT_TAB_SIZE};
23use crate::theme::Theme;
24
25struct Column {
27 header: String,
28 justify: Justify,
29 width: Option<usize>,
31 style: Style,
33 header_content_style: Option<Style>,
37 header_fill: Option<Style>,
41 ratio: Option<usize>,
44 min_width: Option<usize>,
46 max_width: Option<usize>,
49 no_wrap: bool,
51}
52
53pub struct Table {
55 columns: Vec<Column>,
56 rows: Vec<Vec<String>>,
57 box_set: BoxSet,
58 show_header: bool,
59 show_lines: bool,
60 show_edge: bool,
61 pad_edge: bool,
62 collapse_padding: bool,
63 expand: bool,
64 title: Option<String>,
65 caption: Option<String>,
66 padding: (usize, usize, usize, usize),
67 header_style: Style,
68 border_style: Style,
69 style: Style,
70}
71
72impl Default for Table {
73 fn default() -> Self {
74 Table {
75 columns: Vec::new(),
76 rows: Vec::new(),
77 box_set: HEAVY_HEAD,
78 show_header: true,
79 show_lines: false,
80 show_edge: true,
81 pad_edge: true,
82 collapse_padding: false,
83 expand: false,
84 title: None,
85 caption: None,
86 padding: (0, 1, 0, 1),
87 header_style: Style::parse("bold").expect("valid built-in style"),
88 border_style: Style::new(),
89 style: Style::new(),
90 }
91 }
92}
93
94impl Table {
95 pub fn new() -> Self {
96 Table::default()
97 }
98
99 pub fn box_set(mut self, box_set: BoxSet) -> Self {
101 self.box_set = box_set;
102 self
103 }
104
105 pub fn border_style(mut self, style: Style) -> Self {
108 self.border_style = style;
109 self
110 }
111
112 pub fn show_header(mut self, show: bool) -> Self {
114 self.show_header = show;
115 self
116 }
117
118 pub fn expand(mut self, expand: bool) -> Self {
120 self.expand = expand;
121 self
122 }
123
124 pub fn show_lines(mut self, show: bool) -> Self {
126 self.show_lines = show;
127 self
128 }
129
130 pub fn show_edge(mut self, show: bool) -> Self {
133 self.show_edge = show;
134 self
135 }
136
137 pub fn pad_edge(mut self, pad: bool) -> Self {
140 self.pad_edge = pad;
141 self
142 }
143
144 pub fn collapse_padding(mut self, collapse: bool) -> Self {
147 self.collapse_padding = collapse;
148 self
149 }
150
151 pub fn style(mut self, style: Style) -> Self {
155 self.style = style;
156 self
157 }
158
159 fn cell_padding(&self, index: usize, ncols: usize) -> (usize, usize) {
162 let (_, pr, _, pl) = self.padding;
163 let mut left = if self.collapse_padding && index > 0 {
166 pl.saturating_sub(pr)
167 } else {
168 pl
169 };
170 let mut right = pr;
171 if !self.pad_edge && index == 0 {
173 left = 0;
174 }
175 if !self.pad_edge && index + 1 == ncols {
176 right = 0;
177 }
178 (left, right)
179 }
180
181 pub fn title(mut self, title: impl Into<String>) -> Self {
183 self.title = Some(title.into());
184 self
185 }
186
187 pub fn caption(mut self, caption: impl Into<String>) -> Self {
189 self.caption = Some(caption.into());
190 self
191 }
192
193 pub fn add_column(&mut self, header: impl Into<String>) -> &mut Self {
195 self.add_column_justify(header, Justify::Left)
196 }
197
198 pub fn add_column_justify(&mut self, header: impl Into<String>, justify: Justify) -> &mut Self {
200 self.columns.push(Column {
201 header: header.into(),
202 justify,
203 width: None,
204 style: Style::new(),
205 header_content_style: None,
206 header_fill: None,
207 ratio: None,
208 min_width: None,
209 max_width: None,
210 no_wrap: false,
211 });
212 self
213 }
214
215 pub fn column_width(&mut self, width: usize) -> &mut Self {
219 if let Some(column) = self.columns.last_mut() {
220 column.width = Some(width);
221 }
222 self
223 }
224
225 pub fn column_ratio(&mut self, ratio: usize) -> &mut Self {
229 if let Some(column) = self.columns.last_mut() {
230 column.ratio = Some(ratio);
231 }
232 self
233 }
234
235 pub fn column_min_width(&mut self, min_width: usize) -> &mut Self {
238 if let Some(column) = self.columns.last_mut() {
239 column.min_width = Some(min_width);
240 }
241 self
242 }
243
244 pub fn column_max_width(&mut self, max_width: usize) -> &mut Self {
247 if let Some(column) = self.columns.last_mut() {
248 column.max_width = Some(max_width);
249 }
250 self
251 }
252
253 pub fn column_style(&mut self, style: Style) -> &mut Self {
256 if let Some(column) = self.columns.last_mut() {
257 column.style = style;
258 }
259 self
260 }
261
262 pub fn column_header_style(&mut self, style: Style) -> &mut Self {
266 if let Some(column) = self.columns.last_mut() {
267 column.header_content_style = Some(style);
268 }
269 self
270 }
271
272 pub fn column_header_fill(&mut self, style: Style) -> &mut Self {
276 if let Some(column) = self.columns.last_mut() {
277 column.header_fill = Some(style);
278 }
279 self
280 }
281
282 pub fn column_no_wrap(&mut self) -> &mut Self {
285 if let Some(column) = self.columns.last_mut() {
286 column.no_wrap = true;
287 }
288 self
289 }
290
291 pub fn add_row(&mut self, cells: &[&str]) -> &mut Self {
293 self.rows
294 .push(cells.iter().map(|s| s.to_string()).collect());
295 self
296 }
297
298 fn block_width(text: &str) -> usize {
307 text.split('\n').map(cell_len).max().unwrap_or(0)
308 }
309
310 fn max_content_widths(&self) -> Vec<usize> {
311 let mut widths = vec![0usize; self.columns.len()];
312 for (index, column) in self.columns.iter().enumerate() {
313 if self.show_header {
314 widths[index] = Self::block_width(&column.header);
315 }
316 }
317 for row in &self.rows {
318 for (index, cell) in row.iter().enumerate() {
319 if index < widths.len() {
320 widths[index] = widths[index].max(Self::block_width(cell));
321 }
322 }
323 }
324 widths
325 }
326
327 fn column_widths(&self, available: usize) -> Vec<usize> {
331 let ncols = self.columns.len();
332 let content = self.max_content_widths();
335 let mut widths: Vec<i64> = self
336 .columns
337 .iter()
338 .zip(&content)
339 .enumerate()
340 .map(|(index, (column, &measured))| {
341 let (pl, pr) = self.cell_padding(index, ncols);
342 let content_width = match column.width {
343 Some(w) => w,
344 None => {
345 let mut w = measured;
346 if let Some(min) = column.min_width {
347 w = w.max(min);
348 }
349 if let Some(max) = column.max_width {
350 w = w.min(max);
351 }
352 w
353 }
354 };
355 (content_width + pl + pr) as i64
356 })
357 .collect();
358
359 if self.expand {
363 let ratios: Vec<i64> = self
364 .columns
365 .iter()
366 .filter(|c| c.ratio.is_some())
367 .map(|c| c.ratio.unwrap() as i64)
368 .collect();
369 if ratios.iter().any(|&r| r > 0) {
370 let fixed_widths: Vec<i64> = widths
371 .iter()
372 .zip(&self.columns)
373 .map(|(&w, c)| if c.ratio.is_some() { 0 } else { w })
374 .collect();
375 let flex_minimum: Vec<i64> = self
376 .columns
377 .iter()
378 .enumerate()
379 .filter(|(_, c)| c.ratio.is_some())
380 .map(|(index, c)| {
381 let (pl, pr) = self.cell_padding(index, ncols);
382 (c.width.unwrap_or(1) + pl + pr) as i64
383 })
384 .collect();
385 let flexible_width = available as i64 - fixed_widths.iter().sum::<i64>();
386 let flex_widths = ratio_distribute(flexible_width, &ratios, Some(&flex_minimum));
387 let mut iter_flex = flex_widths.into_iter();
388 for (index, column) in self.columns.iter().enumerate() {
389 if column.ratio.is_some() {
390 widths[index] = fixed_widths[index] + iter_flex.next().unwrap_or(0);
391 }
392 }
393 }
394 }
395
396 let table_width: i64 = widths.iter().sum();
397 if table_width > available as i64 {
398 let wrapable: Vec<bool> = self
401 .columns
402 .iter()
403 .map(|c| c.width.is_none() && !c.no_wrap)
404 .collect();
405 widths = collapse_widths(widths, &wrapable, available as i64);
406 let table_width: i64 = widths.iter().sum();
409 if table_width > available as i64 {
410 let excess = table_width - available as i64;
411 let ratios = vec![1i64; widths.len()];
412 widths = ratio_reduce(excess, &ratios, &widths, &widths);
413 }
414 }
415
416 let table_width: i64 = widths.iter().sum();
419 if self.expand && table_width < available as i64 && table_width > 0 {
420 let pad = ratio_distribute(available as i64 - table_width, &widths, None);
421 for (width, extra) in widths.iter_mut().zip(pad) {
422 *width += extra;
423 }
424 }
425 widths.into_iter().map(|w| w.max(0) as usize).collect()
426 }
427
428 fn cell_padding_fitted(&self, index: usize, ncols: usize, rendered: usize) -> (usize, usize) {
438 let (mut pl, mut pr) = self.cell_padding(index, ncols);
439 while pl + pr > rendered {
440 if pr > pl {
441 pr -= 1;
442 } else if pl > 0 {
443 pl -= 1;
444 } else {
445 break;
446 }
447 }
448 (pl, pr)
449 }
450
451 fn cell_style(&self, index: usize, is_header: bool) -> Style {
454 if is_header {
455 match self.columns.get(index).and_then(|c| c.header_fill.as_ref()) {
457 Some(fill) => self.header_style.combine(fill),
458 None => self.header_style.clone(),
459 }
460 } else {
461 self.columns
462 .get(index)
463 .map(|c| c.style.clone())
464 .unwrap_or_default()
465 }
466 }
467
468 fn render_row(
470 &self,
471 theme: &Theme,
472 cells: &[String],
473 rendered_widths: &[usize],
474 is_header: bool,
475 edges: (char, char, char),
476 ) -> Vec<Vec<Segment>> {
477 let (pt, _, pb, _) = self.padding;
480 let (edge_left, edge_vertical, edge_right) = edges;
481 let border = Some(self.style.combine(&self.border_style));
482 let ncols = self.columns.len();
483 let paddings: Vec<(usize, usize)> = (0..ncols)
486 .map(|index| {
487 let rendered = rendered_widths.get(index).copied().unwrap_or(0);
488 self.cell_padding_fitted(index, ncols, rendered)
489 })
490 .collect();
491 let content_widths: Vec<usize> = rendered_widths
492 .iter()
493 .zip(&paddings)
494 .map(|(w, (pl, pr))| w.saturating_sub(pl + pr))
495 .collect();
496
497 let mut cell_lines: Vec<Vec<Vec<Segment>>> = Vec::with_capacity(ncols);
499 let mut height = 1;
500 for (index, width) in content_widths.iter().enumerate() {
501 let style = self.cell_style(index, is_header);
502 let cell_fill = Some(style.clone());
503 let content = cells.get(index).map(String::as_str).unwrap_or("");
504 let column = self.columns.get(index);
505 let justify = column.map(|c| c.justify).unwrap_or(Justify::Left);
506 let no_wrap = column.map(|c| c.no_wrap).unwrap_or(false);
507 let wrapped = if no_wrap {
510 ellipsis_crop(content, *width)
511 } else {
512 wrap_cell(content, *width).join("\n")
513 };
514 let mut text = Text::new(wrapped).justify(justify);
515 if is_header {
518 if let Some(span) = column.and_then(|c| c.header_content_style.clone()) {
519 let len = text.plain().len();
520 text.stylize(span, 0, len);
521 }
522 }
523 let mut lines = text.render_lines(theme, &style, Some(*width));
524 if lines.is_empty() {
525 lines.push(Vec::new());
526 }
527 let blank = || Segment::new(" ".repeat(*width), cell_fill.clone());
529 let mut padded_lines: Vec<Vec<Segment>> = Vec::new();
530 for _ in 0..pt {
531 padded_lines.push(vec![blank()]);
532 }
533 for line in &lines {
534 let padded = Segment::adjust_line_length(line, *width, cell_fill.clone());
535 padded_lines.push(Segment::simplify(&padded));
536 }
537 for _ in 0..pb {
538 padded_lines.push(vec![blank()]);
539 }
540 height = height.max(padded_lines.len());
541 cell_lines.push(padded_lines);
542 }
543
544 for (index, lines) in cell_lines.iter_mut().enumerate() {
546 let fill = Some(self.cell_style(index, is_header));
547 while lines.len() < height {
548 lines.push(vec![Segment::new(
549 " ".repeat(content_widths[index]),
550 fill.clone(),
551 )]);
552 }
553 }
554
555 let last = ncols.saturating_sub(1);
556 let mut rows_out: Vec<Vec<Segment>> = Vec::with_capacity(height);
557 #[allow(clippy::needless_range_loop)]
560 for r in 0..height {
561 let mut row = Vec::new();
562 if self.show_edge {
563 row.push(Segment::new(edge_left.to_string(), border.clone()));
564 }
565 for (c, column_lines) in cell_lines.iter().enumerate() {
566 let fill = Some(self.cell_style(c, is_header));
567 let (cpl, cpr) = paddings[c];
568 if cpl > 0 {
569 row.push(Segment::new(" ".repeat(cpl), fill.clone()));
570 }
571 row.extend(column_lines[r].clone());
572 if cpr > 0 {
573 row.push(Segment::new(" ".repeat(cpr), fill.clone()));
574 }
575 if c != last {
576 row.push(Segment::new(edge_vertical.to_string(), border.clone()));
577 } else if self.show_edge {
578 row.push(Segment::new(edge_right.to_string(), border.clone()));
579 }
580 }
581 rows_out.push(row);
582 }
583 rows_out
584 }
585}
586
587impl LineRenderable for Table {
588 fn try_for_each_line<E>(
596 &self,
597 console: &Console,
598 options: &ConsoleOptions,
599 mut emit: impl FnMut(Vec<Segment>) -> Result<(), E>,
600 ) -> Result<(), E> {
601 if self.columns.is_empty() {
602 return emit(vec![Segment::new("", None)]);
603 }
604 let box_set = self.box_set.substitute(
606 console.legacy_windows(),
607 console.safe_box(),
608 console.ascii_only(),
609 );
610 let ncols = self.columns.len();
611 let extra_width = (if self.show_edge { 2 } else { 0 }) + ncols.saturating_sub(1);
614 let available = options.max_width.saturating_sub(extra_width);
615
616 let rendered_widths = self.column_widths(available);
617 let border = Some(self.style.combine(&self.border_style));
618
619 let table_width: usize = rendered_widths.iter().sum::<usize>() + extra_width;
621
622 if let Some(title) = self.title.as_ref().filter(|title| !title.is_empty()) {
624 for line in render_annotation(console, options, title, "table.title", table_width) {
625 emit(line)?;
626 }
627 }
628
629 let edge = self.show_edge;
630 if edge {
631 emit(vec![Segment::new(
632 box_set.get_top(&rendered_widths, edge),
633 border.clone(),
634 )])?;
635 }
636
637 let head_edges = (box_set.head_left, box_set.head_vertical, box_set.head_right);
638 let body_edges = (box_set.mid_left, box_set.mid_vertical, box_set.mid_right);
639
640 if self.show_header {
641 let headers: Vec<String> = self.columns.iter().map(|c| c.header.clone()).collect();
642 for line in self.render_row(
643 console.theme(),
644 &headers,
645 &rendered_widths,
646 true,
647 head_edges,
648 ) {
649 emit(line)?;
650 }
651 emit(vec![Segment::new(
652 box_set.get_row(&rendered_widths, RowLevel::Head, edge),
653 border.clone(),
654 )])?;
655 }
656
657 let row_last = self.rows.len().saturating_sub(1);
658 for (index, row) in self.rows.iter().enumerate() {
659 for line in self.render_row(console.theme(), row, &rendered_widths, false, body_edges) {
660 emit(line)?;
661 }
662 if self.show_lines && index != row_last {
663 emit(vec![Segment::new(
664 box_set.get_row(&rendered_widths, RowLevel::Row, edge),
665 border.clone(),
666 )])?;
667 }
668 }
669
670 if edge {
671 emit(vec![Segment::new(
672 box_set.get_bottom(&rendered_widths, edge),
673 border.clone(),
674 )])?;
675 }
676
677 if let Some(caption) = self.caption.as_ref().filter(|caption| !caption.is_empty()) {
679 for line in render_annotation(console, options, caption, "table.caption", table_width) {
680 emit(line)?;
681 }
682 }
683
684 Ok(())
685 }
686}
687
688impl Renderable for Table {
689 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
690 let mut segments = Vec::new();
691 let mut first = true;
692 let result: Result<(), std::convert::Infallible> =
693 self.try_for_each_line(console, options, |line| {
694 if !first {
695 segments.push(Segment::line());
696 }
697 first = false;
698 segments.extend(line);
699 Ok(())
700 });
701 match result {
702 Ok(()) => segments,
703 Err(never) => match never {},
704 }
705 }
706}
707
708fn wrap_cell(content: &str, width: usize) -> Vec<String> {
712 if width == 0 {
713 return vec![String::new()];
714 }
715 if content.contains('\n') {
721 return content
722 .split('\n')
723 .flat_map(|line| wrap_cell(line, width))
724 .collect();
725 }
726 let breaks = crate::wrap::divide_line(content, width, false);
729 let chars: Vec<char> = content.chars().collect();
730 let mut lines: Vec<String> = Vec::new();
731 let mut start = 0;
732 for stop in breaks {
733 lines.push(chars[start..stop].iter().collect());
734 start = stop;
735 }
736 lines.push(chars[start..].iter().collect());
737 lines
740 .iter()
741 .map(|line| ellipsis_crop(line.trim_end(), width))
742 .collect()
743}
744
745fn ellipsis_crop(text: &str, width: usize) -> String {
748 if cell_len(text) <= width {
749 return text.to_string();
750 }
751 if width == 0 {
752 return String::new();
753 }
754 format!("{}\u{2026}", set_cell_size(text, width - 1))
755}
756
757fn render_annotation(
760 console: &Console,
761 options: &ConsoleOptions,
762 annotation: &str,
763 style: &str,
764 width: usize,
765) -> Vec<Vec<Segment>> {
766 let expanded = console.expand_emoji(annotation);
767 let mut text = Text::from_markup(&expanded).unwrap_or_else(|_| Text::new(expanded));
768 text.set_base_style(style);
769 let overflow = options.overflow.unwrap_or(Overflow::Fold);
770 let no_wrap = options.no_wrap.unwrap_or(false) || overflow == Overflow::Ignore;
771 let mut lines = Vec::new();
772 for mut hard_line in text.split("\n", false, true) {
773 hard_line.expand_tabs(DEFAULT_TAB_SIZE);
774 let wrapped = if no_wrap {
775 vec![hard_line]
776 } else {
777 let char_offsets: Vec<usize> = hard_line
778 .plain()
779 .char_indices()
780 .map(|(i, _)| i)
781 .chain(std::iter::once(hard_line.plain().len()))
782 .collect();
783 let breaks: Vec<usize> =
784 crate::wrap::divide_line(hard_line.plain(), width, overflow == Overflow::Fold)
785 .into_iter()
786 .map(|i| char_offsets[i])
787 .collect();
788 hard_line.divide(&breaks)
789 };
790 for mut line in wrapped {
791 if overflow != Overflow::Ignore {
792 line.rstrip();
796 line.truncate(width, Some(overflow), false);
797 line.pad_left(width.saturating_sub(line.cell_len()) / 2, ' ');
798 line.pad_right(width.saturating_sub(line.cell_len()), ' ');
799 line.truncate(width, Some(overflow), false);
800 }
801 lines.push(line.render(console.theme(), console.base_style()));
802 }
803 }
804 lines
805}
806
807fn round_half_even(value: f64) -> i64 {
809 let floor = value.floor();
810 let diff = value - floor;
811 if (diff - 0.5).abs() < 1e-9 {
812 let f = floor as i64;
813 if f % 2 == 0 {
814 f
815 } else {
816 f + 1
817 }
818 } else {
819 value.round() as i64
820 }
821}
822
823fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
826 let ratios: Vec<i64> = ratios
827 .iter()
828 .zip(maximums)
829 .map(|(&r, &m)| if m != 0 { r } else { 0 })
830 .collect();
831 let mut total_ratio: i64 = ratios.iter().sum();
832 if total_ratio == 0 {
833 return values.to_vec();
834 }
835 let mut total_remaining = total;
836 let mut result = Vec::with_capacity(values.len());
837 for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
838 if ratio != 0 && total_ratio > 0 {
839 let distributed = maximum.min(round_half_even(
840 ratio as f64 * total_remaining as f64 / total_ratio as f64,
841 ));
842 result.push(value - distributed);
843 total_remaining -= distributed;
844 total_ratio -= ratio;
845 } else {
846 result.push(value);
847 }
848 }
849 result
850}
851
852fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
856 let ratios: Vec<i64> = match minimums {
858 Some(mins) => ratios
859 .iter()
860 .zip(mins)
861 .map(|(&r, &m)| if m != 0 { r } else { 0 })
862 .collect(),
863 None => ratios.to_vec(),
864 };
865 let mut total_ratio: i64 = ratios.iter().sum();
866 let mut total_remaining = total;
867 let mut result = Vec::with_capacity(ratios.len());
868 for (index, &ratio) in ratios.iter().enumerate() {
869 let minimum = minimums.map_or(0, |m| m[index]);
870 let distributed = if total_ratio > 0 {
871 let numerator = ratio * total_remaining;
874 let ceil_div = (numerator + total_ratio - 1) / total_ratio;
875 minimum.max(ceil_div)
876 } else {
877 total_remaining
878 };
879 result.push(distributed);
880 total_ratio -= ratio;
881 total_remaining -= distributed;
882 }
883 result
884}
885
886fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
889 let mut total_width: i64 = widths.iter().sum();
890 let mut excess_width = total_width - max_width;
891 if wrapable.iter().any(|&w| w) {
892 while total_width != 0 && excess_width > 0 {
893 let max_column = widths
894 .iter()
895 .zip(wrapable)
896 .filter(|(_, &w)| w)
897 .map(|(&x, _)| x)
898 .max()
899 .unwrap_or(0);
900 let second_max_column = widths
901 .iter()
902 .zip(wrapable)
903 .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
904 .max()
905 .unwrap_or(0);
906 let column_difference = max_column - second_max_column;
907 let ratios: Vec<i64> = widths
908 .iter()
909 .zip(wrapable)
910 .map(|(&x, &w)| i64::from(x == max_column && w))
911 .collect();
912 if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
913 break;
914 }
915 let max_reduce = vec![excess_width.min(column_difference); widths.len()];
916 widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
917 total_width = widths.iter().sum();
918 excess_width = total_width - max_width;
919 }
920 }
921 widths
922}
923
924#[cfg(test)]
925mod tests {
926 use super::*;
927 use crate::color::ColorSystem;
928 use crate::r#box::SQUARE;
929
930 fn console() -> Console {
931 Console::builder()
932 .force_terminal(true)
933 .color_system(Some(ColorSystem::Truecolor))
934 .width(40)
935 .no_color(false)
936 .build()
937 }
938
939 #[test]
940 fn simple_square_table() {
941 let mut table = Table::new().box_set(SQUARE);
942 table.add_column("Name");
943 table.add_column("Age");
944 table.add_row(&["Alice", "30"]);
945 table.add_row(&["Bob", "7"]);
946 let out = console().render_export(&table);
947 let expected = concat!(
948 "┌───────┬─────┐\n",
949 "│\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",
950 "├───────┼─────┤\n",
951 "│ Alice │ 30 │\n",
952 "│ Bob │ 7 │\n",
953 "└───────┴─────┘\n",
954 );
955 assert_eq!(out, expected);
956 }
957
958 #[test]
959 fn streamed_lines_match_styled_table_output() {
960 let mut table = Table::new().box_set(SQUARE);
961 table.add_column("Name");
962 table.add_column("Age");
963 table.add_row(&["Alice", "30"]);
964 table.add_row(&["Bob", "7"]);
965 let console = console();
966 let mut streamed = String::new();
967 table
968 .try_for_each_line(&console, &console.options(), |line| {
969 assert!(line.iter().all(|segment| !segment.text.contains('\n')));
970 streamed.push_str(&console.segments_to_string(&line));
971 streamed.push('\n');
972 Ok::<_, std::convert::Infallible>(())
973 })
974 .unwrap();
975 assert_eq!(streamed, console.render_export(&table));
978 assert_eq!(streamed.lines().count(), 6);
979 }
980
981 #[test]
982 fn streamed_lines_stop_at_the_first_writer_error() {
983 let mut table = Table::new()
984 .box_set(SQUARE)
985 .title("People")
986 .caption("End")
987 .show_lines(true);
988 table.add_column("Name");
989 table.add_row(&["Alice\nBob"]);
990 table.add_row(&["Carol"]);
991 let console = console();
992 let mut visits = 0;
993 let result = table.try_for_each_line(&console, &console.options(), |_| {
994 visits += 1;
995 if visits == 5 {
996 Err("writer failed")
997 } else {
998 Ok(())
999 }
1000 });
1001 assert_eq!(result, Err("writer failed"));
1002 assert_eq!(visits, 5);
1003 }
1004
1005 #[test]
1010 fn a_column_narrower_than_its_padding_stays_inside_the_border() {
1011 for ncols in [20usize, 29, 40] {
1012 let mut table = Table::new().box_set(SQUARE);
1013 for i in 0..ncols {
1014 table.add_column(format!("c{i}"));
1015 }
1016 let row: Vec<String> = (0..ncols).map(|i| i.to_string()).collect();
1017 table.add_row(&row.iter().map(String::as_str).collect::<Vec<_>>());
1018 let console = Console::builder().width(80).no_color(true).build();
1019 let out = console.render_to_string(&table);
1020 let rows: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
1021 let widths: Vec<usize> = rows.iter().map(|r| r.chars().count()).collect();
1022 assert!(
1023 widths.iter().all(|w| *w == widths[0]),
1024 "{ncols} columns produced ragged rows: {widths:?}"
1025 );
1026 for (index, row) in rows.iter().enumerate() {
1027 let last = row.chars().last().expect("non-empty row");
1028 assert!(
1029 !last.is_whitespace(),
1030 "{ncols} columns: row {index} lost its right border: {row:?}"
1031 );
1032 }
1033 }
1034 }
1035
1036 #[test]
1041 fn a_multi_line_cell_is_measured_by_its_widest_line() {
1042 let mut table = Table::new().box_set(SQUARE);
1043 table.add_column("name");
1044 table.add_column("bio");
1045 table.add_row(&["Alice", "line one\nline two is much longer"]);
1046 table.add_row(&["Bob", "short"]);
1047 let console = Console::builder().width(60).no_color(true).build();
1048 let out = console.render_to_string(&table);
1049 let top = out.lines().next().expect("a top border");
1050 let width = top.chars().count();
1051 assert!(
1053 width < 40,
1054 "the multi-line cell was measured as the sum of its lines: {width} wide"
1055 );
1056 assert!(
1057 out.contains("line two is much longer"),
1058 "content lost: {out:?}"
1059 );
1060 }
1061}