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