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 <zhiburt@gmail.com>
Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
Maxim Zhiburt 2023-08-04 21:50:47 +03:00 committed by GitHub
parent 9d7a1097f2
commit 7e096e61d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1056 additions and 751 deletions

8
Cargo.lock generated
View File

@ -3289,9 +3289,9 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]] [[package]]
name = "papergrid" name = "papergrid"
version = "0.9.1" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae7891b22598926e4398790c8fe6447930c72a67d36d983a49d6ce682ce83290" checksum = "a2ccbe15f2b6db62f9a9871642746427e297b0ceb85f9a7f1ee5ff47d184d0c8"
dependencies = [ dependencies = [
"ansi-str", "ansi-str",
"ansitok", "ansitok",
@ -4947,9 +4947,9 @@ dependencies = [
[[package]] [[package]]
name = "tabled" name = "tabled"
version = "0.12.2" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce69a5028cd9576063ec1f48edb2c75339fd835e6094ef3e05b3a079bf594a6" checksum = "dfe9c3632da101aba5131ed63f9eed38665f8b3c68703a6bb18124835c1a5d22"
dependencies = [ dependencies = [
"ansi-str", "ansi-str",
"ansitok", "ansitok",

View File

@ -84,7 +84,7 @@ serde_yaml = "0.9"
sha2 = "0.10" sha2 = "0.10"
sqlparser = { version = "0.34", features = ["serde"], optional = true } sqlparser = { version = "0.34", features = ["serde"], optional = true }
sysinfo = "0.29" 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" terminal_size = "0.2"
titlecase = "2.0" titlecase = "2.0"
toml = "0.7" toml = "0.7"

View File

@ -6,12 +6,13 @@ use nu_engine::{env::get_config, env_to_string, CallExt};
use nu_protocol::{ use nu_protocol::{
ast::Call, ast::Call,
engine::{Command, EngineState, Stack}, 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, PipelineMetadata, RawStream, ShellError, Signature, Span, SyntaxShape, Type, Value,
}; };
use nu_table::common::create_nu_table_config;
use nu_table::{ use nu_table::{
BuildConfig, Cell, CollapsedTable, ExpandedTable, JustTable, NuTable, StringResult, CollapsedTable, ExpandedTable, JustTable, NuTable, NuTableCell, StringResult, TableOpts,
TableConfig, TableOutput, TableTheme, TableOutput,
}; };
use nu_utils::get_ls_colors; use nu_utils::get_ls_colors;
use std::sync::Arc; use std::sync::Arc;
@ -361,8 +362,8 @@ fn handle_record(
let result = if cols.is_empty() { let result = if cols.is_empty() {
create_empty_placeholder("record", term_width, engine_state, stack) create_empty_placeholder("record", term_width, engine_state, stack)
} else { } else {
let opts = BuildConfig::new(ctrlc, config, style_computer, span, term_width); let opts = TableOpts::new(config, style_computer, ctrlc, span, 0, term_width);
let result = build_table_kv(cols, vals, table_view, opts)?; let result = build_table_kv(cols, vals, table_view, opts, span)?;
match result { match result {
Some(output) => maybe_strip_color(output, config), Some(output) => maybe_strip_color(output, config),
None => report_unsuccessful_output(ctrlc1, term_width), None => report_unsuccessful_output(ctrlc1, term_width),
@ -391,7 +392,8 @@ fn build_table_kv(
cols: Vec<String>, cols: Vec<String>,
vals: Vec<Value>, vals: Vec<Value>,
table_view: TableView, table_view: TableView,
opts: BuildConfig<'_>, opts: TableOpts<'_>,
span: Span,
) -> StringResult { ) -> StringResult {
match table_view { match table_view {
TableView::General => JustTable::kv_table(&cols, &vals, opts), 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) ExpandedTable::new(limit, flatten, sep).build_map(&cols, &vals, opts)
} }
TableView::Collapsed => { TableView::Collapsed => {
let span = opts.span();
let value = Value::Record { cols, vals, span }; let value = Value::Record { cols, vals, span };
CollapsedTable::build(value, opts) CollapsedTable::build(value, opts)
} }
@ -414,21 +415,20 @@ fn build_table_kv(
fn build_table_batch( fn build_table_batch(
vals: Vec<Value>, vals: Vec<Value>,
table_view: TableView, table_view: TableView,
row_offset: usize, opts: TableOpts<'_>,
opts: BuildConfig<'_>, span: Span,
) -> StringResult { ) -> StringResult {
match table_view { match table_view {
TableView::General => JustTable::table(&vals, row_offset, opts), TableView::General => JustTable::table(&vals, opts),
TableView::Expanded { TableView::Expanded {
limit, limit,
flatten, flatten,
flatten_separator, flatten_separator,
} => { } => {
let sep = flatten_separator.unwrap_or_else(|| String::from(' ')); 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 => { TableView::Collapsed => {
let span = opts.span();
let value = Value::List { vals, span }; let value = Value::List { vals, span };
CollapsedTable::build(value, opts) CollapsedTable::build(value, opts)
} }
@ -647,20 +647,16 @@ impl PagingTableCreator {
return Ok(None); return Ok(None);
} }
let config = get_config(&self.engine_state, &self.stack); let cfg = get_config(&self.engine_state, &self.stack);
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack);
let term_width = get_width_param(self.width_param); let opts = self.create_table_opts(&cfg, &style_comp);
let ctrlc = self.ctrlc.clone();
let span = self.head;
let opts = BuildConfig::new(ctrlc, &config, &style_computer, span, term_width);
let view = TableView::Expanded { let view = TableView::Expanded {
limit, limit,
flatten, flatten,
flatten_separator, 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<Value>) -> StringResult { fn build_collapsed(&mut self, batch: Vec<Value>) -> StringResult {
@ -668,26 +664,34 @@ impl PagingTableCreator {
return Ok(None); return Ok(None);
} }
let config = get_config(&self.engine_state, &self.stack); let cfg = get_config(&self.engine_state, &self.stack);
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack);
let term_width = get_width_param(self.width_param); let opts = self.create_table_opts(&cfg, &style_comp);
let ctrlc = self.ctrlc.clone();
let span = self.head;
let opts = BuildConfig::new(ctrlc, &config, &style_computer, span, term_width);
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<Value>) -> StringResult { fn build_general(&mut self, batch: Vec<Value>) -> StringResult {
let term_width = get_width_param(self.width_param); let cfg = get_config(&self.engine_state, &self.stack);
let config = get_config(&self.engine_state, &self.stack); let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack);
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); let opts = self.create_table_opts(&cfg, &style_comp);
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);
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( fn render_path_name(
path: &str, path: &str,
config: &Config, 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( fn create_empty_placeholder(
value_type_name: &str, value_type_name: &str,
termwidth: usize, termwidth: usize,
@ -898,14 +858,14 @@ fn create_empty_placeholder(
return String::new(); 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 data = vec![vec![cell]];
let mut table = NuTable::from(data); 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 out = TableOutput::new(table, false, false);
let style_computer = &StyleComputer::from_config(engine_state, stack); 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 out.table
.draw(config, termwidth) .draw(config, termwidth)

View File

@ -2384,3 +2384,179 @@ fn table_index_offset() {
let expected_suffix = actual.out.strip_suffix(suffix); let expected_suffix = actual.out.strip_suffix(suffix);
assert!(expected_suffix.is_some(), "{:?}", actual.out); 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<String> {
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}")
}

View File

@ -1,6 +1,9 @@
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_protocol::{Span, Value}; 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::atomic::AtomicBool;
use std::sync::Arc; use std::sync::Arc;
@ -18,9 +21,9 @@ pub fn try_build_table(
try_build_map(cols, vals, span, style_computer, ctrlc, config) try_build_map(cols, vals, span, style_computer, ctrlc, config)
} }
val if matches!(val, Value::String { .. }) => { 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<Arc<AtomicBool>>, ctrlc: Option<Arc<AtomicBool>>,
config: &NuConfig, config: &NuConfig,
) -> String { ) -> 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); let result = ExpandedTable::new(None, false, String::new()).build_map(&cols, &vals, opts);
match result { match result {
Ok(Some(result)) => result, Ok(Some(result)) => result,
Ok(None) | Err(_) => { 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, span: Span,
style_computer: &StyleComputer, style_computer: &StyleComputer,
) -> String { ) -> String {
let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX); let opts = TableOpts::new(
let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts, 0); config,
style_computer,
ctrlc,
Span::unknown(),
0,
usize::MAX,
);
let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts);
match result { match result {
Ok(Some(out)) => out, Ok(Some(out)) => out,
Ok(None) | Err(_) => { Ok(None) | Err(_) => {
// it means that the list is empty // 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
} }
} }
} }

View File

@ -70,6 +70,7 @@ pub struct Config {
pub external_completer: Option<usize>, pub external_completer: Option<usize>,
pub filesize_metric: bool, pub filesize_metric: bool,
pub table_mode: String, pub table_mode: String,
pub table_move_header: bool,
pub table_show_empty: bool, pub table_show_empty: bool,
pub use_ls_colors: bool, pub use_ls_colors: bool,
pub color_config: HashMap<String, Value>, pub color_config: HashMap<String, Value>,
@ -126,6 +127,7 @@ impl Default for Config {
table_index_mode: TableIndexMode::Always, table_index_mode: TableIndexMode::Always,
table_show_empty: true, table_show_empty: true,
trim_strategy: TRIM_STRATEGY_DEFAULT, trim_strategy: TRIM_STRATEGY_DEFAULT,
table_move_header: false,
datetime_normal_format: None, datetime_normal_format: None,
datetime_table_format: None, datetime_table_format: None,
@ -926,6 +928,9 @@ impl Value {
Value::string(config.table_mode.clone(), span); Value::string(config.table_mode.clone(), span);
} }
} }
"header_on_separator" => {
try_bool!(cols, vals, index, span, table_move_header)
}
"index_mode" => { "index_mode" => {
if let Ok(b) = value.as_string() { if let Ok(b) = value.as_string() {
let val_str = b.to_lowercase(); let val_str = b.to_lowercase();

View File

@ -16,7 +16,7 @@ nu-utils = { path = "../nu-utils", version = "0.83.2" }
nu-engine = { path = "../nu-engine", 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-color-config = { path = "../nu-color-config", version = "0.83.2" }
nu-ansi-term = "0.49.0" 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] [dev-dependencies]
# nu-test-support = { path="../nu-test-support", version = "0.83.2" } # nu-test-support = { path="../nu-test-support", version = "0.83.2" }

View File

@ -1,6 +1,6 @@
use nu_ansi_term::{Color, Style}; use nu_ansi_term::{Color, Style};
use nu_color_config::TextStyle; use nu_color_config::TextStyle;
use nu_table::{NuTable, TableConfig, TableTheme}; use nu_table::{NuTable, NuTableConfig, TableTheme};
use tabled::grid::records::vec_records::CellInfo; use tabled::grid::records::vec_records::CellInfo;
fn main() { fn main() {
@ -29,8 +29,11 @@ fn main() {
table.set_data_style(TextStyle::basic_left()); table.set_data_style(TextStyle::basic_left());
table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue))); table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue)));
let theme = TableTheme::rounded(); let table_cfg = NuTableConfig {
let table_cfg = TableConfig::new().theme(theme).with_header(true); theme: TableTheme::rounded(),
with_header: true,
..Default::default()
};
let output_table = table let output_table = table
.draw(table_cfg, width) .draw(table_cfg, width)

View File

@ -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<Option<TableOutput>, ShellError>;
pub type StringResult = Result<Option<String>, 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<String, ShellError> {
// vall will always be a f64 so convert it with precision formatting
let val_float = match val.trim().parse::<f64>() {
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)
}

View File

@ -4,12 +4,12 @@ mod types;
mod unstructured_table; mod unstructured_table;
mod util; mod util;
pub mod common;
pub use common::{StringResult, TableResult};
pub use nu_color_config::TextStyle; 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 table_theme::TableTheme;
pub use types::{ pub use types::{CollapsedTable, ExpandedTable, JustTable, TableOpts, TableOutput};
clean_charset, value_to_clean_styled_string, value_to_styled_string, BuildConfig,
CollapsedTable, ExpandedTable, JustTable, NuText, StringResult, TableOutput, TableResult,
};
pub use unstructured_table::UnstructuredTable; pub use unstructured_table::UnstructuredTable;
pub use util::*; pub use util::*;

View File

@ -8,29 +8,32 @@ use tabled::{
builder::Builder, builder::Builder,
grid::{ grid::{
color::AnsiColor, color::AnsiColor,
colors::Colors,
config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position}, config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position},
dimension::CompleteDimensionVecRecords, dimension::CompleteDimensionVecRecords,
records::{ records::{
vec_records::{CellInfo, VecRecords}, vec_records::{CellInfo, VecRecords},
ExactRecords, Records, ExactRecords, PeekableRecords, Records, Resizable,
}, },
}, },
settings::{ settings::{
formatting::AlignmentStrategy, object::Segment, peaker::Peaker, Color, Modify, Settings, formatting::AlignmentStrategy, object::Segment, peaker::Peaker, themes::ColumnNames, Color,
TableOption, Width, Modify, Settings, TableOption, Width,
}, },
Table, Table,
}; };
/// Table represent a table view. /// NuTable is a table rendering implementation.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NuTable { pub struct NuTable {
data: Data, data: NuTableData,
styles: Styles, styles: Styles,
alignments: Alignments, alignments: Alignments,
size: (usize, usize),
} }
type NuTableData = VecRecords<NuTableCell>;
pub type NuTableCell = CellInfo<String>;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
struct Styles { struct Styles {
index: AnsiColor<'static>, index: AnsiColor<'static>,
@ -39,27 +42,39 @@ struct Styles {
data_is_set: bool, data_is_set: bool,
} }
type Data = VecRecords<Cell>; #[derive(Debug, Clone)]
pub type Cell = CellInfo<String>; struct Alignments {
data: AlignmentHorizontal,
index: AlignmentHorizontal,
header: AlignmentHorizontal,
columns: HashMap<usize, AlignmentHorizontal>,
cells: HashMap<Position, AlignmentHorizontal>,
}
impl NuTable { impl NuTable {
/// Creates an empty [Table] instance. /// Creates an empty [Table] instance.
pub fn new(count_rows: usize, count_columns: usize) -> Self { pub fn new(count_rows: usize, count_columns: usize) -> Self {
let data = VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]);
Self { Self {
data, data: VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]),
size: (count_rows, count_columns),
styles: Styles::default(), 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 { pub fn count_rows(&self) -> usize {
self.size.0 self.data.count_rows()
} }
/// Return amount of columns.
pub fn count_columns(&self) -> usize { pub fn count_columns(&self) -> usize {
self.size.1 self.data.count_columns()
} }
pub fn insert(&mut self, pos: Position, text: String) { 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 { if let Some(style) = style.color_style {
let style = AnsiColor::from(convert_style(style)); let style = AnsiColor::from(convert_style(style));
self.styles.data.insert(Entity::Cell(pos.0, pos.1), style); self.styles.data.insert(Entity::Cell(pos.0, pos.1), style);
@ -123,12 +138,12 @@ impl NuTable {
/// Converts a table to a String. /// Converts a table to a String.
/// ///
/// It returns None in case where table cannot be fit to a terminal width. /// It returns None in case where table cannot be fit to a terminal width.
pub fn draw(self, config: TableConfig, termwidth: usize) -> Option<String> { pub fn draw(self, config: NuTableConfig, termwidth: usize) -> Option<String> {
build_table(self.data, config, self.alignments, self.styles, termwidth) build_table(self.data, config, self.alignments, self.styles, termwidth)
} }
/// Return a total table width. /// 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 config = get_config(&config.theme, false, None);
let widths = build_width(&self.data); let widths = build_width(&self.data);
get_total_width2(&widths, &config) get_total_width2(&widths, &config)
@ -137,107 +152,43 @@ impl NuTable {
impl From<Vec<Vec<CellInfo<String>>>> for NuTable { impl From<Vec<Vec<CellInfo<String>>>> for NuTable {
fn from(value: Vec<Vec<CellInfo<String>>>) -> Self { fn from(value: Vec<Vec<CellInfo<String>>>) -> Self {
let data = VecRecords::new(value); let mut nutable = Self::new(0, 0);
let size = (data.count_rows(), data.count_columns()); nutable.data = VecRecords::new(value);
Self {
data, nutable
size,
alignments: Alignments::default(),
styles: Styles::default(),
}
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableConfig { pub struct NuTableConfig {
theme: TableTheme, pub theme: TableTheme,
trim: TrimStrategy, pub trim: TrimStrategy,
split_color: Option<Style>, pub split_color: Option<Style>,
expand: bool, pub expand: bool,
with_index: bool, pub with_index: bool,
with_header: bool, pub with_header: bool,
with_footer: bool, pub with_footer: bool,
pub header_on_border: bool,
} }
impl TableConfig { impl Default for NuTableConfig {
pub fn new() -> Self { fn default() -> Self {
Self { Self {
theme: TableTheme::basic(), theme: TableTheme::basic(),
trim: TrimStrategy::truncate(None),
with_header: false, with_header: false,
with_index: false, with_index: false,
with_footer: false, with_footer: false,
expand: false, expand: false,
trim: TrimStrategy::truncate(None),
split_color: None, split_color: None,
} header_on_border: false,
}
pub fn expand(mut self, on: bool) -> Self {
self.expand = on;
self
}
pub fn trim(mut self, strategy: TrimStrategy) -> Self {
self.trim = strategy;
self
}
pub fn line_style(mut self, color: Style) -> Self {
self.split_color = Some(color);
self
}
pub fn with_header(mut self, on: bool) -> Self {
self.with_header = on;
self
}
pub fn with_footer(mut self, on: bool) -> Self {
self.with_footer = on;
self
}
pub fn with_index(mut self, on: bool) -> Self {
self.with_index = on;
self
}
pub fn theme(mut self, theme: TableTheme) -> Self {
self.theme = theme;
self
}
}
impl Default for TableConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Alignments {
data: AlignmentHorizontal,
index: AlignmentHorizontal,
header: AlignmentHorizontal,
columns: HashMap<usize, AlignmentHorizontal>,
cells: HashMap<Position, AlignmentHorizontal>,
}
impl Default for Alignments {
fn default() -> Self {
Self {
data: AlignmentHorizontal::Left,
index: AlignmentHorizontal::Right,
header: AlignmentHorizontal::Center,
columns: HashMap::default(),
cells: HashMap::default(),
} }
} }
} }
fn build_table( fn build_table(
mut data: Data, mut data: NuTableData,
cfg: TableConfig, cfg: NuTableConfig,
alignments: Alignments, alignments: Alignments,
styles: Styles, styles: Styles,
termwidth: usize, termwidth: usize,
@ -259,28 +210,71 @@ fn build_table(
} }
fn draw_table( fn draw_table(
data: Data, data: NuTableData,
alignments: Alignments, alignments: Alignments,
styles: Styles, styles: Styles,
widths: Vec<usize>, widths: Vec<usize>,
cfg: TableConfig, cfg: NuTableConfig,
termwidth: usize, termwidth: usize,
) -> Option<String> { ) -> Option<String> {
let with_index = cfg.with_index;
let with_header = cfg.with_header && data.count_rows() > 1;
let with_footer = with_header && cfg.with_footer;
let sep_color = cfg.split_color;
let border_header = cfg.header_on_border;
let data: Vec<Vec<_>> = data.into(); let data: Vec<Vec<_>> = data.into();
let mut table = Builder::from(data).build(); let mut table = Builder::from(data).build();
let with_footer = cfg.with_footer;
let with_index = cfg.with_index;
let with_header = cfg.with_header && table.count_rows() > 1;
let sep_color = cfg.split_color;
load_theme(&mut table, &cfg.theme, with_footer, with_header, sep_color); load_theme(&mut table, &cfg.theme, with_footer, with_header, sep_color);
align_table(&mut table, alignments, with_index, with_header, with_footer); align_table(&mut table, alignments, with_index, with_header, with_footer);
colorize_table(&mut table, styles, with_index, with_header, with_footer); colorize_table(&mut table, styles, with_index, with_header, with_footer);
let total_width = control_table_width(&mut table, cfg, widths, termwidth);
// we need to do it after width control cause we of ColumnNames internals.
if with_header && border_header {
set_border_head(&mut table, with_footer);
}
build_table_with_width_check(table, total_width, termwidth)
}
fn set_border_head(table: &mut Table, with_footer: bool) {
let count_rows = table.count_rows();
if with_footer {
table.with(Settings::new(
HeaderMove((0, 0), true),
HeaderMove((count_rows - 1 - 1, count_rows - 1), false),
));
} else {
table.with(HeaderMove((0, 0), true));
}
}
fn build_table_with_width_check(
table: Table,
total_width: usize,
termwidth: usize,
) -> Option<String> {
if total_width > termwidth {
None
} else {
let content = table.to_string();
Some(content)
}
}
fn control_table_width(
table: &mut Table,
cfg: NuTableConfig,
widths: Vec<usize>,
termwidth: usize,
) -> usize {
let total_width = get_total_width2(&widths, table.get_config()); let total_width = get_total_width2(&widths, table.get_config());
let total_width = if total_width > termwidth { if total_width > termwidth {
table_trim_columns(&mut table, widths, termwidth, &cfg.trim); table_trim_columns(table, widths, termwidth, &cfg.trim);
table.total_width() table.total_width()
} else if cfg.expand && termwidth > total_width { } else if cfg.expand && termwidth > total_width {
table.with(Settings::new( table.with(Settings::new(
@ -291,13 +285,6 @@ fn draw_table(
termwidth termwidth
} else { } else {
total_width total_width
};
if total_width > termwidth {
None
} else {
let content = table.to_string();
Some(content)
} }
} }
@ -429,7 +416,11 @@ fn table_trim_columns(
} }
} }
fn maybe_truncate_columns(data: &mut Data, theme: &TableTheme, termwidth: usize) -> Vec<usize> { fn maybe_truncate_columns(
data: &mut NuTableData,
theme: &TableTheme,
termwidth: usize,
) -> Vec<usize> {
const TERMWIDTH_THRESHOLD: usize = 120; const TERMWIDTH_THRESHOLD: usize = 120;
let truncate = if termwidth > TERMWIDTH_THRESHOLD { let truncate = if termwidth > TERMWIDTH_THRESHOLD {
@ -443,7 +434,7 @@ fn maybe_truncate_columns(data: &mut Data, theme: &TableTheme, termwidth: usize)
// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. // VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE.
fn truncate_columns_by_content( fn truncate_columns_by_content(
data: &mut Data, data: &mut NuTableData,
theme: &TableTheme, theme: &TableTheme,
termwidth: usize, termwidth: usize,
) -> Vec<usize> { ) -> Vec<usize> {
@ -522,7 +513,7 @@ fn truncate_columns_by_content(
// VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE // VERSION where we are showing AS MANY COLUMNS AS POSSIBLE but as a side affect they MIGHT CONTAIN AS LITTLE CONTENT AS POSSIBLE
fn truncate_columns_by_columns( fn truncate_columns_by_columns(
data: &mut Data, data: &mut NuTableData,
theme: &TableTheme, theme: &TableTheme,
termwidth: usize, termwidth: usize,
) -> Vec<usize> { ) -> Vec<usize> {
@ -620,7 +611,7 @@ fn get_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> Co
table.get_config().clone() table.get_config().clone()
} }
fn push_empty_column(data: &mut Data) { fn push_empty_column(data: &mut NuTableData) {
let records = std::mem::take(data); let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into(); let mut inner: Vec<Vec<_>> = records.into();
@ -632,7 +623,7 @@ fn push_empty_column(data: &mut Data) {
*data = VecRecords::new(inner); *data = VecRecords::new(inner);
} }
fn duplicate_row(data: &mut Data, row: usize) { fn duplicate_row(data: &mut NuTableData, row: usize) {
let records = std::mem::take(data); let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into(); let mut inner: Vec<Vec<_>> = records.into();
@ -642,7 +633,7 @@ fn duplicate_row(data: &mut Data, row: usize) {
*data = VecRecords::new(inner); *data = VecRecords::new(inner);
} }
fn truncate_columns(data: &mut Data, count: usize) { fn truncate_columns(data: &mut NuTableData, count: usize) {
let records = std::mem::take(data); let records = std::mem::take(data);
let mut inner: Vec<Vec<_>> = records.into(); let mut inner: Vec<Vec<_>> = records.into();
@ -697,3 +688,178 @@ fn build_width(records: &VecRecords<CellInfo<String>>) -> Vec<usize> {
widths widths
} }
struct HeaderMove((usize, usize), bool);
impl TableOption<VecRecords<CellInfo<String>>, CompleteDimensionVecRecords<'_>, ColoredConfig>
for HeaderMove
{
fn change(
self,
recs: &mut VecRecords<CellInfo<String>>,
cfg: &mut ColoredConfig,
dims: &mut CompleteDimensionVecRecords<'_>,
) {
let (row, line) = self.0;
if self.1 {
move_header_on_next(recs, cfg, dims, row, line);
} else {
move_header_on_prev(recs, cfg, dims, row, line);
}
}
}
fn move_header_on_next(
recs: &mut VecRecords<CellInfo<String>>,
cfg: &mut ColoredConfig,
dims: &mut CompleteDimensionVecRecords<'_>,
row: usize,
line: usize,
) {
let count_rows = recs.count_rows();
let count_columns = recs.count_columns();
let has_line = cfg.has_horizontal(line, count_rows);
let has_next_line = cfg.has_horizontal(line + 1, count_rows);
let align = *cfg.get_alignment_horizontal(Entity::Row(row));
let color = cfg
.get_colors()
.get_color((row, 0))
.cloned()
.map(Color::from);
if !has_line && !has_next_line {
return;
}
if !has_line {
let head = remove_row(recs, row);
let count_rows = recs.count_rows();
set_column_names(recs, cfg, dims, head, line, align, color);
shift_alignments_down(cfg, row, count_rows, count_columns);
shift_colors_down(cfg, row, count_rows, count_columns);
shift_lines_up(cfg, count_rows, &[line + 1]);
shift_lines_up(cfg, count_rows, &[count_rows]);
return;
}
let head = remove_row(recs, row);
let count_rows = recs.count_rows();
set_column_names(recs, cfg, dims, head, line, align, color);
shift_alignments_down(cfg, row, count_rows, count_columns);
shift_colors_down(cfg, row, count_rows, count_columns);
remove_lines(cfg, count_rows, &[line + 1]);
shift_lines_up(cfg, count_rows, &[count_rows]);
}
fn move_header_on_prev(
recs: &mut VecRecords<CellInfo<String>>,
cfg: &mut ColoredConfig,
dims: &mut CompleteDimensionVecRecords<'_>,
row: usize,
line: usize,
) {
let count_rows = recs.count_rows();
let count_columns = recs.count_columns();
let has_line = cfg.has_horizontal(line, count_rows);
let has_prev_line = cfg.has_horizontal(line - 1, count_rows);
let align = *cfg.get_alignment_horizontal(Entity::Row(row));
let color = cfg
.get_colors()
.get_color((row, 0))
.cloned()
.map(Color::from);
if !has_line && !has_prev_line {
return;
}
if !has_line {
let head = remove_row(recs, row);
// shift_lines_down(table, &[line - 1]);
set_column_names(recs, cfg, dims, head, line - 1, align, color);
return;
}
let head = remove_row(recs, row);
let count_rows = count_rows - 1;
set_column_names(recs, cfg, dims, head, line - 1, align, color);
shift_alignments_down(cfg, row, count_rows, count_columns);
shift_colors_down(cfg, row, count_rows, count_columns);
remove_lines(cfg, count_rows, &[line - 1]);
}
fn remove_lines(cfg: &mut ColoredConfig, count_rows: usize, line: &[usize]) {
for &line in line {
cfg.remove_horizontal_line(line, count_rows)
}
}
fn shift_alignments_down(
cfg: &mut ColoredConfig,
row: usize,
count_rows: usize,
count_columns: usize,
) {
for row in row..count_rows {
for col in 0..count_columns {
let pos = (row + 1, col).into();
let posn = (row, col).into();
let align = *cfg.get_alignment_horizontal(pos);
cfg.set_alignment_horizontal(posn, align);
}
let align = *cfg.get_alignment_horizontal(Entity::Row(row + 1));
cfg.set_alignment_horizontal(Entity::Row(row), align);
}
}
fn shift_colors_down(cfg: &mut ColoredConfig, row: usize, count_rows: usize, count_columns: usize) {
for row in row..count_rows {
for col in 0..count_columns {
let pos = (row + 1, col);
let posn = (row, col).into();
let color = cfg.get_colors().get_color(pos).cloned();
if let Some(color) = color {
cfg.set_color(posn, color);
}
}
}
}
fn shift_lines_up(cfg: &mut ColoredConfig, count_rows: usize, lines: &[usize]) {
for &i in lines {
let line = cfg.get_horizontal_line(i).cloned();
if let Some(line) = line {
cfg.insert_horizontal_line(i - 1, line);
cfg.remove_horizontal_line(i, count_rows);
}
}
}
fn set_column_names(
records: &mut VecRecords<CellInfo<String>>,
cfg: &mut ColoredConfig,
dims: &mut CompleteDimensionVecRecords<'_>,
head: Vec<String>,
line: usize,
align: AlignmentHorizontal,
color: Option<Color>,
) {
let mut names = ColumnNames::new(head).set_line(line).set_alignment(align);
if let Some(color) = color {
names = names.set_color(color);
}
ColumnNames::change(names, records, cfg, dims)
}
fn remove_row(recs: &mut VecRecords<CellInfo<String>>, row: usize) -> Vec<String> {
let count_columns = recs.count_columns();
let columns = (0..count_columns)
.map(|column| recs.get_text((row, column)).to_owned())
.collect::<Vec<_>>();
recs.remove_row(row);
columns
}

View File

@ -191,6 +191,10 @@ impl TableTheme {
self.has_inner self.has_inner
} }
pub fn has_horizontals(&self) -> bool {
self.full_theme.get_borders().has_horizontal()
}
pub fn get_theme_full(&self) -> RawStyle { pub fn get_theme_full(&self) -> RawStyle {
self.full_theme.clone() self.full_theme.clone()
} }

View File

@ -3,16 +3,17 @@ use nu_protocol::{Config, Span, Value};
use crate::UnstructuredTable; use crate::UnstructuredTable;
use super::{ use crate::common::nu_value_to_string_clean;
clean_charset, general::BuildConfig, get_index_style, load_theme_from_config, use crate::{
value_to_styled_string, StringResult, common::{get_index_style, load_theme_from_config},
StringResult, TableOpts,
}; };
pub struct CollapsedTable; pub struct CollapsedTable;
impl CollapsedTable { impl CollapsedTable {
pub fn build(value: Value, opts: BuildConfig<'_>) -> StringResult { pub fn build(value: Value, opts: TableOpts<'_>) -> StringResult {
collapsed_table(value, opts.config, opts.term_width, opts.style_computer) collapsed_table(value, opts.config, opts.width, opts.style_computer)
} }
} }
@ -56,20 +57,7 @@ fn colorize_value(value: &mut Value, config: &Config, style_computer: &StyleComp
} }
} }
value => { value => {
let (text, style) = value_to_styled_string(value, config, style_computer); let (text, style) = nu_value_to_string_clean(value, config, style_computer);
let is_string = matches!(value, Value::String { .. });
if is_string {
let mut text = clean_charset(&text);
if let Some(color) = style.color_style {
text = color.paint(text).to_string();
}
let span = value.span().unwrap_or(Span::unknown());
*value = Value::string(text, span);
return;
}
if let Some(color) = style.color_style { if let Some(color) = style.color_style {
let text = color.paint(text).to_string(); let text = color.paint(text).to_string();
let span = value.span().unwrap_or(Span::unknown()); let span = value.span().unwrap_or(Span::unknown());

View File

@ -1,17 +1,18 @@
use std::cmp::max;
use std::collections::HashMap;
use nu_color_config::{Alignment, StyleComputer, TextStyle}; use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_engine::column::get_columns; use nu_engine::column::get_columns;
use nu_protocol::{ast::PathMember, Config, Span, TableIndexMode, Value}; use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value};
use std::collections::HashMap; use tabled::grid::config::Position;
use std::sync::Arc;
use std::{cmp::max, sync::atomic::AtomicBool};
use crate::{string_width, Cell, NuTable}; use crate::{
common::{
use super::{clean_charset, value_to_clean_styled_string}; create_nu_table_config, error_sign, get_header_style, get_index_style,
use super::{ load_theme_from_config, nu_value_to_string, nu_value_to_string_clean, wrap_text, NuText,
create_table_config, error_sign, general::BuildConfig, get_header_style, get_index_style, StringResult, TableResult, INDEX_COLUMN_NAME,
load_theme_from_config, set_data_styles, value_to_styled_string, wrap_text, NuText, },
StringResult, TableOutput, TableResult, INDEX_COLUMN_NAME, string_width, NuTable, NuTableCell, TableOpts, TableOutput,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -30,69 +31,35 @@ impl ExpandedTable {
} }
} }
pub fn build_value(&self, item: &Value, opts: BuildConfig<'_>) -> NuText { pub fn build_value(self, item: &Value, opts: TableOpts<'_>) -> NuText {
let opts = Options { expanded_table_entry2(item, Cfg { opts, format: self })
ctrlc: opts.ctrlc,
config: opts.config,
style_computer: opts.style_computer,
available_width: opts.term_width,
span: opts.span,
format: self.clone(),
};
expanded_table_entry2(item, opts)
} }
pub fn build_map( pub fn build_map(self, cols: &[String], vals: &[Value], opts: TableOpts<'_>) -> StringResult {
&self, expanded_table_kv(cols, vals, Cfg { opts, format: self })
cols: &[String],
vals: &[Value],
opts: BuildConfig<'_>,
) -> StringResult {
let opts = Options {
ctrlc: opts.ctrlc,
config: opts.config,
style_computer: opts.style_computer,
available_width: opts.term_width,
span: opts.span,
format: self.clone(),
};
expanded_table_kv(cols, vals, opts)
} }
pub fn build_list( pub fn build_list(self, vals: &[Value], opts: TableOpts<'_>) -> StringResult {
&self, let cfg = Cfg {
vals: &[Value], opts: opts.clone(),
opts: BuildConfig<'_>, format: self,
row_offset: usize,
) -> StringResult {
let opts1 = Options {
ctrlc: opts.ctrlc,
config: opts.config,
style_computer: opts.style_computer,
available_width: opts.term_width,
span: opts.span,
format: self.clone(),
}; };
let out = match expanded_table_list(vals, row_offset, opts1)? { let out = match expanded_table_list(vals, cfg)? {
Some(out) => out, Some(out) => out,
None => return Ok(None), None => return Ok(None),
}; };
maybe_expand_table(out, opts.term_width, opts.config, opts.style_computer) maybe_expand_table(out, opts.width, &opts)
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Options<'a> { struct Cfg<'a> {
ctrlc: Option<Arc<AtomicBool>>, opts: TableOpts<'a>,
config: &'a Config,
style_computer: &'a StyleComputer<'a>,
available_width: usize,
format: ExpandedTable, format: ExpandedTable,
span: Span,
} }
fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> TableResult { fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
const PADDING_SPACE: usize = 2; const PADDING_SPACE: usize = 2;
const SPLIT_LINE_SPACE: usize = 1; const SPLIT_LINE_SPACE: usize = 1;
const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE; const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE;
@ -105,8 +72,9 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
} }
// 2 - split lines // 2 - split lines
let mut available_width = opts let mut available_width = cfg
.available_width .opts
.width
.saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE); .saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE);
if available_width < MIN_CELL_CONTENT_WIDTH { if available_width < MIN_CELL_CONTENT_WIDTH {
return Ok(None); return Ok(None);
@ -114,7 +82,7 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
let headers = get_columns(input); let headers = get_columns(input);
let with_index = match opts.config.table_index_mode { let with_index = match cfg.opts.config.table_index_mode {
TableIndexMode::Always => true, TableIndexMode::Always => true,
TableIndexMode::Never => false, TableIndexMode::Never => false,
TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME), TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME),
@ -134,11 +102,11 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
if with_index { if with_index {
if with_header { if with_header {
data[0].push(Cell::exact(String::from("#"), 1, vec![])); data[0].push(NuTableCell::exact(String::from("#"), 1, vec![]));
} }
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) {
return Ok(None); return Ok(None);
} }
@ -146,14 +114,15 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
return Err(*error.clone()); return Err(*error.clone());
} }
let index = row + row_offset; let index = row + cfg.opts.row_offset;
let text = matches!(item, Value::Record { .. }) let text = matches!(item, Value::Record { .. })
.then(|| lookup_index_value(item, opts.config).unwrap_or_else(|| index.to_string())) .then(|| {
lookup_index_value(item, cfg.opts.config).unwrap_or_else(|| index.to_string())
})
.unwrap_or_else(|| index.to_string()); .unwrap_or_else(|| index.to_string());
let value = Cell::new(text);
let row = row + with_header as usize; let row = row + with_header as usize;
let value = NuTableCell::new(text);
data[row].push(value); data[row].push(value);
} }
@ -177,7 +146,7 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
} }
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) {
return Ok(None); return Ok(None);
} }
@ -185,9 +154,9 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
return Err(*error.clone()); return Err(*error.clone());
} }
let mut oopts = opts.clone(); let mut inner_cfg = cfg.clone();
oopts.available_width = available_width; inner_cfg.opts.width = available_width;
let (mut text, style) = expanded_table_entry2(item, oopts.clone()); let (mut text, style) = expanded_table_entry2(item, inner_cfg);
let value_width = string_width(&text); let value_width = string_width(&text);
if value_width > available_width { if value_width > available_width {
@ -196,16 +165,16 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
// //
// todo: Maybe convert_to_table2_entry could do for strings to not mess caller code? // todo: Maybe convert_to_table2_entry could do for strings to not mess caller code?
text = wrap_text(&text, available_width, opts.config); text = wrap_text(&text, available_width, cfg.opts.config);
} }
let value = Cell::new(text); let value = NuTableCell::new(text);
data[row].push(value); data[row].push(value);
data_styles.insert((row, with_index as usize), style); data_styles.insert((row, with_index as usize), style);
} }
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_index_style(get_index_style(opts.style_computer)); table.set_index_style(get_index_style(cfg.opts.style_computer));
set_data_styles(&mut table, data_styles); set_data_styles(&mut table, data_styles);
return Ok(Some(TableOutput::new(table, false, with_index))); return Ok(Some(TableOutput::new(table, false, with_index)));
@ -264,7 +233,7 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
} }
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) {
return Ok(None); return Ok(None);
} }
@ -272,27 +241,27 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
return Err(*error.clone()); return Err(*error.clone());
} }
let mut oopts = opts.clone(); let mut inner_cfg = cfg.clone();
oopts.available_width = available; inner_cfg.opts.width = available;
let (mut text, style) = expanded_table_entry(item, header.as_str(), oopts); let (mut text, style) = expanded_table_entry(item, header.as_str(), inner_cfg);
let mut value_width = string_width(&text); let mut value_width = string_width(&text);
if value_width > available { if value_width > available {
// it must only happen when a string is produced, so we can safely wrap it. // it must only happen when a string is produced, so we can safely wrap it.
// (it might be string table representation as well) // (it might be string table representation as well)
text = wrap_text(&text, available, opts.config); text = wrap_text(&text, available, cfg.opts.config);
value_width = available; value_width = available;
} }
column_width = max(column_width, value_width); column_width = max(column_width, value_width);
let value = Cell::new(text); let value = NuTableCell::new(text);
data[row + 1].push(value); data[row + 1].push(value);
data_styles.insert((row + 1, col + with_index as usize), style); data_styles.insert((row + 1, col + with_index as usize), style);
} }
let head_cell = Cell::new(header); let head_cell = NuTableCell::new(header);
data[0].push(head_cell); data[0].push(head_cell);
if column_width > available { if column_width > available {
@ -352,7 +321,7 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
let is_last_column = widths.len() == count_columns; let is_last_column = widths.len() == count_columns;
if !is_last_column { if !is_last_column {
let shift = Cell::exact(String::from("..."), 3, vec![]); let shift = NuTableCell::exact(String::from("..."), 3, vec![]);
for row in &mut data { for row in &mut data {
row.push(shift.clone()); row.push(shift.clone());
} }
@ -362,92 +331,34 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
} }
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_index_style(get_index_style(opts.style_computer)); table.set_index_style(get_index_style(cfg.opts.style_computer));
table.set_header_style(get_header_style(opts.style_computer)); table.set_header_style(get_header_style(cfg.opts.style_computer));
set_data_styles(&mut table, data_styles); set_data_styles(&mut table, data_styles);
Ok(Some(TableOutput::new(table, true, with_index))) Ok(Some(TableOutput::new(table, true, with_index)))
} }
fn expanded_table_kv(cols: &[String], vals: &[Value], opts: Options<'_>) -> StringResult { fn expanded_table_kv(cols: &[String], vals: &[Value], cfg: Cfg<'_>) -> StringResult {
let theme = load_theme_from_config(opts.config); let theme = load_theme_from_config(cfg.opts.config);
let key_width = cols.iter().map(|col| string_width(col)).max().unwrap_or(0); let key_width = cols.iter().map(|col| string_width(col)).max().unwrap_or(0);
let count_borders = let count_borders =
theme.has_inner() as usize + theme.has_right() as usize + theme.has_left() as usize; theme.has_inner() as usize + theme.has_right() as usize + theme.has_left() as usize;
let padding = 2; let padding = 2;
if key_width + count_borders + padding + padding > opts.available_width { if key_width + count_borders + padding + padding > cfg.opts.width {
return Ok(None); return Ok(None);
} }
let value_width = opts.available_width - key_width - count_borders - padding - padding; let value_width = cfg.opts.width - key_width - count_borders - padding - padding;
let mut data = Vec::with_capacity(cols.len()); let mut data = Vec::with_capacity(cols.len());
for (key, value) in cols.iter().zip(vals) { for (key, value) in cols.iter().zip(vals) {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) {
return Ok(None); return Ok(None);
} }
let is_limited = matches!(opts.format.expand_limit, Some(0)); let (value, is_expanded) = match expand_table_value(value, value_width, &cfg)? {
let mut is_expanded = false; Some(val) => val,
let value = if is_limited { None => return Ok(None),
let (text, _) = value_to_styled_string(value, opts.config, opts.style_computer);
clean_charset(&text)
} else {
match value {
Value::List { vals, span } => {
let mut oopts = dive_options(&opts, *span);
oopts.available_width = value_width;
let table = expanded_table_list(vals, 0, oopts)?;
match table {
Some(out) => {
is_expanded = true;
let table_config =
create_table_config(opts.config, opts.style_computer, &out);
let value = out.table.draw(table_config, value_width);
match value {
Some(result) => result,
None => return Ok(None),
}
}
None => {
// it means that the list is empty
let text =
value_to_styled_string(value, opts.config, opts.style_computer).0;
wrap_text(&text, value_width, opts.config)
}
}
}
Value::Record { cols, vals, span } => {
if cols.is_empty() {
// Like list case return styled string instead of empty value
let text =
value_to_styled_string(value, opts.config, opts.style_computer).0;
wrap_text(&text, value_width, opts.config)
} else {
let mut oopts = dive_options(&opts, *span);
oopts.available_width = value_width;
let result = expanded_table_kv(cols, vals, oopts)?;
match result {
Some(result) => {
is_expanded = true;
result
}
None => {
let failed_value =
value_to_styled_string(value, opts.config, opts.style_computer);
wrap_text(&failed_value.0, value_width, opts.config)
}
}
}
}
val => {
let text =
value_to_clean_styled_string(val, opts.config, opts.style_computer).0;
wrap_text(&text, value_width, opts.config)
}
}
}; };
// we want to have a key being aligned to 2nd line, // we want to have a key being aligned to 2nd line,
@ -458,90 +369,154 @@ fn expanded_table_kv(cols: &[String], vals: &[Value], opts: Options<'_>) -> Stri
key.insert(0, '\n'); key.insert(0, '\n');
} }
let key = Cell::new(key); let key = NuTableCell::new(key);
let val = Cell::new(value); let val = NuTableCell::new(value);
let row = vec![key, val]; let row = vec![key, val];
data.push(row); data.push(row);
} }
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
let keys_style = get_header_style(opts.style_computer).alignment(Alignment::Left); table.set_index_style(get_key_style(&cfg));
table.set_index_style(keys_style);
let out = TableOutput::new(table, false, true); let out = TableOutput::new(table, false, true);
maybe_expand_table(out, opts.available_width, opts.config, opts.style_computer) maybe_expand_table(out, cfg.opts.width, &cfg.opts)
} }
fn expanded_table_entry(item: &Value, header: &str, opts: Options<'_>) -> NuText { // the flag is used as an optimization to not do `value.lines().count()` search.
fn expand_table_value(
value: &Value,
value_width: usize,
cfg: &Cfg<'_>,
) -> Result<Option<(String, bool)>, ShellError> {
let is_limited = matches!(cfg.format.expand_limit, Some(0));
if is_limited {
return Ok(Some((value_to_string_clean(value, cfg), false)));
}
match value {
Value::List { vals, span } => {
let mut inner_cfg = dive_options(cfg, *span);
inner_cfg.opts.width = value_width;
let table = expanded_table_list(vals, inner_cfg)?;
match table {
Some(out) => {
let cfg = create_table_cfg(cfg, &out);
let value = out.table.draw(cfg, value_width);
match value {
Some(result) => Ok(Some((result, true))),
None => Ok(None),
}
}
None => {
// it means that the list is empty
Ok(Some((
value_to_wrapped_string(value, cfg, value_width),
false,
)))
}
}
}
Value::Record { cols, vals, span } => {
if cols.is_empty() {
// Like list case return styled string instead of empty value
return Ok(Some((
value_to_wrapped_string(value, cfg, value_width),
false,
)));
}
let mut inner_cfg = dive_options(cfg, *span);
inner_cfg.opts.width = value_width;
let result = expanded_table_kv(cols, vals, inner_cfg)?;
match result {
Some(result) => Ok(Some((result, true))),
None => Ok(Some((
value_to_wrapped_string(value, cfg, value_width),
false,
))),
}
}
_ => Ok(Some((
value_to_wrapped_string_clean(value, cfg, value_width),
false,
))),
}
}
fn get_key_style(cfg: &Cfg<'_>) -> TextStyle {
get_header_style(cfg.opts.style_computer).alignment(Alignment::Left)
}
fn expanded_table_entry(item: &Value, header: &str, cfg: Cfg<'_>) -> NuText {
match item { match item {
Value::Record { .. } => { Value::Record { .. } => {
let val = header.to_owned(); let val = header.to_owned();
let path = PathMember::String { let path = PathMember::String {
val, val,
span: opts.span, span: cfg.opts.span,
optional: false, optional: false,
}; };
let val = item.clone().follow_cell_path(&[path], false); let val = item.clone().follow_cell_path(&[path], false);
match val { match val {
Ok(val) => expanded_table_entry2(&val, opts), Ok(val) => expanded_table_entry2(&val, cfg),
Err(_) => error_sign(opts.style_computer), Err(_) => error_sign(cfg.opts.style_computer),
} }
} }
_ => expanded_table_entry2(item, opts), _ => expanded_table_entry2(item, cfg),
} }
} }
fn expanded_table_entry2(item: &Value, opts: Options<'_>) -> NuText { fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> NuText {
let is_limit_reached = matches!(opts.format.expand_limit, Some(0)); let is_limit_reached = matches!(cfg.format.expand_limit, Some(0));
if is_limit_reached { if is_limit_reached {
return value_to_clean_styled_string(item, opts.config, opts.style_computer); return nu_value_to_string_clean(item, cfg.opts.config, cfg.opts.style_computer);
} }
match &item { match &item {
Value::Record { cols, vals, span } => { Value::Record { cols, vals, span } => {
if cols.is_empty() && vals.is_empty() { if cols.is_empty() && vals.is_empty() {
return value_to_styled_string(item, opts.config, opts.style_computer); return nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer);
} }
// we verify what is the structure of a Record cause it might represent // we verify what is the structure of a Record cause it might represent
let oopts = dive_options(&opts, *span); let inner_cfg = dive_options(&cfg, *span);
let table = expanded_table_kv(cols, vals, oopts); let table = expanded_table_kv(cols, vals, inner_cfg);
match table { match table {
Ok(Some(table)) => (table, TextStyle::default()), Ok(Some(table)) => (table, TextStyle::default()),
_ => value_to_styled_string(item, opts.config, opts.style_computer), _ => nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer),
} }
} }
Value::List { vals, span } => { Value::List { vals, span } => {
if opts.format.flatten && is_simple_list(vals) { if cfg.format.flatten && is_simple_list(vals) {
return value_list_to_string( return value_list_to_string(
vals, vals,
opts.config, cfg.opts.config,
opts.style_computer, cfg.opts.style_computer,
&opts.format.flatten_sep, &cfg.format.flatten_sep,
); );
} }
let oopts = dive_options(&opts, *span); let inner_cfg = dive_options(&cfg, *span);
let table = expanded_table_list(vals, 0, oopts); let table = expanded_table_list(vals, inner_cfg);
let out = match table { let out = match table {
Ok(Some(out)) => out, Ok(Some(out)) => out,
_ => return value_to_styled_string(item, opts.config, opts.style_computer), _ => return nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer),
}; };
let table_config = create_table_config(opts.config, opts.style_computer, &out); let table_config = create_table_cfg(&cfg, &out);
let table = out.table.draw(table_config, usize::MAX); let table = out.table.draw(table_config, usize::MAX);
match table { match table {
Some(table) => (table, TextStyle::default()), Some(table) => (table, TextStyle::default()),
None => value_to_styled_string(item, opts.config, opts.style_computer), None => nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer),
} }
} }
_ => value_to_clean_styled_string(item, opts.config, opts.style_computer), _ => nu_value_to_string_clean(item, cfg.opts.config, cfg.opts.style_computer),
} }
} }
@ -562,21 +537,21 @@ fn value_list_to_string(
buf.push_str(flatten_sep); buf.push_str(flatten_sep);
} }
let (text, _) = value_to_clean_styled_string(value, config, style_computer); let text = nu_value_to_string_clean(value, config, style_computer).0;
buf.push_str(&text); buf.push_str(&text);
} }
(buf, TextStyle::default()) (buf, TextStyle::default())
} }
fn dive_options<'b>(opts: &Options<'b>, span: Span) -> Options<'b> { fn dive_options<'b>(cfg: &Cfg<'b>, span: Span) -> Cfg<'b> {
let mut opts = opts.clone(); let mut cfg = cfg.clone();
opts.span = span; cfg.opts.span = span;
if let Some(deep) = opts.format.expand_limit.as_mut() { if let Some(deep) = cfg.format.expand_limit.as_mut() {
*deep -= 1 *deep -= 1
} }
opts cfg
} }
fn lookup_index_value(item: &Value, config: &Config) -> Option<String> { fn lookup_index_value(item: &Value, config: &Config) -> Option<String> {
@ -584,23 +559,47 @@ fn lookup_index_value(item: &Value, config: &Config) -> Option<String> {
.map(|value| value.into_string("", config)) .map(|value| value.into_string("", config))
} }
fn maybe_expand_table( fn maybe_expand_table(out: TableOutput, term_width: usize, opts: &TableOpts<'_>) -> StringResult {
out: TableOutput, let mut table_config = create_nu_table_config(opts.config, opts.style_computer, &out, false);
term_width: usize,
config: &Config,
style_computer: &StyleComputer,
) -> StringResult {
let mut table_config = create_table_config(config, style_computer, &out);
let total_width = out.table.total_width(&table_config); let total_width = out.table.total_width(&table_config);
if total_width < term_width { if total_width < term_width {
const EXPAND_THRESHOLD: f32 = 0.80; const EXPAND_THRESHOLD: f32 = 0.80;
let used_percent = total_width as f32 / term_width as f32; let used_percent = total_width as f32 / term_width as f32;
let need_expansion = total_width < term_width && used_percent > EXPAND_THRESHOLD; let need_expansion = total_width < term_width && used_percent > EXPAND_THRESHOLD;
if need_expansion { if need_expansion {
table_config = table_config.expand(true); table_config.expand = true;
} }
} }
let output = out.table.draw(table_config, term_width); Ok(out.table.draw(table_config, term_width))
Ok(output) }
fn set_data_styles(table: &mut NuTable, styles: HashMap<Position, TextStyle>) {
for (pos, style) in styles {
table.insert_style(pos, style);
}
}
fn create_table_cfg(cfg: &Cfg<'_>, out: &TableOutput) -> crate::NuTableConfig {
create_nu_table_config(cfg.opts.config, cfg.opts.style_computer, out, false)
}
fn value_to_string(value: &Value, cfg: &Cfg<'_>) -> String {
nu_value_to_string(value, cfg.opts.config, cfg.opts.style_computer).0
}
fn value_to_string_clean(value: &Value, cfg: &Cfg<'_>) -> String {
nu_value_to_string_clean(value, cfg.opts.config, cfg.opts.style_computer).0
}
fn value_to_wrapped_string(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
wrap_text(&value_to_string(value, cfg), value_width, cfg.opts.config)
}
fn value_to_wrapped_string_clean(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
wrap_text(
&value_to_string_clean(value, cfg),
value_width,
cfg.opts.config,
)
} }

View File

@ -1,84 +1,40 @@
use nu_color_config::{StyleComputer, TextStyle}; use nu_color_config::TextStyle;
use nu_engine::column::get_columns; use nu_engine::column::get_columns;
use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value}; use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use crate::{Cell, NuTable, NuText}; use crate::{
clean_charset,
use super::{ common::{
clean_charset, create_table_config, get_empty_style, get_header_style, get_index_style, create_nu_table_config, get_empty_style, get_header_style, get_index_style,
get_value_style, StringResult, TableOutput, TableResult, INDEX_COLUMN_NAME, get_value_style, NuText, INDEX_COLUMN_NAME,
},
NuTable, NuTableCell, StringResult, TableOpts, TableOutput, TableResult,
}; };
pub struct JustTable; pub struct JustTable;
impl JustTable { impl JustTable {
pub fn table(input: &[Value], row_offset: usize, opts: BuildConfig<'_>) -> StringResult { pub fn table(input: &[Value], opts: TableOpts<'_>) -> StringResult {
let out = match table(input, row_offset, opts.clone())? { create_table(input, opts)
Some(out) => out,
None => return Ok(None),
};
let table_config = create_table_config(opts.config, opts.style_computer, &out);
let table = out.table.draw(table_config, opts.term_width);
Ok(table)
} }
pub fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> StringResult { pub fn kv_table(cols: &[String], vals: &[Value], opts: TableOpts<'_>) -> StringResult {
kv_table(cols, vals, opts) kv_table(cols, vals, opts)
} }
} }
#[derive(Debug, Clone)] fn create_table(input: &[Value], opts: TableOpts<'_>) -> Result<Option<String>, ShellError> {
pub struct BuildConfig<'a> { match table(input, opts.row_offset, opts.clone())? {
pub(crate) ctrlc: Option<Arc<AtomicBool>>, Some(out) => {
pub(crate) config: &'a Config, let table_config =
pub(crate) style_computer: &'a StyleComputer<'a>, create_nu_table_config(opts.config, opts.style_computer, &out, false);
pub(crate) span: Span, Ok(out.table.draw(table_config, opts.width))
pub(crate) term_width: usize,
}
impl<'a> BuildConfig<'a> {
pub fn new(
ctrlc: Option<Arc<AtomicBool>>,
config: &'a Config,
style_computer: &'a StyleComputer<'a>,
span: Span,
term_width: usize,
) -> Self {
Self {
ctrlc,
config,
style_computer,
span,
term_width,
} }
} None => Ok(None),
pub fn span(&self) -> Span {
self.span
}
pub fn term_width(&self) -> usize {
self.term_width
}
pub fn config(&self) -> &Config {
self.config
}
pub fn style_computer(&self) -> &StyleComputer {
self.style_computer
}
pub fn ctrlc(&self) -> Option<&Arc<AtomicBool>> {
self.ctrlc.as_ref()
} }
} }
fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> StringResult { fn kv_table(cols: &[String], vals: &[Value], opts: TableOpts<'_>) -> StringResult {
let mut data = vec![Vec::with_capacity(2); cols.len()]; let mut data = vec![Vec::with_capacity(2); cols.len()];
for ((column, value), row) in cols.iter().zip(vals.iter()).zip(data.iter_mut()) { for ((column, value), row) in cols.iter().zip(vals.iter()).zip(data.iter_mut()) {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
@ -91,8 +47,8 @@ fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> StringRes
value = clean_charset(&value); value = clean_charset(&value);
} }
let key = Cell::new(column.to_string()); let key = NuTableCell::new(column.to_string());
let value = Cell::new(value); let value = NuTableCell::new(value);
row.push(key); row.push(key);
row.push(value); row.push(value);
} }
@ -101,13 +57,13 @@ fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> StringRes
table.set_index_style(TextStyle::default_field()); table.set_index_style(TextStyle::default_field());
let out = TableOutput::new(table, false, true); let out = TableOutput::new(table, false, true);
let table_config = create_table_config(opts.config, opts.style_computer, &out); let table_config = create_nu_table_config(opts.config, opts.style_computer, &out, false);
let table = out.table.draw(table_config, opts.term_width); let table = out.table.draw(table_config, opts.width);
Ok(table) Ok(table)
} }
fn table(input: &[Value], row_offset: usize, opts: BuildConfig<'_>) -> TableResult { fn table(input: &[Value], row_offset: usize, opts: TableOpts<'_>) -> TableResult {
if input.is_empty() { if input.is_empty() {
return Ok(None); return Ok(None);
} }
@ -148,7 +104,7 @@ fn to_table_with_header(
headers: Vec<String>, headers: Vec<String>,
with_index: bool, with_index: bool,
row_offset: usize, row_offset: usize,
opts: BuildConfig<'_>, opts: TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> { ) -> Result<Option<NuTable>, ShellError> {
let count_rows = input.len() + 1; let count_rows = input.len() + 1;
let count_columns = headers.len(); let count_columns = headers.len();
@ -179,7 +135,7 @@ fn to_table_with_header(
let (text, style) = get_string_value_with_header(item, header, &opts); let (text, style) = get_string_value_with_header(item, header, &opts);
table.insert((row + 1, col), text); table.insert((row + 1, col), text);
table.set_cell_style((row + 1, col), style); table.insert_style((row + 1, col), style);
} }
} }
@ -190,7 +146,7 @@ fn to_table_with_no_header(
input: &[Value], input: &[Value],
with_index: bool, with_index: bool,
row_offset: usize, row_offset: usize,
opts: BuildConfig<'_>, opts: TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> { ) -> Result<Option<NuTable>, ShellError> {
let mut table = NuTable::new(input.len(), with_index as usize + 1); let mut table = NuTable::new(input.len(), with_index as usize + 1);
table.set_index_style(get_index_style(opts.style_computer)); table.set_index_style(get_index_style(opts.style_computer));
@ -213,13 +169,13 @@ fn to_table_with_no_header(
let pos = (row, with_index as usize); let pos = (row, with_index as usize);
table.insert(pos, text); table.insert(pos, text);
table.set_cell_style(pos, style); table.insert_style(pos, style);
} }
Ok(Some(table)) Ok(Some(table))
} }
fn get_string_value_with_header(item: &Value, header: &str, opts: &BuildConfig) -> NuText { fn get_string_value_with_header(item: &Value, header: &str, opts: &TableOpts) -> NuText {
match item { match item {
Value::Record { .. } => { Value::Record { .. } => {
let path = PathMember::String { let path = PathMember::String {
@ -238,7 +194,7 @@ fn get_string_value_with_header(item: &Value, header: &str, opts: &BuildConfig)
} }
} }
fn get_string_value(item: &Value, opts: &BuildConfig) -> NuText { fn get_string_value(item: &Value, opts: &TableOpts) -> NuText {
let (mut text, style) = get_value_style(item, opts.config, opts.style_computer); let (mut text, style) = get_value_style(item, opts.config, opts.style_computer);
let is_string_value = matches!(item, Value::String { .. }); let is_string_value = matches!(item, Value::String { .. });
if is_string_value { if is_string_value {

View File

@ -2,20 +2,15 @@ mod collapse;
mod expanded; mod expanded;
mod general; mod general;
use nu_color_config::{Alignment, StyleComputer, TextStyle}; use std::sync::{atomic::AtomicBool, Arc};
use nu_protocol::TrimStrategy;
use nu_protocol::{Config, FooterMode, ShellError, Span, Value};
use std::collections::HashMap;
use crate::{string_wrap, NuTable, TableConfig, TableTheme};
pub use collapse::CollapsedTable; pub use collapse::CollapsedTable;
pub use expanded::ExpandedTable; pub use expanded::ExpandedTable;
pub use general::{BuildConfig, JustTable}; pub use general::JustTable;
use nu_color_config::StyleComputer;
use nu_protocol::{Config, Span};
pub type NuText = (String, TextStyle); use crate::NuTable;
pub type TableResult = Result<Option<TableOutput>, ShellError>;
pub type StringResult = Result<Option<String>, ShellError>;
pub struct TableOutput { pub struct TableOutput {
pub table: NuTable, pub table: NuTable,
@ -33,176 +28,32 @@ impl TableOutput {
} }
} }
pub fn value_to_styled_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText { #[derive(Debug, Clone)]
let float_precision = cfg.float_precision as usize; pub struct TableOpts<'a> {
let text = val.into_abbreviated_string(cfg); ctrlc: Option<Arc<AtomicBool>>,
make_styled_string(style, text, Some(val), float_precision) config: &'a Config,
style_computer: &'a StyleComputer<'a>,
span: Span,
row_offset: usize,
width: usize,
} }
pub fn value_to_clean_styled_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText { impl<'a> TableOpts<'a> {
let (text, style) = value_to_styled_string(val, cfg, style); pub fn new(
let text = clean_charset(&text); config: &'a Config,
(text, style) style_computer: &'a StyleComputer<'a>,
} ctrlc: Option<Arc<AtomicBool>>,
span: Span,
pub fn clean_charset(text: &str) -> String { row_offset: usize,
// todo: optimize, I bet it can be done in 1 path available_width: usize,
text.replace('\t', " ").replace('\r', "") ) -> Self {
} Self {
ctrlc,
const INDEX_COLUMN_NAME: &str = "index"; config,
style_computer,
fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) { span,
make_styled_string(style_computer, String::from(""), None, 0) row_offset,
} width: available_width,
fn wrap_text(text: &str, width: usize, config: &Config) -> String {
string_wrap(text, width, is_cfg_trim_keep_words(config))
}
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<String, ShellError> {
// vall will always be a f64 so convert it with precision formatting
let val_float = match val.trim().parse::<f64>() {
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
}
)
}
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 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 set_data_styles(table: &mut NuTable, styles: HashMap<(usize, usize), TextStyle>) {
for (pos, style) in styles {
table.set_cell_style(pos, style);
}
}
fn get_header_style(style_computer: &StyleComputer) -> TextStyle {
TextStyle::with_style(
Alignment::Center,
style_computer.compute("header", &Value::string("", Span::unknown())),
)
}
fn get_index_style(style_computer: &StyleComputer) -> TextStyle {
TextStyle::with_style(
Alignment::Right,
style_computer.compute("row_index", &Value::string("", Span::unknown())),
)
}
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),
),
}
}
fn get_empty_style(style_computer: &StyleComputer) -> NuText {
(
String::from(""),
TextStyle::with_style(
Alignment::Right,
style_computer.compute("empty", &Value::nothing(Span::unknown())),
),
)
}

View File

@ -42,3 +42,8 @@ pub fn string_truncate(text: &str, width: usize) -> String {
Truncate::truncate_text(line, width).into_owned() Truncate::truncate_text(line, width).into_owned()
} }
pub fn clean_charset(text: &str) -> String {
// todo: optimize, I bet it can be done in 1 path
text.replace('\t', " ").replace('\r', "")
}

View File

@ -1,16 +1,16 @@
#![allow(dead_code)] #![allow(dead_code)]
use nu_table::{string_width, NuTable, TableConfig}; use nu_table::{string_width, NuTable, NuTableConfig};
use tabled::grid::records::vec_records::CellInfo; use tabled::grid::records::vec_records::CellInfo;
pub struct TestCase { pub struct TestCase {
cfg: TableConfig, cfg: NuTableConfig,
termwidth: usize, termwidth: usize,
expected: Option<String>, expected: Option<String>,
} }
impl TestCase { impl TestCase {
pub fn new(cfg: TableConfig, termwidth: usize, expected: Option<String>) -> Self { pub fn new(cfg: NuTableConfig, termwidth: usize, expected: Option<String>) -> Self {
Self { Self {
cfg, cfg,
termwidth, termwidth,
@ -37,7 +37,7 @@ pub fn test_table<I: IntoIterator<Item = TestCase>>(data: Data, tests: I) {
} }
} }
pub fn create_table(data: Data, config: TableConfig, termwidth: usize) -> Option<String> { pub fn create_table(data: Data, config: NuTableConfig, termwidth: usize) -> Option<String> {
let table = NuTable::from(data); let table = NuTable::from(data);
table.draw(config, termwidth) table.draw(config, termwidth)
} }

View File

@ -1,7 +1,7 @@
mod common; mod common;
use nu_protocol::TrimStrategy; use nu_protocol::TrimStrategy;
use nu_table::{NuTable, TableConfig, TableTheme as theme}; use nu_table::{NuTable, NuTableConfig, TableTheme as theme};
use common::{create_row, test_table, TestCase}; use common::{create_row, test_table, TestCase};
use tabled::grid::records::vec_records::CellInfo; use tabled::grid::records::vec_records::CellInfo;
@ -9,11 +9,13 @@ use tabled::grid::records::vec_records::CellInfo;
#[test] #[test]
fn data_and_header_has_different_size_doesnt_work() { fn data_and_header_has_different_size_doesnt_work() {
let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]); let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
let cfg = NuTableConfig {
theme: theme::heavy(),
with_header: true,
..Default::default()
};
let table = table.draw( let table = table.draw(cfg.clone(), usize::MAX);
TableConfig::new().theme(theme::heavy()).with_header(true),
usize::MAX,
);
assert_eq!( assert_eq!(
table.as_deref(), table.as_deref(),
@ -29,10 +31,7 @@ fn data_and_header_has_different_size_doesnt_work() {
let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]); let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
let table = table.draw( let table = table.draw(cfg, usize::MAX);
TableConfig::new().theme(theme::heavy()).with_header(true),
usize::MAX,
);
assert_eq!( assert_eq!(
table.as_deref(), table.as_deref(),
@ -49,7 +48,7 @@ fn data_and_header_has_different_size_doesnt_work() {
#[test] #[test]
fn termwidth_too_small() { fn termwidth_too_small() {
let test_loop = |config: TableConfig| { let test_loop = |config: NuTableConfig| {
for i in 0..10 { for i in 0..10 {
let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]); let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
let table = table.draw(config.clone(), i); let table = table.draw(config.clone(), i);
@ -58,29 +57,22 @@ fn termwidth_too_small() {
} }
}; };
let base_config = TableConfig::new().theme(theme::heavy()).with_header(true); let mut cfg = NuTableConfig {
theme: theme::heavy(),
with_header: true,
..Default::default()
};
let config = base_config.clone(); for case in [
test_loop(config); TrimStrategy::truncate(None),
TrimStrategy::truncate(Some(String::from("**"))),
let config = base_config.clone().trim(TrimStrategy::truncate(None)); TrimStrategy::truncate(Some(String::from(""))),
test_loop(config); TrimStrategy::wrap(false),
TrimStrategy::wrap(true),
let config = base_config ] {
.clone() cfg.trim = case;
.trim(TrimStrategy::truncate(Some(String::from("**")))); test_loop(cfg.clone());
test_loop(config); }
let config = base_config
.clone()
.trim(TrimStrategy::truncate(Some(String::from(""))));
test_loop(config);
let config = base_config.clone().trim(TrimStrategy::wrap(false));
test_loop(config);
let config = base_config.trim(TrimStrategy::wrap(true));
test_loop(config);
} }
#[test] #[test]
@ -204,11 +196,12 @@ fn width_control_test_0() {
} }
fn test_width(data: Vec<Vec<CellInfo<String>>>, tests: &[(usize, &str)]) { fn test_width(data: Vec<Vec<CellInfo<String>>>, tests: &[(usize, &str)]) {
let trim = TrimStrategy::truncate(Some(String::from("..."))); let config = NuTableConfig {
let config = TableConfig::new() theme: theme::heavy(),
.theme(theme::heavy()) trim: TrimStrategy::truncate(Some(String::from("..."))),
.with_header(true) with_header: true,
.trim(trim); ..Default::default()
};
let tests = tests.iter().map(|&(termwidth, expected)| { let tests = tests.iter().map(|&(termwidth, expected)| {
TestCase::new(config.clone(), termwidth, Some(expected.to_owned())) TestCase::new(config.clone(), termwidth, Some(expected.to_owned()))
@ -218,10 +211,13 @@ fn test_width(data: Vec<Vec<CellInfo<String>>>, tests: &[(usize, &str)]) {
} }
fn test_trim(tests: &[(usize, Option<&str>)], trim: TrimStrategy) { fn test_trim(tests: &[(usize, Option<&str>)], trim: TrimStrategy) {
let config = TableConfig::new() let config = NuTableConfig {
.theme(theme::heavy()) theme: theme::heavy(),
.with_header(true) with_header: true,
.trim(trim); trim,
..Default::default()
};
let tests = tests.iter().map(|&(termwidth, expected)| { let tests = tests.iter().map(|&(termwidth, expected)| {
TestCase::new(config.clone(), termwidth, expected.map(|s| s.to_string())) TestCase::new(config.clone(), termwidth, expected.map(|s| s.to_string()))
}); });

View File

@ -2,16 +2,18 @@ mod common;
use common::{create_row, create_table}; use common::{create_row, create_table};
use nu_table::{TableConfig, TableTheme as theme}; use nu_table::{NuTableConfig, TableTheme as theme};
#[test] #[test]
fn test_expand() { fn test_expand() {
let table = create_table( let table = create_table(
vec![create_row(4); 3], vec![create_row(4); 3],
TableConfig::new() NuTableConfig {
.theme(theme::rounded()) theme: theme::rounded(),
.with_header(true) with_header: true,
.expand(true), expand: true,
..Default::default()
},
50, 50,
); );

View File

@ -1,7 +1,7 @@
mod common; mod common;
use common::create_row as row; use common::create_row as row;
use nu_table::{NuTable, TableConfig, TableTheme as theme}; use nu_table::{NuTable, NuTableConfig, TableTheme as theme};
use tabled::grid::records::vec_records::CellInfo; use tabled::grid::records::vec_records::CellInfo;
#[test] #[test]
@ -476,10 +476,11 @@ fn test_with_love() {
} }
fn create_table(data: Vec<Vec<CellInfo<String>>>, with_header: bool, theme: theme) -> String { fn create_table(data: Vec<Vec<CellInfo<String>>>, with_header: bool, theme: theme) -> String {
let mut config = TableConfig::new().theme(theme); let config = NuTableConfig {
if with_header { theme,
config = config.with_header(true); with_header,
} ..Default::default()
};
let out = common::create_table(data, config, usize::MAX); let out = common::create_table(data, config, usize::MAX);
@ -491,10 +492,11 @@ fn create_table_with_size(
with_header: bool, with_header: bool,
theme: theme, theme: theme,
) -> String { ) -> String {
let mut config = TableConfig::new().theme(theme); let config = NuTableConfig {
if with_header { theme,
config = config.with_header(true); with_header,
} ..Default::default()
};
let table = NuTable::from(data); let table = NuTable::from(data);