Bump tabled to 0.17 (#14415)

With this comes a new `unicode-width` as I remember there was some issue
with `ratatui`.
 
And a bit of refactorings which are ment to reduce code lines while not
breaking anything.
Not yet complete, I think I'll try to improve some more places,
just wanted to trigger CI 😄 

And yessssssssss we have a new `unicode-width` but I sort of doubtful,
I mean the original issue with emojie.
I think it may require an additional "clean" call.
I am just saying I was not testing it with that case of complex emojies.

---------

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>
This commit is contained in:
Maxim Zhiburt 2024-12-28 17:19:48 +03:00 committed by GitHub
parent 5314b31b12
commit 4401924128
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1419 additions and 1382 deletions

10
Cargo.lock generated
View File

@ -4680,15 +4680,15 @@ checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
[[package]] [[package]]
name = "papergrid" name = "papergrid"
version = "0.12.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725" checksum = "d2b0f8def1f117e13c895f3eda65a7b5650688da29d6ad04635f61bc7b92eebd"
dependencies = [ dependencies = [
"ansi-str", "ansi-str",
"ansitok", "ansitok",
"bytecount", "bytecount",
"fnv", "fnv",
"unicode-width 0.1.11", "unicode-width 0.2.0",
] ]
[[package]] [[package]]
@ -7233,9 +7233,9 @@ dependencies = [
[[package]] [[package]]
name = "tabled" name = "tabled"
version = "0.16.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b" checksum = "c6709222f3973137427ce50559cd564dc187a95b9cfe01613d2f4e93610e510a"
dependencies = [ dependencies = [
"ansi-str", "ansi-str",
"ansitok", "ansitok",

View File

@ -156,7 +156,7 @@ sha2 = "0.10"
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.2.0"
syn = "2.0" syn = "2.0"
sysinfo = "0.32" sysinfo = "0.32"
tabled = { version = "0.16.0", default-features = false } tabled = { version = "0.17.0", default-features = false }
tempfile = "3.14" tempfile = "3.14"
terminal_size = "0.4" terminal_size = "0.4"
titlecase = "3.0" titlecase = "3.0"

View File

@ -1,14 +1,17 @@
use crate::debug::inspect_table::{ // note: Seems like could be simplified
global_horizontal_char::SetHorizontalChar, set_widths::SetWidths, // IMHO: it shall not take 300+ lines :)
};
use nu_protocol::Value; use nu_protocol::Value;
use nu_table::{string_width, string_wrap}; use nu_table::{string_width, string_wrap};
use tabled::{ use tabled::{
grid::config::ColoredConfig, grid::config::ColoredConfig,
settings::{peaker::PriorityMax, width::Wrap, Settings, Style}, settings::{peaker::Priority, width::Wrap, Settings, Style},
Table, Table,
}; };
use self::{global_horizontal_char::SetHorizontalChar, set_widths::SetWidths};
pub fn build_table(value: Value, description: String, termsize: usize) -> String { pub fn build_table(value: Value, description: String, termsize: usize) -> String {
let (head, mut data) = util::collect_input(value); let (head, mut data) = util::collect_input(value);
let count_columns = head.len(); let count_columns = head.len();
@ -57,7 +60,7 @@ pub fn build_table(value: Value, description: String, termsize: usize) -> String
Settings::default() Settings::default()
.with(Style::rounded().corner_top_left('├').corner_top_right('┤')) .with(Style::rounded().corner_top_left('├').corner_top_right('┤'))
.with(SetWidths(widths)) .with(SetWidths(widths))
.with(Wrap::new(width).priority(PriorityMax)) .with(Wrap::new(width).priority(Priority::max(true)))
.with(SetHorizontalChar::new('┼', '┴', 11 + 2 + 1)), .with(SetHorizontalChar::new('┼', '┴', 11 + 2 + 1)),
); );

View File

@ -2,7 +2,12 @@
// overall reduce the redundant calls to StyleComputer etc. // overall reduce the redundant calls to StyleComputer etc.
// the goal is to configure it once... // the goal is to configure it once...
use std::{collections::VecDeque, io::IsTerminal, io::Read, path::PathBuf, str::FromStr};
use lscolors::{LsColors, Style}; use lscolors::{LsColors, Style};
use url::Url;
use web_time::Instant;
use nu_color_config::{color_from_hex, StyleComputer, TextStyle}; use nu_color_config::{color_from_hex, StyleComputer, TextStyle};
use nu_engine::{command_prelude::*, env_to_string}; use nu_engine::{command_prelude::*, env_to_string};
use nu_path::form::Absolute; use nu_path::form::Absolute;
@ -11,25 +16,16 @@ use nu_protocol::{
ByteStream, Config, DataSource, ListStream, PipelineMetadata, Signals, TableMode, ValueIterator, ByteStream, Config, DataSource, ListStream, PipelineMetadata, Signals, TableMode, ValueIterator,
}; };
use nu_table::{ use nu_table::{
common::create_nu_table_config, CollapsedTable, ExpandedTable, JustTable, NuTable, NuTableCell, common::configure_table, CollapsedTable, ExpandedTable, JustTable, NuRecordsValue, NuTable,
StringResult, TableOpts, TableOutput, StringResult, TableOpts, TableOutput,
}; };
use nu_utils::{get_ls_colors, terminal_size}; use nu_utils::{get_ls_colors, terminal_size};
use std::{collections::VecDeque, io::Read, path::PathBuf, str::FromStr};
use url::Url; type ShellResult<T> = Result<T, ShellError>;
use web_time::Instant; type NuPathBuf = nu_path::PathBuf<Absolute>;
const STREAM_PAGE_SIZE: usize = 1000; const STREAM_PAGE_SIZE: usize = 1000;
const DEFAULT_TABLE_WIDTH: usize = 80;
fn get_width_param(width_param: Option<i64>) -> usize {
if let Some(col) = width_param {
col as usize
} else if let Ok((w, _h)) = terminal_size() {
w as usize
} else {
80
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Table; pub struct Table;
@ -113,17 +109,15 @@ impl Command for Table {
stack: &mut Stack, stack: &mut Stack,
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> ShellResult<PipelineData> {
let list_themes: bool = call.has_flag(engine_state, stack, "list")?; let list_themes: bool = call.has_flag(engine_state, stack, "list")?;
// if list argument is present we just need to return a list of supported table themes // if list argument is present we just need to return a list of supported table themes
if list_themes { if list_themes {
let val = Value::list(supported_table_modes(), Span::test_data()); let val = Value::list(supported_table_modes(), Span::test_data());
return Ok(val.into_pipeline_data()); return Ok(val.into_pipeline_data());
} }
#[cfg(feature = "os")]
let cwd = engine_state.cwd(Some(stack))?; let input = CmdInput::parse(engine_state, stack, call, input)?;
let cfg = parse_table_config(call, engine_state, stack)?;
let input = CmdInput::new(engine_state, stack, call, input);
// reset vt processing, aka ansi because illbehaved externals can break it // reset vt processing, aka ansi because illbehaved externals can break it
#[cfg(windows)] #[cfg(windows)]
@ -131,12 +125,7 @@ impl Command for Table {
let _ = nu_utils::enable_vt_processing(); let _ = nu_utils::enable_vt_processing();
} }
handle_table_command( handle_table_command(input)
input,
cfg,
#[cfg(feature = "os")]
cwd,
)
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -221,80 +210,127 @@ impl Command for Table {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct TableConfig { struct TableConfig {
index: Option<usize>, view: TableView,
table_view: TableView, width: usize,
term_width: usize,
theme: TableMode, theme: TableMode,
abbreviation: Option<usize>, abbreviation: Option<usize>,
index: Option<usize>,
use_ansi_coloring: bool, use_ansi_coloring: bool,
} }
impl TableConfig { impl TableConfig {
fn new( fn new(
table_view: TableView, view: TableView,
term_width: usize, width: usize,
theme: TableMode, theme: TableMode,
abbreviation: Option<usize>, abbreviation: Option<usize>,
index: Option<usize>, index: Option<usize>,
use_ansi_coloring: bool, use_ansi_coloring: bool,
) -> Self { ) -> Self {
Self { Self {
index, view,
table_view, width,
term_width,
abbreviation,
theme, theme,
abbreviation,
index,
use_ansi_coloring, use_ansi_coloring,
} }
} }
} }
#[derive(Debug, Clone)]
enum TableView {
General,
Collapsed,
Expanded {
limit: Option<usize>,
flatten: bool,
flatten_separator: Option<String>,
},
}
struct CLIArgs {
width: Option<i64>,
abbrivation: Option<usize>,
theme: TableMode,
expand: bool,
expand_limit: Option<usize>,
expand_flatten: bool,
expand_flatten_separator: Option<String>,
collapse: bool,
index: Option<usize>,
use_ansi_coloring: bool,
}
fn parse_table_config( fn parse_table_config(
call: &Call, call: &Call,
state: &EngineState, state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
) -> Result<TableConfig, ShellError> { ) -> ShellResult<TableConfig> {
let width_param: Option<i64> = call.get_flag(state, stack, "width")?; let args = get_cli_args(call, state, stack)?;
let expand: bool = call.has_flag(state, stack, "expand")?; let table_view = get_table_view(&args);
let expand_limit: Option<usize> = call.get_flag(state, stack, "expand-deep")?; let term_width = get_table_width(args.width);
let collapse: bool = call.has_flag(state, stack, "collapse")?;
let flatten: bool = call.has_flag(state, stack, "flatten")?; let cfg = TableConfig::new(
let flatten_separator: Option<String> = call.get_flag(state, stack, "flatten-separator")?; table_view,
let abbrivation: Option<usize> = call term_width,
.get_flag(state, stack, "abbreviated")? args.theme,
.or_else(|| stack.get_config(state).table.abbreviated_row_count); args.abbrivation,
let table_view = match (expand, collapse) { args.index,
args.use_ansi_coloring,
);
Ok(cfg)
}
fn get_table_view(args: &CLIArgs) -> TableView {
match (args.expand, args.collapse) {
(false, false) => TableView::General, (false, false) => TableView::General,
(_, true) => TableView::Collapsed, (_, true) => TableView::Collapsed,
(true, _) => TableView::Expanded { (true, _) => TableView::Expanded {
limit: expand_limit, limit: args.expand_limit,
flatten, flatten: args.expand_flatten,
flatten_separator, flatten_separator: args.expand_flatten_separator.clone(),
}, },
}; }
}
fn get_cli_args(call: &Call<'_>, state: &EngineState, stack: &mut Stack) -> ShellResult<CLIArgs> {
let width: Option<i64> = call.get_flag(state, stack, "width")?;
let expand: bool = call.has_flag(state, stack, "expand")?;
let expand_limit: Option<usize> = call.get_flag(state, stack, "expand-deep")?;
let expand_flatten: bool = call.has_flag(state, stack, "flatten")?;
let expand_flatten_separator: Option<String> =
call.get_flag(state, stack, "flatten-separator")?;
let collapse: bool = call.has_flag(state, stack, "collapse")?;
let abbrivation: Option<usize> = call
.get_flag(state, stack, "abbreviated")?
.or_else(|| stack.get_config(state).table.abbreviated_row_count);
let theme = let theme =
get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode); get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode);
let index = get_index_flag(call, state, stack)?; let index = get_index_flag(call, state, stack)?;
let term_width = get_width_param(width_param);
let use_ansi_coloring = state.get_config().use_ansi_coloring.get(state); let use_ansi_coloring = state.get_config().use_ansi_coloring.get(state);
Ok(TableConfig::new( Ok(CLIArgs {
table_view,
term_width,
theme, theme,
abbrivation, abbrivation,
collapse,
expand,
expand_limit,
expand_flatten,
expand_flatten_separator,
width,
index, index,
use_ansi_coloring, use_ansi_coloring,
)) })
} }
fn get_index_flag( fn get_index_flag(
call: &Call, call: &Call,
state: &EngineState, state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
) -> Result<Option<usize>, ShellError> { ) -> ShellResult<Option<usize>> {
let index: Option<Value> = call.get_flag(state, stack, "index")?; let index: Option<Value> = call.get_flag(state, stack, "index")?;
let value = match index { let value = match index {
Some(value) => value, Some(value) => value,
@ -335,7 +371,7 @@ fn get_theme_flag(
call: &Call, call: &Call,
state: &EngineState, state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
) -> Result<Option<TableMode>, ShellError> { ) -> ShellResult<Option<TableMode>> {
call.get_flag(state, stack, "theme")? call.get_flag(state, stack, "theme")?
.map(|theme: String| { .map(|theme: String| {
TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert { TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert {
@ -353,29 +389,36 @@ struct CmdInput<'a> {
stack: &'a mut Stack, stack: &'a mut Stack,
call: &'a Call<'a>, call: &'a Call<'a>,
data: PipelineData, data: PipelineData,
cfg: TableConfig,
cwd: Option<NuPathBuf>,
} }
impl<'a> CmdInput<'a> { impl<'a> CmdInput<'a> {
fn new( fn parse(
engine_state: &'a EngineState, engine_state: &'a EngineState,
stack: &'a mut Stack, stack: &'a mut Stack,
call: &'a Call<'a>, call: &'a Call<'a>,
data: PipelineData, data: PipelineData,
) -> Self { ) -> ShellResult<Self> {
Self { let cfg = parse_table_config(call, engine_state, stack)?;
let cwd = get_cwd(engine_state, stack)?;
Ok(Self {
engine_state, engine_state,
stack, stack,
call, call,
data, data,
} cfg,
cwd,
})
}
fn get_config(&self) -> std::sync::Arc<Config> {
self.stack.get_config(self.engine_state)
} }
} }
fn handle_table_command( fn handle_table_command(mut input: CmdInput<'_>) -> ShellResult<PipelineData> {
mut input: CmdInput<'_>,
cfg: TableConfig,
#[cfg(feature = "os")] cwd: nu_path::PathBuf<Absolute>,
) -> Result<PipelineData, ShellError> {
let span = input.data.span().unwrap_or(input.call.head); let span = input.data.span().unwrap_or(input.call.head);
match input.data { match input.data {
// Binary streams should behave as if they really are `binary` data, and printed as hex // Binary streams should behave as if they really are `binary` data, and printed as hex
@ -397,29 +440,15 @@ fn handle_table_command(
let stream = ListStream::new(vals.into_iter(), span, signals); let stream = ListStream::new(vals.into_iter(), span, signals);
input.data = PipelineData::Empty; input.data = PipelineData::Empty;
handle_row_stream( handle_row_stream(input, stream, metadata)
input,
cfg,
stream,
metadata,
#[cfg(feature = "os")]
cwd,
)
} }
PipelineData::ListStream(stream, metadata) => { PipelineData::ListStream(stream, metadata) => {
input.data = PipelineData::Empty; input.data = PipelineData::Empty;
handle_row_stream( handle_row_stream(input, stream, metadata)
input,
cfg,
stream,
metadata,
#[cfg(feature = "os")]
cwd,
)
} }
PipelineData::Value(Value::Record { val, .. }, ..) => { PipelineData::Value(Value::Record { val, .. }, ..) => {
input.data = PipelineData::Empty; input.data = PipelineData::Empty;
handle_record(input, cfg, val.into_owned()) handle_record(input, val.into_owned())
} }
PipelineData::Value(Value::Error { error, .. }, ..) => { PipelineData::Value(Value::Error { error, .. }, ..) => {
// Propagate this error outward, so that it goes to stderr // Propagate this error outward, so that it goes to stderr
@ -435,14 +464,7 @@ fn handle_table_command(
let stream = let stream =
ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals); ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals);
input.data = PipelineData::Empty; input.data = PipelineData::Empty;
handle_row_stream( handle_row_stream(input, stream, metadata)
input,
cfg,
stream,
metadata,
#[cfg(feature = "os")]
cwd,
)
} }
x => Ok(x), x => Ok(x),
} }
@ -517,60 +539,55 @@ fn pretty_hex_stream(stream: ByteStream, span: Span) -> ByteStream {
) )
} }
fn handle_record( fn handle_record(input: CmdInput, mut record: Record) -> ShellResult<PipelineData> {
input: CmdInput,
cfg: TableConfig,
mut record: Record,
) -> Result<PipelineData, ShellError> {
let config = {
let state = input.engine_state;
let stack: &Stack = input.stack;
stack.get_config(state)
};
let span = input.data.span().unwrap_or(input.call.head); let span = input.data.span().unwrap_or(input.call.head);
let styles = &StyleComputer::from_config(input.engine_state, input.stack);
if record.is_empty() { if record.is_empty() {
let value = let value =
create_empty_placeholder("record", cfg.term_width, input.engine_state, input.stack); create_empty_placeholder("record", input.cfg.width, input.engine_state, input.stack);
let value = Value::string(value, span); let value = Value::string(value, span);
return Ok(value.into_pipeline_data()); return Ok(value.into_pipeline_data());
}; };
if let Some(limit) = cfg.abbreviation { if let Some(limit) = input.cfg.abbreviation {
let prev_len = record.len(); record = make_record_abbreviation(record, limit);
if record.len() > limit * 2 + 1 {
// TODO: see if the following table builders would be happy with a simple iterator
let mut record_iter = record.into_iter();
record = Record::with_capacity(limit * 2 + 1);
record.extend(record_iter.by_ref().take(limit));
record.push(String::from("..."), Value::string("...", Span::unknown()));
record.extend(record_iter.skip(prev_len - 2 * limit));
}
} }
let indent = (config.table.padding.left, config.table.padding.right); let config = input.get_config();
let opts = TableOpts::new( let opts = create_table_opts(
input.engine_state,
input.stack,
&config, &config,
styles, &input.cfg,
input.engine_state.signals(),
span, span,
cfg.term_width, 0,
indent,
cfg.theme,
cfg.index.unwrap_or(0),
cfg.index.is_none(),
); );
let result = build_table_kv(record, cfg.table_view, opts, span)?; let result = build_table_kv(record, input.cfg.view.clone(), opts, span)?;
let result = match result { let result = match result {
Some(output) => maybe_strip_color(output, cfg.use_ansi_coloring), Some(output) => maybe_strip_color(output, input.cfg.use_ansi_coloring),
None => report_unsuccessful_output(input.engine_state.signals(), cfg.term_width), None => report_unsuccessful_output(input.engine_state.signals(), input.cfg.width),
}; };
let val = Value::string(result, span); let val = Value::string(result, span);
let data = val.into_pipeline_data();
Ok(val.into_pipeline_data()) Ok(data)
}
fn make_record_abbreviation(mut record: Record, limit: usize) -> Record {
if record.len() <= limit * 2 + 1 {
return record;
}
// TODO: see if the following table builders would be happy with a simple iterator
let prev_len = record.len();
let mut record_iter = record.into_iter();
record = Record::with_capacity(limit * 2 + 1);
record.extend(record_iter.by_ref().take(limit));
record.push(String::from("..."), Value::string("...", Span::unknown()));
record.extend(record_iter.skip(prev_len - 2 * limit));
record
} }
fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String { fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String {
@ -608,11 +625,11 @@ fn build_table_kv(
fn build_table_batch( fn build_table_batch(
vals: Vec<Value>, vals: Vec<Value>,
table_view: TableView, view: TableView,
opts: TableOpts<'_>, opts: TableOpts<'_>,
span: Span, span: Span,
) -> StringResult { ) -> StringResult {
match table_view { match view {
TableView::General => JustTable::table(&vals, opts), TableView::General => JustTable::table(&vals, opts),
TableView::Expanded { TableView::Expanded {
limit, limit,
@ -631,22 +648,17 @@ fn build_table_batch(
fn handle_row_stream( fn handle_row_stream(
input: CmdInput<'_>, input: CmdInput<'_>,
cfg: TableConfig,
stream: ListStream, stream: ListStream,
metadata: Option<PipelineMetadata>, metadata: Option<PipelineMetadata>,
#[cfg(feature = "os")] cwd: nu_path::PathBuf<Absolute>, ) -> ShellResult<PipelineData> {
) -> Result<PipelineData, ShellError> { let cfg = input.get_config();
let stream = match metadata.as_ref() { let stream = match metadata.as_ref() {
// First, `ls` sources: // First, `ls` sources:
Some(PipelineMetadata { Some(PipelineMetadata {
data_source: DataSource::Ls, data_source: DataSource::Ls,
.. ..
}) => { }) => {
let config = { let config = cfg.clone();
let state = input.engine_state;
let stack: &Stack = input.stack;
stack.get_config(state)
};
let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") { let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
Some(v) => Some(env_to_string( Some(v) => Some(env_to_string(
"LS_COLORS", "LS_COLORS",
@ -664,14 +676,9 @@ fn handle_row_stream(
if let Some(value) = record.to_mut().get_mut("name") { if let Some(value) = record.to_mut().get_mut("name") {
let span = value.span(); let span = value.span();
if let Value::String { val, .. } = value { if let Value::String { val, .. } = value {
if let Some(val) = render_path_name( if let Some(val) =
val, render_path_name(val, &config, &ls_colors, input.cwd.clone(), span)
&config, {
&ls_colors,
#[cfg(feature = "os")]
cwd.clone(),
span,
) {
*value = val; *value = val;
} }
} }
@ -726,6 +733,7 @@ fn handle_row_stream(
// for the values it outputs. Because engine_state is passed in, config doesn't need to. // for the values it outputs. Because engine_state is passed in, config doesn't need to.
input.engine_state.clone(), input.engine_state.clone(),
input.stack.clone(), input.stack.clone(),
input.cfg,
cfg, cfg,
); );
let stream = ByteStream::from_result_iter( let stream = ByteStream::from_result_iter(
@ -787,8 +795,9 @@ struct PagingTableCreator {
stack: Stack, stack: Stack,
elements_displayed: usize, elements_displayed: usize,
reached_end: bool, reached_end: bool,
cfg: TableConfig, table_config: TableConfig,
row_offset: usize, row_offset: usize,
config: std::sync::Arc<Config>,
} }
impl PagingTableCreator { impl PagingTableCreator {
@ -797,114 +806,51 @@ impl PagingTableCreator {
stream: ListStream, stream: ListStream,
engine_state: EngineState, engine_state: EngineState,
stack: Stack, stack: Stack,
cfg: TableConfig, table_config: TableConfig,
config: std::sync::Arc<Config>,
) -> Self { ) -> Self {
PagingTableCreator { PagingTableCreator {
head, head,
stream: stream.into_inner(), stream: stream.into_inner(),
engine_state, engine_state,
stack, stack,
cfg, config,
table_config,
elements_displayed: 0, elements_displayed: 0,
reached_end: false, reached_end: false,
row_offset: 0, row_offset: 0,
} }
} }
fn build_extended( fn build_table(&mut self, batch: Vec<Value>) -> ShellResult<Option<String>> {
&mut self,
batch: Vec<Value>,
limit: Option<usize>,
flatten: bool,
flatten_separator: Option<String>,
) -> StringResult {
if batch.is_empty() { if batch.is_empty() {
return Ok(None); return Ok(None);
} }
let cfg = { let opts = self.create_table_opts();
let state = &self.engine_state; build_table_batch(batch, self.table_config.view.clone(), opts, self.head)
let stack = &self.stack;
stack.get_config(state)
};
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, opts, self.head)
} }
fn build_collapsed(&mut self, batch: Vec<Value>) -> StringResult { fn create_table_opts(&self) -> TableOpts<'_> {
if batch.is_empty() { create_table_opts(
return Ok(None); &self.engine_state,
} &self.stack,
&self.config,
let cfg = { &self.table_config,
let state = &self.engine_state;
let stack = &self.stack;
stack.get_config(state)
};
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, opts, self.head)
}
fn build_general(&mut self, batch: Vec<Value>) -> StringResult {
let cfg = {
let state = &self.engine_state;
let stack = &self.stack;
stack.get_config(state)
};
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, opts, self.head)
}
fn create_table_opts<'a>(
&'a self,
cfg: &'a Config,
style_comp: &'a StyleComputer<'a>,
) -> TableOpts<'a> {
TableOpts::new(
cfg,
style_comp,
self.engine_state.signals(),
self.head, self.head,
self.cfg.term_width, self.row_offset,
(cfg.table.padding.left, cfg.table.padding.right),
self.cfg.theme,
self.cfg.index.unwrap_or(0) + self.row_offset,
self.cfg.index.is_none(),
) )
} }
fn build_table(&mut self, batch: Vec<Value>) -> Result<Option<String>, ShellError> {
match &self.cfg.table_view {
TableView::General => self.build_general(batch),
TableView::Collapsed => self.build_collapsed(batch),
TableView::Expanded {
limit,
flatten,
flatten_separator,
} => self.build_extended(batch, *limit, *flatten, flatten_separator.clone()),
}
}
} }
impl Iterator for PagingTableCreator { impl Iterator for PagingTableCreator {
type Item = Result<Vec<u8>, ShellError>; type Item = ShellResult<Vec<u8>>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let batch; let batch;
let end; let end;
match self.cfg.abbreviation { match self.table_config.abbreviation {
Some(abbr) => { Some(abbr) => {
(batch, _, end) = (batch, _, end) =
stream_collect_abbriviated(&mut self.stream, abbr, self.engine_state.signals()); stream_collect_abbriviated(&mut self.stream, abbr, self.engine_state.signals());
@ -934,7 +880,7 @@ impl Iterator for PagingTableCreator {
self.elements_displayed = 1; self.elements_displayed = 1;
let result = create_empty_placeholder( let result = create_empty_placeholder(
"list", "list",
self.cfg.term_width, self.table_config.width,
&self.engine_state, &self.engine_state,
&self.stack, &self.stack,
); );
@ -950,9 +896,9 @@ impl Iterator for PagingTableCreator {
convert_table_to_output( convert_table_to_output(
table, table,
&self.cfg,
self.engine_state.signals(), self.engine_state.signals(),
self.cfg.term_width, self.table_config.width,
self.table_config.use_ansi_coloring,
) )
} }
} }
@ -1059,17 +1005,17 @@ fn render_path_name(
path: &str, path: &str,
config: &Config, config: &Config,
ls_colors: &LsColors, ls_colors: &LsColors,
#[cfg(feature = "os")] cwd: nu_path::PathBuf<Absolute>, cwd: Option<NuPathBuf>,
span: Span, span: Span,
) -> Option<Value> { ) -> Option<Value> {
if !config.ls.use_ls_colors { if !config.ls.use_ls_colors {
return None; return None;
} }
#[cfg(feature = "os")] let fullpath = match cwd {
let fullpath = cwd.join(path); Some(cwd) => PathBuf::from(cwd.join(path)),
#[cfg(not(feature = "os"))] None => PathBuf::from(path),
let fullpath = path; };
let stripped_path = nu_utils::strip_ansi_unlikely(path); let stripped_path = nu_utils::strip_ansi_unlikely(path);
let metadata = std::fs::symlink_metadata(fullpath); let metadata = std::fs::symlink_metadata(fullpath);
@ -1101,19 +1047,9 @@ fn render_path_name(
Some(Value::string(val, span)) Some(Value::string(val, span))
} }
#[derive(Debug, Clone)]
enum TableView {
General,
Collapsed,
Expanded {
limit: Option<usize>,
flatten: bool,
flatten_separator: Option<String>,
},
}
fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String { fn maybe_strip_color(output: String, use_ansi_coloring: bool) -> String {
if !use_ansi_coloring { // the terminal is for when people do ls from vim, there should be no coloring there
if !use_ansi_coloring || !std::io::stdout().is_terminal() {
// Draw the table without ansi colors // Draw the table without ansi colors
nu_utils::strip_ansi_string_likely(output) nu_utils::strip_ansi_string_likely(output)
} else { } else {
@ -1133,29 +1069,29 @@ fn create_empty_placeholder(
return String::new(); return String::new();
} }
let cell = NuTableCell::new(format!("empty {}", value_type_name)); let cell = NuRecordsValue::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_data_style(TextStyle::default().dimmed());
let out = TableOutput::new(table, false, false, 1); let mut out = TableOutput::from_table(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, TableMode::default()); configure_table(&mut out, &config, style_computer, TableMode::default());
out.table out.table
.draw(config, termwidth) .draw(termwidth)
.expect("Could not create empty table placeholder") .expect("Could not create empty table placeholder")
} }
fn convert_table_to_output( fn convert_table_to_output(
table: Result<Option<String>, ShellError>, table: ShellResult<Option<String>>,
cfg: &TableConfig,
signals: &Signals, signals: &Signals,
term_width: usize, term_width: usize,
) -> Option<Result<Vec<u8>, ShellError>> { use_ansi_coloring: bool,
) -> Option<ShellResult<Vec<u8>>> {
match table { match table {
Ok(Some(table)) => { Ok(Some(table)) => {
let table = maybe_strip_color(table, cfg.use_ansi_coloring); let table = maybe_strip_color(table, use_ansi_coloring);
let mut bytes = table.as_bytes().to_vec(); let mut bytes = table.as_bytes().to_vec();
bytes.push(b'\n'); // nu-table tables don't come with a newline on the end bytes.push(b'\n'); // nu-table tables don't come with a newline on the end
@ -1198,3 +1134,41 @@ fn supported_table_modes() -> Vec<Value> {
Value::test_string("basic_compact"), Value::test_string("basic_compact"),
] ]
} }
fn create_table_opts<'a>(
engine_state: &'a EngineState,
stack: &'a Stack,
cfg: &'a Config,
table_cfg: &'a TableConfig,
span: Span,
offset: usize,
) -> TableOpts<'a> {
let comp = StyleComputer::from_config(engine_state, stack);
let signals = engine_state.signals();
let offset = table_cfg.index.unwrap_or(0) + offset;
let index = table_cfg.index.is_none();
let width = table_cfg.width;
let theme = table_cfg.theme;
TableOpts::new(cfg, comp, signals, span, width, theme, offset, index)
}
fn get_cwd(engine_state: &EngineState, stack: &mut Stack) -> ShellResult<Option<NuPathBuf>> {
#[cfg(feature = "os")]
let cwd = engine_state.cwd(Some(stack)).map(Some)?;
#[cfg(not(feature = "os"))]
let cwd = None;
Ok(cwd)
}
fn get_table_width(width_param: Option<i64>) -> usize {
if let Some(col) = width_param {
col as usize
} else if let Ok((w, _h)) = terminal_size() {
w as usize
} else {
DEFAULT_TABLE_WIDTH
}
}

View File

@ -2867,13 +2867,52 @@ fn table_index_arg() {
#[test] #[test]
fn table_expand_index_arg() { fn table_expand_index_arg() {
let actual = nu!("[[a b]; [1 2] [2 [4 4]]] | table --width=80 --theme basic --expand -i false"); let actual = nu!("[[a b]; [1 2] [2 [4 4]]] | table --width=80 --theme basic --expand -i false");
assert_eq!(actual.out, "+---+-------+| a | b |+---+-------+| 1 | 2 |+---+-------+| 2 | +---+ || | | 4 | || | +---+ || | | 4 | || | +---+ |+---+-------+"); assert_eq!(
actual.out,
"+---+-------+\
| a | b |\
+---+-------+\
| 1 | 2 |\
+---+-------+\
| 2 | +---+ |\
| | | 4 | |\
| | +---+ |\
| | | 4 | |\
| | +---+ |\
+---+-------+"
);
let actual = nu!("[[a b]; [1 2] [2 [4 4]]] | table --width=80 --theme basic --expand -i true"); let actual = nu!("[[a b]; [1 2] [2 [4 4]]] | table --width=80 --theme basic --expand -i true");
assert_eq!(actual.out, "+---+---+-----------+| # | a | b |+---+---+-----------+| 0 | 1 | 2 |+---+---+-----------+| 1 | 2 | +---+---+ || | | | 0 | 4 | || | | +---+---+ || | | | 1 | 4 | || | | +---+---+ |+---+---+-----------+"); assert_eq!(
actual.out,
"+---+---+-----------+\
| # | a | b |\
+---+---+-----------+\
| 0 | 1 | 2 |\
+---+---+-----------+\
| 1 | 2 | +---+---+ |\
| | | | 0 | 4 | |\
| | | +---+---+ |\
| | | | 1 | 4 | |\
| | | +---+---+ |\
+---+---+-----------+"
);
let actual = nu!("[[a b]; [1 2] [2 [4 4]]] | table --width=80 --theme basic --expand -i 10"); let actual = nu!("[[a b]; [1 2] [2 [4 4]]] | table --width=80 --theme basic --expand -i 10");
assert_eq!(actual.out, "+----+---+-----------+| # | a | b |+----+---+-----------+| 10 | 1 | 2 |+----+---+-----------+| 11 | 2 | +---+---+ || | | | 0 | 4 | || | | +---+---+ || | | | 1 | 4 | || | | +---+---+ |+----+---+-----------+"); assert_eq!(
actual.out,
"+----+---+-----------+\
| # | a | b |\
+----+---+-----------+\
| 10 | 1 | 2 |\
+----+---+-----------+\
| 11 | 2 | +---+---+ |\
| | | | 0 | 4 | |\
| | | +---+---+ |\
| | | | 1 | 4 | |\
| | | +---+---+ |\
+----+---+-----------+"
);
} }
#[test] #[test]

View File

@ -69,12 +69,9 @@ fn convert_value_to_string(
} else { } else {
let config = engine_state.get_config(); let config = engine_state.get_config();
let style_computer = StyleComputer::from_config(engine_state, stack); let style_computer = StyleComputer::from_config(engine_state, stack);
let table =
nu_common::try_build_table(value, engine_state.signals(), config, style_computer);
Ok(nu_common::try_build_table( Ok(table)
engine_state.signals(),
config,
&style_computer,
value,
))
} }
} }

View File

@ -1,80 +1,57 @@
use crate::nu_common::NuConfig; use crate::nu_common::NuConfig;
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_protocol::{Record, Signals, Span, Value}; use nu_protocol::{Record, Signals, Value};
use nu_table::{ use nu_table::{
common::{nu_value_to_string, nu_value_to_string_clean}, common::{nu_value_to_string, nu_value_to_string_clean},
ExpandedTable, TableOpts, ExpandedTable, TableOpts,
}; };
pub fn try_build_table( pub fn try_build_table(
value: Value,
signals: &Signals, signals: &Signals,
config: &NuConfig, config: &NuConfig,
style_computer: &StyleComputer, style_computer: StyleComputer,
value: Value,
) -> String { ) -> String {
let span = value.span(); let span = value.span();
match value {
Value::List { vals, .. } => try_build_list(vals, signals, config, span, style_computer),
Value::Record { val, .. } => try_build_map(&val, span, style_computer, signals, config),
val if matches!(val, Value::String { .. }) => {
nu_value_to_string_clean(&val, config, style_computer).0
}
val => nu_value_to_string(&val, config, style_computer).0,
}
}
fn try_build_map(
record: &Record,
span: Span,
style_computer: &StyleComputer,
signals: &Signals,
config: &NuConfig,
) -> String {
let opts = TableOpts::new( let opts = TableOpts::new(
config, config,
style_computer, style_computer,
signals, signals,
Span::unknown(), span,
usize::MAX, usize::MAX,
(config.table.padding.left, config.table.padding.right),
config.table.mode, config.table.mode,
0, 0,
false, false,
); );
let result = ExpandedTable::new(None, false, String::new()).build_map(record, opts); match value {
Value::List { vals, .. } => try_build_list(vals, opts),
Value::Record { val, .. } => try_build_map(&val, opts),
val if matches!(val, Value::String { .. }) => {
nu_value_to_string_clean(&val, config, &opts.style_computer).0
}
val => nu_value_to_string(&val, config, &opts.style_computer).0,
}
}
fn try_build_map(record: &Record, opts: TableOpts<'_>) -> String {
let result = ExpandedTable::new(None, false, String::new()).build_map(record, opts.clone());
match result { match result {
Ok(Some(result)) => result, Ok(Some(result)) => result,
Ok(None) | Err(_) => { _ => {
nu_value_to_string(&Value::record(record.clone(), span), config, style_computer).0 let value = Value::record(record.clone(), opts.span);
nu_value_to_string(&value, opts.config, &opts.style_computer).0
} }
} }
} }
fn try_build_list( fn try_build_list(vals: Vec<Value>, opts: TableOpts<'_>) -> String {
vals: Vec<Value>, let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts.clone());
signals: &Signals,
config: &NuConfig,
span: Span,
style_computer: &StyleComputer,
) -> String {
let opts = TableOpts::new(
config,
style_computer,
signals,
Span::unknown(),
usize::MAX,
(config.table.padding.left, config.table.padding.right),
config.table.mode,
0,
false,
);
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(_) => { _ => {
// 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 let value = Value::list(vals, opts.span);
nu_value_to_string(&value, opts.config, &opts.style_computer).0
} }
} }
} }

View File

@ -22,7 +22,7 @@ pub use plugin_gc::{PluginGcConfig, PluginGcConfigs};
pub use reedline::{CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu}; pub use reedline::{CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu};
pub use rm::RmConfig; pub use rm::RmConfig;
pub use shell_integration::ShellIntegrationConfig; pub use shell_integration::ShellIntegrationConfig;
pub use table::{FooterMode, TableConfig, TableIndexMode, TableMode, TrimStrategy}; pub use table::{FooterMode, TableConfig, TableIndent, TableIndexMode, TableMode, TrimStrategy};
mod ansi_coloring; mod ansi_coloring;
mod completions; mod completions;

View File

@ -277,6 +277,12 @@ pub struct TableIndent {
pub right: usize, pub right: usize,
} }
impl TableIndent {
pub fn new(left: usize, right: usize) -> Self {
Self { left, right }
}
}
impl IntoValue for TableIndent { impl IntoValue for TableIndent {
fn into_value(self, span: Span) -> Value { fn into_value(self, span: Span) -> Value {
record! { record! {

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, TableTheme};
use tabled::grid::records::vec_records::Text; use tabled::grid::records::vec_records::Text;
fn main() { fn main() {
@ -28,15 +28,11 @@ fn main() {
table.set_data_style(TextStyle::basic_left()); table.set_data_style(TextStyle::basic_left());
table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue))); table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue)));
table.set_theme(TableTheme::rounded());
let table_cfg = NuTableConfig { table.set_structure(false, true, false);
theme: TableTheme::rounded(),
with_header: true,
..Default::default()
};
let output_table = table let output_table = table
.draw(table_cfg, width) .draw(width)
.unwrap_or_else(|| format!("Couldn't fit table into {width} columns!")); .unwrap_or_else(|| format!("Couldn't fit table into {width} columns!"));
println!("{output_table}") println!("{output_table}")

View File

@ -1,51 +1,57 @@
use crate::{
clean_charset, colorize_space_str, string_wrap, NuTableConfig, TableOutput, TableTheme,
};
use nu_color_config::{Alignment, StyleComputer, TextStyle}; use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_protocol::{Config, FooterMode, ShellError, Span, TableMode, TrimStrategy, Value}; use nu_protocol::{Config, FooterMode, ShellError, Span, TableMode, TrimStrategy, Value};
use terminal_size::{terminal_size, Height, Width}; use terminal_size::{terminal_size, Height, Width};
use crate::{clean_charset, colorize_space_str, string_wrap, TableOutput, TableTheme};
pub type NuText = (String, TextStyle); pub type NuText = (String, TextStyle);
pub type TableResult = Result<Option<TableOutput>, ShellError>; pub type TableResult = Result<Option<TableOutput>, ShellError>;
pub type StringResult = Result<Option<String>, ShellError>; pub type StringResult = Result<Option<String>, ShellError>;
pub const INDEX_COLUMN_NAME: &str = "index"; pub const INDEX_COLUMN_NAME: &str = "index";
pub fn create_nu_table_config( pub fn configure_table(
out: &mut TableOutput,
config: &Config, config: &Config,
comp: &StyleComputer, comp: &StyleComputer,
out: &TableOutput,
expand: bool,
mode: TableMode, mode: TableMode,
) -> NuTableConfig { ) {
let with_footer = is_footer_needed(config, out);
let theme = load_theme(mode);
out.table.set_theme(theme);
out.table
.set_structure(out.with_index, out.with_header, with_footer);
out.table.set_trim(config.table.trim.clone());
out.table
.set_border_header(config.table.header_on_separator);
out.table.set_border_color(lookup_separator_color(comp));
}
fn is_footer_needed(config: &Config, out: &TableOutput) -> bool {
let mut count_rows = out.table.count_rows(); let mut count_rows = out.table.count_rows();
if config.table.footer_inheritance { if config.table.footer_inheritance {
count_rows = out.count_rows; count_rows = out.count_rows;
} }
let with_footer = with_footer(config, out.with_header, count_rows); with_footer(config, out.with_header, count_rows)
NuTableConfig {
theme: load_theme(mode),
with_footer,
with_index: out.with_index,
with_header: out.with_header,
split_color: Some(lookup_separator_color(comp)),
trim: config.table.trim.clone(),
header_on_border: config.table.header_on_separator,
expand,
}
} }
pub fn nu_value_to_string_colored(val: &Value, cfg: &Config, style: &StyleComputer) -> String { pub fn nu_value_to_string_colored(val: &Value, cfg: &Config, comp: &StyleComputer) -> String {
let (mut text, value_style) = nu_value_to_string(val, cfg, style); let (mut text, style) = nu_value_to_string(val, cfg, comp);
if let Some(color) = value_style.color_style {
let is_string = matches!(val, Value::String { .. });
if is_string {
text = clean_charset(&text);
}
if let Some(color) = style.color_style {
text = color.paint(text).to_string(); text = color.paint(text).to_string();
} }
if matches!(val, Value::String { .. }) { if is_string {
text = clean_charset(&text); colorize_space_str(&mut text, comp);
colorize_space_str(&mut text, style);
} }
text text
@ -54,9 +60,11 @@ pub fn nu_value_to_string_colored(val: &Value, cfg: &Config, style: &StyleComput
pub fn nu_value_to_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText { pub fn nu_value_to_string(val: &Value, cfg: &Config, style: &StyleComputer) -> NuText {
let float_precision = cfg.float_precision as usize; let float_precision = cfg.float_precision as usize;
let text = val.to_abbreviated_string(cfg); let text = val.to_abbreviated_string(cfg);
make_styled_string(style, text, Some(val), float_precision) make_styled_value(text, val, float_precision, style)
} }
// todo: Expose a method which returns just style
pub fn nu_value_to_string_clean(val: &Value, cfg: &Config, style_comp: &StyleComputer) -> NuText { pub fn nu_value_to_string_clean(val: &Value, cfg: &Config, style_comp: &StyleComputer) -> NuText {
let (text, style) = nu_value_to_string(val, cfg, style_comp); let (text, style) = nu_value_to_string(val, cfg, style_comp);
let mut text = clean_charset(&text); let mut text = clean_charset(&text);
@ -66,7 +74,11 @@ pub fn nu_value_to_string_clean(val: &Value, cfg: &Config, style_comp: &StyleCom
} }
pub fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) { pub fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) {
make_styled_string(style_computer, String::from(""), None, 0) // Though holes are not the same as null, the closure for "empty" is passed a null anyway.
let text = String::from("");
let style = style_computer.compute("empty", &Value::nothing(Span::unknown()));
(text, TextStyle::with_style(Alignment::Center, style))
} }
pub fn wrap_text(text: &str, width: usize, config: &Config) -> String { pub fn wrap_text(text: &str, width: usize, config: &Config) -> String {
@ -122,36 +134,23 @@ pub fn get_empty_style(style_computer: &StyleComputer) -> NuText {
) )
} }
fn make_styled_string( fn make_styled_value(
style_computer: &StyleComputer,
text: String, text: String,
value: Option<&Value>, // None represents table holes. value: &Value,
float_precision: usize, float_precision: usize,
style_computer: &StyleComputer,
) -> NuText { ) -> NuText {
match value { match value {
Some(value) => { Value::Float { .. } => {
match value { // set dynamic precision from config
Value::Float { .. } => { let precise_number = match convert_with_precision(&text, float_precision) {
// set dynamic precision from config Ok(num) => num,
let precise_number = match convert_with_precision(&text, float_precision) { Err(e) => e.to_string(),
Ok(num) => num, };
Err(e) => e.to_string(),
}; (precise_number, style_computer.style_primitive(value))
(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())),
),
)
} }
_ => (text, style_computer.style_primitive(value)),
} }
} }
@ -220,3 +219,10 @@ fn need_footer(config: &Config, count_records: u64) -> bool {
} }
} }
} }
pub fn check_value(value: &Value) -> Result<(), ShellError> {
match value {
Value::Error { error, .. } => Err(*error.clone()),
_ => Ok(()),
}
}

View File

@ -1,4 +1,5 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
mod table; mod table;
mod table_theme; mod table_theme;
mod types; mod types;
@ -9,7 +10,7 @@ pub mod common;
pub use common::{StringResult, TableResult}; 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::{NuRecordsValue, NuTable};
pub use table_theme::TableTheme; pub use table_theme::TableTheme;
pub use types::{CollapsedTable, ExpandedTable, JustTable, TableOpts, TableOutput}; pub use types::{CollapsedTable, ExpandedTable, JustTable, TableOpts, TableOutput};
pub use unstructured_table::UnstructuredTable; pub use unstructured_table::UnstructuredTable;

File diff suppressed because it is too large Load Diff

View File

@ -1,67 +1,63 @@
use tabled::settings::style::{HorizontalLine, Style}; use tabled::settings::{
use tabled::settings::themes::Theme; style::{HorizontalLine, Style},
themes::Theme,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableTheme { pub struct TableTheme {
theme: Theme, base: Theme,
full_theme: Theme, full: Theme,
has_inner: bool,
} }
impl TableTheme { impl TableTheme {
pub fn new(theme: impl Into<Theme>, full_theme: impl Into<Theme>, has_inner: bool) -> Self { fn new(base: impl Into<Theme>, full: impl Into<Theme>) -> Self {
Self { Self {
theme: theme.into(), base: base.into(),
full_theme: full_theme.into(), full: full.into(),
has_inner,
} }
} }
pub fn basic() -> TableTheme { pub fn basic() -> TableTheme {
Self::new(Style::ascii(), Style::ascii(), true) Self::new(Style::ascii(), Style::ascii())
} }
pub fn thin() -> TableTheme { pub fn thin() -> TableTheme {
Self::new(Style::modern(), Style::modern(), true) Self::new(Style::modern(), Style::modern())
} }
pub fn light() -> TableTheme { pub fn light() -> TableTheme {
let mut theme = Theme::from_style(Style::blank()); let mut theme = Theme::from_style(Style::blank());
theme.insert_horizontal_line(1, HorizontalLine::new('─').intersection('─')); theme.insert_horizontal_line(1, HorizontalLine::new('─').intersection('─'));
Self::new(theme, Style::modern(), true) Self::new(theme, Style::modern())
} }
pub fn psql() -> TableTheme { pub fn psql() -> TableTheme {
Self::new(Style::psql(), Style::psql(), true) Self::new(Style::psql(), Style::psql())
} }
pub fn markdown() -> TableTheme { pub fn markdown() -> TableTheme {
Self::new(Style::markdown(), Style::markdown(), true) Self::new(Style::markdown(), Style::markdown())
} }
pub fn dots() -> TableTheme { pub fn dots() -> TableTheme {
let theme = Style::dots().remove_horizontal(); let theme = Style::dots().remove_horizontal();
Self::new(theme, Style::dots(), true) Self::new(theme, Style::dots())
} }
pub fn restructured() -> TableTheme { pub fn restructured() -> TableTheme {
Self::new( Self::new(Style::re_structured_text(), Style::re_structured_text())
Style::re_structured_text(),
Style::re_structured_text(),
true,
)
} }
pub fn ascii_rounded() -> TableTheme { pub fn ascii_rounded() -> TableTheme {
Self::new(Style::ascii_rounded(), Style::ascii_rounded(), true) Self::new(Style::ascii_rounded(), Style::ascii_rounded())
} }
pub fn basic_compact() -> TableTheme { pub fn basic_compact() -> TableTheme {
let theme = Style::ascii().remove_horizontal(); let theme = Style::ascii().remove_horizontal();
Self::new(theme, Style::ascii(), true) Self::new(theme, Style::ascii())
} }
pub fn compact() -> TableTheme { pub fn compact() -> TableTheme {
@ -72,7 +68,7 @@ impl TableTheme {
.remove_horizontal() .remove_horizontal()
.horizontals([(1, hline)]); .horizontals([(1, hline)]);
Self::new(theme, Style::modern(), true) Self::new(theme, Style::modern())
} }
pub fn with_love() -> TableTheme { pub fn with_love() -> TableTheme {
@ -99,7 +95,7 @@ impl TableTheme {
.intersection_left('❤') .intersection_left('❤')
.intersection('❤'); .intersection('❤');
Self::new(theme, full_theme, true) Self::new(theme, full_theme)
} }
pub fn compact_double() -> TableTheme { pub fn compact_double() -> TableTheme {
@ -112,7 +108,7 @@ impl TableTheme {
.remove_horizontal() .remove_horizontal()
.horizontals([(1, hline)]); .horizontals([(1, hline)]);
Self::new(theme, Style::extended(), true) Self::new(theme, Style::extended())
} }
pub fn rounded() -> TableTheme { pub fn rounded() -> TableTheme {
@ -122,7 +118,7 @@ impl TableTheme {
.corner_bottom_left('╰') .corner_bottom_left('╰')
.corner_bottom_right('╯'); .corner_bottom_right('╯');
Self::new(Style::rounded(), full, true) Self::new(Style::rounded(), full)
} }
pub fn reinforced() -> TableTheme { pub fn reinforced() -> TableTheme {
@ -132,7 +128,7 @@ impl TableTheme {
.corner_bottom_left('┗') .corner_bottom_left('┗')
.corner_bottom_right('┛'); .corner_bottom_right('┛');
Self::new(full.clone().remove_horizontal(), full, true) Self::new(full.clone().remove_horizontal(), full)
} }
pub fn heavy() -> TableTheme { pub fn heavy() -> TableTheme {
@ -157,38 +153,18 @@ impl TableTheme {
.intersection_right('┫') .intersection_right('┫')
.intersection('╋'); .intersection('╋');
Self::new(theme, full, true) Self::new(theme, full)
} }
pub fn none() -> TableTheme { pub fn none() -> TableTheme {
Self::new(Style::blank(), Style::blank(), true) Self::new(Style::blank(), Style::blank())
} }
pub fn has_top(&self) -> bool { pub fn as_full(&self) -> &Theme {
self.theme.borders_has_top() &self.full
} }
pub fn has_left(&self) -> bool { pub fn as_base(&self) -> &Theme {
self.theme.borders_has_left() &self.base
}
pub fn has_right(&self) -> bool {
self.theme.borders_has_right()
}
pub fn has_inner(&self) -> bool {
self.has_inner
}
pub fn has_horizontals(&self) -> bool {
self.full_theme.get_borders().has_horizontal()
}
pub fn get_theme_full(&self) -> Theme {
self.full_theme.clone()
}
pub fn get_theme(&self) -> Theme {
self.theme.clone()
} }
} }

View File

@ -1,48 +1,40 @@
use nu_ansi_term::Style;
use nu_color_config::StyleComputer;
use nu_protocol::{Config, Value};
use nu_utils::SharedCow;
use crate::{ use crate::{
common::{get_index_style, load_theme, nu_value_to_string_clean}, common::{get_index_style, load_theme, nu_value_to_string_clean},
StringResult, TableOpts, UnstructuredTable, StringResult, TableOpts, UnstructuredTable,
}; };
use nu_color_config::StyleComputer;
use nu_protocol::{Config, Record, TableMode, Value};
use nu_utils::SharedCow;
pub struct CollapsedTable; pub struct CollapsedTable;
impl CollapsedTable { impl CollapsedTable {
pub fn build(value: Value, opts: TableOpts<'_>) -> StringResult { pub fn build(value: Value, opts: TableOpts<'_>) -> StringResult {
collapsed_table( collapsed_table(value, opts)
value,
opts.config,
opts.width,
opts.style_computer,
opts.mode,
)
} }
} }
fn collapsed_table( fn collapsed_table(mut value: Value, opts: TableOpts<'_>) -> StringResult {
mut value: Value, colorize_value(&mut value, opts.config, &opts.style_computer);
config: &Config,
term_width: usize,
style_computer: &StyleComputer,
mode: TableMode,
) -> StringResult {
colorize_value(&mut value, config, style_computer);
let theme = load_theme(mode); let mut table = UnstructuredTable::new(value, opts.config);
let mut table = UnstructuredTable::new(value, config);
let is_empty = table.truncate(&theme, term_width); let theme = load_theme(opts.mode);
let is_empty = table.truncate(&theme, opts.width);
if is_empty { if is_empty {
return Ok(None); return Ok(None);
} }
let indent = (config.table.padding.left, config.table.padding.right); let table = table.draw(&theme, opts.config.table.padding, &opts.style_computer);
let table = table.draw(style_computer, &theme, indent);
Ok(Some(table)) Ok(Some(table))
} }
fn colorize_value(value: &mut Value, config: &Config, style_computer: &StyleComputer) { fn colorize_value(value: &mut Value, config: &Config, style_computer: &StyleComputer) {
// todo: Remove recursion?
match value { match value {
Value::Record { ref mut val, .. } => { Value::Record { ref mut val, .. } => {
let style = get_index_style(style_computer); let style = get_index_style(style_computer);
@ -55,13 +47,11 @@ fn colorize_value(value: &mut Value, config: &Config, style_computer: &StyleComp
.into_iter() .into_iter()
.map(|(mut header, mut val)| { .map(|(mut header, mut val)| {
colorize_value(&mut val, config, style_computer); colorize_value(&mut val, config, style_computer);
header = colorize_text(&header, style.color_style).unwrap_or(header);
if let Some(color) = style.color_style {
header = color.paint(header).to_string();
}
(header, val) (header, val)
}) })
.collect::<Record>(), .collect(),
); );
} }
Value::List { vals, .. } => { Value::List { vals, .. } => {
@ -71,11 +61,19 @@ 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) = nu_value_to_string_clean(value, config, style_computer);
if let Some(color) = style.color_style { if let Some(text) = colorize_text(&text, style.color_style) {
let text = color.paint(text).to_string(); *value = Value::string(text, value.span());
let span = value.span();
*value = Value::string(text, span);
} }
} }
} }
} }
fn colorize_text(text: &str, color: Option<Style>) -> Option<String> {
if let Some(color) = color {
if !color.is_plain() {
return Some(color.paint(text).to_string());
}
}
None
}

View File

@ -1,18 +1,21 @@
use std::{cmp::max, collections::HashMap};
use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_engine::column::get_columns;
use nu_protocol::{Config, Record, ShellError, Span, Value};
use tabled::grid::config::Position;
use crate::{ use crate::{
common::{ common::{
create_nu_table_config, error_sign, get_header_style, get_index_style, load_theme, check_value, configure_table, error_sign, get_header_style, get_index_style, load_theme,
nu_value_to_string, nu_value_to_string_clean, nu_value_to_string_colored, wrap_text, nu_value_to_string, nu_value_to_string_clean, nu_value_to_string_colored, wrap_text,
NuText, StringResult, TableResult, INDEX_COLUMN_NAME, NuText, StringResult, TableResult, INDEX_COLUMN_NAME,
}, },
string_width, string_width,
types::has_index, types::has_index,
NuTable, NuTableCell, TableOpts, TableOutput, NuRecordsValue, NuTable, TableOpts, TableOutput,
}; };
use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_engine::column::get_columns;
use nu_protocol::{Config, Record, ShellError, Span, Value};
use std::{cmp::max, collections::HashMap};
use tabled::grid::config::Position;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ExpandedTable { pub struct ExpandedTable {
@ -31,25 +34,32 @@ impl ExpandedTable {
} }
pub fn build_value(self, item: &Value, opts: TableOpts<'_>) -> NuText { pub fn build_value(self, item: &Value, opts: TableOpts<'_>) -> NuText {
let cell = expanded_table_entry2(item, Cfg { opts, format: self }); let cfg = Cfg { opts, format: self };
let cell = expand_entry(item, cfg);
(cell.text, cell.style) (cell.text, cell.style)
} }
pub fn build_map(self, record: &Record, opts: TableOpts<'_>) -> StringResult { pub fn build_map(self, record: &Record, opts: TableOpts<'_>) -> StringResult {
expanded_table_kv(record, Cfg { opts, format: self }).map(|cell| cell.map(|cell| cell.text)) let cfg = Cfg { opts, format: self };
expanded_table_kv(record, cfg).map(|cell| cell.map(|cell| cell.text))
} }
pub fn build_list(self, vals: &[Value], opts: TableOpts<'_>) -> StringResult { pub fn build_list(self, vals: &[Value], opts: TableOpts<'_>) -> StringResult {
let cfg = Cfg { let cfg = Cfg { opts, format: self };
opts: opts.clone(), let output = expand_list(vals, cfg.clone())?;
format: self, let mut output = match output {
};
let out = match expanded_table_list(vals, cfg)? {
Some(out) => out, Some(out) => out,
None => return Ok(None), None => return Ok(None),
}; };
maybe_expand_table(out, opts.width, &opts) configure_table(
&mut output,
cfg.opts.config,
&cfg.opts.style_computer,
cfg.opts.mode,
);
maybe_expand_table(output, cfg.opts.width)
} }
} }
@ -92,7 +102,7 @@ impl CellOutput {
type CellResult = Result<Option<CellOutput>, ShellError>; type CellResult = Result<Option<CellOutput>, ShellError>;
fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
const PADDING_SPACE: usize = 2; const PADDING_SPACE: usize = 2;
const SPLIT_LINE_SPACE: usize = 1; const SPLIT_LINE_SPACE: usize = 1;
const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE; const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE;
@ -133,15 +143,12 @@ 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(NuRecordsValue::exact(String::from("#"), 1, vec![]));
} }
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?; cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
if let Value::Error { error, .. } = item {
return Err(*error.clone());
}
let index = row + row_offset; let index = row + row_offset;
let text = item let text = item
@ -152,7 +159,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
.unwrap_or_else(|| index.to_string()); .unwrap_or_else(|| index.to_string());
let row = row + with_header as usize; let row = row + with_header as usize;
let value = NuTableCell::new(text); let value = NuRecordsValue::new(text);
data[row].push(value); data[row].push(value);
} }
@ -177,13 +184,10 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?; cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
if let Value::Error { error, .. } = item { let inner_cfg = cfg_expand_reset_table(cfg.clone(), available_width);
return Err(*error.clone()); let mut cell = expand_entry(item, inner_cfg);
}
let inner_cfg = update_config(cfg.clone(), available_width);
let mut cell = expanded_table_entry2(item, inner_cfg);
let value_width = string_width(&cell.text); let value_width = string_width(&cell.text);
if value_width > available_width { if value_width > available_width {
@ -195,7 +199,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
cell.text = wrap_text(&cell.text, available_width, cfg.opts.config); cell.text = wrap_text(&cell.text, available_width, cfg.opts.config);
} }
let value = NuTableCell::new(cell.text); let value = NuRecordsValue::new(cell.text);
data[row].push(value); data[row].push(value);
data_styles.insert((row, with_index as usize), cell.style); data_styles.insert((row, with_index as usize), cell.style);
@ -203,8 +207,8 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
} }
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1); table.set_indent(cfg.opts.config.table.padding);
table.set_index_style(get_index_style(cfg.opts.style_computer)); table.set_index_style(get_index_style(&cfg.opts.style_computer));
set_data_styles(&mut table, data_styles); set_data_styles(&mut table, data_styles);
return Ok(Some(TableOutput::new(table, false, with_index, rows_count))); return Ok(Some(TableOutput::new(table, false, with_index, rows_count)));
@ -266,13 +270,10 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
cfg.opts.signals.check(cfg.opts.span)?; cfg.opts.signals.check(cfg.opts.span)?;
check_value(item)?;
if let Value::Error { error, .. } = item { let inner_cfg = cfg_expand_reset_table(cfg.clone(), available);
return Err(*error.clone()); let mut cell = expand_entry_with_header(item, &header, inner_cfg);
}
let inner_cfg = update_config(cfg.clone(), available);
let mut cell = expanded_table_entry(item, header.as_str(), inner_cfg);
let mut value_width = string_width(&cell.text); let mut value_width = string_width(&cell.text);
if value_width > available { if value_width > available {
@ -285,14 +286,14 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
column_width = max(column_width, value_width); column_width = max(column_width, value_width);
let value = NuTableCell::new(cell.text); let value = NuRecordsValue::new(cell.text);
data[row + 1].push(value); data[row + 1].push(value);
data_styles.insert((row + 1, col + with_index as usize), cell.style); data_styles.insert((row + 1, col + with_index as usize), cell.style);
column_rows = column_rows.saturating_add(cell.size); column_rows = column_rows.saturating_add(cell.size);
} }
let head_cell = NuTableCell::new(header); let head_cell = NuRecordsValue::new(header);
data[0].push(head_cell); data[0].push(head_cell);
if column_width > available { if column_width > available {
@ -354,7 +355,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 = NuRecordsValue::exact(String::from("..."), 3, vec![]);
for row in &mut data { for row in &mut data {
row.push(shift.clone()); row.push(shift.clone());
} }
@ -364,9 +365,9 @@ 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(&cfg.opts.style_computer));
table.set_header_style(get_header_style(cfg.opts.style_computer)); table.set_header_style(get_header_style(&cfg.opts.style_computer));
table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1); table.set_indent(cfg.opts.config.table.padding);
set_data_styles(&mut table, data_styles); set_data_styles(&mut table, data_styles);
Ok(Some(TableOutput::new(table, true, with_index, rows_count))) Ok(Some(TableOutput::new(table, true, with_index, rows_count)))
@ -374,13 +375,15 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult {
fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult { fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
let theme = load_theme(cfg.opts.mode); let theme = load_theme(cfg.opts.mode);
let theme = theme.as_base();
let key_width = record let key_width = record
.columns() .columns()
.map(|col| string_width(col)) .map(|col| string_width(col))
.max() .max()
.unwrap_or(0); .unwrap_or(0);
let count_borders = let count_borders = theme.borders_has_vertical() as usize
theme.has_inner() as usize + theme.has_right() as usize + theme.has_left() as usize; + theme.borders_has_right() as usize
+ theme.borders_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 > cfg.opts.width {
return Ok(None); return Ok(None);
@ -394,7 +397,7 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
for (key, value) in record { for (key, value) in record {
cfg.opts.signals.check(cfg.opts.span)?; cfg.opts.signals.check(cfg.opts.span)?;
let cell = match expand_table_value(value, value_width, &cfg)? { let cell = match expand_value(value, value_width, &cfg)? {
Some(val) => val, Some(val) => val,
None => return Ok(None), None => return Ok(None),
}; };
@ -403,12 +406,13 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
// we could use Padding for it but, // we could use Padding for it but,
// the easiest way to do so is just push a new_line char before // the easiest way to do so is just push a new_line char before
let mut key = key.to_owned(); let mut key = key.to_owned();
if !key.is_empty() && cell.is_expanded && theme.has_top() { let is_key_on_next_line = !key.is_empty() && cell.is_expanded && theme.borders_has_top();
if is_key_on_next_line {
key.insert(0, '\n'); key.insert(0, '\n');
} }
let key = NuTableCell::new(key); let key = NuRecordsValue::new(key);
let val = NuTableCell::new(cell.text); let val = NuRecordsValue::new(cell.text);
let row = vec![key, val]; let row = vec![key, val];
data.push(row); data.push(row);
@ -418,35 +422,38 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult {
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_index_style(get_key_style(&cfg)); table.set_index_style(get_key_style(&cfg));
table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1); table.set_indent(cfg.opts.config.table.padding);
let out = TableOutput::new(table, false, true, count_rows); let mut out = TableOutput::new(table, false, true, count_rows);
maybe_expand_table(out, cfg.opts.width, &cfg.opts) configure_table(
&mut out,
cfg.opts.config,
&cfg.opts.style_computer,
cfg.opts.mode,
);
maybe_expand_table(out, cfg.opts.width)
.map(|value| value.map(|value| CellOutput::clean(value, count_rows, false))) .map(|value| value.map(|value| CellOutput::clean(value, count_rows, false)))
} }
// the flag is used as an optimization to not do `value.lines().count()` search. // 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<'_>) -> CellResult { fn expand_value(value: &Value, width: usize, cfg: &Cfg<'_>) -> CellResult {
let is_limited = matches!(cfg.format.expand_limit, Some(0)); if is_limit_reached(cfg) {
if is_limited { let value = value_to_string_clean(value, cfg);
return Ok(Some(CellOutput::clean( return Ok(Some(CellOutput::clean(value, 1, false)));
value_to_string_clean(value, cfg),
1,
false,
)));
} }
let span = value.span(); let span = value.span();
match value { match value {
Value::List { vals, .. } => { Value::List { vals, .. } => {
let inner_cfg = update_config(dive_options(cfg, span), value_width); let inner_cfg = cfg_expand_reset_table(cfg_expand_next_level(cfg.clone(), span), width);
let table = expanded_table_list(vals, inner_cfg)?; let table = expand_list(vals, inner_cfg)?;
match table { match table {
Some(out) => { Some(mut out) => {
let cfg = create_table_cfg(cfg, &out); table_apply_config(&mut out, cfg);
let value = out.table.draw(cfg, value_width); let value = out.table.draw(width);
match value { match value {
Some(value) => Ok(Some(CellOutput::clean(value, out.count_rows, true))), Some(value) => Ok(Some(CellOutput::clean(value, out.count_rows, true))),
None => Ok(None), None => Ok(None),
@ -454,182 +461,157 @@ fn expand_table_value(value: &Value, value_width: usize, cfg: &Cfg<'_>) -> CellR
} }
None => { None => {
// it means that the list is empty // it means that the list is empty
Ok(Some(CellOutput::text(value_to_wrapped_string( let value = value_to_wrapped_string(value, cfg, width);
value, Ok(Some(CellOutput::text(value)))
cfg,
value_width,
))))
} }
} }
} }
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
if record.is_empty() { if record.is_empty() {
// Like list case return styled string instead of empty value // Like list case return styled string instead of empty value
return Ok(Some(CellOutput::text(value_to_wrapped_string( let value = value_to_wrapped_string(value, cfg, width);
value, return Ok(Some(CellOutput::text(value)));
cfg,
value_width,
))));
} }
let inner_cfg = update_config(dive_options(cfg, span), value_width); let inner_cfg = cfg_expand_reset_table(cfg_expand_next_level(cfg.clone(), span), width);
let result = expanded_table_kv(record, inner_cfg)?; let result = expanded_table_kv(record, inner_cfg)?;
match result { match result {
Some(result) => Ok(Some(CellOutput::clean(result.text, result.size, true))), Some(result) => Ok(Some(CellOutput::clean(result.text, result.size, true))),
None => Ok(Some(CellOutput::text(value_to_wrapped_string( None => {
value, let value = value_to_wrapped_string(value, cfg, width);
cfg, Ok(Some(CellOutput::text(value)))
value_width, }
)))),
} }
} }
_ => Ok(Some(CellOutput::text(value_to_wrapped_string_clean( _ => {
value, let value = value_to_wrapped_string_clean(value, cfg, width);
cfg, Ok(Some(CellOutput::text(value)))
value_width, }
)))),
} }
} }
fn get_key_style(cfg: &Cfg<'_>) -> TextStyle { fn get_key_style(cfg: &Cfg<'_>) -> TextStyle {
get_header_style(cfg.opts.style_computer).alignment(Alignment::Left) get_header_style(&cfg.opts.style_computer).alignment(Alignment::Left)
} }
fn expanded_table_entry(item: &Value, header: &str, cfg: Cfg<'_>) -> CellOutput { fn expand_entry_with_header(item: &Value, header: &str, cfg: Cfg<'_>) -> CellOutput {
match item { match item {
Value::Record { val, .. } => match val.get(header) { Value::Record { val, .. } => match val.get(header) {
Some(val) => expanded_table_entry2(val, cfg), Some(val) => expand_entry(val, cfg),
None => CellOutput::styled(error_sign(cfg.opts.style_computer)), None => CellOutput::styled(error_sign(&cfg.opts.style_computer)),
}, },
_ => expanded_table_entry2(item, cfg), _ => expand_entry(item, cfg),
} }
} }
fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> CellOutput { fn expand_entry(item: &Value, cfg: Cfg<'_>) -> CellOutput {
let is_limit_reached = matches!(cfg.format.expand_limit, Some(0)); if is_limit_reached(&cfg) {
if is_limit_reached { let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
return CellOutput::styled(nu_value_to_string_clean( return CellOutput::styled(value);
item,
cfg.opts.config,
cfg.opts.style_computer,
));
} }
let span = item.span(); let span = item.span();
match &item { match &item {
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
if record.is_empty() { if record.is_empty() {
return CellOutput::styled(nu_value_to_string( let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
item, return CellOutput::styled(value);
cfg.opts.config,
cfg.opts.style_computer,
));
} }
// we verify what is the structure of a Record cause it might represent // we verify what is the structure of a Record cause it might represent
let inner_cfg = dive_options(&cfg, span); let inner_cfg = cfg_expand_next_level(cfg.clone(), span);
let table = expanded_table_kv(record, inner_cfg); let table = expanded_table_kv(record, inner_cfg);
match table { match table {
Ok(Some(table)) => table, Ok(Some(table)) => table,
_ => CellOutput::styled(nu_value_to_string( _ => {
item, let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
cfg.opts.config, CellOutput::styled(value)
cfg.opts.style_computer, }
)),
} }
} }
Value::List { vals, .. } => { Value::List { vals, .. } => {
if cfg.format.flatten && is_simple_list(vals) { if cfg.format.flatten && is_simple_list(vals) {
return CellOutput::styled(value_list_to_string( let value = list_to_string(
vals, vals,
cfg.opts.config, cfg.opts.config,
cfg.opts.style_computer, &cfg.opts.style_computer,
&cfg.format.flatten_sep, &cfg.format.flatten_sep,
)); );
return CellOutput::text(value);
} }
let inner_cfg = dive_options(&cfg, span); let inner_cfg = cfg_expand_next_level(cfg.clone(), span);
let table = expanded_table_list(vals, inner_cfg); let table = expand_list(vals, inner_cfg);
let out = match table { let mut out = match table {
Ok(Some(out)) => out, Ok(Some(out)) => out,
_ => { _ => {
return CellOutput::styled(nu_value_to_string( let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
item, return CellOutput::styled(value);
cfg.opts.config,
cfg.opts.style_computer,
))
} }
}; };
let table_config = create_table_cfg(&cfg, &out); table_apply_config(&mut out, &cfg);
let table = out.table.draw(table_config, usize::MAX);
let table = out.table.draw(usize::MAX);
match table { match table {
Some(table) => CellOutput::clean(table, out.count_rows, false), Some(table) => CellOutput::clean(table, out.count_rows, false),
None => CellOutput::styled(nu_value_to_string( None => {
item, let value = nu_value_to_string(item, cfg.opts.config, &cfg.opts.style_computer);
cfg.opts.config, CellOutput::styled(value)
cfg.opts.style_computer, }
)),
} }
} }
_ => CellOutput::styled(nu_value_to_string_clean( _ => {
item, let value = nu_value_to_string_clean(item, cfg.opts.config, &cfg.opts.style_computer);
cfg.opts.config, CellOutput::styled(value)
cfg.opts.style_computer, }
)),
} }
} }
fn is_limit_reached(cfg: &Cfg<'_>) -> bool {
matches!(cfg.format.expand_limit, Some(0))
}
fn is_simple_list(vals: &[Value]) -> bool { fn is_simple_list(vals: &[Value]) -> bool {
vals.iter() vals.iter()
.all(|v| !matches!(v, Value::Record { .. } | Value::List { .. })) .all(|v| !matches!(v, Value::Record { .. } | Value::List { .. }))
} }
fn value_list_to_string( fn list_to_string(
vals: &[Value], vals: &[Value],
config: &Config, config: &Config,
style_computer: &StyleComputer, style_computer: &StyleComputer,
flatten_sep: &str, sep: &str,
) -> NuText { ) -> String {
let mut buf = String::new(); let mut buf = String::new();
for (i, value) in vals.iter().enumerate() { for (i, value) in vals.iter().enumerate() {
if i > 0 { if i > 0 {
buf.push_str(flatten_sep); buf.push_str(sep);
} }
let text = nu_value_to_string_clean(value, config, style_computer).0; let (text, _) = nu_value_to_string_clean(value, config, style_computer);
buf.push_str(&text); buf.push_str(&text);
} }
(buf, TextStyle::default()) buf
} }
fn dive_options<'b>(cfg: &Cfg<'b>, span: Span) -> Cfg<'b> { fn maybe_expand_table(mut out: TableOutput, term_width: usize) -> StringResult {
let mut cfg = cfg.clone(); let total_width = out.table.total_width();
cfg.opts.span = span;
if let Some(deep) = cfg.format.expand_limit.as_mut() {
*deep -= 1
}
cfg
}
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, opts.mode);
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; out.table.set_strategy(true);
} }
} }
Ok(out.table.draw(table_config, term_width)) let table = out.table.draw(term_width);
Ok(table)
} }
fn set_data_styles(table: &mut NuTable, styles: HashMap<Position, TextStyle>) { fn set_data_styles(table: &mut NuTable, styles: HashMap<Position, TextStyle>) {
@ -638,22 +620,21 @@ fn set_data_styles(table: &mut NuTable, styles: HashMap<Position, TextStyle>) {
} }
} }
fn create_table_cfg(cfg: &Cfg<'_>, out: &TableOutput) -> crate::NuTableConfig { fn table_apply_config(out: &mut TableOutput, cfg: &Cfg<'_>) {
create_nu_table_config( configure_table(
cfg.opts.config,
cfg.opts.style_computer,
out, out,
false, cfg.opts.config,
&cfg.opts.style_computer,
cfg.opts.mode, cfg.opts.mode,
) )
} }
fn value_to_string(value: &Value, cfg: &Cfg<'_>) -> String { fn value_to_string(value: &Value, cfg: &Cfg<'_>) -> String {
nu_value_to_string(value, cfg.opts.config, cfg.opts.style_computer).0 nu_value_to_string(value, cfg.opts.config, &cfg.opts.style_computer).0
} }
fn value_to_string_clean(value: &Value, cfg: &Cfg<'_>) -> String { fn value_to_string_clean(value: &Value, cfg: &Cfg<'_>) -> String {
nu_value_to_string_clean(value, cfg.opts.config, cfg.opts.style_computer).0 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 { fn value_to_wrapped_string(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
@ -661,13 +642,21 @@ fn value_to_wrapped_string(value: &Value, cfg: &Cfg<'_>, value_width: usize) ->
} }
fn value_to_wrapped_string_clean(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String { fn value_to_wrapped_string_clean(value: &Value, cfg: &Cfg<'_>, value_width: usize) -> String {
let text = nu_value_to_string_colored(value, cfg.opts.config, cfg.opts.style_computer); let text = nu_value_to_string_colored(value, cfg.opts.config, &cfg.opts.style_computer);
wrap_text(&text, value_width, cfg.opts.config) wrap_text(&text, value_width, cfg.opts.config)
} }
fn update_config(cfg: Cfg<'_>, width: usize) -> Cfg<'_> { fn cfg_expand_next_level(mut cfg: Cfg<'_>, span: Span) -> Cfg<'_> {
let mut inner_cfg = cfg.clone(); cfg.opts.span = span;
inner_cfg.opts.width = width; if let Some(deep) = cfg.format.expand_limit.as_mut() {
inner_cfg.opts.index_offset = 0; *deep -= 1
inner_cfg }
cfg
}
fn cfg_expand_reset_table(mut cfg: Cfg<'_>, width: usize) -> Cfg<'_> {
cfg.opts.width = width;
cfg.opts.index_offset = 0;
cfg
} }

View File

@ -1,21 +1,22 @@
use super::has_index;
use crate::{
clean_charset, colorize_space,
common::{
create_nu_table_config, get_empty_style, get_header_style, get_index_style,
get_value_style, nu_value_to_string_colored, NuText, INDEX_COLUMN_NAME,
},
NuTable, NuTableCell, StringResult, TableOpts, TableOutput, TableResult,
};
use nu_color_config::TextStyle; use nu_color_config::TextStyle;
use nu_engine::column::get_columns; use nu_engine::column::get_columns;
use nu_protocol::{Config, Record, ShellError, Value}; use nu_protocol::{Config, Record, ShellError, Value};
use crate::{
clean_charset, colorize_space,
common::{
check_value, configure_table, get_empty_style, get_header_style, get_index_style,
get_value_style, nu_value_to_string_colored, NuText, INDEX_COLUMN_NAME,
},
types::has_index,
NuRecordsValue, NuTable, 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], opts: TableOpts<'_>) -> StringResult {
create_table(input, opts) list_table(input, opts)
} }
pub fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult { pub fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult {
@ -23,32 +24,32 @@ impl JustTable {
} }
} }
fn create_table(input: &[Value], opts: TableOpts<'_>) -> Result<Option<String>, ShellError> { fn list_table(input: &[Value], opts: TableOpts<'_>) -> Result<Option<String>, ShellError> {
match table(input, &opts)? { let mut out = match create_table(input, &opts)? {
Some(mut out) => { Some(out) => out,
let left = opts.config.table.padding.left; None => return Ok(None),
let right = opts.config.table.padding.right; };
out.table.set_indent(left, right);
colorize_space(out.table.get_records_mut(), opts.style_computer); out.table.set_indent(opts.config.table.padding);
let table_config = colorize_space(out.table.get_records_mut(), &opts.style_computer);
create_nu_table_config(opts.config, opts.style_computer, &out, false, opts.mode);
Ok(out.table.draw(table_config, opts.width)) configure_table(&mut out, opts.config, &opts.style_computer, opts.mode);
} let table = out.table.draw(opts.width);
None => Ok(None),
} Ok(table)
} }
fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult { fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult {
let mut data = vec![Vec::with_capacity(2); record.len()]; let mut data = vec![Vec::with_capacity(2); record.len()];
for ((column, value), row) in record.iter().zip(data.iter_mut()) { for ((column, value), row) in record.iter().zip(data.iter_mut()) {
opts.signals.check(opts.span)?; opts.signals.check(opts.span)?;
let value = nu_value_to_string_colored(value, opts.config, opts.style_computer); let key = NuRecordsValue::new(column.to_string());
let key = NuTableCell::new(column.to_string()); let value = nu_value_to_string_colored(value, opts.config, &opts.style_computer);
let value = NuTableCell::new(value); let value = NuRecordsValue::new(value);
row.push(key); row.push(key);
row.push(value); row.push(value);
@ -56,125 +57,144 @@ fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult {
let mut table = NuTable::from(data); let mut table = NuTable::from(data);
table.set_index_style(TextStyle::default_field()); table.set_index_style(TextStyle::default_field());
let count_rows = table.count_rows(); table.set_indent(opts.config.table.padding);
let mut out = TableOutput::new(table, false, true, count_rows); let mut out = TableOutput::from_table(table, false, true);
configure_table(&mut out, opts.config, &opts.style_computer, opts.mode);
let left = opts.config.table.padding.left; let table = out.table.draw(opts.width);
let right = opts.config.table.padding.right;
out.table.set_indent(left, right);
let table_config =
create_nu_table_config(opts.config, opts.style_computer, &out, false, opts.mode);
let table = out.table.draw(table_config, opts.width);
Ok(table) Ok(table)
} }
fn table(input: &[Value], opts: &TableOpts<'_>) -> TableResult { fn create_table(input: &[Value], opts: &TableOpts<'_>) -> TableResult {
if input.is_empty() { if input.is_empty() {
return Ok(None); return Ok(None);
} }
let mut headers = get_columns(input); let headers = get_columns(input);
let with_index = has_index(opts, &headers); let with_index = has_index(opts, &headers);
let with_header = !headers.is_empty();
let row_offset = opts.index_offset; let row_offset = opts.index_offset;
let with_header = !headers.is_empty(); let table = match (with_header, with_index) {
if !with_header { (true, true) => create_table_with_header_and_index(input, headers, row_offset, opts)?,
let table = to_table_with_no_header(input, with_index, row_offset, opts)?; (true, false) => create_table_with_header(input, headers, opts)?,
let table = table.map(|table| { (false, true) => create_table_with_no_header_and_index(input, row_offset, opts)?,
let count_rows = table.count_rows(); (false, false) => create_table_with_no_header(input, opts)?,
TableOutput::new(table, false, with_index, count_rows) };
});
return Ok(table);
}
if with_header && with_index { let table = table.map(|table| TableOutput::from_table(table, with_header, with_index));
headers.insert(0, "#".into());
}
// The header with the INDEX is removed from the table headers since
// it is added to the natural table index
let headers: Vec<_> = headers
.into_iter()
.filter(|header| header != INDEX_COLUMN_NAME)
.collect();
let table = to_table_with_header(input, &headers, with_index, row_offset, opts)?;
let table = table.map(|table| {
let count_rows = table.count_rows();
TableOutput::new(table, true, with_index, count_rows)
});
Ok(table) Ok(table)
} }
fn to_table_with_header( fn create_table_with_header(
input: &[Value], input: &[Value],
headers: &[String], headers: Vec<String>,
with_index: bool,
row_offset: usize,
opts: &TableOpts<'_>, opts: &TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> { ) -> Result<Option<NuTable>, ShellError> {
let headers = collect_headers(headers, false);
let count_rows = input.len() + 1; let count_rows = input.len() + 1;
let count_columns = headers.len(); let count_columns = headers.len();
let mut table = NuTable::new(count_rows, count_columns); let mut table = NuTable::new(count_rows, count_columns);
table.set_header_style(get_header_style(opts.style_computer));
table.set_index_style(get_index_style(opts.style_computer));
for (i, text) in headers.iter().enumerate() { table.set_header_style(get_header_style(&opts.style_computer));
table.insert((0, i), text.to_owned()); table.set_index_style(get_index_style(&opts.style_computer));
}
table.set_row(0, headers.clone());
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
opts.signals.check(opts.span)?; opts.signals.check(opts.span)?;
check_value(item)?;
if let Value::Error { error, .. } = item { for (col, header) in headers.iter().enumerate() {
return Err(*error.clone()); let (text, style) = get_string_value_with_header(item, header.as_ref(), opts);
}
if with_index { let pos = (row + 1, col);
let text = get_table_row_index(item, opts.config, row, row_offset); table.insert(pos, text);
table.insert((row + 1, 0), text); table.insert_style(pos, style);
}
let skip_index = usize::from(with_index);
for (col, header) in headers.iter().enumerate().skip(skip_index) {
let (text, style) = get_string_value_with_header(item, header, opts);
table.insert((row + 1, col), text);
table.insert_style((row + 1, col), style);
} }
} }
Ok(Some(table)) Ok(Some(table))
} }
fn to_table_with_no_header( fn create_table_with_header_and_index(
input: &[Value], input: &[Value],
with_index: bool, headers: Vec<String>,
row_offset: usize, row_offset: usize,
opts: &TableOpts<'_>, opts: &TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> { ) -> Result<Option<NuTable>, ShellError> {
let mut table = NuTable::new(input.len(), with_index as usize + 1); let headers = collect_headers(headers, true);
table.set_index_style(get_index_style(opts.style_computer));
let count_rows = input.len() + 1;
let count_columns = headers.len();
let mut table = NuTable::new(count_rows, count_columns);
table.set_header_style(get_header_style(&opts.style_computer));
table.set_index_style(get_index_style(&opts.style_computer));
table.set_row(0, headers.clone());
for (row, item) in input.iter().enumerate() { for (row, item) in input.iter().enumerate() {
opts.signals.check(opts.span)?; opts.signals.check(opts.span)?;
check_value(item)?;
if let Value::Error { error, .. } = item { let text = get_table_row_index(item, opts.config, row, row_offset);
return Err(*error.clone()); table.insert((row + 1, 0), text);
}
if with_index { for (col, header) in headers.iter().enumerate().skip(1) {
let text = get_table_row_index(item, opts.config, row, row_offset); let (text, style) = get_string_value_with_header(item, header.as_ref(), opts);
table.insert((row, 0), text);
let pos = (row + 1, col);
table.insert(pos, text);
table.insert_style(pos, style);
} }
}
Ok(Some(table))
}
fn create_table_with_no_header(
input: &[Value],
opts: &TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> {
let mut table = NuTable::new(input.len(), 1);
table.set_index_style(get_index_style(&opts.style_computer));
for (row, item) in input.iter().enumerate() {
opts.signals.check(opts.span)?;
check_value(item)?;
let (text, style) = get_string_value(item, opts); let (text, style) = get_string_value(item, opts);
let pos = (row, with_index as usize); let pos = (row, 0);
table.insert(pos, text);
table.insert_style(pos, style);
}
Ok(Some(table))
}
fn create_table_with_no_header_and_index(
input: &[Value],
row_offset: usize,
opts: &TableOpts<'_>,
) -> Result<Option<NuTable>, ShellError> {
let mut table = NuTable::new(input.len(), 1 + 1);
table.set_index_style(get_index_style(&opts.style_computer));
for (row, item) in input.iter().enumerate() {
opts.signals.check(opts.span)?;
check_value(item)?;
let text = get_table_row_index(item, opts.config, row, row_offset);
table.insert((row, 0), text);
let (text, style) = get_string_value(item, opts);
let pos = (row, 1);
table.insert(pos, text); table.insert(pos, text);
table.insert_style(pos, style); table.insert_style(pos, style);
} }
@ -186,16 +206,17 @@ fn get_string_value_with_header(item: &Value, header: &str, opts: &TableOpts) ->
match item { match item {
Value::Record { val, .. } => match val.get(header) { Value::Record { val, .. } => match val.get(header) {
Some(value) => get_string_value(value, opts), Some(value) => get_string_value(value, opts),
None => get_empty_style(opts.style_computer), None => get_empty_style(&opts.style_computer),
}, },
value => get_string_value(value, opts), value => get_string_value(value, opts),
} }
} }
fn get_string_value(item: &Value, opts: &TableOpts) -> NuText { fn get_string_value(item: &Value, opts: &TableOpts) -> NuText {
let (mut text, style) = get_value_style(item, opts.config, opts.style_computer); let (mut text, style) = get_value_style(item, opts.config, &opts.style_computer);
let is_string_value = matches!(item, Value::String { .. });
if is_string_value { let is_string = matches!(item, Value::String { .. });
if is_string {
text = clean_charset(&text); text = clean_charset(&text);
} }
@ -211,3 +232,24 @@ fn get_table_row_index(item: &Value, config: &Config, row: usize, offset: usize)
_ => (row + offset).to_string(), _ => (row + offset).to_string(),
} }
} }
fn collect_headers(headers: Vec<String>, index: bool) -> Vec<NuRecordsValue> {
// The header with the INDEX is removed from the table headers since
// it is added to the natural table index
if !index {
headers
.into_iter()
.filter(|header| header != INDEX_COLUMN_NAME)
.map(NuRecordsValue::new)
.collect()
} else {
let mut v = Vec::with_capacity(headers.len() + 1);
v.insert(0, NuRecordsValue::new("#".into()));
for text in headers {
v.push(NuRecordsValue::new(text));
}
v
}
}

View File

@ -12,9 +12,14 @@ pub use expanded::ExpandedTable;
pub use general::JustTable; pub use general::JustTable;
pub struct TableOutput { pub struct TableOutput {
/// A table structure.
pub table: NuTable, pub table: NuTable,
/// A flag whether a header was present in the table.
pub with_header: bool, pub with_header: bool,
/// A flag whether a index was present in the table.
pub with_index: bool, pub with_index: bool,
/// The value may be bigger then table.count_rows(),
/// Specifically in case of expanded table we collect the whole structure size here.
pub count_rows: usize, pub count_rows: usize,
} }
@ -27,40 +32,43 @@ impl TableOutput {
count_rows, count_rows,
} }
} }
pub fn from_table(table: NuTable, with_header: bool, with_index: bool) -> Self {
let count_rows = table.count_rows();
Self::new(table, with_header, with_index, count_rows)
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableOpts<'a> { pub struct TableOpts<'a> {
signals: &'a Signals, pub signals: &'a Signals,
config: &'a Config, pub config: &'a Config,
style_computer: &'a StyleComputer<'a>, pub style_computer: std::rc::Rc<StyleComputer<'a>>,
span: Span, pub span: Span,
width: usize, pub width: usize,
indent: (usize, usize), pub mode: TableMode,
mode: TableMode, pub index_offset: usize,
index_offset: usize, pub index_remove: bool,
index_remove: bool,
} }
impl<'a> TableOpts<'a> { impl<'a> TableOpts<'a> {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
config: &'a Config, config: &'a Config,
style_computer: &'a StyleComputer<'a>, style_computer: StyleComputer<'a>,
signals: &'a Signals, signals: &'a Signals,
span: Span, span: Span,
width: usize, width: usize,
indent: (usize, usize),
mode: TableMode, mode: TableMode,
index_offset: usize, index_offset: usize,
index_remove: bool, index_remove: bool,
) -> Self { ) -> Self {
let style_computer = std::rc::Rc::new(style_computer);
Self { Self {
signals, signals,
config, config,
style_computer, style_computer,
span, span,
indent,
width, width,
mode, mode,
index_offset, index_offset,

View File

@ -1,19 +1,21 @@
use crate::{string_width, string_wrap, TableTheme};
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_protocol::{Config, Record, Span, Value}; use nu_protocol::{Config, Record, Span, TableIndent, Value};
use tabled::{ use tabled::{
grid::{ grid::{
ansi::{ANSIBuf, ANSIStr}, ansi::ANSIStr,
config::{AlignmentHorizontal, Borders, CompactMultilineConfig}, config::{Borders, CompactMultilineConfig},
dimension::{DimensionPriority, PoolTableDimension}, dimension::{DimensionPriority, PoolTableDimension},
}, },
settings::{Color, Padding, TableOption, Theme}, settings::{Alignment, Color, Padding, TableOption},
tables::{PoolTable, TableValue}, tables::{PoolTable, TableValue},
}; };
use crate::{is_color_empty, string_width, string_wrap, TableTheme};
/// UnstructuredTable has a recursive table representation of nu_protocol::Value. /// UnstructuredTable has a recursive table representation of nu_protocol::Value.
/// ///
/// It doesn't support alignment and a proper width control. /// It doesn't support alignment and a proper width control (although it's possible to achieve).
pub struct UnstructuredTable { pub struct UnstructuredTable {
value: TableValue, value: TableValue,
} }
@ -26,7 +28,7 @@ impl UnstructuredTable {
pub fn truncate(&mut self, theme: &TableTheme, width: usize) -> bool { pub fn truncate(&mut self, theme: &TableTheme, width: usize) -> bool {
let mut available = width; let mut available = width;
let has_vertical = theme.has_left(); let has_vertical = theme.as_full().borders_has_left();
if has_vertical { if has_vertical {
available = available.saturating_sub(2); available = available.saturating_sub(2);
} }
@ -34,55 +36,34 @@ impl UnstructuredTable {
truncate_table_value(&mut self.value, has_vertical, available).is_none() truncate_table_value(&mut self.value, has_vertical, available).is_none()
} }
pub fn draw( pub fn draw(self, theme: &TableTheme, indent: TableIndent, style: &StyleComputer) -> String {
self, build_table(self.value, style, theme, indent)
style_computer: &StyleComputer,
theme: &TableTheme,
indent: (usize, usize),
) -> String {
build_table(self.value, style_computer, theme, indent)
} }
} }
fn build_table( fn build_table(
val: TableValue, val: TableValue,
style_computer: &StyleComputer, style: &StyleComputer,
theme: &TableTheme, theme: &TableTheme,
indent: (usize, usize), indent: TableIndent,
) -> String { ) -> String {
let mut table = PoolTable::from(val); let mut table = PoolTable::from(val);
let mut theme = theme.get_theme_full(); let mut theme = theme.as_full().clone();
theme.set_horizontal_lines(Default::default()); theme.set_horizontal_lines(Default::default());
table.with(Padding::new(indent.0, indent.1, 0, 0)); table.with(Padding::new(indent.left, indent.right, 0, 0));
table.with(SetRawStyle(theme)); table.with(*theme.get_borders());
table.with(SetAlignment(AlignmentHorizontal::Left)); table.with(Alignment::left());
table.with(PoolTableDimension::new( table.with(PoolTableDimension::new(
DimensionPriority::Last, DimensionPriority::Last,
DimensionPriority::Last, DimensionPriority::Last,
)); ));
// color_config closures for "separator" are just given a null. if let Some(color) = get_border_color(style) {
let color = style_computer.compute("separator", &Value::nothing(Span::unknown())); if !is_color_empty(&color) {
let color = color.paint(" ").to_string(); return build_table_with_border_color(table, color);
if let Ok(color) = Color::try_from(color) { }
// # SAFETY
//
// It's perfectly save to do cause table does not store the reference internally.
// We just need this unsafe section to cope with some limitations of [`PoolTable`].
// Mitigation of this is definitely on a todo list.
let color: ANSIBuf = color.into();
let prefix = color.get_prefix();
let suffix = color.get_suffix();
let prefix: &'static str = unsafe { std::mem::transmute(prefix) };
let suffix: &'static str = unsafe { std::mem::transmute(suffix) };
table.with(SetBorderColor(ANSIStr::new(prefix, suffix)));
let table = table.to_string();
return table;
} }
table.to_string() table.to_string()
@ -99,76 +80,74 @@ fn convert_nu_value_to_table_value(value: Value, config: &Config) -> TableValue
build_vertical_array(vals, config) build_vertical_array(vals, config)
} }
} }
value => { value => build_string_value(value, config),
let mut text = value.to_abbreviated_string(config);
if string_width(&text) > 50 {
text = string_wrap(&text, 30, false);
}
TableValue::Cell(text)
}
} }
} }
fn build_vertical_map(record: Record, config: &Config) -> TableValue { fn build_string_value(value: Value, config: &Config) -> TableValue {
let mut rows = Vec::with_capacity(record.len()); const MAX_STRING_WIDTH: usize = 50;
for (key, value) in record { const WRAP_STRING_WIDTH: usize = 30;
let val = convert_nu_value_to_table_value(value, config);
let row = TableValue::Row(vec![TableValue::Cell(key), val]); let mut text = value.to_abbreviated_string(config);
rows.push(row); if string_width(&text) > MAX_STRING_WIDTH {
text = string_wrap(&text, WRAP_STRING_WIDTH, false);
} }
let max_key_width = rows TableValue::Cell(text)
}
fn build_vertical_map(record: Record, config: &Config) -> TableValue {
let max_key_width = record
.iter() .iter()
.map(|row| match row { .map(|(k, _)| string_width(k))
TableValue::Row(list) => match &list[0] {
TableValue::Cell(key) => string_width(key),
_ => unreachable!(),
},
_ => unreachable!(),
})
.max() .max()
.unwrap_or(0); .unwrap_or(0);
rows.iter_mut().for_each(|row| { let mut rows = Vec::with_capacity(record.len());
match row { for (mut key, value) in record {
TableValue::Row(list) => match &mut list[0] { string_append_to_width(&mut key, max_key_width);
TableValue::Cell(key) => {
let width = string_width(key); let value = convert_nu_value_to_table_value(value, config);
let rest = max_key_width - width;
key.extend(std::iter::repeat(' ').take(rest)); let row = TableValue::Row(vec![TableValue::Cell(key), value]);
} rows.push(row);
_ => unreachable!(), }
},
_ => unreachable!(),
};
});
TableValue::Column(rows) TableValue::Column(rows)
} }
fn string_append_to_width(key: &mut String, max: usize) {
let width = string_width(key);
let rest = max - width;
key.extend(std::iter::repeat(' ').take(rest));
}
fn build_vertical_array(vals: Vec<Value>, config: &Config) -> TableValue { fn build_vertical_array(vals: Vec<Value>, config: &Config) -> TableValue {
let map = vals let map = vals
.into_iter() .into_iter()
.map(|val| convert_nu_value_to_table_value(val, config)) .map(|val| convert_nu_value_to_table_value(val, config))
.collect::<Vec<_>>(); .collect();
TableValue::Column(map) TableValue::Column(map)
} }
fn is_valid_record(vals: &[Value]) -> bool { fn is_valid_record(vals: &[Value]) -> bool {
let mut first_record: Option<&Record> = None; if vals.is_empty() {
for val in vals { return true;
}
let first_value = match &vals[0] {
Value::Record { val, .. } => val,
_ => return false,
};
for val in &vals[1..] {
match val { match val {
Value::Record { val, .. } => { Value::Record { val, .. } => {
if let Some(known) = first_record { let equal = val.columns().eq(first_value.columns());
let equal = known.columns().eq(val.columns()); if !equal {
if !equal { return false;
return false; }
}
} else {
first_record = Some(val)
};
} }
_ => return false, _ => return false,
} }
@ -185,30 +164,41 @@ fn count_columns_in_record(vals: &[Value]) -> usize {
} }
fn build_map_from_record(vals: Vec<Value>, config: &Config) -> TableValue { fn build_map_from_record(vals: Vec<Value>, config: &Config) -> TableValue {
let mut list = vec![]; // assumes that we have a valid record structure (checked by is_valid_record)
let head = get_columns_in_record(&vals); let head = get_columns_in_record(&vals);
let count_columns = head.len(); let mut list = Vec::with_capacity(head.len());
for col in head { for col in head {
list.push(vec![TableValue::Cell(col)]); list.push(TableValue::Column(vec![TableValue::Cell(col)]));
} }
for val in vals { for val in vals {
match val { let val = get_as_record(val);
Value::Record { val, .. } => { for (i, (_, val)) in val.into_owned().into_iter().enumerate() {
for (i, (_key, val)) in val.into_owned().into_iter().take(count_columns).enumerate() let value = convert_nu_value_to_table_value(val, config);
{ let list = get_table_value_column_mut(&mut list[i]);
let cell = convert_nu_value_to_table_value(val, config);
list[i].push(cell); list.push(value);
}
}
_ => unreachable!(),
} }
} }
let columns = list.into_iter().map(TableValue::Column).collect::<Vec<_>>(); TableValue::Row(list)
}
TableValue::Row(columns) fn get_table_value_column_mut(val: &mut TableValue) -> &mut Vec<TableValue> {
match val {
TableValue::Column(row) => row,
_ => {
unreachable!();
}
}
}
fn get_as_record(val: Value) -> nu_utils::SharedCow<Record> {
match val {
Value::Record { val, .. } => val,
_ => unreachable!(),
}
} }
fn get_columns_in_record(vals: &[Value]) -> Vec<String> { fn get_columns_in_record(vals: &[Value]) -> Vec<String> {
@ -218,32 +208,6 @@ fn get_columns_in_record(vals: &[Value]) -> Vec<String> {
} }
} }
struct SetRawStyle(Theme);
impl<R, D> TableOption<R, CompactMultilineConfig, D> for SetRawStyle {
fn change(self, _: &mut R, cfg: &mut CompactMultilineConfig, _: &mut D) {
let borders = *self.0.get_borders();
cfg.set_borders(borders);
}
}
struct SetBorderColor(ANSIStr<'static>);
impl<R, D> TableOption<R, CompactMultilineConfig, D> for SetBorderColor {
fn change(self, _: &mut R, cfg: &mut CompactMultilineConfig, _: &mut D) {
let borders = Borders::filled(self.0);
cfg.set_borders_color(borders);
}
}
struct SetAlignment(AlignmentHorizontal);
impl<R, D> TableOption<R, CompactMultilineConfig, D> for SetAlignment {
fn change(self, _: &mut R, cfg: &mut CompactMultilineConfig, _: &mut D) {
cfg.set_alignment_horizontal(self.0);
}
}
fn truncate_table_value( fn truncate_table_value(
value: &mut TableValue, value: &mut TableValue,
has_vertical: bool, has_vertical: bool,
@ -348,3 +312,44 @@ fn truncate_table_value(
} }
} }
} }
fn build_table_with_border_color(mut table: PoolTable, color: Color) -> String {
// NOTE: We have this function presizely because of color_into_ansistr internals
// color must be alive why we build table
let color = color_into_ansistr(&color);
table.with(SetBorderColor(color));
table.to_string()
}
fn color_into_ansistr(color: &Color) -> ANSIStr<'static> {
// # SAFETY
//
// It's perfectly save to do cause table does not store the reference internally.
// We just need this unsafe section to cope with some limitations of [`PoolTable`].
// Mitigation of this is definitely on a todo list.
let prefix = color.get_prefix();
let suffix = color.get_suffix();
let prefix: &'static str = unsafe { std::mem::transmute(prefix) };
let suffix: &'static str = unsafe { std::mem::transmute(suffix) };
ANSIStr::new(prefix, suffix)
}
struct SetBorderColor(ANSIStr<'static>);
impl<R, D> TableOption<R, CompactMultilineConfig, D> for SetBorderColor {
fn change(self, _: &mut R, cfg: &mut CompactMultilineConfig, _: &mut D) {
let borders = Borders::filled(self.0);
cfg.set_borders_color(borders);
}
}
fn get_border_color(style: &StyleComputer<'_>) -> Option<Color> {
// color_config closures for "separator" are just given a null.
let color = style.compute("separator", &Value::nothing(Span::unknown()));
let color = color.paint(" ").to_string();
let color = Color::try_from(color);
color.ok()
}

View File

@ -1,12 +1,15 @@
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use tabled::{ use tabled::{
builder::Builder,
grid::{ grid::{
ansi::{ANSIBuf, ANSIStr}, ansi::{ANSIBuf, ANSIStr},
records::vec_records::Text, records::vec_records::Text,
util::string::get_text_width, util::string::get_text_width,
}, },
settings::{width::Truncate, Color, Modify, Padding, Style, Width}, settings::{
width::{Truncate, Wrap},
Color,
},
}; };
use crate::common::get_leading_trailing_space_style; use crate::common::get_leading_trailing_space_style;
@ -16,34 +19,21 @@ pub fn string_width(text: &str) -> usize {
} }
pub fn string_wrap(text: &str, width: usize, keep_words: bool) -> String { pub fn string_wrap(text: &str, width: usize, keep_words: bool) -> String {
// todo: change me...
//
// well... it's not efficient to build a table to wrap a string,
// but ... it's better than a copy paste (is it?)
if text.is_empty() { if text.is_empty() {
return String::new(); return String::new();
} }
let wrap = if keep_words { let text_width = string_width(text);
Width::wrap(width).keep_words(true) if text_width <= width {
} else { return text.to_owned();
Width::wrap(width) }
};
Builder::from_iter([[text]]) Wrap::wrap(text, width, keep_words)
.build()
.with(Style::empty())
.with(Padding::zero())
.with(Modify::new((0, 0)).with(wrap))
.to_string()
} }
pub fn string_truncate(text: &str, width: usize) -> String { pub fn string_truncate(text: &str, width: usize) -> String {
// todo: change me...
let line = match text.lines().next() { let line = match text.lines().next() {
Some(first_line) => first_line, Some(line) => line,
None => return String::new(), None => return String::new(),
}; };
@ -51,35 +41,77 @@ pub fn string_truncate(text: &str, width: usize) -> String {
} }
pub fn clean_charset(text: &str) -> String { pub fn clean_charset(text: &str) -> String {
// todo: optimize, I bet it can be done in 1 path // TODO: We could make an optimization to take a String and modify it
text.replace('\t', " ").replace('\r', "") // We could check if there was any changes and if not make no allocations at all and don't change the origin.
// Why it's not done...
// Cause I am not sure how the `if` in a loop will affect performance.
// So it's better be profiled, but likely the optimization be worth it.
// At least because it's a base case where we won't change anything....
// allocating at least the text size,
// in most cases the buf will be a copy of text anyhow.
//
// but yes sometimes we will alloc more then necessary.
// We could shrink it but...it will be another realloc which make no scense.
let mut buf = String::with_capacity(text.len());
for c in text.chars() {
if c == '\n' {
buf.push(c);
continue;
}
if c == '\t' {
buf.push(' ');
buf.push(' ');
buf.push(' ');
buf.push(' ');
continue;
}
// note: Overall maybe we shall delete this check?
// it was made in order to cope with emojie issue.
// if c < ' ' && c != '\u{1b}' {
// continue;
// }
buf.push(c);
}
buf
} }
pub fn colorize_space(data: &mut [Vec<Text<String>>], style_computer: &StyleComputer<'_>) { pub fn colorize_space(data: &mut [Vec<Text<String>>], style_computer: &StyleComputer<'_>) {
if let Some(style) = get_leading_trailing_space_style(style_computer).color_style { let style = match get_leading_trailing_space_style(style_computer).color_style {
let style = ANSIBuf::from(convert_style(style)); Some(color) => color,
let style = style.as_ref(); None => return,
colorize_lead_trail_space(data, Some(style), Some(style)); };
}
}
pub fn colorize_space_str(text: &mut String, style_computer: &StyleComputer<'_>) { let style = ANSIBuf::from(convert_style(style));
if let Some(style) = get_leading_trailing_space_style(style_computer).color_style { let style = style.as_ref();
let style = ANSIBuf::from(convert_style(style)); if style.is_empty() {
let style = style.as_ref();
*text = colorize_space_one(text, Some(style), Some(style));
}
}
fn colorize_lead_trail_space(
data: &mut [Vec<Text<String>>],
lead: Option<ANSIStr<'_>>,
trail: Option<ANSIStr<'_>>,
) {
if lead.is_none() && trail.is_none() {
return; return;
} }
colorize_list(data, style, style);
}
pub fn colorize_space_str(text: &mut String, style_computer: &StyleComputer<'_>) {
let style = match get_leading_trailing_space_style(style_computer).color_style {
Some(color) => color,
None => return,
};
let style = ANSIBuf::from(convert_style(style));
let style = style.as_ref();
if style.is_empty() {
return;
}
*text = colorize_space_one(text, style, style);
}
fn colorize_list(data: &mut [Vec<Text<String>>], lead: ANSIStr<'_>, trail: ANSIStr<'_>) {
for row in data.iter_mut() { for row in data.iter_mut() {
for cell in row { for cell in row {
let buf = colorize_space_one(cell.as_ref(), lead, trail); let buf = colorize_space_one(cell.as_ref(), lead, trail);
@ -88,7 +120,7 @@ fn colorize_lead_trail_space(
} }
} }
fn colorize_space_one(text: &str, lead: Option<ANSIStr<'_>>, trail: Option<ANSIStr<'_>>) -> String { fn colorize_space_one(text: &str, lead: ANSIStr<'_>, trail: ANSIStr<'_>) -> String {
use fancy_regex::Captures; use fancy_regex::Captures;
use fancy_regex::Regex; use fancy_regex::Regex;
use std::sync::LazyLock; use std::sync::LazyLock;
@ -102,20 +134,20 @@ fn colorize_space_one(text: &str, lead: Option<ANSIStr<'_>>, trail: Option<ANSIS
let mut buf = text.to_owned(); let mut buf = text.to_owned();
if let Some(color) = &lead { if !lead.is_empty() {
buf = RE_LEADING buf = RE_LEADING
.replace_all(&buf, |cap: &Captures| { .replace_all(&buf, |cap: &Captures| {
let spaces = cap.get(1).expect("valid").as_str(); let spaces = cap.get(1).expect("valid").as_str();
format!("{}{}{}", color.get_prefix(), spaces, color.get_suffix()) format!("{}{}{}", lead.get_prefix(), spaces, lead.get_suffix())
}) })
.into_owned(); .into_owned();
} }
if let Some(color) = &trail { if !trail.is_empty() {
buf = RE_TRAILING buf = RE_TRAILING
.replace_all(&buf, |cap: &Captures| { .replace_all(&buf, |cap: &Captures| {
let spaces = cap.get(1).expect("valid").as_str(); let spaces = cap.get(1).expect("valid").as_str();
format!("{}{}{}", color.get_prefix(), spaces, color.get_suffix()) format!("{}{}{}", trail.get_prefix(), spaces, trail.get_suffix())
}) })
.into_owned(); .into_owned();
} }
@ -126,3 +158,7 @@ fn colorize_space_one(text: &str, lead: Option<ANSIStr<'_>>, trail: Option<ANSIS
pub fn convert_style(style: nu_ansi_term::Style) -> Color { pub fn convert_style(style: nu_ansi_term::Style) -> Color {
Color::new(style.prefix().to_string(), style.suffix().to_string()) Color::new(style.prefix().to_string(), style.suffix().to_string())
} }
pub fn is_color_empty(c: &Color) -> bool {
c.get_prefix().is_empty() && c.get_suffix().is_empty()
}

View File

@ -1,29 +1,79 @@
#![allow(dead_code)] #![allow(dead_code)]
use nu_table::{string_width, NuTable, NuTableConfig}; use nu_protocol::TrimStrategy;
use nu_table::{string_width, NuTable, TableTheme};
use tabled::grid::records::vec_records::Text; use tabled::grid::records::vec_records::Text;
#[derive(Debug, Clone)]
pub struct TestCase { pub struct TestCase {
cfg: NuTableConfig, theme: TableTheme,
with_header: bool,
with_footer: bool,
with_index: bool,
expand: bool,
strategy: TrimStrategy,
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(termwidth: usize) -> Self {
Self { Self {
cfg,
termwidth, termwidth,
expected, expected: None,
theme: TableTheme::basic(),
with_header: false,
with_footer: false,
with_index: false,
expand: false,
strategy: TrimStrategy::truncate(None),
} }
} }
pub fn expected(mut self, value: Option<String>) -> Self {
self.expected = value;
self
}
pub fn theme(mut self, theme: TableTheme) -> Self {
self.theme = theme;
self
}
pub fn expand(mut self) -> Self {
self.expand = true;
self
}
pub fn header(mut self) -> Self {
self.with_header = true;
self
}
pub fn footer(mut self) -> Self {
self.with_footer = true;
self
}
pub fn index(mut self) -> Self {
self.with_index = true;
self
}
pub fn trim(mut self, trim: TrimStrategy) -> Self {
self.strategy = trim;
self
}
} }
type Data = Vec<Vec<Text<String>>>; type Data = Vec<Vec<Text<String>>>;
pub fn test_table<I: IntoIterator<Item = TestCase>>(data: Data, tests: I) { pub fn test_table<I>(data: Data, tests: I)
where
I: IntoIterator<Item = TestCase>,
{
for (i, test) in tests.into_iter().enumerate() { for (i, test) in tests.into_iter().enumerate() {
let actual = create_table(data.clone(), test.cfg.clone(), test.termwidth); let actual = create_table(data.clone(), test.clone());
assert_eq!( assert_eq!(
actual, test.expected, actual, test.expected,
@ -37,9 +87,14 @@ 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, case: TestCase) -> Option<String> {
let table = NuTable::from(data); let mut table = NuTable::from(data);
table.draw(config, termwidth) table.set_theme(case.theme);
table.set_structure(case.with_index, case.with_header, case.with_footer);
table.set_trim(case.strategy);
table.set_strategy(case.expand);
table.draw(case.termwidth)
} }
pub fn create_row(count_columns: usize) -> Vec<Text<String>> { pub fn create_row(count_columns: usize) -> Vec<Text<String>> {

View File

@ -1,20 +1,19 @@
mod common; mod common;
use common::{create_row, test_table, TestCase};
use nu_protocol::TrimStrategy; use nu_protocol::TrimStrategy;
use nu_table::{NuTable, NuTableConfig, TableTheme as theme}; use nu_table::{NuTable, TableTheme as theme};
use common::{create_row, test_table, TestCase};
use tabled::grid::records::vec_records::Text; use tabled::grid::records::vec_records::Text;
#[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 mut table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
let cfg = NuTableConfig { table.set_theme(theme::heavy());
theme: theme::heavy(), table.set_structure(false, true, false);
with_header: true,
..Default::default()
};
let table = table.draw(cfg.clone(), usize::MAX); let table = table.draw(usize::MAX);
assert_eq!( assert_eq!(
table.as_deref(), table.as_deref(),
@ -28,9 +27,11 @@ fn data_and_header_has_different_size_doesnt_work() {
) )
); );
let table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]); let mut table = NuTable::from(vec![create_row(5), create_row(5), create_row(5)]);
table.set_theme(theme::heavy());
table.set_structure(false, true, false);
let table = table.draw(cfg, usize::MAX); let table = table.draw(usize::MAX);
assert_eq!( assert_eq!(
table.as_deref(), table.as_deref(),
@ -47,30 +48,27 @@ 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 tests = [
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);
assert!(table.is_none());
}
};
let mut cfg = NuTableConfig {
theme: theme::heavy(),
with_header: true,
..Default::default()
};
for case in [
TrimStrategy::truncate(None), TrimStrategy::truncate(None),
TrimStrategy::truncate(Some(String::from("**"))), TrimStrategy::truncate(Some(String::from("**"))),
TrimStrategy::truncate(Some(String::from(""))), TrimStrategy::truncate(Some(String::from(""))),
TrimStrategy::wrap(false), TrimStrategy::wrap(false),
TrimStrategy::wrap(true), TrimStrategy::wrap(true),
] { ];
cfg.trim = case;
test_loop(cfg.clone()); let data = vec![create_row(5), create_row(5), create_row(5)];
for case in tests {
for i in 0..10 {
let mut table = NuTable::from(data.clone());
table.set_theme(theme::heavy());
table.set_structure(false, true, false);
table.set_trim(case.clone());
let table = table.draw(i);
assert!(table.is_none());
}
} }
} }
@ -195,30 +193,24 @@ fn width_control_test_0() {
} }
fn test_width(data: Vec<Vec<Text<String>>>, tests: &[(usize, &str)]) { fn test_width(data: Vec<Vec<Text<String>>>, tests: &[(usize, &str)]) {
let config = NuTableConfig {
theme: theme::heavy(),
trim: TrimStrategy::truncate(Some(String::from("..."))),
with_header: true,
..Default::default()
};
let tests = tests.iter().map(|&(termwidth, expected)| { let tests = tests.iter().map(|&(termwidth, expected)| {
TestCase::new(config.clone(), termwidth, Some(expected.to_owned())) TestCase::new(termwidth)
.theme(theme::heavy())
.trim(TrimStrategy::truncate(Some(String::from("..."))))
.header()
.expected(Some(expected.to_owned()))
}); });
test_table(data, tests); test_table(data, tests);
} }
fn test_trim(tests: &[(usize, Option<&str>)], trim: TrimStrategy) { fn test_trim(tests: &[(usize, Option<&str>)], trim: TrimStrategy) {
let config = NuTableConfig {
theme: theme::heavy(),
with_header: true,
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(termwidth)
.theme(theme::heavy())
.trim(trim.clone())
.header()
.expected(expected.map(|s| s.to_string()))
}); });
let data = vec![ let data = vec![

View File

@ -1,20 +1,14 @@
mod common; mod common;
use common::{create_row, create_table}; use common::{create_row, create_table, TestCase};
use nu_table::{NuTableConfig, TableTheme as theme}; use nu_table::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 { TestCase::new(50).theme(theme::rounded()).header().expand(),
theme: theme::rounded(),
with_header: true,
expand: true,
..Default::default()
},
50,
); );
assert_eq!( assert_eq!(

View File

@ -1,7 +1,7 @@
mod common; mod common;
use common::create_row as row; use common::{create_row as row, TestCase};
use nu_table::{NuTable, NuTableConfig, TableTheme as theme}; use nu_table::{NuTable, TableTheme as theme};
use tabled::grid::records::vec_records::Text; use tabled::grid::records::vec_records::Text;
#[test] #[test]
@ -452,27 +452,18 @@ fn test_with_love() {
} }
fn create_table(data: Vec<Vec<Text<String>>>, with_header: bool, theme: theme) -> String { fn create_table(data: Vec<Vec<Text<String>>>, with_header: bool, theme: theme) -> String {
let config = NuTableConfig { let mut case = TestCase::new(usize::MAX).theme(theme);
theme, if with_header {
with_header, case = case.header();
..Default::default() }
};
let out = common::create_table(data, config, usize::MAX); common::create_table(data, case).expect("not expected to get None")
out.expect("not expected to get None")
} }
fn create_table_with_size(data: Vec<Vec<Text<String>>>, with_header: bool, theme: theme) -> String { fn create_table_with_size(data: Vec<Vec<Text<String>>>, with_header: bool, theme: theme) -> String {
let config = NuTableConfig { let mut table = NuTable::from(data);
theme, table.set_theme(theme);
with_header, table.set_structure(false, with_header, false);
..Default::default()
};
let table = NuTable::from(data); table.draw(usize::MAX).expect("not expected to get None")
table
.draw(config, usize::MAX)
.expect("not expected to get None")
} }