1use 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#[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    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    pub fn count_rows(&self) -> usize {
89        self.count_rows
90    }
91
92    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        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    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    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    pub fn draw(self, termwidth: usize) -> Option<String> {
296        build_table(self, termwidth)
297    }
298
299    pub fn draw_unchecked(self, termwidth: usize) -> Option<String> {
303        build_table_unchecked(self, termwidth)
304    }
305
306    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
313impl 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 widths = maybe_truncate_columns(&mut t.data, t.widths.clone(), &t.config, termwidth);
465    if widths.needed.is_empty() {
466        return None;
467    }
468
469    if widths.trail {
471        let col = widths.needed.len() - 1;
472        for row in 0..t.count_rows {
473            t.styles
474                .cfg
475                .set_alignment_horizontal(Entity::Cell(row, col), t.styles.alignments.data);
476            t.styles
477                .cfg
478                .set_color(Entity::Cell(row, col), ANSIBuf::default());
479        }
480    }
481
482    Some(widths)
483}
484
485fn remove_header(t: &mut NuTable) -> HeadInfo {
486    for row in 1..t.data.len() {
488        for col in 0..t.count_cols {
489            let from = Position::new(row, col);
490            let to = Position::new(row - 1, col);
491
492            let alignment = *t.styles.cfg.get_alignment_horizontal(from);
493            if alignment != t.styles.alignments.data {
494                t.styles.cfg.set_alignment_horizontal(to.into(), alignment);
495            }
496
497            let color = t.styles.cfg.get_color(from);
498            if let Some(color) = color
499                && !color.is_empty()
500            {
501                let color = color.clone();
502                t.styles.cfg.set_color(to.into(), color);
503            }
504        }
505    }
506
507    let head = t
508        .data
509        .remove(0)
510        .into_iter()
511        .map(|s| s.to_string())
512        .collect();
513
514    t.heights.remove(0);
516
517    table_recalculate_widths(t);
521
522    let color = get_color_if_exists(&t.styles.colors.header);
523    let alignment = t.styles.alignments.header;
524    let alignment_index = if t.config.structure.with_index {
525        t.styles.alignments.index
526    } else {
527        t.styles.alignments.header
528    };
529
530    t.styles.alignments.header = AlignmentHorizontal::Center;
531    t.styles.colors.header = Color::empty();
532
533    HeadInfo::new(head, alignment, alignment_index, color)
534}
535
536fn draw_table(
537    t: NuTable,
538    width: WidthEstimation,
539    head: Option<HeadInfo>,
540    termwidth: usize,
541) -> Option<String> {
542    let mut structure = t.config.structure;
543    structure.with_footer = structure.with_footer && head.is_none();
544    let sep_color = t.config.border_color;
545
546    let data = t.data;
547    let mut table = Builder::from_vec(data).build();
548
549    set_styles(&mut table, t.styles, &structure);
550    set_indent(&mut table, t.config.indent);
551    load_theme(&mut table, &t.config.theme, &structure, sep_color);
552    truncate_table(&mut table, &t.config, width, termwidth, t.heights);
553    table_set_border_header(&mut table, head, &t.config);
554
555    let string = table.to_string();
556    Some(string)
557}
558
559fn set_styles(table: &mut Table, styles: Styles, structure: &TableStructure) {
560    table.with(styles.cfg);
561    align_table(table, styles.alignments, structure);
562    colorize_table(table, styles.colors, structure);
563}
564
565fn table_set_border_header(table: &mut Table, head: Option<HeadInfo>, cfg: &TableConfig) {
566    let head = match head {
567        Some(head) => head,
568        None => return,
569    };
570
571    let theme = &cfg.theme;
572    let with_footer = cfg.structure.with_footer;
573
574    if !theme.as_base().borders_has_top() {
575        let line = theme.as_base().get_horizontal_line(1);
576        if let Some(line) = line.cloned() {
577            table.get_config_mut().insert_horizontal_line(0, line);
578            if with_footer {
579                let last_row = table.count_rows();
580                table
581                    .get_config_mut()
582                    .insert_horizontal_line(last_row, line);
583            }
584        };
585    }
586
587    if with_footer {
589        let last_row = table.count_rows();
590        table.with(SetLineHeaders::new(head.clone(), last_row, cfg.indent));
591    }
592
593    table.with(SetLineHeaders::new(head, 0, cfg.indent));
594}
595
596fn truncate_table(
597    table: &mut Table,
598    cfg: &TableConfig,
599    width: WidthEstimation,
600    termwidth: usize,
601    heights: Vec<usize>,
602) {
603    let trim = cfg.trim.clone();
604    let pad = indent_sum(cfg.indent);
605    let ctrl = DimensionCtrl::new(termwidth, width, trim, cfg.expand, pad, heights);
606    table.with(ctrl);
607}
608
609fn indent_sum(indent: TableIndent) -> usize {
610    indent.left + indent.right
611}
612
613fn set_indent(table: &mut Table, indent: TableIndent) {
614    table.with(Padding::new(indent.left, indent.right, 0, 0));
615}
616
617struct DimensionCtrl {
618    width: WidthEstimation,
619    trim_strategy: TrimStrategy,
620    max_width: usize,
621    expand: bool,
622    pad: usize,
623    heights: Vec<usize>,
624}
625
626impl DimensionCtrl {
627    fn new(
628        max_width: usize,
629        width: WidthEstimation,
630        trim_strategy: TrimStrategy,
631        expand: bool,
632        pad: usize,
633        heights: Vec<usize>,
634    ) -> Self {
635        Self {
636            width,
637            trim_strategy,
638            max_width,
639            expand,
640            pad,
641            heights,
642        }
643    }
644}
645
646#[derive(Debug, Clone)]
647struct WidthEstimation {
648    original: Vec<usize>,
649    needed: Vec<usize>,
650    #[allow(dead_code)]
651    total: usize,
652    truncate: bool,
653    trail: bool,
654}
655
656impl WidthEstimation {
657    fn new(
658        original: Vec<usize>,
659        needed: Vec<usize>,
660        total: usize,
661        truncate: bool,
662        trail: bool,
663    ) -> Self {
664        Self {
665            original,
666            needed,
667            total,
668            truncate,
669            trail,
670        }
671    }
672}
673
674impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for DimensionCtrl {
675    fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
676        if self.width.truncate {
677            width_ctrl_truncate(self, recs, cfg, dims);
678            return;
679        }
680
681        if self.expand {
682            width_ctrl_expand(self, recs, cfg, dims);
683            return;
684        }
685
686        dims.set_heights(self.heights);
688        dims.set_widths(self.width.needed);
689    }
690
691    fn hint_change(&self) -> Option<Entity> {
692        if self.width.truncate && matches!(self.trim_strategy, TrimStrategy::Truncate { .. }) {
698            Some(Entity::Row(0))
699        } else {
700            None
701        }
702    }
703}
704
705fn width_ctrl_expand(
706    ctrl: DimensionCtrl,
707    recs: &mut NuRecords,
708    cfg: &mut ColoredConfig,
709    dims: &mut CompleteDimension,
710) {
711    dims.set_heights(ctrl.heights);
712    let opt = Width::increase(ctrl.max_width);
713    TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
714}
715
716fn width_ctrl_truncate(
717    ctrl: DimensionCtrl,
718    recs: &mut NuRecords,
719    cfg: &mut ColoredConfig,
720    dims: &mut CompleteDimension,
721) {
722    let mut heights = ctrl.heights;
723
724    for (col, (&width, width_original)) in ctrl
726        .width
727        .needed
728        .iter()
729        .zip(ctrl.width.original)
730        .enumerate()
731    {
732        if width == width_original {
733            continue;
734        }
735
736        let width = width - ctrl.pad;
737
738        match &ctrl.trim_strategy {
739            TrimStrategy::Wrap { try_to_keep_words } => {
740                let wrap = Width::wrap(width).keep_words(*try_to_keep_words);
741
742                CellOption::<NuRecords, _>::change(wrap, recs, cfg, Entity::Column(col));
743
744                for (row, row_height) in heights.iter_mut().enumerate() {
747                    let height = recs.count_lines(Position::new(row, col));
748                    *row_height = max(*row_height, height);
749                }
750            }
751            TrimStrategy::Truncate { suffix } => {
752                let mut truncate = Width::truncate(width);
753                if let Some(suffix) = suffix {
754                    truncate = truncate.suffix(suffix).suffix_try_color(true);
755                }
756
757                CellOption::<NuRecords, _>::change(truncate, recs, cfg, Entity::Column(col));
758            }
759        }
760    }
761
762    dims.set_heights(heights);
763    dims.set_widths(ctrl.width.needed);
764}
765
766fn align_table(
767    table: &mut Table,
768    alignments: CellConfiguration<AlignmentHorizontal>,
769    structure: &TableStructure,
770) {
771    table.with(AlignmentStrategy::PerLine);
772
773    if structure.with_header {
774        table.modify(Rows::first(), AlignmentStrategy::PerCell);
775        table.modify(Rows::first(), Alignment::from(alignments.header));
776
777        if structure.with_footer {
778            table.modify(Rows::last(), AlignmentStrategy::PerCell);
779            table.modify(Rows::last(), Alignment::from(alignments.header));
780        }
781    }
782
783    if structure.with_index {
784        table.modify(Columns::first(), Alignment::from(alignments.index));
785    }
786}
787
788fn colorize_table(table: &mut Table, styles: CellConfiguration<Color>, structure: &TableStructure) {
789    if structure.with_index && !is_color_empty(&styles.index) {
790        table.modify(Columns::first(), styles.index);
791    }
792
793    if structure.with_header && !is_color_empty(&styles.header) {
794        table.modify(Rows::first(), styles.header.clone());
795    }
796
797    if structure.with_header && structure.with_footer && !is_color_empty(&styles.header) {
798        table.modify(Rows::last(), styles.header);
799    }
800}
801
802fn load_theme(
803    table: &mut Table,
804    theme: &TableTheme,
805    structure: &TableStructure,
806    sep_color: Option<Style>,
807) {
808    let with_header = table.count_rows() > 1 && structure.with_header;
809    let with_footer = with_header && structure.with_footer;
810    let mut theme = theme.as_base().clone();
811
812    if !with_header {
813        let borders = *theme.get_borders();
814        theme.remove_horizontal_lines();
815        theme.set_borders(borders);
816    } else if with_footer {
817        theme_copy_horizontal_line(&mut theme, 1, table.count_rows() - 1);
818    }
819
820    table.with(theme);
821
822    if let Some(style) = sep_color {
823        let color = convert_style(style);
824        let color = ANSIBuf::from(color);
825        table.get_config_mut().set_border_color_default(color);
826    }
827}
828
829fn maybe_truncate_columns(
830    data: &mut Vec<Vec<NuRecordsValue>>,
831    widths: Vec<usize>,
832    cfg: &TableConfig,
833    termwidth: usize,
834) -> WidthEstimation {
835    const TERMWIDTH_THRESHOLD: usize = 120;
836
837    let pad = cfg.indent.left + cfg.indent.right;
838    let preserve_content = termwidth > TERMWIDTH_THRESHOLD;
839
840    if preserve_content {
841        truncate_columns_by_columns(data, widths, &cfg.theme, pad, termwidth)
842    } else {
843        truncate_columns_by_content(data, widths, &cfg.theme, pad, termwidth)
844    }
845}
846
847fn truncate_columns_by_content(
849    data: &mut Vec<Vec<NuRecordsValue>>,
850    widths: Vec<usize>,
851    theme: &TableTheme,
852    pad: usize,
853    termwidth: usize,
854) -> WidthEstimation {
855    const MIN_ACCEPTABLE_WIDTH: usize = 5;
856    const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
857
858    let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
859    let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
860
861    let count_columns = data[0].len();
862
863    let config = create_config(theme, false, None);
864    let widths_original = widths;
865    let mut widths = vec![];
866
867    let borders = config.get_borders();
868    let vertical = borders.has_vertical() as usize;
869
870    let mut width = borders.has_left() as usize + borders.has_right() as usize;
871    let mut truncate_pos = 0;
872
873    for (i, &column_width) in widths_original.iter().enumerate() {
874        let mut next_move = column_width;
875        if i > 0 {
876            next_move += vertical;
877        }
878
879        if width + next_move > termwidth {
880            break;
881        }
882
883        widths.push(column_width);
884        width += next_move;
885        truncate_pos += 1;
886    }
887
888    if truncate_pos == count_columns {
889        return WidthEstimation::new(widths_original, widths, width, false, false);
890    }
891
892    if truncate_pos == 0 {
893        if termwidth > width {
894            let available = termwidth - width;
895            if available >= min_column_width + vertical + trailing_column_width {
896                truncate_rows(data, 1);
897
898                let first_col_width = available - (vertical + trailing_column_width);
899                widths.push(first_col_width);
900                width += first_col_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        return WidthEstimation::new(widths_original, widths, width, false, false);
911    }
912
913    let available = termwidth - width;
914
915    let is_last_column = truncate_pos + 1 == count_columns;
916    let can_fit_last_column = available >= min_column_width + vertical;
917    if is_last_column && can_fit_last_column {
918        let w = available - vertical;
919        widths.push(w);
920        width += w + vertical;
921
922        return WidthEstimation::new(widths_original, widths, width, true, false);
923    }
924
925    let is_almost_last_column = truncate_pos + 2 == count_columns;
927    if is_almost_last_column {
928        let next_column_width = widths_original[truncate_pos + 1];
929        let has_space_for_two_columns =
930            available >= min_column_width + vertical + next_column_width + vertical;
931
932        if !is_last_column && has_space_for_two_columns {
933            let rest = available - vertical - next_column_width - vertical;
934            widths.push(rest);
935            width += rest + vertical;
936
937            widths.push(next_column_width);
938            width += next_column_width + vertical;
939
940            return WidthEstimation::new(widths_original, widths, width, true, false);
941        }
942    }
943
944    let has_space_for_two_columns =
945        available >= min_column_width + vertical + trailing_column_width + vertical;
946    if !is_last_column && has_space_for_two_columns {
947        truncate_rows(data, truncate_pos + 1);
948
949        let rest = available - vertical - trailing_column_width - vertical;
950        widths.push(rest);
951        width += rest + vertical;
952
953        push_empty_column(data);
954        widths.push(trailing_column_width);
955        width += trailing_column_width + vertical;
956
957        return WidthEstimation::new(widths_original, widths, width, true, true);
958    }
959
960    if available >= trailing_column_width + vertical {
961        truncate_rows(data, truncate_pos);
962
963        push_empty_column(data);
964        widths.push(trailing_column_width);
965        width += trailing_column_width + vertical;
966
967        return WidthEstimation::new(widths_original, widths, width, false, true);
968    }
969
970    let last_width = widths.last().cloned().expect("ok");
971    let can_truncate_last = last_width > min_column_width;
972
973    if can_truncate_last {
974        let rest = last_width - min_column_width;
975        let maybe_available = available + rest;
976
977        if maybe_available >= trailing_column_width + vertical {
978            truncate_rows(data, truncate_pos);
979
980            let left = maybe_available - trailing_column_width - vertical;
981            let new_last_width = min_column_width + left;
982
983            widths[truncate_pos - 1] = new_last_width;
984            width -= last_width;
985            width += new_last_width;
986
987            push_empty_column(data);
988            widths.push(trailing_column_width);
989            width += trailing_column_width + vertical;
990
991            return WidthEstimation::new(widths_original, widths, width, true, true);
992        }
993    }
994
995    truncate_rows(data, truncate_pos - 1);
996    let w = widths.pop().expect("ok");
997    width -= w;
998
999    push_empty_column(data);
1000    widths.push(trailing_column_width);
1001    width += trailing_column_width;
1002
1003    let has_only_trail = widths.len() == 1;
1004    let is_enough_space = width <= termwidth;
1005    if has_only_trail || !is_enough_space {
1006        return WidthEstimation::new(widths_original, vec![], width, false, true);
1008    }
1009
1010    WidthEstimation::new(widths_original, widths, width, false, true)
1011}
1012
1013fn truncate_columns_by_columns(
1024    data: &mut Vec<Vec<NuRecordsValue>>,
1025    widths: Vec<usize>,
1026    theme: &TableTheme,
1027    pad: usize,
1028    termwidth: usize,
1029) -> WidthEstimation {
1030    const MIN_ACCEPTABLE_WIDTH: usize = 10;
1031    const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
1032
1033    let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
1034    let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
1035
1036    let count_columns = data[0].len();
1037
1038    let config = create_config(theme, false, None);
1039    let widths_original = widths;
1040    let mut widths = vec![];
1041
1042    let borders = config.get_borders();
1043    let vertical = borders.has_vertical() as usize;
1044
1045    let mut width = borders.has_left() as usize + borders.has_right() as usize;
1046    let mut truncate_pos = 0;
1047
1048    for (i, &width_orig) in widths_original.iter().enumerate() {
1049        let use_width = min(min_column_width, width_orig);
1050        let mut next_move = use_width;
1051        if i > 0 {
1052            next_move += vertical;
1053        }
1054
1055        if width + next_move > termwidth {
1056            break;
1057        }
1058
1059        widths.push(use_width);
1060        width += next_move;
1061        truncate_pos += 1;
1062    }
1063
1064    if truncate_pos == 0 {
1065        return WidthEstimation::new(widths_original, widths, width, false, false);
1066    }
1067
1068    let mut available = termwidth - width;
1069
1070    if available > 0 {
1071        for i in 0..truncate_pos {
1072            let used_width = widths[i];
1073            let col_width = widths_original[i];
1074            if used_width < col_width {
1075                let need = col_width - used_width;
1076                let take = min(available, need);
1077                available -= take;
1078
1079                widths[i] += take;
1080                width += take;
1081
1082                if available == 0 {
1083                    break;
1084                }
1085            }
1086        }
1087    }
1088
1089    if truncate_pos == count_columns {
1090        return WidthEstimation::new(widths_original, widths, width, true, false);
1091    }
1092
1093    if available >= trailing_column_width + vertical {
1094        truncate_rows(data, truncate_pos);
1095
1096        push_empty_column(data);
1097        widths.push(trailing_column_width);
1098        width += trailing_column_width + vertical;
1099
1100        return WidthEstimation::new(widths_original, widths, width, true, true);
1101    }
1102
1103    truncate_rows(data, truncate_pos - 1);
1104    let w = widths.pop().expect("ok");
1105    width -= w;
1106
1107    push_empty_column(data);
1108    widths.push(trailing_column_width);
1109    width += trailing_column_width;
1110
1111    WidthEstimation::new(widths_original, widths, width, true, true)
1112}
1113
1114fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
1115    let total = widths.iter().sum::<usize>();
1116    let countv = cfg.count_vertical(widths.len());
1117    let margin = cfg.get_margin();
1118
1119    total + countv + margin.left.size + margin.right.size
1120}
1121
1122fn create_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> ColoredConfig {
1123    let structure = TableStructure::new(false, with_header, false);
1124    let mut table = Table::new([[""]]);
1125    load_theme(&mut table, theme, &structure, color);
1126    table.get_config().clone()
1127}
1128
1129fn push_empty_column(data: &mut Vec<Vec<NuRecordsValue>>) {
1130    let empty_cell = Text::new(String::from(EMPTY_COLUMN_TEXT));
1131    for row in data {
1132        row.push(empty_cell.clone());
1133    }
1134}
1135
1136fn duplicate_row(data: &mut Vec<Vec<NuRecordsValue>>, row: usize) {
1137    let duplicate = data[row].clone();
1138    data.push(duplicate);
1139}
1140
1141fn truncate_rows(data: &mut Vec<Vec<NuRecordsValue>>, count: usize) {
1142    for row in data {
1143        row.truncate(count);
1144    }
1145}
1146
1147fn convert_alignment(alignment: nu_color_config::Alignment) -> AlignmentHorizontal {
1148    match alignment {
1149        nu_color_config::Alignment::Center => AlignmentHorizontal::Center,
1150        nu_color_config::Alignment::Left => AlignmentHorizontal::Left,
1151        nu_color_config::Alignment::Right => AlignmentHorizontal::Right,
1152    }
1153}
1154
1155fn build_width(
1156    records: &[Vec<NuRecordsValue>],
1157    count_cols: usize,
1158    count_rows: usize,
1159    pad: usize,
1160) -> Vec<usize> {
1161    let mut cfg = SpannedConfig::default();
1163    cfg.set_padding(
1164        Entity::Global,
1165        Sides::new(
1166            Indent::spaced(pad),
1167            Indent::zero(),
1168            Indent::zero(),
1169            Indent::zero(),
1170        ),
1171    );
1172
1173    let records = IterRecords::new(records, count_cols, Some(count_rows));
1174
1175    PeekableGridDimension::width(records, &cfg)
1176}
1177
1178struct SetLineHeaders {
1181    line: usize,
1182    pad: TableIndent,
1183    head: HeadInfo,
1184}
1185
1186impl SetLineHeaders {
1187    fn new(head: HeadInfo, line: usize, pad: TableIndent) -> Self {
1188        Self { line, head, pad }
1189    }
1190}
1191
1192impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for SetLineHeaders {
1193    fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
1194        let widths = match dims.get_widths() {
1195            Some(widths) => widths,
1196            None => {
1197                unreachable!("must never be the case");
1203            }
1204        };
1205
1206        let pad = self.pad.left + self.pad.right;
1207
1208        let columns = self
1209            .head
1210            .values
1211            .into_iter()
1212            .zip(widths.iter().cloned()) .map(|(s, width)| Truncate::truncate(&s, width - pad).into_owned())
1214            .collect::<Vec<_>>();
1215
1216        let mut names = ColumnNames::new(columns).line(self.line);
1218
1219        if let Some(color) = self.head.color {
1220            names = names.color(color);
1221        }
1222
1223        names = names.alignment(Alignment::from(self.head.align));
1224
1225        names.change(recs, cfg, dims);
1240    }
1241
1242    fn hint_change(&self) -> Option<Entity> {
1243        None
1244    }
1245}
1246
1247fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) {
1248    if let Some(line) = theme.get_horizontal_line(from) {
1249        theme.insert_horizontal_line(to, *line);
1250    }
1251}
1252
1253pub fn get_color_if_exists(c: &Color) -> Option<Color> {
1254    if !is_color_empty(c) {
1255        Some(c.clone())
1256    } else {
1257        None
1258    }
1259}