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