Skip to main content

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            && !style.is_plain()
192        {
193            let style = convert_style(style);
194            self.styles.cfg.set_color(pos.into(), style.into());
195        }
196
197        let alignment = convert_alignment(style.alignment);
198        if alignment != self.styles.alignments.data {
199            self.styles
200                .cfg
201                .set_alignment_horizontal(pos.into(), alignment);
202        }
203    }
204
205    pub fn set_header_style(&mut self, style: TextStyle) {
206        if let Some(style) = style.color_style
207            && !style.is_plain()
208        {
209            let style = convert_style(style);
210            self.styles.colors.header = style;
211        }
212
213        self.styles.alignments.header = convert_alignment(style.alignment);
214    }
215
216    pub fn set_index_style(&mut self, style: TextStyle) {
217        if let Some(style) = style.color_style
218            && !style.is_plain()
219        {
220            let style = convert_style(style);
221            self.styles.colors.index = style;
222        }
223
224        self.styles.alignments.index = convert_alignment(style.alignment);
225    }
226
227    pub fn set_data_style(&mut self, style: TextStyle) {
228        if let Some(style) = style.color_style
229            && !style.is_plain()
230        {
231            let style = convert_style(style);
232            self.styles.cfg.set_color(Entity::Global, style.into());
233        }
234
235        let alignment = convert_alignment(style.alignment);
236        self.styles
237            .cfg
238            .set_alignment_horizontal(Entity::Global, alignment);
239        self.styles.alignments.data = alignment;
240    }
241
242    // NOTE: Crusial to be called before data changes (todo fix interface)
243    pub fn set_indent(&mut self, indent: TableIndent) {
244        self.config.indent = indent;
245
246        let pad = indent_sum(indent);
247        for w in &mut self.widths {
248            *w = pad;
249        }
250    }
251
252    pub fn set_theme(&mut self, theme: TableTheme) {
253        self.config.theme = theme;
254    }
255
256    pub fn set_structure(&mut self, index: bool, header: bool, footer: bool) {
257        self.config.structure = TableStructure::new(index, header, footer);
258    }
259
260    pub fn set_border_header(&mut self, on: bool) {
261        self.config.header_on_border = on;
262    }
263
264    pub fn set_trim(&mut self, strategy: TrimStrategy) {
265        self.config.trim = strategy;
266    }
267
268    pub fn set_strategy(&mut self, expand: bool) {
269        self.config.expand = expand;
270    }
271
272    pub fn set_border_color(&mut self, color: Style) {
273        self.config.border_color = (!color.is_plain()).then_some(color);
274    }
275
276    pub fn clear_border_color(&mut self) {
277        self.config.border_color = None;
278    }
279
280    // NOTE: BE CAREFUL TO KEEP WIDTH UNCHANGED
281    // TODO: fix interface
282    pub fn get_records_mut(&mut self) -> &mut [Vec<NuRecordsValue>] {
283        &mut self.data
284    }
285
286    pub fn clear_all_colors(&mut self) {
287        self.clear_border_color();
288        let cfg = std::mem::take(&mut self.styles.cfg);
289        self.styles.cfg = ColoredConfig::new(cfg.into_inner());
290    }
291
292    /// Converts a table to a String.
293    ///
294    /// It returns None in case where table cannot be fit to a terminal width.
295    pub fn draw(self, termwidth: usize) -> Option<String> {
296        build_table(self, termwidth)
297    }
298
299    /// Converts a table to a String.
300    ///
301    /// It returns None in case where table cannot be fit to a terminal width.
302    pub fn draw_unchecked(self, termwidth: usize) -> Option<String> {
303        build_table_unchecked(self, termwidth)
304    }
305
306    /// Return a total table width.
307    pub fn total_width(&self) -> usize {
308        let config = create_config(&self.config.theme, false, None);
309        get_total_width2(&self.widths, &config)
310    }
311}
312
313// NOTE: Must never be called from nu-table - made only for tests
314// FIXME: remove it?
315// #[cfg(test)]
316impl From<Vec<Vec<Text<String>>>> for NuTable {
317    fn from(value: Vec<Vec<Text<String>>>) -> Self {
318        let count_rows = value.len();
319        let count_cols = if value.is_empty() { 0 } else { value[0].len() };
320
321        let mut t = Self::new(count_rows, count_cols);
322        for (i, row) in value.into_iter().enumerate() {
323            t.set_row(i, row);
324        }
325
326        table_recalculate_widths(&mut t);
327
328        t
329    }
330}
331
332fn table_recalculate_widths(t: &mut NuTable) {
333    let pad = indent_sum(t.config.indent);
334    t.widths = build_width(&t.data, t.count_cols, t.count_rows, pad);
335}
336
337#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Hash)]
338struct CellConfiguration<Value> {
339    index: Value,
340    header: Value,
341    data: Value,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
345struct Styles {
346    cfg: ColoredConfig,
347    colors: CellConfiguration<Color>,
348    alignments: CellConfiguration<AlignmentHorizontal>,
349}
350
351#[derive(Debug, Clone)]
352pub struct TableConfig {
353    theme: TableTheme,
354    trim: TrimStrategy,
355    border_color: Option<Style>,
356    expand: bool,
357    structure: TableStructure,
358    header_on_border: bool,
359    indent: TableIndent,
360}
361
362#[derive(Debug, Clone, Copy)]
363struct TableStructure {
364    with_index: bool,
365    with_header: bool,
366    with_footer: bool,
367}
368
369impl TableStructure {
370    fn new(with_index: bool, with_header: bool, with_footer: bool) -> Self {
371        Self {
372            with_index,
373            with_header,
374            with_footer,
375        }
376    }
377}
378
379#[derive(Debug, Clone)]
380struct HeadInfo {
381    values: Vec<String>,
382    align: AlignmentHorizontal,
383    #[allow(dead_code)]
384    align_index: AlignmentHorizontal,
385    color: Option<Color>,
386}
387
388impl HeadInfo {
389    fn new(
390        values: Vec<String>,
391        align: AlignmentHorizontal,
392        align_index: AlignmentHorizontal,
393        color: Option<Color>,
394    ) -> Self {
395        Self {
396            values,
397            align,
398            align_index,
399            color,
400        }
401    }
402}
403
404fn build_table_unchecked(mut t: NuTable, termwidth: usize) -> Option<String> {
405    if t.count_columns() == 0 || t.count_rows() == 0 {
406        return Some(String::new());
407    }
408
409    let widths = std::mem::take(&mut t.widths);
410    let config = create_config(&t.config.theme, false, None);
411    let totalwidth = get_total_width2(&t.widths, &config);
412    let widths = WidthEstimation::new(widths.clone(), widths, totalwidth, false, false);
413
414    let head = remove_header_if(&mut t);
415    table_insert_footer_if(&mut t);
416
417    draw_table(t, widths, head, termwidth)
418}
419
420fn build_table(mut t: NuTable, termwidth: usize) -> Option<String> {
421    if t.count_columns() == 0 || t.count_rows() == 0 {
422        return Some(String::new());
423    }
424
425    let widths = table_truncate(&mut t, termwidth)?;
426    let head = remove_header_if(&mut t);
427    table_insert_footer_if(&mut t);
428
429    draw_table(t, widths, head, termwidth)
430}
431
432fn remove_header_if(t: &mut NuTable) -> Option<HeadInfo> {
433    if !is_header_on_border(t) {
434        return None;
435    }
436
437    let head = remove_header(t);
438    t.config.structure.with_header = false;
439
440    Some(head)
441}
442
443fn is_header_on_border(t: &NuTable) -> bool {
444    let is_configured = t.config.structure.with_header && t.config.header_on_border;
445    let has_horizontal = t.config.theme.as_base().borders_has_top()
446        || t.config.theme.as_base().get_horizontal_line(1).is_some();
447    is_configured && has_horizontal
448}
449
450fn table_insert_footer_if(t: &mut NuTable) {
451    let with_footer = t.config.structure.with_header && t.config.structure.with_footer;
452    if !with_footer {
453        return;
454    }
455
456    duplicate_row(&mut t.data, 0);
457
458    if !t.heights.is_empty() {
459        t.heights.push(t.heights[0]);
460    }
461}
462
463fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option<WidthEstimation> {
464    let truncate_by_head = is_header_on_border(t);
465    let widths = maybe_truncate_columns(
466        &mut t.data,
467        t.widths.clone(),
468        &t.config,
469        termwidth,
470        truncate_by_head,
471    );
472    if widths.needed.is_empty() {
473        return None;
474    }
475
476    // reset style for last column which is a trail one
477    if widths.trail {
478        let col = widths.needed.len() - 1;
479        for row in 0..t.count_rows {
480            t.styles
481                .cfg
482                .set_alignment_horizontal(Entity::Cell(row, col), t.styles.alignments.data);
483            t.styles
484                .cfg
485                .set_color(Entity::Cell(row, col), ANSIBuf::default());
486        }
487    }
488
489    Some(widths)
490}
491
492fn remove_header(t: &mut NuTable) -> HeadInfo {
493    // move settings by one row down
494    for row in 1..t.data.len() {
495        for col in 0..t.count_cols {
496            let from = Position::new(row, col);
497            let to = Position::new(row - 1, col);
498
499            let alignment = *t.styles.cfg.get_alignment_horizontal(from);
500            if alignment != t.styles.alignments.data {
501                t.styles.cfg.set_alignment_horizontal(to.into(), alignment);
502            }
503
504            let color = t.styles.cfg.get_color(from);
505            if let Some(color) = color
506                && !color.is_empty()
507            {
508                let color = color.clone();
509                t.styles.cfg.set_color(to.into(), color);
510            }
511        }
512    }
513
514    let head = t
515        .data
516        .remove(0)
517        .into_iter()
518        .map(|s| s.to_string())
519        .collect();
520
521    // drop height row
522    t.heights.remove(0);
523
524    // WE NEED TO RELCULATE WIDTH.
525    // TODO: cause we have configuration beforehand we can just not calculate it in?
526    // Why we do it exactly??
527    table_recalculate_widths(t);
528
529    let color = get_color_if_exists(&t.styles.colors.header);
530    let alignment = t.styles.alignments.header;
531    let alignment_index = if t.config.structure.with_index {
532        t.styles.alignments.index
533    } else {
534        t.styles.alignments.header
535    };
536
537    t.styles.alignments.header = AlignmentHorizontal::Center;
538    t.styles.colors.header = Color::empty();
539
540    HeadInfo::new(head, alignment, alignment_index, color)
541}
542
543fn draw_table(
544    t: NuTable,
545    width: WidthEstimation,
546    head: Option<HeadInfo>,
547    termwidth: usize,
548) -> Option<String> {
549    let mut structure = t.config.structure;
550    structure.with_footer = structure.with_footer && head.is_none();
551    let sep_color = t.config.border_color;
552
553    let data = t.data;
554    let mut table = Builder::from_vec(data).build();
555
556    set_styles(&mut table, t.styles, &structure);
557    set_indent(&mut table, t.config.indent);
558    load_theme(&mut table, &t.config.theme, &structure, sep_color);
559    truncate_table(&mut table, &t.config, width, termwidth, t.heights);
560    table_set_border_header(&mut table, head, &t.config);
561
562    let string = table.to_string();
563    Some(string)
564}
565
566fn set_styles(table: &mut Table, styles: Styles, structure: &TableStructure) {
567    table.with(styles.cfg);
568    align_table(table, styles.alignments, structure);
569    colorize_table(table, styles.colors, structure);
570}
571
572fn table_set_border_header(table: &mut Table, head: Option<HeadInfo>, cfg: &TableConfig) {
573    let head = match head {
574        Some(head) => head,
575        None => return,
576    };
577
578    let theme = &cfg.theme;
579    let with_footer = cfg.structure.with_footer;
580
581    if !theme.as_base().borders_has_top() {
582        let line = theme.as_base().get_horizontal_line(1);
583        if let Some(line) = line.cloned() {
584            table.get_config_mut().insert_horizontal_line(0, line);
585            if with_footer {
586                let last_row = table.count_rows();
587                table
588                    .get_config_mut()
589                    .insert_horizontal_line(last_row, line);
590            }
591        };
592    }
593
594    // todo: Move logic to SetLineHeaders - so it be faster - cleaner
595    if with_footer {
596        let last_row = table.count_rows();
597        table.with(SetLineHeaders::new(head.clone(), last_row, cfg.indent));
598    }
599
600    table.with(SetLineHeaders::new(head, 0, cfg.indent));
601}
602
603fn truncate_table(
604    table: &mut Table,
605    cfg: &TableConfig,
606    width: WidthEstimation,
607    termwidth: usize,
608    heights: Vec<usize>,
609) {
610    let trim = cfg.trim.clone();
611    let pad = indent_sum(cfg.indent);
612    let ctrl = DimensionCtrl::new(termwidth, width, trim, cfg.expand, pad, heights);
613    table.with(ctrl);
614}
615
616fn indent_sum(indent: TableIndent) -> usize {
617    indent.left + indent.right
618}
619
620fn set_indent(table: &mut Table, indent: TableIndent) {
621    table.with(Padding::new(indent.left, indent.right, 0, 0));
622}
623
624struct DimensionCtrl {
625    width: WidthEstimation,
626    trim_strategy: TrimStrategy,
627    max_width: usize,
628    expand: bool,
629    pad: usize,
630    heights: Vec<usize>,
631}
632
633impl DimensionCtrl {
634    fn new(
635        max_width: usize,
636        width: WidthEstimation,
637        trim_strategy: TrimStrategy,
638        expand: bool,
639        pad: usize,
640        heights: Vec<usize>,
641    ) -> Self {
642        Self {
643            width,
644            trim_strategy,
645            max_width,
646            expand,
647            pad,
648            heights,
649        }
650    }
651}
652
653#[derive(Debug, Clone)]
654struct WidthEstimation {
655    original: Vec<usize>,
656    needed: Vec<usize>,
657    #[allow(dead_code)]
658    total: usize,
659    truncate: bool,
660    trail: bool,
661}
662
663impl WidthEstimation {
664    fn new(
665        original: Vec<usize>,
666        needed: Vec<usize>,
667        total: usize,
668        truncate: bool,
669        trail: bool,
670    ) -> Self {
671        Self {
672            original,
673            needed,
674            total,
675            truncate,
676            trail,
677        }
678    }
679}
680
681impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for DimensionCtrl {
682    fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
683        if self.width.truncate {
684            width_ctrl_truncate(self, recs, cfg, dims);
685            return;
686        }
687
688        if self.expand {
689            width_ctrl_expand(self, recs, cfg, dims);
690            return;
691        }
692
693        // NOTE: just an optimization; to not recalculate it internally
694        dims.set_heights(self.heights);
695        dims.set_widths(self.width.needed);
696    }
697
698    fn hint_change(&self) -> Option<Entity> {
699        // NOTE:
700        // Because we are assuming that:
701        // len(lines(wrapped(string))) >= len(lines(string))
702        //
703        // Only truncation case must be relaclucated in term of height.
704        if self.width.truncate && matches!(self.trim_strategy, TrimStrategy::Truncate { .. }) {
705            Some(Entity::Row(0))
706        } else {
707            None
708        }
709    }
710}
711
712fn width_ctrl_expand(
713    ctrl: DimensionCtrl,
714    recs: &mut NuRecords,
715    cfg: &mut ColoredConfig,
716    dims: &mut CompleteDimension,
717) {
718    dims.set_heights(ctrl.heights);
719    let opt = Width::increase(ctrl.max_width);
720    TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
721}
722
723fn width_ctrl_truncate(
724    ctrl: DimensionCtrl,
725    recs: &mut NuRecords,
726    cfg: &mut ColoredConfig,
727    dims: &mut CompleteDimension,
728) {
729    let mut heights = ctrl.heights;
730
731    // todo: maybe general for loop better
732    for (col, (&width, width_original)) in ctrl
733        .width
734        .needed
735        .iter()
736        .zip(ctrl.width.original)
737        .enumerate()
738    {
739        if width == width_original {
740            continue;
741        }
742
743        let width = width - ctrl.pad;
744
745        match &ctrl.trim_strategy {
746            TrimStrategy::Wrap { try_to_keep_words } => {
747                let wrap = Width::wrap(width).keep_words(*try_to_keep_words);
748
749                CellOption::<NuRecords, _>::change(wrap, recs, cfg, Entity::Column(col));
750
751                // NOTE: An optimization to have proper heights without going over all the data again.
752                // We are going only for all rows in changed columns
753                for (row, row_height) in heights.iter_mut().enumerate() {
754                    let height = recs.count_lines(Position::new(row, col));
755                    *row_height = max(*row_height, height);
756                }
757            }
758            TrimStrategy::Truncate { suffix } => {
759                let mut truncate = Width::truncate(width);
760                if let Some(suffix) = suffix {
761                    truncate = truncate.suffix(suffix).suffix_try_color(true);
762                }
763
764                CellOption::<NuRecords, _>::change(truncate, recs, cfg, Entity::Column(col));
765            }
766        }
767    }
768
769    dims.set_heights(heights);
770    dims.set_widths(ctrl.width.needed);
771}
772
773fn align_table(
774    table: &mut Table,
775    alignments: CellConfiguration<AlignmentHorizontal>,
776    structure: &TableStructure,
777) {
778    table.with(AlignmentStrategy::PerLine);
779
780    if structure.with_header {
781        table.modify(Rows::first(), AlignmentStrategy::PerCell);
782        table.modify(Rows::first(), Alignment::from(alignments.header));
783
784        if structure.with_footer {
785            table.modify(Rows::last(), AlignmentStrategy::PerCell);
786            table.modify(Rows::last(), Alignment::from(alignments.header));
787        }
788    }
789
790    if structure.with_index {
791        table.modify(Columns::first(), Alignment::from(alignments.index));
792    }
793}
794
795fn colorize_table(table: &mut Table, styles: CellConfiguration<Color>, structure: &TableStructure) {
796    if structure.with_index && !is_color_empty(&styles.index) {
797        table.modify(Columns::first(), styles.index);
798    }
799
800    if structure.with_header && !is_color_empty(&styles.header) {
801        table.modify(Rows::first(), styles.header.clone());
802    }
803
804    if structure.with_header && structure.with_footer && !is_color_empty(&styles.header) {
805        table.modify(Rows::last(), styles.header);
806    }
807}
808
809fn load_theme(
810    table: &mut Table,
811    theme: &TableTheme,
812    structure: &TableStructure,
813    sep_color: Option<Style>,
814) {
815    let with_header = table.count_rows() > 1 && structure.with_header;
816    let with_footer = with_header && structure.with_footer;
817    let mut theme = theme.as_base().clone();
818
819    if !with_header {
820        let borders = *theme.get_borders();
821        theme.remove_horizontal_lines();
822        theme.set_borders(borders);
823    } else if with_footer {
824        theme_copy_horizontal_line(&mut theme, 1, table.count_rows() - 1);
825    }
826
827    table.with(theme);
828
829    if let Some(style) = sep_color {
830        let color = convert_style(style);
831        let color = ANSIBuf::from(color);
832        table.get_config_mut().set_border_color_default(color);
833    }
834}
835
836fn maybe_truncate_columns(
837    data: &mut Vec<Vec<NuRecordsValue>>,
838    widths: Vec<usize>,
839    cfg: &TableConfig,
840    termwidth: usize,
841    truncate_by_head: bool,
842) -> WidthEstimation {
843    const TERMWIDTH_THRESHOLD: usize = 120;
844
845    let pad = cfg.indent.left + cfg.indent.right;
846    let preserve_content = termwidth > TERMWIDTH_THRESHOLD;
847
848    if truncate_by_head {
849        truncate_columns_by_head(data, widths, &cfg.theme, pad, termwidth)
850    } else if preserve_content {
851        truncate_columns_by_columns(data, widths, &cfg.theme, pad, termwidth)
852    } else {
853        truncate_columns_by_content(data, widths, &cfg.theme, pad, termwidth)
854    }
855}
856
857// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE.
858fn truncate_columns_by_content(
859    data: &mut Vec<Vec<NuRecordsValue>>,
860    widths: Vec<usize>,
861    theme: &TableTheme,
862    pad: usize,
863    termwidth: usize,
864) -> WidthEstimation {
865    const MIN_ACCEPTABLE_WIDTH: usize = 5;
866    const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
867
868    let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
869    let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
870
871    let count_columns = data[0].len();
872
873    let config = create_config(theme, false, None);
874    let widths_original = widths;
875    let mut widths = vec![];
876
877    let borders = config.get_borders();
878    let vertical = borders.has_vertical() as usize;
879
880    let mut width = borders.has_left() as usize + borders.has_right() as usize;
881    let mut truncate_pos = 0;
882
883    for (i, &column_width) in widths_original.iter().enumerate() {
884        let mut next_move = column_width;
885        if i > 0 {
886            next_move += vertical;
887        }
888        if width + next_move > termwidth {
889            break;
890        }
891        widths.push(column_width);
892        width += next_move;
893        truncate_pos += 1;
894    }
895
896    if truncate_pos == count_columns {
897        return WidthEstimation::new(widths_original, widths, width, false, false);
898    }
899
900    let is_last_column = truncate_pos + 1 == count_columns;
901    if truncate_pos == 0 && !is_last_column {
902        if termwidth > width {
903            let available = termwidth - width;
904            if available >= min_column_width + vertical + trailing_column_width {
905                truncate_rows(data, 1);
906
907                let first_col_width = available - (vertical + trailing_column_width);
908                widths.push(first_col_width);
909                width += first_col_width;
910
911                push_empty_column(data);
912                widths.push(trailing_column_width);
913                width += trailing_column_width + vertical;
914
915                return WidthEstimation::new(widths_original, widths, width, true, true);
916            }
917        }
918
919        return WidthEstimation::new(widths_original, widths, width, false, false);
920    }
921
922    let available = termwidth - width;
923
924    let can_fit_last_column = available >= min_column_width + vertical;
925    if is_last_column && can_fit_last_column {
926        let w = available - vertical;
927        widths.push(w);
928        width += w + vertical;
929
930        return WidthEstimation::new(widths_original, widths, width, true, false);
931    }
932
933    // special case where the last column is smaller then a trailing column
934    let is_almost_last_column = truncate_pos + 2 == count_columns;
935    if is_almost_last_column {
936        let next_column_width = widths_original[truncate_pos + 1];
937        let has_space_for_two_columns =
938            available >= min_column_width + vertical + next_column_width + vertical;
939
940        if !is_last_column && has_space_for_two_columns {
941            let rest = available - vertical - next_column_width - vertical;
942            widths.push(rest);
943            width += rest + vertical;
944
945            widths.push(next_column_width);
946            width += next_column_width + vertical;
947
948            return WidthEstimation::new(widths_original, widths, width, true, false);
949        }
950    }
951
952    let has_space_for_two_columns =
953        available >= min_column_width + vertical + trailing_column_width + vertical;
954    if !is_last_column && has_space_for_two_columns {
955        truncate_rows(data, truncate_pos + 1);
956
957        let rest = available - vertical - trailing_column_width - vertical;
958        widths.push(rest);
959        width += rest + vertical;
960
961        push_empty_column(data);
962        widths.push(trailing_column_width);
963        width += trailing_column_width + vertical;
964
965        return WidthEstimation::new(widths_original, widths, width, true, true);
966    }
967
968    if available >= trailing_column_width + vertical {
969        truncate_rows(data, truncate_pos);
970
971        push_empty_column(data);
972        widths.push(trailing_column_width);
973        width += trailing_column_width + vertical;
974
975        return WidthEstimation::new(widths_original, widths, width, false, true);
976    }
977
978    let last_width = widths.last().cloned().expect("ok");
979    let can_truncate_last = last_width > min_column_width;
980
981    if can_truncate_last {
982        let rest = last_width - min_column_width;
983        let maybe_available = available + rest;
984
985        if maybe_available >= trailing_column_width + vertical {
986            truncate_rows(data, truncate_pos);
987
988            let left = maybe_available - trailing_column_width - vertical;
989            let new_last_width = min_column_width + left;
990
991            widths[truncate_pos - 1] = new_last_width;
992            width -= last_width;
993            width += new_last_width;
994
995            push_empty_column(data);
996            widths.push(trailing_column_width);
997            width += trailing_column_width + vertical;
998
999            return WidthEstimation::new(widths_original, widths, width, true, true);
1000        }
1001    }
1002
1003    truncate_rows(data, truncate_pos - 1);
1004    let w = widths.pop().expect("ok");
1005    width -= w;
1006
1007    push_empty_column(data);
1008    widths.push(trailing_column_width);
1009    width += trailing_column_width;
1010
1011    let has_only_trail = widths.len() == 1;
1012    let is_enough_space = width <= termwidth;
1013    if has_only_trail || !is_enough_space {
1014        // nothing to show anyhow
1015        return WidthEstimation::new(widths_original, vec![], width, false, true);
1016    }
1017
1018    WidthEstimation::new(widths_original, widths, width, false, true)
1019}
1020
1021// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE
1022//
1023// TODO: Currently there's no prioritization of anything meaning all columns are equal
1024//       But I'd suggest to try to give a little more space for left most columns
1025//
1026//       So for example for instead of columns [10, 10, 10]
1027//       We would get [15, 10, 5]
1028//
1029//       Point being of the column needs more space we do can give it a little more based on it's distance from the start.
1030//       Percentage wise.
1031fn truncate_columns_by_columns(
1032    data: &mut Vec<Vec<NuRecordsValue>>,
1033    widths: Vec<usize>,
1034    theme: &TableTheme,
1035    pad: usize,
1036    termwidth: usize,
1037) -> WidthEstimation {
1038    const MIN_ACCEPTABLE_WIDTH: usize = 10;
1039    const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
1040
1041    let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
1042    let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
1043
1044    let count_columns = data[0].len();
1045
1046    let config = create_config(theme, false, None);
1047    let widths_original = widths;
1048    let mut widths = vec![];
1049
1050    let borders = config.get_borders();
1051    let vertical = borders.has_vertical() as usize;
1052
1053    let mut width = borders.has_left() as usize + borders.has_right() as usize;
1054    let mut truncate_pos = 0;
1055
1056    for (i, &width_orig) in widths_original.iter().enumerate() {
1057        let use_width = min(min_column_width, width_orig);
1058        let mut next_move = use_width;
1059        if i > 0 {
1060            next_move += vertical;
1061        }
1062
1063        if width + next_move > termwidth {
1064            break;
1065        }
1066
1067        widths.push(use_width);
1068        width += next_move;
1069        truncate_pos += 1;
1070    }
1071
1072    if truncate_pos == 0 {
1073        return WidthEstimation::new(widths_original, widths, width, false, false);
1074    }
1075
1076    let mut available = termwidth - width;
1077
1078    if available > 0 {
1079        for i in 0..truncate_pos {
1080            let used_width = widths[i];
1081            let col_width = widths_original[i];
1082            if used_width < col_width {
1083                let need = col_width - used_width;
1084                let take = min(available, need);
1085                available -= take;
1086
1087                widths[i] += take;
1088                width += take;
1089
1090                if available == 0 {
1091                    break;
1092                }
1093            }
1094        }
1095    }
1096
1097    if truncate_pos == count_columns {
1098        return WidthEstimation::new(widths_original, widths, width, true, false);
1099    }
1100
1101    if available >= trailing_column_width + vertical {
1102        truncate_rows(data, truncate_pos);
1103
1104        push_empty_column(data);
1105        widths.push(trailing_column_width);
1106        width += trailing_column_width + vertical;
1107
1108        return WidthEstimation::new(widths_original, widths, width, true, true);
1109    }
1110
1111    truncate_rows(data, truncate_pos - 1);
1112    let w = widths.pop().expect("ok");
1113    width -= w;
1114
1115    push_empty_column(data);
1116    widths.push(trailing_column_width);
1117    width += trailing_column_width;
1118
1119    WidthEstimation::new(widths_original, widths, width, true, true)
1120}
1121
1122// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE solely based on first column.
1123fn truncate_columns_by_head(
1124    data: &mut Vec<Vec<NuRecordsValue>>,
1125    widths: Vec<usize>,
1126    theme: &TableTheme,
1127    pad: usize,
1128    termwidth: usize,
1129) -> WidthEstimation {
1130    const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
1131
1132    let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
1133
1134    let count_columns = data[0].len();
1135
1136    let config = create_config(theme, false, None);
1137    let widths_original = widths;
1138    let mut widths = vec![];
1139
1140    let borders = config.get_borders();
1141    let vertical = borders.has_vertical() as usize;
1142
1143    let mut width = borders.has_left() as usize + borders.has_right() as usize;
1144    let mut truncate_pos = 0;
1145
1146    for (i, &column_width) in widths_original.iter().enumerate() {
1147        let head_width = NuRecordsValue::width(&data[0][i]) + pad;
1148        let vertical_width = if i > 0 { vertical } else { 0 };
1149
1150        let mut use_width = column_width;
1151        let mut next_move = use_width + vertical_width;
1152        if width + next_move > termwidth {
1153            use_width = head_width;
1154            next_move = use_width + vertical_width;
1155            if width + next_move > termwidth {
1156                break;
1157            }
1158        }
1159
1160        widths.push(use_width);
1161        width += next_move;
1162        truncate_pos += 1;
1163    }
1164
1165    if truncate_pos == 0 {
1166        return WidthEstimation::new(widths_original, widths, width, false, false);
1167    }
1168
1169    let mut available = termwidth - width;
1170
1171    if available > 0 {
1172        for i in 0..truncate_pos {
1173            let used_width = widths[i];
1174            let col_width = widths_original[i];
1175            if used_width < col_width {
1176                let need = col_width - used_width;
1177                let take = min(available, need);
1178                available -= take;
1179
1180                widths[i] += take;
1181                width += take;
1182
1183                if available == 0 {
1184                    break;
1185                }
1186            }
1187        }
1188    }
1189
1190    if truncate_pos == count_columns {
1191        return WidthEstimation::new(widths_original, widths, width, true, false);
1192    }
1193
1194    if available >= trailing_column_width + vertical {
1195        truncate_rows(data, truncate_pos);
1196
1197        push_empty_column(data);
1198        widths.push(trailing_column_width);
1199        width += trailing_column_width + vertical;
1200
1201        return WidthEstimation::new(widths_original, widths, width, true, true);
1202    }
1203
1204    // NOTE: we must check if some columns are bigger than head_width
1205    //       and cut width from them first.
1206    //       rather than removing last column.
1207    //
1208    //       We intentionally check only last column.
1209    //       Although space could be given from any column.
1210    let last_column_width = widths[truncate_pos - 1];
1211    let last_column_width_min = NuRecordsValue::width(&data[0][truncate_pos - 1]) + pad;
1212    let last_column_width_free = last_column_width - last_column_width_min;
1213    if available + last_column_width_free >= trailing_column_width + vertical {
1214        let use_width = trailing_column_width + vertical - available;
1215        widths[truncate_pos - 1] -= use_width;
1216        width -= use_width;
1217
1218        truncate_rows(data, truncate_pos);
1219
1220        push_empty_column(data);
1221        widths.push(trailing_column_width);
1222        width += trailing_column_width + vertical;
1223
1224        return WidthEstimation::new(widths_original, widths, width, true, true);
1225    }
1226
1227    truncate_rows(data, truncate_pos - 1);
1228    let w = widths.pop().expect("ok");
1229    width -= w;
1230
1231    push_empty_column(data);
1232    widths.push(trailing_column_width);
1233    width += trailing_column_width;
1234
1235    WidthEstimation::new(widths_original, widths, width, true, true)
1236}
1237
1238fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
1239    let total = widths.iter().sum::<usize>();
1240    let countv = cfg.count_vertical(widths.len());
1241    let margin = cfg.get_margin();
1242
1243    total + countv + margin.left.size + margin.right.size
1244}
1245
1246fn create_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> ColoredConfig {
1247    let structure = TableStructure::new(false, with_header, false);
1248    let mut table = Table::new([[""]]);
1249    load_theme(&mut table, theme, &structure, color);
1250    table.get_config().clone()
1251}
1252
1253fn push_empty_column(data: &mut Vec<Vec<NuRecordsValue>>) {
1254    let empty_cell = Text::new(String::from(EMPTY_COLUMN_TEXT));
1255    for row in data {
1256        row.push(empty_cell.clone());
1257    }
1258}
1259
1260fn duplicate_row(data: &mut Vec<Vec<NuRecordsValue>>, row: usize) {
1261    let duplicate = data[row].clone();
1262    data.push(duplicate);
1263}
1264
1265fn truncate_rows(data: &mut Vec<Vec<NuRecordsValue>>, count: usize) {
1266    for row in data {
1267        row.truncate(count);
1268    }
1269}
1270
1271fn convert_alignment(alignment: nu_color_config::Alignment) -> AlignmentHorizontal {
1272    match alignment {
1273        nu_color_config::Alignment::Center => AlignmentHorizontal::Center,
1274        nu_color_config::Alignment::Left => AlignmentHorizontal::Left,
1275        nu_color_config::Alignment::Right => AlignmentHorizontal::Right,
1276    }
1277}
1278
1279fn build_width(
1280    records: &[Vec<NuRecordsValue>],
1281    count_cols: usize,
1282    count_rows: usize,
1283    pad: usize,
1284) -> Vec<usize> {
1285    // TODO: Expose not spaned version (could be optimized).
1286    let mut cfg = SpannedConfig::default();
1287    cfg.set_padding(
1288        Entity::Global,
1289        Sides::new(
1290            Indent::spaced(pad),
1291            Indent::zero(),
1292            Indent::zero(),
1293            Indent::zero(),
1294        ),
1295    );
1296
1297    let records = IterRecords::new(records, count_cols, Some(count_rows));
1298
1299    PeekableGridDimension::width(records, &cfg)
1300}
1301
1302// It's laverages a use of guuaranted cached widths before hand
1303// to speed up things a bit.
1304struct SetLineHeaders {
1305    line: usize,
1306    pad: TableIndent,
1307    head: HeadInfo,
1308}
1309
1310impl SetLineHeaders {
1311    fn new(head: HeadInfo, line: usize, pad: TableIndent) -> Self {
1312        Self { line, head, pad }
1313    }
1314}
1315
1316impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for SetLineHeaders {
1317    fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
1318        let widths = match dims.get_widths() {
1319            Some(widths) => widths,
1320            None => {
1321                // we don't have widths cached; which means that NO width adjustments were done
1322                // which means we are OK to leave columns as they are.
1323                //
1324                // but we actually always have to have widths at this point
1325
1326                unreachable!("must never be the case");
1327            }
1328        };
1329
1330        let pad = self.pad.left + self.pad.right;
1331
1332        let columns = self
1333            .head
1334            .values
1335            .into_iter()
1336            .zip(widths.iter().cloned()) // it must be always safe to do
1337            .map(|(s, width)| Truncate::truncate(&s, width - pad).into_owned())
1338            .collect::<Vec<_>>();
1339
1340        // TODO: Isn't it too complicated interface for such a small feature?
1341        let mut names = ColumnNames::new(columns).line(self.line);
1342
1343        if let Some(color) = self.head.color {
1344            names = names.color(color);
1345        }
1346
1347        names = names.alignment(Alignment::from(self.head.align));
1348
1349        //  FIXME: because of bug in tabled(latest) we got to modify columns
1350        //         because it fails to regognize right padding value
1351        //  UNCOMMENT when fixed
1352
1353        // let alignment_head = Alignment::from(self.head.align);
1354        // let alignment_index = Alignment::from(self.head.align_index);
1355        // if self.head.align == self.head.align_index {
1356        //     names = names.alignment(alignment_head);
1357        // } else {
1358        //     let mut v = vec![alignment_head; widths.len()];
1359        //     v[0] = alignment_index;
1360        //     names = names.alignment(v);
1361        // }
1362
1363        names.change(recs, cfg, dims);
1364    }
1365
1366    fn hint_change(&self) -> Option<Entity> {
1367        None
1368    }
1369}
1370
1371fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) {
1372    if let Some(line) = theme.get_horizontal_line(from) {
1373        theme.insert_horizontal_line(to, *line);
1374    }
1375}
1376
1377pub fn get_color_if_exists(c: &Color) -> Option<Color> {
1378    if !is_color_empty(c) {
1379        Some(c.clone())
1380    } else {
1381        None
1382    }
1383}