diff --git a/Cargo.lock b/Cargo.lock index 61eaf53b2..03dc7890d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3289,9 +3289,9 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] name = "papergrid" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae7891b22598926e4398790c8fe6447930c72a67d36d983a49d6ce682ce83290" +checksum = "a2ccbe15f2b6db62f9a9871642746427e297b0ceb85f9a7f1ee5ff47d184d0c8" dependencies = [ "ansi-str", "ansitok", @@ -4947,9 +4947,9 @@ dependencies = [ [[package]] name = "tabled" -version = "0.12.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce69a5028cd9576063ec1f48edb2c75339fd835e6094ef3e05b3a079bf594a6" +checksum = "dfe9c3632da101aba5131ed63f9eed38665f8b3c68703a6bb18124835c1a5d22" dependencies = [ "ansi-str", "ansitok", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 7bc23b0b1..b8b2cd3dc 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -84,7 +84,7 @@ serde_yaml = "0.9" sha2 = "0.10" sqlparser = { version = "0.34", features = ["serde"], optional = true } sysinfo = "0.29" -tabled = { version = "0.12.2", features = ["color"], default-features = false } +tabled = { version = "0.14.0", features = ["color"], default-features = false } terminal_size = "0.2" titlecase = "2.0" toml = "0.7" diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index b41e290de..dd7ddb423 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -6,12 +6,13 @@ use nu_engine::{env::get_config, env_to_string, CallExt}; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Category, Config, DataSource, Example, FooterMode, IntoPipelineData, ListStream, PipelineData, + Category, Config, DataSource, Example, IntoPipelineData, ListStream, PipelineData, PipelineMetadata, RawStream, ShellError, Signature, Span, SyntaxShape, Type, Value, }; +use nu_table::common::create_nu_table_config; use nu_table::{ - BuildConfig, Cell, CollapsedTable, ExpandedTable, JustTable, NuTable, StringResult, - TableConfig, TableOutput, TableTheme, + CollapsedTable, ExpandedTable, JustTable, NuTable, NuTableCell, StringResult, TableOpts, + TableOutput, }; use nu_utils::get_ls_colors; use std::sync::Arc; @@ -361,8 +362,8 @@ fn handle_record( let result = if cols.is_empty() { create_empty_placeholder("record", term_width, engine_state, stack) } else { - let opts = BuildConfig::new(ctrlc, config, style_computer, span, term_width); - let result = build_table_kv(cols, vals, table_view, opts)?; + let opts = TableOpts::new(config, style_computer, ctrlc, span, 0, term_width); + let result = build_table_kv(cols, vals, table_view, opts, span)?; match result { Some(output) => maybe_strip_color(output, config), None => report_unsuccessful_output(ctrlc1, term_width), @@ -391,7 +392,8 @@ fn build_table_kv( cols: Vec, vals: Vec, table_view: TableView, - opts: BuildConfig<'_>, + opts: TableOpts<'_>, + span: Span, ) -> StringResult { match table_view { TableView::General => JustTable::kv_table(&cols, &vals, opts), @@ -404,7 +406,6 @@ fn build_table_kv( ExpandedTable::new(limit, flatten, sep).build_map(&cols, &vals, opts) } TableView::Collapsed => { - let span = opts.span(); let value = Value::Record { cols, vals, span }; CollapsedTable::build(value, opts) } @@ -414,21 +415,20 @@ fn build_table_kv( fn build_table_batch( vals: Vec, table_view: TableView, - row_offset: usize, - opts: BuildConfig<'_>, + opts: TableOpts<'_>, + span: Span, ) -> StringResult { match table_view { - TableView::General => JustTable::table(&vals, row_offset, opts), + TableView::General => JustTable::table(&vals, opts), TableView::Expanded { limit, flatten, flatten_separator, } => { let sep = flatten_separator.unwrap_or_else(|| String::from(' ')); - ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts, row_offset) + ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts) } TableView::Collapsed => { - let span = opts.span(); let value = Value::List { vals, span }; CollapsedTable::build(value, opts) } @@ -647,20 +647,16 @@ impl PagingTableCreator { return Ok(None); } - let config = get_config(&self.engine_state, &self.stack); - let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); - let term_width = get_width_param(self.width_param); - - let ctrlc = self.ctrlc.clone(); - let span = self.head; - let opts = BuildConfig::new(ctrlc, &config, &style_computer, span, term_width); + let cfg = get_config(&self.engine_state, &self.stack); + let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); + let opts = self.create_table_opts(&cfg, &style_comp); let view = TableView::Expanded { limit, flatten, flatten_separator, }; - build_table_batch(batch, view, self.row_offset, opts) + build_table_batch(batch, view, opts, self.head) } fn build_collapsed(&mut self, batch: Vec) -> StringResult { @@ -668,26 +664,34 @@ impl PagingTableCreator { return Ok(None); } - let config = get_config(&self.engine_state, &self.stack); - let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); - let term_width = get_width_param(self.width_param); - let ctrlc = self.ctrlc.clone(); - let span = self.head; - let opts = BuildConfig::new(ctrlc, &config, &style_computer, span, term_width); + let cfg = get_config(&self.engine_state, &self.stack); + let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); + let opts = self.create_table_opts(&cfg, &style_comp); - build_table_batch(batch, TableView::Collapsed, self.row_offset, opts) + build_table_batch(batch, TableView::Collapsed, opts, self.head) } fn build_general(&mut self, batch: Vec) -> StringResult { - let term_width = get_width_param(self.width_param); - let config = get_config(&self.engine_state, &self.stack); - let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); - let ctrlc = self.ctrlc.clone(); - let span = self.head; - let row_offset = self.row_offset; - let opts = BuildConfig::new(ctrlc, &config, &style_computer, span, term_width); + let cfg = get_config(&self.engine_state, &self.stack); + let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); + let opts = self.create_table_opts(&cfg, &style_comp); - build_table_batch(batch, TableView::General, row_offset, opts) + build_table_batch(batch, TableView::General, opts, self.head) + } + + fn create_table_opts<'a>( + &self, + cfg: &'a Config, + style_comp: &'a StyleComputer<'a>, + ) -> TableOpts<'a> { + TableOpts::new( + cfg, + style_comp, + self.ctrlc.clone(), + self.head, + self.row_offset, + get_width_param(self.width_param), + ) } } @@ -780,22 +784,6 @@ impl Iterator for PagingTableCreator { } } -fn load_theme_from_config(config: &Config) -> TableTheme { - match config.table_mode.as_str() { - "basic" => TableTheme::basic(), - "thin" => TableTheme::thin(), - "light" => TableTheme::light(), - "compact" => TableTheme::compact(), - "with_love" => TableTheme::with_love(), - "compact_double" => TableTheme::compact_double(), - "rounded" => TableTheme::rounded(), - "reinforced" => TableTheme::reinforced(), - "heavy" => TableTheme::heavy(), - "none" => TableTheme::none(), - _ => TableTheme::rounded(), - } -} - fn render_path_name( path: &str, config: &Config, @@ -859,34 +847,6 @@ fn maybe_strip_color(output: String, config: &Config) -> String { } } -fn create_table_config(config: &Config, comp: &StyleComputer, out: &TableOutput) -> TableConfig { - let theme = load_theme_from_config(config); - let footer = with_footer(config, out.with_header, out.table.count_rows()); - let line_style = lookup_separator_color(comp); - let trim = config.trim_strategy.clone(); - - TableConfig::new() - .theme(theme) - .with_footer(footer) - .with_header(out.with_header) - .with_index(out.with_index) - .line_style(line_style) - .trim(trim) -} - -fn lookup_separator_color(style_computer: &StyleComputer) -> nu_ansi_term::Style { - style_computer.compute("separator", &Value::nothing(Span::unknown())) -} - -fn with_footer(config: &Config, with_header: bool, count_records: usize) -> bool { - with_header && need_footer(config, count_records as u64) -} - -fn need_footer(config: &Config, count_records: u64) -> bool { - matches!(config.footer_mode, FooterMode::RowCount(limit) if count_records > limit) - || matches!(config.footer_mode, FooterMode::Always) -} - fn create_empty_placeholder( value_type_name: &str, termwidth: usize, @@ -898,14 +858,14 @@ fn create_empty_placeholder( return String::new(); } - let cell = Cell::new(format!("empty {}", value_type_name)); + let cell = NuTableCell::new(format!("empty {}", value_type_name)); let data = vec![vec![cell]]; let mut table = NuTable::from(data); - table.set_cell_style((0, 0), TextStyle::default().dimmed()); + table.set_data_style(TextStyle::default().dimmed()); let out = TableOutput::new(table, false, false); let style_computer = &StyleComputer::from_config(engine_state, stack); - let config = create_table_config(&config, style_computer, &out); + let config = create_nu_table_config(&config, style_computer, &out, false); out.table .draw(config, termwidth) diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index aec59a5fb..826cbae94 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -2384,3 +2384,179 @@ fn table_index_offset() { let expected_suffix = actual.out.strip_suffix(suffix); assert!(expected_suffix.is_some(), "{:?}", actual.out); } + +#[test] +fn table_theme_on_border_light() { + assert_eq!( + create_theme_output("light"), + [ + "─#───a───b─────────c──────── 0 1 2 3 1 4 5 [list 3 items] ", + "─#───a───b─────────c──────── 0 1 2 3 1 4 5 [list 3 items] ─#───a───b─────────c────────", + "─#───a───b───c─ 0 1 2 3 ─#───a───b───c─", + "─#───a_looooooong_name───b───c─ 0 1 2 3 ─#───a_looooooong_name───b───c─", + ] + ); +} + +#[test] +fn table_theme_on_border_basic() { + assert_eq!( + create_theme_output("basic"), + [ + "+-#-+-a-+-b-+-------c--------+| 0 | 1 | 2 | 3 |+---+---+---+----------------+| 1 | 4 | 5 | [list 3 items] |+---+---+---+----------------+", + "+-#-+-a-+-b-+-------c--------+| 0 | 1 | 2 | 3 |+---+---+---+----------------+| 1 | 4 | 5 | [list 3 items] |+-#-+-a-+-b-+-------c--------+", + "+-#-+-a-+-b-+-c-+| 0 | 1 | 2 | 3 |+-#-+-a-+-b-+-c-+", + "+-#-+-a_looooooong_name-+-b-+-c-+| 0 | 1 | 2 | 3 |+-#-+-a_looooooong_name-+-b-+-c-+" + ] + ); +} + +#[test] +fn table_theme_on_border_compact() { + assert_eq!( + create_theme_output("compact"), + [ + "─#─┬─a─┬─b─┬───────c──────── 0 │ 1 │ 2 │ 3 1 │ 4 │ 5 │ [list 3 items] ───┴───┴───┴────────────────", + "─#─┬─a─┬─b─┬───────c──────── 0 │ 1 │ 2 │ 3 1 │ 4 │ 5 │ [list 3 items] ─#─┴─a─┴─b─┴───────c────────", + "─#─┬─a─┬─b─┬─c─ 0 │ 1 │ 2 │ 3 ─#─┴─a─┴─b─┴─c─", + "─#─┬─a_looooooong_name─┬─b─┬─c─ 0 │ 1 │ 2 │ 3 ─#─┴─a_looooooong_name─┴─b─┴─c─" + ] + ); +} + +#[test] +fn table_theme_on_border_compact_double() { + assert_eq!( + create_theme_output("compact_double"), + [ + "═#═╦═a═╦═b═╦═══════c════════ 0 ║ 1 ║ 2 ║ 3 1 ║ 4 ║ 5 ║ [list 3 items] ═══╩═══╩═══╩════════════════", + "═#═╦═a═╦═b═╦═══════c════════ 0 ║ 1 ║ 2 ║ 3 1 ║ 4 ║ 5 ║ [list 3 items] ═#═╩═a═╩═b═╩═══════c════════", + "═#═╦═a═╦═b═╦═c═ 0 ║ 1 ║ 2 ║ 3 ═#═╩═a═╩═b═╩═c═", + "═#═╦═a_looooooong_name═╦═b═╦═c═ 0 ║ 1 ║ 2 ║ 3 ═#═╩═a_looooooong_name═╩═b═╩═c═" + ] + ); +} + +#[test] +fn table_theme_on_border_default() { + assert_eq!( + create_theme_output("default"), + [ + "╭─#─┬─a─┬─b─┬───────c────────╮│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │╰───┴───┴───┴────────────────╯", + "╭─#─┬─a─┬─b─┬───────c────────╮│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │╰─#─┴─a─┴─b─┴───────c────────╯", + "╭─#─┬─a─┬─b─┬─c─╮│ 0 │ 1 │ 2 │ 3 │╰─#─┴─a─┴─b─┴─c─╯", + "╭─#─┬─a_looooooong_name─┬─b─┬─c─╮│ 0 │ 1 │ 2 │ 3 │╰─#─┴─a_looooooong_name─┴─b─┴─c─╯" + ] + ); +} + +#[test] +fn table_theme_on_border_heavy() { + assert_eq!( + create_theme_output("heavy"), + [ + "┏━#━┳━a━┳━b━┳━━━━━━━c━━━━━━━━┓┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃┃ 1 ┃ 4 ┃ 5 ┃ [list 3 items] ┃┗━━━┻━━━┻━━━┻━━━━━━━━━━━━━━━━┛", + "┏━#━┳━a━┳━b━┳━━━━━━━c━━━━━━━━┓┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃┃ 1 ┃ 4 ┃ 5 ┃ [list 3 items] ┃┗━#━┻━a━┻━b━┻━━━━━━━c━━━━━━━━┛", + "┏━#━┳━a━┳━b━┳━c━┓┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃┗━#━┻━a━┻━b━┻━c━┛", + "┏━#━┳━a_looooooong_name━┳━b━┳━c━┓┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃┗━#━┻━a_looooooong_name━┻━b━┻━c━┛" + ] + ); +} + +#[test] +fn table_theme_on_border_reinforced() { + assert_eq!( + create_theme_output("reinforced"), + [ + "┏─#─┬─a─┬─b─┬───────c────────┓│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │┗───┴───┴───┴────────────────┛", + "┏─#─┬─a─┬─b─┬───────c────────┓│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │┗─#─┴─a─┴─b─┴───────c────────┛", + "┏─#─┬─a─┬─b─┬─c─┓│ 0 │ 1 │ 2 │ 3 │┗─#─┴─a─┴─b─┴─c─┛", + "┏─#─┬─a_looooooong_name─┬─b─┬─c─┓│ 0 │ 1 │ 2 │ 3 │┗─#─┴─a_looooooong_name─┴─b─┴─c─┛" + ] + ); +} + +#[test] +fn table_theme_on_border_none() { + assert_eq!( + create_theme_output("none"), + [ + " # a b c 0 1 2 3 1 4 5 [list 3 items] ", + " # a b c 0 1 2 3 1 4 5 [list 3 items] # a b c ", + " # a b c 0 1 2 3 # a b c ", + " # a_looooooong_name b c 0 1 2 3 # a_looooooong_name b c " + ] + ); +} + +#[test] +fn table_theme_on_border_rounded() { + assert_eq!( + create_theme_output("rounded"), + [ + "╭─#─┬─a─┬─b─┬───────c────────╮│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │╰───┴───┴───┴────────────────╯", + "╭─#─┬─a─┬─b─┬───────c────────╮│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │╰─#─┴─a─┴─b─┴───────c────────╯", + "╭─#─┬─a─┬─b─┬─c─╮│ 0 │ 1 │ 2 │ 3 │╰─#─┴─a─┴─b─┴─c─╯", + "╭─#─┬─a_looooooong_name─┬─b─┬─c─╮│ 0 │ 1 │ 2 │ 3 │╰─#─┴─a_looooooong_name─┴─b─┴─c─╯" + ] + ); +} + +#[test] +fn table_theme_on_border_with_love() { + assert_eq!( + create_theme_output("with_love"), + [ + "❤#❤❤❤a❤❤❤b❤❤❤❤❤❤❤❤❤c❤❤❤❤❤❤❤❤ 0 ❤ 1 ❤ 2 ❤ 3 1 ❤ 4 ❤ 5 ❤ [list 3 items] ❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤", + "❤#❤❤❤a❤❤❤b❤❤❤❤❤❤❤❤❤c❤❤❤❤❤❤❤❤ 0 ❤ 1 ❤ 2 ❤ 3 1 ❤ 4 ❤ 5 ❤ [list 3 items] ❤#❤❤❤a❤❤❤b❤❤❤❤❤❤❤❤❤c❤❤❤❤❤❤❤❤", + "❤#❤❤❤a❤❤❤b❤❤❤c❤ 0 ❤ 1 ❤ 2 ❤ 3 ❤#❤❤❤a❤❤❤b❤❤❤c❤", + "❤#❤❤❤a_looooooong_name❤❤❤b❤❤❤c❤ 0 ❤ 1 ❤ 2 ❤ 3 ❤#❤❤❤a_looooooong_name❤❤❤b❤❤❤c❤" + ] + ); +} + +#[test] +fn table_theme_on_border_thin() { + assert_eq!( + create_theme_output("thin"), + [ + "┌─#─┬─a─┬─b─┬───────c────────┐│ 0 │ 1 │ 2 │ 3 │├───┼───┼───┼────────────────┤│ 1 │ 4 │ 5 │ [list 3 items] │└───┴───┴───┴────────────────┘", + "┌─#─┬─a─┬─b─┬───────c────────┐│ 0 │ 1 │ 2 │ 3 │├───┼───┼───┼────────────────┤│ 1 │ 4 │ 5 │ [list 3 items] │└─#─┴─a─┴─b─┴───────c────────┘", + "┌─#─┬─a─┬─b─┬─c─┐│ 0 │ 1 │ 2 │ 3 │└─#─┴─a─┴─b─┴─c─┘", + "┌─#─┬─a_looooooong_name─┬─b─┬─c─┐│ 0 │ 1 │ 2 │ 3 │└─#─┴─a_looooooong_name─┴─b─┴─c─┘", + ] + ); +} + +fn create_theme_output(theme: &str) -> Vec { + vec![ + nu!(theme_cmd( + theme, + false, + "[[a b, c]; [1 2 3] [4 5 [1 2 3]]] | table" + )) + .out, + nu!(theme_cmd( + theme, + true, + "[[a b, c]; [1 2 3] [4 5 [1 2 3]]] | table" + )) + .out, + nu!(theme_cmd(theme, true, "[[a b, c]; [1 2 3]] | table")).out, + nu!(theme_cmd( + theme, + true, + "[[a_looooooong_name b, c]; [1 2 3]] | table" + )) + .out, + ] +} + +fn theme_cmd(theme: &str, footer: bool, then: &str) -> String { + let mut with_foorter = String::new(); + if footer { + with_foorter = "$env.config.footer_mode = \"always\"".to_string(); + } + + format!("$env.config.table.mode = {theme}; $env.config.table.header_on_separator = true; {with_foorter}; {then}") +} diff --git a/crates/nu-explore/src/nu_common/table.rs b/crates/nu-explore/src/nu_common/table.rs index 5286216d3..345e8a111 100644 --- a/crates/nu-explore/src/nu_common/table.rs +++ b/crates/nu-explore/src/nu_common/table.rs @@ -1,6 +1,9 @@ use nu_color_config::StyleComputer; use nu_protocol::{Span, Value}; -use nu_table::{value_to_clean_styled_string, value_to_styled_string, BuildConfig, ExpandedTable}; +use nu_table::{ + common::{nu_value_to_string, nu_value_to_string_clean}, + ExpandedTable, TableOpts, +}; use std::sync::atomic::AtomicBool; use std::sync::Arc; @@ -18,9 +21,9 @@ pub fn try_build_table( try_build_map(cols, vals, span, style_computer, ctrlc, config) } val if matches!(val, Value::String { .. }) => { - value_to_clean_styled_string(&val, config, style_computer).0 + nu_value_to_string_clean(&val, config, style_computer).0 } - val => value_to_styled_string(&val, config, style_computer).0, + val => nu_value_to_string(&val, config, style_computer).0, } } @@ -32,12 +35,19 @@ fn try_build_map( ctrlc: Option>, config: &NuConfig, ) -> String { - let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX); + let opts = TableOpts::new( + config, + style_computer, + ctrlc, + Span::unknown(), + 0, + usize::MAX, + ); let result = ExpandedTable::new(None, false, String::new()).build_map(&cols, &vals, opts); match result { Ok(Some(result)) => result, Ok(None) | Err(_) => { - value_to_styled_string(&Value::Record { cols, vals, span }, config, style_computer).0 + nu_value_to_string(&Value::Record { cols, vals, span }, config, style_computer).0 } } } @@ -49,13 +59,20 @@ fn try_build_list( span: Span, style_computer: &StyleComputer, ) -> String { - let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX); - let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts, 0); + let opts = TableOpts::new( + config, + style_computer, + ctrlc, + Span::unknown(), + 0, + usize::MAX, + ); + let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts); match result { Ok(Some(out)) => out, Ok(None) | Err(_) => { // it means that the list is empty - value_to_styled_string(&Value::List { vals, span }, config, style_computer).0 + nu_value_to_string(&Value::List { vals, span }, config, style_computer).0 } } } diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index c11c0f5b3..03c03c0be 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -70,6 +70,7 @@ pub struct Config { pub external_completer: Option, pub filesize_metric: bool, pub table_mode: String, + pub table_move_header: bool, pub table_show_empty: bool, pub use_ls_colors: bool, pub color_config: HashMap, @@ -126,6 +127,7 @@ impl Default for Config { table_index_mode: TableIndexMode::Always, table_show_empty: true, trim_strategy: TRIM_STRATEGY_DEFAULT, + table_move_header: false, datetime_normal_format: None, datetime_table_format: None, @@ -926,6 +928,9 @@ impl Value { Value::string(config.table_mode.clone(), span); } } + "header_on_separator" => { + try_bool!(cols, vals, index, span, table_move_header) + } "index_mode" => { if let Ok(b) = value.as_string() { let val_str = b.to_lowercase(); diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml index 83e8117bd..6c12d6009 100644 --- a/crates/nu-table/Cargo.toml +++ b/crates/nu-table/Cargo.toml @@ -16,7 +16,7 @@ nu-utils = { path = "../nu-utils", version = "0.83.2" } nu-engine = { path = "../nu-engine", version = "0.83.2" } nu-color-config = { path = "../nu-color-config", version = "0.83.2" } nu-ansi-term = "0.49.0" -tabled = { version = "0.12.2", features = ["color"], default-features = false } +tabled = { version = "0.14.0", features = ["color"], default-features = false } [dev-dependencies] # nu-test-support = { path="../nu-test-support", version = "0.83.2" } diff --git a/crates/nu-table/examples/table_demo.rs b/crates/nu-table/examples/table_demo.rs index 216041100..c36f5d78d 100644 --- a/crates/nu-table/examples/table_demo.rs +++ b/crates/nu-table/examples/table_demo.rs @@ -1,6 +1,6 @@ use nu_ansi_term::{Color, Style}; use nu_color_config::TextStyle; -use nu_table::{NuTable, TableConfig, TableTheme}; +use nu_table::{NuTable, NuTableConfig, TableTheme}; use tabled::grid::records::vec_records::CellInfo; fn main() { @@ -29,8 +29,11 @@ fn main() { table.set_data_style(TextStyle::basic_left()); table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue))); - let theme = TableTheme::rounded(); - let table_cfg = TableConfig::new().theme(theme).with_header(true); + let table_cfg = NuTableConfig { + theme: TableTheme::rounded(), + with_header: true, + ..Default::default() + }; let output_table = table .draw(table_cfg, width) diff --git a/crates/nu-table/src/common.rs b/crates/nu-table/src/common.rs new file mode 100644 index 000000000..257c66749 --- /dev/null +++ b/crates/nu-table/src/common.rs @@ -0,0 +1,175 @@ +use nu_color_config::{Alignment, StyleComputer, TextStyle}; +use nu_protocol::TrimStrategy; +use nu_protocol::{Config, FooterMode, ShellError, Span, Value}; + +use crate::{clean_charset, string_wrap, NuTableConfig, TableOutput, TableTheme}; + +pub type NuText = (String, TextStyle); +pub type TableResult = Result, ShellError>; +pub type StringResult = Result, ShellError>; + +pub const INDEX_COLUMN_NAME: &str = "index"; + +pub fn create_nu_table_config( + config: &Config, + comp: &StyleComputer, + out: &TableOutput, + expand: bool, +) -> NuTableConfig { + NuTableConfig { + theme: load_theme_from_config(config), + with_footer: with_footer(config, out.with_header, out.table.count_rows()), + with_index: out.with_index, + with_header: out.with_header, + split_color: Some(lookup_separator_color(comp)), + trim: config.trim_strategy.clone(), + header_on_border: config.table_move_header, + expand, + } +} + +pub fn nu_value_to_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText { + let float_precision = cfg.float_precision as usize; + let text = val.into_abbreviated_string(cfg); + make_styled_string(style, text, Some(val), float_precision) +} + +pub fn nu_value_to_string_clean(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText { + let (text, style) = nu_value_to_string(val, cfg, style); + let text = clean_charset(&text); + (text, style) +} + +pub fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) { + make_styled_string(style_computer, String::from("❎"), None, 0) +} + +pub fn wrap_text(text: &str, width: usize, config: &Config) -> String { + string_wrap(text, width, is_cfg_trim_keep_words(config)) +} + +pub fn get_header_style(style_computer: &StyleComputer) -> TextStyle { + TextStyle::with_style( + Alignment::Center, + style_computer.compute("header", &Value::string("", Span::unknown())), + ) +} + +pub fn get_index_style(style_computer: &StyleComputer) -> TextStyle { + TextStyle::with_style( + Alignment::Right, + style_computer.compute("row_index", &Value::string("", Span::unknown())), + ) +} + +pub fn get_value_style(value: &Value, config: &Config, style_computer: &StyleComputer) -> NuText { + match value { + // Float precision is required here. + Value::Float { val, .. } => ( + format!("{:.prec$}", val, prec = config.float_precision as usize), + style_computer.style_primitive(value), + ), + _ => ( + value.into_abbreviated_string(config), + style_computer.style_primitive(value), + ), + } +} + +pub fn get_empty_style(style_computer: &StyleComputer) -> NuText { + ( + String::from("❎"), + TextStyle::with_style( + Alignment::Right, + style_computer.compute("empty", &Value::nothing(Span::unknown())), + ), + ) +} + +fn make_styled_string( + style_computer: &StyleComputer, + text: String, + value: Option<&Value>, // None represents table holes. + float_precision: usize, +) -> NuText { + match value { + Some(value) => { + match value { + Value::Float { .. } => { + // set dynamic precision from config + let precise_number = match convert_with_precision(&text, float_precision) { + Ok(num) => num, + Err(e) => e.to_string(), + }; + (precise_number, style_computer.style_primitive(value)) + } + _ => (text, style_computer.style_primitive(value)), + } + } + None => { + // Though holes are not the same as null, the closure for "empty" is passed a null anyway. + ( + text, + TextStyle::with_style( + Alignment::Center, + style_computer.compute("empty", &Value::nothing(Span::unknown())), + ), + ) + } + } +} + +fn convert_with_precision(val: &str, precision: usize) -> Result { + // vall will always be a f64 so convert it with precision formatting + let val_float = match val.trim().parse::() { + Ok(f) => f, + Err(e) => { + return Err(ShellError::GenericError( + format!("error converting string [{}] to f64", &val), + "".to_string(), + None, + Some(e.to_string()), + Vec::new(), + )); + } + }; + Ok(format!("{val_float:.precision$}")) +} + +fn is_cfg_trim_keep_words(config: &Config) -> bool { + matches!( + config.trim_strategy, + TrimStrategy::Wrap { + try_to_keep_words: true + } + ) +} + +pub fn load_theme_from_config(config: &Config) -> TableTheme { + match config.table_mode.as_str() { + "basic" => TableTheme::basic(), + "thin" => TableTheme::thin(), + "light" => TableTheme::light(), + "compact" => TableTheme::compact(), + "with_love" => TableTheme::with_love(), + "compact_double" => TableTheme::compact_double(), + "rounded" => TableTheme::rounded(), + "reinforced" => TableTheme::reinforced(), + "heavy" => TableTheme::heavy(), + "none" => TableTheme::none(), + _ => TableTheme::rounded(), + } +} + +fn lookup_separator_color(style_computer: &StyleComputer) -> nu_ansi_term::Style { + style_computer.compute("separator", &Value::nothing(Span::unknown())) +} + +fn with_footer(config: &Config, with_header: bool, count_records: usize) -> bool { + with_header && need_footer(config, count_records as u64) +} + +fn need_footer(config: &Config, count_records: u64) -> bool { + matches!(config.footer_mode, FooterMode::RowCount(limit) if count_records > limit) + || matches!(config.footer_mode, FooterMode::Always) +} diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs index 219fc435e..66cc2a3a8 100644 --- a/crates/nu-table/src/lib.rs +++ b/crates/nu-table/src/lib.rs @@ -4,12 +4,12 @@ mod types; mod unstructured_table; mod util; +pub mod common; + +pub use common::{StringResult, TableResult}; pub use nu_color_config::TextStyle; -pub use table::{Alignments, Cell, NuTable, TableConfig}; +pub use table::{NuTable, NuTableCell, NuTableConfig}; pub use table_theme::TableTheme; -pub use types::{ - clean_charset, value_to_clean_styled_string, value_to_styled_string, BuildConfig, - CollapsedTable, ExpandedTable, JustTable, NuText, StringResult, TableOutput, TableResult, -}; +pub use types::{CollapsedTable, ExpandedTable, JustTable, TableOpts, TableOutput}; pub use unstructured_table::UnstructuredTable; pub use util::*; diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index 153d6de31..2d0ec77ad 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -8,29 +8,32 @@ use tabled::{ builder::Builder, grid::{ color::AnsiColor, + colors::Colors, config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position}, dimension::CompleteDimensionVecRecords, records::{ vec_records::{CellInfo, VecRecords}, - ExactRecords, Records, + ExactRecords, PeekableRecords, Records, Resizable, }, }, settings::{ - formatting::AlignmentStrategy, object::Segment, peaker::Peaker, Color, Modify, Settings, - TableOption, Width, + formatting::AlignmentStrategy, object::Segment, peaker::Peaker, themes::ColumnNames, Color, + Modify, Settings, TableOption, Width, }, Table, }; -/// Table represent a table view. +/// NuTable is a table rendering implementation. #[derive(Debug, Clone)] pub struct NuTable { - data: Data, + data: NuTableData, styles: Styles, alignments: Alignments, - size: (usize, usize), } +type NuTableData = VecRecords; +pub type NuTableCell = CellInfo; + #[derive(Debug, Default, Clone)] struct Styles { index: AnsiColor<'static>, @@ -39,27 +42,39 @@ struct Styles { data_is_set: bool, } -type Data = VecRecords; -pub type Cell = CellInfo; +#[derive(Debug, Clone)] +struct Alignments { + data: AlignmentHorizontal, + index: AlignmentHorizontal, + header: AlignmentHorizontal, + columns: HashMap, + cells: HashMap, +} impl NuTable { /// Creates an empty [Table] instance. pub fn new(count_rows: usize, count_columns: usize) -> Self { - let data = VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]); Self { - data, - size: (count_rows, count_columns), + data: VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]), styles: Styles::default(), - alignments: Alignments::default(), + alignments: Alignments { + data: AlignmentHorizontal::Left, + index: AlignmentHorizontal::Right, + header: AlignmentHorizontal::Center, + columns: HashMap::default(), + cells: HashMap::default(), + }, } } + /// Return amount of rows. pub fn count_rows(&self) -> usize { - self.size.0 + self.data.count_rows() } + /// Return amount of columns. pub fn count_columns(&self) -> usize { - self.size.1 + self.data.count_columns() } pub fn insert(&mut self, pos: Position, text: String) { @@ -79,7 +94,7 @@ impl NuTable { } } - pub fn set_cell_style(&mut self, pos: Position, style: TextStyle) { + pub fn insert_style(&mut self, pos: Position, style: TextStyle) { if let Some(style) = style.color_style { let style = AnsiColor::from(convert_style(style)); self.styles.data.insert(Entity::Cell(pos.0, pos.1), style); @@ -123,12 +138,12 @@ impl NuTable { /// Converts a table to a String. /// /// It returns None in case where table cannot be fit to a terminal width. - pub fn draw(self, config: TableConfig, termwidth: usize) -> Option { + pub fn draw(self, config: NuTableConfig, termwidth: usize) -> Option { build_table(self.data, config, self.alignments, self.styles, termwidth) } /// Return a total table width. - pub fn total_width(&self, config: &TableConfig) -> usize { + pub fn total_width(&self, config: &NuTableConfig) -> usize { let config = get_config(&config.theme, false, None); let widths = build_width(&self.data); get_total_width2(&widths, &config) @@ -137,107 +152,43 @@ impl NuTable { impl From>>> for NuTable { fn from(value: Vec>>) -> Self { - let data = VecRecords::new(value); - let size = (data.count_rows(), data.count_columns()); - Self { - data, - size, - alignments: Alignments::default(), - styles: Styles::default(), - } + let mut nutable = Self::new(0, 0); + nutable.data = VecRecords::new(value); + + nutable } } #[derive(Debug, Clone)] -pub struct TableConfig { - theme: TableTheme, - trim: TrimStrategy, - split_color: Option