diff --git a/Cargo.lock b/Cargo.lock index 515d8b0035..194c9898da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4680,15 +4680,15 @@ checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" [[package]] name = "papergrid" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725" +checksum = "d2b0f8def1f117e13c895f3eda65a7b5650688da29d6ad04635f61bc7b92eebd" dependencies = [ "ansi-str", "ansitok", "bytecount", "fnv", - "unicode-width 0.1.11", + "unicode-width 0.2.0", ] [[package]] @@ -7233,9 +7233,9 @@ dependencies = [ [[package]] name = "tabled" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b" +checksum = "c6709222f3973137427ce50559cd564dc187a95b9cfe01613d2f4e93610e510a" dependencies = [ "ansi-str", "ansitok", diff --git a/Cargo.toml b/Cargo.toml index f23e035822..a13c5489d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,7 +156,7 @@ sha2 = "0.10" strip-ansi-escapes = "0.2.0" syn = "2.0" sysinfo = "0.32" -tabled = { version = "0.16.0", default-features = false } +tabled = { version = "0.17.0", default-features = false } tempfile = "3.14" terminal_size = "0.4" titlecase = "3.0" diff --git a/crates/nu-command/src/debug/inspect_table.rs b/crates/nu-command/src/debug/inspect_table.rs index f7195992a8..e2a2483484 100644 --- a/crates/nu-command/src/debug/inspect_table.rs +++ b/crates/nu-command/src/debug/inspect_table.rs @@ -1,14 +1,17 @@ -use crate::debug::inspect_table::{ - global_horizontal_char::SetHorizontalChar, set_widths::SetWidths, -}; +// note: Seems like could be simplified +// IMHO: it shall not take 300+ lines :) + use nu_protocol::Value; use nu_table::{string_width, string_wrap}; + use tabled::{ grid::config::ColoredConfig, - settings::{peaker::PriorityMax, width::Wrap, Settings, Style}, + settings::{peaker::Priority, width::Wrap, Settings, Style}, Table, }; +use self::{global_horizontal_char::SetHorizontalChar, set_widths::SetWidths}; + pub fn build_table(value: Value, description: String, termsize: usize) -> String { let (head, mut data) = util::collect_input(value); let count_columns = head.len(); @@ -57,7 +60,7 @@ pub fn build_table(value: Value, description: String, termsize: usize) -> String Settings::default() .with(Style::rounded().corner_top_left('├').corner_top_right('┤')) .with(SetWidths(widths)) - .with(Wrap::new(width).priority(PriorityMax)) + .with(Wrap::new(width).priority(Priority::max(true))) .with(SetHorizontalChar::new('┼', '┴', 11 + 2 + 1)), ); diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 3c699f52df..263a1cea55 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -2,7 +2,12 @@ // overall reduce the redundant calls to StyleComputer etc. // the goal is to configure it once... +use std::{collections::VecDeque, io::IsTerminal, io::Read, path::PathBuf, str::FromStr}; + use lscolors::{LsColors, Style}; +use url::Url; +use web_time::Instant; + use nu_color_config::{color_from_hex, StyleComputer, TextStyle}; use nu_engine::{command_prelude::*, env_to_string}; use nu_path::form::Absolute; @@ -11,25 +16,16 @@ use nu_protocol::{ ByteStream, Config, DataSource, ListStream, PipelineMetadata, Signals, TableMode, ValueIterator, }; use nu_table::{ - common::create_nu_table_config, CollapsedTable, ExpandedTable, JustTable, NuTable, NuTableCell, + common::configure_table, CollapsedTable, ExpandedTable, JustTable, NuRecordsValue, NuTable, StringResult, TableOpts, TableOutput, }; use nu_utils::{get_ls_colors, terminal_size}; -use std::{collections::VecDeque, io::Read, path::PathBuf, str::FromStr}; -use url::Url; -use web_time::Instant; + +type ShellResult = Result; +type NuPathBuf = nu_path::PathBuf; const STREAM_PAGE_SIZE: usize = 1000; - -fn get_width_param(width_param: Option) -> usize { - if let Some(col) = width_param { - col as usize - } else if let Ok((w, _h)) = terminal_size() { - w as usize - } else { - 80 - } -} +const DEFAULT_TABLE_WIDTH: usize = 80; #[derive(Clone)] pub struct Table; @@ -113,17 +109,15 @@ impl Command for Table { stack: &mut Stack, call: &Call, input: PipelineData, - ) -> Result { + ) -> ShellResult { 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_themes { let val = Value::list(supported_table_modes(), Span::test_data()); return Ok(val.into_pipeline_data()); } - #[cfg(feature = "os")] - let cwd = engine_state.cwd(Some(stack))?; - let cfg = parse_table_config(call, engine_state, stack)?; - let input = CmdInput::new(engine_state, stack, call, input); + + let input = CmdInput::parse(engine_state, stack, call, input)?; // reset vt processing, aka ansi because illbehaved externals can break it #[cfg(windows)] @@ -131,12 +125,7 @@ impl Command for Table { let _ = nu_utils::enable_vt_processing(); } - handle_table_command( - input, - cfg, - #[cfg(feature = "os")] - cwd, - ) + handle_table_command(input) } fn examples(&self) -> Vec { @@ -221,80 +210,127 @@ impl Command for Table { #[derive(Debug, Clone)] struct TableConfig { - index: Option, - table_view: TableView, - term_width: usize, + view: TableView, + width: usize, theme: TableMode, abbreviation: Option, + index: Option, use_ansi_coloring: bool, } impl TableConfig { fn new( - table_view: TableView, - term_width: usize, + view: TableView, + width: usize, theme: TableMode, abbreviation: Option, index: Option, use_ansi_coloring: bool, ) -> Self { Self { - index, - table_view, - term_width, - abbreviation, + view, + width, theme, + abbreviation, + index, use_ansi_coloring, } } } +#[derive(Debug, Clone)] +enum TableView { + General, + Collapsed, + Expanded { + limit: Option, + flatten: bool, + flatten_separator: Option, + }, +} + +struct CLIArgs { + width: Option, + abbrivation: Option, + theme: TableMode, + expand: bool, + expand_limit: Option, + expand_flatten: bool, + expand_flatten_separator: Option, + collapse: bool, + index: Option, + use_ansi_coloring: bool, +} + fn parse_table_config( call: &Call, state: &EngineState, stack: &mut Stack, -) -> Result { - let width_param: Option = call.get_flag(state, stack, "width")?; - let expand: bool = call.has_flag(state, stack, "expand")?; - let expand_limit: Option = call.get_flag(state, stack, "expand-deep")?; - let collapse: bool = call.has_flag(state, stack, "collapse")?; - let flatten: bool = call.has_flag(state, stack, "flatten")?; - let flatten_separator: Option = call.get_flag(state, stack, "flatten-separator")?; - let abbrivation: Option = call - .get_flag(state, stack, "abbreviated")? - .or_else(|| stack.get_config(state).table.abbreviated_row_count); - let table_view = match (expand, collapse) { +) -> ShellResult { + let args = get_cli_args(call, state, stack)?; + let table_view = get_table_view(&args); + let term_width = get_table_width(args.width); + + let cfg = TableConfig::new( + table_view, + term_width, + args.theme, + args.abbrivation, + args.index, + args.use_ansi_coloring, + ); + + Ok(cfg) +} + +fn get_table_view(args: &CLIArgs) -> TableView { + match (args.expand, args.collapse) { (false, false) => TableView::General, (_, true) => TableView::Collapsed, (true, _) => TableView::Expanded { - limit: expand_limit, - flatten, - flatten_separator, + limit: args.expand_limit, + flatten: args.expand_flatten, + flatten_separator: args.expand_flatten_separator.clone(), }, - }; + } +} + +fn get_cli_args(call: &Call<'_>, state: &EngineState, stack: &mut Stack) -> ShellResult { + let width: Option = call.get_flag(state, stack, "width")?; + let expand: bool = call.has_flag(state, stack, "expand")?; + let expand_limit: Option = call.get_flag(state, stack, "expand-deep")?; + let expand_flatten: bool = call.has_flag(state, stack, "flatten")?; + let expand_flatten_separator: Option = + call.get_flag(state, stack, "flatten-separator")?; + let collapse: bool = call.has_flag(state, stack, "collapse")?; + let abbrivation: Option = call + .get_flag(state, stack, "abbreviated")? + .or_else(|| stack.get_config(state).table.abbreviated_row_count); let theme = get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table.mode); 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); - Ok(TableConfig::new( - table_view, - term_width, + Ok(CLIArgs { theme, abbrivation, + collapse, + expand, + expand_limit, + expand_flatten, + expand_flatten_separator, + width, index, use_ansi_coloring, - )) + }) } fn get_index_flag( call: &Call, state: &EngineState, stack: &mut Stack, -) -> Result, ShellError> { +) -> ShellResult> { let index: Option = call.get_flag(state, stack, "index")?; let value = match index { Some(value) => value, @@ -335,7 +371,7 @@ fn get_theme_flag( call: &Call, state: &EngineState, stack: &mut Stack, -) -> Result, ShellError> { +) -> ShellResult> { call.get_flag(state, stack, "theme")? .map(|theme: String| { TableMode::from_str(&theme).map_err(|err| ShellError::CantConvert { @@ -353,29 +389,36 @@ struct CmdInput<'a> { stack: &'a mut Stack, call: &'a Call<'a>, data: PipelineData, + cfg: TableConfig, + cwd: Option, } impl<'a> CmdInput<'a> { - fn new( + fn parse( engine_state: &'a EngineState, stack: &'a mut Stack, call: &'a Call<'a>, data: PipelineData, - ) -> Self { - Self { + ) -> ShellResult { + let cfg = parse_table_config(call, engine_state, stack)?; + let cwd = get_cwd(engine_state, stack)?; + + Ok(Self { engine_state, stack, call, data, - } + cfg, + cwd, + }) + } + + fn get_config(&self) -> std::sync::Arc { + self.stack.get_config(self.engine_state) } } -fn handle_table_command( - mut input: CmdInput<'_>, - cfg: TableConfig, - #[cfg(feature = "os")] cwd: nu_path::PathBuf, -) -> Result { +fn handle_table_command(mut input: CmdInput<'_>) -> ShellResult { let span = input.data.span().unwrap_or(input.call.head); match input.data { // 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); input.data = PipelineData::Empty; - handle_row_stream( - input, - cfg, - stream, - metadata, - #[cfg(feature = "os")] - cwd, - ) + handle_row_stream(input, stream, metadata) } PipelineData::ListStream(stream, metadata) => { input.data = PipelineData::Empty; - handle_row_stream( - input, - cfg, - stream, - metadata, - #[cfg(feature = "os")] - cwd, - ) + handle_row_stream(input, stream, metadata) } PipelineData::Value(Value::Record { val, .. }, ..) => { input.data = PipelineData::Empty; - handle_record(input, cfg, val.into_owned()) + handle_record(input, val.into_owned()) } PipelineData::Value(Value::Error { error, .. }, ..) => { // Propagate this error outward, so that it goes to stderr @@ -435,14 +464,7 @@ fn handle_table_command( let stream = ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals); input.data = PipelineData::Empty; - handle_row_stream( - input, - cfg, - stream, - metadata, - #[cfg(feature = "os")] - cwd, - ) + handle_row_stream(input, stream, metadata) } x => Ok(x), } @@ -517,60 +539,55 @@ fn pretty_hex_stream(stream: ByteStream, span: Span) -> ByteStream { ) } -fn handle_record( - input: CmdInput, - cfg: TableConfig, - mut record: Record, -) -> Result { - let config = { - let state = input.engine_state; - let stack: &Stack = input.stack; - stack.get_config(state) - }; +fn handle_record(input: CmdInput, mut record: Record) -> ShellResult { let span = input.data.span().unwrap_or(input.call.head); - let styles = &StyleComputer::from_config(input.engine_state, input.stack); if record.is_empty() { 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); return Ok(value.into_pipeline_data()); }; - if let Some(limit) = cfg.abbreviation { - let prev_len = record.len(); - 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)); - } + if let Some(limit) = input.cfg.abbreviation { + record = make_record_abbreviation(record, limit); } - let indent = (config.table.padding.left, config.table.padding.right); - let opts = TableOpts::new( + let config = input.get_config(); + let opts = create_table_opts( + input.engine_state, + input.stack, &config, - styles, - input.engine_state.signals(), + &input.cfg, span, - cfg.term_width, - indent, - cfg.theme, - cfg.index.unwrap_or(0), - cfg.index.is_none(), + 0, ); - 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 { - Some(output) => maybe_strip_color(output, cfg.use_ansi_coloring), - None => report_unsuccessful_output(input.engine_state.signals(), cfg.term_width), + Some(output) => maybe_strip_color(output, input.cfg.use_ansi_coloring), + None => report_unsuccessful_output(input.engine_state.signals(), input.cfg.width), }; 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 { @@ -608,11 +625,11 @@ fn build_table_kv( fn build_table_batch( vals: Vec, - table_view: TableView, + view: TableView, opts: TableOpts<'_>, span: Span, ) -> StringResult { - match table_view { + match view { TableView::General => JustTable::table(&vals, opts), TableView::Expanded { limit, @@ -631,22 +648,17 @@ fn build_table_batch( fn handle_row_stream( input: CmdInput<'_>, - cfg: TableConfig, stream: ListStream, metadata: Option, - #[cfg(feature = "os")] cwd: nu_path::PathBuf, -) -> Result { +) -> ShellResult { + let cfg = input.get_config(); let stream = match metadata.as_ref() { // First, `ls` sources: Some(PipelineMetadata { data_source: DataSource::Ls, .. }) => { - let config = { - let state = input.engine_state; - let stack: &Stack = input.stack; - stack.get_config(state) - }; + let config = cfg.clone(); let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") { Some(v) => Some(env_to_string( "LS_COLORS", @@ -664,14 +676,9 @@ fn handle_row_stream( if let Some(value) = record.to_mut().get_mut("name") { let span = value.span(); if let Value::String { val, .. } = value { - if let Some(val) = render_path_name( - val, - &config, - &ls_colors, - #[cfg(feature = "os")] - cwd.clone(), - span, - ) { + if let Some(val) = + render_path_name(val, &config, &ls_colors, input.cwd.clone(), span) + { *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. input.engine_state.clone(), input.stack.clone(), + input.cfg, cfg, ); let stream = ByteStream::from_result_iter( @@ -787,8 +795,9 @@ struct PagingTableCreator { stack: Stack, elements_displayed: usize, reached_end: bool, - cfg: TableConfig, + table_config: TableConfig, row_offset: usize, + config: std::sync::Arc, } impl PagingTableCreator { @@ -797,114 +806,51 @@ impl PagingTableCreator { stream: ListStream, engine_state: EngineState, stack: Stack, - cfg: TableConfig, + table_config: TableConfig, + config: std::sync::Arc, ) -> Self { PagingTableCreator { head, stream: stream.into_inner(), engine_state, stack, - cfg, + config, + table_config, elements_displayed: 0, reached_end: false, row_offset: 0, } } - fn build_extended( - &mut self, - batch: Vec, - limit: Option, - flatten: bool, - flatten_separator: Option, - ) -> StringResult { + fn build_table(&mut self, batch: Vec) -> ShellResult> { if batch.is_empty() { return Ok(None); } - 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); - let view = TableView::Expanded { - limit, - flatten, - flatten_separator, - }; - - build_table_batch(batch, view, opts, self.head) + let opts = self.create_table_opts(); + build_table_batch(batch, self.table_config.view.clone(), opts, self.head) } - fn build_collapsed(&mut self, batch: Vec) -> StringResult { - if batch.is_empty() { - return Ok(None); - } - - 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::Collapsed, opts, self.head) - } - - fn build_general(&mut self, batch: Vec) -> 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(), + fn create_table_opts(&self) -> TableOpts<'_> { + create_table_opts( + &self.engine_state, + &self.stack, + &self.config, + &self.table_config, self.head, - self.cfg.term_width, - (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(), + self.row_offset, ) } - - fn build_table(&mut self, batch: Vec) -> Result, 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 { - type Item = Result, ShellError>; + type Item = ShellResult>; fn next(&mut self) -> Option { let batch; let end; - match self.cfg.abbreviation { + match self.table_config.abbreviation { Some(abbr) => { (batch, _, end) = stream_collect_abbriviated(&mut self.stream, abbr, self.engine_state.signals()); @@ -934,7 +880,7 @@ impl Iterator for PagingTableCreator { self.elements_displayed = 1; let result = create_empty_placeholder( "list", - self.cfg.term_width, + self.table_config.width, &self.engine_state, &self.stack, ); @@ -950,9 +896,9 @@ impl Iterator for PagingTableCreator { convert_table_to_output( table, - &self.cfg, 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, config: &Config, ls_colors: &LsColors, - #[cfg(feature = "os")] cwd: nu_path::PathBuf, + cwd: Option, span: Span, ) -> Option { if !config.ls.use_ls_colors { return None; } - #[cfg(feature = "os")] - let fullpath = cwd.join(path); - #[cfg(not(feature = "os"))] - let fullpath = path; + let fullpath = match cwd { + Some(cwd) => PathBuf::from(cwd.join(path)), + None => PathBuf::from(path), + }; let stripped_path = nu_utils::strip_ansi_unlikely(path); let metadata = std::fs::symlink_metadata(fullpath); @@ -1101,19 +1047,9 @@ fn render_path_name( Some(Value::string(val, span)) } -#[derive(Debug, Clone)] -enum TableView { - General, - Collapsed, - Expanded { - limit: Option, - flatten: bool, - flatten_separator: Option, - }, -} - 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 nu_utils::strip_ansi_string_likely(output) } else { @@ -1133,29 +1069,29 @@ fn create_empty_placeholder( 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 mut table = NuTable::from(data); 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 config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default()); + configure_table(&mut out, &config, style_computer, TableMode::default()); out.table - .draw(config, termwidth) + .draw(termwidth) .expect("Could not create empty table placeholder") } fn convert_table_to_output( - table: Result, ShellError>, - cfg: &TableConfig, + table: ShellResult>, signals: &Signals, term_width: usize, -) -> Option, ShellError>> { + use_ansi_coloring: bool, +) -> Option>> { match 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(); 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::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> { + #[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) -> 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 + } +} diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index b9b80b9e87..586987970c 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -2867,13 +2867,52 @@ fn table_index_arg() { #[test] fn table_expand_index_arg() { 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"); - 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"); - 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] diff --git a/crates/nu-explore/src/commands/expand.rs b/crates/nu-explore/src/commands/expand.rs index 8b03b71dc1..a62dc3538e 100644 --- a/crates/nu-explore/src/commands/expand.rs +++ b/crates/nu-explore/src/commands/expand.rs @@ -69,12 +69,9 @@ fn convert_value_to_string( } else { let config = engine_state.get_config(); 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( - engine_state.signals(), - config, - &style_computer, - value, - )) + Ok(table) } } diff --git a/crates/nu-explore/src/nu_common/table.rs b/crates/nu-explore/src/nu_common/table.rs index 79a97ed9ca..66ea63d7ad 100644 --- a/crates/nu-explore/src/nu_common/table.rs +++ b/crates/nu-explore/src/nu_common/table.rs @@ -1,80 +1,57 @@ use crate::nu_common::NuConfig; use nu_color_config::StyleComputer; -use nu_protocol::{Record, Signals, Span, Value}; +use nu_protocol::{Record, Signals, Value}; use nu_table::{ common::{nu_value_to_string, nu_value_to_string_clean}, ExpandedTable, TableOpts, }; pub fn try_build_table( + value: Value, signals: &Signals, config: &NuConfig, - style_computer: &StyleComputer, - value: Value, + style_computer: StyleComputer, ) -> String { 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( config, style_computer, signals, - Span::unknown(), + span, usize::MAX, - (config.table.padding.left, config.table.padding.right), config.table.mode, 0, 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 { 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( - vals: Vec, - 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); +fn try_build_list(vals: Vec, opts: TableOpts<'_>) -> String { + let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts.clone()); match result { Ok(Some(out)) => out, - Ok(None) | Err(_) => { + _ => { // 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 } } } diff --git a/crates/nu-protocol/src/config/mod.rs b/crates/nu-protocol/src/config/mod.rs index 6ce7335e9c..42da2b3233 100644 --- a/crates/nu-protocol/src/config/mod.rs +++ b/crates/nu-protocol/src/config/mod.rs @@ -22,7 +22,7 @@ pub use plugin_gc::{PluginGcConfig, PluginGcConfigs}; pub use reedline::{CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu}; pub use rm::RmConfig; 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 completions; diff --git a/crates/nu-protocol/src/config/table.rs b/crates/nu-protocol/src/config/table.rs index e88f4ea88f..5c6aaf07d6 100644 --- a/crates/nu-protocol/src/config/table.rs +++ b/crates/nu-protocol/src/config/table.rs @@ -277,6 +277,12 @@ pub struct TableIndent { pub right: usize, } +impl TableIndent { + pub fn new(left: usize, right: usize) -> Self { + Self { left, right } + } +} + impl IntoValue for TableIndent { fn into_value(self, span: Span) -> Value { record! { diff --git a/crates/nu-table/examples/table_demo.rs b/crates/nu-table/examples/table_demo.rs index 402936e8c0..581d8934ea 100644 --- a/crates/nu-table/examples/table_demo.rs +++ b/crates/nu-table/examples/table_demo.rs @@ -1,6 +1,6 @@ use nu_ansi_term::{Color, Style}; use nu_color_config::TextStyle; -use nu_table::{NuTable, NuTableConfig, TableTheme}; +use nu_table::{NuTable, TableTheme}; use tabled::grid::records::vec_records::Text; fn main() { @@ -28,15 +28,11 @@ fn main() { table.set_data_style(TextStyle::basic_left()); table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue))); - - let table_cfg = NuTableConfig { - theme: TableTheme::rounded(), - with_header: true, - ..Default::default() - }; + table.set_theme(TableTheme::rounded()); + table.set_structure(false, true, false); let output_table = table - .draw(table_cfg, width) + .draw(width) .unwrap_or_else(|| format!("Couldn't fit table into {width} columns!")); println!("{output_table}") diff --git a/crates/nu-table/src/common.rs b/crates/nu-table/src/common.rs index 13c8f84fbc..8c89fbbfe2 100644 --- a/crates/nu-table/src/common.rs +++ b/crates/nu-table/src/common.rs @@ -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_protocol::{Config, FooterMode, ShellError, Span, TableMode, TrimStrategy, Value}; + 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 TableResult = Result, ShellError>; pub type StringResult = Result, ShellError>; pub const INDEX_COLUMN_NAME: &str = "index"; -pub fn create_nu_table_config( +pub fn configure_table( + out: &mut TableOutput, config: &Config, comp: &StyleComputer, - out: &TableOutput, - expand: bool, 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(); if config.table.footer_inheritance { count_rows = out.count_rows; } - let with_footer = 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, - } + with_footer(config, out.with_header, count_rows) } -pub fn nu_value_to_string_colored(val: &Value, cfg: &Config, style: &StyleComputer) -> String { - let (mut text, value_style) = nu_value_to_string(val, cfg, style); - if let Some(color) = value_style.color_style { +pub fn nu_value_to_string_colored(val: &Value, cfg: &Config, comp: &StyleComputer) -> String { + let (mut text, style) = nu_value_to_string(val, cfg, comp); + + 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(); } - if matches!(val, Value::String { .. }) { - text = clean_charset(&text); - colorize_space_str(&mut text, style); + if is_string { + colorize_space_str(&mut text, comp); } 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 { let float_precision = cfg.float_precision as usize; 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 { let (text, style) = nu_value_to_string(val, cfg, style_comp); 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) { - 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 { @@ -122,36 +134,23 @@ pub fn get_empty_style(style_computer: &StyleComputer) -> NuText { ) } -fn make_styled_string( - style_computer: &StyleComputer, +fn make_styled_value( text: String, - value: Option<&Value>, // None represents table holes. + value: &Value, float_precision: usize, + style_computer: &StyleComputer, ) -> NuText { match value { - Some(value) => { - match value { - Value::Float { .. } => { - // set dynamic precision from config - let precise_number = match convert_with_precision(&text, float_precision) { - Ok(num) => num, - Err(e) => e.to_string(), - }; - (precise_number, style_computer.style_primitive(value)) - } - _ => (text, style_computer.style_primitive(value)), - } - } - None => { - // Though holes are not the same as null, the closure for "empty" is passed a null anyway. - ( - text, - TextStyle::with_style( - Alignment::Center, - style_computer.compute("empty", &Value::nothing(Span::unknown())), - ), - ) + Value::Float { .. } => { + // set dynamic precision from config + let precise_number = match convert_with_precision(&text, float_precision) { + Ok(num) => num, + Err(e) => e.to_string(), + }; + + (precise_number, style_computer.style_primitive(value)) } + _ => (text, style_computer.style_primitive(value)), } } @@ -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(()), + } +} diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs index 182585f193..eb4934d87e 100644 --- a/crates/nu-table/src/lib.rs +++ b/crates/nu-table/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../README.md")] + mod table; mod table_theme; mod types; @@ -9,7 +10,7 @@ pub mod common; pub use common::{StringResult, TableResult}; pub use nu_color_config::TextStyle; -pub use table::{NuTable, NuTableCell, NuTableConfig}; +pub use table::{NuRecordsValue, NuTable}; pub use table_theme::TableTheme; pub use types::{CollapsedTable, ExpandedTable, JustTable, TableOpts, TableOutput}; pub use unstructured_table::UnstructuredTable; diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index bf75e14380..b706a41eb2 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -1,59 +1,46 @@ -use crate::{convert_style, table_theme::TableTheme}; +use std::{cmp::min, collections::HashMap}; + use nu_ansi_term::Style; use nu_color_config::TextStyle; -use nu_protocol::TrimStrategy; +use nu_protocol::{TableIndent, TrimStrategy}; use nu_utils::strip_ansi_unlikely; -use std::{cmp::min, collections::HashMap}; + use tabled::{ builder::Builder, grid::{ ansi::ANSIBuf, colors::Colors, - config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position}, + config::{AlignmentHorizontal, ColoredConfig, Entity, Position}, dimension::CompleteDimensionVecRecords, records::{ vec_records::{Cell, Text, VecRecords}, - ExactRecords, PeekableRecords, Records, Resizable, + ExactRecords, Records, Resizable, }, }, settings::{ + format::FormatContent, formatting::AlignmentStrategy, - object::{Columns, Segment}, - peaker::Peaker, + object::{Columns, Row, Rows}, + peaker::Priority, themes::ColumnNames, width::Truncate, - Alignment, Color, Modify, Padding, Settings, TableOption, Width, + Alignment, Color, Format, Modify, ModifyList, Padding, Settings, TableOption, Width, }, Table, }; +use crate::{convert_style, is_color_empty, table_theme::TableTheme}; + +pub type NuRecords = VecRecords; +pub type NuRecordsValue = Text; + /// NuTable is a table rendering implementation. #[derive(Debug, Clone)] pub struct NuTable { data: NuRecords, styles: Styles, alignments: Alignments, - indent: (usize, usize), -} - -pub type NuRecords = VecRecords; -pub type NuTableCell = Text; - -#[derive(Debug, Default, Clone)] -struct Styles { - index: ANSIBuf, - header: ANSIBuf, - data: EntityMap, - data_is_set: bool, -} - -#[derive(Debug, Clone)] -struct Alignments { - data: AlignmentHorizontal, - index: AlignmentHorizontal, - header: AlignmentHorizontal, - columns: HashMap, - cells: HashMap, + config: TableConfig, } impl NuTable { @@ -62,7 +49,6 @@ impl NuTable { Self { data: VecRecords::new(vec![vec![Text::default(); count_columns]; count_rows]), styles: Styles::default(), - indent: (1, 1), alignments: Alignments { data: AlignmentHorizontal::Left, index: AlignmentHorizontal::Right, @@ -70,6 +56,15 @@ impl NuTable { columns: HashMap::default(), cells: HashMap::default(), }, + config: TableConfig { + theme: TableTheme::basic(), + trim: TrimStrategy::truncate(None), + structure: TableStructure::new(false, false, false), + indent: TableIndent::new(1, 1), + header_on_border: false, + expand: false, + border_color: None, + }, } } @@ -87,11 +82,23 @@ impl NuTable { self.data[pos.0][pos.1] = Text::new(text); } + pub fn insert_row(&mut self, index: usize, row: Vec) { + let data = &mut self.data[index]; + + for (col, text) in row.into_iter().enumerate() { + data[col] = Text::new(text); + } + } + + pub fn set_row(&mut self, index: usize, row: Vec) { + assert_eq!(self.data[index].len(), row.len()); + self.data[index] = row; + } + pub fn set_column_style(&mut self, column: usize, style: TextStyle) { if let Some(style) = style.color_style { - let style = ANSIBuf::from(convert_style(style)); - self.styles.data.insert(Entity::Column(column), style); - self.styles.data_is_set = true; + let style = convert_style(style); + self.styles.columns.insert(column, style); } let alignment = convert_alignment(style.alignment); @@ -102,9 +109,8 @@ impl NuTable { pub fn insert_style(&mut self, pos: Position, style: TextStyle) { if let Some(style) = style.color_style { - let style = ANSIBuf::from(convert_style(style)); - self.styles.data.insert(Entity::Cell(pos.0, pos.1), style); - self.styles.data_is_set = true; + let style = convert_style(style); + self.styles.cells.insert(pos, style); } let alignment = convert_alignment(style.alignment); @@ -115,7 +121,7 @@ impl NuTable { pub fn set_header_style(&mut self, style: TextStyle) { if let Some(style) = style.color_style { - let style = ANSIBuf::from(convert_style(style)); + let style = convert_style(style); self.styles.header = style; } @@ -124,7 +130,7 @@ impl NuTable { pub fn set_index_style(&mut self, style: TextStyle) { if let Some(style) = style.color_style { - let style = ANSIBuf::from(convert_style(style)); + let style = convert_style(style); self.styles.index = style; } @@ -133,16 +139,39 @@ impl NuTable { pub fn set_data_style(&mut self, style: TextStyle) { if let Some(style) = style.color_style { - let style = ANSIBuf::from(convert_style(style)); - self.styles.data.insert(Entity::Global, style); - self.styles.data_is_set = true; + let style = convert_style(style); + self.styles.data = style; } self.alignments.data = convert_alignment(style.alignment); } - pub fn set_indent(&mut self, left: usize, right: usize) { - self.indent = (left, right); + pub fn set_indent(&mut self, indent: TableIndent) { + self.config.indent = indent; + } + + pub fn set_theme(&mut self, theme: TableTheme) { + self.config.theme = theme; + } + + pub fn set_structure(&mut self, index: bool, header: bool, footer: bool) { + self.config.structure = TableStructure::new(index, header, footer); + } + + pub fn set_border_header(&mut self, on: bool) { + self.config.header_on_border = on; + } + + pub fn set_trim(&mut self, strategy: TrimStrategy) { + self.config.trim = strategy; + } + + pub fn set_strategy(&mut self, expand: bool) { + self.config.expand = expand; + } + + pub fn set_border_color(&mut self, color: Style) { + self.config.border_color = (!color.is_plain()).then_some(color); } pub fn get_records_mut(&mut self) -> &mut NuRecords { @@ -152,21 +181,15 @@ impl NuTable { /// Converts a table to a String. /// /// It returns None in case where table cannot be fit to a terminal width. - pub fn draw(self, config: NuTableConfig, termwidth: usize) -> Option { - build_table( - self.data, - config, - self.alignments, - self.styles, - termwidth, - self.indent, - ) + pub fn draw(self, termwidth: usize) -> Option { + build_table(self, termwidth) } /// Return a total table width. - pub fn total_width(&self, config: &NuTableConfig) -> usize { - let config = get_config(&config.theme, false, None); - let widths = build_width(&self.data, self.indent.0 + self.indent.1); + pub fn total_width(&self) -> usize { + let config = create_config(&self.config.theme, false, None); + let pad = indent_sum(self.config.indent); + let widths = build_width(&self.data, pad); get_total_width2(&widths, &config) } } @@ -180,150 +203,175 @@ impl From>>> for NuTable { } } -#[derive(Debug, Clone)] -pub struct NuTableConfig { - pub theme: TableTheme, - pub trim: TrimStrategy, - pub split_color: Option