nu_table/
table.rs

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