From 7cfd4d2cfabca8f26e8bd1f84125bc256b650787 Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Wed, 20 Sep 2023 17:59:08 +0000 Subject: [PATCH] nu-table: Add table option `--abbreviated` (#10399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `--abbreviated`/`-a` option - Adedd `abbreviate_if_longer_than` config opt for it. ```nu ls | table -a 3 ``` ``` ╭───┬────────────────────┬──────┬───────────┬──────────────╮ │ # │ name │ type │ size │ modified │ ├───┼────────────────────┼──────┼───────────┼──────────────┤ │ 0 │ CODE_OF_CONDUCT.md │ file │ 3.4 KiB │ 4 days ago │ │ 1 │ CONTRIBUTING.md │ file │ 18.3 KiB │ 2 weeks ago │ │ 2 │ Cargo.lock │ file │ 144.3 KiB │ 15 hours ago │ │ 3 │ ... │ ... │ ... │ ... │ │ 4 │ tests │ dir │ 4.0 KiB │ 4 months ago │ │ 5 │ toolkit.nu │ file │ 14.6 KiB │ 5 days ago │ │ 6 │ wix │ dir │ 4.0 KiB │ 2 months ago │ ╰───┴────────────────────┴──────┴───────────┴──────────────╯ ``` ```nu $env | table -a 3 ``` ``` ╭──────────────────┬──────────────────────────────────────────────────────────────────────────╮ │ BROWSER │ firefox │ │ CARGO │ /home/maxim/.rustup/toolchains/1.70.0-x86_64-unknown-linux-gnu/bin/cargo │ │ CARGO_HOME │ /home/maxim/.cargo │ │ ... │ ... │ │ XDG_SESSION_TYPE │ x11 │ │ XDG_VTNR │ 7 │ │ _ │ /home/maxim/.cargo/bin/cargo │ ╰──────────────────┴──────────────────────────────────────────────────────────────────────────╯ ``` close #10393 PS: Maybe as a separate issue (good candidate for `GOOD FIRST ISSUE`) add a config option to change a default `...` truncation sign to a custom? (which would be applicable not only for `--abbreviated` but all kind of tables) --------- Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com> --- crates/nu-command/src/viewers/table.rs | 571 ++++++++++-------- crates/nu-command/tests/commands/table.rs | 111 ++++ crates/nu-protocol/src/config.rs | 13 + .../src/sample_config/default_config.nu | 1 + 4 files changed, 444 insertions(+), 252 deletions(-) diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index bb2523a06..50acfa2d1 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -94,6 +94,12 @@ impl Command for Table { "expand the table structure in collapse mode.\nBe aware collapse mode currently doesn't support width control", Some('c'), ) + .named( + "abbreviated", + SyntaxShape::Int, + "abbreviate the data in the table by truncating the middle part and only showing amount provided on top and bottom", + Some('a'), + ) .category(Category::Viewers) } @@ -104,33 +110,13 @@ impl Command for Table { call: &Call, input: PipelineData, ) -> Result { - let start_num: Option = call.get_flag(engine_state, stack, "start-number")?; - let row_offset = start_num.unwrap_or_default() as usize; - let list: bool = call.has_flag("list"); - - let width_param: Option = call.get_flag(engine_state, stack, "width")?; - - let expand: bool = call.has_flag("expand"); - let expand_limit: Option = call.get_flag(engine_state, stack, "expand-deep")?; - let collapse: bool = call.has_flag("collapse"); - let flatten: bool = call.has_flag("flatten"); - let flatten_separator: Option = - call.get_flag(engine_state, stack, "flatten-separator")?; - - let table_view = match (expand, collapse) { - (false, false) => TableView::General, - (_, true) => TableView::Collapsed, - (true, _) => TableView::Expanded { - limit: expand_limit, - flatten, - flatten_separator, - }, - }; + let list_themes: bool = call.has_flag("list"); + let cfg = parse_table_config(call, engine_state, stack)?; + let input = CmdInput::new(engine_state, stack, call, input); // if list argument is present we just need to return a list of supported table themes - if list { + if list_themes { let val = Value::list(supported_table_modes(), Span::test_data()); - return Ok(val.into_pipeline_data()); } @@ -140,15 +126,7 @@ impl Command for Table { let _ = nu_utils::enable_vt_processing(); } - handle_table_command( - engine_state, - stack, - call, - input, - row_offset, - table_view, - width_param, - ) + handle_table_command(input, cfg) } fn examples(&self) -> Vec { @@ -214,86 +192,138 @@ impl Command for Table { } } -fn handle_table_command( - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - input: PipelineData, +#[derive(Debug, Clone)] +struct TableConfig { row_offset: usize, table_view: TableView, - term_width: Option, + term_width: usize, + abbreviation: Option, +} + +impl TableConfig { + fn new( + row_offset: usize, + table_view: TableView, + term_width: usize, + abbreviation: Option, + ) -> Self { + Self { + row_offset, + table_view, + term_width, + abbreviation, + } + } +} + +fn parse_table_config( + call: &Call, + state: &EngineState, + stack: &mut Stack, +) -> Result { + let start_num: Option = call.get_flag(state, stack, "start-number")?; + let row_offset = start_num.unwrap_or_default() as usize; + let width_param: Option = call.get_flag(state, stack, "width")?; + let expand: bool = call.has_flag("expand"); + let expand_limit: Option = call.get_flag(state, stack, "expand-deep")?; + let collapse: bool = call.has_flag("collapse"); + let flatten: bool = call.has_flag("flatten"); + let flatten_separator: Option = call.get_flag(state, stack, "flatten-separator")?; + let abbrivation: Option = call + .get_flag(state, stack, "abbreviated")? + .or_else(|| get_config(state, stack).table_abbreviation_threshold); + let table_view = match (expand, collapse) { + (false, false) => TableView::General, + (_, true) => TableView::Collapsed, + (true, _) => TableView::Expanded { + limit: expand_limit, + flatten, + flatten_separator, + }, + }; + + let term_width = get_width_param(width_param); + let cfg = TableConfig::new(row_offset, table_view, term_width, abbrivation); + + Ok(cfg) +} + +struct CmdInput<'a> { + engine_state: &'a EngineState, + stack: &'a mut Stack, + call: &'a Call, + data: PipelineData, +} + +impl<'a> CmdInput<'a> { + fn new( + engine_state: &'a EngineState, + stack: &'a mut Stack, + call: &'a Call, + data: PipelineData, + ) -> Self { + Self { + engine_state, + stack, + call, + data, + } + } +} + +fn handle_table_command( + mut input: CmdInput<'_>, + cfg: TableConfig, ) -> Result { - let ctrlc = engine_state.ctrlc.clone(); - let config = get_config(engine_state, stack); + let span = input.data.span().unwrap_or(input.call.head); + match input.data { + PipelineData::ExternalStream { .. } => Ok(input.data), + PipelineData::Value(Value::Binary { val, .. }, ..) => { + let stream_list = if input.call.redirect_stdout { + vec![Ok(val)] + } else { + let hex = format!("{}\n", nu_pretty_hex::pretty_hex(&val)) + .as_bytes() + .to_vec(); + vec![Ok(hex)] + }; - let span = input.span().unwrap_or(call.head); - match input { - PipelineData::ExternalStream { .. } => Ok(input), - PipelineData::Value(Value::Binary { val, .. }, ..) => Ok(PipelineData::ExternalStream { - stdout: Some(RawStream::new( - Box::new(if call.redirect_stdout { - vec![Ok(val)].into_iter() - } else { - vec![Ok(format!("{}\n", nu_pretty_hex::pretty_hex(&val)) - .as_bytes() - .to_vec())] - .into_iter() - }), + let ctrlc = input.engine_state.ctrlc.clone(); + let stream = RawStream::new( + Box::new(stream_list.into_iter()), ctrlc, - call.head, + input.call.head, None, - )), - stderr: None, - exit_code: None, - span: call.head, - metadata: None, - trim_end_newline: false, - }), - // None of these two receive a StyleComputer because handle_row_stream() can produce it by itself using engine_state and stack. - PipelineData::Value(Value::List { vals, .. }, metadata) => handle_row_stream( - engine_state, - stack, - ListStream::from_stream(vals.into_iter(), ctrlc.clone()), - call, - row_offset, - ctrlc, - metadata, - ), - PipelineData::ListStream(stream, metadata) => handle_row_stream( - engine_state, - stack, - stream, - call, - row_offset, - ctrlc, - metadata, - ), - PipelineData::Value(Value::Record { val, .. }, ..) => { - let term_width = get_width_param(term_width); + ); - handle_record( - val, - span, - engine_state, - stack, - call, - table_view, - term_width, - ctrlc, - &config, - ) + Ok(PipelineData::ExternalStream { + stdout: Some(stream), + stderr: None, + exit_code: None, + span: input.call.head, + metadata: None, + trim_end_newline: false, + }) + } + // None of these two receive a StyleComputer because handle_row_stream() can produce it by itself using engine_state and stack. + PipelineData::Value(Value::List { vals, .. }, metadata) => { + let ctrlc = input.engine_state.ctrlc.clone(); + let stream = ListStream::from_stream(vals.into_iter(), ctrlc); + input.data = PipelineData::Empty; + + handle_row_stream(input, cfg, stream, metadata) + } + PipelineData::ListStream(stream, metadata) => { + input.data = PipelineData::Empty; + handle_row_stream(input, cfg, stream, metadata) + } + PipelineData::Value(Value::Record { val, .. }, ..) => { + input.data = PipelineData::Empty; + handle_record(input, cfg, val) } PipelineData::Value(Value::LazyRecord { val, .. }, ..) => { - let collected = val.collect()?.into_pipeline_data(); - handle_table_command( - engine_state, - stack, - call, - collected, - row_offset, - table_view, - term_width, - ) + input.data = val.collect()?.into_pipeline_data(); + handle_table_command(input, cfg) } PipelineData::Value(Value::Error { error, .. }, ..) => { // Propagate this error outward, so that it goes to stderr @@ -302,72 +332,54 @@ fn handle_table_command( } PipelineData::Value(Value::CustomValue { val, .. }, ..) => { let base_pipeline = val.to_base_value(span)?.into_pipeline_data(); - Table.run(engine_state, stack, call, base_pipeline) + Table.run(input.engine_state, input.stack, input.call, base_pipeline) + } + PipelineData::Value(Value::Range { val, .. }, metadata) => { + let ctrlc = input.engine_state.ctrlc.clone(); + let stream = ListStream::from_stream(val.into_range_iter(ctrlc.clone())?, ctrlc); + input.data = PipelineData::Empty; + handle_row_stream(input, cfg, stream, metadata) } - PipelineData::Value(Value::Range { val, .. }, metadata) => handle_row_stream( - engine_state, - stack, - ListStream::from_stream(val.into_range_iter(ctrlc.clone())?, ctrlc.clone()), - call, - row_offset, - ctrlc, - metadata, - ), x => Ok(x), } } -fn supported_table_modes() -> Vec { - vec![ - Value::test_string("basic"), - Value::test_string("compact"), - Value::test_string("compact_double"), - Value::test_string("default"), - Value::test_string("heavy"), - Value::test_string("light"), - Value::test_string("none"), - Value::test_string("reinforced"), - Value::test_string("rounded"), - Value::test_string("thin"), - Value::test_string("with_love"), - Value::test_string("psql"), - Value::test_string("markdown"), - Value::test_string("dots"), - Value::test_string("restructured"), - Value::test_string("ascii_rounded"), - Value::test_string("basic_compact"), - ] -} - -#[allow(clippy::too_many_arguments)] fn handle_record( - record: Record, - span: Span, - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - table_view: TableView, - term_width: usize, - ctrlc: Option>, - config: &Config, + input: CmdInput, + cfg: TableConfig, + mut record: Record, ) -> Result { - // Create a StyleComputer to compute styles for each value in the table. - let style_computer = &StyleComputer::from_config(engine_state, stack); + let config = get_config(input.engine_state, input.stack); + let span = input.data.span().unwrap_or(input.call.head); + let styles = &StyleComputer::from_config(input.engine_state, input.stack); + let ctrlc = input.engine_state.ctrlc.clone(); let ctrlc1 = ctrlc.clone(); - let result = if record.is_empty() { - create_empty_placeholder("record", term_width, engine_state, stack) - } else { - let indent = (config.table_indent.left, config.table_indent.right); - let opts = TableOpts::new(config, style_computer, ctrlc, span, 0, term_width, indent); - let result = build_table_kv(record, table_view, opts, span)?; - match result { - Some(output) => maybe_strip_color(output, config), - None => report_unsuccessful_output(ctrlc1, term_width), - } + if record.is_empty() { + let value = + create_empty_placeholder("record", cfg.term_width, input.engine_state, input.stack); + let value = Value::string(value, span); + return Ok(value.into_pipeline_data()); }; - let val = Value::string(result, call.head); + if let Some(limit) = cfg.abbreviation { + if record.cols.len() > limit * 2 + 1 { + record.cols = abbreviate_list(&record.cols, limit, String::from("...")); + record.vals = + abbreviate_list(&record.vals, limit, Value::string("...", Span::unknown())); + } + } + + let indent = (config.table_indent.left, config.table_indent.right); + let opts = TableOpts::new(&config, styles, ctrlc, span, 0, cfg.term_width, indent); + let result = build_table_kv(record, cfg.table_view, opts, span)?; + + let result = match result { + Some(output) => maybe_strip_color(output, &config), + None => report_unsuccessful_output(ctrlc1, cfg.term_width), + }; + + let val = Value::string(result, span); Ok(val.into_pipeline_data()) } @@ -429,27 +441,31 @@ fn build_table_batch( } fn handle_row_stream( - engine_state: &EngineState, - stack: &mut Stack, + input: CmdInput<'_>, + cfg: TableConfig, stream: ListStream, - call: &Call, - row_offset: usize, - ctrlc: Option>, metadata: Option>, ) -> Result { - let head = call.head; + let ctrlc = input.engine_state.ctrlc.clone(); + let stream = match metadata.as_deref() { // First, `ls` sources: Some(PipelineMetadata { data_source: DataSource::Ls, }) => { - let config = get_config(engine_state, stack); + let config = get_config(input.engine_state, input.stack); let ctrlc = ctrlc.clone(); - let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") { - Some(v) => Some(env_to_string("LS_COLORS", &v, engine_state, stack)?), + let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") { + Some(v) => Some(env_to_string( + "LS_COLORS", + &v, + input.engine_state, + input.stack, + )?), None => None, }; let ls_colors = get_ls_colors(ls_colors_env_str); + let span = input.call.head; ListStream::from_stream( stream.map(move |mut x| match &mut x { @@ -459,7 +475,7 @@ fn handle_row_stream( while idx < record.len() { // Only the name column gets special colors, for now if record.cols[idx] == "name" { - let span = record.vals.get(idx).map(|v| v.span()).unwrap_or(head); + let span = record.vals.get(idx).map(|v| v.span()).unwrap_or(span); if let Some(Value::String { val, .. }) = record.vals.get(idx) { let val = render_path_name(val, &config, &ls_colors, span); if let Some(val) = val { @@ -483,6 +499,7 @@ fn handle_row_stream( data_source: DataSource::HtmlThemes, }) => { let ctrlc = ctrlc.clone(); + let span = input.call.head; ListStream::from_stream( stream.map(move |mut x| match &mut x { @@ -494,7 +511,7 @@ fn handle_row_stream( // Simple routine to grab the hex code, convert to a style, // then place it in a new Value::String. - let span = record.vals.get(idx).map(|v| v.span()).unwrap_or(head); + let span = record.vals.get(idx).map(|v| v.span()).unwrap_or(span); if let Some(Value::String { val, .. }) = record.vals.get(idx) { let s = match color_from_hex(val) { Ok(c) => match c { @@ -523,48 +540,23 @@ fn handle_row_stream( _ => stream, }; - let head = call.head; - let width_param: Option = call.get_flag(engine_state, stack, "width")?; - - let collapse: bool = call.has_flag("collapse"); - - let expand: bool = call.has_flag("expand"); - let limit: Option = call.get_flag(engine_state, stack, "expand-deep")?; - let flatten: bool = call.has_flag("flatten"); - let flatten_separator: Option = - call.get_flag(engine_state, stack, "flatten-separator")?; - - let table_view = match (expand, collapse) { - (_, true) => TableView::Collapsed, - (true, _) => TableView::Expanded { - flatten, - flatten_separator, - limit, - }, - _ => TableView::General, - }; + let paginator = PagingTableCreator::new( + input.call.head, + stream, + // These are passed in as a way to have PagingTable create StyleComputers + // for the values it outputs. Because engine_state is passed in, config doesn't need to. + input.engine_state.clone(), + input.stack.clone(), + ctrlc.clone(), + cfg, + ); + let stream = RawStream::new(Box::new(paginator), ctrlc, input.call.head, None); Ok(PipelineData::ExternalStream { - stdout: Some(RawStream::new( - Box::new(PagingTableCreator::new( - head, - stream, - // These are passed in as a way to have PagingTable create StyleComputers - // for the values it outputs. Because engine_state is passed in, config doesn't need to. - engine_state.clone(), - stack.clone(), - ctrlc.clone(), - row_offset, - width_param, - table_view, - )), - ctrlc, - head, - None, - )), + stdout: Some(stream), stderr: None, exit_code: None, - span: head, + span: input.call.head, metadata: None, trim_end_newline: false, }) @@ -600,24 +592,19 @@ struct PagingTableCreator { engine_state: EngineState, stack: Stack, ctrlc: Option>, - row_offset: usize, - width_param: Option, - view: TableView, elements_displayed: usize, reached_end: bool, + cfg: TableConfig, } impl PagingTableCreator { - #[allow(clippy::too_many_arguments)] fn new( head: Span, stream: ListStream, engine_state: EngineState, stack: Stack, ctrlc: Option>, - row_offset: usize, - width_param: Option, - view: TableView, + cfg: TableConfig, ) -> Self { PagingTableCreator { head, @@ -625,9 +612,7 @@ impl PagingTableCreator { engine_state, stack, ctrlc, - row_offset, - width_param, - view, + cfg, elements_displayed: 0, reached_end: false, } @@ -686,11 +671,23 @@ impl PagingTableCreator { style_comp, self.ctrlc.clone(), self.head, - self.row_offset, - get_width_param(self.width_param), + self.cfg.row_offset, + self.cfg.term_width, (cfg.table_indent.left, cfg.table_indent.right), ) } + + 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 { @@ -736,49 +733,52 @@ impl Iterator for PagingTableCreator { // Increase elements_displayed by one so on next iteration next branch of this // if else triggers and terminates stream self.elements_displayed = 1; - let term_width = get_width_param(self.width_param); - let result = - create_empty_placeholder("list", term_width, &self.engine_state, &self.stack); + let result = create_empty_placeholder( + "list", + self.cfg.term_width, + &self.engine_state, + &self.stack, + ); Some(Ok(result.into_bytes())) } else { None }; } - let table = match &self.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()), - }; + if let Some(limit) = self.cfg.abbreviation { + // todo: could be optimized cause we already consumed the list there's no point in goint back to pagination; - self.row_offset += idx; + if batch.len() > limit * 2 + 1 { + batch = abbreviate_list( + &batch, + limit, + Value::string(String::from("..."), Span::unknown()), + ); - match table { - Ok(Some(table)) => { - let table = maybe_strip_color(table, &get_config(&self.engine_state, &self.stack)); + let is_record_list = batch[..limit] + .iter() + .all(|value| matches!(value, Value::Record { .. })) + && batch[limit + 1..] + .iter() + .all(|value| matches!(value, Value::Record { .. })); - let mut bytes = table.as_bytes().to_vec(); - bytes.push(b'\n'); // nu-table tables don't come with a newline on the end + if limit > 0 && is_record_list { + // in case it's a record list we set a default text to each column instead of a single value. - Some(Ok(bytes)) + let cols = batch[0].as_record().expect("ok").cols.clone(); + let vals = + vec![Value::string(String::from("..."), Span::unknown()); cols.len()]; + batch[limit] = Value::record(Record { cols, vals }, Span::unknown()); + } } - Ok(None) => { - let msg = if nu_utils::ctrl_c::was_pressed(&self.ctrlc) { - "".into() - } else { - // assume this failed because the table was too wide - // TODO: more robust error classification - let term_width = get_width_param(self.width_param); - format!("Couldn't fit table into {term_width} columns!") - }; - Some(Ok(msg.as_bytes().to_vec())) - } - Err(err) => Some(Err(err)), } + + let table = self.build_table(batch); + + self.cfg.row_offset += idx; + + let config = get_config(&self.engine_state, &self.stack); + convert_table_to_output(table, &config, &self.ctrlc, self.cfg.term_width) } } @@ -822,7 +822,7 @@ fn render_path_name( Some(Value::string(val, span)) } -#[derive(Debug)] +#[derive(Debug, Clone)] enum TableView { General, Collapsed, @@ -869,3 +869,70 @@ fn create_empty_placeholder( .draw(config, termwidth) .expect("Could not create empty table placeholder") } + +fn convert_table_to_output( + table: Result, ShellError>, + config: &Config, + ctrlc: &Option>, + term_width: usize, +) -> Option, ShellError>> { + match table { + Ok(Some(table)) => { + let table = maybe_strip_color(table, config); + + let mut bytes = table.as_bytes().to_vec(); + bytes.push(b'\n'); // nu-table tables don't come with a newline on the end + + Some(Ok(bytes)) + } + Ok(None) => { + let msg = if nu_utils::ctrl_c::was_pressed(ctrlc) { + String::from("") + } else { + // assume this failed because the table was too wide + // TODO: more robust error classification + format!("Couldn't fit table into {} columns!", term_width) + }; + + Some(Ok(msg.as_bytes().to_vec())) + } + Err(err) => Some(Err(err)), + } +} + +fn abbreviate_list(list: &[T], limit: usize, text: T) -> Vec +where + T: Clone, +{ + let head = &list[..limit]; + let tail = &list[list.len() - limit..]; + + let mut out = Vec::with_capacity(limit * 2 + 1); + out.extend(head.iter().cloned()); + out.push(text); + out.extend(tail.iter().cloned()); + + out +} + +fn supported_table_modes() -> Vec { + vec![ + Value::test_string("basic"), + Value::test_string("compact"), + Value::test_string("compact_double"), + Value::test_string("default"), + Value::test_string("heavy"), + Value::test_string("light"), + Value::test_string("none"), + Value::test_string("reinforced"), + Value::test_string("rounded"), + Value::test_string("thin"), + Value::test_string("with_love"), + Value::test_string("psql"), + Value::test_string("markdown"), + Value::test_string("dots"), + Value::test_string("restructured"), + Value::test_string("ascii_rounded"), + Value::test_string("basic_compact"), + ] +} diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index 5897f4a55..6a3b7eecf 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -2686,3 +2686,114 @@ fn table_leading_trailing_space_bg_expand() { "╭───┬───────┬───────┬───────────────────────╮│ # │ a │ b │ c │├───┼───────┼───────┼───────────────────────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ hello │ ╭───┬───────────────╮ ││ │ │ world │ │ 0 │ 1 │ ││ │ │ │ │ 1 │ 2 │ ││ │ │ │ │ 2 │ ╭───┬───────╮ │ ││ │ │ │ │ │ │ 0 │ 1 │ │ ││ │ │ │ │ │ │ 1 │ 2 │ │ ││ │ │ │ │ │ │ 2 │ 3 │ │ ││ │ │ │ │ │ ╰───┴───────╯ │ ││ │ │ │ ╰───┴───────────────╯ │╰───┴───────┴───────┴───────────────────────╯" ); } + +#[test] +fn table_abbreviation() { + let actual = nu!( + r#"[[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table -a 100"# + ); + assert_eq!(actual.out, "╭───┬───┬───┬────────────────╮│ # │ a │ b │ c │├───┼───┼───┼────────────────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] ││ 2 │ 1 │ 2 │ 3 ││ 3 │ 1 │ 2 │ 3 ││ 4 │ 1 │ 2 │ 3 ││ 5 │ 1 │ 2 │ 3 ││ 6 │ 1 │ 2 │ 3 │╰───┴───┴───┴────────────────╯"); + + let actual = nu!( + r#"[[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table -a 2"# + ); + assert_eq!(actual.out, "╭───┬─────┬─────┬────────────────╮│ # │ a │ b │ c │├───┼─────┼─────┼────────────────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] ││ 2 │ ... │ ... │ ... ││ 3 │ 1 │ 2 │ 3 ││ 4 │ 1 │ 2 │ 3 │╰───┴─────┴─────┴────────────────╯"); + + let actual = nu!( + r#"[[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table -a 1"# + ); + assert_eq!(actual.out, "╭───┬─────┬─────┬─────╮│ # │ a │ b │ c │├───┼─────┼─────┼─────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ ... │ ... │ ... ││ 2 │ 1 │ 2 │ 3 │╰───┴─────┴─────┴─────╯"); + + let actual = nu!( + r#"[[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table -a 0"# + ); + assert_eq!(actual.out, "╭───┬─────╮│ 0 │ ... │╰───┴─────╯"); +} + +#[test] +fn table_abbreviation_kv() { + let actual = nu!( + r#"{ a: 1 b: { a: 1 b: [1 2 3] c: [1 2 3] } c: [1 2 [1 2 3] 3] e: 1 q: 2 t: 4 r: 1 x: 9 } | table -a 100"# + ); + assert_eq!(actual.out, "╭───┬───────────────────╮│ a │ 1 ││ b │ {record 3 fields} ││ c │ [list 4 items] ││ e │ 1 ││ q │ 2 ││ t │ 4 ││ r │ 1 ││ x │ 9 │╰───┴───────────────────╯"); + + let actual = nu!( + r#"{ a: 1 b: { a: 1 b: [1 2 3] c: [1 2 3] } c: [1 2 [1 2 3] 3] e: 1 q: 2 t: 4 r: 1 x: 9 } | table -a 2"# + ); + assert_eq!(actual.out, "╭─────┬───────────────────╮│ a │ 1 ││ b │ {record 3 fields} ││ ... │ ... ││ r │ 1 ││ x │ 9 │╰─────┴───────────────────╯"); + + let actual = nu!( + r#"{ a: 1 b: { a: 1 b: [1 2 3] c: [1 2 3] } c: [1 2 [1 2 3] 3] e: 1 q: 2 t: 4 r: 1 x: 9 } | table -a 1"# + ); + assert_eq!( + actual.out, + "╭─────┬─────╮│ a │ 1 ││ ... │ ... ││ x │ 9 │╰─────┴─────╯" + ); + + let actual = nu!( + r#"{ a: 1 b: { a: 1 b: [1 2 3] c: [1 2 3] } c: [1 2 [1 2 3] 3] e: 1 q: 2 t: 4 r: 1 x: 9 } | table -a 0"# + ); + assert_eq!(actual.out, "╭─────┬─────╮│ ... │ ... │╰─────┴─────╯"); +} + +#[test] +fn table_abbreviation_kv_expand() { + let actual = nu!( + r#"{ a: 1 b: { a: 1 b: [1 2 3] c: [1 2 3] } c: [1 2 [1 2 3] 3] e: 1 q: 2 t: 4 r: 1 x: 9 } | table -a 100 -e"# + ); + assert_eq!(actual.out, "╭───┬───────────────────╮│ a │ 1 ││ │ ╭───┬───────────╮ ││ b │ │ a │ 1 │ ││ │ │ │ ╭───┬───╮ │ ││ │ │ b │ │ 0 │ 1 │ │ ││ │ │ │ │ 1 │ 2 │ │ ││ │ │ │ │ 2 │ 3 │ │ ││ │ │ │ ╰───┴───╯ │ ││ │ │ │ ╭───┬───╮ │ ││ │ │ c │ │ 0 │ 1 │ │ ││ │ │ │ │ 1 │ 2 │ │ ││ │ │ │ │ 2 │ 3 │ │ ││ │ │ │ ╰───┴───╯ │ ││ │ ╰───┴───────────╯ ││ │ ╭───┬───────────╮ ││ c │ │ 0 │ 1 │ ││ │ │ 1 │ 2 │ ││ │ │ 2 │ ╭───┬───╮ │ ││ │ │ │ │ 0 │ 1 │ │ ││ │ │ │ │ 1 │ 2 │ │ ││ │ │ │ │ 2 │ 3 │ │ ││ │ │ │ ╰───┴───╯ │ ││ │ │ 3 │ 3 │ ││ │ ╰───┴───────────╯ ││ e │ 1 ││ q │ 2 ││ t │ 4 ││ r │ 1 ││ x │ 9 │╰───┴───────────────────╯"); + + let actual = nu!( + r#"{ a: 1 b: { a: 1 b: [1 2 3] c: [1 2 3] } c: [1 2 [1 2 3] 3] e: 1 q: 2 t: 4 r: 1 x: 9 } | table -a 2 -e"# + ); + assert_eq!(actual.out, "╭─────┬───────────────────╮│ a │ 1 ││ │ ╭───┬───────────╮ ││ b │ │ a │ 1 │ ││ │ │ │ ╭───┬───╮ │ ││ │ │ b │ │ 0 │ 1 │ │ ││ │ │ │ │ 1 │ 2 │ │ ││ │ │ │ │ 2 │ 3 │ │ ││ │ │ │ ╰───┴───╯ │ ││ │ │ │ ╭───┬───╮ │ ││ │ │ c │ │ 0 │ 1 │ │ ││ │ │ │ │ 1 │ 2 │ │ ││ │ │ │ │ 2 │ 3 │ │ ││ │ │ │ ╰───┴───╯ │ ││ │ ╰───┴───────────╯ ││ ... │ ... ││ r │ 1 ││ x │ 9 │╰─────┴───────────────────╯"); + + let actual = nu!( + r#"{ a: 1 b: { a: 1 b: [1 2 3] c: [1 2 3] } c: [1 2 [1 2 3] 3] e: 1 q: 2 t: 4 r: 1 x: 9 } | table -a 1 -e"# + ); + assert_eq!( + actual.out, + "╭─────┬─────╮│ a │ 1 ││ ... │ ... ││ x │ 9 │╰─────┴─────╯" + ); + + let actual = nu!( + r#"{ a: 1 b: { a: 1 b: [1 2 3] c: [1 2 3] } c: [1 2 [1 2 3] 3] e: 1 q: 2 t: 4 r: 1 x: 9 } | table -a 0 -e"# + ); + assert_eq!(actual.out, "╭─────┬─────╮│ ... │ ... │╰─────┴─────╯"); +} + +#[test] +fn table_abbreviation_by_config() { + let actual = nu!( + r#"$env.config.table.abbreviated_row_count = 100; [[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table"# + ); + assert_eq!(actual.out, "╭───┬───┬───┬────────────────╮│ # │ a │ b │ c │├───┼───┼───┼────────────────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] ││ 2 │ 1 │ 2 │ 3 ││ 3 │ 1 │ 2 │ 3 ││ 4 │ 1 │ 2 │ 3 ││ 5 │ 1 │ 2 │ 3 ││ 6 │ 1 │ 2 │ 3 │╰───┴───┴───┴────────────────╯"); + + let actual = nu!( + r#"$env.config.table.abbreviated_row_count = 2; [[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table"# + ); + assert_eq!(actual.out, "╭───┬─────┬─────┬────────────────╮│ # │ a │ b │ c │├───┼─────┼─────┼────────────────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] ││ 2 │ ... │ ... │ ... ││ 3 │ 1 │ 2 │ 3 ││ 4 │ 1 │ 2 │ 3 │╰───┴─────┴─────┴────────────────╯"); + + let actual = nu!( + r#"$env.config.table.abbreviated_row_count = 1; [[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table"# + ); + assert_eq!(actual.out, "╭───┬─────┬─────┬─────╮│ # │ a │ b │ c │├───┼─────┼─────┼─────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ ... │ ... │ ... ││ 2 │ 1 │ 2 │ 3 │╰───┴─────┴─────┴─────╯"); + + let actual = nu!( + r#"$env.config.table.abbreviated_row_count = 0; [[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table"# + ); + assert_eq!(actual.out, "╭───┬─────╮│ 0 │ ... │╰───┴─────╯"); +} + +#[test] +fn table_abbreviation_by_config_override() { + let actual = nu!( + r#"$env.config.table.abbreviated_row_count = 2; [[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table -a 1"# + ); + assert_eq!(actual.out, "╭───┬─────┬─────┬─────╮│ # │ a │ b │ c │├───┼─────┼─────┼─────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ ... │ ... │ ... ││ 2 │ 1 │ 2 │ 3 │╰───┴─────┴─────┴─────╯"); + + let actual = nu!( + r#"$env.config.table.abbreviated_row_count = 1; [[a b, c]; [1 2 3] [4 5 [1 2 3]] [1 2 3] [1 2 3] [1 2 3] [1 2 3] [1 2 3]] | table -a 2"# + ); + assert_eq!(actual.out, "╭───┬─────┬─────┬────────────────╮│ # │ a │ b │ c │├───┼─────┼─────┼────────────────┤│ 0 │ 1 │ 2 │ 3 ││ 1 │ 4 │ 5 │ [list 3 items] ││ 2 │ ... │ ... │ ... ││ 3 │ 1 │ 2 │ 3 ││ 4 │ 1 │ 2 │ 3 │╰───┴─────┴─────┴────────────────╯"); +} diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 40b229b99..0e00d5180 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -76,6 +76,7 @@ pub struct Config { pub table_move_header: bool, pub table_show_empty: bool, pub table_indent: TableIndent, + pub table_abbreviation_threshold: Option, pub use_ls_colors: bool, pub color_config: HashMap, pub use_grid_icons: bool, @@ -134,6 +135,7 @@ impl Default for Config { trim_strategy: TRIM_STRATEGY_DEFAULT, table_move_header: false, table_indent: TableIndent { left: 1, right: 1 }, + table_abbreviation_threshold: None, datetime_normal_format: None, datetime_table_format: None, @@ -1023,6 +1025,17 @@ impl Value { "show_empty" => { try_bool!(cols, vals, index, span, table_show_empty) } + "abbreviated_row_count" => { + if let Ok(b) = value.as_int() { + if b < 0 { + invalid!(Some(span), "should be an int unsigned"); + } + + config.table_abbreviation_threshold = Some(b as usize); + } else { + invalid!(Some(span), "should be an int"); + } + } x => { invalid_key!( cols, diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 683a7b8a7..820ea4c65 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -165,6 +165,7 @@ $env.config = { truncating_suffix: "..." # A suffix used by the 'truncating' methodology } header_on_separator: false # show header text on separator/border line + # abbreviated_row_count: 10 # limit data rows from top and bottom after reaching a set point } error_style: "fancy" # "fancy" or "plain" for screen reader-friendly error messages