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 width_priority_columns: vec![],
84 },
85 }
86 }
87
88 pub fn count_rows(&self) -> usize {
90 self.count_rows
91 }
92
93 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 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 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 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 pub fn draw(self, termwidth: usize) -> Option<String> {
307 build_table(self, termwidth)
308 }
309
310 pub fn draw_unchecked(self, termwidth: usize) -> Option<String> {
314 build_table_unchecked(self, termwidth)
315 }
316
317 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
324impl 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 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 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 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 t.heights.remove(0);
536
537 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 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 dims.set_heights(self.heights);
708 dims.set_widths(self.width.needed);
709 }
710
711 fn hint_change(&self) -> Option<Entity> {
712 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 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 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
884fn 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 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 return WidthEstimation::new(widths_original, vec![], width, false, true);
1043 }
1044
1045 WidthEstimation::new(widths_original, widths, width, false, true)
1046}
1047
1048fn 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 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
1230fn 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
1317fn 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
1474fn 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 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
1605fn first_visible_priority_column(
1607 width_priority_columns: &[usize],
1608 visible_columns: usize,
1609) -> Option<usize> {
1610 width_priority_columns
1612 .iter()
1613 .copied()
1614 .find(|&column| column < visible_columns)
1615}
1616
1617fn build_priority_allocation_order(
1619 width_priority_columns: &[usize],
1620 visible_columns: usize,
1621 primary_priority_column: usize,
1622) -> Vec<usize> {
1623 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
1635fn 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 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
1663fn 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 let consumed = distribute_available_width_round_robin(
1677 widths,
1678 widths_original,
1679 available,
1680 width_priority_columns,
1681 );
1682 available -= consumed;
1683
1684 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
1703fn 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 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
1786struct 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 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()) .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 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}