1use std::{cmp::min, collections::HashMap};
2
3use nu_ansi_term::Style;
4use nu_color_config::TextStyle;
5use nu_protocol::{TableIndent, TrimStrategy};
6use nu_utils::strip_ansi_unlikely;
7
8use tabled::{
9 builder::Builder,
10 grid::{
11 ansi::ANSIBuf,
12 colors::Colors,
13 config::{AlignmentHorizontal, ColoredConfig, Entity, Position},
14 dimension::CompleteDimensionVecRecords,
15 records::{
16 vec_records::{Cell, Text, VecRecords},
17 ExactRecords, Records, Resizable,
18 },
19 },
20 settings::{
21 format::FormatContent,
22 formatting::AlignmentStrategy,
23 object::{Columns, Row, Rows},
24 peaker::Priority,
25 themes::ColumnNames,
26 width::Truncate,
27 Alignment, Color, Format, Modify, ModifyList, Padding, Settings, TableOption, Width,
28 },
29 Table,
30};
31
32use crate::{convert_style, is_color_empty, table_theme::TableTheme};
33
34pub type NuRecords = VecRecords<NuRecordsValue>;
35pub type NuRecordsValue = Text<String>;
36
37#[derive(Debug, Clone)]
39pub struct NuTable {
40 data: NuRecords,
41 styles: Styles,
42 alignments: Alignments,
43 config: TableConfig,
44}
45
46impl NuTable {
47 pub fn new(count_rows: usize, count_columns: usize) -> Self {
49 Self {
50 data: VecRecords::new(vec![vec![Text::default(); count_columns]; count_rows]),
51 styles: Styles::default(),
52 alignments: Alignments {
53 data: AlignmentHorizontal::Left,
54 index: AlignmentHorizontal::Right,
55 header: AlignmentHorizontal::Center,
56 columns: HashMap::default(),
57 cells: HashMap::default(),
58 },
59 config: TableConfig {
60 theme: TableTheme::basic(),
61 trim: TrimStrategy::truncate(None),
62 structure: TableStructure::new(false, false, false),
63 indent: TableIndent::new(1, 1),
64 header_on_border: false,
65 expand: false,
66 border_color: None,
67 },
68 }
69 }
70
71 pub fn count_rows(&self) -> usize {
73 self.data.count_rows()
74 }
75
76 pub fn count_columns(&self) -> usize {
78 self.data.count_columns()
79 }
80
81 pub fn insert(&mut self, pos: Position, text: String) {
82 self.data[pos.0][pos.1] = Text::new(text);
83 }
84
85 pub fn insert_row(&mut self, index: usize, row: Vec<String>) {
86 let data = &mut self.data[index];
87
88 for (col, text) in row.into_iter().enumerate() {
89 data[col] = Text::new(text);
90 }
91 }
92
93 pub fn set_row(&mut self, index: usize, row: Vec<NuRecordsValue>) {
94 assert_eq!(self.data[index].len(), row.len());
95 self.data[index] = row;
96 }
97
98 pub fn set_column_style(&mut self, column: usize, style: TextStyle) {
99 if let Some(style) = style.color_style {
100 let style = convert_style(style);
101 self.styles.columns.insert(column, style);
102 }
103
104 let alignment = convert_alignment(style.alignment);
105 if alignment != self.alignments.data {
106 self.alignments.columns.insert(column, alignment);
107 }
108 }
109
110 pub fn insert_style(&mut self, pos: Position, style: TextStyle) {
111 if let Some(style) = style.color_style {
112 let style = convert_style(style);
113 self.styles.cells.insert(pos, style);
114 }
115
116 let alignment = convert_alignment(style.alignment);
117 if alignment != self.alignments.data {
118 self.alignments.cells.insert(pos, alignment);
119 }
120 }
121
122 pub fn set_header_style(&mut self, style: TextStyle) {
123 if let Some(style) = style.color_style {
124 let style = convert_style(style);
125 self.styles.header = style;
126 }
127
128 self.alignments.header = convert_alignment(style.alignment);
129 }
130
131 pub fn set_index_style(&mut self, style: TextStyle) {
132 if let Some(style) = style.color_style {
133 let style = convert_style(style);
134 self.styles.index = style;
135 }
136
137 self.alignments.index = convert_alignment(style.alignment);
138 }
139
140 pub fn set_data_style(&mut self, style: TextStyle) {
141 if let Some(style) = style.color_style {
142 let style = convert_style(style);
143 self.styles.data = style;
144 }
145
146 self.alignments.data = convert_alignment(style.alignment);
147 }
148
149 pub fn set_indent(&mut self, indent: TableIndent) {
150 self.config.indent = indent;
151 }
152
153 pub fn set_theme(&mut self, theme: TableTheme) {
154 self.config.theme = theme;
155 }
156
157 pub fn set_structure(&mut self, index: bool, header: bool, footer: bool) {
158 self.config.structure = TableStructure::new(index, header, footer);
159 }
160
161 pub fn set_border_header(&mut self, on: bool) {
162 self.config.header_on_border = on;
163 }
164
165 pub fn set_trim(&mut self, strategy: TrimStrategy) {
166 self.config.trim = strategy;
167 }
168
169 pub fn set_strategy(&mut self, expand: bool) {
170 self.config.expand = expand;
171 }
172
173 pub fn set_border_color(&mut self, color: Style) {
174 self.config.border_color = (!color.is_plain()).then_some(color);
175 }
176
177 pub fn get_records_mut(&mut self) -> &mut NuRecords {
178 &mut self.data
179 }
180
181 pub fn draw(self, termwidth: usize) -> Option<String> {
185 build_table(self, termwidth)
186 }
187
188 pub fn total_width(&self) -> usize {
190 let config = create_config(&self.config.theme, false, None);
191 let pad = indent_sum(self.config.indent);
192 let widths = build_width(&self.data, pad);
193 get_total_width2(&widths, &config)
194 }
195}
196
197impl From<Vec<Vec<Text<String>>>> for NuTable {
198 fn from(value: Vec<Vec<Text<String>>>) -> Self {
199 let mut nutable = Self::new(0, 0);
200 nutable.data = VecRecords::new(value);
201
202 nutable
203 }
204}
205
206type Alignments = CellConfiguration<AlignmentHorizontal>;
207
208type Styles = CellConfiguration<Color>;
209
210#[derive(Debug, Default, Clone)]
211struct CellConfiguration<Value> {
212 data: Value,
213 index: Value,
214 header: Value,
215 columns: HashMap<usize, Value>,
216 cells: HashMap<Position, Value>,
217}
218
219#[derive(Debug, Clone)]
220pub struct TableConfig {
221 theme: TableTheme,
222 trim: TrimStrategy,
223 border_color: Option<Style>,
224 expand: bool,
225 structure: TableStructure,
226 header_on_border: bool,
227 indent: TableIndent,
228}
229
230#[derive(Debug, Clone)]
231struct TableStructure {
232 with_index: bool,
233 with_header: bool,
234 with_footer: bool,
235}
236
237impl TableStructure {
238 fn new(with_index: bool, with_header: bool, with_footer: bool) -> Self {
239 Self {
240 with_index,
241 with_header,
242 with_footer,
243 }
244 }
245}
246
247fn build_table(mut t: NuTable, termwidth: usize) -> Option<String> {
248 if t.count_columns() == 0 || t.count_rows() == 0 {
249 return Some(String::new());
250 }
251
252 let widths = table_truncate(&mut t, termwidth)?;
253 table_insert_footer(&mut t);
254 draw_table(t, widths, termwidth)
255}
256
257fn table_insert_footer(t: &mut NuTable) {
258 if t.config.structure.with_header && t.config.structure.with_footer {
259 duplicate_row(&mut t.data, 0);
260 }
261}
262
263fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option<Vec<usize>> {
264 let pad = t.config.indent.left + t.config.indent.right;
265 let widths = maybe_truncate_columns(&mut t.data, &t.config, termwidth, pad);
266 if widths.is_empty() {
267 return None;
268 }
269
270 Some(widths)
271}
272
273fn draw_table(t: NuTable, widths: Vec<usize>, termwidth: usize) -> Option<String> {
274 let structure = get_table_structure(&t.data, &t.config);
275 let sep_color = t.config.border_color;
276 let border_header = structure.with_header && t.config.header_on_border;
277
278 let data: Vec<Vec<_>> = t.data.into();
279 let mut table = Builder::from_vec(data).build();
280
281 set_indent(&mut table, t.config.indent);
282 load_theme(&mut table, &t.config.theme, &structure, sep_color);
283 align_table(&mut table, t.alignments, &structure);
284 colorize_table(&mut table, t.styles, &structure);
285
286 let pad = indent_sum(t.config.indent);
287 let width_ctrl = WidthCtrl::new(widths, t.config, termwidth, pad);
288
289 adjust_table(&mut table, width_ctrl, border_header, structure.with_footer);
290
291 table_to_string(table, termwidth)
292}
293
294fn indent_sum(indent: TableIndent) -> usize {
295 indent.left + indent.right
296}
297
298fn get_table_structure(data: &VecRecords<Text<String>>, cfg: &TableConfig) -> TableStructure {
299 let with_index = cfg.structure.with_index;
300 let with_header = cfg.structure.with_header && data.count_rows() > 1;
301 let with_footer = with_header && cfg.structure.with_footer;
302
303 TableStructure::new(with_index, with_header, with_footer)
304}
305
306fn adjust_table(table: &mut Table, width_ctrl: WidthCtrl, border_header: bool, with_footer: bool) {
307 if border_header {
308 if with_footer {
309 set_border_head_with_footer(table, width_ctrl);
310 } else {
311 set_border_head(table, width_ctrl);
312 }
313 } else {
314 table.with(width_ctrl);
315 }
316}
317
318fn set_indent(table: &mut Table, indent: TableIndent) {
319 table.with(Padding::new(indent.left, indent.right, 0, 0));
320}
321
322fn set_border_head(table: &mut Table, wctrl: WidthCtrl) {
323 let mut row = GetRow(0, Vec::new());
324 let mut row_opts = GetRowSettings(0, AlignmentHorizontal::Left, None);
325
326 table.with(&mut row);
327 table.with(&mut row_opts);
328
329 table.with(
330 Settings::default()
331 .with(strip_color_from_row(0))
332 .with(wctrl)
333 .with(MoveRowNext::new(0, 0))
334 .with(SetLineHeaders::new(0, row.1, row_opts.1, row_opts.2)),
335 );
336}
337
338fn set_border_head_with_footer(table: &mut Table, wctrl: WidthCtrl) {
339 let count_rows = table.count_rows();
342 let last_row_index = count_rows - 1;
343
344 let mut first_row = GetRow(0, Vec::new());
345 let mut head_settings = GetRowSettings(0, AlignmentHorizontal::Left, None);
346 let mut last_row = GetRow(last_row_index, Vec::new());
347
348 table.with(&mut first_row);
349 table.with(&mut head_settings);
350 table.with(&mut last_row);
351
352 let head = first_row.1;
353 let footer = last_row.1;
354 let alignment = head_settings.1;
355 let head_color = head_settings.2.clone();
356 let footer_color = head_settings.2;
357
358 table.with(
359 Settings::default()
360 .with(strip_color_from_row(0))
361 .with(strip_color_from_row(count_rows - 1))
362 .with(wctrl)
363 .with(MoveRowNext::new(0, 0))
364 .with(MoveRowPrev::new(last_row_index - 1, last_row_index))
365 .with(SetLineHeaders::new(0, head, alignment, head_color))
366 .with(SetLineHeaders::new(
367 last_row_index - 1,
368 footer,
369 alignment,
370 footer_color,
371 )),
372 );
373}
374
375fn table_to_string(table: Table, termwidth: usize) -> Option<String> {
376 let total_width = table.total_width();
377
378 if total_width > termwidth {
379 None
380 } else {
381 let content = table.to_string();
382 Some(content)
383 }
384}
385
386struct WidthCtrl {
387 width: Vec<usize>,
388 cfg: TableConfig,
389 width_max: usize,
390 pad: usize,
391}
392
393impl WidthCtrl {
394 fn new(width: Vec<usize>, cfg: TableConfig, max: usize, pad: usize) -> Self {
395 Self {
396 width,
397 cfg,
398 width_max: max,
399 pad,
400 }
401 }
402}
403
404impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for WidthCtrl {
405 fn change(
406 self,
407 rec: &mut NuRecords,
408 cfg: &mut ColoredConfig,
409 dim: &mut CompleteDimensionVecRecords<'_>,
410 ) {
411 let total_width = get_total_width2(&self.width, cfg);
412
413 let need_truncation = total_width > self.width_max;
414 if need_truncation {
415 let has_header = self.cfg.structure.with_header && rec.count_rows() > 1;
416 let as_head = has_header && self.cfg.header_on_border;
417
418 let trim = TableTrim::new(self.width, self.width_max, self.cfg.trim, as_head, self.pad);
419 trim.change(rec, cfg, dim);
420 return;
421 }
422
423 let need_expansion = self.cfg.expand && self.width_max > total_width;
424 if need_expansion {
425 let opt = (SetDimensions(self.width), Width::increase(self.width_max));
426 TableOption::<VecRecords<_>, _, _>::change(opt, rec, cfg, dim);
427 return;
428 }
429
430 SetDimensions(self.width).change(rec, cfg, dim);
431 }
432}
433
434struct TableTrim {
435 width: Vec<usize>,
436 width_max: usize,
437 strategy: TrimStrategy,
438 trim_as_head: bool,
439 pad: usize,
440}
441
442impl TableTrim {
443 fn new(
444 width: Vec<usize>,
445 width_max: usize,
446 strategy: TrimStrategy,
447 trim_as_head: bool,
448 pad: usize,
449 ) -> Self {
450 Self {
451 width,
452 strategy,
453 pad,
454 width_max,
455 trim_as_head,
456 }
457 }
458}
459
460impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for TableTrim {
461 fn change(
462 self,
463 recs: &mut NuRecords,
464 cfg: &mut ColoredConfig,
465 dims: &mut CompleteDimensionVecRecords<'_>,
466 ) {
467 if self.trim_as_head {
470 trim_as_header(recs, cfg, dims, self);
471 return;
472 }
473
474 match self.strategy {
475 TrimStrategy::Wrap { try_to_keep_words } => {
476 let wrap = Width::wrap(self.width_max)
477 .keep_words(try_to_keep_words)
478 .priority(Priority::max(false));
479
480 let opt = (SetDimensions(self.width), wrap);
481 TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
482 }
483 TrimStrategy::Truncate { suffix } => {
484 let mut truncate = Width::truncate(self.width_max).priority(Priority::max(false));
485 if let Some(suffix) = suffix {
486 truncate = truncate.suffix(suffix).suffix_try_color(true);
487 }
488
489 let opt = (SetDimensions(self.width), truncate);
490 TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
491 }
492 }
493 }
494}
495
496fn trim_as_header(
497 recs: &mut VecRecords<Text<String>>,
498 cfg: &mut ColoredConfig,
499 dims: &mut CompleteDimensionVecRecords,
500 trim: TableTrim,
501) {
502 if recs.is_empty() {
503 return;
504 }
505
506 let headers = recs[0].to_owned();
507 let headers_widths = headers
508 .iter()
509 .map(Text::width)
510 .map(|v| v + trim.pad)
511 .collect::<Vec<_>>();
512 let min_width_use = get_total_width2(&headers_widths, cfg);
513 let mut free_width = trim.width_max.saturating_sub(min_width_use);
514
515 let mut widths = vec![0; headers_widths.len()];
518 for (i, head_width) in headers_widths.into_iter().enumerate() {
519 let column_width = trim.width[i]; let mut use_width = head_width;
522 if free_width > 0 {
523 debug_assert!(column_width >= head_width);
525
526 let additional_width = min(free_width, column_width - head_width);
527 free_width -= additional_width;
528 use_width += additional_width;
529 }
530
531 widths[i] = use_width;
532 }
533
534 let expected_width = get_total_width2(&widths, cfg);
537 if expected_width > trim.width_max {
538 let mut diff = expected_width - trim.width_max;
539 'out: loop {
540 let (biggest_column, &value) = widths
541 .iter()
542 .enumerate()
543 .max_by_key(|(_, &value)| value)
544 .expect("ok");
545 if value <= trim.pad {
546 unreachable!("theoretically must never happen at this point")
547 }
548
549 widths[biggest_column] -= 1;
550 diff -= 1;
551
552 if diff == 0 {
553 break 'out;
554 }
555 }
556 }
557
558 for (i, width) in widths.iter().cloned().enumerate() {
559 let width = width - trim.pad;
560
561 match &trim.strategy {
562 TrimStrategy::Wrap { try_to_keep_words } => {
563 let wrap = Width::wrap(width).keep_words(*try_to_keep_words);
564
565 let opt = Modify::new(Columns::single(i)).with(wrap);
566 TableOption::<VecRecords<_>, _, _>::change(opt, recs, cfg, dims);
567 }
568 TrimStrategy::Truncate { suffix } => {
569 let mut truncate = Width::truncate(width);
570 if let Some(suffix) = suffix {
571 truncate = truncate.suffix(suffix).suffix_try_color(true);
572 }
573
574 let opt = Modify::new(Columns::single(i)).with(truncate);
575 TableOption::<VecRecords<_>, _, _>::change(opt, recs, cfg, dims);
576 }
577 }
578 }
579
580 TableOption::change(SetDimensions(widths), recs, cfg, dims);
581}
582
583fn align_table(table: &mut Table, alignments: Alignments, structure: &TableStructure) {
584 table.with(AlignmentStrategy::PerLine);
585 table.with(Alignment::from(alignments.data));
586
587 for (column, alignment) in alignments.columns {
588 table.modify(Columns::single(column), Alignment::from(alignment));
589 }
590
591 for (pos, alignment) in alignments.cells {
592 table.modify(pos, Alignment::from(alignment));
593 }
594
595 if structure.with_header {
596 table.modify(Rows::first(), Alignment::from(alignments.header));
597
598 if structure.with_footer {
599 table.modify(Rows::last(), Alignment::from(alignments.header));
600 }
601 }
602
603 if structure.with_index {
604 table.modify(Columns::first(), Alignment::from(alignments.index));
605 }
606}
607
608fn colorize_table(table: &mut Table, styles: Styles, structure: &TableStructure) {
609 if !is_color_empty(&styles.data) {
610 table.with(styles.data);
611 }
612
613 for (column, color) in styles.columns {
614 if !is_color_empty(&color) {
615 table.modify(Columns::single(column), color);
616 }
617 }
618
619 for (pos, color) in styles.cells {
620 if !is_color_empty(&color) {
621 table.modify(pos, color);
622 }
623 }
624
625 if structure.with_index && !is_color_empty(&styles.index) {
626 table.modify(Columns::first(), styles.index);
627 }
628
629 if structure.with_header && !is_color_empty(&styles.header) {
630 table.modify(Rows::first(), styles.header.clone());
631 }
632
633 if structure.with_footer && !is_color_empty(&styles.header) {
634 table.modify(Rows::last(), styles.header);
635 }
636}
637
638fn load_theme(
639 table: &mut Table,
640 theme: &TableTheme,
641 structure: &TableStructure,
642 sep_color: Option<Style>,
643) {
644 let mut theme = theme.as_base().clone();
645
646 if !structure.with_header {
647 let borders = *theme.get_borders();
648 theme.remove_horizontal_lines();
649 theme.set_borders(borders);
650 } else if structure.with_footer {
651 theme_copy_horizontal_line(&mut theme, 1, table.count_rows() - 1);
652 }
653
654 table.with(theme);
655
656 if let Some(style) = sep_color {
657 let color = convert_style(style);
658 let color = ANSIBuf::from(color);
659 table.get_config_mut().set_border_color_default(color);
660 }
661}
662
663fn maybe_truncate_columns(
664 data: &mut NuRecords,
665 cfg: &TableConfig,
666 termwidth: usize,
667 pad: usize,
668) -> Vec<usize> {
669 const TERMWIDTH_THRESHOLD: usize = 120;
670
671 let preserve_content = termwidth > TERMWIDTH_THRESHOLD;
672 let has_header = cfg.structure.with_header && data.count_rows() > 1;
673 let is_header_on_border = has_header && cfg.header_on_border;
674
675 let truncate = if is_header_on_border {
676 truncate_columns_by_head
677 } else if preserve_content {
678 truncate_columns_by_columns
679 } else {
680 truncate_columns_by_content
681 };
682
683 truncate(data, &cfg.theme, pad, termwidth)
684}
685
686fn truncate_columns_by_content(
688 data: &mut NuRecords,
689 theme: &TableTheme,
690 pad: usize,
691 termwidth: usize,
692) -> Vec<usize> {
693 const MIN_ACCEPTABLE_WIDTH: usize = 3;
694 const TRAILING_COLUMN_WIDTH: usize = 5;
695
696 let config = create_config(theme, false, None);
697 let mut widths = build_width(&*data, pad);
698 let total_width = get_total_width2(&widths, &config);
699 if total_width <= termwidth {
700 return widths;
701 }
702
703 let borders = config.get_borders();
704 let vertical_border_i = borders.has_vertical() as usize;
705
706 let mut width = borders.has_left() as usize + borders.has_right() as usize;
707 let mut truncate_pos = 0;
708 for column_width in &widths {
709 width += column_width;
710 width += vertical_border_i;
711
712 if width >= termwidth {
713 width -= column_width;
715 width += MIN_ACCEPTABLE_WIDTH;
716
717 if width <= termwidth {
718 truncate_pos += 1;
719 }
720
721 break;
722 }
723
724 truncate_pos += 1;
725 }
726
727 if truncate_pos == data.count_columns() {
729 return widths;
730 }
731
732 if truncate_pos == 0 {
733 return vec![];
734 }
735
736 truncate_columns(data, truncate_pos);
737 widths.truncate(truncate_pos);
738
739 let min_width = borders.has_left() as usize
742 + borders.has_right() as usize
743 + data.count_columns() * MIN_ACCEPTABLE_WIDTH
744 + (data.count_columns() - 1) * vertical_border_i;
745
746 let diff = termwidth - min_width;
747 let can_be_squeezed = diff > TRAILING_COLUMN_WIDTH + vertical_border_i;
748
749 if can_be_squeezed {
750 push_empty_column(data);
751 widths.push(3 + pad);
752 } else {
753 if data.count_columns() == 1 {
754 return vec![];
755 }
756
757 truncate_columns(data, data.count_columns() - 1);
758 push_empty_column(data);
759 widths.pop();
760 widths.push(3 + pad);
761 }
762
763 widths
764}
765
766fn truncate_columns_by_columns(
768 data: &mut NuRecords,
769 theme: &TableTheme,
770 pad: usize,
771 termwidth: usize,
772) -> Vec<usize> {
773 let acceptable_width = 10 + pad;
774 let trailing_column_width = 3 + pad;
775
776 let config = create_config(theme, false, None);
777 let mut widths = build_width(&*data, pad);
778 let total_width = get_total_width2(&widths, &config);
779 if total_width <= termwidth {
780 return widths;
781 }
782
783 let widths_total = widths.iter().sum::<usize>();
784 let min_widths = widths
785 .iter()
786 .map(|w| min(*w, acceptable_width))
787 .sum::<usize>();
788 let mut min_total = total_width - widths_total + min_widths;
789
790 if min_total <= termwidth {
791 return widths;
792 }
793
794 let mut i = 0;
796 while data.count_columns() > 0 {
797 i += 1;
798
799 let column = data.count_columns() - 1 - i;
800 let width = min(widths[column], acceptable_width);
801 min_total -= width;
802
803 if config.get_borders().has_vertical() {
804 min_total -= 1;
805 }
806
807 if min_total <= termwidth {
808 break;
809 }
810 }
811
812 if i + 1 == data.count_columns() {
813 return vec![];
814 }
815
816 truncate_columns(data, data.count_columns() - i);
817 widths.pop();
818
819 let diff = termwidth - min_total;
821 if diff > trailing_column_width {
822 push_empty_column(data);
823 widths.push(3 + pad);
824 } else {
825 if data.count_columns() == 1 {
826 return vec![];
827 }
828
829 truncate_columns(data, data.count_columns() - 1);
830 push_empty_column(data);
831 widths.pop();
832 widths.push(3 + pad);
833 }
834
835 widths
836}
837
838fn truncate_columns_by_head(
840 data: &mut NuRecords,
841 theme: &TableTheme,
842 pad: usize,
843 termwidth: usize,
844) -> Vec<usize> {
845 const TRAILING_COLUMN_WIDTH: usize = 5;
846
847 let config = create_config(theme, false, None);
848 let mut widths = build_width(&*data, pad);
849 let total_width = get_total_width2(&widths, &config);
850 if total_width <= termwidth {
851 return widths;
852 }
853
854 if data.is_empty() {
855 return widths;
856 }
857
858 let head = &data[0];
859
860 let borders = config.get_borders();
861 let has_vertical = borders.has_vertical();
862
863 let mut width = borders.has_left() as usize + borders.has_right() as usize;
864 let mut truncate_pos = 0;
865 for (i, column_header) in head.iter().enumerate() {
866 let column_header_width = Cell::width(column_header);
867 width += column_header_width + pad;
868
869 if i > 0 {
870 width += has_vertical as usize;
871 }
872
873 if width >= termwidth {
874 width -= column_header_width + (i > 0 && has_vertical) as usize + pad;
875 break;
876 }
877
878 truncate_pos += 1;
879 }
880
881 if truncate_pos == head.len() {
883 return widths;
884 }
885
886 if truncate_pos == 0 {
887 return vec![];
888 }
889
890 truncate_columns(data, truncate_pos);
891 widths.truncate(truncate_pos);
892
893 let min_width = width;
896
897 let diff = termwidth - min_width;
898 let can_trailing_column_be_pushed = diff > TRAILING_COLUMN_WIDTH + has_vertical as usize;
899
900 if !can_trailing_column_be_pushed {
901 if data.count_columns() == 1 {
902 return vec![];
903 }
904
905 truncate_columns(data, data.count_columns() - 1);
906 widths.pop();
907 }
908
909 push_empty_column(data);
910 widths.push(3 + pad);
911
912 widths
913}
914
915fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
916 let total = widths.iter().sum::<usize>();
917 let countv = cfg.count_vertical(widths.len());
918 let margin = cfg.get_margin();
919
920 total + countv + margin.left.size + margin.right.size
921}
922
923fn create_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> ColoredConfig {
924 let structure = TableStructure::new(false, with_header, false);
925 let mut table = Table::new([[""]]);
926 load_theme(&mut table, theme, &structure, color);
927 table.get_config().clone()
928}
929
930fn push_empty_column(data: &mut NuRecords) {
931 let records = std::mem::take(data);
932 let mut inner: Vec<Vec<_>> = records.into();
933
934 let empty_cell = Text::new(String::from("..."));
935 for row in &mut inner {
936 row.push(empty_cell.clone());
937 }
938
939 *data = VecRecords::new(inner);
940}
941
942fn duplicate_row(data: &mut NuRecords, row: usize) {
943 let records = std::mem::take(data);
944 let mut inner: Vec<Vec<_>> = records.into();
945
946 let duplicate = inner[row].clone();
947 inner.push(duplicate);
948
949 *data = VecRecords::new(inner);
950}
951
952fn truncate_columns(data: &mut NuRecords, count: usize) {
953 let records = std::mem::take(data);
954 let mut inner: Vec<Vec<_>> = records.into();
955
956 for row in &mut inner {
957 row.truncate(count);
958 }
959
960 *data = VecRecords::new(inner);
961}
962
963fn convert_alignment(alignment: nu_color_config::Alignment) -> AlignmentHorizontal {
964 match alignment {
965 nu_color_config::Alignment::Center => AlignmentHorizontal::Center,
966 nu_color_config::Alignment::Left => AlignmentHorizontal::Left,
967 nu_color_config::Alignment::Right => AlignmentHorizontal::Right,
968 }
969}
970
971struct SetDimensions(Vec<usize>);
972
973impl<R> TableOption<R, ColoredConfig, CompleteDimensionVecRecords<'_>> for SetDimensions {
974 fn change(self, _: &mut R, _: &mut ColoredConfig, dims: &mut CompleteDimensionVecRecords<'_>) {
975 dims.set_widths(self.0);
976 }
977}
978
979fn build_width(records: &NuRecords, pad: usize) -> Vec<usize> {
982 let count_columns = records.count_columns();
983 let mut widths = vec![0; count_columns];
984 for columns in records.iter_rows() {
985 for (col, cell) in columns.iter().enumerate() {
986 let width = Cell::width(cell) + pad;
987 widths[col] = std::cmp::max(widths[col], width);
988 }
989 }
990
991 widths
992}
993
994struct GetRow(usize, Vec<String>);
995
996impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for &mut GetRow {
997 fn change(
998 self,
999 recs: &mut NuRecords,
1000 _: &mut ColoredConfig,
1001 _: &mut CompleteDimensionVecRecords<'_>,
1002 ) {
1003 let row = self.0;
1004 self.1 = recs[row].iter().map(|c| c.as_ref().to_owned()).collect();
1005 }
1006}
1007
1008struct GetRowSettings(usize, AlignmentHorizontal, Option<Color>);
1009
1010impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>>
1011 for &mut GetRowSettings
1012{
1013 fn change(
1014 self,
1015 _: &mut NuRecords,
1016 cfg: &mut ColoredConfig,
1017 _: &mut CompleteDimensionVecRecords<'_>,
1018 ) {
1019 let row = self.0;
1020 self.1 = *cfg.get_alignment_horizontal(Entity::Row(row));
1021 self.2 = cfg
1022 .get_colors()
1023 .get_color((row, 0))
1024 .cloned()
1025 .map(Color::from);
1026 }
1027}
1028
1029struct SetLineHeaders {
1032 line: usize,
1033 columns: Vec<String>,
1034 alignment: AlignmentHorizontal,
1035 color: Option<Color>,
1036}
1037
1038impl SetLineHeaders {
1039 fn new(
1040 line: usize,
1041 columns: Vec<String>,
1042 alignment: AlignmentHorizontal,
1043 color: Option<Color>,
1044 ) -> Self {
1045 Self {
1046 line,
1047 columns,
1048 alignment,
1049 color,
1050 }
1051 }
1052}
1053
1054impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for SetLineHeaders {
1055 fn change(
1056 self,
1057 recs: &mut NuRecords,
1058 cfg: &mut ColoredConfig,
1059 dims: &mut CompleteDimensionVecRecords<'_>,
1060 ) {
1061 let widths = match dims.get_widths() {
1062 Some(widths) => widths,
1063 None => {
1064 unreachable!("must never be the case");
1070 }
1071 };
1072
1073 let columns: Vec<_> = self
1074 .columns
1075 .into_iter()
1076 .zip(widths.iter().cloned()) .map(|(s, width)| Truncate::truncate(&s, width).into_owned())
1078 .collect();
1079
1080 let mut names = ColumnNames::new(columns)
1081 .line(self.line)
1082 .alignment(Alignment::from(self.alignment));
1083 if let Some(color) = self.color {
1084 names = names.color(color);
1085 }
1086
1087 names.change(recs, cfg, dims);
1088 }
1089
1090 fn hint_change(&self) -> Option<Entity> {
1091 None
1092 }
1093}
1094
1095struct MoveRowNext {
1096 row: usize,
1097 line: usize,
1098}
1099
1100impl MoveRowNext {
1101 fn new(row: usize, line: usize) -> Self {
1102 Self { row, line }
1103 }
1104}
1105
1106struct MoveRowPrev {
1107 row: usize,
1108 line: usize,
1109}
1110
1111impl MoveRowPrev {
1112 fn new(row: usize, line: usize) -> Self {
1113 Self { row, line }
1114 }
1115}
1116
1117impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for MoveRowNext {
1118 fn change(
1119 self,
1120 recs: &mut NuRecords,
1121 cfg: &mut ColoredConfig,
1122 _: &mut CompleteDimensionVecRecords<'_>,
1123 ) {
1124 row_shift_next(recs, cfg, self.row, self.line);
1125 }
1126
1127 fn hint_change(&self) -> Option<Entity> {
1128 None
1129 }
1130}
1131
1132impl TableOption<NuRecords, ColoredConfig, CompleteDimensionVecRecords<'_>> for MoveRowPrev {
1133 fn change(
1134 self,
1135 recs: &mut NuRecords,
1136 cfg: &mut ColoredConfig,
1137 _: &mut CompleteDimensionVecRecords<'_>,
1138 ) {
1139 row_shift_prev(recs, cfg, self.row, self.line);
1140 }
1141
1142 fn hint_change(&self) -> Option<Entity> {
1143 None
1144 }
1145}
1146
1147fn row_shift_next(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) {
1148 let count_rows = recs.count_rows();
1149 let count_columns = recs.count_columns();
1150 let has_line = cfg.has_horizontal(line, count_rows);
1151 let has_next_line = cfg.has_horizontal(line + 1, count_rows);
1152 if !has_line && !has_next_line {
1153 return;
1154 }
1155
1156 recs.remove_row(row);
1157 let count_rows = recs.count_rows();
1158
1159 shift_alignments_down(cfg, row, count_rows, count_columns);
1160 shift_colors_down(cfg, row, count_rows, count_columns);
1161
1162 if !has_line {
1163 shift_lines_up(cfg, count_rows, &[line + 1]);
1164 } else {
1165 remove_lines(cfg, count_rows, &[line + 1]);
1166 }
1167
1168 shift_lines_up(cfg, count_rows, &[count_rows]);
1169}
1170
1171fn row_shift_prev(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) {
1172 let mut count_rows = recs.count_rows();
1173 let count_columns = recs.count_columns();
1174 let has_line = cfg.has_horizontal(line, count_rows);
1175 let has_prev_line = cfg.has_horizontal(line - 1, count_rows);
1176 if !has_line && !has_prev_line {
1177 return;
1178 }
1179
1180 recs.remove_row(row);
1181
1182 if !has_line {
1183 return;
1184 }
1185
1186 count_rows -= 1;
1187
1188 shift_alignments_down(cfg, row, count_rows, count_columns);
1189 shift_colors_down(cfg, row, count_rows, count_columns);
1190 remove_lines(cfg, count_rows, &[line - 1]);
1191}
1192
1193fn remove_lines(cfg: &mut ColoredConfig, count_rows: usize, line: &[usize]) {
1194 for &line in line {
1195 cfg.remove_horizontal_line(line, count_rows)
1196 }
1197}
1198
1199fn shift_alignments_down(
1200 cfg: &mut ColoredConfig,
1201 row: usize,
1202 count_rows: usize,
1203 count_columns: usize,
1204) {
1205 for row in row..count_rows {
1206 for col in 0..count_columns {
1207 let pos = (row + 1, col).into();
1208 let posn = (row, col).into();
1209 let align = *cfg.get_alignment_horizontal(pos);
1210 cfg.set_alignment_horizontal(posn, align);
1211 }
1212
1213 let align = *cfg.get_alignment_horizontal(Entity::Row(row + 1));
1214 cfg.set_alignment_horizontal(Entity::Row(row), align);
1215 }
1216}
1217
1218fn shift_colors_down(cfg: &mut ColoredConfig, row: usize, count_rows: usize, count_columns: usize) {
1219 for row in row..count_rows {
1220 for col in 0..count_columns {
1221 let pos = (row + 1, col);
1222 let posn = (row, col).into();
1223 let color = cfg.get_colors().get_color(pos).cloned();
1224 if let Some(color) = color {
1225 cfg.set_color(posn, color);
1226 }
1227 }
1228 }
1229}
1230
1231fn shift_lines_up(cfg: &mut ColoredConfig, count_rows: usize, lines: &[usize]) {
1232 for &i in lines {
1233 let line = cfg.get_horizontal_line(i).cloned();
1234 if let Some(line) = line {
1235 cfg.insert_horizontal_line(i - 1, line);
1236 cfg.remove_horizontal_line(i, count_rows);
1237 }
1238 }
1239}
1240
1241fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) {
1242 if let Some(line) = theme.get_horizontal_line(from) {
1243 theme.insert_horizontal_line(to, *line);
1244 }
1245}
1246
1247#[allow(clippy::type_complexity)]
1248fn strip_color_from_row(row: usize) -> ModifyList<Row, FormatContent<fn(&str) -> String>> {
1249 fn foo(s: &str) -> String {
1250 strip_ansi_unlikely(s).into_owned()
1251 }
1252
1253 Modify::new(Rows::single(row)).with(Format::content(foo))
1254}