// TODO: Stop building `tabled -e` when it's clear we are out of terminal // TODO: Stop building `tabled` when it's clear we are out of terminal // NOTE: TODO the above we could expose something like [`WidthCtrl`] in which case we could also laverage the width list build right away. // currently it seems like we do recacalculate it for `table -e`? use std::cmp::{max, min}; use nu_ansi_term::Style; use nu_color_config::TextStyle; use nu_protocol::{TableIndent, TrimStrategy}; use tabled::{ builder::Builder, grid::{ ansi::ANSIBuf, colors::Colors, config::{ AlignmentHorizontal, ColoredConfig, Entity, Indent, Position, Sides, SpannedConfig, }, dimension::{CompleteDimensionVecRecords, SpannedGridDimension}, records::{ vec_records::{Cell, Text, VecRecords}, IntoRecords, IterRecords, Records, }, }, settings::{ formatting::AlignmentStrategy, object::{Columns, Rows}, themes::ColumnNames, width::Truncate, Alignment, CellOption, Color, Padding, TableOption, Width, }, Table, }; use crate::{convert_style, is_color_empty, table_theme::TableTheme}; const EMPTY_COLUMN_TEXT: &str = "..."; const EMPTY_COLUMN_TEXT_WIDTH: usize = 3; pub type NuRecords = VecRecords; pub type NuRecordsValue = Text; /// NuTable is a table rendering implementation. #[derive(Debug, Clone)] pub struct NuTable { data: Vec>, widths: Vec, count_rows: usize, count_cols: usize, styles: Styles, config: TableConfig, } impl NuTable { /// Creates an empty [`NuTable`] instance. pub fn new(count_rows: usize, count_cols: usize) -> Self { Self { data: vec![vec![Text::default(); count_cols]; count_rows], widths: vec![2; count_cols], count_rows, count_cols, styles: Styles { cfg: ColoredConfig::default(), alignments: CellConfiguration { data: AlignmentHorizontal::Left, index: AlignmentHorizontal::Right, header: AlignmentHorizontal::Center, }, colors: CellConfiguration::default(), }, config: TableConfig { theme: TableTheme::basic(), trim: TrimStrategy::truncate(None), structure: TableStructure::new(false, false, false), indent: TableIndent::new(1, 1), header_on_border: false, expand: false, border_color: None, }, } } /// Return amount of rows. pub fn count_rows(&self) -> usize { self.count_rows } /// Return amount of columns. pub fn count_columns(&self) -> usize { self.count_cols } pub fn insert(&mut self, pos: Position, text: String) { let text = Text::new(text); self.widths[pos.1] = max( self.widths[pos.1], text.width() + indent_sum(self.config.indent), ); self.data[pos.0][pos.1] = text; } pub fn set_row(&mut self, index: usize, row: Vec) { assert_eq!(self.data[index].len(), row.len()); for (i, text) in row.iter().enumerate() { self.widths[i] = max( self.widths[i], text.width() + indent_sum(self.config.indent), ); } self.data[index] = row; } pub fn pop_column(&mut self, count: usize) { self.count_cols -= count; self.widths.truncate(self.count_cols); for row in &mut self.data[..] { row.truncate(self.count_cols); } // set to default styles of the popped columns for i in 0..count { let col = self.count_cols + i; for row in 0..self.count_rows { self.styles .cfg .set_alignment_horizontal(Entity::Cell(row, col), self.styles.alignments.data); self.styles .cfg .set_color(Entity::Cell(row, col), ANSIBuf::default()); } } } pub fn push_column(&mut self, text: String) { let value = Text::new(text); self.widths .push(value.width() + indent_sum(self.config.indent)); for row in &mut self.data[..] { row.push(value.clone()); } self.count_cols += 1; } pub fn insert_style(&mut self, pos: Position, style: TextStyle) { if let Some(style) = style.color_style { let style = convert_style(style); self.styles.cfg.set_color(pos.into(), style.into()); } let alignment = convert_alignment(style.alignment); if alignment != self.styles.alignments.data { self.styles .cfg .set_alignment_horizontal(pos.into(), alignment); } } pub fn set_header_style(&mut self, style: TextStyle) { if let Some(style) = style.color_style { let style = convert_style(style); self.styles.colors.header = style; } self.styles.alignments.header = convert_alignment(style.alignment); } pub fn set_index_style(&mut self, style: TextStyle) { if let Some(style) = style.color_style { let style = convert_style(style); self.styles.colors.index = style; } self.styles.alignments.index = convert_alignment(style.alignment); } pub fn set_data_style(&mut self, style: TextStyle) { if let Some(style) = style.color_style { if !style.is_plain() { let style = convert_style(style); self.styles.cfg.set_color(Entity::Global, style.into()); } } let alignment = convert_alignment(style.alignment); self.styles .cfg .set_alignment_horizontal(Entity::Global, alignment); self.styles.alignments.data = alignment; } // NOTE: Crusial to be called before data changes (todo fix interface) pub fn set_indent(&mut self, indent: TableIndent) { self.config.indent = indent; let pad = indent_sum(indent); for w in &mut self.widths { *w = pad; } } pub fn set_theme(&mut self, theme: TableTheme) { self.config.theme = theme; } pub fn set_structure(&mut self, index: bool, header: bool, footer: bool) { self.config.structure = TableStructure::new(index, header, footer); } pub fn set_border_header(&mut self, on: bool) { self.config.header_on_border = on; } pub fn set_trim(&mut self, strategy: TrimStrategy) { self.config.trim = strategy; } pub fn set_strategy(&mut self, expand: bool) { self.config.expand = expand; } pub fn set_border_color(&mut self, color: Style) { self.config.border_color = (!color.is_plain()).then_some(color); } // NOTE: BE CAREFUL TO KEEP WIDTH UNCHANGED // TODO: fix interface pub fn get_records_mut(&mut self) -> &mut [Vec] { &mut self.data } /// Converts a table to a String. /// /// It returns None in case where table cannot be fit to a terminal width. pub fn draw(self, termwidth: usize) -> Option { build_table(self, termwidth) } /// Return a total table width. pub fn total_width(&self) -> usize { let config = create_config(&self.config.theme, false, None); get_total_width2(&self.widths, &config) } } impl From>>> for NuTable { fn from(value: Vec>>) -> Self { let count_rows = value.len(); let count_cols = if value.is_empty() { 0 } else { value[0].len() }; let mut t = Self::new(count_rows, count_cols); t.data = value; table_recalculate_widths(&mut t); t } } fn table_recalculate_widths(t: &mut NuTable) { let pad = indent_sum(t.config.indent); let records = IterRecords::new(&t.data, t.count_cols, Some(t.count_rows)); let widths = build_width(records, pad); t.widths = widths; } #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Hash)] struct CellConfiguration { index: Value, header: Value, data: Value, } #[derive(Debug, Clone, PartialEq, Eq)] struct Styles { cfg: ColoredConfig, colors: CellConfiguration, alignments: CellConfiguration, } #[derive(Debug, Clone)] pub struct TableConfig { theme: TableTheme, trim: TrimStrategy, border_color: Option