1use std::cmp::{max, min};
9
10use nu_ansi_term::Style;
11use nu_color_config::TextStyle;
12use nu_protocol::{TableIndent, TrimStrategy};
13
14use tabled::{
15 Table,
16 builder::Builder,
17 grid::{
18 ansi::ANSIBuf,
19 config::{
20 AlignmentHorizontal, ColoredConfig, Entity, Indent, Position, Sides, SpannedConfig,
21 },
22 dimension::{CompleteDimension, PeekableGridDimension},
23 records::{
24 IterRecords, PeekableRecords,
25 vec_records::{Cell, Text, VecRecords},
26 },
27 },
28 settings::{
29 Alignment, CellOption, Color, Padding, TableOption, Width,
30 formatting::AlignmentStrategy,
31 object::{Columns, Rows},
32 themes::ColumnNames,
33 width::Truncate,
34 },
35};
36
37use crate::{convert_style, is_color_empty, table_theme::TableTheme};
38
39const EMPTY_COLUMN_TEXT: &str = "...";
40const EMPTY_COLUMN_TEXT_WIDTH: usize = 3;
41
42pub type NuRecords = VecRecords<NuRecordsValue>;
43pub type NuRecordsValue = Text<String>;
44
45#[derive(Debug, Clone)]
47pub struct NuTable {
48 data: Vec<Vec<NuRecordsValue>>,
49 widths: Vec<usize>,
50 heights: Vec<usize>,
51 count_rows: usize,
52 count_cols: usize,
53 styles: Styles,
54 config: TableConfig,
55}
56
57impl NuTable {
58 pub fn new(count_rows: usize, count_cols: usize) -> Self {
60 Self {
61 data: vec![vec![Text::default(); count_cols]; count_rows],
62 widths: vec![2; count_cols],
63 heights: vec![0; count_rows],
64 count_rows,
65 count_cols,
66 styles: Styles {
67 cfg: ColoredConfig::default(),
68 alignments: CellConfiguration {
69 data: AlignmentHorizontal::Left,
70 index: AlignmentHorizontal::Right,
71 header: AlignmentHorizontal::Center,
72 },
73 colors: CellConfiguration::default(),
74 },
75 config: TableConfig {
76 theme: TableTheme::basic(),
77 trim: TrimStrategy::truncate(None),
78 structure: TableStructure::new(false, false, false),
79 indent: TableIndent::new(1, 1),
80 header_on_border: false,
81 expand: false,
82 border_color: None,
83 },
84 }
85 }
86
87 pub fn count_rows(&self) -> usize {
89 self.count_rows
90 }
91
92 pub fn count_columns(&self) -> usize {
94 self.count_cols
95 }
96
97 pub fn create(text: String) -> NuRecordsValue {
98 Text::new(text)
99 }
100
101 pub fn insert_value(&mut self, pos: (usize, usize), value: NuRecordsValue) {
102 let width = value.width() + indent_sum(self.config.indent);
103 let height = value.count_lines();
104 self.widths[pos.1] = max(self.widths[pos.1], width);
105 self.heights[pos.0] = max(self.heights[pos.0], height);
106 self.data[pos.0][pos.1] = value;
107 }
108
109 pub fn insert(&mut self, pos: (usize, usize), text: String) {
110 let text = Text::new(text);
111 let pad = indent_sum(self.config.indent);
112 let width = text.width() + pad;
113 let height = text.count_lines();
114 self.widths[pos.1] = max(self.widths[pos.1], width);
115 self.heights[pos.0] = max(self.heights[pos.0], height);
116 self.data[pos.0][pos.1] = text;
117 }
118
119 pub fn set_row(&mut self, index: usize, row: Vec<NuRecordsValue>) {
120 assert_eq!(self.data[index].len(), row.len());
121
122 for (i, text) in row.iter().enumerate() {
123 let pad = indent_sum(self.config.indent);
124 let width = text.width() + pad;
125 let height = text.count_lines();
126
127 self.widths[i] = max(self.widths[i], width);
128 self.heights[index] = max(self.heights[index], height);
129 }
130
131 self.data[index] = row;
132 }
133
134 pub fn pop_column(&mut self, count: usize) {
135 self.count_cols -= count;
136 self.widths.truncate(self.count_cols);
137
138 for (row, height) in self.data.iter_mut().zip(self.heights.iter_mut()) {
139 row.truncate(self.count_cols);
140
141 let row_height = *height;
142 let mut new_height = 0;
143 for cell in row.iter() {
144 let height = cell.count_lines();
145 if height == row_height {
146 new_height = height;
147 break;
148 }
149
150 new_height = max(new_height, height);
151 }
152
153 *height = new_height;
154 }
155
156 for i in 0..count {
158 let col = self.count_cols + i;
159 for row in 0..self.count_rows {
160 self.styles
161 .cfg
162 .set_alignment_horizontal(Entity::Cell(row, col), self.styles.alignments.data);
163 self.styles
164 .cfg
165 .set_color(Entity::Cell(row, col), ANSIBuf::default());
166 }
167 }
168 }
169
170 pub fn push_column(&mut self, text: String) {
171 let value = Text::new(text);
172
173 let pad = indent_sum(self.config.indent);
174 let width = value.width() + pad;
175 let height = value.count_lines();
176 self.widths.push(width);
177
178 for row in 0..self.count_rows {
179 self.heights[row] = max(self.heights[row], height);
180 }
181
182 for row in &mut self.data[..] {
183 row.push(value.clone());
184 }
185
186 self.count_cols += 1;
187 }
188
189 pub fn insert_style(&mut self, pos: (usize, usize), style: TextStyle) {
190 if let Some(style) = style.color_style
191 && !style.is_plain()
192 {
193 let style = convert_style(style);
194 self.styles.cfg.set_color(pos.into(), style.into());
195 }
196
197 let alignment = convert_alignment(style.alignment);
198 if alignment != self.styles.alignments.data {
199 self.styles
200 .cfg
201 .set_alignment_horizontal(pos.into(), alignment);
202 }
203 }
204
205 pub fn set_header_style(&mut self, style: TextStyle) {
206 if let Some(style) = style.color_style
207 && !style.is_plain()
208 {
209 let style = convert_style(style);
210 self.styles.colors.header = style;
211 }
212
213 self.styles.alignments.header = convert_alignment(style.alignment);
214 }
215
216 pub fn set_index_style(&mut self, style: TextStyle) {
217 if let Some(style) = style.color_style
218 && !style.is_plain()
219 {
220 let style = convert_style(style);
221 self.styles.colors.index = style;
222 }
223
224 self.styles.alignments.index = convert_alignment(style.alignment);
225 }
226
227 pub fn set_data_style(&mut self, style: TextStyle) {
228 if let Some(style) = style.color_style
229 && !style.is_plain()
230 {
231 let style = convert_style(style);
232 self.styles.cfg.set_color(Entity::Global, style.into());
233 }
234
235 let alignment = convert_alignment(style.alignment);
236 self.styles
237 .cfg
238 .set_alignment_horizontal(Entity::Global, alignment);
239 self.styles.alignments.data = alignment;
240 }
241
242 pub fn set_indent(&mut self, indent: TableIndent) {
244 self.config.indent = indent;
245
246 let pad = indent_sum(indent);
247 for w in &mut self.widths {
248 *w = pad;
249 }
250 }
251
252 pub fn set_theme(&mut self, theme: TableTheme) {
253 self.config.theme = theme;
254 }
255
256 pub fn set_structure(&mut self, index: bool, header: bool, footer: bool) {
257 self.config.structure = TableStructure::new(index, header, footer);
258 }
259
260 pub fn set_border_header(&mut self, on: bool) {
261 self.config.header_on_border = on;
262 }
263
264 pub fn set_trim(&mut self, strategy: TrimStrategy) {
265 self.config.trim = strategy;
266 }
267
268 pub fn set_strategy(&mut self, expand: bool) {
269 self.config.expand = expand;
270 }
271
272 pub fn set_border_color(&mut self, color: Style) {
273 self.config.border_color = (!color.is_plain()).then_some(color);
274 }
275
276 pub fn clear_border_color(&mut self) {
277 self.config.border_color = None;
278 }
279
280 pub fn get_records_mut(&mut self) -> &mut [Vec<NuRecordsValue>] {
283 &mut self.data
284 }
285
286 pub fn clear_all_colors(&mut self) {
287 self.clear_border_color();
288 let cfg = std::mem::take(&mut self.styles.cfg);
289 self.styles.cfg = ColoredConfig::new(cfg.into_inner());
290 }
291
292 pub fn draw(self, termwidth: usize) -> Option<String> {
296 build_table(self, termwidth)
297 }
298
299 pub fn draw_unchecked(self, termwidth: usize) -> Option<String> {
303 build_table_unchecked(self, termwidth)
304 }
305
306 pub fn total_width(&self) -> usize {
308 let config = create_config(&self.config.theme, false, None);
309 get_total_width2(&self.widths, &config)
310 }
311}
312
313impl From<Vec<Vec<Text<String>>>> for NuTable {
317 fn from(value: Vec<Vec<Text<String>>>) -> Self {
318 let count_rows = value.len();
319 let count_cols = if value.is_empty() { 0 } else { value[0].len() };
320
321 let mut t = Self::new(count_rows, count_cols);
322 for (i, row) in value.into_iter().enumerate() {
323 t.set_row(i, row);
324 }
325
326 table_recalculate_widths(&mut t);
327
328 t
329 }
330}
331
332fn table_recalculate_widths(t: &mut NuTable) {
333 let pad = indent_sum(t.config.indent);
334 t.widths = build_width(&t.data, t.count_cols, t.count_rows, pad);
335}
336
337#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Hash)]
338struct CellConfiguration<Value> {
339 index: Value,
340 header: Value,
341 data: Value,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
345struct Styles {
346 cfg: ColoredConfig,
347 colors: CellConfiguration<Color>,
348 alignments: CellConfiguration<AlignmentHorizontal>,
349}
350
351#[derive(Debug, Clone)]
352pub struct TableConfig {
353 theme: TableTheme,
354 trim: TrimStrategy,
355 border_color: Option<Style>,
356 expand: bool,
357 structure: TableStructure,
358 header_on_border: bool,
359 indent: TableIndent,
360}
361
362#[derive(Debug, Clone, Copy)]
363struct TableStructure {
364 with_index: bool,
365 with_header: bool,
366 with_footer: bool,
367}
368
369impl TableStructure {
370 fn new(with_index: bool, with_header: bool, with_footer: bool) -> Self {
371 Self {
372 with_index,
373 with_header,
374 with_footer,
375 }
376 }
377}
378
379#[derive(Debug, Clone)]
380struct HeadInfo {
381 values: Vec<String>,
382 align: AlignmentHorizontal,
383 #[allow(dead_code)]
384 align_index: AlignmentHorizontal,
385 color: Option<Color>,
386}
387
388impl HeadInfo {
389 fn new(
390 values: Vec<String>,
391 align: AlignmentHorizontal,
392 align_index: AlignmentHorizontal,
393 color: Option<Color>,
394 ) -> Self {
395 Self {
396 values,
397 align,
398 align_index,
399 color,
400 }
401 }
402}
403
404fn build_table_unchecked(mut t: NuTable, termwidth: usize) -> Option<String> {
405 if t.count_columns() == 0 || t.count_rows() == 0 {
406 return Some(String::new());
407 }
408
409 let widths = std::mem::take(&mut t.widths);
410 let config = create_config(&t.config.theme, false, None);
411 let totalwidth = get_total_width2(&t.widths, &config);
412 let widths = WidthEstimation::new(widths.clone(), widths, totalwidth, false, false);
413
414 let head = remove_header_if(&mut t);
415 table_insert_footer_if(&mut t);
416
417 draw_table(t, widths, head, termwidth)
418}
419
420fn build_table(mut t: NuTable, termwidth: usize) -> Option<String> {
421 if t.count_columns() == 0 || t.count_rows() == 0 {
422 return Some(String::new());
423 }
424
425 let widths = table_truncate(&mut t, termwidth)?;
426 let head = remove_header_if(&mut t);
427 table_insert_footer_if(&mut t);
428
429 draw_table(t, widths, head, termwidth)
430}
431
432fn remove_header_if(t: &mut NuTable) -> Option<HeadInfo> {
433 if !is_header_on_border(t) {
434 return None;
435 }
436
437 let head = remove_header(t);
438 t.config.structure.with_header = false;
439
440 Some(head)
441}
442
443fn is_header_on_border(t: &NuTable) -> bool {
444 let is_configured = t.config.structure.with_header && t.config.header_on_border;
445 let has_horizontal = t.config.theme.as_base().borders_has_top()
446 || t.config.theme.as_base().get_horizontal_line(1).is_some();
447 is_configured && has_horizontal
448}
449
450fn table_insert_footer_if(t: &mut NuTable) {
451 let with_footer = t.config.structure.with_header && t.config.structure.with_footer;
452 if !with_footer {
453 return;
454 }
455
456 duplicate_row(&mut t.data, 0);
457
458 if !t.heights.is_empty() {
459 t.heights.push(t.heights[0]);
460 }
461}
462
463fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option<WidthEstimation> {
464 let truncate_by_head = is_header_on_border(t);
465 let widths = maybe_truncate_columns(
466 &mut t.data,
467 t.widths.clone(),
468 &t.config,
469 termwidth,
470 truncate_by_head,
471 );
472 if widths.needed.is_empty() {
473 return None;
474 }
475
476 if widths.trail {
478 let col = widths.needed.len() - 1;
479 for row in 0..t.count_rows {
480 t.styles
481 .cfg
482 .set_alignment_horizontal(Entity::Cell(row, col), t.styles.alignments.data);
483 t.styles
484 .cfg
485 .set_color(Entity::Cell(row, col), ANSIBuf::default());
486 }
487 }
488
489 Some(widths)
490}
491
492fn remove_header(t: &mut NuTable) -> HeadInfo {
493 for row in 1..t.data.len() {
495 for col in 0..t.count_cols {
496 let from = Position::new(row, col);
497 let to = Position::new(row - 1, col);
498
499 let alignment = *t.styles.cfg.get_alignment_horizontal(from);
500 if alignment != t.styles.alignments.data {
501 t.styles.cfg.set_alignment_horizontal(to.into(), alignment);
502 }
503
504 let color = t.styles.cfg.get_color(from);
505 if let Some(color) = color
506 && !color.is_empty()
507 {
508 let color = color.clone();
509 t.styles.cfg.set_color(to.into(), color);
510 }
511 }
512 }
513
514 let head = t
515 .data
516 .remove(0)
517 .into_iter()
518 .map(|s| s.to_string())
519 .collect();
520
521 t.heights.remove(0);
523
524 table_recalculate_widths(t);
528
529 let color = get_color_if_exists(&t.styles.colors.header);
530 let alignment = t.styles.alignments.header;
531 let alignment_index = if t.config.structure.with_index {
532 t.styles.alignments.index
533 } else {
534 t.styles.alignments.header
535 };
536
537 t.styles.alignments.header = AlignmentHorizontal::Center;
538 t.styles.colors.header = Color::empty();
539
540 HeadInfo::new(head, alignment, alignment_index, color)
541}
542
543fn draw_table(
544 t: NuTable,
545 width: WidthEstimation,
546 head: Option<HeadInfo>,
547 termwidth: usize,
548) -> Option<String> {
549 let mut structure = t.config.structure;
550 structure.with_footer = structure.with_footer && head.is_none();
551 let sep_color = t.config.border_color;
552
553 let data = t.data;
554 let mut table = Builder::from_vec(data).build();
555
556 set_styles(&mut table, t.styles, &structure);
557 set_indent(&mut table, t.config.indent);
558 load_theme(&mut table, &t.config.theme, &structure, sep_color);
559 truncate_table(&mut table, &t.config, width, termwidth, t.heights);
560 table_set_border_header(&mut table, head, &t.config);
561
562 let string = table.to_string();
563 Some(string)
564}
565
566fn set_styles(table: &mut Table, styles: Styles, structure: &TableStructure) {
567 table.with(styles.cfg);
568 align_table(table, styles.alignments, structure);
569 colorize_table(table, styles.colors, structure);
570}
571
572fn table_set_border_header(table: &mut Table, head: Option<HeadInfo>, cfg: &TableConfig) {
573 let head = match head {
574 Some(head) => head,
575 None => return,
576 };
577
578 let theme = &cfg.theme;
579 let with_footer = cfg.structure.with_footer;
580
581 if !theme.as_base().borders_has_top() {
582 let line = theme.as_base().get_horizontal_line(1);
583 if let Some(line) = line.cloned() {
584 table.get_config_mut().insert_horizontal_line(0, line);
585 if with_footer {
586 let last_row = table.count_rows();
587 table
588 .get_config_mut()
589 .insert_horizontal_line(last_row, line);
590 }
591 };
592 }
593
594 if with_footer {
596 let last_row = table.count_rows();
597 table.with(SetLineHeaders::new(head.clone(), last_row, cfg.indent));
598 }
599
600 table.with(SetLineHeaders::new(head, 0, cfg.indent));
601}
602
603fn truncate_table(
604 table: &mut Table,
605 cfg: &TableConfig,
606 width: WidthEstimation,
607 termwidth: usize,
608 heights: Vec<usize>,
609) {
610 let trim = cfg.trim.clone();
611 let pad = indent_sum(cfg.indent);
612 let ctrl = DimensionCtrl::new(termwidth, width, trim, cfg.expand, pad, heights);
613 table.with(ctrl);
614}
615
616fn indent_sum(indent: TableIndent) -> usize {
617 indent.left + indent.right
618}
619
620fn set_indent(table: &mut Table, indent: TableIndent) {
621 table.with(Padding::new(indent.left, indent.right, 0, 0));
622}
623
624struct DimensionCtrl {
625 width: WidthEstimation,
626 trim_strategy: TrimStrategy,
627 max_width: usize,
628 expand: bool,
629 pad: usize,
630 heights: Vec<usize>,
631}
632
633impl DimensionCtrl {
634 fn new(
635 max_width: usize,
636 width: WidthEstimation,
637 trim_strategy: TrimStrategy,
638 expand: bool,
639 pad: usize,
640 heights: Vec<usize>,
641 ) -> Self {
642 Self {
643 width,
644 trim_strategy,
645 max_width,
646 expand,
647 pad,
648 heights,
649 }
650 }
651}
652
653#[derive(Debug, Clone)]
654struct WidthEstimation {
655 original: Vec<usize>,
656 needed: Vec<usize>,
657 #[allow(dead_code)]
658 total: usize,
659 truncate: bool,
660 trail: bool,
661}
662
663impl WidthEstimation {
664 fn new(
665 original: Vec<usize>,
666 needed: Vec<usize>,
667 total: usize,
668 truncate: bool,
669 trail: bool,
670 ) -> Self {
671 Self {
672 original,
673 needed,
674 total,
675 truncate,
676 trail,
677 }
678 }
679}
680
681impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for DimensionCtrl {
682 fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
683 if self.width.truncate {
684 width_ctrl_truncate(self, recs, cfg, dims);
685 return;
686 }
687
688 if self.expand {
689 width_ctrl_expand(self, recs, cfg, dims);
690 return;
691 }
692
693 dims.set_heights(self.heights);
695 dims.set_widths(self.width.needed);
696 }
697
698 fn hint_change(&self) -> Option<Entity> {
699 if self.width.truncate && matches!(self.trim_strategy, TrimStrategy::Truncate { .. }) {
705 Some(Entity::Row(0))
706 } else {
707 None
708 }
709 }
710}
711
712fn width_ctrl_expand(
713 ctrl: DimensionCtrl,
714 recs: &mut NuRecords,
715 cfg: &mut ColoredConfig,
716 dims: &mut CompleteDimension,
717) {
718 dims.set_heights(ctrl.heights);
719 let opt = Width::increase(ctrl.max_width);
720 TableOption::<NuRecords, _, _>::change(opt, recs, cfg, dims);
721}
722
723fn width_ctrl_truncate(
724 ctrl: DimensionCtrl,
725 recs: &mut NuRecords,
726 cfg: &mut ColoredConfig,
727 dims: &mut CompleteDimension,
728) {
729 let mut heights = ctrl.heights;
730
731 for (col, (&width, width_original)) in ctrl
733 .width
734 .needed
735 .iter()
736 .zip(ctrl.width.original)
737 .enumerate()
738 {
739 if width == width_original {
740 continue;
741 }
742
743 let width = width - ctrl.pad;
744
745 match &ctrl.trim_strategy {
746 TrimStrategy::Wrap { try_to_keep_words } => {
747 let wrap = Width::wrap(width).keep_words(*try_to_keep_words);
748
749 CellOption::<NuRecords, _>::change(wrap, recs, cfg, Entity::Column(col));
750
751 for (row, row_height) in heights.iter_mut().enumerate() {
754 let height = recs.count_lines(Position::new(row, col));
755 *row_height = max(*row_height, height);
756 }
757 }
758 TrimStrategy::Truncate { suffix } => {
759 let mut truncate = Width::truncate(width);
760 if let Some(suffix) = suffix {
761 truncate = truncate.suffix(suffix).suffix_try_color(true);
762 }
763
764 CellOption::<NuRecords, _>::change(truncate, recs, cfg, Entity::Column(col));
765 }
766 }
767 }
768
769 dims.set_heights(heights);
770 dims.set_widths(ctrl.width.needed);
771}
772
773fn align_table(
774 table: &mut Table,
775 alignments: CellConfiguration<AlignmentHorizontal>,
776 structure: &TableStructure,
777) {
778 table.with(AlignmentStrategy::PerLine);
779
780 if structure.with_header {
781 table.modify(Rows::first(), AlignmentStrategy::PerCell);
782 table.modify(Rows::first(), Alignment::from(alignments.header));
783
784 if structure.with_footer {
785 table.modify(Rows::last(), AlignmentStrategy::PerCell);
786 table.modify(Rows::last(), Alignment::from(alignments.header));
787 }
788 }
789
790 if structure.with_index {
791 table.modify(Columns::first(), Alignment::from(alignments.index));
792 }
793}
794
795fn colorize_table(table: &mut Table, styles: CellConfiguration<Color>, structure: &TableStructure) {
796 if structure.with_index && !is_color_empty(&styles.index) {
797 table.modify(Columns::first(), styles.index);
798 }
799
800 if structure.with_header && !is_color_empty(&styles.header) {
801 table.modify(Rows::first(), styles.header.clone());
802 }
803
804 if structure.with_header && structure.with_footer && !is_color_empty(&styles.header) {
805 table.modify(Rows::last(), styles.header);
806 }
807}
808
809fn load_theme(
810 table: &mut Table,
811 theme: &TableTheme,
812 structure: &TableStructure,
813 sep_color: Option<Style>,
814) {
815 let with_header = table.count_rows() > 1 && structure.with_header;
816 let with_footer = with_header && structure.with_footer;
817 let mut theme = theme.as_base().clone();
818
819 if !with_header {
820 let borders = *theme.get_borders();
821 theme.remove_horizontal_lines();
822 theme.set_borders(borders);
823 } else if with_footer {
824 theme_copy_horizontal_line(&mut theme, 1, table.count_rows() - 1);
825 }
826
827 table.with(theme);
828
829 if let Some(style) = sep_color {
830 let color = convert_style(style);
831 let color = ANSIBuf::from(color);
832 table.get_config_mut().set_border_color_default(color);
833 }
834}
835
836fn maybe_truncate_columns(
837 data: &mut Vec<Vec<NuRecordsValue>>,
838 widths: Vec<usize>,
839 cfg: &TableConfig,
840 termwidth: usize,
841 truncate_by_head: bool,
842) -> WidthEstimation {
843 const TERMWIDTH_THRESHOLD: usize = 120;
844
845 let pad = cfg.indent.left + cfg.indent.right;
846 let preserve_content = termwidth > TERMWIDTH_THRESHOLD;
847
848 if truncate_by_head {
849 truncate_columns_by_head(data, widths, &cfg.theme, pad, termwidth)
850 } else if preserve_content {
851 truncate_columns_by_columns(data, widths, &cfg.theme, pad, termwidth)
852 } else {
853 truncate_columns_by_content(data, widths, &cfg.theme, pad, termwidth)
854 }
855}
856
857fn truncate_columns_by_content(
859 data: &mut Vec<Vec<NuRecordsValue>>,
860 widths: Vec<usize>,
861 theme: &TableTheme,
862 pad: usize,
863 termwidth: usize,
864) -> WidthEstimation {
865 const MIN_ACCEPTABLE_WIDTH: usize = 5;
866 const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
867
868 let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
869 let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
870
871 let count_columns = data[0].len();
872
873 let config = create_config(theme, false, None);
874 let widths_original = widths;
875 let mut widths = vec![];
876
877 let borders = config.get_borders();
878 let vertical = borders.has_vertical() as usize;
879
880 let mut width = borders.has_left() as usize + borders.has_right() as usize;
881 let mut truncate_pos = 0;
882
883 for (i, &column_width) in widths_original.iter().enumerate() {
884 let mut next_move = column_width;
885 if i > 0 {
886 next_move += vertical;
887 }
888 if width + next_move > termwidth {
889 break;
890 }
891 widths.push(column_width);
892 width += next_move;
893 truncate_pos += 1;
894 }
895
896 if truncate_pos == count_columns {
897 return WidthEstimation::new(widths_original, widths, width, false, false);
898 }
899
900 let is_last_column = truncate_pos + 1 == count_columns;
901 if truncate_pos == 0 && !is_last_column {
902 if termwidth > width {
903 let available = termwidth - width;
904 if available >= min_column_width + vertical + trailing_column_width {
905 truncate_rows(data, 1);
906
907 let first_col_width = available - (vertical + trailing_column_width);
908 widths.push(first_col_width);
909 width += first_col_width;
910
911 push_empty_column(data);
912 widths.push(trailing_column_width);
913 width += trailing_column_width + vertical;
914
915 return WidthEstimation::new(widths_original, widths, width, true, true);
916 }
917 }
918
919 return WidthEstimation::new(widths_original, widths, width, false, false);
920 }
921
922 let available = termwidth - width;
923
924 let can_fit_last_column = available >= min_column_width + vertical;
925 if is_last_column && can_fit_last_column {
926 let w = available - vertical;
927 widths.push(w);
928 width += w + vertical;
929
930 return WidthEstimation::new(widths_original, widths, width, true, false);
931 }
932
933 let is_almost_last_column = truncate_pos + 2 == count_columns;
935 if is_almost_last_column {
936 let next_column_width = widths_original[truncate_pos + 1];
937 let has_space_for_two_columns =
938 available >= min_column_width + vertical + next_column_width + vertical;
939
940 if !is_last_column && has_space_for_two_columns {
941 let rest = available - vertical - next_column_width - vertical;
942 widths.push(rest);
943 width += rest + vertical;
944
945 widths.push(next_column_width);
946 width += next_column_width + vertical;
947
948 return WidthEstimation::new(widths_original, widths, width, true, false);
949 }
950 }
951
952 let has_space_for_two_columns =
953 available >= min_column_width + vertical + trailing_column_width + vertical;
954 if !is_last_column && has_space_for_two_columns {
955 truncate_rows(data, truncate_pos + 1);
956
957 let rest = available - vertical - trailing_column_width - vertical;
958 widths.push(rest);
959 width += rest + vertical;
960
961 push_empty_column(data);
962 widths.push(trailing_column_width);
963 width += trailing_column_width + vertical;
964
965 return WidthEstimation::new(widths_original, widths, width, true, true);
966 }
967
968 if available >= trailing_column_width + vertical {
969 truncate_rows(data, truncate_pos);
970
971 push_empty_column(data);
972 widths.push(trailing_column_width);
973 width += trailing_column_width + vertical;
974
975 return WidthEstimation::new(widths_original, widths, width, false, true);
976 }
977
978 let last_width = widths.last().cloned().expect("ok");
979 let can_truncate_last = last_width > min_column_width;
980
981 if can_truncate_last {
982 let rest = last_width - min_column_width;
983 let maybe_available = available + rest;
984
985 if maybe_available >= trailing_column_width + vertical {
986 truncate_rows(data, truncate_pos);
987
988 let left = maybe_available - trailing_column_width - vertical;
989 let new_last_width = min_column_width + left;
990
991 widths[truncate_pos - 1] = new_last_width;
992 width -= last_width;
993 width += new_last_width;
994
995 push_empty_column(data);
996 widths.push(trailing_column_width);
997 width += trailing_column_width + vertical;
998
999 return WidthEstimation::new(widths_original, widths, width, true, true);
1000 }
1001 }
1002
1003 truncate_rows(data, truncate_pos - 1);
1004 let w = widths.pop().expect("ok");
1005 width -= w;
1006
1007 push_empty_column(data);
1008 widths.push(trailing_column_width);
1009 width += trailing_column_width;
1010
1011 let has_only_trail = widths.len() == 1;
1012 let is_enough_space = width <= termwidth;
1013 if has_only_trail || !is_enough_space {
1014 return WidthEstimation::new(widths_original, vec![], width, false, true);
1016 }
1017
1018 WidthEstimation::new(widths_original, widths, width, false, true)
1019}
1020
1021fn truncate_columns_by_columns(
1032 data: &mut Vec<Vec<NuRecordsValue>>,
1033 widths: Vec<usize>,
1034 theme: &TableTheme,
1035 pad: usize,
1036 termwidth: usize,
1037) -> WidthEstimation {
1038 const MIN_ACCEPTABLE_WIDTH: usize = 10;
1039 const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
1040
1041 let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
1042 let min_column_width = MIN_ACCEPTABLE_WIDTH + pad;
1043
1044 let count_columns = data[0].len();
1045
1046 let config = create_config(theme, false, None);
1047 let widths_original = widths;
1048 let mut widths = vec![];
1049
1050 let borders = config.get_borders();
1051 let vertical = borders.has_vertical() as usize;
1052
1053 let mut width = borders.has_left() as usize + borders.has_right() as usize;
1054 let mut truncate_pos = 0;
1055
1056 for (i, &width_orig) in widths_original.iter().enumerate() {
1057 let use_width = min(min_column_width, width_orig);
1058 let mut next_move = use_width;
1059 if i > 0 {
1060 next_move += vertical;
1061 }
1062
1063 if width + next_move > termwidth {
1064 break;
1065 }
1066
1067 widths.push(use_width);
1068 width += next_move;
1069 truncate_pos += 1;
1070 }
1071
1072 if truncate_pos == 0 {
1073 return WidthEstimation::new(widths_original, widths, width, false, false);
1074 }
1075
1076 let mut available = termwidth - width;
1077
1078 if available > 0 {
1079 for i in 0..truncate_pos {
1080 let used_width = widths[i];
1081 let col_width = widths_original[i];
1082 if used_width < col_width {
1083 let need = col_width - used_width;
1084 let take = min(available, need);
1085 available -= take;
1086
1087 widths[i] += take;
1088 width += take;
1089
1090 if available == 0 {
1091 break;
1092 }
1093 }
1094 }
1095 }
1096
1097 if truncate_pos == count_columns {
1098 return WidthEstimation::new(widths_original, widths, width, true, false);
1099 }
1100
1101 if available >= trailing_column_width + vertical {
1102 truncate_rows(data, truncate_pos);
1103
1104 push_empty_column(data);
1105 widths.push(trailing_column_width);
1106 width += trailing_column_width + vertical;
1107
1108 return WidthEstimation::new(widths_original, widths, width, true, true);
1109 }
1110
1111 truncate_rows(data, truncate_pos - 1);
1112 let w = widths.pop().expect("ok");
1113 width -= w;
1114
1115 push_empty_column(data);
1116 widths.push(trailing_column_width);
1117 width += trailing_column_width;
1118
1119 WidthEstimation::new(widths_original, widths, width, true, true)
1120}
1121
1122fn truncate_columns_by_head(
1124 data: &mut Vec<Vec<NuRecordsValue>>,
1125 widths: Vec<usize>,
1126 theme: &TableTheme,
1127 pad: usize,
1128 termwidth: usize,
1129) -> WidthEstimation {
1130 const TRAILING_COLUMN_WIDTH: usize = EMPTY_COLUMN_TEXT_WIDTH;
1131
1132 let trailing_column_width = TRAILING_COLUMN_WIDTH + pad;
1133
1134 let count_columns = data[0].len();
1135
1136 let config = create_config(theme, false, None);
1137 let widths_original = widths;
1138 let mut widths = vec![];
1139
1140 let borders = config.get_borders();
1141 let vertical = borders.has_vertical() as usize;
1142
1143 let mut width = borders.has_left() as usize + borders.has_right() as usize;
1144 let mut truncate_pos = 0;
1145
1146 for (i, &column_width) in widths_original.iter().enumerate() {
1147 let head_width = NuRecordsValue::width(&data[0][i]) + pad;
1148 let vertical_width = if i > 0 { vertical } else { 0 };
1149
1150 let mut use_width = column_width;
1151 let mut next_move = use_width + vertical_width;
1152 if width + next_move > termwidth {
1153 use_width = head_width;
1154 next_move = use_width + vertical_width;
1155 if width + next_move > termwidth {
1156 break;
1157 }
1158 }
1159
1160 widths.push(use_width);
1161 width += next_move;
1162 truncate_pos += 1;
1163 }
1164
1165 if truncate_pos == 0 {
1166 return WidthEstimation::new(widths_original, widths, width, false, false);
1167 }
1168
1169 let mut available = termwidth - width;
1170
1171 if available > 0 {
1172 for i in 0..truncate_pos {
1173 let used_width = widths[i];
1174 let col_width = widths_original[i];
1175 if used_width < col_width {
1176 let need = col_width - used_width;
1177 let take = min(available, need);
1178 available -= take;
1179
1180 widths[i] += take;
1181 width += take;
1182
1183 if available == 0 {
1184 break;
1185 }
1186 }
1187 }
1188 }
1189
1190 if truncate_pos == count_columns {
1191 return WidthEstimation::new(widths_original, widths, width, true, false);
1192 }
1193
1194 if available >= trailing_column_width + vertical {
1195 truncate_rows(data, truncate_pos);
1196
1197 push_empty_column(data);
1198 widths.push(trailing_column_width);
1199 width += trailing_column_width + vertical;
1200
1201 return WidthEstimation::new(widths_original, widths, width, true, true);
1202 }
1203
1204 let last_column_width = widths[truncate_pos - 1];
1211 let last_column_width_min = NuRecordsValue::width(&data[0][truncate_pos - 1]) + pad;
1212 let last_column_width_free = last_column_width - last_column_width_min;
1213 if available + last_column_width_free >= trailing_column_width + vertical {
1214 let use_width = trailing_column_width + vertical - available;
1215 widths[truncate_pos - 1] -= use_width;
1216 width -= use_width;
1217
1218 truncate_rows(data, truncate_pos);
1219
1220 push_empty_column(data);
1221 widths.push(trailing_column_width);
1222 width += trailing_column_width + vertical;
1223
1224 return WidthEstimation::new(widths_original, widths, width, true, true);
1225 }
1226
1227 truncate_rows(data, truncate_pos - 1);
1228 let w = widths.pop().expect("ok");
1229 width -= w;
1230
1231 push_empty_column(data);
1232 widths.push(trailing_column_width);
1233 width += trailing_column_width;
1234
1235 WidthEstimation::new(widths_original, widths, width, true, true)
1236}
1237
1238fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize {
1239 let total = widths.iter().sum::<usize>();
1240 let countv = cfg.count_vertical(widths.len());
1241 let margin = cfg.get_margin();
1242
1243 total + countv + margin.left.size + margin.right.size
1244}
1245
1246fn create_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> ColoredConfig {
1247 let structure = TableStructure::new(false, with_header, false);
1248 let mut table = Table::new([[""]]);
1249 load_theme(&mut table, theme, &structure, color);
1250 table.get_config().clone()
1251}
1252
1253fn push_empty_column(data: &mut Vec<Vec<NuRecordsValue>>) {
1254 let empty_cell = Text::new(String::from(EMPTY_COLUMN_TEXT));
1255 for row in data {
1256 row.push(empty_cell.clone());
1257 }
1258}
1259
1260fn duplicate_row(data: &mut Vec<Vec<NuRecordsValue>>, row: usize) {
1261 let duplicate = data[row].clone();
1262 data.push(duplicate);
1263}
1264
1265fn truncate_rows(data: &mut Vec<Vec<NuRecordsValue>>, count: usize) {
1266 for row in data {
1267 row.truncate(count);
1268 }
1269}
1270
1271fn convert_alignment(alignment: nu_color_config::Alignment) -> AlignmentHorizontal {
1272 match alignment {
1273 nu_color_config::Alignment::Center => AlignmentHorizontal::Center,
1274 nu_color_config::Alignment::Left => AlignmentHorizontal::Left,
1275 nu_color_config::Alignment::Right => AlignmentHorizontal::Right,
1276 }
1277}
1278
1279fn build_width(
1280 records: &[Vec<NuRecordsValue>],
1281 count_cols: usize,
1282 count_rows: usize,
1283 pad: usize,
1284) -> Vec<usize> {
1285 let mut cfg = SpannedConfig::default();
1287 cfg.set_padding(
1288 Entity::Global,
1289 Sides::new(
1290 Indent::spaced(pad),
1291 Indent::zero(),
1292 Indent::zero(),
1293 Indent::zero(),
1294 ),
1295 );
1296
1297 let records = IterRecords::new(records, count_cols, Some(count_rows));
1298
1299 PeekableGridDimension::width(records, &cfg)
1300}
1301
1302struct SetLineHeaders {
1305 line: usize,
1306 pad: TableIndent,
1307 head: HeadInfo,
1308}
1309
1310impl SetLineHeaders {
1311 fn new(head: HeadInfo, line: usize, pad: TableIndent) -> Self {
1312 Self { line, head, pad }
1313 }
1314}
1315
1316impl TableOption<NuRecords, ColoredConfig, CompleteDimension> for SetLineHeaders {
1317 fn change(self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimension) {
1318 let widths = match dims.get_widths() {
1319 Some(widths) => widths,
1320 None => {
1321 unreachable!("must never be the case");
1327 }
1328 };
1329
1330 let pad = self.pad.left + self.pad.right;
1331
1332 let columns = self
1333 .head
1334 .values
1335 .into_iter()
1336 .zip(widths.iter().cloned()) .map(|(s, width)| Truncate::truncate(&s, width - pad).into_owned())
1338 .collect::<Vec<_>>();
1339
1340 let mut names = ColumnNames::new(columns).line(self.line);
1342
1343 if let Some(color) = self.head.color {
1344 names = names.color(color);
1345 }
1346
1347 names = names.alignment(Alignment::from(self.head.align));
1348
1349 names.change(recs, cfg, dims);
1364 }
1365
1366 fn hint_change(&self) -> Option<Entity> {
1367 None
1368 }
1369}
1370
1371fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) {
1372 if let Some(line) = theme.get_horizontal_line(from) {
1373 theme.insert_horizontal_line(to, *line);
1374 }
1375}
1376
1377pub fn get_color_if_exists(c: &Color) -> Option<Color> {
1378 if !is_color_empty(c) {
1379 Some(c.clone())
1380 } else {
1381 None
1382 }
1383}