nu-table: Add table option --abbreviated (#10399)

- 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>
This commit is contained in:
Maxim Zhiburt 2023-09-20 17:59:08 +00:00 committed by GitHub
parent 4ae53d93fb
commit 7cfd4d2cfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 444 additions and 252 deletions

View File

@ -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", "expand the table structure in collapse mode.\nBe aware collapse mode currently doesn't support width control",
Some('c'), 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) .category(Category::Viewers)
} }
@ -104,33 +110,13 @@ impl Command for Table {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let start_num: Option<i64> = call.get_flag(engine_state, stack, "start-number")?; let list_themes: bool = call.has_flag("list");
let row_offset = start_num.unwrap_or_default() as usize; let cfg = parse_table_config(call, engine_state, stack)?;
let list: bool = call.has_flag("list"); let input = CmdInput::new(engine_state, stack, call, input);
let width_param: Option<i64> = call.get_flag(engine_state, stack, "width")?;
let expand: bool = call.has_flag("expand");
let expand_limit: Option<usize> = 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<String> =
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,
},
};
// if list argument is present we just need to return a list of supported table themes // if list argument is present we just need to return a list of supported table themes
if list { if list_themes {
let val = Value::list(supported_table_modes(), Span::test_data()); let val = Value::list(supported_table_modes(), Span::test_data());
return Ok(val.into_pipeline_data()); return Ok(val.into_pipeline_data());
} }
@ -140,15 +126,7 @@ impl Command for Table {
let _ = nu_utils::enable_vt_processing(); let _ = nu_utils::enable_vt_processing();
} }
handle_table_command( handle_table_command(input, cfg)
engine_state,
stack,
call,
input,
row_offset,
table_view,
width_param,
)
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
@ -214,86 +192,138 @@ impl Command for Table {
} }
} }
fn handle_table_command( #[derive(Debug, Clone)]
engine_state: &EngineState, struct TableConfig {
stack: &mut Stack,
call: &Call,
input: PipelineData,
row_offset: usize, row_offset: usize,
table_view: TableView, table_view: TableView,
term_width: Option<i64>, term_width: usize,
) -> Result<PipelineData, ShellError> { abbreviation: Option<usize>,
let ctrlc = engine_state.ctrlc.clone(); }
let config = get_config(engine_state, stack);
let span = input.span().unwrap_or(call.head); impl TableConfig {
match input { fn new(
PipelineData::ExternalStream { .. } => Ok(input), row_offset: usize,
PipelineData::Value(Value::Binary { val, .. }, ..) => Ok(PipelineData::ExternalStream { table_view: TableView,
stdout: Some(RawStream::new( term_width: usize,
Box::new(if call.redirect_stdout { abbreviation: Option<usize>,
vec![Ok(val)].into_iter() ) -> Self {
Self {
row_offset,
table_view,
term_width,
abbreviation,
}
}
}
fn parse_table_config(
call: &Call,
state: &EngineState,
stack: &mut Stack,
) -> Result<TableConfig, ShellError> {
let start_num: Option<i64> = call.get_flag(state, stack, "start-number")?;
let row_offset = start_num.unwrap_or_default() as usize;
let width_param: Option<i64> = call.get_flag(state, stack, "width")?;
let expand: bool = call.has_flag("expand");
let expand_limit: Option<usize> = 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<String> = call.get_flag(state, stack, "flatten-separator")?;
let abbrivation: Option<usize> = 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<PipelineData, ShellError> {
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 { } else {
vec![Ok(format!("{}\n", nu_pretty_hex::pretty_hex(&val)) let hex = format!("{}\n", nu_pretty_hex::pretty_hex(&val))
.as_bytes() .as_bytes()
.to_vec())] .to_vec();
.into_iter() vec![Ok(hex)]
}), };
let ctrlc = input.engine_state.ctrlc.clone();
let stream = RawStream::new(
Box::new(stream_list.into_iter()),
ctrlc, ctrlc,
call.head, input.call.head,
None, None,
)), );
Ok(PipelineData::ExternalStream {
stdout: Some(stream),
stderr: None, stderr: None,
exit_code: None, exit_code: None,
span: call.head, span: input.call.head,
metadata: None, metadata: None,
trim_end_newline: false, 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. // 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( PipelineData::Value(Value::List { vals, .. }, metadata) => {
engine_state, let ctrlc = input.engine_state.ctrlc.clone();
stack, let stream = ListStream::from_stream(vals.into_iter(), ctrlc);
ListStream::from_stream(vals.into_iter(), ctrlc.clone()), input.data = PipelineData::Empty;
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( handle_row_stream(input, cfg, stream, metadata)
val, }
span, PipelineData::ListStream(stream, metadata) => {
engine_state, input.data = PipelineData::Empty;
stack, handle_row_stream(input, cfg, stream, metadata)
call, }
table_view, PipelineData::Value(Value::Record { val, .. }, ..) => {
term_width, input.data = PipelineData::Empty;
ctrlc, handle_record(input, cfg, val)
&config,
)
} }
PipelineData::Value(Value::LazyRecord { val, .. }, ..) => { PipelineData::Value(Value::LazyRecord { val, .. }, ..) => {
let collected = val.collect()?.into_pipeline_data(); input.data = val.collect()?.into_pipeline_data();
handle_table_command( handle_table_command(input, cfg)
engine_state,
stack,
call,
collected,
row_offset,
table_view,
term_width,
)
} }
PipelineData::Value(Value::Error { error, .. }, ..) => { PipelineData::Value(Value::Error { error, .. }, ..) => {
// Propagate this error outward, so that it goes to stderr // Propagate this error outward, so that it goes to stderr
@ -302,72 +332,54 @@ fn handle_table_command(
} }
PipelineData::Value(Value::CustomValue { val, .. }, ..) => { PipelineData::Value(Value::CustomValue { val, .. }, ..) => {
let base_pipeline = val.to_base_value(span)?.into_pipeline_data(); 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), x => Ok(x),
} }
} }
fn supported_table_modes() -> Vec<Value> {
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( fn handle_record(
record: Record, input: CmdInput,
span: Span, cfg: TableConfig,
engine_state: &EngineState, mut record: Record,
stack: &mut Stack,
call: &Call,
table_view: TableView,
term_width: usize,
ctrlc: Option<Arc<AtomicBool>>,
config: &Config,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
// Create a StyleComputer to compute styles for each value in the table. let config = get_config(input.engine_state, input.stack);
let style_computer = &StyleComputer::from_config(engine_state, 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 ctrlc1 = ctrlc.clone();
let result = if record.is_empty() { if record.is_empty() {
create_empty_placeholder("record", term_width, engine_state, stack) let value =
} else { create_empty_placeholder("record", cfg.term_width, input.engine_state, input.stack);
let indent = (config.table_indent.left, config.table_indent.right); let value = Value::string(value, span);
let opts = TableOpts::new(config, style_computer, ctrlc, span, 0, term_width, indent); return Ok(value.into_pipeline_data());
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),
}
}; };
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()) Ok(val.into_pipeline_data())
} }
@ -429,27 +441,31 @@ fn build_table_batch(
} }
fn handle_row_stream( fn handle_row_stream(
engine_state: &EngineState, input: CmdInput<'_>,
stack: &mut Stack, cfg: TableConfig,
stream: ListStream, stream: ListStream,
call: &Call,
row_offset: usize,
ctrlc: Option<Arc<AtomicBool>>,
metadata: Option<Box<PipelineMetadata>>, metadata: Option<Box<PipelineMetadata>>,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let ctrlc = input.engine_state.ctrlc.clone();
let stream = match metadata.as_deref() { let stream = match metadata.as_deref() {
// First, `ls` sources: // First, `ls` sources:
Some(PipelineMetadata { Some(PipelineMetadata {
data_source: DataSource::Ls, 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 ctrlc = ctrlc.clone();
let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") { let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") {
Some(v) => Some(env_to_string("LS_COLORS", &v, engine_state, stack)?), Some(v) => Some(env_to_string(
"LS_COLORS",
&v,
input.engine_state,
input.stack,
)?),
None => None, None => None,
}; };
let ls_colors = get_ls_colors(ls_colors_env_str); let ls_colors = get_ls_colors(ls_colors_env_str);
let span = input.call.head;
ListStream::from_stream( ListStream::from_stream(
stream.map(move |mut x| match &mut x { stream.map(move |mut x| match &mut x {
@ -459,7 +475,7 @@ fn handle_row_stream(
while idx < record.len() { while idx < record.len() {
// Only the name column gets special colors, for now // Only the name column gets special colors, for now
if record.cols[idx] == "name" { 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) { if let Some(Value::String { val, .. }) = record.vals.get(idx) {
let val = render_path_name(val, &config, &ls_colors, span); let val = render_path_name(val, &config, &ls_colors, span);
if let Some(val) = val { if let Some(val) = val {
@ -483,6 +499,7 @@ fn handle_row_stream(
data_source: DataSource::HtmlThemes, data_source: DataSource::HtmlThemes,
}) => { }) => {
let ctrlc = ctrlc.clone(); let ctrlc = ctrlc.clone();
let span = input.call.head;
ListStream::from_stream( ListStream::from_stream(
stream.map(move |mut x| match &mut x { 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, // Simple routine to grab the hex code, convert to a style,
// then place it in a new Value::String. // 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) { if let Some(Value::String { val, .. }) = record.vals.get(idx) {
let s = match color_from_hex(val) { let s = match color_from_hex(val) {
Ok(c) => match c { Ok(c) => match c {
@ -523,48 +540,23 @@ fn handle_row_stream(
_ => stream, _ => stream,
}; };
let head = call.head; let paginator = PagingTableCreator::new(
let width_param: Option<i64> = call.get_flag(engine_state, stack, "width")?; input.call.head,
let collapse: bool = call.has_flag("collapse");
let expand: bool = call.has_flag("expand");
let limit: Option<usize> = call.get_flag(engine_state, stack, "expand-deep")?;
let flatten: bool = call.has_flag("flatten");
let flatten_separator: Option<String> =
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,
};
Ok(PipelineData::ExternalStream {
stdout: Some(RawStream::new(
Box::new(PagingTableCreator::new(
head,
stream, stream,
// These are passed in as a way to have PagingTable create StyleComputers // 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. // for the values it outputs. Because engine_state is passed in, config doesn't need to.
engine_state.clone(), input.engine_state.clone(),
stack.clone(), input.stack.clone(),
ctrlc.clone(), ctrlc.clone(),
row_offset, cfg,
width_param, );
table_view, let stream = RawStream::new(Box::new(paginator), ctrlc, input.call.head, None);
)),
ctrlc, Ok(PipelineData::ExternalStream {
head, stdout: Some(stream),
None,
)),
stderr: None, stderr: None,
exit_code: None, exit_code: None,
span: head, span: input.call.head,
metadata: None, metadata: None,
trim_end_newline: false, trim_end_newline: false,
}) })
@ -600,24 +592,19 @@ struct PagingTableCreator {
engine_state: EngineState, engine_state: EngineState,
stack: Stack, stack: Stack,
ctrlc: Option<Arc<AtomicBool>>, ctrlc: Option<Arc<AtomicBool>>,
row_offset: usize,
width_param: Option<i64>,
view: TableView,
elements_displayed: usize, elements_displayed: usize,
reached_end: bool, reached_end: bool,
cfg: TableConfig,
} }
impl PagingTableCreator { impl PagingTableCreator {
#[allow(clippy::too_many_arguments)]
fn new( fn new(
head: Span, head: Span,
stream: ListStream, stream: ListStream,
engine_state: EngineState, engine_state: EngineState,
stack: Stack, stack: Stack,
ctrlc: Option<Arc<AtomicBool>>, ctrlc: Option<Arc<AtomicBool>>,
row_offset: usize, cfg: TableConfig,
width_param: Option<i64>,
view: TableView,
) -> Self { ) -> Self {
PagingTableCreator { PagingTableCreator {
head, head,
@ -625,9 +612,7 @@ impl PagingTableCreator {
engine_state, engine_state,
stack, stack,
ctrlc, ctrlc,
row_offset, cfg,
width_param,
view,
elements_displayed: 0, elements_displayed: 0,
reached_end: false, reached_end: false,
} }
@ -686,11 +671,23 @@ impl PagingTableCreator {
style_comp, style_comp,
self.ctrlc.clone(), self.ctrlc.clone(),
self.head, self.head,
self.row_offset, self.cfg.row_offset,
get_width_param(self.width_param), self.cfg.term_width,
(cfg.table_indent.left, cfg.table_indent.right), (cfg.table_indent.left, cfg.table_indent.right),
) )
} }
fn build_table(&mut self, batch: Vec<Value>) -> Result<Option<String>, ShellError> {
match &self.cfg.table_view {
TableView::General => self.build_general(batch),
TableView::Collapsed => self.build_collapsed(batch),
TableView::Expanded {
limit,
flatten,
flatten_separator,
} => self.build_extended(batch, *limit, *flatten, flatten_separator.clone()),
}
}
} }
impl Iterator for PagingTableCreator { impl Iterator for PagingTableCreator {
@ -736,49 +733,52 @@ impl Iterator for PagingTableCreator {
// Increase elements_displayed by one so on next iteration next branch of this // Increase elements_displayed by one so on next iteration next branch of this
// if else triggers and terminates stream // if else triggers and terminates stream
self.elements_displayed = 1; self.elements_displayed = 1;
let term_width = get_width_param(self.width_param); let result = create_empty_placeholder(
let result = "list",
create_empty_placeholder("list", term_width, &self.engine_state, &self.stack); self.cfg.term_width,
&self.engine_state,
&self.stack,
);
Some(Ok(result.into_bytes())) Some(Ok(result.into_bytes()))
} else { } else {
None None
}; };
} }
let table = match &self.view { if let Some(limit) = self.cfg.abbreviation {
TableView::General => self.build_general(batch), // todo: could be optimized cause we already consumed the list there's no point in goint back to pagination;
TableView::Collapsed => self.build_collapsed(batch),
TableView::Expanded { if batch.len() > limit * 2 + 1 {
batch = abbreviate_list(
&batch,
limit, limit,
flatten, Value::string(String::from("..."), Span::unknown()),
flatten_separator, );
} => self.build_extended(batch, *limit, *flatten, flatten_separator.clone()),
};
self.row_offset += idx; let is_record_list = batch[..limit]
.iter()
.all(|value| matches!(value, Value::Record { .. }))
&& batch[limit + 1..]
.iter()
.all(|value| matches!(value, Value::Record { .. }));
match table { if limit > 0 && is_record_list {
Ok(Some(table)) => { // in case it's a record list we set a default text to each column instead of a single value.
let table = maybe_strip_color(table, &get_config(&self.engine_state, &self.stack));
let mut bytes = table.as_bytes().to_vec(); let cols = batch[0].as_record().expect("ok").cols.clone();
bytes.push(b'\n'); // nu-table tables don't come with a newline on the end let vals =
vec![Value::string(String::from("..."), Span::unknown()); cols.len()];
Some(Ok(bytes)) 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)) Some(Value::string(val, span))
} }
#[derive(Debug)] #[derive(Debug, Clone)]
enum TableView { enum TableView {
General, General,
Collapsed, Collapsed,
@ -869,3 +869,70 @@ fn create_empty_placeholder(
.draw(config, termwidth) .draw(config, termwidth)
.expect("Could not create empty table placeholder") .expect("Could not create empty table placeholder")
} }
fn convert_table_to_output(
table: Result<Option<String>, ShellError>,
config: &Config,
ctrlc: &Option<Arc<AtomicBool>>,
term_width: usize,
) -> Option<Result<Vec<u8>, 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<T>(list: &[T], limit: usize, text: T) -> Vec<T>
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<Value> {
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"),
]
}

View File

@ -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 │ │ ││ │ │ │ │ │ ╰───┴───────╯ │ ││ │ │ │ ╰───┴───────────────╯ │╰───┴───────┴───────┴───────────────────────╯" "╭───┬───────┬───────┬───────────────────────╮│ # │ 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 │╰───┴─────┴─────┴────────────────╯");
}

View File

@ -76,6 +76,7 @@ pub struct Config {
pub table_move_header: bool, pub table_move_header: bool,
pub table_show_empty: bool, pub table_show_empty: bool,
pub table_indent: TableIndent, pub table_indent: TableIndent,
pub table_abbreviation_threshold: Option<usize>,
pub use_ls_colors: bool, pub use_ls_colors: bool,
pub color_config: HashMap<String, Value>, pub color_config: HashMap<String, Value>,
pub use_grid_icons: bool, pub use_grid_icons: bool,
@ -134,6 +135,7 @@ impl Default for Config {
trim_strategy: TRIM_STRATEGY_DEFAULT, trim_strategy: TRIM_STRATEGY_DEFAULT,
table_move_header: false, table_move_header: false,
table_indent: TableIndent { left: 1, right: 1 }, table_indent: TableIndent { left: 1, right: 1 },
table_abbreviation_threshold: None,
datetime_normal_format: None, datetime_normal_format: None,
datetime_table_format: None, datetime_table_format: None,
@ -1023,6 +1025,17 @@ impl Value {
"show_empty" => { "show_empty" => {
try_bool!(cols, vals, index, span, table_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 => { x => {
invalid_key!( invalid_key!(
cols, cols,

View File

@ -165,6 +165,7 @@ $env.config = {
truncating_suffix: "..." # A suffix used by the 'truncating' methodology truncating_suffix: "..." # A suffix used by the 'truncating' methodology
} }
header_on_separator: false # show header text on separator/border line 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 error_style: "fancy" # "fancy" or "plain" for screen reader-friendly error messages