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