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 crate::protocol::OwnedTableRows for Table {
689 fn extend_owned_rows(&mut self, mut rows: Vec<Vec<String>>) -> &mut Self {
690 if self.rows.is_empty() {
691 self.rows = rows;
692 } else {
693 self.rows.append(&mut rows);
694 }
695 self
696 }
697}
698
699impl Renderable for Table {
700 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
701 let mut segments = Vec::new();
702 let mut first = true;
703 let result: Result<(), std::convert::Infallible> =
704 self.try_for_each_line(console, options, |line| {
705 if !first {
706 segments.push(Segment::line());
707 }
708 first = false;
709 segments.extend(line);
710 Ok(())
711 });
712 match result {
713 Ok(()) => segments,
714 Err(never) => match never {},
715 }
716 }
717}
718
719fn wrap_cell(content: &str, width: usize) -> Vec<String> {
723 if width == 0 {
724 return vec![String::new()];
725 }
726 if content.contains('\n') {
732 return content
733 .split('\n')
734 .flat_map(|line| wrap_cell(line, width))
735 .collect();
736 }
737 let breaks = crate::wrap::divide_line(content, width, false);
740 let chars: Vec<char> = content.chars().collect();
741 let mut lines: Vec<String> = Vec::new();
742 let mut start = 0;
743 for stop in breaks {
744 lines.push(chars[start..stop].iter().collect());
745 start = stop;
746 }
747 lines.push(chars[start..].iter().collect());
748 lines
751 .iter()
752 .map(|line| ellipsis_crop(line.trim_end(), width))
753 .collect()
754}
755
756fn ellipsis_crop(text: &str, width: usize) -> String {
759 if cell_len(text) <= width {
760 return text.to_string();
761 }
762 if width == 0 {
763 return String::new();
764 }
765 format!("{}\u{2026}", set_cell_size(text, width - 1))
766}
767
768fn render_annotation(
771 console: &Console,
772 options: &ConsoleOptions,
773 annotation: &str,
774 style: &str,
775 width: usize,
776) -> Vec<Vec<Segment>> {
777 let expanded = console.expand_emoji(annotation);
778 let mut text = Text::from_markup(&expanded).unwrap_or_else(|_| Text::new(expanded));
779 text.set_base_style(style);
780 let overflow = options.overflow.unwrap_or(Overflow::Fold);
781 let no_wrap = options.no_wrap.unwrap_or(false) || overflow == Overflow::Ignore;
782 let mut lines = Vec::new();
783 for mut hard_line in text.split("\n", false, true) {
784 hard_line.expand_tabs(DEFAULT_TAB_SIZE);
785 let wrapped = if no_wrap {
786 vec![hard_line]
787 } else {
788 let char_offsets: Vec<usize> = hard_line
789 .plain()
790 .char_indices()
791 .map(|(i, _)| i)
792 .chain(std::iter::once(hard_line.plain().len()))
793 .collect();
794 let breaks: Vec<usize> =
795 crate::wrap::divide_line(hard_line.plain(), width, overflow == Overflow::Fold)
796 .into_iter()
797 .map(|i| char_offsets[i])
798 .collect();
799 hard_line.divide(&breaks)
800 };
801 for mut line in wrapped {
802 if overflow != Overflow::Ignore {
803 line.rstrip();
807 line.truncate(width, Some(overflow), false);
808 line.pad_left(width.saturating_sub(line.cell_len()) / 2, ' ');
809 line.pad_right(width.saturating_sub(line.cell_len()), ' ');
810 line.truncate(width, Some(overflow), false);
811 }
812 lines.push(line.render(console.theme(), console.base_style()));
813 }
814 }
815 lines
816}
817
818fn round_half_even(value: f64) -> i64 {
820 let floor = value.floor();
821 let diff = value - floor;
822 if (diff - 0.5).abs() < 1e-9 {
823 let f = floor as i64;
824 if f % 2 == 0 {
825 f
826 } else {
827 f + 1
828 }
829 } else {
830 value.round() as i64
831 }
832}
833
834fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
837 let ratios: Vec<i64> = ratios
838 .iter()
839 .zip(maximums)
840 .map(|(&r, &m)| if m != 0 { r } else { 0 })
841 .collect();
842 let mut total_ratio: i64 = ratios.iter().sum();
843 if total_ratio == 0 {
844 return values.to_vec();
845 }
846 let mut total_remaining = total;
847 let mut result = Vec::with_capacity(values.len());
848 for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
849 if ratio != 0 && total_ratio > 0 {
850 let distributed = maximum.min(round_half_even(
851 ratio as f64 * total_remaining as f64 / total_ratio as f64,
852 ));
853 result.push(value - distributed);
854 total_remaining -= distributed;
855 total_ratio -= ratio;
856 } else {
857 result.push(value);
858 }
859 }
860 result
861}
862
863fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
867 let ratios: Vec<i64> = match minimums {
869 Some(mins) => ratios
870 .iter()
871 .zip(mins)
872 .map(|(&r, &m)| if m != 0 { r } else { 0 })
873 .collect(),
874 None => ratios.to_vec(),
875 };
876 let mut total_ratio: i64 = ratios.iter().sum();
877 let mut total_remaining = total;
878 let mut result = Vec::with_capacity(ratios.len());
879 for (index, &ratio) in ratios.iter().enumerate() {
880 let minimum = minimums.map_or(0, |m| m[index]);
881 let distributed = if total_ratio > 0 {
882 let numerator = ratio * total_remaining;
885 let ceil_div = (numerator + total_ratio - 1) / total_ratio;
886 minimum.max(ceil_div)
887 } else {
888 total_remaining
889 };
890 result.push(distributed);
891 total_ratio -= ratio;
892 total_remaining -= distributed;
893 }
894 result
895}
896
897fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
900 let mut total_width: i64 = widths.iter().sum();
901 let mut excess_width = total_width - max_width;
902 if wrapable.iter().any(|&w| w) {
903 while total_width != 0 && excess_width > 0 {
904 let max_column = widths
905 .iter()
906 .zip(wrapable)
907 .filter(|(_, &w)| w)
908 .map(|(&x, _)| x)
909 .max()
910 .unwrap_or(0);
911 let second_max_column = widths
912 .iter()
913 .zip(wrapable)
914 .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
915 .max()
916 .unwrap_or(0);
917 let column_difference = max_column - second_max_column;
918 let ratios: Vec<i64> = widths
919 .iter()
920 .zip(wrapable)
921 .map(|(&x, &w)| i64::from(x == max_column && w))
922 .collect();
923 if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
924 break;
925 }
926 let max_reduce = vec![excess_width.min(column_difference); widths.len()];
927 widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
928 total_width = widths.iter().sum();
929 excess_width = total_width - max_width;
930 }
931 }
932 widths
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938 use crate::color::ColorSystem;
939 use crate::r#box::SQUARE;
940
941 fn console() -> Console {
942 Console::builder()
943 .force_terminal(true)
944 .color_system(Some(ColorSystem::Truecolor))
945 .width(40)
946 .no_color(false)
947 .build()
948 }
949
950 #[test]
951 fn owned_rows_preserve_measurement_styles_and_missing_cells() {
952 use crate::protocol::OwnedTableRows;
953 for width in [1, 12, 40, 80] {
954 let console = Console::builder().width(width).force_terminal(true).build();
955 let build = || {
956 let mut table = Table::new()
957 .title("Rows")
958 .caption("owned or borrowed")
959 .show_lines(true);
960 table.add_column("Name");
961 table.add_column_justify("Value", Justify::Right);
962 table
963 };
964 let mut borrowed = build();
965 let mut owned = build();
966 for row in [
967 vec!["漢字\n🙂", "123"],
968 vec!["short"],
969 vec!["extra", "4", "ignored"],
970 ] {
971 borrowed.add_row(&row);
972 owned.extend_owned_rows(vec![row.into_iter().map(str::to_owned).collect()]);
973 }
974 assert_eq!(
975 console.render_to_string(&borrowed),
976 console.render_to_string(&owned)
977 );
978 }
979 }
980
981 #[test]
982 fn simple_square_table() {
983 let mut table = Table::new().box_set(SQUARE);
984 table.add_column("Name");
985 table.add_column("Age");
986 table.add_row(&["Alice", "30"]);
987 table.add_row(&["Bob", "7"]);
988 let out = console().render_export(&table);
989 let expected = concat!(
990 "┌───────┬─────┐\n",
991 "│\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",
992 "├───────┼─────┤\n",
993 "│ Alice │ 30 │\n",
994 "│ Bob │ 7 │\n",
995 "└───────┴─────┘\n",
996 );
997 assert_eq!(out, expected);
998 }
999
1000 #[test]
1001 fn streamed_lines_match_styled_table_output() {
1002 let mut table = Table::new().box_set(SQUARE);
1003 table.add_column("Name");
1004 table.add_column("Age");
1005 table.add_row(&["Alice", "30"]);
1006 table.add_row(&["Bob", "7"]);
1007 let console = console();
1008 let mut streamed = String::new();
1009 table
1010 .try_for_each_line(&console, &console.options(), |line| {
1011 assert!(line.iter().all(|segment| !segment.text.contains('\n')));
1012 streamed.push_str(&console.segments_to_string(&line));
1013 streamed.push('\n');
1014 Ok::<_, std::convert::Infallible>(())
1015 })
1016 .unwrap();
1017 assert_eq!(streamed, console.render_export(&table));
1020 assert_eq!(streamed.lines().count(), 6);
1021 }
1022
1023 #[test]
1024 fn streamed_lines_stop_at_the_first_writer_error() {
1025 let mut table = Table::new()
1026 .box_set(SQUARE)
1027 .title("People")
1028 .caption("End")
1029 .show_lines(true);
1030 table.add_column("Name");
1031 table.add_row(&["Alice\nBob"]);
1032 table.add_row(&["Carol"]);
1033 let console = console();
1034 let mut visits = 0;
1035 let result = table.try_for_each_line(&console, &console.options(), |_| {
1036 visits += 1;
1037 if visits == 5 {
1038 Err("writer failed")
1039 } else {
1040 Ok(())
1041 }
1042 });
1043 assert_eq!(result, Err("writer failed"));
1044 assert_eq!(visits, 5);
1045 }
1046
1047 #[test]
1052 fn a_column_narrower_than_its_padding_stays_inside_the_border() {
1053 for ncols in [20usize, 29, 40] {
1054 let mut table = Table::new().box_set(SQUARE);
1055 for i in 0..ncols {
1056 table.add_column(format!("c{i}"));
1057 }
1058 let row: Vec<String> = (0..ncols).map(|i| i.to_string()).collect();
1059 table.add_row(&row.iter().map(String::as_str).collect::<Vec<_>>());
1060 let console = Console::builder().width(80).no_color(true).build();
1061 let out = console.render_to_string(&table);
1062 let rows: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
1063 let widths: Vec<usize> = rows.iter().map(|r| r.chars().count()).collect();
1064 assert!(
1065 widths.iter().all(|w| *w == widths[0]),
1066 "{ncols} columns produced ragged rows: {widths:?}"
1067 );
1068 for (index, row) in rows.iter().enumerate() {
1069 let last = row.chars().last().expect("non-empty row");
1070 assert!(
1071 !last.is_whitespace(),
1072 "{ncols} columns: row {index} lost its right border: {row:?}"
1073 );
1074 }
1075 }
1076 }
1077
1078 #[test]
1083 fn a_multi_line_cell_is_measured_by_its_widest_line() {
1084 let mut table = Table::new().box_set(SQUARE);
1085 table.add_column("name");
1086 table.add_column("bio");
1087 table.add_row(&["Alice", "line one\nline two is much longer"]);
1088 table.add_row(&["Bob", "short"]);
1089 let console = Console::builder().width(60).no_color(true).build();
1090 let out = console.render_to_string(&table);
1091 let top = out.lines().next().expect("a top border");
1092 let width = top.chars().count();
1093 assert!(
1095 width < 40,
1096 "the multi-line cell was measured as the sum of its lines: {width} wide"
1097 );
1098 assert!(
1099 out.contains("line two is much longer"),
1100 "content lost: {out:?}"
1101 );
1102 }
1103}