From 7e096e61d7994aa51ef20fd253486b0cb071ccb3 Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Fri, 4 Aug 2023 21:50:47 +0300 Subject: [PATCH] Add an option to set header on border (style) (#9920) fix #9796 Sorry that you've had the issues. I've actually encountered them yesterday too (seems like they have appeared after some refactoring in the middle) but was not able to fix that rapid. Created a bunch of tests. cc: @fdncred Note: This option will be certainly slower then a default ones. (could be fixed but ... maybe later). Maybe it shall be cited somewhere. PS: Haven't tested on a wrapped/expanded tables. --------- Signed-off-by: Maxim Zhiburt Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com> --- Cargo.lock | 8 +- crates/nu-command/Cargo.toml | 2 +- crates/nu-command/src/viewers/table.rs | 124 +++---- crates/nu-command/tests/commands/table.rs | 176 ++++++++++ crates/nu-explore/src/nu_common/table.rs | 33 +- crates/nu-protocol/src/config.rs | 5 + crates/nu-table/Cargo.toml | 2 +- crates/nu-table/examples/table_demo.rs | 9 +- crates/nu-table/src/common.rs | 175 +++++++++ crates/nu-table/src/lib.rs | 10 +- crates/nu-table/src/table.rs | 410 +++++++++++++++------- crates/nu-table/src/table_theme.rs | 4 + crates/nu-table/src/types/collapse.rs | 26 +- crates/nu-table/src/types/expanded.rs | 389 ++++++++++---------- crates/nu-table/src/types/general.rs | 104 ++---- crates/nu-table/src/types/mod.rs | 207 ++--------- crates/nu-table/src/util.rs | 5 + crates/nu-table/tests/common.rs | 8 +- crates/nu-table/tests/constrains.rs | 78 ++-- crates/nu-table/tests/expand.rs | 12 +- crates/nu-table/tests/style.rs | 20 +- 21 files changed, 1056 insertions(+), 751 deletions(-) create mode 100644 crates/nu-table/src/common.rs diff --git a/Cargo.lock b/Cargo.lock index 61eaf53b28..03dc7890d5 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 7bc23b0b10..b8b2cd3dca 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 b41e290ded..dd7ddb4235 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 aec59a5fb3..826cbae94d 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 5286216d3f..345e8a1113 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 c11c0f5b3b..03c03c0beb 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 83e8117bdf..6c12d6009c 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 2160411006..c36f5d78dc 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 0000000000..257c66749c --- /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 219fc435e9..66cc2a3a82 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 153d6de31d..2d0ec77ad0 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