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