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                width_priority_columns: vec![],
84            },
85        }
86    }
87
88    /// Return amount of rows.
89    pub fn count_rows(&self) -> usize {
90        self.count_rows
91    }
92
93    /// Return amount of columns.
94    pub fn count_columns(&self) -> usize {
95        self.count_cols
96    }
97
98    pub fn create(text: String) -> NuRecordsValue {
99        Text::new(text)
100    }
101
102    pub fn insert_value(&mut self, pos: (usize, usize), value: NuRecordsValue) {
103        let width = value.width() + indent_sum(self.config.indent);
104        let height = value.count_lines();
105        self.widths[pos.1] = max(self.widths[pos.1], width);
106        self.heights[pos.0] = max(self.heights[pos.0], height);
107        self.data[pos.0][pos.1] = value;
108    }
109
110    pub fn insert(&mut self, pos: (usize, usize), text: String) {
111        let text = Text::new(text);
112        let pad = indent_sum(self.config.indent);
113        let width = text.width() + pad;
114        let height = text.count_lines();
115        self.widths[pos.1] = max(self.widths[pos.1], width);
116        self.heights[pos.0] = max(self.heights[pos.0], height);
117        self.data[pos.0][pos.1] = text;
118    }
119
120    pub fn set_row(&mut self, index: usize, row: Vec<NuRecordsValue>) {
121        assert_eq!(self.data[index].len(), row.len());
122
123        for (i, text) in row.iter().enumerate() {
124            let pad = indent_sum(self.config.indent);
125            let width = text.width() + pad;
126            let height = text.count_lines();
127
128            self.widths[i] = max(self.widths[i], width);
129            self.heights[index] = max(self.heights[index], height);
130        }
131
132        self.data[index] = row;
133    }
134
135    pub fn pop_column(&mut self, count: usize) {
136        self.count_cols -= count;
137        self.widths.truncate(self.count_cols);
138
139        for (row, height) in self.data.iter_mut().zip(self.heights.iter_mut()) {
140            row.truncate(self.count_cols);
141
142            let row_height = *height;
143            let mut new_height = 0;
144            for cell in row.iter() {
145                let height = cell.count_lines();
146                if height == row_height {
147                    new_height = height;
148                    break;
149                }
150
151                new_height = max(new_height, height);
152            }
153
154            *height = new_height;
155        }
156
157        // set to default styles of the popped columns
158        for i in 0..count {
159            let col = self.count_cols + i;
160            for row in 0..self.count_rows {
161                self.styles
162                    .cfg
163                    .set_alignment_horizontal(Entity::Cell(row, col), self.styles.alignments.data);
164                self.styles
165                    .cfg
166                    .set_color(Entity::Cell(row, col), ANSIBuf::default());
167            }
168        }
169    }
170
171    pub fn push_column(&mut self, text: String) {
172        let value = Text::new(text);
173
174        let pad = indent_sum(self.config.indent);
175        let width = value.width() + pad;
176        let height = value.count_lines();
177        self.widths.push(width);
178
179        for row in 0..self.count_rows {
180            self.heights[row] = max(self.heights[row], height);
181        }
182
183        for row in &mut self.data[..] {
184            row.push(value.clone());
185        }
186
187        self.count_cols += 1;
188    }
189
190    pub fn insert_style(&mut self, pos: (usize, usize), style: TextStyle) {
191        if let Some(style) = style.color_style
192            && !style.is_plain()
193        {
194            let style = convert_style(style);
195            self.styles.cfg.set_color(pos.into(), style.into());
196        }
197
198        let alignment = convert_alignment(style.alignment);
199        if alignment != self.styles.alignments.data {
200            self.styles
201                .cfg
202                .set_alignment_horizontal(pos.into(), alignment);
203        }
204    }
205
206    pub fn set_header_style(&mut self, style: TextStyle) {
207        if let Some(style) = style.color_style
208            && !style.is_plain()
209        {
210            let style = convert_style(style);
211            self.styles.colors.header = style;
212        }
213
214        self.styles.alignments.header = convert_alignment(style.alignment);
215    }
216
217    pub fn set_index_style(&mut self, style: TextStyle) {
218        if let Some(style) = style.color_style
219            && !style.is_plain()
220        {
221            let style = convert_style(style);
222            self.styles.colors.index = style;
223        }
224
225        self.styles.alignments.index = convert_alignment(style.alignment);
226    }
227
228    pub fn set_data_style(&mut self, style: TextStyle) {
229        if let Some(style) = style.color_style
230            && !style.is_plain()
231        {
232            let style = convert_style(style);
233            self.styles.cfg.set_color(Entity::Global, style.into());
234        }
235
236        let alignment = convert_alignment(style.alignment);
237        self.styles
238            .cfg
239            .set_alignment_horizontal(Entity::Global, alignment);
240        self.styles.alignments.data = alignment;
241    }
242
243    // NOTE: Crusial to be called before data changes (todo fix interface)
244    pub fn set_indent(&mut self, indent: TableIndent) {
245        self.config.indent = indent;
246
247        let pad = indent_sum(indent);
248        for w in &mut self.widths {
249            *w = pad;
250        }
251    }
252
253    pub fn set_theme(&mut self, theme: TableTheme) {
254        self.config.theme = theme;
255    }
256
257    pub fn set_structure(&mut self, index: bool, header: bool, footer: bool) {
258        self.config.structure = TableStructure::new(index, header, footer);
259    }
260
261    pub fn set_border_header(&mut self, on: bool) {
262        self.config.header_on_border = on;
263    }
264
265    pub fn set_trim(&mut self, strategy: TrimStrategy) {
266        self.config.trim = strategy;
267    }
268
269    pub fn set_strategy(&mut self, expand: bool) {
270        self.config.expand = expand;
271    }
272
273    pub fn set_border_color(&mut self, color: Style) {
274        self.config.border_color = (!color.is_plain()).then_some(color);
275    }
276
277    pub fn set_width_priority_columns(&mut self, columns: &[usize]) {
278        self.config.width_priority_columns.clear();
279
280        for &column in columns {
281            if column < self.count_cols && !self.config.width_priority_columns.contains(&column) {
282                self.config.width_priority_columns.push(column);
283            }
284        }
285    }
286
287    pub fn clear_border_color(&mut self) {
288        self.config.border_color = None;
289    }
290
291    // NOTE: BE CAREFUL TO KEEP WIDTH UNCHANGED
292    // TODO: fix interface
293    pub fn get_records_mut(&mut self) -> &mut [Vec<NuRecordsValue>] {
294        &mut self.data
295    }
296
297    pub fn clear_all_colors(&mut self) {
298        self.clear_border_color();
299        let cfg = std::mem::take(&mut self.styles.cfg);
300        self.styles.cfg = ColoredConfig::new(cfg.into_inner());
301    }
302
303    /// Converts a table to a String.
304    ///
305    /// It returns None in case where table cannot be fit to a terminal width.
306    pub fn draw(self, termwidth: usize) -> Option<String> {
307        build_table(self, termwidth)
308    }
309
310    /// Converts a table to a String.
311    ///
312    /// It returns None in case where table cannot be fit to a terminal width.
313    pub fn draw_unchecked(self, termwidth: usize) -> Option<String> {
314        build_table_unchecked(self, termwidth)
315    }
316
317    /// Return a total table width.
318    pub fn total_width(&self) -> usize {
319        let config = create_config(&self.config.theme, false, None);
320        get_total_width2(&self.widths, &config)
321    }
322}
323
324// NOTE: Must never be called from nu-table - made only for tests
325// FIXME: remove it?
326// #[cfg(test)]
327impl From<Vec<Vec<Text<String>>>> for NuTable {
328    fn from(value: Vec<Vec<Text<String>>>) -> Self {
329        let count_rows = value.len();
330        let count_cols = if value.is_empty() { 0 } else { value[0].len() };
331
332        let mut t = Self::new(count_rows, count_cols);
333        for (i, row) in value.into_iter().enumerate() {
334            t.set_row(i, row);
335        }
336
337        table_recalculate_widths(&mut t);
338
339        t
340    }
341}
342
343fn table_recalculate_widths(t: &mut NuTable) {
344    let pad = indent_sum(t.config.indent);
345    t.widths = build_width(&t.data, t.count_cols, t.count_rows, pad);
346}
347
348#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Hash)]
349struct CellConfiguration<Value> {
350    index: Value,
351    header: Value,
352    data: Value,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
356struct Styles {
357    cfg: ColoredConfig,
358    colors: CellConfiguration<Color>,
359    alignments: CellConfiguration<AlignmentHorizontal>,
360}
361
362#[derive(Debug, Clone)]
363pub struct TableConfig {
364    theme: TableTheme,
365    trim: TrimStrategy,
366    border_color: Option<Style>,
367    expand: bool,
368    structure: TableStructure,
369    header_on_border: bool,
370    indent: TableIndent,
371    width_priority_columns: Vec<usize>,
372}
373
374#[derive(Debug, Clone, Copy)]
375struct TableStructure {
376    with_index: bool,
377    with_header: bool,
378    with_footer: bool,
379}
380
381impl TableStructure {
382    fn new(with_index: bool, with_header: bool, with_footer: bool) -> Self {
383        Self {
384            with_index,
385            with_header,
386            with_footer,
387        }
388    }
389}
390
391#[derive(Debug, Clone)]
392struct HeadInfo {
393    values: Vec<String>,
394    align: AlignmentHorizontal,
395    #[allow(dead_code)]
396    align_index: AlignmentHorizontal,
397    color: Option<Color>,
398}
399
400impl HeadInfo {
401    fn new(
402        values: Vec<String>,
403        align: AlignmentHorizontal,
404        align_index: AlignmentHorizontal,
405        color: Option<Color>,
406    ) -> Self {
407        Self {
408            values,
409            align,
410            align_index,
411            color,
412        }
413    }
414}
415
416fn build_table_unchecked(mut t: NuTable, termwidth: usize) -> Option<String> {
417    if t.count_columns() == 0 || t.count_rows() == 0 {
418        return Some(String::new());
419    }
420
421    let widths = std::mem::take(&mut t.widths);
422    let config = create_config(&t.config.theme, false, None);
423    let totalwidth = get_total_width2(&t.widths, &config);
424    let widths = WidthEstimation::new(widths.clone(), widths, totalwidth, false, false);
425
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 build_table(mut t: NuTable, termwidth: usize) -> Option<String> {
433    if t.count_columns() == 0 || t.count_rows() == 0 {
434        return Some(String::new());
435    }
436
437    let widths = table_truncate(&mut t, termwidth)?;
438    let head = remove_header_if(&mut t);
439    table_insert_footer_if(&mut t);
440
441    draw_table(t, widths, head, termwidth)
442}
443
444fn remove_header_if(t: &mut NuTable) -> Option<HeadInfo> {
445    if !is_header_on_border(t) {
446        return None;
447    }
448
449    let head = remove_header(t);
450    t.config.structure.with_header = false;
451
452    Some(head)
453}
454
455fn is_header_on_border(t: &NuTable) -> bool {
456    let is_configured = t.config.structure.with_header && t.config.header_on_border;
457    let has_horizontal = t.config.theme.as_base().borders_has_top()
458        || t.config.theme.as_base().get_horizontal_line(1).is_some();
459    is_configured && has_horizontal
460}
461
462fn table_insert_footer_if(t: &mut NuTable) {
463    let with_footer = t.config.structure.with_header && t.config.structure.with_footer;
464    if !with_footer {
465        return;
466    }
467
468    duplicate_row(&mut t.data, 0);
469
470    if !t.heights.is_empty() {
471        t.heights.push(t.heights[0]);
472    }
473}
474
475fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option<WidthEstimation> {
476    // Header-on-border mode normally truncates by header width, but that strategy
477    // can starve explicit width priorities. If priorities are provided, prefer
478    // the column-based strategy so priority columns can be widened first.
479    let truncate_by_head = is_header_on_border(t) && t.config.width_priority_columns.is_empty();
480    let widths = maybe_truncate_columns(
481        &mut t.data,
482        t.widths.clone(),
483        &t.config,
484        termwidth,
485        truncate_by_head,
486    );
487    if widths.needed.is_empty() {
488        return None;
489    }
490
491    // reset style for last column which is a trail one
492    if widths.trail {
493        let col = widths.needed.len() - 1;
494        for row in 0..t.count_rows {
495            t.styles
496                .cfg
497                .set_alignment_horizontal(Entity::Cell(row, col), t.styles.alignments.data);
498            t.styles
499                .cfg
500                .set_color(Entity::Cell(row, col), ANSIBuf::default());
501        }
502    }
503
504    Some(widths)
505}
506
507fn remove_header(t: &mut NuTable) -> HeadInfo {
508    // move settings by one row down
509    for row in 1..t.data.len() {
510        for col in 0..t.count_cols {
511            let from = Position::new(row, col);
512            let to = Position::new(row - 1, col);
513
514            let alignment = *t.styles.cfg.get_alignment_horizontal(from);
515            t.styles.cfg.set_alignment_horizontal(to.into(), alignment);
516
517            let color = t.styles.cfg.get_color(from);
518            if let Some(color) = color
519                && !color.is_empty()
520            {
521                let color = color.clone();
522                t.styles.cfg.set_color(to.into(), color);
523            }
524        }
525    }
526
527    let head = t
528        .data
529        .remove(0)
530        .into_iter()
531        .map(|s| s.to_string())
532        .collect();
533
534    // drop height row
535    t.heights.remove(0);
536
537    // WE NEED TO RELCULATE WIDTH.
538    // TODO: cause we have configuration beforehand we can just not calculate it in?
539    // Why we do it exactly??
540    table_recalculate_widths(t);
541
542    let color = get_color_if_exists(&t.styles.colors.header);
543    let alignment = t.styles.alignments.header;
544    let alignment_index = if t.config.structure.with_index {
545        t.styles.alignments.index
546    } else {
547        t.styles.alignments.header
548    };
549
550    t.styles.alignments.header = AlignmentHorizontal::Center;
551    t.styles.colors.header = Color::empty();
552
553    HeadInfo::new(head, alignment, alignment_index, color)
554}
555
556fn draw_table(
557    t: NuTable,
558    width: WidthEstimation,
559    head: Option<HeadInfo>,
560    termwidth: usize,
561) -> Option<String> {
562    let mut structure = t.config.structure;
563    structure.with_footer = structure.with_footer && head.is_none();
564    let sep_color = t.config.border_color;
565
566    let data = t.data;
567    let mut table = Builder::from_vec(data).build();
568
569    set_styles(&mut table, t.styles, &structure);
570    set_indent(&mut table, t.config.indent);
571    load_theme(&mut table, &t.config.theme, &structure, sep_color);
572    truncate_table(&mut table, &t.config, width, termwidth, t.heights);
573    table_set_border_header(&mut table, head, &t.config);
574
575    let string = table.to_string();
576    Some(string)
577}
578
579fn set_styles(table: &mut Table, styles: Styles, structure: &TableStructure) {
580    table.with(styles.cfg);
581    align_table(table, styles.alignments, structure);
582    colorize_table(table, styles.colors, structure);
583}
584
585fn table_set_border_header(table: &mut Table, head: Option<HeadInfo>, cfg: &TableConfig) {
586    let head = match head {
587        Some(head) => head,
588        None => return,
589    };
590
591    let theme = &cfg.theme;
592    let with_footer = cfg.structure.with_footer;
593
594    if !theme.as_base().borders_has_top() {
595        let line = theme.as_base().get_horizontal_line(1);
596        if let Some(line) = line.cloned() {
597            table.get_config_mut().insert_horizontal_line(0, line);
598            if with_footer {
599                let last_row = table.count_rows();
600                table
601                    .get_config_mut()
602                    .insert_horizontal_line(last_row, line);
603            }
604        };
605    }
606
607    // todo: Move logic to SetLineHeaders - so it be faster - cleaner
608    if with_footer {
609        let last_row = table.count_rows();
610        table.with(SetLineHeaders::new(head.clone(), last_row, cfg.indent));
611    }
612
613    table.with(SetLineHeaders::new(head, 0, cfg.indent));
614}
615
616fn truncate_table(
617    table: &mut Table,
618    cfg: &TableConfig,
619    width: WidthEstimation,
620    termwidth: usize,
621    heights: Vec<usize>,
622) {
623    let trim = cfg.trim.clone();
624    let pad = indent_sum(cfg.indent);
625    let ctrl = DimensionCtrl::new(termwidth, width, trim, cfg.expand, pad, heights);
626    table.with(ctrl);
627}
628
629fn indent_sum(indent: TableIndent) -> usize {
630    indent.left + indent.right
631}
632
633fn set_indent(table: &mut Table, indent: TableIndent) {
634    table.with(Padding::new(indent.left, indent.right, 0, 0));
635}
636
637struct DimensionCtrl {
638    width: WidthEstimation,
639    trim_strategy: TrimStrategy,
640    max_width: usize,
641    expand: bool,
642    pad: usize,
643    heights: Vec<usize>,
644}
645
646impl DimensionCtrl {
647    fn new(
648        max_width: usize,
649        width: WidthEstimation,
650        trim_strategy: TrimStrategy,
651        expand: bool,
652        pad: usize,
653        heights: Vec<usize>,
654    ) -> Self {
655        Self {
656            width,
657            trim_strategy,
658            max_width,
659            expand,
660            pad,
661            heights,
662        }
663    }
664}
665
666#[derive(Debug, Clone)]
667struct WidthEstimation {
668    original: Vec<usize>,
669    needed: Vec<usize>,
670    #[allow(dead_code)]
671    total: usize,
672    truncate: bool,
673    trail: bool,
674}
675
676impl WidthEstimation {
677    fn new(
678        original: Vec<usize>,
679        needed: Vec<usize>,
680        total: usize,
681        truncate: bool,
682        trail: bool,
683    ) -> Self {
684        Self {
685            original,
686            needed,
687            total,
688            truncate,
689            trail,
690        }
691    }
692}
693
694impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for DimensionCtrl {
695    fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
696        if self.width.truncate {
697            width_ctrl_truncate(self, recs, cfg, dims);
698            return;
699        }
700
701        if self.expand {
702            width_ctrl_expand(self, recs, cfg, dims);
703            return;
704        }
705
706        // NOTE: just an optimization; to not recalculate it internally
707        dims.set_heights(self.heights);
708        dims.set_widths(self.width.needed);
709    }
710
711    fn hint_change(&self) -> Option<Entity> {
712        // NOTE:
713        // Because we are assuming that:
714        // len(lines(wrapped(string))) >= len(lines(string))
715        //
716        // Only truncation case must be relaclucated in term of height.
717        if self.width.truncate && matches!(self.trim_strategy, TrimStrategy::Truncate { .. }) {
718            Some(Entity::Row(0))
719        } else {
720            None
721        }
722    }
723}
724
725fn width_ctrl_expand(
726    ctrl: DimensionCtrl,
727    recs: &mut NuRecords,
728    cfg: &mut ColoredConfig,
729    dims: &mut CompleteDimension,
730) {
731    dims.set_heights(ctrl.heights);
732    let opt = Width::increase(ctrl.max_width);
733    TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
734}
735
736fn width_ctrl_truncate(
737    ctrl: DimensionCtrl,
738    recs: &mut NuRecords,
739    cfg: &mut ColoredConfig,
740    dims: &mut CompleteDimension,
741) {
742    let mut heights = ctrl.heights;
743
744    // todo: maybe general for loop better
745    for (col, (&width, width_original)) in ctrl
746        .width
747        .needed
748        .iter()
749        .zip(ctrl.width.original)
750        .enumerate()
751    {
752        if width == width_original {
753            continue;
754        }
755
756        let width = width - ctrl.pad;
757
758        match &ctrl.trim_strategy {
759            TrimStrategy::Wrap { try_to_keep_words } => {
760                let wrap = Width::wrap(width).keep_words(*try_to_keep_words);
761
762                CellOption::<NuRecords, _>::change(wrap, recs, cfg, Entity::Column(col));
763
764                // NOTE: An optimization to have proper heights without going over all the data again.
765                // We are going only for all rows in changed columns
766                for (row, row_height) in heights.iter_mut().enumerate() {
767                    let height = recs.count_lines(Position::new(row, col));
768                    *row_height = max(*row_height, height);
769                }
770            }
771            TrimStrategy::Truncate { suffix } => {
772                let mut truncate = Width::truncate(width);
773                if let Some(suffix) = suffix {
774                    truncate = truncate.suffix(suffix).suffix_try_color(true);
775                }
776
777                CellOption::<NuRecords, _>::change(truncate, recs, cfg, Entity::Column(col));
778            }
779        }
780    }
781
782    dims.set_heights(heights);
783    dims.set_widths(ctrl.width.needed);
784}
785
786fn align_table(
787    table: &mut Table,
788    alignments: CellConfiguration<AlignmentHorizontal>,
789    structure: &TableStructure,
790) {
791    table.with(AlignmentStrategy::PerLine);
792
793    if structure.with_header {
794        table.modify(Rows::first(), AlignmentStrategy::PerCell);
795        table.modify(Rows::first(), Alignment::from(alignments.header));
796
797        if structure.with_footer {
798            table.modify(Rows::last(), AlignmentStrategy::PerCell);
799            table.modify(Rows::last(), Alignment::from(alignments.header));
800        }
801    }
802
803    if structure.with_index {
804        table.modify(Columns::first(), Alignment::from(alignments.index));
805    }
806}
807
808fn colorize_table(table: &mut Table, styles: CellConfiguration<Color>, structure: &TableStructure) {
809    if structure.with_index && !is_color_empty(&styles.index) {
810        table.modify(Columns::first(), styles.index);
811    }
812
813    if structure.with_header && !is_color_empty(&styles.header) {
814        table.modify(Rows::first(), styles.header.clone());
815    }
816
817    if structure.with_header && structure.with_footer && !is_color_empty(&styles.header) {
818        table.modify(Rows::last(), styles.header);
819    }
820}
821
822fn load_theme(
823    table: &mut Table,
824    theme: &TableTheme,
825    structure: &TableStructure,
826    sep_color: Option<Style>,
827) {
828    let with_header = table.count_rows() > 1 && structure.with_header;
829    let with_footer = with_header && structure.with_footer;
830    let mut theme = theme.as_base().clone();
831
832    if !with_header {
833        let borders = *theme.get_borders();
834        theme.remove_horizontal_lines();
835        theme.set_borders(borders);
836    } else if with_footer {
837        theme_copy_horizontal_line(&mut theme, 1, table.count_rows() - 1);
838    }
839
840    table.with(theme);
841
842    if let Some(style) = sep_color {
843        let color = convert_style(style);
844        let color = ANSIBuf::from(color);
845        table.get_config_mut().set_border_color_default(color);
846    }
847}
848
849fn maybe_truncate_columns(
850    data: &mut Vec<Vec<NuRecordsValue>>,
851    widths: Vec<usize>,
852    cfg: &TableConfig,
853    termwidth: usize,
854    truncate_by_head: bool,
855) -> WidthEstimation {
856    const TERMWIDTH_THRESHOLD: usize = 120;
857
858    let pad = cfg.indent.left + cfg.indent.right;
859    let preserve_content = termwidth > TERMWIDTH_THRESHOLD;
860
861    if truncate_by_head {
862        truncate_columns_by_head(
863            data,
864            widths,
865            &cfg.theme,
866            pad,
867            termwidth,
868            &cfg.width_priority_columns,
869        )
870    } else if preserve_content {
871        truncate_columns_by_columns(
872            data,
873            widths,
874            &cfg.theme,
875            pad,
876            termwidth,
877            &cfg.width_priority_columns,
878        )
879    } else {
880        truncate_columns_by_content(data, widths, &cfg.theme, pad, termwidth)
881    }
882}
883
884// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE.
885fn truncate_columns_by_content(
886    data: &mut Vec<Vec<NuRecordsValue>>,
887    widths: Vec<usize>,
888    theme: &TableTheme,
889    pad: usize,
890    termwidth: usize,
891) -> WidthEstimation {
892    const MIN_ACCEPTABLE_WIDTH: usize = 5;
893    const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
894
895    let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
896    let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
897
898    let count_columns = data[0].len();
899
900    let config = create_config(theme, false, None);
901    let widths_original = widths;
902    let mut widths = vec![];
903
904    let borders = config.get_borders();
905    let vertical = borders.has_vertical() as usize;
906
907    let mut width = borders.has_left() as usize + borders.has_right() as usize;
908    let mut truncate_pos = 0;
909
910    for (i, &column_width) in widths_original.iter().enumerate() {
911        let mut next_move = column_width;
912        if i > 0 {
913            next_move += vertical;
914        }
915        if width + next_move > termwidth {
916            break;
917        }
918        widths.push(column_width);
919        width += next_move;
920        truncate_pos += 1;
921    }
922
923    if truncate_pos == count_columns {
924        return WidthEstimation::new(widths_original, widths, width, false, false);
925    }
926
927    let is_last_column = truncate_pos + 1 == count_columns;
928    if truncate_pos == 0 && !is_last_column {
929        if termwidth > width {
930            let available = termwidth - width;
931            if available >= min_column_width + vertical + trailing_column_width {
932                truncate_rows(data, 1);
933
934                let first_col_width = available - (vertical + trailing_column_width);
935                widths.push(first_col_width);
936                width += first_col_width;
937
938                push_empty_column(data);
939                widths.push(trailing_column_width);
940                width += trailing_column_width + vertical;
941
942                return WidthEstimation::new(widths_original, widths, width, true, true);
943            }
944        }
945
946        return WidthEstimation::new(widths_original, widths, width, false, false);
947    }
948
949    let available = termwidth - width;
950
951    let can_fit_last_column = available >= min_column_width + vertical;
952    if is_last_column && can_fit_last_column {
953        let w = available - vertical;
954        widths.push(w);
955        width += w + vertical;
956
957        return WidthEstimation::new(widths_original, widths, width, true, false);
958    }
959
960    // special case where the last column is smaller then a trailing column
961    let is_almost_last_column = truncate_pos + 2 == count_columns;
962    if is_almost_last_column {
963        let next_column_width = widths_original[truncate_pos + 1];
964        let has_space_for_two_columns =
965            available >= min_column_width + vertical + next_column_width + vertical;
966
967        if !is_last_column && has_space_for_two_columns {
968            let rest = available - vertical - next_column_width - vertical;
969            widths.push(rest);
970            width += rest + vertical;
971
972            widths.push(next_column_width);
973            width += next_column_width + vertical;
974
975            return WidthEstimation::new(widths_original, widths, width, true, false);
976        }
977    }
978
979    let has_space_for_two_columns =
980        available >= min_column_width + vertical + trailing_column_width + vertical;
981    if !is_last_column && has_space_for_two_columns {
982        truncate_rows(data, truncate_pos + 1);
983
984        let rest = available - vertical - trailing_column_width - vertical;
985        widths.push(rest);
986        width += rest + vertical;
987
988        push_empty_column(data);
989        widths.push(trailing_column_width);
990        width += trailing_column_width + vertical;
991
992        return WidthEstimation::new(widths_original, widths, width, true, true);
993    }
994
995    if available >= trailing_column_width + vertical {
996        truncate_rows(data, truncate_pos);
997
998        push_empty_column(data);
999        widths.push(trailing_column_width);
1000        width += trailing_column_width + vertical;
1001
1002        return WidthEstimation::new(widths_original, widths, width, false, true);
1003    }
1004
1005    let last_width = widths.last().cloned().expect("ok");
1006    let can_truncate_last = last_width > min_column_width;
1007
1008    if can_truncate_last {
1009        let rest = last_width - min_column_width;
1010        let maybe_available = available + rest;
1011
1012        if maybe_available >= trailing_column_width + vertical {
1013            truncate_rows(data, truncate_pos);
1014
1015            let left = maybe_available - trailing_column_width - vertical;
1016            let new_last_width = min_column_width + left;
1017
1018            widths[truncate_pos - 1] = new_last_width;
1019            width -= last_width;
1020            width += new_last_width;
1021
1022            push_empty_column(data);
1023            widths.push(trailing_column_width);
1024            width += trailing_column_width + vertical;
1025
1026            return WidthEstimation::new(widths_original, widths, width, true, true);
1027        }
1028    }
1029
1030    truncate_rows(data, truncate_pos - 1);
1031    let w = widths.pop().expect("ok");
1032    width -= w;
1033
1034    push_empty_column(data);
1035    widths.push(trailing_column_width);
1036    width += trailing_column_width;
1037
1038    let has_only_trail = widths.len() == 1;
1039    let is_enough_space = width <= termwidth;
1040    if has_only_trail || !is_enough_space {
1041        // nothing to show anyhow
1042        return WidthEstimation::new(widths_original, vec![], width, false, true);
1043    }
1044
1045    WidthEstimation::new(widths_original, widths, width, false, true)
1046}
1047
1048// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE
1049//
1050// TODO: Currently there's no prioritization of anything meaning all columns are equal
1051//       But I'd suggest to try to give a little more space for left most columns
1052//
1053//       So for example for instead of columns [10, 10, 10]
1054//       We would get [15, 10, 5]
1055//
1056//       Point being of the column needs more space we do can give it a little more based on it's distance from the start.
1057//       Percentage wise.
1058fn truncate_columns_by_columns(
1059    data: &mut Vec<Vec<NuRecordsValue>>,
1060    widths: Vec<usize>,
1061    theme: &TableTheme,
1062    pad: usize,
1063    termwidth: usize,
1064    width_priority_columns: &[usize],
1065) -> WidthEstimation {
1066    const MIN_ACCEPTABLE_WIDTH: usize = 10;
1067    const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
1068    const SECONDARY_PRIORITY_BONUS_LIMIT: usize = 6;
1069
1070    let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
1071    let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
1072
1073    let count_columns = data[0].len();
1074
1075    let config = create_config(theme, false, None);
1076    let widths_original = widths;
1077    let mut widths = vec![];
1078
1079    let borders = config.get_borders();
1080    let vertical = borders.has_vertical() as usize;
1081
1082    let mut width = borders.has_left() as usize + borders.has_right() as usize;
1083    let mut truncate_pos = 0;
1084
1085    for (i, &width_orig) in widths_original.iter().enumerate() {
1086        let use_width = min(min_column_width, width_orig);
1087        let mut next_move = use_width;
1088        if i > 0 {
1089            next_move += vertical;
1090        }
1091
1092        if width + next_move > termwidth {
1093            break;
1094        }
1095
1096        widths.push(use_width);
1097        width += next_move;
1098        truncate_pos += 1;
1099    }
1100
1101    if truncate_pos == 0 {
1102        return WidthEstimation::new(widths_original, widths, width, false, false);
1103    }
1104
1105    let mut available = termwidth - width;
1106
1107    if available > 0 {
1108        let consumed = distribute_available_width(
1109            &mut widths[..truncate_pos],
1110            &widths_original[..truncate_pos],
1111            available,
1112            width_priority_columns,
1113        );
1114        available -= consumed;
1115        width += consumed;
1116    }
1117
1118    // If not all columns fit and the primary priority is on the right side,
1119    // compact columns to the right of it so the priority column can dominate.
1120    if truncate_pos < count_columns {
1121        let mut state = PriorityCompactionState {
1122            widths: &mut widths,
1123            truncate_pos: &mut truncate_pos,
1124            width: &mut width,
1125        };
1126        let compaction_data = PriorityCompactionData {
1127            widths_original: &widths_original,
1128            width_priority_columns,
1129        };
1130        let limits = PriorityCompactionLimits {
1131            termwidth,
1132            trailing_column_width,
1133            vertical,
1134            secondary_priority_bonus_limit: SECONDARY_PRIORITY_BONUS_LIMIT,
1135        };
1136        compact_partial_visibility_for_priority(&mut state, &compaction_data, &limits);
1137
1138        available = termwidth - width;
1139    }
1140
1141    if truncate_pos == count_columns {
1142        let mut state = PriorityCompactionState {
1143            widths: &mut widths,
1144            truncate_pos: &mut truncate_pos,
1145            width: &mut width,
1146        };
1147        let compaction_data = PriorityCompactionData {
1148            widths_original: &widths_original,
1149            width_priority_columns,
1150        };
1151        let limits = PriorityCompactionLimits {
1152            termwidth,
1153            trailing_column_width,
1154            vertical,
1155            secondary_priority_bonus_limit: SECONDARY_PRIORITY_BONUS_LIMIT,
1156        };
1157        let should_add_trailing =
1158            compact_full_visibility_for_priority(&mut state, &compaction_data, &limits);
1159        if should_add_trailing {
1160            truncate_rows(data, truncate_pos);
1161
1162            push_empty_column(data);
1163            widths.push(trailing_column_width);
1164            width += trailing_column_width + vertical;
1165
1166            return WidthEstimation::new(widths_original, widths, width, true, true);
1167        }
1168
1169        return WidthEstimation::new(widths_original, widths, width, true, false);
1170    }
1171
1172    if available >= trailing_column_width + vertical {
1173        let extra_budget = available - (trailing_column_width + vertical);
1174        let applied = apply_extra_budget_to_visible_columns(
1175            &mut widths,
1176            extra_budget,
1177            width_priority_columns,
1178            truncate_pos,
1179        );
1180        width += applied;
1181
1182        truncate_rows(data, truncate_pos);
1183
1184        push_empty_column(data);
1185        widths.push(trailing_column_width);
1186        width += trailing_column_width + vertical;
1187
1188        return WidthEstimation::new(widths_original, widths, width, true, true);
1189    }
1190
1191    truncate_rows(data, truncate_pos - 1);
1192    let w = widths.pop().expect("ok");
1193    width -= w;
1194
1195    push_empty_column(data);
1196    widths.push(trailing_column_width);
1197    width += trailing_column_width;
1198
1199    let extra_budget = termwidth.saturating_sub(width);
1200    let last_visible_column = widths.len().saturating_sub(1);
1201    let applied = apply_extra_budget_to_visible_columns(
1202        &mut widths,
1203        extra_budget,
1204        width_priority_columns,
1205        last_visible_column,
1206    );
1207    width += applied;
1208
1209    WidthEstimation::new(widths_original, widths, width, true, true)
1210}
1211
1212struct PriorityCompactionState<'a> {
1213    widths: &'a mut Vec<usize>,
1214    truncate_pos: &'a mut usize,
1215    width: &'a mut usize,
1216}
1217
1218struct PriorityCompactionData<'a> {
1219    widths_original: &'a [usize],
1220    width_priority_columns: &'a [usize],
1221}
1222
1223struct PriorityCompactionLimits {
1224    termwidth: usize,
1225    trailing_column_width: usize,
1226    vertical: usize,
1227    secondary_priority_bonus_limit: usize,
1228}
1229
1230/// Reclaims right-side columns when a visible primary priority column is still constrained.
1231///
1232/// This helper updates `widths`, `truncate_pos`, and `width` in place to reserve room for a
1233/// trailing marker and then reallocates the recovered budget toward priority columns first.
1234fn compact_partial_visibility_for_priority(
1235    state: &mut PriorityCompactionState,
1236    data: &PriorityCompactionData,
1237    limits: &PriorityCompactionLimits,
1238) {
1239    let Some(priority_column) =
1240        first_visible_priority_column(data.width_priority_columns, *state.truncate_pos)
1241    else {
1242        return;
1243    };
1244
1245    let priority_is_constrained =
1246        state.widths[priority_column] < data.widths_original[priority_column];
1247    let has_columns_on_the_right = *state.truncate_pos > priority_column + 1;
1248    let single_priority = data.width_priority_columns.len() == 1;
1249    let force_priority_to_right_edge = priority_column >= *state.truncate_pos / 2;
1250
1251    if !priority_is_constrained
1252        || !has_columns_on_the_right
1253        || !(force_priority_to_right_edge || single_priority)
1254    {
1255        return;
1256    }
1257
1258    let mut available = limits.termwidth - *state.width;
1259
1260    while *state.truncate_pos > priority_column + 1 {
1261        if single_priority && !force_priority_to_right_edge {
1262            let reserve_for_trailing = limits.trailing_column_width + limits.vertical;
1263            let need_for_priority =
1264                data.widths_original[priority_column].saturating_sub(state.widths[priority_column]);
1265
1266            if available >= reserve_for_trailing + need_for_priority {
1267                break;
1268            }
1269        }
1270
1271        let dropped = state.widths.pop().expect("ok");
1272        *state.truncate_pos -= 1;
1273
1274        let freed = dropped + limits.vertical;
1275        *state.width -= freed;
1276        available += freed;
1277    }
1278
1279    let reserve_for_trailing = limits.trailing_column_width + limits.vertical;
1280    if available <= reserve_for_trailing {
1281        return;
1282    }
1283
1284    let mut budget = available - reserve_for_trailing;
1285    let allocation_order = build_priority_allocation_order(
1286        data.width_priority_columns,
1287        *state.truncate_pos,
1288        priority_column,
1289    );
1290
1291    let consumed = distribute_available_width_round_robin(
1292        &mut state.widths[..*state.truncate_pos],
1293        &data.widths_original[..*state.truncate_pos],
1294        budget,
1295        &allocation_order,
1296    );
1297    *state.width += consumed;
1298    budget -= consumed;
1299
1300    if budget > 0 {
1301        let consumed = distribute_available_width(
1302            &mut state.widths[..*state.truncate_pos],
1303            &data.widths_original[..*state.truncate_pos],
1304            budget,
1305            &allocation_order,
1306        );
1307        *state.width += consumed;
1308        budget -= consumed;
1309    }
1310
1311    if budget > 0 {
1312        state.widths[priority_column] += budget;
1313        *state.width += budget;
1314    }
1315}
1316
1317/// Rebalances a fully visible column set so a constrained primary priority can dominate.
1318///
1319/// Returns `true` when the caller should append a trailing `...` column after compaction.
1320/// Returns `false` when no trailing marker should be added and the current visible set can be
1321/// rendered as-is.
1322fn compact_full_visibility_for_priority(
1323    state: &mut PriorityCompactionState,
1324    data: &PriorityCompactionData,
1325    limits: &PriorityCompactionLimits,
1326) -> bool {
1327    let Some(priority_column) =
1328        first_visible_priority_column(data.width_priority_columns, *state.truncate_pos)
1329    else {
1330        return false;
1331    };
1332
1333    let priority_is_constrained =
1334        state.widths[priority_column] < data.widths_original[priority_column];
1335    let has_columns_on_the_right = *state.truncate_pos > priority_column + 1;
1336    if !priority_is_constrained || !has_columns_on_the_right {
1337        return false;
1338    }
1339
1340    let mut available = limits.termwidth - *state.width;
1341    let force_priority_to_right_edge = priority_column >= *state.truncate_pos / 2;
1342
1343    loop {
1344        if *state.truncate_pos <= priority_column + 1 {
1345            break;
1346        }
1347
1348        if !force_priority_to_right_edge {
1349            let reserve_for_trailing = limits.trailing_column_width + limits.vertical;
1350            let has_budget_for_priority_and_trailing = if data.width_priority_columns.len() == 1 {
1351                let need_for_priority = data.widths_original[priority_column]
1352                    .saturating_sub(state.widths[priority_column]);
1353                available >= reserve_for_trailing + need_for_priority
1354            } else {
1355                let max_other_width = state
1356                    .widths
1357                    .iter()
1358                    .enumerate()
1359                    .filter_map(|(i, &col_width)| (i != priority_column).then_some(col_width))
1360                    .max()
1361                    .unwrap_or(0);
1362
1363                let need_for_widest =
1364                    (max_other_width + 1).saturating_sub(state.widths[priority_column]);
1365                available >= reserve_for_trailing + need_for_widest
1366            };
1367
1368            if has_budget_for_priority_and_trailing {
1369                break;
1370            }
1371        }
1372
1373        let dropped = state.widths.pop().expect("ok");
1374        *state.truncate_pos -= 1;
1375
1376        let freed = dropped + limits.vertical;
1377        *state.width -= freed;
1378        available += freed;
1379    }
1380
1381    let reserve_for_trailing = limits.trailing_column_width + limits.vertical;
1382    if available < reserve_for_trailing {
1383        return false;
1384    }
1385
1386    let mut budget = available - reserve_for_trailing;
1387
1388    let max_other = state
1389        .widths
1390        .iter()
1391        .enumerate()
1392        .filter_map(|(i, &col_width)| (i != priority_column).then_some(col_width))
1393        .max()
1394        .unwrap_or(0);
1395
1396    if state.widths[priority_column] <= max_other && budget > 0 {
1397        let target = max_other + 1;
1398        let need = min(
1399            data.widths_original[priority_column].saturating_sub(state.widths[priority_column]),
1400            target.saturating_sub(state.widths[priority_column]),
1401        );
1402        let take = min(budget, need);
1403
1404        state.widths[priority_column] += take;
1405        *state.width += take;
1406        budget -= take;
1407    }
1408
1409    if budget > 0 {
1410        let allocation_order = build_priority_allocation_order(
1411            data.width_priority_columns,
1412            *state.truncate_pos,
1413            priority_column,
1414        );
1415
1416        let consumed = distribute_available_width_round_robin(
1417            &mut state.widths[..*state.truncate_pos],
1418            &data.widths_original[..*state.truncate_pos],
1419            budget,
1420            &allocation_order,
1421        );
1422        *state.width += consumed;
1423        budget -= consumed;
1424
1425        let consumed = distribute_available_width(
1426            &mut state.widths[..*state.truncate_pos],
1427            &data.widths_original[..*state.truncate_pos],
1428            budget,
1429            &allocation_order,
1430        );
1431        *state.width += consumed;
1432        budget -= consumed;
1433
1434        if budget > 0 {
1435            state.widths[priority_column] += budget;
1436            *state.width += budget;
1437        }
1438    }
1439
1440    for &secondary in data
1441        .width_priority_columns
1442        .iter()
1443        .filter(|&&column| column < *state.truncate_pos && column != priority_column)
1444    {
1445        let max_other = state
1446            .widths
1447            .iter()
1448            .enumerate()
1449            .filter_map(|(i, &col_width)| {
1450                (i != priority_column && i != secondary).then_some(col_width)
1451            })
1452            .max()
1453            .unwrap_or(0);
1454
1455        let headroom_over_others = state.widths[priority_column].saturating_sub(max_other + 1);
1456        let headroom_over_secondary =
1457            state.widths[priority_column].saturating_sub(state.widths[secondary] + 1) / 2;
1458        let transferable = min(
1459            min(headroom_over_others, headroom_over_secondary),
1460            limits.secondary_priority_bonus_limit,
1461        );
1462
1463        if transferable == 0 {
1464            continue;
1465        }
1466
1467        state.widths[priority_column] -= transferable;
1468        state.widths[secondary] += transferable;
1469    }
1470
1471    true
1472}
1473
1474// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE solely based on first column.
1475fn truncate_columns_by_head(
1476    data: &mut Vec<Vec<NuRecordsValue>>,
1477    widths: Vec<usize>,
1478    theme: &TableTheme,
1479    pad: usize,
1480    termwidth: usize,
1481    width_priority_columns: &[usize],
1482) -> WidthEstimation {
1483    const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
1484
1485    let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
1486
1487    let count_columns = data[0].len();
1488
1489    let config = create_config(theme, false, None);
1490    let widths_original = widths;
1491    let mut widths = vec![];
1492
1493    let borders = config.get_borders();
1494    let vertical = borders.has_vertical() as usize;
1495
1496    let mut width = borders.has_left() as usize + borders.has_right() as usize;
1497    let mut truncate_pos = 0;
1498
1499    for (i, &column_width) in widths_original.iter().enumerate() {
1500        let head_width = NuRecordsValue::width(&data[0][i]) + pad;
1501        let vertical_width = if i > 0 { vertical } else { 0 };
1502
1503        let mut use_width = column_width;
1504        let mut next_move = use_width + vertical_width;
1505        if width + next_move > termwidth {
1506            use_width = head_width;
1507            next_move = use_width + vertical_width;
1508            if width + next_move > termwidth {
1509                break;
1510            }
1511        }
1512
1513        widths.push(use_width);
1514        width += next_move;
1515        truncate_pos += 1;
1516    }
1517
1518    if truncate_pos == 0 {
1519        return WidthEstimation::new(widths_original, widths, width, false, false);
1520    }
1521
1522    let mut available = termwidth - width;
1523
1524    if available > 0 {
1525        let consumed = distribute_available_width(
1526            &mut widths[..truncate_pos],
1527            &widths_original[..truncate_pos],
1528            available,
1529            width_priority_columns,
1530        );
1531        available -= consumed;
1532        width += consumed;
1533    }
1534
1535    if truncate_pos == count_columns {
1536        return WidthEstimation::new(widths_original, widths, width, true, false);
1537    }
1538
1539    if available >= trailing_column_width + vertical {
1540        truncate_rows(data, truncate_pos);
1541
1542        push_empty_column(data);
1543        widths.push(trailing_column_width);
1544        width += trailing_column_width + vertical;
1545
1546        return WidthEstimation::new(widths_original, widths, width, true, true);
1547    }
1548
1549    // NOTE: we must check if some columns are bigger than head_width
1550    //       and cut width from them first.
1551    //       rather than removing last column.
1552    //
1553    //       We intentionally check only last column.
1554    //       Although space could be given from any column.
1555    let last_column_width = widths[truncate_pos - 1];
1556    let last_column_width_min = NuRecordsValue::width(&data[0][truncate_pos - 1]) + pad;
1557    let last_column_width_free = last_column_width - last_column_width_min;
1558    if available + last_column_width_free >= trailing_column_width + vertical {
1559        let use_width = trailing_column_width + vertical - available;
1560        widths[truncate_pos - 1] -= use_width;
1561        width -= use_width;
1562
1563        truncate_rows(data, truncate_pos);
1564
1565        push_empty_column(data);
1566        widths.push(trailing_column_width);
1567        width += trailing_column_width + vertical;
1568
1569        return WidthEstimation::new(widths_original, widths, width, true, true);
1570    }
1571
1572    truncate_rows(data, truncate_pos - 1);
1573    let w = widths.pop().expect("ok");
1574    width -= w;
1575
1576    push_empty_column(data);
1577    widths.push(trailing_column_width);
1578    width += trailing_column_width;
1579
1580    WidthEstimation::new(widths_original, widths, width, true, true)
1581}
1582
1583fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
1584    let total = widths.iter().sum::<usize>();
1585    let countv = cfg.count_vertical(widths.len());
1586    let margin = cfg.get_margin();
1587
1588    total + countv + margin.left.size + margin.right.size
1589}
1590
1591fn create_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> ColoredConfig {
1592    let structure = TableStructure::new(false, with_header, false);
1593    let mut table = Table::new([[""]]);
1594    load_theme(&mut table, theme, &structure, color);
1595    table.get_config().clone()
1596}
1597
1598fn push_empty_column(data: &mut Vec<Vec<NuRecordsValue>>) {
1599    let empty_cell = Text::new(String::from(EMPTY_COLUMN_TEXT));
1600    for row in data {
1601        row.push(empty_cell.clone());
1602    }
1603}
1604
1605/// Returns the first configured priority column that is currently visible.
1606fn first_visible_priority_column(
1607    width_priority_columns: &[usize],
1608    visible_columns: usize,
1609) -> Option<usize> {
1610    // Width priorities are ordered; the first visible one is treated as primary.
1611    width_priority_columns
1612        .iter()
1613        .copied()
1614        .find(|&column| column < visible_columns)
1615}
1616
1617/// Builds the allocation order with the primary priority first, followed by visible secondaries.
1618fn build_priority_allocation_order(
1619    width_priority_columns: &[usize],
1620    visible_columns: usize,
1621    primary_priority_column: usize,
1622) -> Vec<usize> {
1623    // Keep the primary priority first, then retain the caller-provided order
1624    // for secondary priorities that are currently visible.
1625    let mut allocation_order = vec![primary_priority_column];
1626    allocation_order.extend(
1627        width_priority_columns
1628            .iter()
1629            .copied()
1630            .filter(|&column| column < visible_columns && column != primary_priority_column),
1631    );
1632    allocation_order
1633}
1634
1635/// Applies leftover width to visible columns, preferring explicit priorities when possible.
1636fn apply_extra_budget_to_visible_columns(
1637    widths: &mut [usize],
1638    extra_budget: usize,
1639    width_priority_columns: &[usize],
1640    visible_columns: usize,
1641) -> usize {
1642    // Any leftover width is intentionally biased toward priority columns,
1643    // with a fallback to the last visible data column.
1644    if extra_budget == 0 || width_priority_columns.is_empty() {
1645        return 0;
1646    }
1647
1648    if let Some(priority_column) =
1649        first_visible_priority_column(width_priority_columns, visible_columns)
1650    {
1651        widths[priority_column] += extra_budget;
1652        return extra_budget;
1653    }
1654
1655    if visible_columns > 0 {
1656        widths[visible_columns - 1] += extra_budget;
1657        return extra_budget;
1658    }
1659
1660    0
1661}
1662
1663/// Distributes available width with a priority-first pass and a legacy all-columns fallback.
1664///
1665/// Returns the total number of width units consumed from `available`.
1666fn distribute_available_width(
1667    widths: &mut [usize],
1668    widths_original: &[usize],
1669    available: usize,
1670    width_priority_columns: &[usize],
1671) -> usize {
1672    let initial_available = available;
1673    let mut available = available;
1674
1675    // First pass: give every explicitly-prioritized column a chance to grow.
1676    let consumed = distribute_available_width_round_robin(
1677        widths,
1678        widths_original,
1679        available,
1680        width_priority_columns,
1681    );
1682    available -= consumed;
1683
1684    // Second pass: preserve existing behavior for all columns.
1685    for i in 0..widths.len() {
1686        if available == 0 {
1687            break;
1688        }
1689
1690        let used_width = widths[i];
1691        let col_width = widths_original[i];
1692        if used_width < col_width {
1693            let need = col_width - used_width;
1694            let take = min(available, need);
1695            widths[i] += take;
1696            available -= take;
1697        }
1698    }
1699
1700    initial_available - available
1701}
1702
1703/// Distributes available width one unit at a time across priority columns in round-robin order.
1704///
1705/// Returns the total number of width units consumed from `available`.
1706fn distribute_available_width_round_robin(
1707    widths: &mut [usize],
1708    widths_original: &[usize],
1709    available: usize,
1710    width_priority_columns: &[usize],
1711) -> usize {
1712    let initial_available = available;
1713    let mut available = available;
1714
1715    while available > 0 {
1716        let mut consumed_in_round = 0;
1717
1718        for &column in width_priority_columns {
1719            if available == 0 {
1720                break;
1721            }
1722
1723            if column >= widths.len() {
1724                continue;
1725            }
1726
1727            let used_width = widths[column];
1728            let col_width = widths_original[column];
1729            if used_width < col_width {
1730                widths[column] += 1;
1731                available -= 1;
1732                consumed_in_round += 1;
1733            }
1734        }
1735
1736        if consumed_in_round == 0 {
1737            break;
1738        }
1739    }
1740
1741    initial_available - available
1742}
1743
1744fn duplicate_row(data: &mut Vec<Vec<NuRecordsValue>>, row: usize) {
1745    let duplicate = data[row].clone();
1746    data.push(duplicate);
1747}
1748
1749fn truncate_rows(data: &mut Vec<Vec<NuRecordsValue>>, count: usize) {
1750    for row in data {
1751        row.truncate(count);
1752    }
1753}
1754
1755fn convert_alignment(alignment: nu_color_config::Alignment) -> AlignmentHorizontal {
1756    match alignment {
1757        nu_color_config::Alignment::Center => AlignmentHorizontal::Center,
1758        nu_color_config::Alignment::Left => AlignmentHorizontal::Left,
1759        nu_color_config::Alignment::Right => AlignmentHorizontal::Right,
1760    }
1761}
1762
1763fn build_width(
1764    records: &[Vec<NuRecordsValue>],
1765    count_cols: usize,
1766    count_rows: usize,
1767    pad: usize,
1768) -> Vec<usize> {
1769    // TODO: Expose not spaned version (could be optimized).
1770    let mut cfg = SpannedConfig::default();
1771    cfg.set_padding(
1772        Entity::Global,
1773        Sides::new(
1774            Indent::spaced(pad),
1775            Indent::zero(),
1776            Indent::zero(),
1777            Indent::zero(),
1778        ),
1779    );
1780
1781    let records = IterRecords::new(records, count_cols, Some(count_rows));
1782
1783    PeekableGridDimension::width(records, &cfg)
1784}
1785
1786// It's laverages a use of guuaranted cached widths before hand
1787// to speed up things a bit.
1788struct SetLineHeaders {
1789    line: usize,
1790    pad: TableIndent,
1791    head: HeadInfo,
1792}
1793
1794impl SetLineHeaders {
1795    fn new(head: HeadInfo, line: usize, pad: TableIndent) -> Self {
1796        Self { line, head, pad }
1797    }
1798}
1799
1800impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for SetLineHeaders {
1801    fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
1802        let widths = match dims.get_widths() {
1803            Some(widths) => widths,
1804            None => {
1805                // we don't have widths cached; which means that NO width adjustments were done
1806                // which means we are OK to leave columns as they are.
1807                //
1808                // but we actually always have to have widths at this point
1809
1810                unreachable!("must never be the case");
1811            }
1812        };
1813
1814        let pad = self.pad.left + self.pad.right;
1815
1816        let columns = self
1817            .head
1818            .values
1819            .into_iter()
1820            .zip(widths.iter().cloned()) // it must be always safe to do
1821            .map(|(s, width)| Truncate::truncate(&s, width - pad).into_owned())
1822            .collect::<Vec<_>>();
1823
1824        let mut names = ColumnNames::new(columns)
1825            .line(self.line)
1826            .alignment(Alignment::from(self.head.align));
1827        if let Some(color) = self.head.color {
1828            names = names.color(color);
1829        }
1830
1831        //  FIXME: because of bug in tabled(latest) we got to modify columns
1832        //         because it fails to regognize right padding value
1833        //  UNCOMMENT when fixed
1834
1835        // let alignment_head = Alignment::from(self.head.align);
1836        // let alignment_index = Alignment::from(self.head.align_index);
1837        // if self.head.align == self.head.align_index {
1838        //     names = names.alignment(alignment_head);
1839        // } else {
1840        //     let mut v = vec![alignment_head; widths.len()];
1841        //     v[0] = alignment_index;
1842        //     names = names.alignment(v);
1843        // }
1844
1845        names.change(recs, cfg, dims);
1846    }
1847
1848    fn hint_change(&self) -> Option<Entity> {
1849        None
1850    }
1851}
1852
1853fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) {
1854    if let Some(line) = theme.get_horizontal_line(from) {
1855        theme.insert_horizontal_line(to, *line);
1856    }
1857}
1858
1859pub fn get_color_if_exists(c: &Color) -> Option<Color> {
1860    if !is_color_empty(c) {
1861        Some(c.clone())
1862    } else {
1863        None
1864    }
1865}