Revert "Add an option to move header on borders" (#9908)

Reverts nushell/nushell#9796

This is just draft since we're seeing some issues with the latest fixes
to table drawing that just landed with #9796. We're hoping to get these
fixed, but if we're not able to fix them before the next release, we'll
need to revert (hence this PR, just in case we need it).
This commit is contained in:
JT 2023-08-04 07:52:12 +12:00 committed by GitHub
parent 572698bf3e
commit a98b3124c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 747 additions and 762 deletions

23
Cargo.lock generated
View file

@ -2737,7 +2737,7 @@ dependencies = [
"sha2", "sha2",
"sqlparser", "sqlparser",
"sysinfo", "sysinfo",
"tabled 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "tabled",
"terminal_size 0.2.6", "terminal_size 0.2.6",
"titlecase", "titlecase",
"toml", "toml",
@ -2904,7 +2904,7 @@ dependencies = [
"nu-engine", "nu-engine",
"nu-protocol", "nu-protocol",
"nu-utils", "nu-utils",
"tabled 0.13.0 (git+https://github.com/zhiburt/tabled/?rev=6c51e3eaa362914a71b868ccb78e7addbebd3657)", "tabled",
] ]
[[package]] [[package]]
@ -3289,9 +3289,9 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]] [[package]]
name = "papergrid" name = "papergrid"
version = "0.10.0" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ccbe15f2b6db62f9a9871642746427e297b0ceb85f9a7f1ee5ff47d184d0c8" checksum = "ae7891b22598926e4398790c8fe6447930c72a67d36d983a49d6ce682ce83290"
dependencies = [ dependencies = [
"ansi-str", "ansi-str",
"ansitok", "ansitok",
@ -4947,20 +4947,9 @@ dependencies = [
[[package]] [[package]]
name = "tabled" name = "tabled"
version = "0.13.0" version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d38d39c754ae037a9bc3ca1580a985db7371cd14f1229172d1db9093feb6739" checksum = "0ce69a5028cd9576063ec1f48edb2c75339fd835e6094ef3e05b3a079bf594a6"
dependencies = [
"ansi-str",
"ansitok",
"papergrid",
"unicode-width",
]
[[package]]
name = "tabled"
version = "0.13.0"
source = "git+https://github.com/zhiburt/tabled/?rev=6c51e3eaa362914a71b868ccb78e7addbebd3657#6c51e3eaa362914a71b868ccb78e7addbebd3657"
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.13.0", features = ["color"], default-features = false } tabled = { version = "0.12.2", 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,13 +6,12 @@ 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, IntoPipelineData, ListStream, PipelineData, Category, Config, DataSource, Example, FooterMode, 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::{
CollapsedTable, ExpandedTable, JustTable, NuTable, NuTableCell, StringResult, TableOpts, BuildConfig, Cell, CollapsedTable, ExpandedTable, JustTable, NuTable, StringResult,
TableOutput, TableConfig, TableOutput, TableTheme,
}; };
use nu_utils::get_ls_colors; use nu_utils::get_ls_colors;
use std::sync::Arc; use std::sync::Arc;
@ -362,8 +361,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 = TableOpts::new(config, style_computer, ctrlc, span, 0, term_width); let opts = BuildConfig::new(ctrlc, config, style_computer, span, term_width);
let result = build_table_kv(cols, vals, table_view, opts, span)?; let result = build_table_kv(cols, vals, table_view, opts)?;
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),
@ -392,8 +391,7 @@ fn build_table_kv(
cols: Vec<String>, cols: Vec<String>,
vals: Vec<Value>, vals: Vec<Value>,
table_view: TableView, table_view: TableView,
opts: TableOpts<'_>, opts: BuildConfig<'_>,
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),
@ -406,6 +404,7 @@ 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)
} }
@ -415,20 +414,21 @@ fn build_table_kv(
fn build_table_batch( fn build_table_batch(
vals: Vec<Value>, vals: Vec<Value>,
table_view: TableView, table_view: TableView,
opts: TableOpts<'_>, row_offset: usize,
span: Span, opts: BuildConfig<'_>,
) -> StringResult { ) -> StringResult {
match table_view { match table_view {
TableView::General => JustTable::table(&vals, opts), TableView::General => JustTable::table(&vals, row_offset, 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) ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts, row_offset)
} }
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,16 +647,20 @@ impl PagingTableCreator {
return Ok(None); return Ok(None);
} }
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 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 view = TableView::Expanded { let view = TableView::Expanded {
limit, limit,
flatten, flatten,
flatten_separator, flatten_separator,
}; };
build_table_batch(batch, view, opts, self.head) build_table_batch(batch, view, self.row_offset, opts)
} }
fn build_collapsed(&mut self, batch: Vec<Value>) -> StringResult { fn build_collapsed(&mut self, batch: Vec<Value>) -> StringResult {
@ -664,34 +668,26 @@ impl PagingTableCreator {
return Ok(None); return Ok(None);
} }
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 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);
build_table_batch(batch, TableView::Collapsed, opts, self.head) build_table_batch(batch, TableView::Collapsed, self.row_offset, opts)
} }
fn build_general(&mut self, batch: Vec<Value>) -> StringResult { fn build_general(&mut self, batch: Vec<Value>) -> StringResult {
let cfg = get_config(&self.engine_state, &self.stack); let term_width = get_width_param(self.width_param);
let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); let config = get_config(&self.engine_state, &self.stack);
let opts = self.create_table_opts(&cfg, &style_comp); 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);
build_table_batch(batch, TableView::General, opts, self.head) build_table_batch(batch, TableView::General, row_offset, opts)
}
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),
)
} }
} }
@ -784,6 +780,22 @@ 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,
@ -847,6 +859,34 @@ 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,
@ -858,14 +898,14 @@ fn create_empty_placeholder(
return String::new(); return String::new();
} }
let cell = NuTableCell::new(format!("empty {}", value_type_name)); let cell = Cell::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_data_style(TextStyle::default().dimmed()); table.set_cell_style((0, 0), 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_nu_table_config(&config, style_computer, &out, false); let config = create_table_config(&config, style_computer, &out);
out.table out.table
.draw(config, termwidth) .draw(config, termwidth)

View file

@ -1,9 +1,6 @@
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_protocol::{Span, Value}; use nu_protocol::{Span, Value};
use nu_table::{ use nu_table::{value_to_clean_styled_string, value_to_styled_string, BuildConfig, ExpandedTable};
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;
@ -21,9 +18,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 { .. }) => {
nu_value_to_string_clean(&val, config, style_computer).0 value_to_clean_styled_string(&val, config, style_computer).0
} }
val => nu_value_to_string(&val, config, style_computer).0, val => value_to_styled_string(&val, config, style_computer).0,
} }
} }
@ -35,19 +32,12 @@ fn try_build_map(
ctrlc: Option<Arc<AtomicBool>>, ctrlc: Option<Arc<AtomicBool>>,
config: &NuConfig, config: &NuConfig,
) -> String { ) -> String {
let opts = TableOpts::new( let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX);
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(_) => {
nu_value_to_string(&Value::Record { cols, vals, span }, config, style_computer).0 value_to_styled_string(&Value::Record { cols, vals, span }, config, style_computer).0
} }
} }
} }
@ -59,20 +49,13 @@ fn try_build_list(
span: Span, span: Span,
style_computer: &StyleComputer, style_computer: &StyleComputer,
) -> String { ) -> String {
let opts = TableOpts::new( let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX);
config, let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts, 0);
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
nu_value_to_string(&Value::List { vals, span }, config, style_computer).0 value_to_styled_string(&Value::List { vals, span }, config, style_computer).0
} }
} }
} }

View file

@ -70,7 +70,6 @@ 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>,
@ -127,7 +126,6 @@ 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,
@ -928,9 +926,6 @@ 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 = { git = "https://github.com/zhiburt/tabled/", rev = "6c51e3eaa362914a71b868ccb78e7addbebd3657", features = ["color"], default-features = false } tabled = { version = "0.12.2", 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, NuTableConfig, TableTheme}; use nu_table::{NuTable, TableConfig, TableTheme};
use tabled::grid::records::vec_records::CellInfo; use tabled::grid::records::vec_records::CellInfo;
fn main() { fn main() {
@ -29,11 +29,8 @@ 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 table_cfg = NuTableConfig { let theme = TableTheme::rounded();
theme: TableTheme::rounded(), let table_cfg = TableConfig::new().theme(theme).with_header(true);
with_header: true,
..Default::default()
};
let output_table = table let output_table = table
.draw(table_cfg, width) .draw(table_cfg, width)

View file

@ -1,175 +0,0 @@
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::{NuTable, NuTableCell, NuTableConfig}; pub use table::{Alignments, Cell, NuTable, TableConfig};
pub use table_theme::TableTheme; pub use table_theme::TableTheme;
pub use types::{CollapsedTable, ExpandedTable, JustTable, TableOpts, TableOutput}; pub use types::{
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

@ -12,27 +12,25 @@ use tabled::{
dimension::CompleteDimensionVecRecords, dimension::CompleteDimensionVecRecords,
records::{ records::{
vec_records::{CellInfo, VecRecords}, vec_records::{CellInfo, VecRecords},
ExactRecords, PeekableRecords, Records, Resizable, ExactRecords, Records,
}, },
}, },
settings::{ settings::{
formatting::AlignmentStrategy, object::Segment, peaker::Peaker, themes::ColumnNames, Color, formatting::AlignmentStrategy, object::Segment, peaker::Peaker, Color, Modify, Settings,
Modify, Settings, TableOption, Width, TableOption, Width,
}, },
Table, Table,
}; };
/// NuTable is a table rendering implementation. /// Table represent a table view.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NuTable { pub struct NuTable {
data: NuTableData, data: Data,
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>,
@ -41,39 +39,27 @@ struct Styles {
data_is_set: bool, data_is_set: bool,
} }
#[derive(Debug, Clone)] type Data = VecRecords<Cell>;
struct Alignments { pub type Cell = CellInfo<String>;
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: VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]), data,
size: (count_rows, count_columns),
styles: Styles::default(), styles: Styles::default(),
alignments: Alignments { alignments: Alignments::default(),
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.data.count_rows() self.size.0
} }
/// Return amount of columns.
pub fn count_columns(&self) -> usize { pub fn count_columns(&self) -> usize {
self.data.count_columns() self.size.1
} }
pub fn insert(&mut self, pos: Position, text: String) { pub fn insert(&mut self, pos: Position, text: String) {
@ -93,7 +79,7 @@ impl NuTable {
} }
} }
pub fn insert_style(&mut self, pos: Position, style: TextStyle) { pub fn set_cell_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);
@ -137,12 +123,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: NuTableConfig, termwidth: usize) -> Option<String> { pub fn draw(self, config: TableConfig, 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: &NuTableConfig) -> usize { pub fn total_width(&self, config: &TableConfig) -> 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)
@ -151,43 +137,107 @@ 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 mut nutable = Self::new(0, 0); let data = VecRecords::new(value);
nutable.data = VecRecords::new(value); let size = (data.count_rows(), data.count_columns());
Self {
nutable data,
size,
alignments: Alignments::default(),
styles: Styles::default(),
}
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NuTableConfig { pub struct TableConfig {
pub theme: TableTheme, theme: TableTheme,
pub trim: TrimStrategy, trim: TrimStrategy,
pub split_color: Option<Style>, split_color: Option<Style>,
pub expand: bool, expand: bool,
pub with_index: bool, with_index: bool,
pub with_header: bool, with_header: bool,
pub with_footer: bool, with_footer: bool,
pub header_on_border: bool,
} }
impl Default for NuTableConfig { impl TableConfig {
fn default() -> Self { pub fn new() -> 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: NuTableData, mut data: Data,
cfg: NuTableConfig, cfg: TableConfig,
alignments: Alignments, alignments: Alignments,
styles: Styles, styles: Styles,
termwidth: usize, termwidth: usize,
@ -209,24 +259,22 @@ fn build_table(
} }
fn draw_table( fn draw_table(
data: NuTableData, data: Data,
mut alignments: Alignments, alignments: Alignments,
mut styles: Styles, styles: Styles,
widths: Vec<usize>, widths: Vec<usize>,
cfg: NuTableConfig, cfg: TableConfig,
termwidth: usize, termwidth: usize,
) -> Option<String> { ) -> Option<String> {
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 need_header_move = cfg.header_on_border && has_horizontals_for_header(&cfg); let with_footer = cfg.with_footer;
let with_index = cfg.with_index; let with_index = cfg.with_index;
let with_header = cfg.with_header && table.count_rows() > 1 && !need_header_move; let with_header = cfg.with_header && table.count_rows() > 1;
let with_footer = cfg.with_footer && !need_header_move;
let sep_color = cfg.split_color; 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);
move_header_on_border(&mut table, &mut alignments, &mut styles, &cfg);
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);
@ -253,55 +301,6 @@ fn draw_table(
} }
} }
fn move_header_on_border(
table: &mut Table,
alignments: &mut Alignments,
styles: &mut Styles,
cfg: &NuTableConfig,
) {
if !cfg.header_on_border || table.count_rows() <= 1 {
return;
}
let color = Color::from(styles.header.clone());
let has_bottom_line = table
.get_config()
.has_horizontal(table.count_rows(), table.count_rows());
let has_top_line = table.get_config().has_horizontal(0, table.count_rows());
if cfg.with_header && has_top_line {
move_row_on_border(table, 0, color.clone(), alignments.header)
}
if cfg.with_footer && has_bottom_line {
let last_row = table.count_rows() - 1;
move_row_on_border(table, last_row, color, alignments.header)
}
// because we remove rows we will invalidate the data alignments and colors
// so we need to restore it back
if cfg.with_header && has_top_line {
if !alignments.cells.is_empty() {
for row in 1..table.count_rows() {
for col in 0..table.count_rows() {
let val = alignments.cells.get(&(row, col));
if let Some(val) = val {
alignments.cells.insert((row - 1, col), *val);
}
}
}
}
for row in 1..table.count_rows() {
for col in 0..table.count_rows() {
let val = styles.data.get(Entity::Cell(row, col)).clone();
styles.data.insert(Entity::Cell(row - 1, col), val);
}
}
}
}
fn align_table( fn align_table(
table: &mut Table, table: &mut Table,
alignments: Alignments, alignments: Alignments,
@ -430,11 +429,7 @@ fn table_trim_columns(
} }
} }
fn maybe_truncate_columns( fn maybe_truncate_columns(data: &mut Data, theme: &TableTheme, termwidth: usize) -> Vec<usize> {
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 {
@ -448,7 +443,7 @@ fn maybe_truncate_columns(
// 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 NuTableData, data: &mut Data,
theme: &TableTheme, theme: &TableTheme,
termwidth: usize, termwidth: usize,
) -> Vec<usize> { ) -> Vec<usize> {
@ -527,7 +522,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 NuTableData, data: &mut Data,
theme: &TableTheme, theme: &TableTheme,
termwidth: usize, termwidth: usize,
) -> Vec<usize> { ) -> Vec<usize> {
@ -625,7 +620,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 NuTableData) { fn push_empty_column(data: &mut Data) {
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();
@ -637,7 +632,7 @@ fn push_empty_column(data: &mut NuTableData) {
*data = VecRecords::new(inner); *data = VecRecords::new(inner);
} }
fn duplicate_row(data: &mut NuTableData, row: usize) { fn duplicate_row(data: &mut Data, 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();
@ -647,7 +642,7 @@ fn duplicate_row(data: &mut NuTableData, row: usize) {
*data = VecRecords::new(inner); *data = VecRecords::new(inner);
} }
fn truncate_columns(data: &mut NuTableData, count: usize) { fn truncate_columns(data: &mut Data, 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();
@ -702,39 +697,3 @@ fn build_width(records: &VecRecords<CellInfo<String>>) -> Vec<usize> {
widths widths
} }
fn move_row_on_border(table: &mut Table, row: usize, color: Color, alignment: AlignmentHorizontal) {
if table.is_empty() {
return;
}
let columns = (0..table.get_records().count_columns())
.map(|column| table.get_records().get_text((row, column)).to_owned())
.collect::<Vec<_>>();
table.get_records_mut().remove_row(row);
if table.is_empty() {
table.get_records_mut().push_row();
}
let mut line = 0;
if row != 0 {
line = row;
}
let colors = std::iter::repeat(color)
.take(table.count_columns())
.collect::<Vec<_>>();
let names = ColumnNames::new(columns)
.set_line(line)
.set_colors(colors)
.set_alignment(alignment);
table.with(names);
}
fn has_horizontals_for_header(cfg: &NuTableConfig) -> bool {
cfg.theme.get_theme().get_horizontal(0).is_some()
|| cfg.theme.get_theme().get_borders().has_horizontal()
}

View file

@ -191,10 +191,6 @@ 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,17 +3,16 @@ use nu_protocol::{Config, Span, Value};
use crate::UnstructuredTable; use crate::UnstructuredTable;
use crate::common::nu_value_to_string_clean; use super::{
use crate::{ clean_charset, general::BuildConfig, get_index_style, load_theme_from_config,
common::{get_index_style, load_theme_from_config}, value_to_styled_string, StringResult,
StringResult, TableOpts,
}; };
pub struct CollapsedTable; pub struct CollapsedTable;
impl CollapsedTable { impl CollapsedTable {
pub fn build(value: Value, opts: TableOpts<'_>) -> StringResult { pub fn build(value: Value, opts: BuildConfig<'_>) -> StringResult {
collapsed_table(value, opts.config, opts.width, opts.style_computer) collapsed_table(value, opts.config, opts.term_width, opts.style_computer)
} }
} }
@ -57,7 +56,20 @@ fn colorize_value(value: &mut Value, config: &Config, style_computer: &StyleComp
} }
} }
value => { value => {
let (text, style) = nu_value_to_string_clean(value, config, style_computer); 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;
}
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,18 +1,17 @@
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, ShellError, Span, TableIndexMode, Value}; use nu_protocol::{ast::PathMember, Config, Span, TableIndexMode, Value};
use tabled::grid::config::Position; use std::collections::HashMap;
use std::sync::Arc;
use std::{cmp::max, sync::atomic::AtomicBool};
use crate::{ use crate::{string_width, Cell, NuTable};
common::{
create_nu_table_config, error_sign, get_header_style, get_index_style, use super::{clean_charset, value_to_clean_styled_string};
load_theme_from_config, nu_value_to_string, nu_value_to_string_clean, wrap_text, NuText, use super::{
StringResult, TableResult, INDEX_COLUMN_NAME, 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,
string_width, NuTable, NuTableCell, TableOpts, TableOutput, StringResult, TableOutput, TableResult, INDEX_COLUMN_NAME,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -31,35 +30,69 @@ impl ExpandedTable {
} }
} }
pub fn build_value(self, item: &Value, opts: TableOpts<'_>) -> NuText { pub fn build_value(&self, item: &Value, opts: BuildConfig<'_>) -> NuText {
expanded_table_entry2(item, Cfg { opts, format: self }) let opts = Options {
} ctrlc: opts.ctrlc,
config: opts.config,
pub fn build_map(self, cols: &[String], vals: &[Value], opts: TableOpts<'_>) -> StringResult { style_computer: opts.style_computer,
expanded_table_kv(cols, vals, Cfg { opts, format: self }) 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, cfg)? { expanded_table_entry2(item, opts)
}
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_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(),
};
let out = match expanded_table_list(vals, row_offset, opts1)? {
Some(out) => out, Some(out) => out,
None => return Ok(None), None => return Ok(None),
}; };
maybe_expand_table(out, opts.width, &opts) maybe_expand_table(out, opts.term_width, opts.config, opts.style_computer)
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Cfg<'a> { struct Options<'a> {
opts: TableOpts<'a>, ctrlc: Option<Arc<AtomicBool>>,
config: &'a Config,
style_computer: &'a StyleComputer<'a>,
available_width: usize,
format: ExpandedTable, format: ExpandedTable,
span: Span,
} }
fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { fn expanded_table_list(input: &[Value], row_offset: usize, opts: Options) -> 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;
@ -72,9 +105,8 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
} }
// 2 - split lines // 2 - split lines
let mut available_width = cfg let mut available_width = opts
.opts .available_width
.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);
@ -82,7 +114,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
let headers = get_columns(input); let headers = get_columns(input);
let with_index = match cfg.opts.config.table_index_mode { let with_index = match 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),
@ -102,11 +134,11 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
if with_index { if with_index {
if with_header { if with_header {
data[0].push(NuTableCell::exact(String::from("#"), 1, vec![])); data[0].push(Cell::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(&cfg.opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None); return Ok(None);
} }
@ -114,15 +146,14 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
return Err(*error.clone()); return Err(*error.clone());
} }
let index = row + cfg.opts.row_offset; let index = row + row_offset;
let text = matches!(item, Value::Record { .. }) let text = matches!(item, Value::Record { .. })
.then(|| { .then(|| lookup_index_value(item, opts.config).unwrap_or_else(|| index.to_string()))
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);
} }
@ -146,7 +177,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
} }
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None); return Ok(None);
} }
@ -154,9 +185,9 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
return Err(*error.clone()); return Err(*error.clone());
} }
let mut inner_cfg = cfg.clone(); let mut oopts = opts.clone();
inner_cfg.opts.width = available_width; oopts.available_width = available_width;
let (mut text, style) = expanded_table_entry2(item, inner_cfg); let (mut text, style) = expanded_table_entry2(item, oopts.clone());
let value_width = string_width(&text); let value_width = string_width(&text);
if value_width > available_width { if value_width > available_width {
@ -165,16 +196,16 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
// //
// 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, cfg.opts.config); text = wrap_text(&text, available_width, opts.config);
} }
let value = NuTableCell::new(text); let value = Cell::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(cfg.opts.style_computer)); table.set_index_style(get_index_style(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)));
@ -233,7 +264,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
} }
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None); return Ok(None);
} }
@ -241,27 +272,27 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
return Err(*error.clone()); return Err(*error.clone());
} }
let mut inner_cfg = cfg.clone(); let mut oopts = opts.clone();
inner_cfg.opts.width = available; oopts.available_width = available;
let (mut text, style) = expanded_table_entry(item, header.as_str(), inner_cfg); let (mut text, style) = expanded_table_entry(item, header.as_str(), oopts);
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, cfg.opts.config); text = wrap_text(&text, available, opts.config);
value_width = available; value_width = available;
} }
column_width = max(column_width, value_width); column_width = max(column_width, value_width);
let value = NuTableCell::new(text); let value = Cell::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 = NuTableCell::new(header); let head_cell = Cell::new(header);
data[0].push(head_cell); data[0].push(head_cell);
if column_width > available { if column_width > available {
@ -321,7 +352,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
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 = NuTableCell::exact(String::from("..."), 3, vec![]); let shift = Cell::exact(String::from("..."), 3, vec![]);
for row in &mut data { for row in &mut data {
row.push(shift.clone()); row.push(shift.clone());
} }
@ -331,34 +362,92 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
} }
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_index_style(get_index_style(cfg.opts.style_computer)); table.set_index_style(get_index_style(opts.style_computer));
table.set_header_style(get_header_style(cfg.opts.style_computer)); table.set_header_style(get_header_style(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], cfg: Cfg<'_>) -> StringResult { fn expanded_table_kv(cols: &[String], vals: &[Value], opts: Options<'_>) -> StringResult {
let theme = load_theme_from_config(cfg.opts.config); let theme = load_theme_from_config(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 > cfg.opts.width { if key_width + count_borders + padding + padding > opts.available_width {
return Ok(None); return Ok(None);
} }
let value_width = cfg.opts.width - key_width - count_borders - padding - padding; let value_width = opts.available_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(&cfg.opts.ctrlc) { if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) {
return Ok(None); return Ok(None);
} }
let (value, is_expanded) = match expand_table_value(value, value_width, &cfg)? { let is_limited = matches!(opts.format.expand_limit, Some(0));
Some(val) => val, let mut is_expanded = false;
None => return Ok(None), 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)
}
}
}; };
// we want to have a key being aligned to 2nd line, // we want to have a key being aligned to 2nd line,
@ -369,154 +458,90 @@ fn expanded_table_kv(cols: &[String], vals: &[Value], cfg: Cfg<'_>) -> StringRes
key.insert(0, '\n'); key.insert(0, '\n');
} }
let key = NuTableCell::new(key); let key = Cell::new(key);
let val = NuTableCell::new(value); let val = Cell::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);
table.set_index_style(get_key_style(&cfg)); let keys_style = get_header_style(opts.style_computer).alignment(Alignment::Left);
table.set_index_style(keys_style);
let out = TableOutput::new(table, false, true); let out = TableOutput::new(table, false, true);
maybe_expand_table(out, cfg.opts.width, &cfg.opts) maybe_expand_table(out, opts.available_width, opts.config, opts.style_computer)
} }
// the flag is used as an optimization to not do `value.lines().count()` search. fn expanded_table_entry(item: &Value, header: &str, opts: Options<'_>) -> NuText {
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: cfg.opts.span, span: 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, cfg), Ok(val) => expanded_table_entry2(&val, opts),
Err(_) => error_sign(cfg.opts.style_computer), Err(_) => error_sign(opts.style_computer),
} }
} }
_ => expanded_table_entry2(item, cfg), _ => expanded_table_entry2(item, opts),
} }
} }
fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> NuText { fn expanded_table_entry2(item: &Value, opts: Options<'_>) -> NuText {
let is_limit_reached = matches!(cfg.format.expand_limit, Some(0)); let is_limit_reached = matches!(opts.format.expand_limit, Some(0));
if is_limit_reached { if is_limit_reached {
return nu_value_to_string_clean(item, cfg.opts.config, cfg.opts.style_computer); return value_to_clean_styled_string(item, opts.config, 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 nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer); return value_to_styled_string(item, opts.config, 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 inner_cfg = dive_options(&cfg, *span); let oopts = dive_options(&opts, *span);
let table = expanded_table_kv(cols, vals, inner_cfg); let table = expanded_table_kv(cols, vals, oopts);
match table { match table {
Ok(Some(table)) => (table, TextStyle::default()), Ok(Some(table)) => (table, TextStyle::default()),
_ => nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer), _ => value_to_styled_string(item, opts.config, opts.style_computer),
} }
} }
Value::List { vals, span } => { Value::List { vals, span } => {
if cfg.format.flatten && is_simple_list(vals) { if opts.format.flatten && is_simple_list(vals) {
return value_list_to_string( return value_list_to_string(
vals, vals,
cfg.opts.config, opts.config,
cfg.opts.style_computer, opts.style_computer,
&cfg.format.flatten_sep, &opts.format.flatten_sep,
); );
} }
let inner_cfg = dive_options(&cfg, *span); let oopts = dive_options(&opts, *span);
let table = expanded_table_list(vals, inner_cfg); let table = expanded_table_list(vals, 0, oopts);
let out = match table { let out = match table {
Ok(Some(out)) => out, Ok(Some(out)) => out,
_ => return nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer), _ => return value_to_styled_string(item, opts.config, opts.style_computer),
}; };
let table_config = create_table_cfg(&cfg, &out); let table_config = create_table_config(opts.config, opts.style_computer, &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 => nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer), None => value_to_styled_string(item, opts.config, opts.style_computer),
} }
} }
_ => nu_value_to_string_clean(item, cfg.opts.config, cfg.opts.style_computer), _ => value_to_clean_styled_string(item, opts.config, opts.style_computer),
} }
} }
@ -537,21 +562,21 @@ fn value_list_to_string(
buf.push_str(flatten_sep); buf.push_str(flatten_sep);
} }
let text = nu_value_to_string_clean(value, config, style_computer).0; let (text, _) = value_to_clean_styled_string(value, config, style_computer);
buf.push_str(&text); buf.push_str(&text);
} }
(buf, TextStyle::default()) (buf, TextStyle::default())
} }
fn dive_options<'b>(cfg: &Cfg<'b>, span: Span) -> Cfg<'b> { fn dive_options<'b>(opts: &Options<'b>, span: Span) -> Options<'b> {
let mut cfg = cfg.clone(); let mut opts = opts.clone();
cfg.opts.span = span; opts.span = span;
if let Some(deep) = cfg.format.expand_limit.as_mut() { if let Some(deep) = opts.format.expand_limit.as_mut() {
*deep -= 1 *deep -= 1
} }
cfg opts
} }
fn lookup_index_value(item: &Value, config: &Config) -> Option<String> { fn lookup_index_value(item: &Value, config: &Config) -> Option<String> {
@ -559,47 +584,23 @@ 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(out: TableOutput, term_width: usize, opts: &TableOpts<'_>) -> StringResult { fn maybe_expand_table(
let mut table_config = create_nu_table_config(opts.config, opts.style_computer, &out, false); out: TableOutput,
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.expand = true; table_config = table_config.expand(true);
} }
} }
Ok(out.table.draw(table_config, term_width)) let output = 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,40 +1,84 @@
use nu_color_config::TextStyle; use nu_color_config::{StyleComputer, 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::{ use crate::{Cell, NuTable, NuText};
clean_charset,
common::{ use super::{
create_nu_table_config, get_empty_style, get_header_style, get_index_style, clean_charset, create_table_config, get_empty_style, get_header_style, get_index_style,
get_value_style, NuText, INDEX_COLUMN_NAME, get_value_style, StringResult, TableOutput, TableResult, INDEX_COLUMN_NAME,
},
NuTable, NuTableCell, StringResult, TableOpts, TableOutput, TableResult,
}; };
pub struct JustTable; pub struct JustTable;
impl JustTable { impl JustTable {
pub fn table(input: &[Value], opts: TableOpts<'_>) -> StringResult { pub fn table(input: &[Value], row_offset: usize, opts: BuildConfig<'_>) -> StringResult {
create_table(input, opts) 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 kv_table(cols: &[String], vals: &[Value], opts: TableOpts<'_>) -> StringResult { pub fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> StringResult {
kv_table(cols, vals, opts) kv_table(cols, vals, opts)
} }
} }
fn create_table(input: &[Value], opts: TableOpts<'_>) -> Result<Option<String>, ShellError> { #[derive(Debug, Clone)]
match table(input, opts.row_offset, opts.clone())? { pub struct BuildConfig<'a> {
Some(out) => { pub(crate) ctrlc: Option<Arc<AtomicBool>>,
let table_config = pub(crate) config: &'a Config,
create_nu_table_config(opts.config, opts.style_computer, &out, false); pub(crate) style_computer: &'a StyleComputer<'a>,
Ok(out.table.draw(table_config, opts.width)) 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,
} }
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: TableOpts<'_>) -> StringResult { fn kv_table(cols: &[String], vals: &[Value], opts: BuildConfig<'_>) -> 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) {
@ -47,8 +91,8 @@ fn kv_table(cols: &[String], vals: &[Value], opts: TableOpts<'_>) -> StringResul
value = clean_charset(&value); value = clean_charset(&value);
} }
let key = NuTableCell::new(column.to_string()); let key = Cell::new(column.to_string());
let value = NuTableCell::new(value); let value = Cell::new(value);
row.push(key); row.push(key);
row.push(value); row.push(value);
} }
@ -57,13 +101,13 @@ fn kv_table(cols: &[String], vals: &[Value], opts: TableOpts<'_>) -> StringResul
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_nu_table_config(opts.config, opts.style_computer, &out, false); let table_config = create_table_config(opts.config, opts.style_computer, &out);
let table = out.table.draw(table_config, opts.width); let table = out.table.draw(table_config, opts.term_width);
Ok(table) Ok(table)
} }
fn table(input: &[Value], row_offset: usize, opts: TableOpts<'_>) -> TableResult { fn table(input: &[Value], row_offset: usize, opts: BuildConfig<'_>) -> TableResult {
if input.is_empty() { if input.is_empty() {
return Ok(None); return Ok(None);
} }
@ -104,7 +148,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: TableOpts<'_>, opts: BuildConfig<'_>,
) -> 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();
@ -135,7 +179,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.insert_style((row + 1, col), style); table.set_cell_style((row + 1, col), style);
} }
} }
@ -146,7 +190,7 @@ fn to_table_with_no_header(
input: &[Value], input: &[Value],
with_index: bool, with_index: bool,
row_offset: usize, row_offset: usize,
opts: TableOpts<'_>, opts: BuildConfig<'_>,
) -> 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));
@ -169,13 +213,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.insert_style(pos, style); table.set_cell_style(pos, style);
} }
Ok(Some(table)) Ok(Some(table))
} }
fn get_string_value_with_header(item: &Value, header: &str, opts: &TableOpts) -> NuText { fn get_string_value_with_header(item: &Value, header: &str, opts: &BuildConfig) -> NuText {
match item { match item {
Value::Record { .. } => { Value::Record { .. } => {
let path = PathMember::String { let path = PathMember::String {
@ -194,7 +238,7 @@ fn get_string_value_with_header(item: &Value, header: &str, opts: &TableOpts) ->
} }
} }
fn get_string_value(item: &Value, opts: &TableOpts) -> NuText { fn get_string_value(item: &Value, opts: &BuildConfig) -> 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,15 +2,20 @@ mod collapse;
mod expanded; mod expanded;
mod general; mod general;
use std::sync::{atomic::AtomicBool, Arc}; 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};
pub use collapse::CollapsedTable; pub use collapse::CollapsedTable;
pub use expanded::ExpandedTable; pub use expanded::ExpandedTable;
pub use general::JustTable; pub use general::{BuildConfig, JustTable};
use nu_color_config::StyleComputer;
use nu_protocol::{Config, Span};
use crate::NuTable; pub type NuText = (String, TextStyle);
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,
@ -28,32 +33,176 @@ impl TableOutput {
} }
} }
#[derive(Debug, Clone)] pub fn value_to_styled_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText {
pub struct TableOpts<'a> { let float_precision = cfg.float_precision as usize;
ctrlc: Option<Arc<AtomicBool>>, let text = val.into_abbreviated_string(cfg);
config: &'a Config, make_styled_string(style, text, Some(val), float_precision)
style_computer: &'a StyleComputer<'a>,
span: Span,
row_offset: usize,
width: usize,
} }
impl<'a> TableOpts<'a> { pub fn value_to_clean_styled_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText {
pub fn new( let (text, style) = value_to_styled_string(val, cfg, style);
config: &'a Config, let text = clean_charset(&text);
style_computer: &'a StyleComputer<'a>, (text, style)
ctrlc: Option<Arc<AtomicBool>>, }
span: Span,
row_offset: usize, pub fn clean_charset(text: &str) -> String {
available_width: usize, // todo: optimize, I bet it can be done in 1 path
) -> Self { text.replace('\t', " ").replace('\r', "")
Self { }
ctrlc,
config, const INDEX_COLUMN_NAME: &str = "index";
style_computer,
span, fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) {
row_offset, make_styled_string(style_computer, String::from(""), None, 0)
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,8 +42,3 @@ 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, NuTableConfig}; use nu_table::{string_width, NuTable, TableConfig};
use tabled::grid::records::vec_records::CellInfo; use tabled::grid::records::vec_records::CellInfo;
pub struct TestCase { pub struct TestCase {
cfg: NuTableConfig, cfg: TableConfig,
termwidth: usize, termwidth: usize,
expected: Option<String>, expected: Option<String>,
} }
impl TestCase { impl TestCase {
pub fn new(cfg: NuTableConfig, termwidth: usize, expected: Option<String>) -> Self { pub fn new(cfg: TableConfig, 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: NuTableConfig, termwidth: usize) -> Option<String> { pub fn create_table(data: Data, config: TableConfig, 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, NuTableConfig, TableTheme as theme}; use nu_table::{NuTable, TableConfig, 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,13 +9,11 @@ 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(cfg.clone(), usize::MAX); let table = table.draw(
TableConfig::new().theme(theme::heavy()).with_header(true),
usize::MAX,
);
assert_eq!( assert_eq!(
table.as_deref(), table.as_deref(),
@ -31,7 +29,10 @@ 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(cfg, usize::MAX); let table = table.draw(
TableConfig::new().theme(theme::heavy()).with_header(true),
usize::MAX,
);
assert_eq!( assert_eq!(
table.as_deref(), table.as_deref(),
@ -48,7 +49,7 @@ fn data_and_header_has_different_size_doesnt_work() {
#[test] #[test]
fn termwidth_too_small() { fn termwidth_too_small() {
let test_loop = |config: NuTableConfig| { let test_loop = |config: TableConfig| {
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);
@ -57,22 +58,29 @@ fn termwidth_too_small() {
} }
}; };
let mut cfg = NuTableConfig { let base_config = TableConfig::new().theme(theme::heavy()).with_header(true);
theme: theme::heavy(),
with_header: true,
..Default::default()
};
for case in [ let config = base_config.clone();
TrimStrategy::truncate(None), test_loop(config);
TrimStrategy::truncate(Some(String::from("**"))),
TrimStrategy::truncate(Some(String::from(""))), let config = base_config.clone().trim(TrimStrategy::truncate(None));
TrimStrategy::wrap(false), test_loop(config);
TrimStrategy::wrap(true),
] { let config = base_config
cfg.trim = case; .clone()
test_loop(cfg.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);
} }
#[test] #[test]
@ -196,12 +204,11 @@ 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 config = NuTableConfig { let trim = TrimStrategy::truncate(Some(String::from("...")));
theme: theme::heavy(), let config = TableConfig::new()
trim: TrimStrategy::truncate(Some(String::from("..."))), .theme(theme::heavy())
with_header: true, .with_header(true)
..Default::default() .trim(trim);
};
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()))
@ -211,13 +218,10 @@ 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 = NuTableConfig { let config = TableConfig::new()
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,18 +2,16 @@ mod common;
use common::{create_row, create_table}; use common::{create_row, create_table};
use nu_table::{NuTableConfig, TableTheme as theme}; use nu_table::{TableConfig, 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],
NuTableConfig { TableConfig::new()
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, NuTableConfig, TableTheme as theme}; use nu_table::{NuTable, TableConfig, TableTheme as theme};
use tabled::grid::records::vec_records::CellInfo; use tabled::grid::records::vec_records::CellInfo;
#[test] #[test]
@ -476,11 +476,10 @@ 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 config = NuTableConfig { let mut config = TableConfig::new().theme(theme);
theme, if with_header {
with_header, config = config.with_header(true);
..Default::default() }
};
let out = common::create_table(data, config, usize::MAX); let out = common::create_table(data, config, usize::MAX);
@ -492,11 +491,10 @@ fn create_table_with_size(
with_header: bool, with_header: bool,
theme: theme, theme: theme,
) -> String { ) -> String {
let config = NuTableConfig { let mut config = TableConfig::new().theme(theme);
theme, if with_header {
with_header, config = config.with_header(true);
..Default::default() }
};
let table = NuTable::from(data); let table = NuTable::from(data);