diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 44e96eb060..a9e7361a9e 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -1088,7 +1088,7 @@ fn create_empty_placeholder( 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); + let out = TableOutput::new(table, false, false, false); let style_computer = &StyleComputer::from_config(engine_state, stack); let config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default()); diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index 24484b1206..96ae395e47 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -2905,3 +2905,39 @@ fn table_general_header_on_separator_issue1() { let actual = nu!("$env.config.table.header_on_separator = true; [['Llll oo Bbbbbbbb' 'Bbbbbbbb Aaaa' Nnnnnn Ggggg 'Xxxxx Llllllll #' Bbb 'Pppp Ccccc' 'Rrrrrrrr Dddd' Rrrrrr 'Rrrrrr Ccccc II' 'Rrrrrr Ccccc Ppppppp II' 'Pppppp Dddddddd Tttt' 'Pppppp Dddddddd Dddd' 'Rrrrrrrrr Trrrrrr' 'Pppppp Ppppp Dddd' 'Ppppp Dddd' Hhhh]; [RRRRRRR FFFFFFFF UUUU VV 202407160001 BBB 1 '7/16/2024' '' AAA-1111 AAA-1111-11 '7 YEARS' 2555 'RRRRRRRR DDDD' '7/16/2031' '7/16/2031' NN]] | table --width=87 --theme basic"); assert_eq!(actual.out, "+-#-+-Llll oo Bbbbbbbb-+-Bbbbbbbb Aaaa-+-Nnnnnn-+-Ggggg-+-Xxxxx Llllllll #-+-...-+| 0 | RRRRRRR | FFFFFFFF | UUUU | VV | 202407160001 | ... |+---+------------------+---------------+--------+-------+------------------+-----+"); } + +#[test] +fn table_footer_inheritance() { + let table1 = format!( + "[ [ head1, head2, head3 ]; {} ]", + (0..212) + .map(|_| "[ 79 79 79 ]") + .collect::>() + .join(" ") + ); + + let structure = format!( + "{{\ + field0: [ [ y1, y2, y3 ]; [ 1 2 3 ] [ 79 79 79 ] [ {{ f1: 'a string', f2: 1000 }}, 1, 2 ] ],\ + field1: [ a, b, c ],\ + field2: [ 123, 234, 345 ],\ + field3: {},\ + field4: {{ f1: 1, f2: 3, f3: {{ f1: f1, f2: f2, f3: f3 }} }},\ + field5: [ [ x1, x2, x3 ]; [ 1 2 3 ] [ 79 79 79 ] [ {{ f1: 'a string', f2: 1000 }}, 1, 2 ] ],\ + }}", + table1 + ); + let actual = nu!(format!( + "$env.config.table.footer_inheritance = true; {structure} | table --width=80 --expand" + )); + + assert_eq!(actual.out.match_indices("head1").count(), 2); + assert_eq!(actual.out.match_indices("head2").count(), 2); + assert_eq!(actual.out.match_indices("head3").count(), 2); + assert_eq!(actual.out.match_indices("y1").count(), 1); + assert_eq!(actual.out.match_indices("y2").count(), 1); + assert_eq!(actual.out.match_indices("y3").count(), 1); + assert_eq!(actual.out.match_indices("x1").count(), 1); + assert_eq!(actual.out.match_indices("x2").count(), 1); + assert_eq!(actual.out.match_indices("x3").count(), 1); +} diff --git a/crates/nu-protocol/src/config/table.rs b/crates/nu-protocol/src/config/table.rs index 7dbe1dd9d2..e88f4ea88f 100644 --- a/crates/nu-protocol/src/config/table.rs +++ b/crates/nu-protocol/src/config/table.rs @@ -333,6 +333,7 @@ pub struct TableConfig { pub trim: TrimStrategy, pub header_on_separator: bool, pub abbreviated_row_count: Option, + pub footer_inheritance: bool, } impl IntoValue for TableConfig { @@ -350,6 +351,7 @@ impl IntoValue for TableConfig { "trim" => self.trim.into_value(span), "header_on_separator" => self.header_on_separator.into_value(span), "abbreviated_row_count" => abbv_count, + "footer_inheritance" => self.footer_inheritance.into_value(span), } .into_value(span) } @@ -365,6 +367,7 @@ impl Default for TableConfig { header_on_separator: false, padding: TableIndent::default(), abbreviated_row_count: None, + footer_inheritance: false, } } } @@ -401,6 +404,7 @@ impl UpdateFromValue for TableConfig { } _ => errors.type_mismatch(path, Type::custom("int or nothing"), val), }, + "footer_inheritance" => self.footer_inheritance.update(val, path, errors), _ => errors.unknown_option(path, val), } } diff --git a/crates/nu-table/src/common.rs b/crates/nu-table/src/common.rs index 3a793a690e..d18e053a1c 100644 --- a/crates/nu-table/src/common.rs +++ b/crates/nu-table/src/common.rs @@ -18,9 +18,12 @@ pub fn create_nu_table_config( expand: bool, mode: TableMode, ) -> NuTableConfig { + let with_footer = (config.table.footer_inheritance && out.with_footer) + || with_footer(config, out.with_header, out.table.count_rows()); + NuTableConfig { theme: load_theme(mode), - with_footer: with_footer(config, out.with_header, out.table.count_rows()), + with_footer, with_index: out.with_index, with_header: out.with_header, split_color: Some(lookup_separator_color(comp)), diff --git a/crates/nu-table/src/types/expanded.rs b/crates/nu-table/src/types/expanded.rs index 81215a89c3..a0b6cd0ff2 100644 --- a/crates/nu-table/src/types/expanded.rs +++ b/crates/nu-table/src/types/expanded.rs @@ -5,7 +5,7 @@ use crate::{ NuText, StringResult, TableResult, INDEX_COLUMN_NAME, }, string_width, - types::has_index, + types::{has_footer, has_index}, NuTable, NuTableCell, TableOpts, TableOutput, }; use nu_color_config::{Alignment, StyleComputer, TextStyle}; @@ -31,11 +31,12 @@ impl ExpandedTable { } pub fn build_value(self, item: &Value, opts: TableOpts<'_>) -> NuText { - expanded_table_entry2(item, Cfg { opts, format: self }) + let cell = expanded_table_entry2(item, Cfg { opts, format: self }); + (cell.text, cell.style) } pub fn build_map(self, record: &Record, opts: TableOpts<'_>) -> StringResult { - expanded_table_kv(record, Cfg { opts, format: self }) + expanded_table_kv(record, Cfg { opts, format: self }).map(|cell| cell.map(|cell| cell.text)) } pub fn build_list(self, vals: &[Value], opts: TableOpts<'_>) -> StringResult { @@ -58,6 +59,39 @@ struct Cfg<'a> { format: ExpandedTable, } +#[derive(Debug, Clone)] +struct CellOutput { + text: String, + style: TextStyle, + is_big: bool, + is_expanded: bool, +} + +impl CellOutput { + fn new(text: String, style: TextStyle, is_big: bool, is_expanded: bool) -> Self { + Self { + text, + style, + is_big, + is_expanded, + } + } + + fn clean(text: String, is_big: bool, is_expanded: bool) -> Self { + Self::new(text, Default::default(), is_big, is_expanded) + } + + fn text(text: String) -> Self { + Self::styled((text, Default::default())) + } + + fn styled(text: NuText) -> Self { + Self::new(text.0, text.1, false, false) + } +} + +type CellResult = Result, ShellError>; + fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { const PADDING_SPACE: usize = 2; const SPLIT_LINE_SPACE: usize = 1; @@ -83,6 +117,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { let with_index = has_index(&cfg.opts, &headers); let row_offset = cfg.opts.index_offset; + let mut is_footer_used = false; // The header with the INDEX is removed from the table headers since // it is added to the natural table index @@ -148,21 +183,25 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } let inner_cfg = update_config(cfg.clone(), available_width); - let (mut text, style) = expanded_table_entry2(item, inner_cfg); + let mut cell = expanded_table_entry2(item, inner_cfg); - let value_width = string_width(&text); + let value_width = string_width(&cell.text); if value_width > available_width { // it must only happen when a string is produced, so we can safely wrap it. // (it might be string table representation as well) (I guess I mean default { table ...} { list ...}) // // todo: Maybe convert_to_table2_entry could do for strings to not mess caller code? - text = wrap_text(&text, available_width, cfg.opts.config); + cell.text = wrap_text(&cell.text, available_width, cfg.opts.config); } - let value = NuTableCell::new(text); + let value = NuTableCell::new(cell.text); data[row].push(value); - data_styles.insert((row, with_index as usize), style); + data_styles.insert((row, with_index as usize), cell.style); + + if cell.is_big { + is_footer_used = cell.is_big; + } } let mut table = NuTable::from(data); @@ -170,7 +209,12 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { table.set_index_style(get_index_style(cfg.opts.style_computer)); set_data_styles(&mut table, data_styles); - return Ok(Some(TableOutput::new(table, false, with_index))); + return Ok(Some(TableOutput::new( + table, + false, + with_index, + is_footer_used, + ))); } if !headers.is_empty() { @@ -233,22 +277,26 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } let inner_cfg = update_config(cfg.clone(), available); - let (mut text, style) = expanded_table_entry(item, header.as_str(), inner_cfg); + let mut cell = expanded_table_entry(item, header.as_str(), inner_cfg); - let mut value_width = string_width(&text); + let mut value_width = string_width(&cell.text); if value_width > available { // it must only happen when a string is produced, so we can safely wrap it. // (it might be string table representation as well) - text = wrap_text(&text, available, cfg.opts.config); + cell.text = wrap_text(&cell.text, available, cfg.opts.config); value_width = available; } column_width = max(column_width, value_width); - let value = NuTableCell::new(text); + let value = NuTableCell::new(cell.text); data[row + 1].push(value); - data_styles.insert((row + 1, col + with_index as usize), style); + data_styles.insert((row + 1, col + with_index as usize), cell.style); + + if cell.is_big { + is_footer_used = cell.is_big; + } } let head_cell = NuTableCell::new(header); @@ -326,10 +374,12 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1); set_data_styles(&mut table, data_styles); - Ok(Some(TableOutput::new(table, true, with_index))) + let has_footer = is_footer_used || has_footer(&cfg.opts, table.count_rows() as u64); + + Ok(Some(TableOutput::new(table, true, with_index, has_footer))) } -fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> StringResult { +fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> CellResult { let theme = load_theme(cfg.opts.mode); let key_width = record .columns() @@ -345,11 +395,13 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> StringResult { let value_width = cfg.opts.width - key_width - count_borders - padding - padding; + let mut with_footer = false; + let mut data = Vec::with_capacity(record.len()); for (key, value) in record { cfg.opts.signals.check(cfg.opts.span)?; - let (value, is_expanded) = match expand_table_value(value, value_width, &cfg)? { + let cell = match expand_table_value(value, value_width, &cfg)? { Some(val) => val, None => return Ok(None), }; @@ -358,35 +410,40 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> StringResult { // we could use Padding for it but, // the easiest way to do so is just push a new_line char before let mut key = key.to_owned(); - if !key.is_empty() && is_expanded && theme.has_top() { + if !key.is_empty() && cell.is_expanded && theme.has_top() { key.insert(0, '\n'); } let key = NuTableCell::new(key); - let val = NuTableCell::new(value); + let val = NuTableCell::new(cell.text); let row = vec![key, val]; data.push(row); + + if cell.is_big { + with_footer = cell.is_big; + } } let mut table = NuTable::from(data); table.set_index_style(get_key_style(&cfg)); table.set_indent(cfg.opts.indent.0, cfg.opts.indent.1); - let out = TableOutput::new(table, false, true); + let out = TableOutput::new(table, false, true, with_footer); maybe_expand_table(out, cfg.opts.width, &cfg.opts) + .map(|value| value.map(|value| CellOutput::clean(value, with_footer, false))) } // the flag is used as an optimization to not do `value.lines().count()` search. -fn expand_table_value( - value: &Value, - value_width: usize, - cfg: &Cfg<'_>, -) -> Result, ShellError> { +fn expand_table_value(value: &Value, value_width: usize, cfg: &Cfg<'_>) -> CellResult { let is_limited = matches!(cfg.format.expand_limit, Some(0)); if is_limited { - return Ok(Some((value_to_string_clean(value, cfg), false))); + return Ok(Some(CellOutput::clean( + value_to_string_clean(value, cfg), + false, + false, + ))); } let span = value.span(); @@ -400,42 +457,46 @@ fn expand_table_value( let cfg = create_table_cfg(cfg, &out); let value = out.table.draw(cfg, value_width); match value { - Some(result) => Ok(Some((result, true))), + Some(value) => Ok(Some(CellOutput::clean(value, out.with_footer, true))), None => Ok(None), } } None => { // it means that the list is empty - Ok(Some(( - value_to_wrapped_string(value, cfg, value_width), - false, - ))) + Ok(Some(CellOutput::text(value_to_wrapped_string( + value, + cfg, + value_width, + )))) } } } Value::Record { val: record, .. } => { if record.is_empty() { // Like list case return styled string instead of empty value - return Ok(Some(( - value_to_wrapped_string(value, cfg, value_width), - false, - ))); + return Ok(Some(CellOutput::text(value_to_wrapped_string( + value, + cfg, + value_width, + )))); } let inner_cfg = update_config(dive_options(cfg, span), value_width); let result = expanded_table_kv(record, inner_cfg)?; match result { - Some(result) => Ok(Some((result, true))), - None => Ok(Some(( - value_to_wrapped_string(value, cfg, value_width), - false, - ))), + Some(result) => Ok(Some(CellOutput::clean(result.text, result.is_big, true))), + None => Ok(Some(CellOutput::text(value_to_wrapped_string( + value, + cfg, + value_width, + )))), } } - _ => { - let text = value_to_wrapped_string_clean(value, cfg, value_width); - Ok(Some((text, false))) - } + _ => Ok(Some(CellOutput::text(value_to_wrapped_string_clean( + value, + cfg, + value_width, + )))), } } @@ -443,27 +504,35 @@ fn get_key_style(cfg: &Cfg<'_>) -> TextStyle { get_header_style(cfg.opts.style_computer).alignment(Alignment::Left) } -fn expanded_table_entry(item: &Value, header: &str, cfg: Cfg<'_>) -> NuText { +fn expanded_table_entry(item: &Value, header: &str, cfg: Cfg<'_>) -> CellOutput { match item { Value::Record { val, .. } => match val.get(header) { Some(val) => expanded_table_entry2(val, cfg), - None => error_sign(cfg.opts.style_computer), + None => CellOutput::styled(error_sign(cfg.opts.style_computer)), }, _ => expanded_table_entry2(item, cfg), } } -fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> NuText { +fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> CellOutput { let is_limit_reached = matches!(cfg.format.expand_limit, Some(0)); if is_limit_reached { - return nu_value_to_string_clean(item, cfg.opts.config, cfg.opts.style_computer); + return CellOutput::styled(nu_value_to_string_clean( + item, + cfg.opts.config, + cfg.opts.style_computer, + )); } let span = item.span(); match &item { Value::Record { val: record, .. } => { if record.is_empty() { - return nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer); + return CellOutput::styled(nu_value_to_string( + item, + cfg.opts.config, + cfg.opts.style_computer, + )); } // we verify what is the structure of a Record cause it might represent @@ -471,18 +540,22 @@ fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> NuText { let table = expanded_table_kv(record, inner_cfg); match table { - Ok(Some(table)) => (table, TextStyle::default()), - _ => nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer), + Ok(Some(table)) => table, + _ => CellOutput::styled(nu_value_to_string( + item, + cfg.opts.config, + cfg.opts.style_computer, + )), } } Value::List { vals, .. } => { if cfg.format.flatten && is_simple_list(vals) { - return value_list_to_string( + return CellOutput::styled(value_list_to_string( vals, cfg.opts.config, cfg.opts.style_computer, &cfg.format.flatten_sep, - ); + )); } let inner_cfg = dive_options(&cfg, span); @@ -490,17 +563,31 @@ fn expanded_table_entry2(item: &Value, cfg: Cfg<'_>) -> NuText { let out = match table { Ok(Some(out)) => out, - _ => return nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer), + _ => { + return CellOutput::styled(nu_value_to_string( + item, + cfg.opts.config, + cfg.opts.style_computer, + )) + } }; let table_config = create_table_cfg(&cfg, &out); let table = out.table.draw(table_config, usize::MAX); match table { - Some(table) => (table, TextStyle::default()), - None => nu_value_to_string(item, cfg.opts.config, cfg.opts.style_computer), + Some(table) => CellOutput::clean(table, out.with_footer, false), + None => CellOutput::styled(nu_value_to_string( + item, + cfg.opts.config, + cfg.opts.style_computer, + )), } } - _ => nu_value_to_string_clean(item, cfg.opts.config, cfg.opts.style_computer), + _ => CellOutput::styled(nu_value_to_string_clean( + item, + cfg.opts.config, + cfg.opts.style_computer, + )), } } diff --git a/crates/nu-table/src/types/general.rs b/crates/nu-table/src/types/general.rs index f7bd24f2e2..ba0a1ceefa 100644 --- a/crates/nu-table/src/types/general.rs +++ b/crates/nu-table/src/types/general.rs @@ -57,7 +57,7 @@ fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult { let mut table = NuTable::from(data); table.set_index_style(TextStyle::default_field()); - let mut out = TableOutput::new(table, false, true); + let mut out = TableOutput::new(table, false, true, false); let left = opts.config.table.padding.left; let right = opts.config.table.padding.right; @@ -82,7 +82,7 @@ fn table(input: &[Value], opts: &TableOpts<'_>) -> TableResult { let with_header = !headers.is_empty(); if !with_header { let table = to_table_with_no_header(input, with_index, row_offset, opts)?; - let table = table.map(|table| TableOutput::new(table, false, with_index)); + let table = table.map(|table| TableOutput::new(table, false, with_index, false)); return Ok(table); } @@ -98,7 +98,7 @@ fn table(input: &[Value], opts: &TableOpts<'_>) -> TableResult { .collect(); let table = to_table_with_header(input, &headers, with_index, row_offset, opts)?; - let table = table.map(|table| TableOutput::new(table, true, with_index)); + let table = table.map(|table| TableOutput::new(table, true, with_index, false)); Ok(table) } diff --git a/crates/nu-table/src/types/mod.rs b/crates/nu-table/src/types/mod.rs index 571dda26ab..adbe56bf27 100644 --- a/crates/nu-table/src/types/mod.rs +++ b/crates/nu-table/src/types/mod.rs @@ -1,3 +1,9 @@ +use terminal_size::{terminal_size, Height, Width}; + +use crate::{common::INDEX_COLUMN_NAME, NuTable}; +use nu_color_config::StyleComputer; +use nu_protocol::{Config, FooterMode, Signals, Span, TableIndexMode, TableMode}; + mod collapse; mod expanded; mod general; @@ -6,22 +12,20 @@ pub use collapse::CollapsedTable; pub use expanded::ExpandedTable; pub use general::JustTable; -use crate::{common::INDEX_COLUMN_NAME, NuTable}; -use nu_color_config::StyleComputer; -use nu_protocol::{Config, Signals, Span, TableIndexMode, TableMode}; - pub struct TableOutput { pub table: NuTable, pub with_header: bool, pub with_index: bool, + pub with_footer: bool, } impl TableOutput { - pub fn new(table: NuTable, with_header: bool, with_index: bool) -> Self { + pub fn new(table: NuTable, with_header: bool, with_index: bool, with_footer: bool) -> Self { Self { table, with_header, with_index, + with_footer, } } } @@ -75,3 +79,23 @@ fn has_index(opts: &TableOpts<'_>, headers: &[String]) -> bool { with_index && !opts.index_remove } + +fn has_footer(opts: &TableOpts<'_>, count_records: u64) -> bool { + match opts.config.footer_mode { + // Only show the footer if there are more than RowCount rows + FooterMode::RowCount(limit) => count_records > limit, + // Always show the footer + FooterMode::Always => true, + // Never show the footer + FooterMode::Never => false, + // Calculate the screen height and row count, if screen height is larger than row count, don't show footer + FooterMode::Auto => { + let (_width, height) = match terminal_size() { + Some((w, h)) => (Width(w.0).0 as u64, Height(h.0).0 as u64), + None => (Width(0).0 as u64, Height(0).0 as u64), + }; + + height <= count_records + } + } +} diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 44afdb8644..5271932221 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -169,6 +169,7 @@ $env.config = { truncating_suffix: "..." # A suffix used by the 'truncating' methodology } header_on_separator: false # show header text on separator/border line + footer_inheritance: false # render footer in parent table if child is big enough (extended table option) # abbreviated_row_count: 10 # limit data rows from top and bottom after reaching a set point }