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