1use std::cmp::{max, min};
9
10use nu_ansi_term::Style;
11use nu_color_config::TextStyle;
12use nu_protocol::{TableIndent, TrimStrategy};
13
14use tabled::{
15 Table,
16 builder::Builder,
17 grid::{
18 ansi::ANSIBuf,
19 config::{
20 AlignmentHorizontal, ColoredConfig, Entity, Indent, Position, Sides, SpannedConfig,
21 },
22 dimension::{CompleteDimension, PeekableGridDimension},
23 records::{
24 IterRecords,
25 vec_records::{Cell, Text, VecRecords},
26 },
27 },
28 settings::{
29 Alignment, CellOption, Color, Padding, TableOption, Width,
30 formatting::AlignmentStrategy,
31 object::{Columns, Rows},
32 themes::ColumnNames,
33 width::Truncate,
34 },
35};
36
37use crate::{convert_style, is_color_empty, table_theme::TableTheme};
38
39const EMPTY_COLUMN_TEXT: &str = "...";
40const EMPTY_COLUMN_TEXT_WIDTH: usize = 3;
41
42pub type NuRecords = VecRecords<NuRecordsValue>;
43pub type NuRecordsValue = Text<String>;
44
45#[derive(Debug, Clone)]
47pub struct NuTable {
48 data: Vec<Vec<NuRecordsValue>>,
49 widths: Vec<usize>,
50 count_rows: usize,
51 count_cols: usize,
52 styles: Styles,
53 config: TableConfig,
54}
55
56impl NuTable {
57 pub fn new(count_rows: usize, count_cols: usize) -> Self {
59 Self {
60 data: vec![vec![Text::default(); count_cols]; count_rows],
61 widths: vec![2; count_cols],
62 count_rows,
63 count_cols,
64 styles: Styles {
65 cfg: ColoredConfig::default(),
66 alignments: CellConfiguration {
67 data: AlignmentHorizontal::Left,
68 index: AlignmentHorizontal::Right,
69 header: AlignmentHorizontal::Center,
70 },
71 colors: CellConfiguration::default(),
72 },
73 config: TableConfig {
74 theme: TableTheme::basic(),
75 trim: TrimStrategy::truncate(None),
76 structure: TableStructure::new(false, false, false),
77 indent: TableIndent::new(1, 1),
78 header_on_border: false,
79 expand: false,
80 border_color: None,
81 },
82 }
83 }
84
85 pub fn count_rows(&self) -> usize {
87 self.count_rows
88 }
89
90 pub fn count_columns(&self) -> usize {
92 self.count_cols
93 }
94
95 pub fn insert(&mut self, pos: (usize, usize), text: String) {
96 let text = Text::new(text);
97 let width = text.width() + indent_sum(self.config.indent);
98 self.widths[pos.1] = max(self.widths[pos.1], width);
99 self.data[pos.0][pos.1] = text;
100 }
101
102 pub fn set_row(&mut self, index: usize, row: Vec<NuRecordsValue>) {
103 assert_eq!(self.data[index].len(), row.len());
104
105 for (i, text) in row.iter().enumerate() {
106 self.widths[i] = max(
107 self.widths[i],
108 text.width() + indent_sum(self.config.indent),
109 );
110 }
111
112 self.data[index] = row;
113 }
114
115 pub fn pop_column(&mut self, count: usize) {
116 self.count_cols -= count;
117 self.widths.truncate(self.count_cols);
118 for row in &mut self.data[..] {
119 row.truncate(self.count_cols);
120 }
121
122 for i in 0..count {
124 let col = self.count_cols + i;
125 for row in 0..self.count_rows {
126 self.styles
127 .cfg
128 .set_alignment_horizontal(Entity::Cell(row, col), self.styles.alignments.data);
129 self.styles
130 .cfg
131 .set_color(Entity::Cell(row, col), ANSIBuf::default());
132 }
133 }
134 }
135
136 pub fn push_column(&mut self, text: String) {
137 let value = Text::new(text);
138
139 self.widths
140 .push(value.width() + indent_sum(self.config.indent));
141
142 for row in &mut self.data[..] {
143 row.push(value.clone());
144 }
145
146 self.count_cols += 1;
147 }
148
149 pub fn insert_style(&mut self, pos: (usize, usize), style: TextStyle) {
150 if let Some(style) = style.color_style {
151 let style = convert_style(style);
152 self.styles.cfg.set_color(pos.into(), style.into());
153 }
154
155 let alignment = convert_alignment(style.alignment);
156 if alignment != self.styles.alignments.data {
157 self.styles
158 .cfg
159 .set_alignment_horizontal(pos.into(), alignment);
160 }
161 }
162
163 pub fn set_header_style(&mut self, style: TextStyle) {
164 if let Some(style) = style.color_style {
165 let style = convert_style(style);
166 self.styles.colors.header = style;
167 }
168
169 self.styles.alignments.header = convert_alignment(style.alignment);
170 }
171
172 pub fn set_index_style(&mut self, style: TextStyle) {
173 if let Some(style) = style.color_style {
174 let style = convert_style(style);
175 self.styles.colors.index = style;
176 }
177
178 self.styles.alignments.index = convert_alignment(style.alignment);
179 }
180
181 pub fn set_data_style(&mut self, style: TextStyle) {
182 if let Some(style) = style.color_style {
183 if !style.is_plain() {
184 let style = convert_style(style);
185 self.styles.cfg.set_color(Entity::Global, style.into());
186 }
187 }
188
189 let alignment = convert_alignment(style.alignment);
190 self.styles
191 .cfg
192 .set_alignment_horizontal(Entity::Global, alignment);
193 self.styles.alignments.data = alignment;
194 }
195
196 pub fn set_indent(&mut self, indent: TableIndent) {
198 self.config.indent = indent;
199
200 let pad = indent_sum(indent);
201 for w in &mut self.widths {
202 *w = pad;
203 }
204 }
205
206 pub fn set_theme(&mut self, theme: TableTheme) {
207 self.config.theme = theme;
208 }
209
210 pub fn set_structure(&mut self, index: bool, header: bool, footer: bool) {
211 self.config.structure = TableStructure::new(index, header, footer);
212 }
213
214 pub fn set_border_header(&mut self, on: bool) {
215 self.config.header_on_border = on;
216 }
217
218 pub fn set_trim(&mut self, strategy: TrimStrategy) {
219 self.config.trim = strategy;
220 }
221
222 pub fn set_strategy(&mut self, expand: bool) {
223 self.config.expand = expand;
224 }
225
226 pub fn set_border_color(&mut self, color: Style) {
227 self.config.border_color = (!color.is_plain()).then_some(color);
228 }
229
230 pub fn clear_border_color(&mut self) {
231 self.config.border_color = None;
232 }
233
234 pub fn get_records_mut(&mut self) -> &mut [Vec<NuRecordsValue>] {
237 &mut self.data
238 }
239
240 pub fn clear_all_colors(&mut self) {
241 self.clear_border_color();
242 let cfg = std::mem::take(&mut self.styles.cfg);
243 self.styles.cfg = ColoredConfig::new(cfg.into_inner());
244 }
245
246 pub fn draw(self, termwidth: usize) -> Option<String> {
250 build_table(self, termwidth)
251 }
252
253 pub fn draw_unchecked(self, termwidth: usize) -> Option<String> {
257 build_table_unchecked(self, termwidth)
258 }
259
260 pub fn total_width(&self) -> usize {
262 let config = create_config(&self.config.theme, false, None);
263 get_total_width2(&self.widths, &config)
264 }
265}
266
267impl From<Vec<Vec<Text<String>>>> for NuTable {
268 fn from(value: Vec<Vec<Text<String>>>) -> Self {
269 let count_rows = value.len();
270 let count_cols = if value.is_empty() { 0 } else { value[0].len() };
271
272 let mut t = Self::new(count_rows, count_cols);
273 t.data = value;
274 table_recalculate_widths(&mut t);
275
276 t
277 }
278}
279
280fn table_recalculate_widths(t: &mut NuTable) {
281 let pad = indent_sum(t.config.indent);
282 t.widths = build_width(&t.data, t.count_cols, t.count_rows, pad);
283}
284
285#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Hash)]
286struct CellConfiguration<Value> {
287 index: Value,
288 header: Value,
289 data: Value,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
293struct Styles {
294 cfg: ColoredConfig,
295 colors: CellConfiguration<Color>,
296 alignments: CellConfiguration<AlignmentHorizontal>,
297}
298
299#[derive(Debug, Clone)]
300pub struct TableConfig {
301 theme: TableTheme,
302 trim: TrimStrategy,
303 border_color: Option<Style>,
304 expand: bool,
305 structure: TableStructure,
306 header_on_border: bool,
307 indent: TableIndent,
308}
309
310#[derive(Debug, Clone, Copy)]
311struct TableStructure {
312 with_index: bool,
313 with_header: bool,
314 with_footer: bool,
315}
316
317impl TableStructure {
318 fn new(with_index: bool, with_header: bool, with_footer: bool) -> Self {
319 Self {
320 with_index,
321 with_header,
322 with_footer,
323 }
324 }
325}
326
327#[derive(Debug, Clone)]
328struct HeadInfo {
329 values: Vec<String>,
330 align: AlignmentHorizontal,
331 color: Option<Color>,
332}
333
334impl HeadInfo {
335 fn new(values: Vec<String>, align: AlignmentHorizontal, color: Option<Color>) -> Self {
336 Self {
337 values,
338 align,
339 color,
340 }
341 }
342}
343
344fn build_table_unchecked(mut t: NuTable, termwidth: usize) -> Option<String> {
345 if t.count_columns() == 0 || t.count_rows() == 0 {
346 return Some(String::new());
347 }
348
349 let widths = std::mem::take(&mut t.widths);
350 let config = create_config(&t.config.theme, false, None);
351 let totalwidth = get_total_width2(&t.widths, &config);
352 let widths = WidthEstimation::new(widths.clone(), widths, totalwidth, false, false);
353
354 let head = remove_header_if(&mut t);
355 table_insert_footer_if(&mut t);
356
357 draw_table(t, widths, head, termwidth)
358}
359
360fn build_table(mut t: NuTable, termwidth: usize) -> Option<String> {
361 if t.count_columns() == 0 || t.count_rows() == 0 {
362 return Some(String::new());
363 }
364
365 let widths = table_truncate(&mut t, termwidth)?;
366 let head = remove_header_if(&mut t);
367 table_insert_footer_if(&mut t);
368
369 draw_table(t, widths, head, termwidth)
370}
371
372fn remove_header_if(t: &mut NuTable) -> Option<HeadInfo> {
373 if !is_header_on_border(t) {
374 return None;
375 }
376
377 let head = remove_header(t);
378 t.config.structure.with_header = false;
379
380 Some(head)
381}
382
383fn is_header_on_border(t: &NuTable) -> bool {
384 let is_configured = t.config.structure.with_header && t.config.header_on_border;
385 let has_horizontal = t.config.theme.as_base().borders_has_top()
386 || t.config.theme.as_base().get_horizontal_line(1).is_some();
387 is_configured && has_horizontal
388}
389
390fn table_insert_footer_if(t: &mut NuTable) {
391 if t.config.structure.with_header && t.config.structure.with_footer {
392 duplicate_row(&mut t.data, 0);
393 }
394}
395
396fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option<WidthEstimation> {
397 let widths = maybe_truncate_columns(&mut t.data, t.widths.clone(), &t.config, termwidth);
398 if widths.needed.is_empty() {
399 return None;
400 }
401
402 if widths.trail {
404 let col = widths.needed.len() - 1;
405 for row in 0..t.count_rows {
406 t.styles
407 .cfg
408 .set_alignment_horizontal(Entity::Cell(row, col), t.styles.alignments.data);
409 t.styles
410 .cfg
411 .set_color(Entity::Cell(row, col), ANSIBuf::default());
412 }
413 }
414
415 Some(widths)
416}
417
418fn remove_header(t: &mut NuTable) -> HeadInfo {
419 for row in 1..t.data.len() {
421 for col in 0..t.count_cols {
422 let alignment = *t
423 .styles
424 .cfg
425 .get_alignment_horizontal(Position::new(row, col));
426 if alignment != t.styles.alignments.data {
427 t.styles
428 .cfg
429 .set_alignment_horizontal(Entity::Cell(row - 1, col), alignment);
430 }
431
432 let color = t.styles.cfg.get_color(Position::new(row, col)).cloned();
433 if let Some(color) = color {
434 t.styles.cfg.set_color(Entity::Cell(row - 1, col), color);
435 }
436 }
437 }
438
439 let head = t
440 .data
441 .remove(0)
442 .into_iter()
443 .map(|s| s.to_string())
444 .collect();
445
446 table_recalculate_widths(t);
449
450 let alignment = t.styles.alignments.header;
451 let color = get_color_if_exists(&t.styles.colors.header);
452
453 t.styles.alignments.header = AlignmentHorizontal::Center;
454 t.styles.colors.header = Color::empty();
455
456 HeadInfo::new(head, alignment, color)
457}
458
459fn draw_table(
460 t: NuTable,
461 width: WidthEstimation,
462 head: Option<HeadInfo>,
463 termwidth: usize,
464) -> Option<String> {
465 let mut structure = t.config.structure;
466 structure.with_footer = structure.with_footer && head.is_none();
467 let sep_color = t.config.border_color;
468
469 let data = t.data;
470 let mut table = Builder::from_vec(data).build();
471
472 set_styles(&mut table, t.styles, &structure);
473 set_indent(&mut table, t.config.indent);
474 load_theme(&mut table, &t.config.theme, &structure, sep_color);
475 truncate_table(&mut table, &t.config, width, termwidth);
476 table_set_border_header(&mut table, head, &t.config);
477
478 table_to_string(table, termwidth)
479}
480
481fn set_styles(table: &mut Table, styles: Styles, structure: &TableStructure) {
482 table.with(styles.cfg);
483 align_table(table, styles.alignments, structure);
484 colorize_table(table, styles.colors, structure);
485}
486
487fn table_set_border_header(table: &mut Table, head: Option<HeadInfo>, cfg: &TableConfig) {
488 let head = match head {
489 Some(head) => head,
490 None => return,
491 };
492
493 let theme = &cfg.theme;
494 let with_footer = cfg.structure.with_footer;
495 let pad = cfg.indent.left + cfg.indent.right;
496
497 if !theme.as_base().borders_has_top() {
498 let line = theme.as_base().get_horizontal_line(1);
499 if let Some(line) = line.cloned() {
500 table.get_config_mut().insert_horizontal_line(0, line);
501 if with_footer {
502 let last_row = table.count_rows();
503 table
504 .get_config_mut()
505 .insert_horizontal_line(last_row, line);
506 }
507 };
508 }
509
510 if with_footer {
512 let last_row = table.count_rows();
513 table.with(SetLineHeaders::new(head.clone(), last_row, pad));
514 }
515
516 table.with(SetLineHeaders::new(head, 0, pad));
517}
518
519fn truncate_table(table: &mut Table, cfg: &TableConfig, width: WidthEstimation, termwidth: usize) {
520 let trim = cfg.trim.clone();
521 let pad = cfg.indent.left + cfg.indent.right;
522 let ctrl = WidthCtrl::new(termwidth, width, trim, cfg.expand, pad);
523 table.with(ctrl);
524}
525
526fn indent_sum(indent: TableIndent) -> usize {
527 indent.left + indent.right
528}
529
530fn set_indent(table: &mut Table, indent: TableIndent) {
531 table.with(Padding::new(indent.left, indent.right, 0, 0));
532}
533
534fn table_to_string(table: Table, termwidth: usize) -> Option<String> {
535 let total_width = table.total_width();
538 if total_width > termwidth {
539 None
540 } else {
541 let content = table.to_string();
542 Some(content)
543 }
544}
545
546struct WidthCtrl {
547 width: WidthEstimation,
548 trim_strategy: TrimStrategy,
549 max_width: usize,
550 expand: bool,
551 pad: usize,
552}
553
554impl WidthCtrl {
555 fn new(
556 max_width: usize,
557 width: WidthEstimation,
558 trim_strategy: TrimStrategy,
559 expand: bool,
560 pad: usize,
561 ) -> Self {
562 Self {
563 width,
564 trim_strategy,
565 max_width,
566 expand,
567 pad,
568 }
569 }
570}
571
572#[derive(Debug, Clone)]
573struct WidthEstimation {
574 original: Vec<usize>,
575 needed: Vec<usize>,
576 #[allow(dead_code)]
577 total: usize,
578 truncate: bool,
579 trail: bool,
580}
581
582impl WidthEstimation {
583 fn new(
584 original: Vec<usize>,
585 needed: Vec<usize>,
586 total: usize,
587 truncate: bool,
588 trail: bool,
589 ) -> Self {
590 Self {
591 original,
592 needed,
593 total,
594 truncate,
595 trail,
596 }
597 }
598}
599
600impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for WidthCtrl {
601 fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
602 if self.width.truncate {
603 width_ctrl_truncate(self, recs, cfg, dims);
604 return;
605 }
606
607 if self.expand {
608 width_ctrl_expand(self, recs, cfg, dims);
609 return;
610 }
611
612 dims.set_widths(self.width.needed);
614 }
615
616 fn hint_change(&self) -> Option<Entity> {
617 None
618 }
619}
620
621fn width_ctrl_expand(
622 ctrl: WidthCtrl,
623 recs: &mut NuRecords,
624 cfg: &mut ColoredConfig,
625 dims: &mut CompleteDimension,
626) {
627 let opt = Width::increase(ctrl.max_width);
628 TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
629}
630
631fn width_ctrl_truncate(
632 ctrl: WidthCtrl,
633 recs: &mut NuRecords,
634 cfg: &mut ColoredConfig,
635 dims: &mut CompleteDimension,
636) {
637 for (col, (&width, width_original)) in ctrl
639 .width
640 .needed
641 .iter()
642 .zip(ctrl.width.original)
643 .enumerate()
644 {
645 if width == width_original {
646 continue;
647 }
648
649 let width = width - ctrl.pad;
650
651 match &ctrl.trim_strategy {
652 TrimStrategy::Wrap { try_to_keep_words } => {
653 let wrap = Width::wrap(width).keep_words(*try_to_keep_words);
654
655 CellOption::<NuRecords, _>::change(wrap, recs, cfg, Entity::Column(col));
656 }
657 TrimStrategy::Truncate { suffix } => {
658 let mut truncate = Width::truncate(width);
659 if let Some(suffix) = suffix {
660 truncate = truncate.suffix(suffix).suffix_try_color(true);
661 }
662
663 CellOption::<NuRecords, _>::change(truncate, recs, cfg, Entity::Column(col));
664 }
665 }
666 }
667
668 dims.set_widths(ctrl.width.needed);
669}
670
671fn align_table(
672 table: &mut Table,
673 alignments: CellConfiguration<AlignmentHorizontal>,
674 structure: &TableStructure,
675) {
676 table.with(AlignmentStrategy::PerLine);
677
678 if structure.with_header {
679 table.modify(
680 Rows::first(),
681 (
682 AlignmentStrategy::PerCell,
683 Alignment::from(alignments.header),
684 ),
685 );
686
687 if structure.with_footer {
688 table.modify(
689 Rows::last(),
690 (
691 AlignmentStrategy::PerCell,
692 Alignment::from(alignments.header),
693 ),
694 );
695 }
696 }
697
698 if structure.with_index {
699 table.modify(Columns::first(), Alignment::from(alignments.index));
700 }
701}
702
703fn colorize_table(table: &mut Table, styles: CellConfiguration<Color>, structure: &TableStructure) {
704 if structure.with_index && !is_color_empty(&styles.index) {
705 table.modify(Columns::first(), styles.index);
706 }
707
708 if structure.with_header && !is_color_empty(&styles.header) {
709 table.modify(Rows::first(), styles.header.clone());
710 }
711
712 if structure.with_header && structure.with_footer && !is_color_empty(&styles.header) {
713 table.modify(Rows::last(), styles.header);
714 }
715}
716
717fn load_theme(
718 table: &mut Table,
719 theme: &TableTheme,
720 structure: &TableStructure,
721 sep_color: Option<Style>,
722) {
723 let with_header = table.count_rows() > 1 && structure.with_header;
724 let with_footer = with_header && structure.with_footer;
725 let mut theme = theme.as_base().clone();
726
727 if !with_header {
728 let borders = *theme.get_borders();
729 theme.remove_horizontal_lines();
730 theme.set_borders(borders);
731 } else if with_footer {
732 theme_copy_horizontal_line(&mut theme, 1, table.count_rows() - 1);
733 }
734
735 table.with(theme);
736
737 if let Some(style) = sep_color {
738 let color = convert_style(style);
739 let color = ANSIBuf::from(color);
740 table.get_config_mut().set_border_color_default(color);
741 }
742}
743
744fn maybe_truncate_columns(
745 data: &mut Vec<Vec<NuRecordsValue>>,
746 widths: Vec<usize>,
747 cfg: &TableConfig,
748 termwidth: usize,
749) -> WidthEstimation {
750 const TERMWIDTH_THRESHOLD: usize = 120;
751
752 let pad = cfg.indent.left + cfg.indent.right;
753 let preserve_content = termwidth > TERMWIDTH_THRESHOLD;
754
755 if preserve_content {
756 truncate_columns_by_columns(data, widths, &cfg.theme, pad, termwidth)
757 } else {
758 truncate_columns_by_content(data, widths, &cfg.theme, pad, termwidth)
759 }
760}
761
762fn truncate_columns_by_content(
764 data: &mut Vec<Vec<NuRecordsValue>>,
765 widths: Vec<usize>,
766 theme: &TableTheme,
767 pad: usize,
768 termwidth: usize,
769) -> WidthEstimation {
770 const MIN_ACCEPTABLE_WIDTH: usize = 5;
771 const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
772
773 let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
774 let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
775
776 let count_columns = data[0].len();
777
778 let config = create_config(theme, false, None);
779 let widths_original = widths;
780 let mut widths = vec![];
781
782 let borders = config.get_borders();
783 let vertical = borders.has_vertical() as usize;
784
785 let mut width = borders.has_left() as usize + borders.has_right() as usize;
786 let mut truncate_pos = 0;
787
788 for (i, &column_width) in widths_original.iter().enumerate() {
789 let mut next_move = column_width;
790 if i > 0 {
791 next_move += vertical;
792 }
793
794 if width + next_move > termwidth {
795 break;
796 }
797
798 widths.push(column_width);
799 width += next_move;
800 truncate_pos += 1;
801 }
802
803 if truncate_pos == count_columns {
804 return WidthEstimation::new(widths_original, widths, width, false, false);
805 }
806
807 if truncate_pos == 0 {
808 if termwidth > width {
809 let available = termwidth - width;
810 if available >= min_column_width + vertical + trailing_column_width {
811 truncate_rows(data, 1);
812
813 let first_col_width = available - (vertical + trailing_column_width);
814 widths.push(first_col_width);
815 width += first_col_width;
816
817 push_empty_column(data);
818 widths.push(trailing_column_width);
819 width += trailing_column_width + vertical;
820
821 return WidthEstimation::new(widths_original, widths, width, true, true);
822 }
823 }
824
825 return WidthEstimation::new(widths_original, widths, width, false, false);
826 }
827
828 let available = termwidth - width;
829
830 let is_last_column = truncate_pos + 1 == count_columns;
831 let can_fit_last_column = available >= min_column_width + vertical;
832 if is_last_column && can_fit_last_column {
833 let w = available - vertical;
834 widths.push(w);
835 width += w + vertical;
836
837 return WidthEstimation::new(widths_original, widths, width, true, false);
838 }
839
840 let is_almost_last_column = truncate_pos + 2 == count_columns;
842 if is_almost_last_column {
843 let next_column_width = widths_original[truncate_pos + 1];
844 let has_space_for_two_columns =
845 available >= min_column_width + vertical + next_column_width + vertical;
846
847 if !is_last_column && has_space_for_two_columns {
848 let rest = available - vertical - next_column_width - vertical;
849 widths.push(rest);
850 width += rest + vertical;
851
852 widths.push(next_column_width);
853 width += next_column_width + vertical;
854
855 return WidthEstimation::new(widths_original, widths, width, true, false);
856 }
857 }
858
859 let has_space_for_two_columns =
860 available >= min_column_width + vertical + trailing_column_width + vertical;
861 if !is_last_column && has_space_for_two_columns {
862 truncate_rows(data, truncate_pos + 1);
863
864 let rest = available - vertical - trailing_column_width - vertical;
865 widths.push(rest);
866 width += rest + vertical;
867
868 push_empty_column(data);
869 widths.push(trailing_column_width);
870 width += trailing_column_width + vertical;
871
872 return WidthEstimation::new(widths_original, widths, width, true, true);
873 }
874
875 if available >= trailing_column_width + vertical {
876 truncate_rows(data, truncate_pos);
877
878 push_empty_column(data);
879 widths.push(trailing_column_width);
880 width += trailing_column_width + vertical;
881
882 return WidthEstimation::new(widths_original, widths, width, false, true);
883 }
884
885 let last_width = widths.last().cloned().expect("ok");
886 let can_truncate_last = last_width > min_column_width;
887
888 if can_truncate_last {
889 let rest = last_width - min_column_width;
890 let maybe_available = available + rest;
891
892 if maybe_available >= trailing_column_width + vertical {
893 truncate_rows(data, truncate_pos);
894
895 let left = maybe_available - trailing_column_width - vertical;
896 let new_last_width = min_column_width + left;
897
898 widths[truncate_pos - 1] = new_last_width;
899 width -= last_width;
900 width += new_last_width;
901
902 push_empty_column(data);
903 widths.push(trailing_column_width);
904 width += trailing_column_width + vertical;
905
906 return WidthEstimation::new(widths_original, widths, width, true, true);
907 }
908 }
909
910 truncate_rows(data, truncate_pos - 1);
911 let w = widths.pop().expect("ok");
912 width -= w;
913
914 push_empty_column(data);
915 widths.push(trailing_column_width);
916 width += trailing_column_width;
917
918 if widths.len() == 1 {
919 return WidthEstimation::new(widths_original, vec![], width, false, true);
921 }
922
923 WidthEstimation::new(widths_original, widths, width, false, true)
924}
925
926fn truncate_columns_by_columns(
937 data: &mut Vec<Vec<NuRecordsValue>>,
938 widths: Vec<usize>,
939 theme: &TableTheme,
940 pad: usize,
941 termwidth: usize,
942) -> WidthEstimation {
943 const MIN_ACCEPTABLE_WIDTH: usize = 10;
944 const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
945
946 let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
947 let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
948
949 let count_columns = data[0].len();
950
951 let config = create_config(theme, false, None);
952 let widths_original = widths;
953 let mut widths = vec![];
954
955 let borders = config.get_borders();
956 let vertical = borders.has_vertical() as usize;
957
958 let mut width = borders.has_left() as usize + borders.has_right() as usize;
959 let mut truncate_pos = 0;
960
961 for (i, &width_orig) in widths_original.iter().enumerate() {
962 let use_width = min(min_column_width, width_orig);
963 let mut next_move = use_width;
964 if i > 0 {
965 next_move += vertical;
966 }
967
968 if width + next_move > termwidth {
969 break;
970 }
971
972 widths.push(use_width);
973 width += next_move;
974 truncate_pos += 1;
975 }
976
977 if truncate_pos == 0 {
978 return WidthEstimation::new(widths_original, widths, width, false, false);
979 }
980
981 let mut available = termwidth - width;
982
983 if available > 0 {
984 for i in 0..truncate_pos {
985 let used_width = widths[i];
986 let col_width = widths_original[i];
987 if used_width < col_width {
988 let need = col_width - used_width;
989 let take = min(available, need);
990 available -= take;
991
992 widths[i] += take;
993 width += take;
994
995 if available == 0 {
996 break;
997 }
998 }
999 }
1000 }
1001
1002 if truncate_pos == count_columns {
1003 return WidthEstimation::new(widths_original, widths, width, true, false);
1004 }
1005
1006 if available >= trailing_column_width + vertical {
1007 truncate_rows(data, truncate_pos);
1008
1009 push_empty_column(data);
1010 widths.push(trailing_column_width);
1011 width += trailing_column_width + vertical;
1012
1013 return WidthEstimation::new(widths_original, widths, width, true, true);
1014 }
1015
1016 truncate_rows(data, truncate_pos - 1);
1017 let w = widths.pop().expect("ok");
1018 width -= w;
1019
1020 push_empty_column(data);
1021 widths.push(trailing_column_width);
1022 width += trailing_column_width;
1023
1024 WidthEstimation::new(widths_original, widths, width, true, true)
1025}
1026
1027fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
1028 let total = widths.iter().sum::<usize>();
1029 let countv = cfg.count_vertical(widths.len());
1030 let margin = cfg.get_margin();
1031
1032 total + countv + margin.left.size + margin.right.size
1033}
1034
1035fn create_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> ColoredConfig {
1036 let structure = TableStructure::new(false, with_header, false);
1037 let mut table = Table::new([[""]]);
1038 load_theme(&mut table, theme, &structure, color);
1039 table.get_config().clone()
1040}
1041
1042fn push_empty_column(data: &mut Vec<Vec<NuRecordsValue>>) {
1043 let empty_cell = Text::new(String::from(EMPTY_COLUMN_TEXT));
1044 for row in data {
1045 row.push(empty_cell.clone());
1046 }
1047}
1048
1049fn duplicate_row(data: &mut Vec<Vec<NuRecordsValue>>, row: usize) {
1050 let duplicate = data[row].clone();
1051 data.push(duplicate);
1052}
1053
1054fn truncate_rows(data: &mut Vec<Vec<NuRecordsValue>>, count: usize) {
1055 for row in data {
1056 row.truncate(count);
1057 }
1058}
1059
1060fn convert_alignment(alignment: nu_color_config::Alignment) -> AlignmentHorizontal {
1061 match alignment {
1062 nu_color_config::Alignment::Center => AlignmentHorizontal::Center,
1063 nu_color_config::Alignment::Left => AlignmentHorizontal::Left,
1064 nu_color_config::Alignment::Right => AlignmentHorizontal::Right,
1065 }
1066}
1067
1068fn build_width(
1069 records: &[Vec<NuRecordsValue>],
1070 count_cols: usize,
1071 count_rows: usize,
1072 pad: usize,
1073) -> Vec<usize> {
1074 let mut cfg = SpannedConfig::default();
1076 cfg.set_padding(
1077 Entity::Global,
1078 Sides::new(
1079 Indent::spaced(pad),
1080 Indent::zero(),
1081 Indent::zero(),
1082 Indent::zero(),
1083 ),
1084 );
1085
1086 let records = IterRecords::new(records, count_cols, Some(count_rows));
1087
1088 PeekableGridDimension::width(records, &cfg)
1089}
1090
1091struct SetLineHeaders {
1094 line: usize,
1095 pad: usize,
1096 head: HeadInfo,
1097}
1098
1099impl SetLineHeaders {
1100 fn new(head: HeadInfo, line: usize, pad: usize) -> Self {
1101 Self { line, head, pad }
1102 }
1103}
1104
1105impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for SetLineHeaders {
1106 fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
1107 let widths = match dims.get_widths() {
1108 Some(widths) => widths,
1109 None => {
1110 unreachable!("must never be the case");
1116 }
1117 };
1118
1119 let columns: Vec<_> = self
1120 .head
1121 .values
1122 .into_iter()
1123 .zip(widths.iter().cloned()) .map(|(s, width)| Truncate::truncate(&s, width - self.pad).into_owned())
1125 .collect();
1126
1127 let mut names = ColumnNames::new(columns)
1128 .line(self.line)
1129 .alignment(Alignment::from(self.head.align));
1130 if let Some(color) = self.head.color {
1131 names = names.color(color);
1132 }
1133
1134 names.change(recs, cfg, dims);
1135 }
1136
1137 fn hint_change(&self) -> Option<Entity> {
1138 None
1139 }
1140}
1141
1142fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) {
1143 if let Some(line) = theme.get_horizontal_line(from) {
1144 theme.insert_horizontal_line(to, *line);
1145 }
1146}
1147
1148pub fn get_color_if_exists(c: &Color) -> Option<Color> {
1149 if !is_color_empty(c) {
1150 Some(c.clone())
1151 } else {
1152 None
1153 }
1154}