From 8d8b01170230bf8e8c19837be389c087234e0b96 Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Wed, 26 Apr 2023 21:56:10 +0300 Subject: [PATCH] Bump `tabled` dependency to 0.11 (#8922) close? #8060 Quite a bit of refactoring took place. I believe a few improvements to collapse/expand were made. I've tried to track any performance regressions and seems like it is fine. I've noticed something different now with default configuration path or something in this regard? So I might missed something while testing because of this. Requires some oversight. --------- Signed-off-by: Maxim Zhiburt --- Cargo.lock | 41 +- crates/nu-color-config/Cargo.toml | 2 - crates/nu-color-config/src/style_computer.rs | 44 +- crates/nu-color-config/src/text_style.rs | 28 +- crates/nu-command/Cargo.toml | 3 +- crates/nu-command/src/debug/inspect_table.rs | 514 +++---- crates/nu-command/src/viewers/table.rs | 1311 ++---------------- crates/nu-command/tests/commands/table.rs | 543 +++++++- crates/nu-engine/src/column.rs | 3 +- crates/nu-explore/src/nu_common/table.rs | 923 +----------- crates/nu-table/Cargo.toml | 4 +- crates/nu-table/examples/table_demo.rs | 46 +- crates/nu-table/src/lib.rs | 11 +- crates/nu-table/src/nu_protocol_table.rs | 227 --- crates/nu-table/src/table.rs | 701 ++++++---- crates/nu-table/src/table_theme.rs | 221 ++- crates/nu-table/src/types/collapse.rs | 80 ++ crates/nu-table/src/types/expanded.rs | 601 ++++++++ crates/nu-table/src/types/general.rs | 254 ++++ crates/nu-table/src/types/mod.rs | 207 +++ crates/nu-table/src/unstructured_table.rs | 339 +++++ crates/nu-table/src/util.rs | 17 +- crates/nu-table/tests/common.rs | 36 +- crates/nu-table/tests/constrains.rs | 67 +- crates/nu-table/tests/expand.rs | 5 +- crates/nu-table/tests/style.rs | 43 +- 26 files changed, 3041 insertions(+), 3230 deletions(-) delete mode 100644 crates/nu-table/src/nu_protocol_table.rs create mode 100644 crates/nu-table/src/types/collapse.rs create mode 100644 crates/nu-table/src/types/expanded.rs create mode 100644 crates/nu-table/src/types/general.rs create mode 100644 crates/nu-table/src/types/mod.rs create mode 100644 crates/nu-table/src/unstructured_table.rs diff --git a/Cargo.lock b/Cargo.lock index 2b32a4608a..4bfb9f2db1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,15 +87,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" -[[package]] -name = "ansi-str" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84252a7e1a0df81706ce70bbad85ed1e4916448a4093ccd52dd98c6a44a477cd" -dependencies = [ - "ansitok", -] - [[package]] name = "ansi-str" version = "0.7.2" @@ -2087,16 +2078,6 @@ dependencies = [ "indexmap", ] -[[package]] -name = "json_to_table" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0be33515faeb3773f550c80fd7a889148164e58f7e3cf36467718c8ce71ee55" -dependencies = [ - "serde_json", - "tabled", -] - [[package]] name = "kernel32-sys" version = "0.2.2" @@ -2823,7 +2804,6 @@ dependencies = [ "nu-test-support", "nu-utils", "serde", - "tabled", ] [[package]] @@ -2947,7 +2927,7 @@ dependencies = [ name = "nu-explore" version = "0.79.1" dependencies = [ - "ansi-str 0.7.2", + "ansi-str", "crossterm 0.26.1", "lscolors", "nu-ansi-term", @@ -3082,13 +3062,11 @@ dependencies = [ name = "nu-table" version = "0.79.1" dependencies = [ - "json_to_table", "nu-ansi-term", "nu-color-config", "nu-engine", "nu-protocol", "nu-utils", - "serde_json", "tabled", ] @@ -3472,11 +3450,11 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] name = "papergrid" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1526bb6aa9f10ec339fb10360f22c57edf81d5678d0278e93bc12a47ffbe4b01" +checksum = "1fdfe703c51ddc52887ad78fc69cd2ea78d895ffcd6e955c9d03566db8ab5bb1" dependencies = [ - "ansi-str 0.5.0", + "ansi-str", "ansitok", "bytecount", "fnv", @@ -5128,11 +5106,12 @@ dependencies = [ [[package]] name = "tabled" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c3ee73732ffceaea7b8f6b719ce3bb17f253fa27461ffeaf568ebd0cdb4b85" +checksum = "da1a2e56bbf7bfdd08aaa7592157a742205459eff774b73bc01809ae2d99dc2a" dependencies = [ - "ansi-str 0.5.0", + "ansi-str", + "ansitok", "papergrid", "tabled_derive", "unicode-width", @@ -5140,9 +5119,9 @@ dependencies = [ [[package]] name = "tabled_derive" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beca1b4eaceb4f2755df858b88d9b9315b7ccfd1ffd0d7a48a52602301f01a57" +checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4" dependencies = [ "heck", "proc-macro-error", diff --git a/crates/nu-color-config/Cargo.toml b/crates/nu-color-config/Cargo.toml index 826e9aaa21..536fbde527 100644 --- a/crates/nu-color-config/Cargo.toml +++ b/crates/nu-color-config/Cargo.toml @@ -12,8 +12,6 @@ bench = false [dependencies] serde = { version="1.0.123", features=["derive"] } -# used only for text_style Alignments -tabled = { version = "0.10.0", features = ["color"], default-features = false } nu-protocol = { path = "../nu-protocol", version = "0.79.1" } nu-ansi-term = "0.47.0" diff --git a/crates/nu-color-config/src/style_computer.rs b/crates/nu-color-config/src/style_computer.rs index 158f7320df..4c2b47de9f 100644 --- a/crates/nu-color-config/src/style_computer.rs +++ b/crates/nu-color-config/src/style_computer.rs @@ -1,3 +1,4 @@ +use crate::text_style::Alignment; use crate::{color_record_to_nustyle, lookup_ansi_color_style, TextStyle}; use nu_ansi_term::{Color, Style}; use nu_engine::eval_block; @@ -5,7 +6,6 @@ use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, CliError, IntoPipelineData, Value, }; -use tabled::alignment::AlignmentHorizontal; use std::{ collections::HashMap, @@ -111,34 +111,28 @@ impl<'a> StyleComputer<'a> { // Used only by the `table` command. pub fn style_primitive(&self, value: &Value) -> TextStyle { + use Alignment::*; let s = self.compute(&value.get_type().get_non_specified_string(), value); match *value { - Value::Bool { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), - - Value::Int { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s), - - Value::Filesize { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s), - - Value::Duration { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s), - - Value::Date { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), - - Value::Range { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), - - Value::Float { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s), - - Value::String { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), - - Value::Nothing { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), - - Value::Binary { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), - - Value::CellPath { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s), - + Value::Bool { .. } => TextStyle::with_style(Left, s), + Value::Int { .. } => TextStyle::with_style(Right, s), + Value::Filesize { .. } => TextStyle::with_style(Right, s), + Value::Duration { .. } => TextStyle::with_style(Right, s), + Value::Date { .. } => TextStyle::with_style(Left, s), + Value::Range { .. } => TextStyle::with_style(Left, s), + Value::Float { .. } => TextStyle::with_style(Right, s), + Value::String { .. } => TextStyle::with_style(Left, s), + Value::Nothing { .. } => TextStyle::with_style(Left, s), + Value::Binary { .. } => TextStyle::with_style(Left, s), + Value::CellPath { .. } => TextStyle::with_style(Left, s), Value::Record { .. } | Value::List { .. } | Value::Block { .. } => { - TextStyle::with_style(AlignmentHorizontal::Left, s) + TextStyle::with_style(Left, s) } - _ => TextStyle::basic_left(), + Value::Closure { .. } + | Value::CustomValue { .. } + | Value::Error { .. } + | Value::LazyRecord { .. } + | Value::MatchPattern { .. } => TextStyle::basic_left(), } } diff --git a/crates/nu-color-config/src/text_style.rs b/crates/nu-color-config/src/text_style.rs index cba23923b9..d0c3b98ae2 100644 --- a/crates/nu-color-config/src/text_style.rs +++ b/crates/nu-color-config/src/text_style.rs @@ -1,7 +1,11 @@ use nu_ansi_term::{Color, Style}; -use std::fmt::Display; -pub type Alignment = tabled::alignment::AlignmentHorizontal; +#[derive(Debug, Clone, Copy)] +pub enum Alignment { + Center, + Left, + Right, +} #[derive(Debug, Clone, Copy)] pub struct TextStyle { @@ -240,23 +244,3 @@ impl Default for TextStyle { Self::new() } } - -impl tabled::papergrid::Color for TextStyle { - fn fmt_prefix(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(color) = &self.color_style { - color.prefix().fmt(f)?; - } - - Ok(()) - } - - fn fmt_suffix(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(color) = &self.color_style { - if !color.is_plain() { - f.write_str("\u{1b}[0m")?; - } - } - - Ok(()) - } -} diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index c934589c50..c2667f6bf9 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -28,7 +28,6 @@ nu-table = { path = "../nu-table", version = "0.79.1" } nu-term-grid = { path = "../nu-term-grid", version = "0.79.1" } nu-utils = { path = "../nu-utils", version = "0.79.1" } num-format = { version = "0.4.3" } - nu-ansi-term = "0.47.0" # Potential dependencies for extras @@ -89,7 +88,7 @@ percent-encoding = "2.2.0" rusqlite = { version = "0.28.0", features = ["bundled"], optional = true } sqlparser = { version = "0.32.0", features = ["serde"], optional = true } sysinfo = "0.28.2" -tabled = "0.10.0" +tabled = "0.12.0" terminal_size = "0.2.1" thiserror = "1.0.31" titlecase = "2.0.0" diff --git a/crates/nu-command/src/debug/inspect_table.rs b/crates/nu-command/src/debug/inspect_table.rs index bb7009b2b4..1a127ae798 100644 --- a/crates/nu-command/src/debug/inspect_table.rs +++ b/crates/nu-command/src/debug/inspect_table.rs @@ -1,123 +1,193 @@ use nu_protocol::Value; +use nu_table::{string_width, string_wrap}; use tabled::{ - builder::Builder, - peaker::PriorityMax, - width::{MinWidth, Wrap}, - Style, + grid::config::ColoredConfig, + settings::{peaker::PriorityMax, width::Wrap, Settings, Style}, + Table, }; -use self::{ - global_horizontal_char::SetHorizontalCharOnFirstRow, peak2::Peak2, - table_column_width::get_first_cell_width, truncate_table::TruncateTable, - width_increase::IncWidth, +use crate::debug::inspect_table::{ + global_horizontal_char::SetHorizontalChar, set_widths::SetWidths, }; pub fn build_table(value: Value, description: String, termsize: usize) -> String { let (head, mut data) = util::collect_input(value); + let count_columns = head.len(); data.insert(0, head); - let mut val_table = Builder::from(data).build(); - let val_table_width = val_table.total_width(); + let mut desc = description; + let mut desc_width = string_width(&desc); + let mut desc_table_width = get_total_width_2_column_table(11, desc_width); - let desc = vec![vec![String::from("description"), description]]; + let cfg = Table::default().with(Style::modern()).get_config().clone(); + let mut widths = get_data_widths(&data, count_columns); + truncate_data(&mut data, &mut widths, &cfg, termsize); - let mut desc_table = Builder::from(desc).build(); - let desc_table_width = desc_table.total_width(); + let val_table_width = get_total_width2(&widths, &cfg); + if val_table_width < desc_table_width { + increase_widths(&mut widths, desc_table_width - val_table_width); + increase_data_width(&mut data, &widths); + } + + if val_table_width > desc_table_width { + increase_string_width(&mut desc, val_table_width); + } + + if desc_table_width > termsize { + let delete_width = desc_table_width - termsize; + if delete_width >= desc_width { + // we can't fit in a description; we consider it's no point in showing then? + return String::new(); + } + + desc_width -= delete_width; + desc = string_wrap(&desc, desc_width, false); + desc_table_width = termsize; + } + + add_padding_to_widths(&mut widths); #[allow(clippy::manual_clamp)] let width = val_table_width.max(desc_table_width).min(termsize); - desc_table - .with(Style::rounded().off_bottom()) - .with(Wrap::new(width).priority::()) - .with(MinWidth::new(width).priority::()); + let mut desc_table = Table::from_iter([[String::from("description"), desc]]); + desc_table.with(Style::rounded().remove_bottom().remove_horizontals()); - val_table - .with(Style::rounded().top_left_corner('├').top_right_corner('┤')) - .with(TruncateTable(width)) - .with(Wrap::new(width).priority::()) - .with(IncWidth(width)); - - // we use only 1, cause left border considered 0 position - let count_split_lines = 1; - let desc_width = get_first_cell_width(&mut desc_table) + count_split_lines; - - val_table.with(SetHorizontalCharOnFirstRow::new('┼', '┴', desc_width)); + let mut val_table = Table::from_iter(data); + val_table.with( + Settings::default() + .with(Style::rounded().corner_top_left('├').corner_top_right('┤')) + .with(SetWidths(widths)) + .with(Wrap::new(width).priority::()) + .with(SetHorizontalChar::new('┼', '┴', 11 + 2 + 1)), + ); format!("{desc_table}\n{val_table}") } -mod truncate_table { - use tabled::{ - papergrid::{ - records::{Records, RecordsMut, Resizable}, - width::{CfgWidthFunction, WidthEstimator}, - Estimate, - }, - TableOption, - }; - - pub struct TruncateTable(pub usize); - - impl TableOption for TruncateTable - where - R: Records + RecordsMut + Resizable, - { - fn change(&mut self, table: &mut tabled::Table) { - let width = table.total_width(); - if width <= self.0 { - return; - } - - let count_columns = table.get_records().count_columns(); - if count_columns < 1 { - return; - } - - let mut evaluator = WidthEstimator::default(); - evaluator.estimate(table.get_records(), table.get_config()); - let columns_width: Vec<_> = evaluator.into(); - - const SPLIT_LINE_WIDTH: usize = 1; - let mut width = 0; - let mut i = 0; - for w in columns_width { - width += w + SPLIT_LINE_WIDTH; - - if width >= self.0 { - break; - } - - i += 1; - } - - if i == 0 && count_columns > 0 { - i = 1; - } else if i + 1 == count_columns { - // we want to left at least 1 column - i -= 1; - } - - let count_columns = table.get_records().count_columns(); - let y = count_columns - i; - - let mut column = count_columns; - for _ in 0..y { - column -= 1; - table.get_records_mut().remove_column(column); - } - - table.get_records_mut().push_column(); - - let width_ctrl = CfgWidthFunction::from_cfg(table.get_config()); - let last_column = table.get_records().count_columns() - 1; - for row in 0..table.get_records().count_rows() { - table - .get_records_mut() - .set((row, last_column), String::from("‥"), &width_ctrl) - } +fn get_data_widths(data: &[Vec], count_columns: usize) -> Vec { + let mut widths = vec![0; count_columns]; + for row in data { + for col in 0..count_columns { + let text = &row[col]; + let width = string_width(text); + widths[col] = std::cmp::max(widths[col], width); } } + + widths +} + +fn add_padding_to_widths(widths: &mut [usize]) { + for width in widths { + *width += 2; + } +} + +fn increase_widths(widths: &mut [usize], need: usize) { + let all = need / widths.len(); + let mut rest = need - all * widths.len(); + + for width in widths { + *width += all; + + if rest > 0 { + *width += 1; + rest -= 1; + } + } +} + +fn increase_data_width(data: &mut Vec>, widths: &[usize]) { + for row in data { + for (col, max_width) in widths.iter().enumerate() { + let text = &mut row[col]; + increase_string_width(text, *max_width); + } + } +} + +fn increase_string_width(text: &mut String, total: usize) { + let width = string_width(text); + let rest = total - width; + + if rest > 0 { + text.extend(std::iter::repeat(' ').take(rest)); + } +} + +fn get_total_width_2_column_table(col1: usize, col2: usize) -> usize { + const PAD: usize = 1; + const SPLIT_LINE: usize = 1; + SPLIT_LINE + PAD + col1 + PAD + SPLIT_LINE + PAD + col2 + PAD + SPLIT_LINE +} + +fn truncate_data( + data: &mut Vec>, + widths: &mut Vec, + cfg: &ColoredConfig, + expected_width: usize, +) { + const SPLIT_LINE_WIDTH: usize = 1; + const PAD: usize = 2; + + let total_width = get_total_width2(widths, cfg); + if total_width <= expected_width { + return; + } + + let mut width = 0; + let mut peak_count = 0; + for column_width in widths.iter() { + let next_width = width + *column_width + SPLIT_LINE_WIDTH + PAD; + if next_width >= expected_width { + break; + } + + width = next_width; + peak_count += 1; + } + + debug_assert!(peak_count < widths.len()); + + let left_space = expected_width - width; + let has_space_for_truncation_column = left_space > PAD; + if !has_space_for_truncation_column { + peak_count -= 1; + } + + remove_columns(data, peak_count); + widths.drain(peak_count..); + push_empty_column(data); + widths.push(1); +} + +fn remove_columns(data: &mut Vec>, peak_count: usize) { + if peak_count == 0 { + for row in data { + row.clear(); + } + } else { + for row in data { + row.drain(peak_count..); + } + } +} + +fn get_total_width2(widths: &[usize], cfg: &ColoredConfig) -> usize { + let pad = 2; + let total = widths.iter().sum::() + pad * widths.len(); + let countv = cfg.count_vertical(widths.len()); + let margin = cfg.get_margin(); + + total + countv + margin.left.size + margin.right.size +} + +fn push_empty_column(data: &mut Vec>) { + let empty_cell = String::from("‥"); + for row in data { + row.push(empty_cell.clone()); + } } mod util { @@ -223,135 +293,74 @@ mod util { } } -mod style_no_left_right_1st { - use tabled::{papergrid::records::Records, Table, TableOption}; - - struct StyleOffLeftRightFirstLine; - - impl TableOption for StyleOffLeftRightFirstLine - where - R: Records, - { - fn change(&mut self, table: &mut Table) { - let shape = table.shape(); - let cfg = table.get_config_mut(); - - let mut b = cfg.get_border((0, 0), shape); - b.left = Some(' '); - cfg.set_border((0, 0), b); - - let mut b = cfg.get_border((0, shape.1 - 1), shape); - b.right = Some(' '); - cfg.set_border((0, 0), b); - } - } -} - -mod peak2 { - use tabled::peaker::Peaker; - - pub struct Peak2; - - impl Peaker for Peak2 { - fn create() -> Self { - Self - } - - fn peak(&mut self, _: &[usize], _: &[usize]) -> Option { - Some(1) - } - } -} - -mod table_column_width { - use tabled::{ - papergrid::{records::Records, width::CfgWidthFunction}, - Table, - }; - - pub fn get_first_cell_width(table: &mut Table) -> usize { - let mut opt = GetFirstCellWidth(0); - table.with(&mut opt); - opt.0 - } - - struct GetFirstCellWidth(pub usize); - - impl tabled::TableOption for GetFirstCellWidth { - fn change(&mut self, table: &mut tabled::Table) { - let w = table - .get_records() - .get_width((0, 0), CfgWidthFunction::default()); - let pad = table - .get_config() - .get_padding(tabled::papergrid::Entity::Cell(0, 0)); - let pad = pad.left.size + pad.right.size; - - self.0 = w + pad; - } - } -} - mod global_horizontal_char { use tabled::{ - papergrid::{records::Records, width::WidthEstimator, Estimate, Offset::Begin}, - Table, TableOption, + grid::{ + config::{ColoredConfig, Offset}, + dimension::{CompleteDimensionVecRecords, Dimension}, + records::{ExactRecords, Records}, + }, + settings::TableOption, }; - pub struct SetHorizontalCharOnFirstRow { - c1: char, - c2: char, - pos: usize, + pub struct SetHorizontalChar { + intersection: char, + split: char, + index: usize, } - impl SetHorizontalCharOnFirstRow { - pub fn new(c1: char, c2: char, pos: usize) -> Self { - Self { c1, c2, pos } + impl SetHorizontalChar { + pub fn new(intersection: char, split: char, index: usize) -> Self { + Self { + intersection, + split, + index, + } } } - impl TableOption for SetHorizontalCharOnFirstRow - where - R: Records, + impl TableOption, ColoredConfig> + for SetHorizontalChar { - fn change(&mut self, table: &mut Table) { - if table.is_empty() { + fn change( + self, + records: &mut R, + cfg: &mut ColoredConfig, + dimension: &mut CompleteDimensionVecRecords<'_>, + ) { + let count_columns = records.count_columns(); + let count_rows = records.count_rows(); + + if count_columns == 0 || count_rows == 0 { return; } - let shape = table.shape(); + let widths = get_widths(dimension, records.count_columns()); - let mut evaluator = WidthEstimator::default(); - evaluator.estimate(table.get_records(), table.get_config()); - let widths: Vec<_> = evaluator.into(); - - let has_vertical = table.get_config().has_vertical(0, shape.1); - if has_vertical && self.pos == 0 { - let mut border = table.get_config().get_border((0, 0), shape); - border.left_top_corner = Some(self.c1); - table.get_config_mut().set_border((0, 0), border); + let has_vertical = cfg.has_vertical(0, count_columns); + if has_vertical && self.index == 0 { + let mut border = cfg.get_border((0, 0), (count_rows, count_columns)); + border.left_top_corner = Some(self.intersection); + cfg.set_border((0, 0), border); return; } let mut i = 1; - #[allow(clippy::needless_range_loop)] for (col, width) in widths.into_iter().enumerate() { - if self.pos < i + width { - let o = self.pos - i; - table - .get_config_mut() - .override_horizontal_border((0, col), self.c2, Begin(o)); + if self.index < i + width { + let o = self.index - i; + cfg.set_horizontal_char((0, col), self.split, Offset::Begin(o)); return; } i += width; - let has_vertical = table.get_config().has_vertical(col, shape.1); + let has_vertical = cfg.has_vertical(col, count_columns); if has_vertical { - if self.pos == i { - let mut border = table.get_config().get_border((0, col), shape); - border.right_top_corner = Some(self.c1); - table.get_config_mut().set_border((0, col), border); + if self.index == i { + let mut border = cfg.get_border((0, col), (count_rows, count_columns)); + border.right_top_corner = Some(self.intersection); + cfg.set_border((0, col), border); return; } @@ -360,96 +369,33 @@ mod global_horizontal_char { } } } -} -mod width_increase { - use tabled::{ - object::Cell, - papergrid::{ - records::{Records, RecordsMut}, - width::WidthEstimator, - Entity, Estimate, GridConfig, - }, - peaker::PriorityNone, - Modify, Width, - }; - - use tabled::{peaker::Peaker, Table, TableOption}; - - #[derive(Debug)] - pub struct IncWidth(pub usize); - - impl TableOption for IncWidth - where - R: Records + RecordsMut, - { - fn change(&mut self, table: &mut Table) { - if table.is_empty() { - return; - } - - let (widths, total_width) = - get_table_widths_with_total(table.get_records(), table.get_config()); - if total_width >= self.0 { - return; - } - - let increase_list = - get_increase_list(widths, self.0, total_width, PriorityNone::default()); - - for (col, width) in increase_list.into_iter().enumerate() { - for row in 0..table.get_records().count_rows() { - let pad = table.get_config().get_padding(Entity::Cell(row, col)); - let width = width - pad.left.size - pad.right.size; - - table.with(Modify::new(Cell(row, col)).with(Width::increase(width))); - } - } - } - } - - fn get_increase_list( - mut widths: Vec, - total_width: usize, - mut width: usize, - mut peaker: F, - ) -> Vec - where - F: Peaker, - { - while width != total_width { - let col = match peaker.peak(&[], &widths) { - Some(col) => col, - None => break, - }; - - widths[col] += 1; - width += 1; + fn get_widths(dims: &CompleteDimensionVecRecords<'_>, count_columns: usize) -> Vec { + let mut widths = vec![0; count_columns]; + for (col, width) in widths.iter_mut().enumerate() { + *width = dims.get_width(col); } widths } +} - fn get_table_widths_with_total(records: R, cfg: &GridConfig) -> (Vec, usize) - where - R: Records, - { - let mut evaluator = WidthEstimator::default(); - evaluator.estimate(&records, cfg); - let total_width = get_table_total_width(&records, cfg, &evaluator); - let widths = evaluator.into(); +mod set_widths { + use tabled::{ + grid::{config::ColoredConfig, dimension::CompleteDimensionVecRecords}, + settings::TableOption, + }; - (widths, total_width) - } + pub struct SetWidths(pub Vec); - pub(crate) fn get_table_total_width(records: R, cfg: &GridConfig, ctrl: &W) -> usize - where - W: Estimate, - R: Records, - { - ctrl.total() - + cfg.count_vertical(records.count_columns()) - + cfg.get_margin().left.size - + cfg.get_margin().right.size + impl TableOption, ColoredConfig> for SetWidths { + fn change( + self, + _: &mut R, + _: &mut ColoredConfig, + dims: &mut CompleteDimensionVecRecords<'_>, + ) { + dims.set_widths(self.0); + } } } diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 925819fdf7..82e734adfe 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -1,28 +1,25 @@ use lscolors::{LsColors, Style}; use nu_color_config::color_from_hex; -use nu_color_config::{Alignment, StyleComputer, TextStyle}; -use nu_engine::{column::get_columns, env_to_string, CallExt}; -use nu_protocol::TrimStrategy; +use nu_color_config::{StyleComputer, TextStyle}; +use nu_engine::{env_to_string, CallExt}; use nu_protocol::{ - ast::{Call, PathMember}, + ast::Call, engine::{Command, EngineState, Stack}, Category, Config, DataSource, Example, FooterMode, IntoPipelineData, ListStream, PipelineData, - PipelineMetadata, RawStream, ShellError, Signature, Span, SyntaxShape, TableIndexMode, Type, - Value, + PipelineMetadata, RawStream, ShellError, Signature, Span, SyntaxShape, Type, Value, +}; +use nu_table::{ + BuildConfig, Cell, CollapsedTable, ExpandedTable, JustTable, NuTable, StringResult, + TableConfig, TableOutput, TableTheme, }; -use nu_table::{string_width, Table as NuTable, TableConfig, TableTheme}; use nu_utils::get_ls_colors; -use rayon::prelude::*; use std::sync::Arc; use std::time::Instant; -use std::{cmp::max, path::PathBuf, sync::atomic::AtomicBool}; +use std::{path::PathBuf, sync::atomic::AtomicBool}; use terminal_size::{Height, Width}; use url::Url; const STREAM_PAGE_SIZE: usize = 1000; -const INDEX_COLUMN_NAME: &str = "index"; - -type NuText = (String, TextStyle); fn get_width_param(width_param: Option) -> usize { if let Some(col) = width_param { @@ -343,231 +340,6 @@ fn supported_table_modes() -> Vec { ] } -fn build_collapsed_table( - style_computer: &StyleComputer, - cols: Vec, - vals: Vec, - config: &Config, - term_width: usize, -) -> Result, ShellError> { - let mut value = Value::Record { - cols, - vals, - span: Span::new(0, 0), - }; - - colorize_value(&mut value, config, style_computer); - - let theme = load_theme_from_config(config); - let table = nu_table::NuTable::new(value, true, config, style_computer, &theme, false); - - let table = table.draw(term_width); - - Ok(table) -} - -fn build_general_table2( - style_computer: &StyleComputer, - cols: Vec, - vals: Vec, - ctrlc: Option>, - config: &Config, - term_width: usize, -) -> Result, ShellError> { - let mut data = Vec::with_capacity(vals.len()); - for (column, value) in cols.into_iter().zip(vals.into_iter()) { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - return Ok(None); - } - - let row = vec![ - NuTable::create_cell(column, TextStyle::default_field()), - NuTable::create_cell(value.into_abbreviated_string(config), TextStyle::default()), - ]; - - data.push(row); - } - - let data_len = data.len(); - let table_config = create_table_config(config, style_computer, data_len, false, false, false); - - let table = NuTable::new(data, (data_len, 2)); - - let table = table.draw(table_config, term_width); - - Ok(table) -} - -// The table produced by `table -e` -#[allow(clippy::too_many_arguments)] -fn build_expanded_table( - cols: Vec, - vals: Vec, - span: Span, - ctrlc: Option>, - config: &Config, - style_computer: &StyleComputer, - term_width: usize, - expand_limit: Option, - flatten: bool, - flatten_sep: &str, -) -> Result, ShellError> { - let theme = load_theme_from_config(config); - - let key_width = cols.iter().map(|col| string_width(col)).max().unwrap_or(0); - - let count_borders = - theme.has_inner() as usize + theme.has_right() as usize + theme.has_left() as usize; - let padding = 2; - if key_width + count_borders + padding + padding > term_width { - return Ok(None); - } - - let value_width = term_width - key_width - count_borders - padding - padding; - - let mut data = Vec::with_capacity(cols.len()); - for (key, value) in cols.into_iter().zip(vals) { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - return Ok(None); - } - - let is_limited = matches!(expand_limit, Some(0)); - let mut is_expanded = false; - let value = if is_limited { - value_to_styled_string(&value, config, style_computer).0 - } else { - let deep = expand_limit.map(|i| i - 1); - - match value { - Value::List { vals, .. } => { - let table = convert_to_table2( - 0, - vals.iter(), - ctrlc.clone(), - config, - span, - style_computer, - deep, - flatten, - flatten_sep, - value_width, - )?; - - match table { - Some((table, with_header, with_index)) => { - is_expanded = true; - - let table_config = create_table_config( - config, - style_computer, - table.count_rows(), - with_header, - with_index, - false, - ); - - let val = table.draw(table_config, value_width); - match val { - Some(result) => result, - None => return Ok(None), - } - } - None => { - // it means that the list is empty - let value = Value::List { vals, span }; - let text = value_to_styled_string(&value, config, style_computer).0; - wrap_text(&text, value_width, config) - } - } - } - Value::Record { cols, vals, span } => { - if cols.is_empty() { - // Like list case return styled string instead of empty value - let value = Value::Record { cols, vals, span }; - let text = value_to_styled_string(&value, config, style_computer).0; - wrap_text(&text, value_width, config) - } else { - let result = build_expanded_table( - cols.clone(), - vals.clone(), - span, - ctrlc.clone(), - config, - style_computer, - value_width, - deep, - flatten, - flatten_sep, - )?; - - match result { - Some(result) => { - is_expanded = true; - result - } - None => { - let failed_value = value_to_styled_string( - &Value::Record { cols, vals, span }, - config, - style_computer, - ); - - wrap_text(&failed_value.0, value_width, config) - } - } - } - } - val => { - let text = value_to_styled_string(&val, config, style_computer).0; - wrap_text(&text, value_width, config) - } - } - }; - - // we want to have a key being aligned to 2nd line, - // 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; - if !key.is_empty() && is_expanded && theme.has_top_line() { - key.insert(0, '\n'); - } - - let key = NuTable::create_cell(key, TextStyle::default_field()); - let val = NuTable::create_cell(value, TextStyle::default()); - - let row = vec![key, val]; - data.push(row); - } - - let data_len = data.len(); - let table_config = create_table_config(config, style_computer, data_len, false, false, false); - let table = NuTable::new(data, (data_len, 2)); - - let table_s = table.clone().draw(table_config.clone(), term_width); - - let table = match table_s { - Some(s) => { - // check whether we need to expand table or not, - // todo: we can make it more effitient - - const EXPAND_TREASHHOLD: f32 = 0.80; - - let width = string_width(&s); - let used_percent = width as f32 / term_width as f32; - - if width < term_width && used_percent > EXPAND_TREASHHOLD { - let table_config = table_config.expand(); - table.draw(table_config, term_width) - } else { - Some(s) - } - } - None => None, - }; - - Ok(table) -} - #[allow(clippy::too_many_arguments)] fn handle_record( cols: Vec, @@ -583,54 +355,17 @@ fn handle_record( ) -> Result { // Create a StyleComputer to compute styles for each value in the table. let style_computer = &StyleComputer::from_config(engine_state, stack); + let ctrlc1 = ctrlc.clone(); let result = if cols.is_empty() { create_empty_placeholder("record", term_width, engine_state, stack) } else { - let result = match table_view { - TableView::General => build_general_table2( - style_computer, - cols, - vals, - ctrlc.clone(), - config, - term_width, - ), - TableView::Expanded { - limit, - flatten, - flatten_separator, - } => { - let sep = flatten_separator.as_deref().unwrap_or(" "); - build_expanded_table( - cols, - vals, - span, - ctrlc.clone(), - config, - style_computer, - term_width, - limit, - flatten, - sep, - ) - } - TableView::Collapsed => { - build_collapsed_table(style_computer, cols, vals, config, term_width) - } - }?; - - let result = strip_output_color(result, config); - - result.unwrap_or_else(|| { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - "".into() - } else { - // assume this failed because the table was too wide - // TODO: more robust error classification - format!("Couldn't fit table into {term_width} columns!") - } - }) + let opts = BuildConfig::new(ctrlc, config, style_computer, span, term_width); + let result = build_table_kv(cols, vals, table_view, opts)?; + match result { + Some(output) => maybe_strip_color(output, config), + None => report_unsuccessful_output(ctrlc1, term_width), + } }; let val = Value::String { @@ -641,6 +376,64 @@ fn handle_record( Ok(val.into_pipeline_data()) } +fn report_unsuccessful_output(ctrlc1: Option>, term_width: usize) -> String { + if nu_utils::ctrl_c::was_pressed(&ctrlc1) { + "".into() + } else { + // assume this failed because the table was too wide + // TODO: more robust error classification + format!("Couldn't fit table into {term_width} columns!") + } +} + +fn build_table_kv( + cols: Vec, + vals: Vec, + table_view: TableView, + opts: BuildConfig<'_>, +) -> StringResult { + match table_view { + TableView::General => JustTable::kv_table(&cols, &vals, opts), + TableView::Expanded { + limit, + flatten, + flatten_separator, + } => { + let sep = flatten_separator.unwrap_or_else(|| String::from(' ')); + ExpandedTable::new(limit, flatten, sep).build_map(&cols, &vals, opts) + } + TableView::Collapsed => { + let span = opts.span(); + let value = Value::Record { cols, vals, span }; + CollapsedTable::build(value, opts) + } + } +} + +fn build_table_batch( + vals: Vec, + table_view: TableView, + row_offset: usize, + opts: BuildConfig<'_>, +) -> StringResult { + match table_view { + TableView::General => JustTable::table(&vals, row_offset, opts), + TableView::Expanded { + limit, + flatten, + flatten_separator, + } => { + let sep = flatten_separator.unwrap_or_else(|| String::from(' ')); + ExpandedTable::new(limit, flatten, sep).build_list(&vals, opts) + } + TableView::Collapsed => { + let span = opts.span(); + let value = Value::List { vals, span }; + CollapsedTable::build(value, opts) + } + } +} + fn handle_row_stream( engine_state: &EngineState, stack: &mut Stack, @@ -803,755 +596,6 @@ fn make_clickable_link( } } -// convert_to_table() defers all its style computations so that they can be run in parallel using par_extend(). -// This structure holds the intermediate computations. -// Currently, the other table forms don't use this. -// Because of how table-specific this is, I don't think this can be pushed into StyleComputer itself. -enum DeferredStyleComputation { - Value { value: Value }, - Header { text: String }, - RowIndex { text: String }, - Empty {}, -} - -impl DeferredStyleComputation { - // This is only run inside a par_extend(). - fn compute(&self, config: &Config, style_computer: &StyleComputer) -> NuText { - match self { - DeferredStyleComputation::Value { value } => { - match value { - // Float precision is required here. - Value::Float { val, .. } => ( - format!("{:.prec$}", val, prec = config.float_precision as usize), - style_computer.style_primitive(value), - ), - _ => ( - value.into_abbreviated_string(config), - style_computer.style_primitive(value), - ), - } - } - DeferredStyleComputation::Header { text } => ( - text.clone(), - TextStyle::with_style( - Alignment::Center, - style_computer - .compute("header", &Value::string(text.as_str(), Span::unknown())), - ), - ), - DeferredStyleComputation::RowIndex { text } => ( - text.clone(), - TextStyle::with_style( - Alignment::Right, - style_computer - .compute("row_index", &Value::string(text.as_str(), Span::unknown())), - ), - ), - DeferredStyleComputation::Empty {} => ( - "❎".into(), - TextStyle::with_style( - Alignment::Right, - style_computer.compute("empty", &Value::nothing(Span::unknown())), - ), - ), - } - } -} - -fn convert_to_table( - row_offset: usize, - input: &[Value], - ctrlc: Option>, - config: &Config, - head: Span, - style_computer: &StyleComputer, -) -> Result, ShellError> { - let mut headers = get_columns(input); - let mut input = input.iter().peekable(); - let with_index = match config.table_index_mode { - TableIndexMode::Always => true, - TableIndexMode::Never => false, - TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME), - }; - - if input.peek().is_none() { - return Ok(None); - } - - let with_header = !headers.is_empty(); - - if with_header && with_index { - headers.insert(0, "#".into()); - } - - // The header with the INDEX is removed from the table headers since - // it is added to the natural table index - let headers: Vec<_> = headers - .into_iter() - .filter(|header| header != INDEX_COLUMN_NAME) - .map(|text| DeferredStyleComputation::Header { text }) - .collect(); - - let mut count_columns = headers.len(); - - let mut data: Vec> = if !with_header { - Vec::new() - } else { - vec![headers] - }; - - // Turn each item of each row into a DeferredStyleComputation for that item. - for (row_num, item) in input.enumerate() { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - return Ok(None); - } - - if let Value::Error { error } = item { - return Err(*error.clone()); - } - - let mut row = vec![]; - if with_index { - let text = match &item { - Value::Record { .. } => item - .get_data_by_key(INDEX_COLUMN_NAME) - .map(|value| value.into_string("", config)), - _ => None, - } - .unwrap_or_else(|| (row_num + row_offset).to_string()); - - row.push(DeferredStyleComputation::RowIndex { text }); - } - - if !with_header { - row.push(DeferredStyleComputation::Value { - value: item.clone(), - }); - } else { - let skip_num = usize::from(with_index); - // data[0] is used here because headers (the direct reference to it) has been moved. - for header in data[0].iter().skip(skip_num) { - if let DeferredStyleComputation::Header { text } = header { - row.push(match item { - Value::Record { .. } => { - let path = PathMember::String { - val: text.clone(), - span: head, - optional: false, - }; - let val = item.clone().follow_cell_path(&[path], false); - - match val { - Ok(val) => DeferredStyleComputation::Value { value: val }, - Err(_) => DeferredStyleComputation::Empty {}, - } - } - _ => DeferredStyleComputation::Value { - value: item.clone(), - }, - }); - } - } - } - - count_columns = max(count_columns, row.len()); - - data.push(row); - } - - // All the computations are parallelised here. - // NOTE: It's currently not possible to Ctrl-C out of this... - let mut cells: Vec> = Vec::with_capacity(data.len()); - data.into_par_iter() - .map(|row| { - let mut new_row = Vec::with_capacity(row.len()); - row.into_par_iter() - .map(|deferred| { - let pair = deferred.compute(config, style_computer); - - NuTable::create_cell(pair.0, pair.1) - }) - .collect_into_vec(&mut new_row); - new_row - }) - .collect_into_vec(&mut cells); - - let count_rows = cells.len(); - let table = NuTable::new(cells, (count_rows, count_columns)); - - Ok(Some((table, with_header, with_index))) -} - -#[allow(clippy::too_many_arguments)] -#[allow(clippy::into_iter_on_ref)] -fn convert_to_table2<'a>( - row_offset: usize, - input: impl Iterator + ExactSizeIterator + Clone, - ctrlc: Option>, - config: &Config, - head: Span, - style_computer: &StyleComputer, - deep: Option, - flatten: bool, - flatten_sep: &str, - available_width: usize, -) -> Result, ShellError> { - const PADDING_SPACE: usize = 2; - const SPLIT_LINE_SPACE: usize = 1; - const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE; - const MIN_CELL_CONTENT_WIDTH: usize = 1; - const TRUNCATE_CONTENT_WIDTH: usize = 3; - const TRUNCATE_CELL_WIDTH: usize = TRUNCATE_CONTENT_WIDTH + PADDING_SPACE; - - if input.len() == 0 { - return Ok(None); - } - - // 2 - split lines - let mut available_width = available_width.saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE); - if available_width < MIN_CELL_CONTENT_WIDTH { - return Ok(None); - } - - let headers = get_columns(input.clone()); - - let with_index = match config.table_index_mode { - TableIndexMode::Always => true, - TableIndexMode::Never => false, - TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME), - }; - - // The header with the INDEX is removed from the table headers since - // it is added to the natural table index - let headers: Vec<_> = headers - .into_iter() - .filter(|header| header != INDEX_COLUMN_NAME) - .collect(); - - let with_header = !headers.is_empty(); - - let mut data = vec![vec![]; input.len()]; - if !headers.is_empty() { - data.push(vec![]); - }; - - if with_index { - if with_header { - data[0].push(NuTable::create_cell("#", header_style(style_computer, "#"))); - } - - let mut last_index = 0; - for (row, item) in input.clone().enumerate() { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - return Ok(None); - } - - if let Value::Error { error } = item { - return Err(*error.clone()); - } - - let index = row + row_offset; - let text = matches!(item, Value::Record { .. }) - .then(|| lookup_index_value(item, config).unwrap_or_else(|| index.to_string())) - .unwrap_or_else(|| index.to_string()); - let value = make_index_string(text, style_computer); - - let value = NuTable::create_cell(value.0, value.1); - - let row = if with_header { row + 1 } else { row }; - data[row].push(value); - - last_index = index; - } - - let column_width = string_width(&last_index.to_string()); - - if column_width + ADDITIONAL_CELL_SPACE > available_width { - available_width = 0; - } else { - available_width -= column_width + ADDITIONAL_CELL_SPACE; - } - } - - if !with_header { - if available_width > ADDITIONAL_CELL_SPACE { - available_width -= PADDING_SPACE; - } else { - // it means we have no space left for actual content; - // which means there's no point in index itself if it was even used. - // so we do not print it. - return Ok(None); - } - - for (row, item) in input.into_iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - return Ok(None); - } - - if let Value::Error { error } = item { - return Err(*error.clone()); - } - - let mut value = convert_to_table2_entry( - item, - config, - &ctrlc, - style_computer, - deep, - flatten, - flatten_sep, - available_width, - ); - - let value_width = string_width(&value.0); - 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? - - value.0 = wrap_text(&value.0, available_width, config); - } - - let value = NuTable::create_cell(value.0, value.1); - data[row].push(value); - } - - let count_columns = if with_index { 2 } else { 1 }; - let size = (data.len(), count_columns); - - let table = NuTable::new(data, size); - - return Ok(Some((table, with_header, with_index))); - } - - if !headers.is_empty() { - let mut pad_space = PADDING_SPACE; - if headers.len() > 1 { - pad_space += SPLIT_LINE_SPACE; - } - - if available_width < pad_space { - // there's no space for actual data so we don't return index if it's present. - // (also see the comment after the loop) - - return Ok(None); - } - } - - let count_columns = headers.len(); - let mut widths = Vec::new(); - let mut truncate = false; - let mut rendered_column = 0; - for (col, header) in headers.into_iter().enumerate() { - let is_last_column = col + 1 == count_columns; - - let mut pad_space = PADDING_SPACE; - if !is_last_column { - pad_space += SPLIT_LINE_SPACE; - } - - let mut available = available_width - pad_space; - - let mut column_width = string_width(&header); - - if !is_last_column { - // we need to make sure that we have a space for a next column if we use available width - // so we might need to decrease a bit it. - - // we consider a header width be a minimum width - let pad_space = PADDING_SPACE + TRUNCATE_CONTENT_WIDTH; - - if available > pad_space { - // In we have no space for a next column, - // We consider showing something better then nothing, - // So we try to decrease the width to show at least a truncution column - - available -= pad_space; - } else { - truncate = true; - break; - } - - if available < column_width { - truncate = true; - break; - } - } - - let head_cell = NuTable::create_cell(header.clone(), header_style(style_computer, &header)); - data[0].push(head_cell); - - for (row, item) in input.clone().enumerate() { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - return Ok(None); - } - - if let Value::Error { error } = item { - return Err(*error.clone()); - } - - let mut value = create_table2_entry( - item, - header.as_str(), - head, - config, - &ctrlc, - style_computer, - deep, - flatten, - flatten_sep, - available, - ); - - let mut value_width = string_width(&value.0); - - 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) - - value.0 = wrap_text(&value.0, available, config); - value_width = available; - } - - column_width = max(column_width, value_width); - - let value = NuTable::create_cell(value.0, value.1); - - data[row + 1].push(value); - } - - if column_width > available { - // remove the column we just inserted - for row in &mut data { - row.pop(); - } - - truncate = true; - break; - } - - widths.push(column_width); - - available_width -= pad_space + column_width; - rendered_column += 1; - } - - if truncate && rendered_column == 0 { - // it means that no actual data was rendered, there might be only index present, - // so there's no point in rendering the table. - // - // It's actually quite important in case it's called recursively, - // cause we will back up to the basic table view as a string e.g. '[table 123 columns]'. - // - // But potentially if its reached as a 1st called function we might would love to see the index. - - return Ok(None); - } - - if truncate { - if available_width < TRUNCATE_CELL_WIDTH { - // back up by removing last column. - // it's LIKELY that removing only 1 column will leave us enough space for a shift column. - - while let Some(width) = widths.pop() { - for row in &mut data { - row.pop(); - } - - available_width += width + PADDING_SPACE; - if !widths.is_empty() { - available_width += SPLIT_LINE_SPACE; - } - - if available_width > TRUNCATE_CELL_WIDTH { - break; - } - } - } - - // this must be a RARE case or even NEVER happen, - // but we do check it just in case. - if available_width < TRUNCATE_CELL_WIDTH { - return Ok(None); - } - - let is_last_column = widths.len() == count_columns; - if !is_last_column { - let shift = NuTable::create_cell(String::from("..."), TextStyle::default()); - for row in &mut data { - row.push(shift.clone()); - } - - widths.push(3); - } - } - - let count_columns = widths.len() + with_index as usize; - let count_rows = data.len(); - let size = (count_rows, count_columns); - - let table = NuTable::new(data, size); - - Ok(Some((table, with_header, with_index))) -} - -fn lookup_index_value(item: &Value, config: &Config) -> Option { - item.get_data_by_key(INDEX_COLUMN_NAME) - .map(|value| value.into_string("", config)) -} - -fn header_style(style_computer: &StyleComputer, header: &str) -> TextStyle { - let style = style_computer.compute("header", &Value::string(header, Span::unknown())); - TextStyle { - alignment: Alignment::Center, - color_style: Some(style), - } -} - -#[allow(clippy::too_many_arguments)] -fn create_table2_entry( - item: &Value, - header: &str, - head: Span, - config: &Config, - ctrlc: &Option>, - style_computer: &StyleComputer, - deep: Option, - flatten: bool, - flatten_sep: &str, - width: usize, -) -> NuText { - match item { - Value::Record { .. } => { - let val = header.to_owned(); - let path = PathMember::String { - val, - span: head, - optional: false, - }; - let val = item.clone().follow_cell_path(&[path], false); - - match val { - Ok(val) => convert_to_table2_entry( - &val, - config, - ctrlc, - style_computer, - deep, - flatten, - flatten_sep, - width, - ), - Err(_) => error_sign(style_computer), - } - } - _ => convert_to_table2_entry( - item, - config, - ctrlc, - style_computer, - deep, - flatten, - flatten_sep, - width, - ), - } -} - -fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) { - make_styled_string(style_computer, String::from("❎"), None, 0) -} - -fn wrap_text(text: &str, width: usize, config: &Config) -> String { - nu_table::string_wrap(text, width, is_cfg_trim_keep_words(config)) -} - -#[allow(clippy::too_many_arguments)] -fn convert_to_table2_entry( - item: &Value, - config: &Config, - ctrlc: &Option>, - // This is passed in, even though it could be retrieved from config, - // to save reallocation (because it's presumably being used upstream). - style_computer: &StyleComputer, - deep: Option, - flatten: bool, - flatten_sep: &str, - width: usize, -) -> NuText { - let is_limit_reached = matches!(deep, Some(0)); - if is_limit_reached { - return value_to_styled_string(item, config, style_computer); - } - - match &item { - Value::Record { span, cols, vals } => { - if cols.is_empty() && vals.is_empty() { - return value_to_styled_string(item, config, style_computer); - } - - // we verify what is the structure of a Record cause it might represent - - let table = build_expanded_table( - cols.clone(), - vals.clone(), - *span, - ctrlc.clone(), - config, - style_computer, - width, - deep.map(|i| i - 1), - flatten, - flatten_sep, - ); - - match table { - Ok(Some(table)) => (table, TextStyle::default()), - _ => value_to_styled_string(item, config, style_computer), - } - } - Value::List { vals, span } => { - if flatten { - let is_simple_list = vals - .iter() - .all(|v| !matches!(v, Value::Record { .. } | Value::List { .. })); - - if is_simple_list { - return convert_value_list_to_string(vals, config, style_computer, flatten_sep); - } - } - - let table = convert_to_table2( - 0, - vals.iter(), - ctrlc.clone(), - config, - *span, - style_computer, - deep.map(|i| i - 1), - flatten, - flatten_sep, - width, - ); - - let (table, whead, windex) = match table { - Ok(Some(out)) => out, - _ => return value_to_styled_string(item, config, style_computer), - }; - - let count_rows = table.count_rows(); - let table_config = - create_table_config(config, style_computer, count_rows, whead, windex, false); - - let table = table.draw(table_config, usize::MAX); - match table { - Some(table) => (table, TextStyle::default()), - None => value_to_styled_string(item, config, style_computer), - } - } - _ => value_to_styled_string(item, config, style_computer), // unknown type. - } -} - -fn convert_value_list_to_string( - vals: &[Value], - config: &Config, - // This is passed in, even though it could be retrieved from config, - // to save reallocation (because it's presumably being used upstream). - style_computer: &StyleComputer, - flatten_sep: &str, -) -> NuText { - let mut buf = Vec::new(); - for value in vals { - let (text, _) = value_to_styled_string(value, config, style_computer); - - buf.push(text); - } - let text = buf.join(flatten_sep); - (text, TextStyle::default()) -} - -fn value_to_styled_string( - value: &Value, - config: &Config, - // This is passed in, even though it could be retrieved from config, - // to save reallocation (because it's presumably being used upstream). - style_computer: &StyleComputer, -) -> NuText { - let float_precision = config.float_precision as usize; - make_styled_string( - style_computer, - value.into_abbreviated_string(config), - Some(value), - float_precision, - ) -} - -fn make_styled_string( - style_computer: &StyleComputer, - text: String, - value: Option<&Value>, // None represents table holes. - float_precision: usize, -) -> NuText { - match value { - Some(value) => { - match value { - Value::Float { .. } => { - // set dynamic precision from config - let precise_number = match convert_with_precision(&text, float_precision) { - Ok(num) => num, - Err(e) => e.to_string(), - }; - (precise_number, style_computer.style_primitive(value)) - } - _ => (text, style_computer.style_primitive(value)), - } - } - None => { - // Though holes are not the same as null, the closure for "empty" is passed a null anyway. - ( - text, - TextStyle::with_style( - Alignment::Center, - style_computer.compute("empty", &Value::nothing(Span::unknown())), - ), - ) - } - } -} - -fn make_index_string(text: String, style_computer: &StyleComputer) -> NuText { - let style = style_computer.compute("row_index", &Value::string(text.as_str(), Span::unknown())); - (text, TextStyle::with_style(Alignment::Right, style)) -} - -fn convert_with_precision(val: &str, precision: usize) -> Result { - // vall will always be a f64 so convert it with precision formatting - let val_float = match val.trim().parse::() { - Ok(f) => f, - Err(e) => { - return Err(ShellError::GenericError( - format!("error converting string [{}] to f64", &val), - "".to_string(), - None, - Some(e.to_string()), - Vec::new(), - )); - } - }; - Ok(format!("{val_float:.precision$}")) -} - -fn is_cfg_trim_keep_words(config: &Config) -> bool { - matches!( - config.trim_strategy, - TrimStrategy::Wrap { - try_to_keep_words: true - } - ) -} - struct PagingTableCreator { head: Span, stream: ListStream, @@ -1593,11 +637,11 @@ impl PagingTableCreator { fn build_extended( &mut self, - batch: &[Value], + batch: Vec, limit: Option, flatten: bool, flatten_separator: Option, - ) -> Result, ShellError> { + ) -> StringResult { if batch.is_empty() { return Ok(None); } @@ -1606,112 +650,43 @@ impl PagingTableCreator { let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); let term_width = get_width_param(self.width_param); - let table = convert_to_table2( - self.row_offset, - batch.iter(), - self.ctrlc.clone(), - config, - self.head, - &style_computer, + let ctrlc = self.ctrlc.clone(); + let span = self.head; + let opts = BuildConfig::new(ctrlc, config, &style_computer, span, term_width); + let view = TableView::Expanded { limit, flatten, - flatten_separator.as_deref().unwrap_or(" "), - term_width, - )?; - - let (table, with_header, with_index) = match table { - Some(table) => table, - None => return Ok(None), + flatten_separator, }; - let table_config = create_table_config( - config, - &style_computer, - table.count_rows(), - with_header, - with_index, - false, - ); - - let table_s = table.clone().draw(table_config.clone(), term_width); - - let table = match table_s { - Some(s) => { - // check whether we need to expand table or not, - // todo: we can make it more efficient - - const EXPAND_THRESHOLD: f32 = 0.80; - - let width = string_width(&s); - let used_percent = width as f32 / term_width as f32; - - if width < term_width && used_percent > EXPAND_THRESHOLD { - let table_config = table_config.expand(); - table.draw(table_config, term_width) - } else { - Some(s) - } - } - None => None, - }; - - Ok(table) + build_table_batch(batch, view, 0, opts) } - fn build_collapsed(&mut self, batch: Vec) -> Result, ShellError> { + fn build_collapsed(&mut self, batch: Vec) -> StringResult { if batch.is_empty() { return Ok(None); } let config = self.engine_state.get_config(); let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); - let theme = load_theme_from_config(config); let term_width = get_width_param(self.width_param); - let need_footer = matches!(config.footer_mode, FooterMode::RowCount(limit) if batch.len() as u64 > limit) - || matches!(config.footer_mode, FooterMode::Always); - let mut value = Value::List { - vals: batch, - span: Span::new(0, 0), - }; + let ctrlc = self.ctrlc.clone(); + let span = self.head; + let opts = BuildConfig::new(ctrlc, config, &style_computer, span, term_width); - colorize_value(&mut value, config, &style_computer); - - let table = - nu_table::NuTable::new(value, true, config, &style_computer, &theme, need_footer); - - Ok(table.draw(term_width)) + build_table_batch(batch, TableView::Collapsed, 0, opts) } - fn build_general(&mut self, batch: &[Value]) -> Result, ShellError> { + fn build_general(&mut self, batch: Vec) -> StringResult { let term_width = get_width_param(self.width_param); let config = &self.engine_state.get_config(); let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack); - let table = convert_to_table( - self.row_offset, - batch, - self.ctrlc.clone(), - config, - self.head, - &style_computer, - )?; + let ctrlc = self.ctrlc.clone(); + let span = self.head; + let row_offset = self.row_offset; + let opts = BuildConfig::new(ctrlc, config, &style_computer, span, term_width); - let (table, with_header, with_index) = match table { - Some(table) => table, - None => return Ok(None), - }; - - let table_config = create_table_config( - config, - &style_computer, - table.count_rows(), - with_header, - with_index, - false, - ); - - let table = table.draw(table_config, term_width); - - Ok(table) + build_table_batch(batch, TableView::General, row_offset, opts) } } @@ -1768,21 +743,20 @@ impl Iterator for PagingTableCreator { } let table = match &self.view { - TableView::General => self.build_general(&batch), + 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()), + } => self.build_extended(batch, *limit, *flatten, flatten_separator.clone()), }; self.row_offset += idx; match table { Ok(Some(table)) => { - let table = strip_output_color(Some(table), self.engine_state.get_config()) - .expect("must never happen"); + let table = maybe_strip_color(table, self.engine_state.get_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 @@ -1873,42 +847,30 @@ enum TableView { } #[allow(clippy::manual_filter)] -fn strip_output_color(output: Option, config: &Config) -> Option { - match output { - Some(output) => { - // the atty is for when people do ls from vim, there should be no coloring there - if !config.use_ansi_coloring || !atty::is(atty::Stream::Stdout) { - // Draw the table without ansi colors - Some(nu_utils::strip_ansi_string_likely(output)) - } else { - // Draw the table with ansi colors - Some(output) - } - } - None => None, +fn maybe_strip_color(output: String, config: &Config) -> String { + // the atty is for when people do ls from vim, there should be no coloring there + if !config.use_ansi_coloring || !atty::is(atty::Stream::Stdout) { + // Draw the table without ansi colors + nu_utils::strip_ansi_string_likely(output) + } else { + // Draw the table with ansi colors + output } } -fn create_table_config( - config: &Config, - style_computer: &StyleComputer, - count_records: usize, - with_header: bool, - with_index: bool, - expand: bool, -) -> TableConfig { +fn create_table_config(config: &Config, comp: &StyleComputer, out: &TableOutput) -> TableConfig { let theme = load_theme_from_config(config); - let append_footer = with_footer(config, with_header, count_records); + let footer = with_footer(config, out.with_header, out.table.count_rows()); + let line_style = lookup_separator_color(comp); + let trim = config.trim_strategy.clone(); - let mut table_cfg = TableConfig::new(theme, with_header, with_index, append_footer); - - table_cfg = table_cfg.splitline_style(lookup_separator_color(style_computer)); - - if expand { - table_cfg = table_cfg.expand(); - } - - table_cfg.trim(config.trim_strategy.clone()) + TableConfig::new() + .theme(theme) + .with_footer(footer) + .with_header(out.with_header) + .with_index(out.with_index) + .line_style(line_style) + .trim(trim) } fn lookup_separator_color(style_computer: &StyleComputer) -> nu_ansi_term::Style { @@ -1931,49 +893,20 @@ fn create_empty_placeholder( stack: &Stack, ) -> String { let config = engine_state.get_config(); - if !config.table_show_empty { - return "".into(); + return String::new(); } - let empty_info_string = format!("empty {}", value_type_name); - let cell = NuTable::create_cell(empty_info_string, TextStyle::default().dimmed()); + let cell = Cell::new(format!("empty {}", value_type_name)); let data = vec![vec![cell]]; - let table = NuTable::new(data, (1, 1)); + let mut table = NuTable::from(data); + table.set_cell_style((0, 0), TextStyle::default().dimmed()); + let out = TableOutput::new(table, false, false); let style_computer = &StyleComputer::from_config(engine_state, stack); - let config = create_table_config(config, style_computer, 1, false, false, false); + let config = create_table_config(config, style_computer, &out); - table + out.table .draw(config, termwidth) .expect("Could not create empty table placeholder") } - -fn colorize_value(value: &mut Value, config: &Config, style_computer: &StyleComputer) { - match value { - Value::Record { cols, vals, .. } => { - for val in vals { - colorize_value(val, config, style_computer); - } - - let style = header_style(style_computer, ""); - if let Some(color) = style.color_style { - for header in cols { - *header = color.paint(header.to_owned()).to_string(); - } - } - } - Value::List { vals, .. } => { - for val in vals { - colorize_value(val, config, style_computer); - } - } - val => { - let (text, style) = value_to_styled_string(val, config, style_computer); - if let Some(color) = style.color_style { - let text = color.paint(text); - *val = Value::string(text.to_string(), val.span().unwrap_or(Span::unknown())); - } - } - } -} diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index 952337a4bc..c4ddef77fb 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -154,11 +154,11 @@ fn table_collapse_none() { assert_eq!( actual.out, concat!( - " a b c ", - " 1 2 3 ", - " 4 5 1 ", - " 2 ", - " 3 ", + " a b c ", + " 1 2 3 ", + " 4 5 1 ", + " 2 ", + " 3 ", ) ); } @@ -232,11 +232,20 @@ fn table_collapse_hearts() { } #[test] -fn table_collapse_doesnot_support_width_control() { +fn table_collapse_does_wrapping_for_long_strings() { let actual = nu!( r#"[[a]; [11111111111111111111111111111111111111111111111111111111111111111111111111111111]] | table --collapse"# ); - assert_eq!(actual.out, "Couldn't fit table into 80 columns!"); + assert_eq!( + actual.out, + "╭────────────────────────────────╮\ + │ a │\ + ├────────────────────────────────┤\ + │ 111111111111111109312339230430 │\ + │ 179149313814687359833671239329 │\ + │ 01313323321729744896.0000 │\ + ╰────────────────────────────────╯" + ); } #[test] @@ -1795,6 +1804,526 @@ fn table_expande_with_no_header_internally_1() { ); } +#[test] +fn test_collapse_big_0() { + Playground::setup("test_expand_big_0", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + authors = ["The Nushell Project Developers"] + default-run = "nu" + description = "A new type of shell" + documentation = "https://www.nushell.sh/book/" + edition = "2021" + exclude = ["images"] + homepage = "https://www.nushell.sh" + license = "MIT" + name = "nu" + repository = "https://github.com/nushell/nushell" + rust-version = "1.60" + version = "0.74.1" + + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + [package.metadata.binstall] + pkg-url = "{ repo }/releases/download/{ version }/{ name }-{ version }-{ target }.{ archive-format }" + pkg-fmt = "tgz" + + [package.metadata.binstall.overrides.x86_64-pc-windows-msvc] + pkg-fmt = "zip" + + [workspace] + members = [ + "crates/nu-cli", + "crates/nu-engine", + "crates/nu-parser", + "crates/nu-system", + "crates/nu-command", + "crates/nu-protocol", + "crates/nu-plugin", + "crates/nu_plugin_inc", + "crates/nu_plugin_gstat", + "crates/nu_plugin_example", + "crates/nu_plugin_query", + "crates/nu_plugin_custom_values", + "crates/nu-utils", + ] + + [dependencies] + chrono = { version = "0.4.23", features = ["serde"] } + crossterm = "0.24.0" + ctrlc = "3.2.1" + log = "0.4" + miette = { version = "5.5.0", features = ["fancy-no-backtrace"] } + nu-ansi-term = "0.46.0" + nu-cli = { path = "./crates/nu-cli", version = "0.74.1" } + nu-engine = { path = "./crates/nu-engine", version = "0.74.1" } + reedline = { version = "0.14.0", features = ["bashisms", "sqlite"] } + + rayon = "1.6.1" + is_executable = "1.0.1" + simplelog = "0.12.0" + time = "0.3.12" + + [target.'cfg(not(target_os = "windows"))'.dependencies] + # Our dependencies don't use OpenSSL on Windows + openssl = { version = "0.10.38", features = ["vendored"], optional = true } + signal-hook = { version = "0.3.14", default-features = false } + + + [target.'cfg(windows)'.build-dependencies] + winres = "0.1" + + [target.'cfg(target_family = "unix")'.dependencies] + nix = { version = "0.25", default-features = false, features = ["signal", "process", "fs", "term"] } + atty = "0.2" + + [dev-dependencies] + nu-test-support = { path = "./crates/nu-test-support", version = "0.74.1" } + tempfile = "3.2.0" + assert_cmd = "2.0.2" + criterion = "0.4" + pretty_assertions = "1.0.0" + serial_test = "0.10.0" + hamcrest2 = "0.3.0" + rstest = { version = "0.15.0", default-features = false } + itertools = "0.10.3" + + [features] + plugin = [ + "nu-plugin", + "nu-cli/plugin", + "nu-parser/plugin", + "nu-command/plugin", + "nu-protocol/plugin", + "nu-engine/plugin", + ] + # extra used to be more useful but now it's the same as default. Leaving it in for backcompat with existing build scripts + extra = ["default"] + default = ["plugin", "which-support", "trash-support", "sqlite"] + stable = ["default"] + wasi = [] + + # Enable to statically link OpenSSL; otherwise the system version will be used. Not enabled by default because it takes a while to build + static-link-openssl = ["dep:openssl"] + + # Stable (Default) + which-support = ["nu-command/which-support"] + trash-support = ["nu-command/trash-support"] + + # Main nu binary + [[bin]] + name = "nu" + path = "src/main.rs" + + # To use a development version of a dependency please use a global override here + # changing versions in each sub-crate of the workspace is tedious + [patch.crates-io] + reedline = { git = "https://github.com/nushell/reedline.git", branch = "main" } + + # Criterion benchmarking setup + # Run all benchmarks with `cargo bench` + # Run individual benchmarks like `cargo bench -- ` e.g. `cargo bench -- parse` + [[bench]] + name = "benchmarks" + harness = false + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + "open sample.toml | table --collapse" + )); + + _print_lines(&actual.out, 80); + + let expected = join_lines([ + "╭──────────────────┬─────────┬─────────────────────────────────────────────────╮", + "│ bench │ harness │ name │", + "│ ├─────────┼─────────────────────────────────────────────────┤", + "│ │ false │ benchmarks │", + "├──────────────────┼──────┬──┴─────────────────────────────────────────────────┤", + "│ bin │ name │ path │", + "│ ├──────┼────────────────────────────────────────────────────┤", + "│ │ nu │ src/main.rs │", + "├──────────────────┼──────┴────────┬──────────┬────────────────────────────────┤", + "│ dependencies │ chrono │ features │ serde │", + "│ │ ├──────────┼────────────────────────────────┤", + "│ │ │ version │ 0.4.23 │", + "│ ├───────────────┼──────────┴────────────────────────────────┤", + "│ │ crossterm │ 0.24.0 │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ ctrlc │ 3.2.1 │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ is_executable │ 1.0.1 │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ log │ 0.4 │", + "│ ├───────────────┼──────────┬────────────────────────────────┤", + "│ │ miette │ features │ fancy-no-backtrace │", + "│ │ ├──────────┼────────────────────────────────┤", + "│ │ │ version │ 5.5.0 │", + "│ ├───────────────┼──────────┴────────────────────────────────┤", + "│ │ nu-ansi-term │ 0.46.0 │", + "│ ├───────────────┼─────────┬─────────────────────────────────┤", + "│ │ nu-cli │ path │ ./crates/nu-cli │", + "│ │ ├─────────┼─────────────────────────────────┤", + "│ │ │ version │ 0.74.1 │", + "│ ├───────────────┼─────────┼─────────────────────────────────┤", + "│ │ nu-engine │ path │ ./crates/nu-engine │", + "│ │ ├─────────┼─────────────────────────────────┤", + "│ │ │ version │ 0.74.1 │", + "│ ├───────────────┼─────────┴─────────────────────────────────┤", + "│ │ rayon │ 1.6.1 │", + "│ ├───────────────┼──────────┬────────────────────────────────┤", + "│ │ reedline │ features │ bashisms │", + "│ │ │ ├────────────────────────────────┤", + "│ │ │ │ sqlite │", + "│ │ ├──────────┼────────────────────────────────┤", + "│ │ │ version │ 0.14.0 │", + "│ ├───────────────┼──────────┴────────────────────────────────┤", + "│ │ simplelog │ 0.12.0 │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ time │ 0.3.12 │", + "├──────────────────┼───────────────┴───┬───────────────────────────────────────┤", + "│ dev-dependencies │ assert_cmd │ 2.0.2 │", + "│ ├───────────────────┼───────────────────────────────────────┤", + "│ │ criterion │ 0.4 │", + "│ ├───────────────────┼───────────────────────────────────────┤", + "│ │ hamcrest2 │ 0.3.0 │", + "│ ├───────────────────┼───────────────────────────────────────┤", + "│ │ itertools │ 0.10.3 │", + "│ ├───────────────────┼─────────┬─────────────────────────────┤", + "│ │ nu-test-support │ path │ ./crates/nu-test-support │", + "│ │ ├─────────┼─────────────────────────────┤", + "│ │ │ version │ 0.74.1 │", + "│ ├───────────────────┼─────────┴─────────────────────────────┤", + "│ │ pretty_assertions │ 1.0.0 │", + "│ ├───────────────────┼──────────────────┬────────────────────┤", + "│ │ rstest │ default-features │ false │", + "│ │ ├──────────────────┼────────────────────┤", + "│ │ │ version │ 0.15.0 │", + "│ ├───────────────────┼──────────────────┴────────────────────┤", + "│ │ serial_test │ 0.10.0 │", + "│ ├───────────────────┼───────────────────────────────────────┤", + "│ │ tempfile │ 3.2.0 │", + "├──────────────────┼───────────────────┴─┬─────────────────────────────────────┤", + "│ features │ default │ plugin │", + "│ │ ├─────────────────────────────────────┤", + "│ │ │ which-support │", + "│ │ ├─────────────────────────────────────┤", + "│ │ │ trash-support │", + "│ │ ├─────────────────────────────────────┤", + "│ │ │ sqlite │", + "│ ├─────────────────────┼─────────────────────────────────────┤", + "│ │ extra │ default │", + "│ ├─────────────────────┼─────────────────────────────────────┤", + "│ │ plugin │ nu-plugin │", + "│ │ ├─────────────────────────────────────┤", + "│ │ │ nu-cli/plugin │", + "│ │ ├─────────────────────────────────────┤", + "│ │ │ nu-parser/plugin │", + "│ │ ├─────────────────────────────────────┤", + "│ │ │ nu-command/plugin │", + "│ │ ├─────────────────────────────────────┤", + "│ │ │ nu-protocol/plugin │", + "│ │ ├─────────────────────────────────────┤", + "│ │ │ nu-engine/plugin │", + "│ ├─────────────────────┼─────────────────────────────────────┤", + "│ │ stable │ default │", + "│ ├─────────────────────┼─────────────────────────────────────┤", + "│ │ static-link-openssl │ dep:openssl │", + "│ ├─────────────────────┼─────────────────────────────────────┤", + "│ │ trash-support │ nu-command/trash-support │", + "│ ├─────────────────────┼─────────────────────────────────────┤", + "│ │ wasi │ │", + "│ ├─────────────────────┼─────────────────────────────────────┤", + "│ │ which-support │ nu-command/which-support │", + "├──────────────────┼───────────────┬─────┴─────────────────────────────────────┤", + "│ package │ authors │ The Nushell Project Developers │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ default-run │ nu │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ description │ A new type of shell │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ documentation │ https://www.nushell.sh/book/ │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ edition │ 2021 │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ exclude │ images │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ homepage │ https://www.nushell.sh │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ license │ MIT │", + "│ ├───────────────┼──────────┬───────────┬────────────────────┤", + "│ │ metadata │ binstall │ overrides │ ... │", + "│ │ │ ├───────────┼────────────────────┤", + "│ │ │ │ pkg-fmt │ tgz │", + "│ │ │ ├───────────┼────────────────────┤", + "│ │ │ │ pkg-url │ { repo }/releases/ │", + "│ │ │ │ │ download/{ v │", + "│ │ │ │ │ ersion │", + "│ │ │ │ │ }/{ name }-{ vers │", + "│ │ │ │ │ ion }- │", + "│ │ │ │ │ { target }.{ │", + "│ │ │ │ │ archive-format } │", + "│ ├───────────────┼──────────┴───────────┴────────────────────┤", + "│ │ name │ nu │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ repository │ https://github.com/nushell/nushell │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ rust-version │ 1.60 │", + "│ ├───────────────┼───────────────────────────────────────────┤", + "│ │ version │ 0.74.1 │", + "├──────────────────┼───────────┬───┴──────┬────────┬───────────────────────────┤", + "│ patch │ crates-io │ reedline │ branch │ main │", + "│ │ │ ├────────┼───────────────────────────┤", + "│ │ │ │ git │ https://github.com/nushel │", + "│ │ │ │ │ l/reedline.git │", + "├──────────────────┼───────────┴──────────┴────────┴─┬──────────────┬──────────┤", + "│ target │ cfg(not(target_os = \"windows\")) │ dependencies │ ... │", + "│ │ │ ├──────────┤", + "│ │ │ │ ... │", + "│ ├─────────────────────────────────┼──────────────┼──────────┤", + "│ │ cfg(target_family = \"unix\") │ dependencies │ ... │", + "│ │ │ ├──────────┤", + "│ │ │ │ ... │", + "│ ├─────────────────────────────────┼──────────────┴──────────┤", + "│ │ cfg(windows) │ ... │", + "├──────────────────┼─────────┬───────────────────────┴─────────────────────────┤", + "│ workspace │ members │ crates/nu-cli │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu-engine │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu-parser │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu-system │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu-command │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu-protocol │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu-plugin │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_inc │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_gstat │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_example │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_query │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_custom_values │", + "│ │ ├─────────────────────────────────────────────────┤", + "│ │ │ crates/nu-utils │", + "╰──────────────────┴─────────┴─────────────────────────────────────────────────╯", + ]); + + assert_eq!(actual.out, expected); + + let actual = nu!( + cwd: dirs.test(), pipeline( + "open sample.toml | table --collapse --width=160" + )); + + _print_lines(&actual.out, 111); + + let expected = join_lines([ + "╭──────────────────┬─────────┬────────────────────────────────────────────────────────────────────────────────╮", + "│ bench │ harness │ name │", + "│ ├─────────┼────────────────────────────────────────────────────────────────────────────────┤", + "│ │ false │ benchmarks │", + "├──────────────────┼──────┬──┴────────────────────────────────────────────────────────────────────────────────┤", + "│ bin │ name │ path │", + "│ ├──────┼───────────────────────────────────────────────────────────────────────────────────┤", + "│ │ nu │ src/main.rs │", + "├──────────────────┼──────┴────────┬──────────┬───────────────────────────────────────────────────────────────┤", + "│ dependencies │ chrono │ features │ serde │", + "│ │ ├──────────┼───────────────────────────────────────────────────────────────┤", + "│ │ │ version │ 0.4.23 │", + "│ ├───────────────┼──────────┴───────────────────────────────────────────────────────────────┤", + "│ │ crossterm │ 0.24.0 │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ ctrlc │ 3.2.1 │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ is_executable │ 1.0.1 │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ log │ 0.4 │", + "│ ├───────────────┼──────────┬───────────────────────────────────────────────────────────────┤", + "│ │ miette │ features │ fancy-no-backtrace │", + "│ │ ├──────────┼───────────────────────────────────────────────────────────────┤", + "│ │ │ version │ 5.5.0 │", + "│ ├───────────────┼──────────┴───────────────────────────────────────────────────────────────┤", + "│ │ nu-ansi-term │ 0.46.0 │", + "│ ├───────────────┼─────────┬────────────────────────────────────────────────────────────────┤", + "│ │ nu-cli │ path │ ./crates/nu-cli │", + "│ │ ├─────────┼────────────────────────────────────────────────────────────────┤", + "│ │ │ version │ 0.74.1 │", + "│ ├───────────────┼─────────┼────────────────────────────────────────────────────────────────┤", + "│ │ nu-engine │ path │ ./crates/nu-engine │", + "│ │ ├─────────┼────────────────────────────────────────────────────────────────┤", + "│ │ │ version │ 0.74.1 │", + "│ ├───────────────┼─────────┴────────────────────────────────────────────────────────────────┤", + "│ │ rayon │ 1.6.1 │", + "│ ├───────────────┼──────────┬───────────────────────────────────────────────────────────────┤", + "│ │ reedline │ features │ bashisms │", + "│ │ │ ├───────────────────────────────────────────────────────────────┤", + "│ │ │ │ sqlite │", + "│ │ ├──────────┼───────────────────────────────────────────────────────────────┤", + "│ │ │ version │ 0.14.0 │", + "│ ├───────────────┼──────────┴───────────────────────────────────────────────────────────────┤", + "│ │ simplelog │ 0.12.0 │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ time │ 0.3.12 │", + "├──────────────────┼───────────────┴───┬──────────────────────────────────────────────────────────────────────┤", + "│ dev-dependencies │ assert_cmd │ 2.0.2 │", + "│ ├───────────────────┼──────────────────────────────────────────────────────────────────────┤", + "│ │ criterion │ 0.4 │", + "│ ├───────────────────┼──────────────────────────────────────────────────────────────────────┤", + "│ │ hamcrest2 │ 0.3.0 │", + "│ ├───────────────────┼──────────────────────────────────────────────────────────────────────┤", + "│ │ itertools │ 0.10.3 │", + "│ ├───────────────────┼─────────┬────────────────────────────────────────────────────────────┤", + "│ │ nu-test-support │ path │ ./crates/nu-test-support │", + "│ │ ├─────────┼────────────────────────────────────────────────────────────┤", + "│ │ │ version │ 0.74.1 │", + "│ ├───────────────────┼─────────┴────────────────────────────────────────────────────────────┤", + "│ │ pretty_assertions │ 1.0.0 │", + "│ ├───────────────────┼──────────────────┬───────────────────────────────────────────────────┤", + "│ │ rstest │ default-features │ false │", + "│ │ ├──────────────────┼───────────────────────────────────────────────────┤", + "│ │ │ version │ 0.15.0 │", + "│ ├───────────────────┼──────────────────┴───────────────────────────────────────────────────┤", + "│ │ serial_test │ 0.10.0 │", + "│ ├───────────────────┼──────────────────────────────────────────────────────────────────────┤", + "│ │ tempfile │ 3.2.0 │", + "├──────────────────┼───────────────────┴─┬────────────────────────────────────────────────────────────────────┤", + "│ features │ default │ plugin │", + "│ │ ├────────────────────────────────────────────────────────────────────┤", + "│ │ │ which-support │", + "│ │ ├────────────────────────────────────────────────────────────────────┤", + "│ │ │ trash-support │", + "│ │ ├────────────────────────────────────────────────────────────────────┤", + "│ │ │ sqlite │", + "│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤", + "│ │ extra │ default │", + "│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤", + "│ │ plugin │ nu-plugin │", + "│ │ ├────────────────────────────────────────────────────────────────────┤", + "│ │ │ nu-cli/plugin │", + "│ │ ├────────────────────────────────────────────────────────────────────┤", + "│ │ │ nu-parser/plugin │", + "│ │ ├────────────────────────────────────────────────────────────────────┤", + "│ │ │ nu-command/plugin │", + "│ │ ├────────────────────────────────────────────────────────────────────┤", + "│ │ │ nu-protocol/plugin │", + "│ │ ├────────────────────────────────────────────────────────────────────┤", + "│ │ │ nu-engine/plugin │", + "│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤", + "│ │ stable │ default │", + "│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤", + "│ │ static-link-openssl │ dep:openssl │", + "│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤", + "│ │ trash-support │ nu-command/trash-support │", + "│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤", + "│ │ wasi │ │", + "│ ├─────────────────────┼────────────────────────────────────────────────────────────────────┤", + "│ │ which-support │ nu-command/which-support │", + "├──────────────────┼───────────────┬─────┴────────────────────────────────────────────────────────────────────┤", + "│ package │ authors │ The Nushell Project Developers │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ default-run │ nu │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ description │ A new type of shell │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ documentation │ https://www.nushell.sh/book/ │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ edition │ 2021 │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ exclude │ images │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ homepage │ https://www.nushell.sh │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ license │ MIT │", + "│ ├───────────────┼──────────┬───────────┬────────────────────────┬─────────┬────────────────┤", + "│ │ metadata │ binstall │ overrides │ x86_64-pc-windows-msvc │ pkg-fmt │ zip │", + "│ │ │ ├───────────┼────────────────────────┴─────────┴────────────────┤", + "│ │ │ │ pkg-fmt │ tgz │", + "│ │ │ ├───────────┼───────────────────────────────────────────────────┤", + "│ │ │ │ pkg-url │ { repo }/releases/download/{ v │", + "│ │ │ │ │ ersion }/{ name }-{ version }- │", + "│ │ │ │ │ { target }.{ archive-format } │", + "│ ├───────────────┼──────────┴───────────┴───────────────────────────────────────────────────┤", + "│ │ name │ nu │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ repository │ https://github.com/nushell/nushell │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ rust-version │ 1.60 │", + "│ ├───────────────┼──────────────────────────────────────────────────────────────────────────┤", + "│ │ version │ 0.74.1 │", + "├──────────────────┼───────────┬───┴──────┬────────┬──────────────────────────────────────────────────────────┤", + "│ patch │ crates-io │ reedline │ branch │ main │", + "│ │ │ ├────────┼──────────────────────────────────────────────────────────┤", + "│ │ │ │ git │ https://github.com/nushell/reedline.git │", + "├──────────────────┼───────────┴──────────┴────────┴─┬──────────────┬─────────────┬──────────┬────────────────┤", + "│ target │ cfg(not(target_os = \"windows\")) │ dependencies │ openssl │ features │ vendored │", + "│ │ │ │ ├──────────┼────────────────┤", + "│ │ │ │ │ optional │ true │", + "│ │ │ │ ├──────────┼────────────────┤", + "│ │ │ │ │ version │ 0.10.38 │", + "│ │ │ ├─────────────┼──────────┴───────┬────────┤", + "│ │ │ │ signal-hook │ default-features │ false │", + "│ │ │ │ ├──────────────────┼────────┤", + "│ │ │ │ │ version │ 0.3.14 │", + "│ ├─────────────────────────────────┼──────────────┼──────┬──────┴──────────────────┴────────┤", + "│ │ cfg(target_family = \"unix\") │ dependencies │ atty │ 0.2 │", + "│ │ │ ├──────┼──────────────────┬───────────────┤", + "│ │ │ │ nix │ default-features │ false │", + "│ │ │ │ ├──────────────────┼───────────────┤", + "│ │ │ │ │ features │ signal │", + "│ │ │ │ │ ├───────────────┤", + "│ │ │ │ │ │ process │", + "│ │ │ │ │ ├───────────────┤", + "│ │ │ │ │ │ fs │", + "│ │ │ │ │ ├───────────────┤", + "│ │ │ │ │ │ term │", + "│ │ │ │ ├──────────────────┼───────────────┤", + "│ │ │ │ │ version │ 0.25 │", + "│ ├─────────────────────────────────┼──────────────┴─────┬┴───────┬──────────┴───────────────┤", + "│ │ cfg(windows) │ build-dependencies │ winres │ 0.1 │", + "├──────────────────┼─────────┬───────────────────────┴────────────────────┴────────┴──────────────────────────┤", + "│ workspace │ members │ crates/nu-cli │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu-engine │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu-parser │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu-system │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu-command │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu-protocol │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu-plugin │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_inc │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_gstat │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_example │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_query │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu_plugin_custom_values │", + "│ │ ├────────────────────────────────────────────────────────────────────────────────┤", + "│ │ │ crates/nu-utils │", + "╰──────────────────┴─────────┴────────────────────────────────────────────────────────────────────────────────╯", + ]); + + assert_eq!(actual.out, expected); + }) +} + fn join_lines(lines: impl IntoIterator>) -> String { lines .into_iter() diff --git a/crates/nu-engine/src/column.rs b/crates/nu-engine/src/column.rs index 6d4fa29984..aba520672d 100644 --- a/crates/nu-engine/src/column.rs +++ b/crates/nu-engine/src/column.rs @@ -1,9 +1,8 @@ use nu_protocol::Value; use std::collections::HashSet; -pub fn get_columns<'a>(input: impl IntoIterator) -> Vec { +pub fn get_columns(input: &[Value]) -> Vec { let mut columns = vec![]; - for item in input { let Value::Record { cols, .. } = item else { return vec![]; diff --git a/crates/nu-explore/src/nu_common/table.rs b/crates/nu-explore/src/nu_common/table.rs index a033528053..ead93f4034 100644 --- a/crates/nu-explore/src/nu_common/table.rs +++ b/crates/nu-explore/src/nu_common/table.rs @@ -1,17 +1,9 @@ -use nu_color_config::{Alignment, StyleComputer, TextStyle}; -use nu_engine::column::get_columns; -use nu_protocol::{ast::PathMember, Config, ShellError, Span, TableIndexMode, Value}; -use nu_protocol::{FooterMode, TrimStrategy}; -use nu_table::{string_width, Table as NuTable, TableConfig, TableTheme}; +use nu_color_config::StyleComputer; +use nu_protocol::{Span, Value}; +use nu_table::{value_to_clean_styled_string, value_to_styled_string, BuildConfig, ExpandedTable}; +use std::sync::atomic::AtomicBool; use std::sync::Arc; -use std::{ - cmp::max, - sync::atomic::{AtomicBool, Ordering}, -}; -const INDEX_COLUMN_NAME: &str = "index"; - -type NuText = (String, TextStyle); use crate::nu_common::NuConfig; pub fn try_build_table( @@ -21,10 +13,13 @@ pub fn try_build_table( value: Value, ) -> String { match value { - Value::List { vals, span } => try_build_list(vals, &ctrlc, config, span, style_computer), + Value::List { vals, span } => try_build_list(vals, ctrlc, config, span, style_computer), Value::Record { cols, vals, span } => { try_build_map(cols, vals, span, style_computer, ctrlc, config) } + val if matches!(val, Value::String { .. }) => { + value_to_clean_styled_string(&val, config, style_computer).0 + } val => value_to_styled_string(&val, config, style_computer).0, } } @@ -37,18 +32,8 @@ fn try_build_map( ctrlc: Option>, config: &NuConfig, ) -> String { - let result = build_expanded_table( - cols.clone(), - vals.clone(), - span, - ctrlc, - config, - style_computer, - usize::MAX, - None, - false, - "", - ); + let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX); + let result = ExpandedTable::new(None, false, String::new()).build_map(&cols, &vals, opts); match result { Ok(Some(result)) => result, Ok(None) | Err(_) => { @@ -59,896 +44,18 @@ fn try_build_map( fn try_build_list( vals: Vec, - ctrlc: &Option>, + ctrlc: Option>, config: &NuConfig, span: Span, style_computer: &StyleComputer, ) -> String { - let table = convert_to_table2( - 0, - vals.iter(), - ctrlc.clone(), - config, - span, - style_computer, - None, - false, - "", - usize::MAX, - ); - match table { - Ok(Some((table, with_header, with_index))) => { - let table_config = create_table_config( - config, - style_computer, - table.count_rows(), - with_header, - with_index, - false, - ); - - table.draw(table_config, usize::MAX).unwrap_or_else(|| { - value_to_styled_string(&Value::List { vals, span }, config, style_computer).0 - }) - } + let opts = BuildConfig::new(ctrlc, config, style_computer, Span::unknown(), usize::MAX); + let result = ExpandedTable::new(None, false, String::new()).build_list(&vals, opts); + match result { + Ok(Some(out)) => out, Ok(None) | Err(_) => { // it means that the list is empty value_to_styled_string(&Value::List { vals, span }, config, style_computer).0 } } } - -#[allow(clippy::too_many_arguments)] -fn build_expanded_table( - cols: Vec, - vals: Vec, - span: Span, - ctrlc: Option>, - config: &Config, - style_computer: &StyleComputer, - term_width: usize, - expand_limit: Option, - flatten: bool, - flatten_sep: &str, -) -> Result, ShellError> { - let theme = load_theme_from_config(config); - - // calculate the width of a key part + the rest of table so we know the rest of the table width available for value. - let key_width = cols.iter().map(|col| string_width(col)).max().unwrap_or(0); - let key = NuTable::create_cell(" ".repeat(key_width), TextStyle::default()); - let key_table = NuTable::new(vec![vec![key]], (1, 2)); - let key_width = key_table - .draw( - create_table_config(config, style_computer, 1, false, false, false), - usize::MAX, - ) - .map(|table| string_width(&table)) - .unwrap_or(0); - - // 3 - count borders (left, center, right) - // 2 - padding - if key_width + 3 + 2 > term_width { - return Ok(None); - } - - let remaining_width = term_width - key_width - 3 - 2; - - let mut data = Vec::with_capacity(cols.len()); - for (key, value) in cols.into_iter().zip(vals) { - // handle CTRLC event - if let Some(ctrlc) = &ctrlc { - if ctrlc.load(Ordering::SeqCst) { - return Ok(None); - } - } - - let is_limited = matches!(expand_limit, Some(0)); - let mut is_expanded = false; - let value = if is_limited { - value_to_styled_string(&value, config, style_computer).0 - } else { - let deep = expand_limit.map(|i| i - 1); - - match value { - Value::List { vals, .. } => { - let table = convert_to_table2( - 0, - vals.iter(), - ctrlc.clone(), - config, - span, - style_computer, - deep, - flatten, - flatten_sep, - remaining_width, - )?; - - match table { - Some((mut table, with_header, with_index)) => { - // control width via removing table columns. - let theme = load_theme_from_config(config); - table.truncate(remaining_width, &theme); - - is_expanded = true; - - let table_config = create_table_config( - config, - style_computer, - table.count_rows(), - with_header, - with_index, - false, - ); - - let val = table.draw(table_config, remaining_width); - match val { - Some(result) => result, - None => return Ok(None), - } - } - None => { - // it means that the list is empty - let value = Value::List { vals, span }; - value_to_styled_string(&value, config, style_computer).0 - } - } - } - Value::Record { cols, vals, span } => { - let result = build_expanded_table( - cols.clone(), - vals.clone(), - span, - ctrlc.clone(), - config, - style_computer, - remaining_width, - deep, - flatten, - flatten_sep, - )?; - - match result { - Some(result) => { - is_expanded = true; - result - } - None => { - let failed_value = value_to_styled_string( - &Value::Record { cols, vals, span }, - config, - style_computer, - ); - - wrap_nu_text(failed_value, remaining_width, config).0 - } - } - } - val => { - let text = value_to_styled_string(&val, config, style_computer).0; - wrap_nu_text((text, TextStyle::default()), remaining_width, config).0 - } - } - }; - - // we want to have a key being aligned to 2nd line, - // 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; - if !key.is_empty() && is_expanded && theme.has_top_line() { - key.insert(0, '\n'); - } - - let key = NuTable::create_cell(key, TextStyle::default_field()); - let val = NuTable::create_cell(value, TextStyle::default()); - - let row = vec![key, val]; - data.push(row); - } - - let table_config = create_table_config(config, style_computer, data.len(), false, false, false); - - let data_len = data.len(); - let table = NuTable::new(data, (data_len, 2)); - - let table_s = table.clone().draw(table_config.clone(), term_width); - - let table = match table_s { - Some(s) => { - // check whether we need to expand table or not, - // todo: we can make it more effitient - - const EXPAND_THRESHOLD: f32 = 0.80; - - let width = string_width(&s); - let used_percent = width as f32 / term_width as f32; - - if width < term_width && used_percent > EXPAND_THRESHOLD { - let table_config = table_config.expand(); - table.draw(table_config, term_width) - } else { - Some(s) - } - } - None => None, - }; - - Ok(table) -} - -#[allow(clippy::too_many_arguments)] -#[allow(clippy::into_iter_on_ref)] -fn convert_to_table2<'a>( - row_offset: usize, - input: impl Iterator + ExactSizeIterator + Clone, - ctrlc: Option>, - config: &Config, - head: Span, - style_computer: &StyleComputer, - deep: Option, - flatten: bool, - flatten_sep: &str, - available_width: usize, -) -> Result, ShellError> { - const PADDING_SPACE: usize = 2; - const SPLIT_LINE_SPACE: usize = 1; - const ADDITIONAL_CELL_SPACE: usize = PADDING_SPACE + SPLIT_LINE_SPACE; - const TRUNCATE_CELL_WIDTH: usize = 3; - const MIN_CELL_CONTENT_WIDTH: usize = 1; - const OK_CELL_CONTENT_WIDTH: usize = 25; - - if input.len() == 0 { - return Ok(None); - } - - // 2 - split lines - let mut available_width = available_width.saturating_sub(SPLIT_LINE_SPACE + SPLIT_LINE_SPACE); - if available_width < MIN_CELL_CONTENT_WIDTH { - return Ok(None); - } - - let headers = get_columns(input.clone()); - - let with_index = match config.table_index_mode { - TableIndexMode::Always => true, - TableIndexMode::Never => false, - TableIndexMode::Auto => headers.iter().any(|header| header == INDEX_COLUMN_NAME), - }; - - // The header with the INDEX is removed from the table headers since - // it is added to the natural table index - let headers: Vec<_> = headers - .into_iter() - .filter(|header| header != INDEX_COLUMN_NAME) - .collect(); - - let with_header = !headers.is_empty(); - - let mut data = vec![vec![]; input.len()]; - if !headers.is_empty() { - data.push(vec![]); - }; - - if with_index { - let mut column_width = 0; - - if with_header { - data[0].push(NuTable::create_cell( - "#", - header_style(style_computer, String::from("#")), - )); - } - - for (row, item) in input.clone().enumerate() { - if let Some(ctrlc) = &ctrlc { - if ctrlc.load(Ordering::SeqCst) { - return Ok(None); - } - } - - if let Value::Error { error } = item { - return Err(*error.clone()); - } - - let index = row + row_offset; - let text = matches!(item, Value::Record { .. }) - .then(|| lookup_index_value(item, config).unwrap_or_else(|| index.to_string())) - .unwrap_or_else(|| index.to_string()); - - let value = make_index_string(text, style_computer); - - let width = string_width(&value.0); - column_width = max(column_width, width); - - let value = NuTable::create_cell(value.0, value.1); - - let row = if with_header { row + 1 } else { row }; - data[row].push(value); - } - - if column_width + ADDITIONAL_CELL_SPACE > available_width { - available_width = 0; - } else { - available_width -= column_width + ADDITIONAL_CELL_SPACE; - } - } - - if !with_header { - for (row, item) in input.into_iter().enumerate() { - if let Some(ctrlc) = &ctrlc { - if ctrlc.load(Ordering::SeqCst) { - return Ok(None); - } - } - - if let Value::Error { error } = item { - return Err(*error.clone()); - } - - let value = convert_to_table2_entry( - item, - config, - &ctrlc, - style_computer, - deep, - flatten, - flatten_sep, - available_width, - ); - - let value = NuTable::create_cell(value.0, value.1); - data[row].push(value); - } - - let count_columns = if with_index { 2 } else { 1 }; - let size = (data.len(), count_columns); - - let table = NuTable::new(data, size); - - return Ok(Some((table, with_header, with_index))); - } - - let mut widths = Vec::new(); - let mut truncate = false; - let count_columns = headers.len(); - for (col, header) in headers.into_iter().enumerate() { - let is_last_col = col + 1 == count_columns; - - let mut necessary_space = PADDING_SPACE; - if !is_last_col { - necessary_space += SPLIT_LINE_SPACE; - } - - if available_width == 0 || available_width <= necessary_space { - // MUST NEVER HAPPEN (ideally) - // but it does... - - truncate = true; - break; - } - - available_width -= necessary_space; - - let mut column_width = string_width(&header); - - data[0].push(NuTable::create_cell( - &header, - header_style(style_computer, header.clone()), - )); - - for (row, item) in input.clone().enumerate() { - if let Some(ctrlc) = &ctrlc { - if ctrlc.load(Ordering::SeqCst) { - return Ok(None); - } - } - - if let Value::Error { error } = item { - return Err(*error.clone()); - } - - let value = create_table2_entry( - item, - &header, - head, - config, - &ctrlc, - style_computer, - deep, - flatten, - flatten_sep, - available_width, - ); - - let value_width = string_width(&value.0); - column_width = max(column_width, value_width); - - let value = NuTable::create_cell(value.0, value.1); - - data[row + 1].push(value); - } - - if column_width >= available_width - || (!is_last_col && column_width + necessary_space >= available_width) - { - // so we try to do soft landing - // by doing a truncating in case there will be enough space for it. - - column_width = string_width(&header); - - for (row, item) in input.clone().enumerate() { - if let Some(ctrlc) = &ctrlc { - if ctrlc.load(Ordering::SeqCst) { - return Ok(None); - } - } - - let value = create_table2_entry_basic(item, &header, head, config, style_computer); - let value = wrap_nu_text(value, available_width, config); - - let value_width = string_width(&value.0); - column_width = max(column_width, value_width); - - let value = NuTable::create_cell(value.0, value.1); - - *data[row + 1].last_mut().expect("unwrap") = value; - } - } - - let is_suitable_for_wrap = - available_width >= string_width(&header) && available_width >= OK_CELL_CONTENT_WIDTH; - if column_width >= available_width && is_suitable_for_wrap { - // so we try to do soft landing ONCE AGAIN - // but including a wrap - - column_width = string_width(&header); - - for (row, item) in input.clone().enumerate() { - if let Some(ctrlc) = &ctrlc { - if ctrlc.load(Ordering::SeqCst) { - return Ok(None); - } - } - - let value = create_table2_entry_basic(item, &header, head, config, style_computer); - let value = wrap_nu_text(value, OK_CELL_CONTENT_WIDTH, config); - - let value = NuTable::create_cell(value.0, value.1); - - *data[row + 1].last_mut().expect("unwrap") = value; - } - } - - if column_width > available_width { - // remove just added column - for row in &mut data { - row.pop(); - } - - available_width += necessary_space; - - truncate = true; - break; - } - - available_width -= column_width; - widths.push(column_width); - } - - if truncate { - if available_width <= TRUNCATE_CELL_WIDTH + PADDING_SPACE { - // back up by removing last column. - // it's ALWAYS MUST has us enough space for a shift column - while let Some(width) = widths.pop() { - for row in &mut data { - row.pop(); - } - - available_width += width + PADDING_SPACE + SPLIT_LINE_SPACE; - - if available_width > TRUNCATE_CELL_WIDTH + PADDING_SPACE { - break; - } - } - } - - // this must be a RARE case or even NEVER happen, - // but we do check it just in case. - if widths.is_empty() { - return Ok(None); - } - - let shift = NuTable::create_cell(String::from("..."), TextStyle::default()); - for row in &mut data { - row.push(shift.clone()); - } - - widths.push(3); - } - - let count_columns = widths.len() + with_index as usize; - let count_rows = data.len(); - let size = (count_rows, count_columns); - - let table = NuTable::new(data, size); - - Ok(Some((table, with_header, with_index))) -} - -fn lookup_index_value(item: &Value, config: &Config) -> Option { - item.get_data_by_key(INDEX_COLUMN_NAME) - .map(|value| value.into_string("", config)) -} - -fn header_style(style_computer: &StyleComputer, header: String) -> TextStyle { - let style = style_computer.compute("header", &Value::string(header.as_str(), Span::unknown())); - TextStyle { - alignment: Alignment::Center, - color_style: Some(style), - } -} - -#[allow(clippy::too_many_arguments)] -fn create_table2_entry_basic( - item: &Value, - header: &str, - head: Span, - config: &Config, - style_computer: &StyleComputer, -) -> NuText { - match item { - Value::Record { .. } => { - let val = header.to_owned(); - let path = PathMember::String { - val, - span: head, - optional: false, - }; - let val = item.clone().follow_cell_path(&[path], false); - - match val { - Ok(val) => value_to_styled_string(&val, config, style_computer), - Err(_) => error_sign(style_computer), - } - } - _ => value_to_styled_string(item, config, style_computer), - } -} - -#[allow(clippy::too_many_arguments)] -fn create_table2_entry( - item: &Value, - header: &str, - head: Span, - config: &Config, - ctrlc: &Option>, - style_computer: &StyleComputer, - deep: Option, - flatten: bool, - flatten_sep: &str, - width: usize, -) -> NuText { - match item { - Value::Record { .. } => { - let val = header.to_owned(); - let path = PathMember::String { - val, - span: head, - optional: false, - }; - let val = item.clone().follow_cell_path(&[path], false); - - match val { - Ok(val) => convert_to_table2_entry( - &val, - config, - ctrlc, - style_computer, - deep, - flatten, - flatten_sep, - width, - ), - Err(_) => wrap_nu_text(error_sign(style_computer), width, config), - } - } - _ => convert_to_table2_entry( - item, - config, - ctrlc, - style_computer, - deep, - flatten, - flatten_sep, - width, - ), - } -} - -fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) { - make_styled_string(style_computer, String::from("❎"), None, 0) -} - -fn wrap_nu_text(mut text: NuText, width: usize, config: &Config) -> NuText { - text.0 = nu_table::string_wrap(&text.0, width, is_cfg_trim_keep_words(config)); - text -} - -#[allow(clippy::too_many_arguments)] -fn convert_to_table2_entry( - item: &Value, - config: &Config, - ctrlc: &Option>, - // This is passed in, even though it could be retrieved from config, - // to save reallocation (because it's presumably being used upstream). - style_computer: &StyleComputer, - deep: Option, - flatten: bool, - flatten_sep: &str, - width: usize, -) -> NuText { - let is_limit_reached = matches!(deep, Some(0)); - if is_limit_reached { - return wrap_nu_text( - value_to_styled_string(item, config, style_computer), - width, - config, - ); - } - - match &item { - Value::Record { span, cols, vals } => { - if cols.is_empty() && vals.is_empty() { - wrap_nu_text( - value_to_styled_string(item, config, style_computer), - width, - config, - ) - } else { - let table = convert_to_table2( - 0, - std::iter::once(item), - ctrlc.clone(), - config, - *span, - style_computer, - deep.map(|i| i - 1), - flatten, - flatten_sep, - width, - ); - - let inner_table = table.map(|table| { - table.and_then(|(table, with_header, with_index)| { - let table_config = create_table_config( - config, - style_computer, - table.count_rows(), - with_header, - with_index, - false, - ); - - table.draw(table_config, usize::MAX) - }) - }); - - if let Ok(Some(table)) = inner_table { - (table, TextStyle::default()) - } else { - // error so back down to the default - wrap_nu_text( - value_to_styled_string(item, config, style_computer), - width, - config, - ) - } - } - } - Value::List { vals, span } => { - let is_simple_list = vals - .iter() - .all(|v| !matches!(v, Value::Record { .. } | Value::List { .. })); - - if flatten && is_simple_list { - wrap_nu_text( - convert_value_list_to_string(vals, config, style_computer, flatten_sep), - width, - config, - ) - } else { - let table = convert_to_table2( - 0, - vals.iter(), - ctrlc.clone(), - config, - *span, - style_computer, - deep.map(|i| i - 1), - flatten, - flatten_sep, - width, - ); - - let inner_table = table.map(|table| { - table.and_then(|(table, with_header, with_index)| { - let table_config = create_table_config( - config, - style_computer, - table.count_rows(), - with_header, - with_index, - false, - ); - - table.draw(table_config, usize::MAX) - }) - }); - - if let Ok(Some(table)) = inner_table { - (table, TextStyle::default()) - } else { - // error so back down to the default - - wrap_nu_text( - value_to_styled_string(item, config, style_computer), - width, - config, - ) - } - } - } - _ => wrap_nu_text( - value_to_styled_string(item, config, style_computer), - width, - config, - ), // unknown type. - } -} - -fn convert_value_list_to_string( - vals: &[Value], - config: &Config, - // This is passed in, even though it could be retrieved from config, - // to save reallocation (because it's presumably being used upstream). - style_computer: &StyleComputer, - flatten_sep: &str, -) -> NuText { - let mut buf = Vec::new(); - for value in vals { - let (text, _) = value_to_styled_string(value, config, style_computer); - - buf.push(text); - } - let text = buf.join(flatten_sep); - (text, TextStyle::default()) -} - -fn value_to_styled_string( - value: &Value, - config: &Config, - // This is passed in, even though it could be retrieved from config, - // to save reallocation (because it's presumably being used upstream). - style_computer: &StyleComputer, -) -> NuText { - let float_precision = config.float_precision as usize; - make_styled_string( - style_computer, - value.into_abbreviated_string(config), - Some(value), - float_precision, - ) -} - -fn make_styled_string( - style_computer: &StyleComputer, - text: String, - value: Option<&Value>, // None represents table holes. - float_precision: usize, -) -> NuText { - match value { - Some(value) => { - match value { - Value::Float { .. } => { - // set dynamic precision from config - let precise_number = convert_with_precision(&text, float_precision) - .unwrap_or_else(|e| e.to_string()); - (precise_number, style_computer.style_primitive(value)) - } - _ => (text, style_computer.style_primitive(value)), - } - } - None => { - // Though holes are not the same as null, the closure for "empty" is passed a null anyway. - ( - text, - TextStyle::with_style( - Alignment::Center, - style_computer.compute("empty", &Value::nothing(Span::unknown())), - ), - ) - } - } -} - -fn make_index_string(text: String, style_computer: &StyleComputer) -> NuText { - let style = style_computer.compute("row_index", &Value::string(text.as_str(), Span::unknown())); - (text, TextStyle::with_style(Alignment::Right, style)) -} - -fn convert_with_precision(val: &str, precision: usize) -> Result { - // vall will always be a f64 so convert it with precision formatting - let val_float = match val.trim().parse::() { - Ok(f) => f, - Err(e) => { - return Err(ShellError::GenericError( - format!("error converting string [{}] to f64", &val), - "".to_string(), - None, - Some(e.to_string()), - Vec::new(), - )); - } - }; - Ok(format!("{val_float:.precision$}")) -} - -fn load_theme_from_config(config: &Config) -> TableTheme { - match config.table_mode.as_str() { - "basic" => nu_table::TableTheme::basic(), - "thin" => nu_table::TableTheme::thin(), - "light" => nu_table::TableTheme::light(), - "compact" => nu_table::TableTheme::compact(), - "with_love" => nu_table::TableTheme::with_love(), - "compact_double" => nu_table::TableTheme::compact_double(), - "rounded" => nu_table::TableTheme::rounded(), - "reinforced" => nu_table::TableTheme::reinforced(), - "heavy" => nu_table::TableTheme::heavy(), - "none" => nu_table::TableTheme::none(), - _ => nu_table::TableTheme::rounded(), - } -} - -fn create_table_config( - config: &Config, - style_computer: &StyleComputer, - count_records: usize, - with_header: bool, - with_index: bool, - expand: bool, -) -> TableConfig { - let theme = load_theme_from_config(config); - let append_footer = with_footer(config, with_header, count_records); - - let mut table_cfg = TableConfig::new(theme, with_header, with_index, append_footer); - - table_cfg = table_cfg.splitline_style(lookup_separator_color(style_computer)); - - if expand { - table_cfg = table_cfg.expand(); - } - - table_cfg.trim(config.trim_strategy.clone()) -} - -fn lookup_separator_color(style_computer: &StyleComputer) -> nu_ansi_term::Style { - style_computer.compute("separator", &Value::nothing(Span::unknown())) -} - -fn with_footer(config: &Config, with_header: bool, count_records: usize) -> bool { - with_header && need_footer(config, count_records as u64) -} - -fn need_footer(config: &Config, count_records: u64) -> bool { - matches!(config.footer_mode, FooterMode::RowCount(limit) if count_records > limit) - || matches!(config.footer_mode, FooterMode::Always) -} - -fn is_cfg_trim_keep_words(config: &Config) -> bool { - matches!( - config.trim_strategy, - TrimStrategy::Wrap { - try_to_keep_words: true - } - ) -} diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml index 579f764ccb..953c771475 100644 --- a/crates/nu-table/Cargo.toml +++ b/crates/nu-table/Cargo.toml @@ -18,9 +18,7 @@ nu-color-config = { path = "../nu-color-config", version = "0.79.1" } nu-ansi-term = "0.47.0" -tabled = { version = "0.10.0", features = ["color"], default-features = false } -json_to_table = { version = "0.3.1", features = ["color"] } -serde_json = "1" +tabled = { version = "0.12.0", features = ["color"], default-features = false } [dev-dependencies] # nu-test-support = { path="../nu-test-support", version = "0.79.1" } diff --git a/crates/nu-table/examples/table_demo.rs b/crates/nu-table/examples/table_demo.rs index 46d9b68cc2..2160411006 100644 --- a/crates/nu-table/examples/table_demo.rs +++ b/crates/nu-table/examples/table_demo.rs @@ -1,13 +1,13 @@ +use nu_ansi_term::{Color, Style}; use nu_color_config::TextStyle; -use nu_table::{Table, TableConfig, TableTheme}; -use tabled::papergrid::records::{cell_info::CellInfo, tcell::TCell}; +use nu_table::{NuTable, TableConfig, TableTheme}; +use tabled::grid::records::vec_records::CellInfo; fn main() { let args: Vec<_> = std::env::args().collect(); let mut width = 0; if args.len() > 1 { - // Width in terminal characters width = args[1].parse::().expect("Need a width in columns"); } @@ -16,31 +16,26 @@ fn main() { width = 80; } - // The mocked up table data let (table_headers, row_data) = make_table_data(); - // The table headers - let headers = vec_of_str_to_vec_of_styledstr(&table_headers, true); + let headers = to_cell_info_vec(&table_headers); + let rows = to_cell_info_vec(&row_data); - // The table rows - let rows = vec_of_str_to_vec_of_styledstr(&row_data, false); - - // The table itself - let count_cols = std::cmp::max(rows.len(), headers.len()); let mut rows = vec![rows; 3]; rows.insert(0, headers); + let mut table = NuTable::from(rows); + + table.set_data_style(TextStyle::basic_left()); + table.set_header_style(TextStyle::basic_center().style(Style::new().on(Color::Blue))); + let theme = TableTheme::rounded(); - let table_cfg = TableConfig::new(theme, true, false, false); + let table_cfg = TableConfig::new().theme(theme).with_header(true); - let table = Table::new(rows, (3, count_cols)); - - // Capture the table as a string let output_table = table .draw(table_cfg, width) .unwrap_or_else(|| format!("Couldn't fit table into {width} columns!")); - // Draw the table println!("{output_table}") } @@ -82,24 +77,11 @@ fn make_table_data() -> (Vec<&'static str>, Vec<&'static str>) { (table_headers, row_data) } -fn vec_of_str_to_vec_of_styledstr( - data: &[&str], - is_header: bool, -) -> Vec, TextStyle>> { +fn to_cell_info_vec(data: &[&str]) -> Vec> { let mut v = vec![]; - for x in data { - if is_header { - v.push(Table::create_cell( - String::from(*x), - TextStyle::default_header(), - )) - } else { - v.push(Table::create_cell( - String::from(*x), - TextStyle::basic_left(), - )) - } + v.push(CellInfo::new(String::from(*x))); } + v } diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs index 5022ddb5bb..219fc435e9 100644 --- a/crates/nu-table/src/lib.rs +++ b/crates/nu-table/src/lib.rs @@ -1,10 +1,15 @@ -mod nu_protocol_table; mod table; mod table_theme; +mod types; +mod unstructured_table; mod util; pub use nu_color_config::TextStyle; -pub use nu_protocol_table::NuTable; -pub use table::{Alignments, Table, TableConfig}; +pub use table::{Alignments, Cell, NuTable, TableConfig}; pub use table_theme::TableTheme; +pub use types::{ + clean_charset, value_to_clean_styled_string, value_to_styled_string, BuildConfig, + CollapsedTable, ExpandedTable, JustTable, NuText, StringResult, TableOutput, TableResult, +}; +pub use unstructured_table::UnstructuredTable; pub use util::*; diff --git a/crates/nu-table/src/nu_protocol_table.rs b/crates/nu-table/src/nu_protocol_table.rs deleted file mode 100644 index 21e05fb6f7..0000000000 --- a/crates/nu-table/src/nu_protocol_table.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::collections::HashMap; - -use crate::{string_width, Alignments, TableTheme}; -use nu_color_config::StyleComputer; -use nu_protocol::{Config, Span, Value}; -use tabled::{ - color::Color, - formatting::AlignmentStrategy, - object::Segment, - papergrid::{records::Records, GridConfig}, - Alignment, Modify, -}; - -use serde_json::Value as Json; - -/// NuTable has a recursive table representation of nu_protocol::Value. -/// -/// It doesn't support alignment and a proper width control. -pub struct NuTable { - inner: String, -} - -impl NuTable { - pub fn new( - value: Value, - collapse: bool, - config: &Config, - style_computer: &StyleComputer, - theme: &TableTheme, - with_footer: bool, - ) -> Self { - let mut table = tabled::Table::new([""]); - load_theme(&mut table, style_computer, theme); - let cfg = table.get_config().clone(); - - let val = nu_protocol_value_to_json(value, config, with_footer); - let table = build_table(val, cfg, collapse); - - Self { inner: table } - } - - pub fn draw(&self, termwidth: usize) -> Option { - let table_width = string_width(&self.inner); - if table_width > termwidth { - None - } else { - Some(self.inner.clone()) - } - } -} - -fn build_table(val: Json, cfg: GridConfig, collapse: bool) -> String { - let mut table = json_to_table::json_to_table(&val); - table.set_config(cfg); - - if collapse { - table.collapse(); - } - - table.to_string() -} - -fn nu_protocol_value_to_json(value: Value, config: &Config, with_footer: bool) -> Json { - match value { - Value::Record { cols, vals, .. } => { - let mut map = serde_json::Map::new(); - for (key, value) in cols.into_iter().zip(vals) { - let val = nu_protocol_value_to_json(value, config, false); - map.insert(key, val); - } - - Json::Object(map) - } - Value::List { vals, .. } => { - let mut used_cols: Option<&[String]> = None; - for val in &vals { - match val { - Value::Record { cols, .. } => { - if let Some(_cols) = &used_cols { - if _cols != cols { - used_cols = None; - break; - } - } else { - used_cols = Some(cols) - } - } - _ => { - used_cols = None; - break; - } - } - } - - if let Some(cols) = used_cols { - // rebuild array as a map - if cols.len() > 1 { - let mut arr = vec![]; - - let head = cols.iter().map(|s| Value::String { - val: s.to_owned(), - span: Span::new(0, 0), - }); - let head = build_map(head, config); - - arr.push(Json::Object(head.clone())); - - for value in &vals { - if let Ok((_, vals)) = value.as_record() { - let vals = build_map(vals.iter().cloned(), config); - - let mut map = serde_json::Map::new(); - connect_maps(&mut map, Json::Object(vals)); - - arr.push(Json::Object(map)); - } - } - - if with_footer { - arr.push(Json::Object(head)); - } - - return Json::Array(arr); - } else { - let mut map = vec![]; - let head = Json::Array(vec![Json::String(cols[0].to_owned())]); - - map.push(head.clone()); - for value in vals { - if let Value::Record { vals, .. } = value { - let list = Value::List { - vals, - span: Span::new(0, 0), - }; - let val = nu_protocol_value_to_json(list, config, false); // rebuild array as a map - - map.push(val); - } - } - - if with_footer { - map.push(head); - } - - return Json::Array(map); - }; - } - - let mut map = Vec::new(); - for value in vals { - let val = nu_protocol_value_to_json(value, config, false); - map.push(val); - } - - Json::Array(map) - } - val => Json::String(val.into_abbreviated_string(config)), - } -} - -fn build_map( - values: impl Iterator + DoubleEndedIterator, - config: &Config, -) -> serde_json::Map { - let mut map = serde_json::Map::new(); - let mut last_val: Option = None; - for val in values.rev() { - if map.is_empty() { - match last_val.take() { - Some(prev_val) => { - let col = val.into_abbreviated_string(&Config::default()); - let prev = nu_protocol_value_to_json(prev_val, config, false); - map.insert(col, prev); - } - None => { - last_val = Some(val); - } - } - } else { - let mut new_m = serde_json::Map::new(); - let col = val.into_abbreviated_string(&Config::default()); - - new_m.insert(col, Json::Object(map)); - map = new_m; - } - } - - map -} - -fn connect_maps(map: &mut serde_json::Map, value: Json) { - if let Json::Object(m) = value { - for (key, value) in m { - if value.is_object() { - let mut new_m = serde_json::Map::new(); - connect_maps(&mut new_m, value); - map.insert(key, Json::Object(new_m)); - } else { - map.insert(key, value); - } - } - } -} - -// -fn load_theme(table: &mut tabled::Table, style_computer: &StyleComputer, theme: &TableTheme) -where - R: Records, -{ - let mut theme = theme.into_full().unwrap_or_else(|| theme.theme.clone()); - theme.set_horizontals(HashMap::default()); - - table.with(theme); - - // color_config closures for "separator" are just given a null. - let color = style_computer.compute("separator", &Value::nothing(Span::unknown())); - let color = color.paint(" ").to_string(); - if let Ok(color) = Color::try_from(color) { - table.with(color); - } - - table.with( - Modify::new(Segment::all()) - .with(Alignment::Horizontal(Alignments::default().data)) - .with(AlignmentStrategy::PerLine), - ); -} diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index b0138fec85..846ea3149f 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -4,108 +4,145 @@ use nu_color_config::TextStyle; use nu_protocol::TrimStrategy; use std::{cmp::min, collections::HashMap}; use tabled::{ - alignment::AlignmentHorizontal, builder::Builder, - color::Color, - formatting::AlignmentStrategy, - object::{Cell, Columns, Rows, Segment}, - papergrid::{ + grid::{ + color::AnsiColor, + config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position}, + dimension::CompleteDimensionVecRecords, records::{ - cell_info::CellInfo, tcell::TCell, vec_records::VecRecords, Records, RecordsMut, + vec_records::{CellInfo, VecRecords}, + ExactRecords, Records, }, - util::string_width_multiline, - width::{CfgWidthFunction, WidthEstimator}, - Estimate, }, - peaker::Peaker, - Alignment, Modify, ModifyObject, TableOption, Width, + settings::{ + formatting::AlignmentStrategy, object::Segment, peaker::Peaker, Color, Modify, Settings, + TableOption, Width, + }, + Table, }; /// Table represent a table view. #[derive(Debug, Clone)] -pub struct Table { +pub struct NuTable { data: Data, + styles: Styles, + alignments: Alignments, + size: (usize, usize), } -type Data = VecRecords, TextStyle>>; +#[derive(Debug, Default, Clone)] +struct Styles { + index: AnsiColor<'static>, + header: AnsiColor<'static>, + data: EntityMap>, + data_is_set: bool, +} -impl Table { - /// Creates a [Table] instance. - /// - /// If `headers.is_empty` then no headers will be rendered. - pub fn new(data: Vec, TextStyle>>>, size: (usize, usize)) -> Table { - // it's not guaranteed that data will have all rows with the same number of columns. - // but VecRecords::with_hint require this constrain. - // - // so we do a check to make it certainly true +type Data = VecRecords; +pub type Cell = CellInfo; - let mut data = data; - make_data_consistent(&mut data, size); - - let data = VecRecords::with_hint(data, size.1); - - Table { data } +impl NuTable { + /// Creates an empty [Table] instance. + pub fn new(count_rows: usize, count_columns: usize) -> Self { + let data = VecRecords::new(vec![vec![CellInfo::default(); count_columns]; count_rows]); + Self { + data, + size: (count_rows, count_columns), + styles: Styles::default(), + alignments: Alignments::default(), + } } pub fn count_rows(&self) -> usize { - self.data.count_rows() + self.size.0 } - pub fn create_cell( - text: impl Into, - style: TextStyle, - ) -> TCell, TextStyle> { - TCell::new(CellInfo::new(text.into(), CfgWidthFunction::new(4)), style) + pub fn count_columns(&self) -> usize { + self.size.1 } - pub fn truncate(&mut self, width: usize, theme: &TableTheme) -> bool { - let mut truncated = false; - while self.data.count_rows() > 0 && self.data.count_columns() > 0 { - let total; - { - let mut table = Builder::custom(self.data.clone()).build(); - load_theme(&mut table, theme, false, false, None); - total = table.total_width(); - } + pub fn insert(&mut self, pos: Position, text: String) { + self.data[pos.0][pos.1] = CellInfo::new(text); + } - if total > width { - truncated = true; - self.data.truncate(self.data.count_columns() - 1); - } else { - break; - } + pub fn set_column_style(&mut self, column: usize, style: TextStyle) { + if let Some(style) = style.color_style { + let style = AnsiColor::from(convert_style(style)); + self.styles.data.insert(Entity::Column(column), style); + self.styles.data_is_set = true; } - let is_empty = self.data.count_rows() == 0 || self.data.count_columns() == 0; - if is_empty { - return true; + let alignment = convert_alignment(style.alignment); + if alignment != self.alignments.data { + self.alignments.columns.insert(column, alignment); + } + } + + pub fn set_cell_style(&mut self, pos: Position, style: TextStyle) { + if let Some(style) = style.color_style { + let style = AnsiColor::from(convert_style(style)); + self.styles.data.insert(Entity::Cell(pos.0, pos.1), style); + self.styles.data_is_set = true; } - if truncated { - self.data.push(Table::create_cell( - String::from("..."), - TextStyle::default(), - )); + let alignment = convert_alignment(style.alignment); + if alignment != self.alignments.data { + self.alignments.cells.insert(pos, alignment); + } + } + + pub fn set_header_style(&mut self, style: TextStyle) { + if let Some(style) = style.color_style { + let style = AnsiColor::from(convert_style(style)); + self.styles.header = style; } - false + self.alignments.header = convert_alignment(style.alignment); + } + + pub fn set_index_style(&mut self, style: TextStyle) { + if let Some(style) = style.color_style { + let style = AnsiColor::from(convert_style(style)); + self.styles.index = style; + } + + self.alignments.index = convert_alignment(style.alignment); + } + + pub fn set_data_style(&mut self, style: TextStyle) { + if let Some(style) = style.color_style { + let style = AnsiColor::from(convert_style(style)); + self.styles.data.insert(Entity::Global, style); + self.styles.data_is_set = true; + } + + self.alignments.data = convert_alignment(style.alignment); } /// Converts a table to a String. /// /// It returns None in case where table cannot be fit to a terminal width. pub fn draw(self, config: TableConfig, termwidth: usize) -> Option { - build_table(self.data, config, termwidth) + build_table(self.data, config, self.alignments, self.styles, termwidth) + } + + /// Return a total table width. + pub fn total_width(&self, config: &TableConfig) -> usize { + let config = get_config(&config.theme, false, None); + let widths = build_width(&self.data); + get_total_width2(&widths, &config) } } -fn make_data_consistent(data: &mut Vec>>, size: (usize, usize)) { - for row in data { - if row.len() < size.1 { - row.extend( - std::iter::repeat(Table::create_cell(String::default(), TextStyle::default())) - .take(size.1 - row.len()), - ); +impl From>>> for NuTable { + fn from(value: Vec>>) -> Self { + let data = VecRecords::new(value); + let size = (data.count_rows(), data.count_columns()); + Self { + data, + size, + alignments: Alignments::default(), + styles: Styles::default(), } } } @@ -113,7 +150,6 @@ fn make_data_consistent(data: &mut Vec>>, size: ( #[derive(Debug, Clone)] pub struct TableConfig { theme: TableTheme, - alignments: Alignments, trim: TrimStrategy, split_color: Option