diff --git a/Cargo.lock b/Cargo.lock index 95ecdec3de..04e73cf93c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ "getrandom 0.2.6", "once_cell", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -68,6 +68,32 @@ version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0" +[[package]] +name = "ansi-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb2392079bf27198570d6af79ecbd9ec7d8f16d3ec6b60933922fdb66287127" +dependencies = [ + "heapless 0.5.6", + "nom 4.2.3", +] + +[[package]] +name = "ansi-str" +version = "0.1.1" +source = "git+https://github.com/zhiburt/ansi-str?rev=e0f5cfe2ee3ee8815371800ece150832e9273acb#e0f5cfe2ee3ee8815371800ece150832e9273acb" +dependencies = [ + "ansi-parser", +] + +[[package]] +name = "ansi-str" +version = "0.2.0" +source = "git+https://github.com/zhiburt/ansi-str?branch=master#e1ee42abb46374ebd8403a8fc2c8c88e8f9cedc6" +dependencies = [ + "ansi-parser", +] + [[package]] name = "ansi-str" version = "0.2.0" @@ -161,6 +187,18 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.5", + "stable_deref_trait", +] + [[package]] name = "assert_cmd" version = "2.0.4" @@ -321,7 +359,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.5", ] [[package]] @@ -330,7 +368,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ - "generic-array", + "generic-array 0.14.5", ] [[package]] @@ -754,7 +792,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ - "generic-array", + "generic-array 0.14.5", "typenum", ] @@ -886,7 +924,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.5", ] [[package]] @@ -1365,6 +1403,24 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.5" @@ -1372,7 +1428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -1501,6 +1557,15 @@ dependencies = [ "regex", ] +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + [[package]] name = "hash32" version = "0.2.1" @@ -1544,6 +1609,18 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "heapless" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1" +dependencies = [ + "as-slice", + "generic-array 0.13.3", + "hash32 0.1.1", + "stable_deref_trait", +] + [[package]] name = "heapless" version = "0.7.13" @@ -1551,7 +1628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a08e755adbc0ad283725b29f4a4883deee15336f372d5f61fae59efec40f983" dependencies = [ "atomic-polyfill", - "hash32", + "hash32 0.2.1", "rustc_version 0.4.0", "spin", "stable_deref_trait", @@ -2427,6 +2504,16 @@ version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + [[package]] name = "nom" version = "7.1.1" @@ -2726,7 +2813,7 @@ dependencies = [ name = "nu-pretty-hex" version = "0.65.1" dependencies = [ - "heapless", + "heapless 0.7.13", "nu-ansi-term", "rand 0.8.5", ] @@ -2769,7 +2856,7 @@ dependencies = [ name = "nu-table" version = "0.65.1" dependencies = [ - "ansi-str", + "ansi-str 0.2.0 (git+https://github.com/zhiburt/ansi-str?rev=655cd8125a032286082794690c2cc6dc835345b4)", "atty", "nu-ansi-term", "nu-protocol", @@ -3100,9 +3187,9 @@ checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" [[package]] name = "papergrid" version = "0.4.0" -source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423" +source = "git+https://github.com/zhiburt/tabled?branch=master#b4fbad01ac95cd4a7d71fa0bf67eba02541c7963" dependencies = [ - "ansi-str", + "ansi-str 0.1.1", "bytecount", "strip-ansi-escapes", "unicode-width", @@ -3566,7 +3653,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -3577,7 +3664,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -4718,9 +4805,9 @@ dependencies = [ [[package]] name = "tabled" version = "0.7.0" -source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423" +source = "git+https://github.com/zhiburt/tabled?branch=master#b4fbad01ac95cd4a7d71fa0bf67eba02541c7963" dependencies = [ - "ansi-str", + "ansi-str 0.2.0 (git+https://github.com/zhiburt/ansi-str?branch=master)", "papergrid", "tabled_derive", "unicode-width", @@ -4729,7 +4816,7 @@ dependencies = [ [[package]] name = "tabled_derive" version = "0.3.0" -source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423" +source = "git+https://github.com/zhiburt/tabled?branch=master#b4fbad01ac95cd4a7d71fa0bf67eba02541c7963" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -5042,7 +5129,7 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" dependencies = [ - "version_check", + "version_check 0.9.4", ] [[package]] @@ -5170,6 +5257,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc1631c774f0f9570797191e01247cbefde789eebfbf128074cb934115a6133" +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + [[package]] name = "version_check" version = "0.9.4" diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 130c608e58..dd76036404 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -2,6 +2,10 @@ use crate::{ShellError, Span, Value}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +const TRIM_STRATEGY_DEFAULT: TrimStrategy = TrimStrategy::Wrap { + try_to_keep_words: true, +}; + /// Definition of a parsed keybinding from the config object #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ParsedKeybinding { @@ -75,6 +79,7 @@ pub struct Config { pub cd_with_abbreviations: bool, pub case_sensitive_completions: bool, pub enable_external_completion: bool, + pub trim_strategy: TrimStrategy, } impl Default for Config { @@ -107,6 +112,7 @@ impl Default for Config { cd_with_abbreviations: false, case_sensitive_completions: false, enable_external_completion: true, + trim_strategy: TRIM_STRATEGY_DEFAULT, } } } @@ -131,6 +137,30 @@ pub enum HistoryFileFormat { PlainText, } +/// A Table view configuration, for a situation where +/// we need to limit cell width in order to adjust for a terminal size. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum TrimStrategy { + /// Wrapping strategy. + /// + /// It it's simmilar to original nu_table, strategy. + Wrap { + /// A flag which indicates whether is it necessary to try + /// to keep word bounderies. + try_to_keep_words: bool, + }, + /// Truncating strategy, where we just cut the string. + /// And append the suffix if applicable. + Truncate { + /// Suffix which can be appended to a truncated string after being cut. + /// + /// It will be applied only when there's enough room for it. + /// For example in case where a cell width must be 12 chars, but + /// the suffix takes 13 chars it won't be used. + suffix: Option, + }, +} + impl Value { pub fn into_config(self) -> Result { let v = self.as_record(); @@ -348,6 +378,7 @@ impl Value { eprintln!("$config.enable_external_completion is not a bool") } } + "table_trim" => config.trim_strategy = try_parse_trim_strategy(value, &config)?, x => { eprintln!("$config.{} is an unknown config setting", x) } @@ -361,6 +392,64 @@ impl Value { } } +fn try_parse_trim_strategy(value: &Value, config: &Config) -> Result { + let map = create_map(value, config).map_err(|e| { + eprintln!("$config.table_trim is not a record"); + e + })?; + + let mut methodology = match map.get("methodology") { + Some(value) => match try_parse_trim_methodology(value) { + Some(methodology) => methodology, + None => return Ok(TRIM_STRATEGY_DEFAULT), + }, + None => { + eprintln!("$config.table_trim.methodology was not provided"); + return Ok(TRIM_STRATEGY_DEFAULT); + } + }; + + match &mut methodology { + TrimStrategy::Wrap { try_to_keep_words } => { + if let Some(value) = map.get("wrapping_try_keep_words") { + if let Ok(b) = value.as_bool() { + *try_to_keep_words = b; + } else { + eprintln!("$config.table_trim.wrap_try_keep_words is not a bool"); + } + } + } + TrimStrategy::Truncate { suffix } => { + if let Some(value) = map.get("truncating_suffix") { + if let Ok(v) = value.as_string() { + *suffix = Some(v); + } else { + eprintln!("$config.table_trim.truncating_suffix is not a string") + } + } + } + } + + Ok(methodology) +} + +fn try_parse_trim_methodology(value: &Value) -> Option { + match value.as_string() { + Ok(value) => match value.to_lowercase().as_str() { + "wrapping" => { + return Some(TrimStrategy::Wrap { + try_to_keep_words: false, + }); + } + "truncating" => return Some(TrimStrategy::Truncate { suffix: None }), + _ => eprintln!("unrecognized $config.trim_methodology value; expected values ['truncating', 'wrapping']"), + }, + Err(_) => eprintln!("$config.trim_methodology is not a string"), + } + + None +} + fn create_map(value: &Value, config: &Config) -> Result, ShellError> { let (cols, inner_vals) = value.as_record()?; let mut hm: HashMap = HashMap::new(); diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml index 11a9e11cf8..4ece8b999a 100644 --- a/crates/nu-table/Cargo.toml +++ b/crates/nu-table/Cargo.toml @@ -19,6 +19,4 @@ unicode-width = "0.1.8" strip-ansi-escapes = "0.1.1" ansi-str = { git = "https://github.com/zhiburt/ansi-str", rev = "655cd8125a032286082794690c2cc6dc835345b4" } atty = "0.2.14" -tabled = { git = "https://github.com/zhiburt/tabled", rev = "e3cbdea5b81edda8b3cc7b9f64f98fecae7db423", features = [ - "color", -] } +tabled = { git = "https://github.com/zhiburt/tabled", branch = "master", features = ["color"] } diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs index 22491ae9ed..b647e84ddd 100644 --- a/crates/nu-table/src/lib.rs +++ b/crates/nu-table/src/lib.rs @@ -1,7 +1,7 @@ mod table; mod table_theme; mod textstyle; -mod wrap; +mod width_control; pub use table::{draw_table, Table}; pub use table_theme::TableTheme; diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index 2b94ba95d2..4754b4e9e6 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -1,15 +1,20 @@ -use crate::table_theme::TableTheme; -use crate::StyledString; -use nu_ansi_term::Style; -use nu_protocol::{Config, FooterMode}; use std::collections::HashMap; + +use nu_ansi_term::Style; +use nu_protocol::{Config, FooterMode, TrimStrategy}; use tabled::{ builder::Builder, formatting_settings::AlignmentStrategy, object::{Cell, Columns, Rows}, papergrid, style::BorderColor, - Alignment, Modify, TableOption, + Alignment, Modify, TableOption, Width, +}; + +use crate::{ + table_theme::TableTheme, + width_control::{estimate_max_column_width, fix_termwidth, maybe_truncate_columns}, + StyledString, }; #[derive(Debug)] @@ -39,27 +44,89 @@ pub fn draw_table( color_hm: &HashMap, config: &Config, ) -> Option { - // Remove the edges, if used - let (headers, data) = crate::wrap::wrap(&table.headers, &table.data, termwidth, &table.theme)?; + let termwidth = fix_termwidth(termwidth, &table.theme)?; + + let (mut headers, mut data) = table_fix_lengths(&table.headers, &table.data); + + maybe_truncate_columns(&mut headers, &mut data, termwidth); + + let max_column_width = estimate_max_column_width(&headers, &data, termwidth)?; + + let alignments = build_alignment_map(&table.data); + + let headers = table_header_to_strings(headers); + let data = table_data_to_strings(data, headers.len()); + let headers = if headers.is_empty() { None } else { Some(headers) }; - let alignments = build_alignment_map(&table.data); - let theme = &table.theme; - let with_header = headers.is_some(); let with_footer = with_header && need_footer(config, data.len() as u64); let table = build_table(data, headers, Some(alignments), config, with_footer); let table = load_theme(table, color_hm, theme, with_footer, with_header); + let (count_columns, table) = count_columns_on_table(table); + + let table = table_trim_columns( + table, + count_columns, + termwidth, + max_column_width, + &config.trim_strategy, + ); + Some(table.to_string()) } +fn count_columns_on_table(mut table: tabled::Table) -> (usize, tabled::Table) { + let mut c = CountColumns(0); + table = table.with(&mut c); + + (c.0, table) +} + +fn table_data_to_strings( + table_data: Vec>, + count_headers: usize, +) -> Vec> { + let mut data = vec![Vec::with_capacity(count_headers); table_data.len()]; + for (row, row_data) in table_data.into_iter().enumerate() { + for cell in row_data { + let colored_text = cell + .style + .color_style + .as_ref() + .map(|color| color.paint(&cell.contents).to_string()) + .unwrap_or(cell.contents); + + data[row].push(colored_text) + } + } + + data +} + +fn table_header_to_strings(table_headers: Vec) -> Vec { + let mut headers = Vec::with_capacity(table_headers.len()); + for cell in table_headers { + let colored_text = cell + .style + .color_style + .as_ref() + .map(|color| color.paint(&cell.contents).to_string()) + .unwrap_or(cell.contents); + + headers.push(colored_text) + } + + headers +} + fn build_alignment_map(data: &[Vec]) -> Vec> { let mut v = vec![Vec::new(); data.len()]; for (i, row) in data.iter().enumerate() { @@ -195,3 +262,89 @@ impl TableOption for RemoveHeaderLine { grid.set_split_line(1, papergrid::Line::default()); } } + +struct CountColumns(usize); + +impl TableOption for &mut CountColumns { + fn change(&mut self, grid: &mut papergrid::Grid) { + self.0 = grid.count_columns(); + } +} + +fn table_trim_columns( + table: tabled::Table, + count_columns: usize, + termwidth: usize, + max_column_width: usize, + trim_strategy: &TrimStrategy, +) -> tabled::Table { + let mut table_width = max_column_width * count_columns; + if table_width > termwidth { + table_width = termwidth; + } + + table.with(&TrimStrategyModifier { + termwidth: table_width, + trim_strategy, + }) +} + +pub struct TrimStrategyModifier<'a> { + termwidth: usize, + trim_strategy: &'a TrimStrategy, +} + +impl tabled::TableOption for &TrimStrategyModifier<'_> { + fn change(&mut self, grid: &mut papergrid::Grid) { + match self.trim_strategy { + TrimStrategy::Wrap { try_to_keep_words } => { + let mut w = Width::wrap(self.termwidth); + if *try_to_keep_words { + w = w.keep_words(); + } + let mut w = w.priority::(); + + w.change(grid) + } + TrimStrategy::Truncate { suffix } => { + let mut w = + Width::truncate(self.termwidth).priority::(); + if let Some(suffix) = suffix { + w = w.suffix(suffix); + } + + w.change(grid); + } + }; + } +} + +fn table_fix_lengths( + headers: &[StyledString], + data: &[Vec], +) -> (Vec, Vec>) { + let length = table_find_max_length(headers, data); + + let mut headers_fixed = Vec::with_capacity(length); + headers_fixed.extend(headers.iter().cloned()); + headers_fixed.extend(std::iter::repeat(StyledString::default()).take(length - headers.len())); + + let mut data_fixed = Vec::with_capacity(data.len()); + for row in data { + let mut row_fixed = Vec::with_capacity(length); + row_fixed.extend(row.iter().cloned()); + row_fixed.extend(std::iter::repeat(StyledString::default()).take(length - row.len())); + data_fixed.push(row_fixed); + } + + (headers_fixed, data_fixed) +} + +fn table_find_max_length(headers: &[StyledString], data: &[Vec]) -> usize { + let mut length = headers.len(); + for row in data { + length = std::cmp::max(length, row.len()); + } + + length +} diff --git a/crates/nu-table/src/width_control.rs b/crates/nu-table/src/width_control.rs new file mode 100644 index 0000000000..ef01cbc857 --- /dev/null +++ b/crates/nu-table/src/width_control.rs @@ -0,0 +1,213 @@ +use crate::textstyle::TextStyle; +use crate::{StyledString, TableTheme}; +use std::iter::Iterator; + +pub(crate) fn maybe_truncate_columns( + headers: &mut Vec, + data: &mut [Vec], + termwidth: usize, +) { + // Make sure we have enough space for the columns we have + let max_num_of_columns = termwidth / 10; + + // If we have too many columns, truncate the table + if max_num_of_columns < headers.len() { + headers.truncate(max_num_of_columns); + headers.push(StyledString::new( + String::from("..."), + TextStyle::basic_center(), + )); + } + + if max_num_of_columns < headers.len() { + for entry in data.iter_mut() { + entry.truncate(max_num_of_columns); + entry.push(StyledString::new( + String::from("..."), + TextStyle::basic_center(), + )); + } + } +} + +pub(crate) fn estimate_max_column_width( + headers: &[StyledString], + data: &[Vec], + termwidth: usize, +) -> Option { + let max_per_column = get_max_column_widths(headers, data); + + let headers_len = headers.len(); + // Measure how big our columns need to be (accounting for separators also) + let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len; + + let column_space = ColumnSpace::measure(&max_per_column, max_naive_column_width, headers_len); + + // This gives us the max column width + let max_column_width = column_space.max_width(termwidth)?; + + // This width isn't quite right, as we're rounding off some of our space + let column_space = column_space.fix_almost_column_width( + &max_per_column, + max_naive_column_width, + max_column_width, + headers_len, + ); + + // This should give us the final max column width + let max_column_width = column_space.max_width(termwidth)?; + + Some(max_column_width) +} + +pub(crate) fn fix_termwidth(termwidth: usize, theme: &TableTheme) -> Option { + let edges_width = if theme.is_left_set && theme.is_right_set { + 3 + } else if theme.is_left_set || theme.is_right_set { + 1 + } else { + 0 + }; + + if termwidth < edges_width { + return None; + } + + Some(termwidth - edges_width - 1) +} + +fn get_max_column_widths(headers: &[StyledString], data: &[Vec]) -> Vec { + use std::cmp::max; + + let mut output = vec![0; headers.len()]; + + for (col, content) in headers.iter().enumerate() { + let content = clean(&content.contents); + let content_width = tabled::papergrid::string_width_multiline(&content); + output[col] = max(output[col], content_width); + } + + for row in data { + for (col, content) in row.iter().enumerate() { + let content = clean(&content.contents); + let content_width = tabled::papergrid::string_width_multiline(&content); + output[col] = max(output[col], content_width); + } + } + + output +} + +struct ColumnSpace { + num_overages: usize, + underage_sum: usize, + overage_separator_sum: usize, +} + +impl ColumnSpace { + /// Measure how much space we have once we subtract off the columns who are small enough + fn measure( + max_per_column: &[usize], + max_naive_column_width: usize, + headers_len: usize, + ) -> ColumnSpace { + let mut num_overages = 0; + let mut underage_sum = 0; + let mut overage_separator_sum = 0; + let iter = max_per_column.iter().enumerate().take(headers_len); + + for (i, &column_max) in iter { + if column_max > max_naive_column_width { + num_overages += 1; + if i != (headers_len - 1) { + overage_separator_sum += 3; + } + if i == 0 { + overage_separator_sum += 1; + } + } else { + underage_sum += column_max; + // if column isn't last, add 3 for its separator + if i != (headers_len - 1) { + underage_sum += 3; + } + if i == 0 { + underage_sum += 1; + } + } + } + + ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } + } + + fn fix_almost_column_width( + self, + max_per_column: &[usize], + max_naive_column_width: usize, + max_column_width: usize, + headers_len: usize, + ) -> ColumnSpace { + let mut num_overages = 0; + let mut overage_separator_sum = 0; + let mut underage_sum = self.underage_sum; + let iter = max_per_column.iter().enumerate().take(headers_len); + + for (i, &column_max) in iter { + if column_max > max_naive_column_width { + if column_max <= max_column_width { + underage_sum += column_max; + // if column isn't last, add 3 for its separator + if i != (headers_len - 1) { + underage_sum += 3; + } + if i == 0 { + underage_sum += 1; + } + } else { + // Column is still too large, so let's count it + num_overages += 1; + if i != (headers_len - 1) { + overage_separator_sum += 3; + } + if i == 0 { + overage_separator_sum += 1; + } + } + } + } + + ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } + } + + fn max_width(&self, termwidth: usize) -> Option { + let ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } = self; + + if *num_overages > 0 { + termwidth + .checked_sub(1)? + .checked_sub(*underage_sum)? + .checked_sub(*overage_separator_sum)? + .checked_div(*num_overages) + } else { + Some(99999) + } + } +} + +fn clean(input: &str) -> String { + let input = input.replace('\r', ""); + + input.replace('\t', " ") +} diff --git a/crates/nu-table/src/wrap.rs b/crates/nu-table/src/wrap.rs deleted file mode 100644 index c581a84498..0000000000 --- a/crates/nu-table/src/wrap.rs +++ /dev/null @@ -1,653 +0,0 @@ -use crate::textstyle::TextStyle; -use crate::{StyledString, TableTheme}; -use ansi_str::AnsiStr; -use nu_ansi_term::Style; -use std::borrow::Cow; -use std::collections::HashMap; -use std::iter::Iterator; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; - -#[derive(Debug)] -pub struct Subline { - pub subline: String, - pub width: usize, -} - -#[derive(Debug)] -pub struct Line { - pub sublines: Vec, - pub width: usize, -} - -#[derive(Debug, Clone)] -pub struct WrappedLine { - pub line: String, - pub width: usize, -} - -#[derive(Debug, Clone)] -pub struct WrappedCell { - pub lines: Vec, - pub max_width: usize, - - pub style: TextStyle, -} - -/// Removes ANSI escape codes and some ASCII control characters -/// -/// Keeps `\n` removes `\r`, `\t` etc. -/// -/// If parsing fails silently returns the input string -fn strip_ansi(string: &str) -> Cow { - // Check if any ascii control character except LF(0x0A = 10) is present, - // which will be stripped. Includes the primary start of ANSI sequences ESC - // (0x1B = decimal 27) - if string.bytes().any(|x| matches!(x, 0..=9 | 11..=31)) { - if let Ok(stripped) = strip_ansi_escapes::strip(string) { - if let Ok(new_string) = String::from_utf8(stripped) { - return Cow::Owned(new_string); - } - } - } - // Else case includes failures to parse! - Cow::Borrowed(string) -} - -// fn special_width(astring: &str) -> usize { -// // remove the zwj's '\u{200d}' -// // remove the fe0f's -// let stripped_string: String = { -// if let Ok(bytes) = strip_ansi_escapes::strip(astring) { -// String::from_utf8_lossy(&bytes).to_string() -// } else { -// astring.to_string() -// } -// }; - -// let no_zwj = stripped_string.replace('\u{200d}', ""); -// let no_fe0f = no_zwj.replace('\u{fe0f}', ""); -// UnicodeWidthStr::width(&no_fe0f[..]) -// } - -pub fn split_sublines(input: &str) -> Vec> { - input - .ansi_split("\n") - .map(|line| { - line.ansi_split(" ") - .map(|x| Subline { - subline: x.to_string(), - width: { - // We've tried UnicodeWidthStr::width(x), UnicodeSegmentation::graphemes(x, true).count() - // and x.chars().count() with all types of combinations. Currently, it appears that - // getting the max of char count and Unicode width seems to produce the best layout. - // However, it's not perfect. - // let c = x.chars().count(); - // let u = UnicodeWidthStr::width(x); - // std::cmp::min(c, u) - - // let c = strip_ansi(x).chars().count(); - // let u = special_width(x); - // std::cmp::max(c, u) - let stripped = strip_ansi(&x); - - let c = stripped.chars().count(); - let u = stripped.width(); - std::cmp::max(c, u) - }, - }) - .collect::>() - }) - .collect::>() -} - -pub fn column_width(input: &[Vec]) -> usize { - let mut max = 0; - - for line in input { - let mut total = 0; - - let mut first = true; - for inp in line { - if !first { - // Account for the space - total += 1; - } else { - first = false; - } - - total += inp.width; - } - - if total > max { - max = total; - } - } - - max -} - -fn split_word(cell_width: usize, word: &str) -> Vec { - let mut output = vec![]; - let mut current_width = 0; - let mut start_index = 0; - let mut end_index; - - let word_no_ansi = strip_ansi(word); - for c in word_no_ansi.char_indices() { - if let Some(width) = c.1.width() { - end_index = c.0; - if current_width + width > cell_width { - output.push(Subline { - subline: word.ansi_cut(start_index..end_index), - width: current_width, - }); - - start_index = c.0; - current_width = width; - } else { - current_width += width; - } - } - } - - if start_index != word_no_ansi.len() { - output.push(Subline { - subline: word.ansi_cut(start_index..), - width: current_width, - }); - } - - output -} - -pub fn wrap_content( - cell_width: usize, - mut input: impl Iterator, - color_hm: &HashMap, - re_leading: ®ex::Regex, - re_trailing: ®ex::Regex, -) -> (Vec, usize) { - let mut lines = vec![]; - let mut current_line: Vec = vec![]; - let mut current_width = 0; - let mut first = true; - let mut max_width = 0; - let lead_trail_space_bg_color = color_hm - .get("leading_trailing_space_bg") - .unwrap_or(&Style::default()) - .to_owned(); - - loop { - match input.next() { - Some(item) => { - if !first { - current_width += 1; - } else { - first = false; - } - - if item.width + current_width > cell_width { - // If this is a really long single word, we need to split the word - if current_line.len() == 1 && current_width > cell_width { - max_width = cell_width; - let sublines = split_word(cell_width, ¤t_line[0].subline); - for subline in sublines { - let width = subline.width; - lines.push(Line { - sublines: vec![subline], - width, - }); - } - - first = true; - - current_width = item.width; - current_line = vec![item]; - } else { - if !current_line.is_empty() { - lines.push(Line { - sublines: current_line, - width: current_width, - }); - } - - first = true; - - current_width = item.width; - current_line = vec![item]; - max_width = std::cmp::max(max_width, current_width); - } - } else { - current_width += item.width; - current_line.push(item); - } - } - None => { - if current_width > cell_width { - // We need to break up the last word - let sublines = split_word(cell_width, ¤t_line[0].subline); - for subline in sublines { - let width = subline.width; - lines.push(Line { - sublines: vec![subline], - width, - }); - } - } else if current_width > 0 { - lines.push(Line { - sublines: current_line, - width: current_width, - }); - } - break; - } - } - } - - let mut current_max = 0; - let mut output = vec![]; - - for line in lines { - let mut current_line_width = 0; - let mut first = true; - let mut current_line = String::new(); - - for subline in line.sublines { - if !first { - current_line_width += subline.width; - - if current_line_width + 1 < cell_width { - current_line_width += 1; - current_line.push(' '); - } - } else { - first = false; - current_line_width = subline.width; - } - current_line.push_str(&subline.subline); - } - - if current_line_width > current_max { - current_max = current_line_width; - } - - // highlight leading and trailing spaces so they stand out. - let mut bg_color_string = Style::default().prefix().to_string(); - // right now config settings can only set foreground colors so, in this - // instance we take the foreground color and make it a background color - if let Some(bg) = lead_trail_space_bg_color.foreground { - bg_color_string = Style::default().on(bg).prefix().to_string() - }; - - if let Some(leading_match) = re_leading.find(¤t_line.clone()) { - String::insert_str( - &mut current_line, - leading_match.end(), - nu_ansi_term::ansi::RESET, - ); - String::insert_str(&mut current_line, leading_match.start(), &bg_color_string); - } - - if let Some(trailing_match) = re_trailing.find(¤t_line.clone()) { - String::insert_str(&mut current_line, trailing_match.start(), &bg_color_string); - current_line += nu_ansi_term::ansi::RESET; - } - - output.push(WrappedLine { - line: current_line, - width: current_line_width, - }); - } - - (output, current_max) -} - -pub fn wrap( - headers: &[StyledString], - data: &[Vec], - termwidth: usize, - theme: &TableTheme, -) -> Option<(Vec, Vec>)> { - // Remove the edges, if used - let edges_width = if theme.is_left_set && theme.is_right_set { - 3 - } else if theme.is_left_set || theme.is_right_set { - 1 - } else { - 0 - }; - - if termwidth < edges_width { - return None; - } - - let termwidth = termwidth - edges_width; - - let (mut headers_splited, mut data_splited) = split_lines(headers, data); - - let max_per_column = get_max_column_widths(&headers_splited, &data_splited); - - maybe_truncate_columns(termwidth, &mut headers_splited, &mut data_splited); - - let mut headers_len = headers_splited.len(); - if headers_len == 0 { - if !data.is_empty() && !data[0].is_empty() { - headers_len = data_splited[0].len(); - } else { - return Some((Vec::new(), Vec::new())); - } - } - - // Measure how big our columns need to be (accounting for separators also) - let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len; - - let column_space = ColumnSpace::measure(&max_per_column, max_naive_column_width, headers_len); - - // This gives us the max column width - let max_column_width = column_space.max_width(termwidth)?; - - // This width isn't quite right, as we're rounding off some of our space - let column_space = column_space.fix_almost_column_width( - &max_per_column, - max_naive_column_width, - max_column_width, - headers_len, - ); - - // This should give us the final max column width - let max_column_width = column_space.max_width(termwidth)?; - - let re_leading = - regex::Regex::new(r"(?P^\s+)").expect("error with leading space regex"); - let re_trailing = - regex::Regex::new(r"(?P\s+$)").expect("error with trailing space regex"); - - let result = wrap_cells( - headers_splited, - data_splited, - max_column_width, - &re_leading, - &re_trailing, - ); - - Some(result) -} - -struct ContentLines { - pub lines: Vec>, - pub style: TextStyle, -} - -fn split_lines( - headers: &[StyledString], - data: &[Vec], -) -> (Vec, Vec>) { - let mut splited_headers = Vec::with_capacity(headers.len()); - for column in headers { - let content = clean(&column.contents); - let lines = split_sublines(&content); - splited_headers.push(ContentLines { - lines, - style: column.style, - }); - } - - let mut splited_data = Vec::with_capacity(data.len()); - for row in data { - let mut splited_row = Vec::with_capacity(row.len()); - for column in row { - let content = clean(&column.contents); - let lines = split_sublines(&content); - splited_row.push(ContentLines { - lines, - style: column.style, - }); - } - - splited_data.push(splited_row); - } - - (splited_headers, splited_data) -} - -fn get_max_column_widths(headers: &[ContentLines], data: &[Vec]) -> Vec { - use std::cmp::max; - - let mut max_num_columns = 0; - - max_num_columns = max(max_num_columns, headers.len()); - - for row in data { - max_num_columns = max(max_num_columns, row.len()); - } - - let mut output = vec![0; max_num_columns]; - - for (col, content) in headers.iter().enumerate() { - output[col] = max(output[col], column_width(&content.lines)); - } - - for row in data { - for (col, content) in row.iter().enumerate() { - output[col] = max(output[col], column_width(&content.lines)); - } - } - - output -} - -fn wrap_cells( - headers_splited: Vec, - data_splited: Vec>, - max_column_width: usize, - re_leading: ®ex::Regex, - re_trailing: ®ex::Regex, -) -> (Vec, Vec>) { - let mut header = vec![String::new(); headers_splited.len()]; - for (col, splited) in headers_splited.into_iter().enumerate() { - let mut wrapped = vec![]; - for contents in splited.lines { - let (mut lines, _) = wrap_content( - max_column_width, - contents.into_iter(), - &HashMap::new(), - re_leading, - re_trailing, - ); - wrapped.append(&mut lines); - } - - let content = wrapped - .into_iter() - .map(|l| l.line) - .collect::>() - .join("\n"); - let content = splited - .style - .color_style - .map(|color| color.paint(&content).to_string()) - .unwrap_or(content); - - header[col] = content; - } - - let mut data = vec![Vec::new(); data_splited.len()]; - for (row, splited) in data_splited.into_iter().enumerate() { - for splited in splited.into_iter() { - let mut wrapped = vec![]; - for contents in splited.lines { - let (mut lines, _) = wrap_content( - max_column_width, - contents.into_iter(), - &HashMap::new(), - re_leading, - re_trailing, - ); - wrapped.append(&mut lines); - } - - let content = wrapped - .into_iter() - .map(|l| l.line) - .collect::>() - .join("\n"); - let content = splited - .style - .color_style - .map(|color| color.paint(&content).to_string()) - .unwrap_or(content); - - data[row].push(content); - } - } - - (header, data) -} - -fn maybe_truncate_columns( - termwidth: usize, - headers: &mut Vec, - data: &mut [Vec], -) { - // Make sure we have enough space for the columns we have - let max_num_of_columns = termwidth / 10; - - // If we have too many columns, truncate the table - if max_num_of_columns < headers.len() { - headers.truncate(max_num_of_columns); - headers.push(ContentLines { - lines: vec![vec![Subline { - subline: String::from("..."), - width: 3, - }]], - style: TextStyle::basic_center(), - }); - } - - if max_num_of_columns < headers.len() { - for entry in data.iter_mut() { - entry.truncate(max_num_of_columns); - entry.push(ContentLines { - lines: vec![vec![Subline { - subline: String::from("..."), - width: 3, - }]], - style: TextStyle::basic_center(), - }); - } - } -} - -struct ColumnSpace { - num_overages: usize, - underage_sum: usize, - overage_separator_sum: usize, -} - -impl ColumnSpace { - /// Measure how much space we have once we subtract off the columns who are small enough - fn measure( - max_per_column: &[usize], - max_naive_column_width: usize, - headers_len: usize, - ) -> ColumnSpace { - let mut num_overages = 0; - let mut underage_sum = 0; - let mut overage_separator_sum = 0; - let iter = max_per_column.iter().enumerate().take(headers_len); - - for (i, &column_max) in iter { - if column_max > max_naive_column_width { - num_overages += 1; - if i != (headers_len - 1) { - overage_separator_sum += 3; - } - if i == 0 { - overage_separator_sum += 1; - } - } else { - underage_sum += column_max; - // if column isn't last, add 3 for its separator - if i != (headers_len - 1) { - underage_sum += 3; - } - if i == 0 { - underage_sum += 1; - } - } - } - - ColumnSpace { - num_overages, - underage_sum, - overage_separator_sum, - } - } - - fn fix_almost_column_width( - self, - max_per_column: &[usize], - max_naive_column_width: usize, - max_column_width: usize, - headers_len: usize, - ) -> ColumnSpace { - let mut num_overages = 0; - let mut overage_separator_sum = 0; - let mut underage_sum = self.underage_sum; - let iter = max_per_column.iter().enumerate().take(headers_len); - - for (i, &column_max) in iter { - if column_max > max_naive_column_width { - if column_max <= max_column_width { - underage_sum += column_max; - // if column isn't last, add 3 for its separator - if i != (headers_len - 1) { - underage_sum += 3; - } - if i == 0 { - underage_sum += 1; - } - } else { - // Column is still too large, so let's count it - num_overages += 1; - if i != (headers_len - 1) { - overage_separator_sum += 3; - } - if i == 0 { - overage_separator_sum += 1; - } - } - } - } - - ColumnSpace { - num_overages, - underage_sum, - overage_separator_sum, - } - } - - fn max_width(&self, termwidth: usize) -> Option { - let ColumnSpace { - num_overages, - underage_sum, - overage_separator_sum, - } = self; - - if *num_overages > 0 { - termwidth - .checked_sub(1)? - .checked_sub(*underage_sum)? - .checked_sub(*overage_separator_sum)? - .checked_div(*num_overages) - } else { - Some(99999) - } - } -} - -fn clean(input: &str) -> String { - let input = input.replace('\r', ""); - - input.replace('\t', " ") -} diff --git a/docs/sample_config/default_config.nu b/docs/sample_config/default_config.nu index e05a956767..42276f22f9 100644 --- a/docs/sample_config/default_config.nu +++ b/docs/sample_config/default_config.nu @@ -257,6 +257,15 @@ let-env config = { case_sensitive_completions: false # set to true to enable case-sensitive completions enable_external_completion: true # set to false to prevent nushell looking into $env.PATH to find more suggestions, `false` recommended for WSL users as this look up my be very slow + # A strategy of managing table view in case of limited space. + table_trim: { + methodology: wrapping, # truncating + # A strategy which will be used by 'wrapping' methodology + wrapping_try_keep_words: true, + # A suffix which will be used with 'truncating' methodology + # truncating_suffix: "..." + } + hooks: { pre_prompt: [{ $nothing # replace with source code to run before the prompt is shown