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

View file

@ -84,7 +84,7 @@ serde_yaml = "0.9"
sha2 = "0.10"
sqlparser = { version = "0.34", features = ["serde"], optional = true }
sysinfo = "0.29"
tabled = { version = "0.12.2", features = ["color"], default-features = false }
tabled = { version = "0.14.0", features = ["color"], default-features = false }
terminal_size = "0.2"
titlecase = "2.0"
toml = "0.7"

View file

@ -6,12 +6,13 @@ use nu_engine::{env::get_config, env_to_string, CallExt};
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Config, DataSource, Example, FooterMode, IntoPipelineData, ListStream, PipelineData,
Category, Config, DataSource, Example, IntoPipelineData, ListStream, PipelineData,
PipelineMetadata, RawStream, ShellError, Signature, Span, SyntaxShape, Type, Value,
};
use nu_table::common::create_nu_table_config;
use nu_table::{
BuildConfig, Cell, CollapsedTable, ExpandedTable, JustTable, NuTable, StringResult,
TableConfig, TableOutput, TableTheme,
CollapsedTable, ExpandedTable, JustTable, NuTable, NuTableCell, StringResult, TableOpts,
TableOutput,
};
use nu_utils::get_ls_colors;
use std::sync::Arc;
@ -361,8 +362,8 @@ fn handle_record(
let result = if cols.is_empty() {
create_empty_placeholder("record", term_width, engine_state, stack)
} else {
let opts = BuildConfig::new(ctrlc, config, style_computer, span, term_width);
let result = build_table_kv(cols, vals, table_view, opts)?;
let opts = TableOpts::new(config, style_computer, ctrlc, span, 0, term_width);
let result = build_table_kv(cols, vals, table_view, opts, span)?;
match result {
Some(output) => maybe_strip_color(output, config),
None => report_unsuccessful_output(ctrlc1, term_width),
@ -391,7 +392,8 @@ fn build_table_kv(
cols: Vec<String>,
vals: Vec<Value>,
table_view: TableView,
opts: BuildConfig<'_>,
opts: TableOpts<'_>,
span: Span,
) -> StringResult {
match table_view {
TableView::General => JustTable::kv_table(&cols, &vals, opts),
@ -404,7 +406,6 @@ fn build_table_kv(
ExpandedTable::new(limit, flatten, sep).build_map(&cols, &vals, opts)
}
TableView::Collapsed => {
let span = opts.span();
let value = Value::Record { cols, vals, span };
CollapsedTable::build(value, opts)
}
@ -414,21 +415,20 @@ fn build_table_kv(
fn build_table_batch(
vals: Vec<Value>,
table_view: TableView,
row_offset: usize,
opts: BuildConfig<'_>,
opts: TableOpts<'_>,
span: Span,
) -> StringResult {
match table_view {
TableView::General => JustTable::table(&vals, row_offset, opts),
TableView::General => JustTable::table(&vals, opts),
TableView::Expanded {
limit,
flatten,
flatten_separator,
} => {
let sep = flatten_separator.unwrap_or_else(|| String::from(' '));
ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts, row_offset)
ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts)
}
TableView::Collapsed => {
let span = opts.span();
let value = Value::List { vals, span };
CollapsedTable::build(value, opts)
}
@ -647,20 +647,16 @@ impl PagingTableCreator {
return Ok(None);
}
let config = get_config(&self.engine_state, &self.stack);
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack);
let term_width = get_width_param(self.width_param);
let ctrlc = self.ctrlc.clone();
let span = self.head;
let opts = BuildConfig::new(ctrlc, &config, &style_computer, span, term_width);
let cfg = get_config(&self.engine_state, &self.stack);
let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack);
let opts = self.create_table_opts(&cfg, &style_comp);
let view = TableView::Expanded {
limit,
flatten,
flatten_separator,
};
build_table_batch(batch, view, self.row_offset, opts)
build_table_batch(batch, view, opts, self.head)
}
fn build_collapsed(&mut self, batch: Vec<Value>) -> StringResult {
@ -668,26 +664,34 @@ impl PagingTableCreator {
return Ok(None);
}
let config = get_config(&self.engine_state, &self.stack);
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack);
let term_width = get_width_param(self.width_param);
let ctrlc = self.ctrlc.clone();
let span = self.head;
let opts = BuildConfig::new(ctrlc, &config, &style_computer, span, term_width);
let cfg = get_config(&self.engine_state, &self.stack);
let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack);
let opts = self.create_table_opts(&cfg, &style_comp);
build_table_batch(batch, TableView::Collapsed, self.row_offset, opts)
build_table_batch(batch, TableView::Collapsed, opts, self.head)
}
fn build_general(&mut self, batch: Vec<Value>) -> StringResult {
let term_width = get_width_param(self.width_param);
let config = get_config(&self.engine_state, &self.stack);
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack);
let ctrlc = self.ctrlc.clone();
let span = self.head;
let row_offset = self.row_offset;
let opts = BuildConfig::new(ctrlc, &config, &style_computer, span, term_width);
let cfg = get_config(&self.engine_state, &self.stack);
let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack);
let opts = self.create_table_opts(&cfg, &style_comp);
build_table_batch(batch, TableView::General, row_offset, opts)
build_table_batch(batch, TableView::General, opts, self.head)
}
fn create_table_opts<'a>(
&self,
cfg: &'a Config,
style_comp: &'a StyleComputer<'a>,
) -> TableOpts<'a> {
TableOpts::new(
cfg,
style_comp,
self.ctrlc.clone(),
self.head,
self.row_offset,
get_width_param(self.width_param),
)
}
}
@ -780,22 +784,6 @@ impl Iterator for PagingTableCreator {
}
}
fn load_theme_from_config(config: &Config) -> TableTheme {
match config.table_mode.as_str() {
"basic" => TableTheme::basic(),
"thin" => TableTheme::thin(),
"light" => TableTheme::light(),
"compact" => TableTheme::compact(),
"with_love" => TableTheme::with_love(),
"compact_double" => TableTheme::compact_double(),
"rounded" => TableTheme::rounded(),
"reinforced" => TableTheme::reinforced(),
"heavy" => TableTheme::heavy(),
"none" => TableTheme::none(),
_ => TableTheme::rounded(),
}
}
fn render_path_name(
path: &str,
config: &Config,
@ -859,34 +847,6 @@ fn maybe_strip_color(output: String, config: &Config) -> String {
}
}
fn create_table_config(config: &Config, comp: &StyleComputer, out: &TableOutput) -> TableConfig {
let theme = load_theme_from_config(config);
let footer = with_footer(config, out.with_header, out.table.count_rows());
let line_style = lookup_separator_color(comp);
let trim = config.trim_strategy.clone();
TableConfig::new()
.theme(theme)
.with_footer(footer)
.with_header(out.with_header)
.with_index(out.with_index)
.line_style(line_style)
.trim(trim)
}
fn lookup_separator_color(style_computer: &StyleComputer) -> nu_ansi_term::Style {
style_computer.compute("separator", &Value::nothing(Span::unknown()))
}
fn with_footer(config: &Config, with_header: bool, count_records: usize) -> bool {
with_header && need_footer(config, count_records as u64)
}
fn need_footer(config: &Config, count_records: u64) -> bool {
matches!(config.footer_mode, FooterMode::RowCount(limit) if count_records > limit)
|| matches!(config.footer_mode, FooterMode::Always)
}
fn create_empty_placeholder(
value_type_name: &str,
termwidth: usize,
@ -898,14 +858,14 @@ fn create_empty_placeholder(
return String::new();
}
let cell = Cell::new(format!("empty {}", value_type_name));
let cell = NuTableCell::new(format!("empty {}", value_type_name));
let data = vec![vec![cell]];
let mut table = NuTable::from(data);
table.set_cell_style((0, 0), TextStyle::default().dimmed());
table.set_data_style(TextStyle::default().dimmed());
let out = TableOutput::new(table, false, false);
let style_computer = &StyleComputer::from_config(engine_state, stack);
let config = create_table_config(&config, style_computer, &out);
let config = create_nu_table_config(&config, style_computer, &out, false);
out.table
.draw(config, termwidth)

View file

@ -2384,3 +2384,179 @@ fn table_index_offset() {
let expected_suffix = actual.out.strip_suffix(suffix);
assert!(expected_suffix.is_some(), "{:?}", actual.out);
}
#[test]
fn table_theme_on_border_light() {
assert_eq!(
create_theme_output("light"),
[
"─#───a───b─────────c──────── 0 1 2 3 1 4 5 [list 3 items] ",
"─#───a───b─────────c──────── 0 1 2 3 1 4 5 [list 3 items] ─#───a───b─────────c────────",
"─#───a───b───c─ 0 1 2 3 ─#───a───b───c─",
"─#───a_looooooong_name───b───c─ 0 1 2 3 ─#───a_looooooong_name───b───c─",
]
);
}
#[test]
fn table_theme_on_border_basic() {
assert_eq!(
create_theme_output("basic"),
[
"+-#-+-a-+-b-+-------c--------+| 0 | 1 | 2 | 3 |+---+---+---+----------------+| 1 | 4 | 5 | [list 3 items] |+---+---+---+----------------+",
"+-#-+-a-+-b-+-------c--------+| 0 | 1 | 2 | 3 |+---+---+---+----------------+| 1 | 4 | 5 | [list 3 items] |+-#-+-a-+-b-+-------c--------+",
"+-#-+-a-+-b-+-c-+| 0 | 1 | 2 | 3 |+-#-+-a-+-b-+-c-+",
"+-#-+-a_looooooong_name-+-b-+-c-+| 0 | 1 | 2 | 3 |+-#-+-a_looooooong_name-+-b-+-c-+"
]
);
}
#[test]
fn table_theme_on_border_compact() {
assert_eq!(
create_theme_output("compact"),
[
"─#─┬─a─┬─b─┬───────c──────── 0 │ 1 │ 2 │ 3 1 │ 4 │ 5 │ [list 3 items] ───┴───┴───┴────────────────",
"─#─┬─a─┬─b─┬───────c──────── 0 │ 1 │ 2 │ 3 1 │ 4 │ 5 │ [list 3 items] ─#─┴─a─┴─b─┴───────c────────",
"─#─┬─a─┬─b─┬─c─ 0 │ 1 │ 2 │ 3 ─#─┴─a─┴─b─┴─c─",
"─#─┬─a_looooooong_name─┬─b─┬─c─ 0 │ 1 │ 2 │ 3 ─#─┴─a_looooooong_name─┴─b─┴─c─"
]
);
}
#[test]
fn table_theme_on_border_compact_double() {
assert_eq!(
create_theme_output("compact_double"),
[
"═#═╦═a═╦═b═╦═══════c════════ 0 ║ 1 ║ 2 ║ 3 1 ║ 4 ║ 5 ║ [list 3 items] ═══╩═══╩═══╩════════════════",
"═#═╦═a═╦═b═╦═══════c════════ 0 ║ 1 ║ 2 ║ 3 1 ║ 4 ║ 5 ║ [list 3 items] ═#═╩═a═╩═b═╩═══════c════════",
"═#═╦═a═╦═b═╦═c═ 0 ║ 1 ║ 2 ║ 3 ═#═╩═a═╩═b═╩═c═",
"═#═╦═a_looooooong_name═╦═b═╦═c═ 0 ║ 1 ║ 2 ║ 3 ═#═╩═a_looooooong_name═╩═b═╩═c═"
]
);
}
#[test]
fn table_theme_on_border_default() {
assert_eq!(
create_theme_output("default"),
[
"╭─#─┬─a─┬─b─┬───────c────────╮│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │╰───┴───┴───┴────────────────╯",
"╭─#─┬─a─┬─b─┬───────c────────╮│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │╰─#─┴─a─┴─b─┴───────c────────╯",
"╭─#─┬─a─┬─b─┬─c─╮│ 0 │ 1 │ 2 │ 3 │╰─#─┴─a─┴─b─┴─c─╯",
"╭─#─┬─a_looooooong_name─┬─b─┬─c─╮│ 0 │ 1 │ 2 │ 3 │╰─#─┴─a_looooooong_name─┴─b─┴─c─╯"
]
);
}
#[test]
fn table_theme_on_border_heavy() {
assert_eq!(
create_theme_output("heavy"),
[
"┏━#━┳━a━┳━b━┳━━━━━━━c━━━━━━━━┓┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃┃ 1 ┃ 4 ┃ 5 ┃ [list 3 items] ┃┗━━━┻━━━┻━━━┻━━━━━━━━━━━━━━━━┛",
"┏━#━┳━a━┳━b━┳━━━━━━━c━━━━━━━━┓┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃┃ 1 ┃ 4 ┃ 5 ┃ [list 3 items] ┃┗━#━┻━a━┻━b━┻━━━━━━━c━━━━━━━━┛",
"┏━#━┳━a━┳━b━┳━c━┓┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃┗━#━┻━a━┻━b━┻━c━┛",
"┏━#━┳━a_looooooong_name━┳━b━┳━c━┓┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃┗━#━┻━a_looooooong_name━┻━b━┻━c━┛"
]
);
}
#[test]
fn table_theme_on_border_reinforced() {
assert_eq!(
create_theme_output("reinforced"),
[
"┏─#─┬─a─┬─b─┬───────c────────┓│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │┗───┴───┴───┴────────────────┛",
"┏─#─┬─a─┬─b─┬───────c────────┓│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │┗─#─┴─a─┴─b─┴───────c────────┛",
"┏─#─┬─a─┬─b─┬─c─┓│ 0 │ 1 │ 2 │ 3 │┗─#─┴─a─┴─b─┴─c─┛",
"┏─#─┬─a_looooooong_name─┬─b─┬─c─┓│ 0 │ 1 │ 2 │ 3 │┗─#─┴─a_looooooong_name─┴─b─┴─c─┛"
]
);
}
#[test]
fn table_theme_on_border_none() {
assert_eq!(
create_theme_output("none"),
[
" # a b c 0 1 2 3 1 4 5 [list 3 items] ",
" # a b c 0 1 2 3 1 4 5 [list 3 items] # a b c ",
" # a b c 0 1 2 3 # a b c ",
" # a_looooooong_name b c 0 1 2 3 # a_looooooong_name b c "
]
);
}
#[test]
fn table_theme_on_border_rounded() {
assert_eq!(
create_theme_output("rounded"),
[
"╭─#─┬─a─┬─b─┬───────c────────╮│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │╰───┴───┴───┴────────────────╯",
"╭─#─┬─a─┬─b─┬───────c────────╮│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] │╰─#─┴─a─┴─b─┴───────c────────╯",
"╭─#─┬─a─┬─b─┬─c─╮│ 0 │ 1 │ 2 │ 3 │╰─#─┴─a─┴─b─┴─c─╯",
"╭─#─┬─a_looooooong_name─┬─b─┬─c─╮│ 0 │ 1 │ 2 │ 3 │╰─#─┴─a_looooooong_name─┴─b─┴─c─╯"
]
);
}
#[test]
fn table_theme_on_border_with_love() {
assert_eq!(
create_theme_output("with_love"),
[
"❤#❤❤❤a❤❤❤b❤❤❤❤❤❤❤❤❤c❤❤❤❤❤❤❤❤ 0 ❤ 1 ❤ 2 ❤ 3 1 ❤ 4 ❤ 5 ❤ [list 3 items] ❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤",
"❤#❤❤❤a❤❤❤b❤❤❤❤❤❤❤❤❤c❤❤❤❤❤❤❤❤ 0 ❤ 1 ❤ 2 ❤ 3 1 ❤ 4 ❤ 5 ❤ [list 3 items] ❤#❤❤❤a❤❤❤b❤❤❤❤❤❤❤❤❤c❤❤❤❤❤❤❤❤",
"❤#❤❤❤a❤❤❤b❤❤❤c❤ 0 ❤ 1 ❤ 2 ❤ 3 ❤#❤❤❤a❤❤❤b❤❤❤c❤",
"❤#❤❤❤a_looooooong_name❤❤❤b❤❤❤c❤ 0 ❤ 1 ❤ 2 ❤ 3 ❤#❤❤❤a_looooooong_name❤❤❤b❤❤❤c❤"
]
);
}
#[test]
fn table_theme_on_border_thin() {
assert_eq!(
create_theme_output("thin"),
[
"┌─#─┬─a─┬─b─┬───────c────────┐│ 0 │ 1 │ 2 │ 3 │├───┼───┼───┼────────────────┤│ 1 │ 4 │ 5 │ [list 3 items] │└───┴───┴───┴────────────────┘",
"┌─#─┬─a─┬─b─┬───────c────────┐│ 0 │ 1 │ 2 │ 3 │├───┼───┼───┼────────────────┤│ 1 │ 4 │ 5 │ [list 3 items] │└─#─┴─a─┴─b─┴───────c────────┘",
"┌─#─┬─a─┬─b─┬─c─┐│ 0 │ 1 │ 2 │ 3 │└─#─┴─a─┴─b─┴─c─┘",
"┌─#─┬─a_looooooong_name─┬─b─┬─c─┐│ 0 │ 1 │ 2 │ 3 │└─#─┴─a_looooooong_name─┴─b─┴─c─┘",
]
);
}
fn create_theme_output(theme: &str) -> Vec<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_protocol::{Span, Value};
use nu_table::{value_to_clean_styled_string, value_to_styled_string, BuildConfig, ExpandedTable};
use nu_table::{
common::{nu_value_to_string, nu_value_to_string_clean},
ExpandedTable, TableOpts,
};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@ -18,9 +21,9 @@ pub fn try_build_table(
try_build_map(cols, vals, span, style_computer, ctrlc, config)
}
val if matches!(val, Value::String { .. }) => {
value_to_clean_styled_string(&val, config, style_computer).0
nu_value_to_string_clean(&val, config, style_computer).0
}
val => value_to_styled_string(&val, config, style_computer).0,
val => nu_value_to_string(&val, config, style_computer).0,
}
}
@ -32,12 +35,19 @@ fn try_build_map(
ctrlc: Option<Arc<AtomicBool>>,
config: &NuConfig,
) -> String {
let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX);
let opts = TableOpts::new(
config,
style_computer,
ctrlc,
Span::unknown(),
0,
usize::MAX,
);
let result = ExpandedTable::new(None, false, String::new()).build_map(&cols, &vals, opts);
match result {
Ok(Some(result)) => result,
Ok(None) | Err(_) => {
value_to_styled_string(&Value::Record { cols, vals, span }, config, style_computer).0
nu_value_to_string(&Value::Record { cols, vals, span }, config, style_computer).0
}
}
}
@ -49,13 +59,20 @@ fn try_build_list(
span: Span,
style_computer: &StyleComputer,
) -> String {
let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX);
let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts, 0);
let opts = TableOpts::new(
config,
style_computer,
ctrlc,
Span::unknown(),
0,
usize::MAX,
);
let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts);
match result {
Ok(Some(out)) => out,
Ok(None) | Err(_) => {
// it means that the list is empty
value_to_styled_string(&Value::List { vals, span }, config, style_computer).0
nu_value_to_string(&Value::List { vals, span }, config, style_computer).0
}
}
}

View file

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

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-color-config = { path = "../nu-color-config", version = "0.83.2" }
nu-ansi-term = "0.49.0"
tabled = { version = "0.12.2", features = ["color"], default-features = false }
tabled = { version = "0.14.0", features = ["color"], default-features = false }
[dev-dependencies]
# nu-test-support = { path="../nu-test-support", version = "0.83.2" }

View file

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

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 util;
pub mod common;
pub use common::{StringResult, TableResult};
pub use nu_color_config::TextStyle;
pub use table::{Alignments, Cell, NuTable, TableConfig};
pub use table::{NuTable, NuTableCell, NuTableConfig};
pub use table_theme::TableTheme;
pub use types::{
clean_charset, value_to_clean_styled_string, value_to_styled_string, BuildConfig,
CollapsedTable, ExpandedTable, JustTable, NuText, StringResult, TableOutput, TableResult,
};
pub use types::{CollapsedTable, ExpandedTable, JustTable, TableOpts, TableOutput};
pub use unstructured_table::UnstructuredTable;
pub use util::*;

View file

@ -8,29 +8,32 @@ use tabled::{
builder::Builder,
grid::{
color::AnsiColor,
colors::Colors,
config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position},
dimension::CompleteDimensionVecRecords,
records::{
vec_records::{CellInfo, VecRecords},
ExactRecords, Records,
ExactRecords, PeekableRecords, Records, Resizable,
},
},
settings::{
formatting::AlignmentStrategy, object::Segment, peaker::Peaker, Color, Modify, Settings,
TableOption, Width,
formatting::AlignmentStrategy, object::Segment, peaker::Peaker, themes::ColumnNames, Color,
Modify, Settings, TableOption, Width,
},
Table,
};
/// Table represent a table view.
/// NuTable is a table rendering implementation.
#[derive(Debug, Clone)]
pub struct NuTable {
data: Data,
data: NuTableData,
styles: Styles,
alignments: Alignments,
size: (usize, usize),
}
type NuTableData = VecRecords<NuTableCell>;
pub type NuTableCell = CellInfo<String>;
#[derive(Debug, Default, Clone)]
struct Styles {
index: AnsiColor<'static>,
@ -39,27 +42,39 @@ struct Styles {
data_is_set: bool,
}
type Data = VecRecords<Cell>;
pub type Cell = CellInfo<String>;
#[derive(Debug, Clone)]
struct Alignments {
data: AlignmentHorizontal,
index: AlignmentHorizontal,
header: AlignmentHorizontal,
columns: HashMap<usize, AlignmentHorizontal>,
cells: HashMap<Position, AlignmentHorizontal>,
}
impl NuTable {
/// Creates an empty [Table] instance.
pub fn new(count_rows: usize, count_columns: usize) -> Self {
let data = VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]);
Self {
data,
size: (count_rows, count_columns),
data: VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]),
styles: Styles::default(),
alignments: Alignments::default(),
alignments: Alignments {
data: AlignmentHorizontal::Left,
index: AlignmentHorizontal::Right,
header: AlignmentHorizontal::Center,
columns: HashMap::default(),
cells: HashMap::default(),
},
}
}
/// Return amount of rows.
pub fn count_rows(&self) -> usize {
self.size.0
self.data.count_rows()
}
/// Return amount of columns.
pub fn count_columns(&self) -> usize {
self.size.1
self.data.count_columns()
}
pub fn insert(&mut self, pos: Position, text: String) {
@ -79,7 +94,7 @@ impl NuTable {
}
}
pub fn set_cell_style(&mut self, pos: Position, style: TextStyle) {
pub fn insert_style(&mut self, pos: Position, style: TextStyle) {
if let Some(style) = style.color_style {
let style = AnsiColor::from(convert_style(style));
self.styles.data.insert(Entity::Cell(pos.0, pos.1), style);
@ -123,12 +138,12 @@ impl NuTable {
/// Converts a table to a String.
///
/// It returns None in case where table cannot be fit to a terminal width.
pub fn draw(self, config: TableConfig, termwidth: usize) -> Option<String> {
pub fn draw(self, config: NuTableConfig, termwidth: usize) -> Option<String> {
build_table(self.data, config, self.alignments, self.styles, termwidth)
}
/// Return a total table width.
pub fn total_width(&self, config: &TableConfig) -> usize {
pub fn total_width(&self, config: &NuTableConfig) -> usize {
let config = get_config(&config.theme, false, None);
let widths = build_width(&self.data);
get_total_width2(&widths, &config)
@ -137,107 +152,43 @@ impl NuTable {
impl From<Vec<Vec<CellInfo<String>>>> for NuTable {
fn from(value: Vec<Vec<CellInfo<String>>>) -> Self {
let data = VecRecords::new(value);
let size = (data.count_rows(), data.count_columns());
Self {
data,
size,
alignments: Alignments::default(),
styles: Styles::default(),
}
let mut nutable = Self::new(0, 0);
nutable.data = VecRecords::new(value);
nutable
}
}
#[derive(Debug, Clone)]
pub struct TableConfig {
theme: TableTheme,
trim: TrimStrategy,
split_color: Option<Style>,
expand: bool,
with_index: bool,
with_header: bool,
with_footer: bool,
pub struct NuTableConfig {
pub theme: TableTheme,
pub trim: TrimStrategy,
pub split_color: Option<Style>,
pub expand: bool,
pub with_index: bool,
pub with_header: bool,
pub with_footer: bool,
pub header_on_border: bool,
}
impl TableConfig {
pub fn new() -> Self {
impl Default for NuTableConfig {
fn default() -> Self {
Self {
theme: TableTheme::basic(),
trim: TrimStrategy::truncate(None),
with_header: false,
with_index: false,
with_footer: false,
expand: false,
trim: TrimStrategy::truncate(None),
split_color: None,
}
}
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(),
header_on_border: false,
}
}
}
fn build_table(
mut data: Data,
cfg: TableConfig,
mut data: NuTableData,
cfg: NuTableConfig,
alignments: Alignments,
styles: Styles,
termwidth: usize,
@ -259,28 +210,71 @@ fn build_table(
}
fn draw_table(
data: Data,
data: NuTableData,
alignments: Alignments,
styles: Styles,
widths: Vec<usize>,
cfg: TableConfig,
cfg: NuTableConfig,
termwidth: usize,
) -> 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 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);
align_table(&mut table, alignments, 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 = if total_width > termwidth {
table_trim_columns(&mut table, widths, termwidth, &cfg.trim);
if total_width > termwidth {
table_trim_columns(table, widths, termwidth, &cfg.trim);
table.total_width()
} else if cfg.expand && termwidth > total_width {
table.with(Settings::new(
@ -291,13 +285,6 @@ fn draw_table(
termwidth
} else {
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;
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.
fn truncate_columns_by_content(
data: &mut Data,
data: &mut NuTableData,
theme: &TableTheme,
termwidth: 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
fn truncate_columns_by_columns(
data: &mut Data,
data: &mut NuTableData,
theme: &TableTheme,
termwidth: usize,
) -> Vec<usize> {
@ -620,7 +611,7 @@ fn get_config(theme: &TableTheme, with_header: bool, color: Option<Style>) -> Co
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 mut inner: Vec<Vec<_>> = records.into();
@ -632,7 +623,7 @@ fn push_empty_column(data: &mut Data) {
*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 mut inner: Vec<Vec<_>> = records.into();
@ -642,7 +633,7 @@ fn duplicate_row(data: &mut Data, row: usize) {
*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 mut inner: Vec<Vec<_>> = records.into();
@ -697,3 +688,178 @@ fn build_width(records: &VecRecords<CellInfo<String>>) -> Vec<usize> {
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
}
pub fn has_horizontals(&self) -> bool {
self.full_theme.get_borders().has_horizontal()
}
pub fn get_theme_full(&self) -> RawStyle {
self.full_theme.clone()
}

View file

@ -3,16 +3,17 @@ use nu_protocol::{Config, Span, Value};
use crate::UnstructuredTable;
use super::{
clean_charset, general::BuildConfig, get_index_style, load_theme_from_config,
value_to_styled_string, StringResult,
use crate::common::nu_value_to_string_clean;
use crate::{
common::{get_index_style, load_theme_from_config},
StringResult, TableOpts,
};
pub struct CollapsedTable;
impl CollapsedTable {
pub fn build(value: Value, opts: BuildConfig<'_>) -> StringResult {
collapsed_table(value, opts.config, opts.term_width, opts.style_computer)
pub fn build(value: Value, opts: TableOpts<'_>) -> StringResult {
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 => {
let (text, style) = value_to_styled_string(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;
}
let (text, style) = nu_value_to_string_clean(value, config, style_computer);
if let Some(color) = style.color_style {
let text = color.paint(text).to_string();
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_engine::column::get_columns;
use nu_protocol::{ast::PathMember, Config, Span, TableIndexMode, Value};
use std::collections::HashMap;
use std::sync::Arc;
use std::{cmp::max, sync::atomic::AtomicBool};
use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value};
use tabled::grid::config::Position;
use crate::{string_width, Cell, NuTable};
use super::{clean_charset, value_to_clean_styled_string};
use super::{
create_table_config, error_sign, general::BuildConfig, get_header_style, get_index_style,
load_theme_from_config, set_data_styles, value_to_styled_string, wrap_text, NuText,
StringResult, TableOutput, TableResult, INDEX_COLUMN_NAME,
use crate::{
common::{
create_nu_table_config, error_sign, get_header_style, get_index_style,
load_theme_from_config, nu_value_to_string, nu_value_to_string_clean, wrap_text, NuText,
StringResult, TableResult, INDEX_COLUMN_NAME,
},
string_width, NuTable, NuTableCell, TableOpts, TableOutput,
};
#[derive(Debug, Clone)]
@ -30,69 +31,35 @@ impl ExpandedTable {
}
}
pub fn build_value(&self, item: &Value, opts: BuildConfig<'_>) -> NuText {
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_entry2(item, opts)
pub fn build_value(self, item: &Value, opts: TableOpts<'_>) -> NuText {
expanded_table_entry2(item, Cfg { opts, format: self })
}
pub fn build_map(
&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_map(self, cols: &[String], vals: &[Value], opts: TableOpts<'_>) -> StringResult {
expanded_table_kv(cols, vals, Cfg { opts, format: self })
}
pub fn build_list(
&self,
vals: &[Value],
opts: BuildConfig<'_>,
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(),
pub fn build_list(self, vals: &[Value], opts: TableOpts<'_>) -> StringResult {
let cfg = Cfg {
opts: opts.clone(),
format: self,
};
let out = match expanded_table_list(vals, row_offset, opts1)? {
let out = match expanded_table_list(vals, cfg)? {
Some(out) => out,
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)]
struct Options<'a> {
ctrlc: Option<Arc<AtomicBool>>,
config: &'a Config,
style_computer: &'a StyleComputer<'a>,
available_width: usize,
struct Cfg<'a> {
opts: TableOpts<'a>,
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 SPLIT_LINE_SPACE: usize = 1;
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
let mut available_width = opts
.available_width
let mut available_width = cfg
.opts
.width
.saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE);
if available_width < MIN_CELL_CONTENT_WIDTH {
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 with_index = match opts.config.table_index_mode {
let with_index = match cfg.opts.config.table_index_mode {
TableIndexMode::Always => true,
TableIndexMode::Never => false,
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_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() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) {
return Ok(None);
}
@ -146,14 +114,15 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
return Err(*error.clone());
}
let index = row + row_offset;
let index = row + cfg.opts.row_offset;
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());
let value = Cell::new(text);
let row = row + with_header as usize;
let value = NuTableCell::new(text);
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() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) {
return Ok(None);
}
@ -185,9 +154,9 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
return Err(*error.clone());
}
let mut oopts = opts.clone();
oopts.available_width = available_width;
let (mut text, style) = expanded_table_entry2(item, oopts.clone());
let mut inner_cfg = cfg.clone();
inner_cfg.opts.width = available_width;
let (mut text, style) = expanded_table_entry2(item, inner_cfg);
let value_width = string_width(&text);
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?
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_styles.insert((row, with_index as usize), style);
}
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);
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() {
if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) {
return Ok(None);
}
@ -272,27 +241,27 @@ fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> Tab
return Err(*error.clone());
}
let mut oopts = opts.clone();
oopts.available_width = available;
let (mut text, style) = expanded_table_entry(item, header.as_str(), oopts);
let mut inner_cfg = cfg.clone();
inner_cfg.opts.width = available;
let (mut text, style) = expanded_table_entry(item, header.as_str(), inner_cfg);
let mut value_width = string_width(&text);
if value_width > available {
// it must only happen when a string is produced, so we can safely wrap it.
// (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;
}
column_width = max(column_width, value_width);
let value = Cell::new(text);
let value = NuTableCell::new(text);
data[row + 1].push(value);
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);
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;
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 {
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);
table.set_index_style(get_index_style(opts.style_computer));
table.set_header_style(get_header_style(opts.style_computer));
table.set_index_style(get_index_style(cfg.opts.style_computer));
table.set_header_style(get_header_style(cfg.opts.style_computer));
set_data_styles(&mut table, data_styles);
Ok(Some(TableOutput::new(table, true, with_index)))
}
fn expanded_table_kv(cols: &[String], vals: &[Value], opts: Options<'_>) -> StringResult {
let theme = load_theme_from_config(opts.config);
fn expanded_table_kv(cols: &[String], vals: &[Value], cfg: Cfg<'_>) -> StringResult {
let theme = load_theme_from_config(cfg.opts.config);
let key_width = cols.iter().map(|col| string_width(col)).max().unwrap_or(0);
let count_borders =
theme.has_inner() as usize + theme.has_right() as usize + theme.has_left() as usize;
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);
}
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());
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);
}
let is_limited = matches!(opts.format.expand_limit, Some(0));
let mut is_expanded = false;
let value = if is_limited {
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)
}
}
let (value, is_expanded) = match expand_table_value(value, value_width, &cfg)? {
Some(val) => val,
None => return Ok(None),
};
// 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');
}
let key = Cell::new(key);
let val = Cell::new(value);
let key = NuTableCell::new(key);
let val = NuTableCell::new(value);
let row = vec![key, val];
data.push(row);
}
let mut table = NuTable::from(data);
let keys_style = get_header_style(opts.style_computer).alignment(Alignment::Left);
table.set_index_style(keys_style);
table.set_index_style(get_key_style(&cfg));
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 {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String {
val,
span: opts.span,
span: cfg.opts.span,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => expanded_table_entry2(&val, opts),
Err(_) => error_sign(opts.style_computer),
Ok(val) => expanded_table_entry2(&val, cfg),
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 {
let is_limit_reached = matches!(opts.format.expand_limit, Some(0));
fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> NuText {
let is_limit_reached = matches!(cfg.format.expand_limit, Some(0));
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 {
Value::Record { cols, vals, span } => {
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
let oopts = dive_options(&opts, *span);
let table = expanded_table_kv(cols, vals, oopts);
let inner_cfg = dive_options(&cfg, *span);
let table = expanded_table_kv(cols, vals, inner_cfg);
match table {
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 } => {
if opts.format.flatten && is_simple_list(vals) {
if cfg.format.flatten && is_simple_list(vals) {
return value_list_to_string(
vals,
opts.config,
opts.style_computer,
&opts.format.flatten_sep,
cfg.opts.config,
cfg.opts.style_computer,
&cfg.format.flatten_sep,
);
}
let oopts = dive_options(&opts, *span);
let table = expanded_table_list(vals, 0, oopts);
let inner_cfg = dive_options(&cfg, *span);
let table = expanded_table_list(vals, inner_cfg);
let out = match table {
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);
match table {
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);
}
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, TextStyle::default())
}
fn dive_options<'b>(opts: &Options<'b>, span: Span) -> Options<'b> {
let mut opts = opts.clone();
opts.span = span;
if let Some(deep) = opts.format.expand_limit.as_mut() {
fn dive_options<'b>(cfg: &Cfg<'b>, span: Span) -> Cfg<'b> {
let mut cfg = cfg.clone();
cfg.opts.span = span;
if let Some(deep) = cfg.format.expand_limit.as_mut() {
*deep -= 1
}
opts
cfg
}
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))
}
fn maybe_expand_table(
out: TableOutput,
term_width: usize,
config: &Config,
style_computer: &StyleComputer,
) -> StringResult {
let mut table_config = create_table_config(config, style_computer, &out);
fn maybe_expand_table(out: TableOutput, term_width: usize, opts: &TableOpts<'_>) -> StringResult {
let mut table_config = create_nu_table_config(opts.config, opts.style_computer, &out, false);
let total_width = out.table.total_width(&table_config);
if total_width < term_width {
const EXPAND_THRESHOLD: f32 = 0.80;
let used_percent = total_width as f32 / term_width as f32;
let need_expansion = total_width < term_width && used_percent > EXPAND_THRESHOLD;
if need_expansion {
table_config = table_config.expand(true);
table_config.expand = true;
}
}
let output = out.table.draw(table_config, term_width);
Ok(output)
Ok(out.table.draw(table_config, term_width))
}
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_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use crate::{Cell, NuTable, NuText};
use super::{
clean_charset, create_table_config, get_empty_style, get_header_style, get_index_style,
get_value_style, StringResult, TableOutput, TableResult, INDEX_COLUMN_NAME,
use crate::{
clean_charset,
common::{
create_nu_table_config, get_empty_style, get_header_style, get_index_style,
get_value_style, NuText, INDEX_COLUMN_NAME,
},
NuTable, NuTableCell, StringResult, TableOpts, TableOutput, TableResult,
};
pub struct JustTable;
impl JustTable {
pub fn table(input: &[Value], row_offset: usize, opts: BuildConfig<'_>) -> StringResult {
let out = match table(input, row_offset, opts.clone())? {
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 table(input: &[Value], opts: TableOpts<'_>) -> StringResult {
create_table(input, opts)
}
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)
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig<'a> {
pub(crate) ctrlc: Option<Arc<AtomicBool>>,
pub(crate) config: &'a Config,
pub(crate) style_computer: &'a StyleComputer<'a>,
pub(crate) span: Span,
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,
fn create_table(input: &[Value], opts: TableOpts<'_>) -> Result<Option<String>, ShellError> {
match table(input, opts.row_offset, opts.clone())? {
Some(out) => {
let table_config =
create_nu_table_config(opts.config, opts.style_computer, &out, false);
Ok(out.table.draw(table_config, opts.width))
}
}
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()
None => Ok(None),
}
}
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()];
for ((column, value), row) in cols.iter().zip(vals.iter()).zip(data.iter_mut()) {
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);
}
let key = Cell::new(column.to_string());
let value = Cell::new(value);
let key = NuTableCell::new(column.to_string());
let value = NuTableCell::new(value);
row.push(key);
row.push(value);
}
@ -101,13 +57,13 @@ fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> StringRes
table.set_index_style(TextStyle::default_field());
let out = TableOutput::new(table, false, true);
let table_config = create_table_config(opts.config, opts.style_computer, &out);
let table = out.table.draw(table_config, opts.term_width);
let table_config = create_nu_table_config(opts.config, opts.style_computer, &out, false);
let table = out.table.draw(table_config, opts.width);
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() {
return Ok(None);
}
@ -148,7 +104,7 @@ fn to_table_with_header(
headers: Vec<String>,
with_index: bool,
row_offset: usize,
opts: BuildConfig<'_>,
opts: TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> {
let count_rows = input.len() + 1;
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);
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],
with_index: bool,
row_offset: usize,
opts: BuildConfig<'_>,
opts: TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> {
let mut table = NuTable::new(input.len(), with_index as usize + 1);
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);
table.insert(pos, text);
table.set_cell_style(pos, style);
table.insert_style(pos, style);
}
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 {
Value::Record { .. } => {
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 is_string_value = matches!(item, Value::String { .. });
if is_string_value {

View file

@ -2,20 +2,15 @@ mod collapse;
mod expanded;
mod general;
use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_protocol::TrimStrategy;
use nu_protocol::{Config, FooterMode, ShellError, Span, Value};
use std::collections::HashMap;
use crate::{string_wrap, NuTable, TableConfig, TableTheme};
use std::sync::{atomic::AtomicBool, Arc};
pub use collapse::CollapsedTable;
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);
pub type TableResult = Result<Option<TableOutput>, ShellError>;
pub type StringResult = Result<Option<String>, ShellError>;
use crate::NuTable;
pub struct TableOutput {
pub table: NuTable,
@ -33,176 +28,32 @@ impl TableOutput {
}
}
pub fn value_to_styled_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)
#[derive(Debug, Clone)]
pub struct TableOpts<'a> {
ctrlc: Option<Arc<AtomicBool>>,
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 {
let (text, style) = value_to_styled_string(val, cfg, style);
let text = clean_charset(&text);
(text, style)
}
pub fn clean_charset(text: &str) -> String {
// todo: optimize, I bet it can be done in 1 path
text.replace('\t', " ").replace('\r', "")
}
const INDEX_COLUMN_NAME: &str = "index";
fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) {
make_styled_string(style_computer, String::from(""), None, 0)
}
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())),
),
)
impl<'a> TableOpts<'a> {
pub fn new(
config: &'a Config,
style_computer: &'a StyleComputer<'a>,
ctrlc: Option<Arc<AtomicBool>>,
span: Span,
row_offset: usize,
available_width: usize,
) -> Self {
Self {
ctrlc,
config,
style_computer,
span,
row_offset,
width: available_width,
}
}
}
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()
}
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)]
use nu_table::{string_width, NuTable, TableConfig};
use nu_table::{string_width, NuTable, NuTableConfig};
use tabled::grid::records::vec_records::CellInfo;
pub struct TestCase {
cfg: TableConfig,
cfg: NuTableConfig,
termwidth: usize,
expected: Option<String>,
}
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 {
cfg,
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);
table.draw(config, termwidth)
}

View file

@ -1,7 +1,7 @@
mod common;
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 tabled::grid::records::vec_records::CellInfo;
@ -9,11 +9,13 @@ use tabled::grid::records::vec_records::CellInfo;
#[test]
fn data_and_header_has_different_size_doesnt_work() {
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(
TableConfig::new().theme(theme::heavy()).with_header(true),
usize::MAX,
);
let table = table.draw(cfg.clone(), usize::MAX);
assert_eq!(
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 = table.draw(
TableConfig::new().theme(theme::heavy()).with_header(true),
usize::MAX,
);
let table = table.draw(cfg, usize::MAX);
assert_eq!(
table.as_deref(),
@ -49,7 +48,7 @@ fn data_and_header_has_different_size_doesnt_work() {
#[test]
fn termwidth_too_small() {
let test_loop = |config: TableConfig| {
let test_loop = |config: NuTableConfig| {
for i in 0..10 {
let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
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();
test_loop(config);
let config = base_config.clone().trim(TrimStrategy::truncate(None));
test_loop(config);
let config = base_config
.clone()
.trim(TrimStrategy::truncate(Some(String::from("**"))));
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);
for case in [
TrimStrategy::truncate(None),
TrimStrategy::truncate(Some(String::from("**"))),
TrimStrategy::truncate(Some(String::from(""))),
TrimStrategy::wrap(false),
TrimStrategy::wrap(true),
] {
cfg.trim = case;
test_loop(cfg.clone());
}
}
#[test]
@ -204,11 +196,12 @@ fn width_control_test_0() {
}
fn test_width(data: Vec<Vec<CellInfo<String>>>, tests: &[(usize, &str)]) {
let trim = TrimStrategy::truncate(Some(String::from("...")));
let config = TableConfig::new()
.theme(theme::heavy())
.with_header(true)
.trim(trim);
let config = NuTableConfig {
theme: theme::heavy(),
trim: TrimStrategy::truncate(Some(String::from("..."))),
with_header: true,
..Default::default()
};
let tests = tests.iter().map(|&(termwidth, expected)| {
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) {
let config = TableConfig::new()
.theme(theme::heavy())
.with_header(true)
.trim(trim);
let config = NuTableConfig {
theme: theme::heavy(),
with_header: true,
trim,
..Default::default()
};
let tests = tests.iter().map(|&(termwidth, expected)| {
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 nu_table::{TableConfig, TableTheme as theme};
use nu_table::{NuTableConfig, TableTheme as theme};
#[test]
fn test_expand() {
let table = create_table(
vec![create_row(4); 3],
TableConfig::new()
.theme(theme::rounded())
.with_header(true)
.expand(true),
NuTableConfig {
theme: theme::rounded(),
with_header: true,
expand: true,
..Default::default()
},
50,
);

View file

@ -1,7 +1,7 @@
mod common;
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;
#[test]
@ -476,10 +476,11 @@ fn test_with_love() {
}
fn create_table(data: Vec<Vec<CellInfo<String>>>, with_header: bool, theme: theme) -> String {
let mut config = TableConfig::new().theme(theme);
if with_header {
config = config.with_header(true);
}
let config = NuTableConfig {
theme,
with_header,
..Default::default()
};
let out = common::create_table(data, config, usize::MAX);
@ -491,10 +492,11 @@ fn create_table_with_size(
with_header: bool,
theme: theme,
) -> String {
let mut config = TableConfig::new().theme(theme);
if with_header {
config = config.with_header(true);
}
let config = NuTableConfig {
theme,
with_header,
..Default::default()
};
let table = NuTable::from(data);