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