nu_table/
table.rs

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/// NuTable is a table rendering implementation.
38#[derive(Debug, Clone)]
39pub struct NuTable {
40    data: NuRecords,
41    styles: Styles,
42    alignments: Alignments,
43    config: TableConfig,
44}
45
46impl NuTable {
47    /// Creates an empty [`NuTable`] instance.
48    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    /// Return amount of rows.
72    pub fn count_rows(&self) -> usize {
73        self.data.count_rows()
74    }
75
76    /// Return amount of columns.
77    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    /// Converts a table to a String.
182    ///
183    /// It returns None in case where table cannot be fit to a terminal width.
184    pub fn draw(self, termwidth: usize) -> Option<String> {
185        build_table(self, termwidth)
186    }
187
188    /// Return a total table width.
189    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    // note: funnily last and row must be equal at this point but we do not rely on it just in case.
340
341    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        // we already must have been estimated that it's safe to do.
468        // and all dims will be suffitient
469        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    // even though it's safe to trim columns by header there might be left unused space
516    // so we do use it if possible prioritizing left columns
517    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]; // safe to assume width is bigger then padding
520
521        let mut use_width = head_width;
522        if free_width > 0 {
523            // it's safe to assume that column_width is always bigger or equal to head_width
524            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    // make sure we are fit in;
535    // which is might not be the case where we need to truncate columns further then a column head width
536    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
686// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE.
687fn 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            // check whether we CAN limit the column width
714            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    // we don't need any truncation then (is it possible?)
728    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    // Append columns with a trailing column
740
741    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
766// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE
767fn 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    // todo: simplify the loop
795    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    // Append columns with a trailing column
820    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
838// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE.
839fn 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    // we don't need any truncation then (is it possible?)
882    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    // Append columns with a trailing column
894
895    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
979// it assumes no spans is used.
980// todo: Could be replaced by Dimension impl usage
981fn 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
1029// It's laverages a use of guuaranted cached widths before hand
1030// to speed up things a bit.
1031struct 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                // we don't have widths cached; which means that NO width adjustments were done
1065                // which means we are OK to leave columns as they are.
1066                //
1067                // but we actually always have to have widths at this point
1068
1069                unreachable!("must never be the case");
1070            }
1071        };
1072
1073        let columns: Vec<_> = self
1074            .columns
1075            .into_iter()
1076            .zip(widths.iter().cloned()) // it must be always safe to do
1077            .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}