Introduce footer_inheritance option (#14070)

```nu
$env.config.table.footer_inheritance = true
```

close #14060
This commit is contained in:
Maxim Zhiburt 2024-10-23 20:45:47 +03:00 committed by GitHub
parent 619211c1bf
commit 3ec1c40320
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 222 additions and 67 deletions

View File

@ -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());

View File

@ -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::<Vec<_>>()
.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);
}

View File

@ -333,6 +333,7 @@ pub struct TableConfig {
pub trim: TrimStrategy,
pub header_on_separator: bool,
pub abbreviated_row_count: Option<usize>,
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),
}
}

View File

@ -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)),

View File

@ -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<Option<CellOutput>, 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<Option<(String, bool)>, 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,
)),
}
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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
}