Move wrap responsibility on tabled (#5999)

* nu_table/ Replace wrap.rs logic by tabled::Width::wrap

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* nu_table: Rename wrap.rs to width_control.rs

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* nu_table: Add configuration of trimming

```
let-env config = ($env.config | upsert table_trim { methodology: 'wrapping', wrapping_try_keep_words: false })
let-env config = ($env.config | upsert table_trim { methodology: 'truncating', truncatting_suffix: '...@@...' })
```

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* nu_table: Fix right padding issue

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* nu_table: Fix trancate issue

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* nu_table: Fix spelling in config

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* nu_table: Update tabled dependency

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>

* Update default_config.nu with a table_trim options

Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com>
This commit is contained in:
Maxim Zhiburt 2022-07-12 19:23:50 +03:00 committed by GitHub
parent b9bbf0c10f
commit 217c2bae99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 586 additions and 684 deletions

127
Cargo.lock generated
View file

@ -35,7 +35,7 @@ checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [ dependencies = [
"getrandom 0.2.6", "getrandom 0.2.6",
"once_cell", "once_cell",
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -68,6 +68,32 @@ version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0" 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]] [[package]]
name = "ansi-str" name = "ansi-str"
version = "0.2.0" version = "0.2.0"
@ -161,6 +187,18 @@ dependencies = [
"strength_reduce", "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]] [[package]]
name = "assert_cmd" name = "assert_cmd"
version = "2.0.4" version = "2.0.4"
@ -321,7 +359,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.5",
] ]
[[package]] [[package]]
@ -330,7 +368,7 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.5",
] ]
[[package]] [[package]]
@ -754,7 +792,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.5",
"typenum", "typenum",
] ]
@ -886,7 +924,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [ dependencies = [
"generic-array", "generic-array 0.14.5",
] ]
[[package]] [[package]]
@ -1365,6 +1403,24 @@ dependencies = [
"byteorder", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.5" version = "0.14.5"
@ -1372,7 +1428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -1501,6 +1557,15 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "hash32"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "hash32" name = "hash32"
version = "0.2.1" version = "0.2.1"
@ -1544,6 +1609,18 @@ dependencies = [
"hashbrown 0.11.2", "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]] [[package]]
name = "heapless" name = "heapless"
version = "0.7.13" version = "0.7.13"
@ -1551,7 +1628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a08e755adbc0ad283725b29f4a4883deee15336f372d5f61fae59efec40f983" checksum = "8a08e755adbc0ad283725b29f4a4883deee15336f372d5f61fae59efec40f983"
dependencies = [ dependencies = [
"atomic-polyfill", "atomic-polyfill",
"hash32", "hash32 0.2.1",
"rustc_version 0.4.0", "rustc_version 0.4.0",
"spin", "spin",
"stable_deref_trait", "stable_deref_trait",
@ -2427,6 +2504,16 @@ version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" 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]] [[package]]
name = "nom" name = "nom"
version = "7.1.1" version = "7.1.1"
@ -2726,7 +2813,7 @@ dependencies = [
name = "nu-pretty-hex" name = "nu-pretty-hex"
version = "0.65.1" version = "0.65.1"
dependencies = [ dependencies = [
"heapless", "heapless 0.7.13",
"nu-ansi-term", "nu-ansi-term",
"rand 0.8.5", "rand 0.8.5",
] ]
@ -2769,7 +2856,7 @@ dependencies = [
name = "nu-table" name = "nu-table"
version = "0.65.1" version = "0.65.1"
dependencies = [ dependencies = [
"ansi-str", "ansi-str 0.2.0 (git+https://github.com/zhiburt/ansi-str?rev=655cd8125a032286082794690c2cc6dc835345b4)",
"atty", "atty",
"nu-ansi-term", "nu-ansi-term",
"nu-protocol", "nu-protocol",
@ -3100,9 +3187,9 @@ checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
[[package]] [[package]]
name = "papergrid" name = "papergrid"
version = "0.4.0" 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 = [ dependencies = [
"ansi-str", "ansi-str 0.1.1",
"bytecount", "bytecount",
"strip-ansi-escapes", "strip-ansi-escapes",
"unicode-width", "unicode-width",
@ -3566,7 +3653,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -3577,7 +3664,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -4718,9 +4805,9 @@ dependencies = [
[[package]] [[package]]
name = "tabled" name = "tabled"
version = "0.7.0" 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 = [ dependencies = [
"ansi-str", "ansi-str 0.2.0 (git+https://github.com/zhiburt/ansi-str?branch=master)",
"papergrid", "papergrid",
"tabled_derive", "tabled_derive",
"unicode-width", "unicode-width",
@ -4729,7 +4816,7 @@ dependencies = [
[[package]] [[package]]
name = "tabled_derive" name = "tabled_derive"
version = "0.3.0" 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 = [ dependencies = [
"heck 0.4.0", "heck 0.4.0",
"proc-macro-error", "proc-macro-error",
@ -5042,7 +5129,7 @@ version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622"
dependencies = [ dependencies = [
"version_check", "version_check 0.9.4",
] ]
[[package]] [[package]]
@ -5170,6 +5257,12 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc1631c774f0f9570797191e01247cbefde789eebfbf128074cb934115a6133" checksum = "5fc1631c774f0f9570797191e01247cbefde789eebfbf128074cb934115a6133"
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View file

@ -2,6 +2,10 @@ use crate::{ShellError, Span, Value};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; 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 /// Definition of a parsed keybinding from the config object
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ParsedKeybinding { pub struct ParsedKeybinding {
@ -75,6 +79,7 @@ pub struct Config {
pub cd_with_abbreviations: bool, pub cd_with_abbreviations: bool,
pub case_sensitive_completions: bool, pub case_sensitive_completions: bool,
pub enable_external_completion: bool, pub enable_external_completion: bool,
pub trim_strategy: TrimStrategy,
} }
impl Default for Config { impl Default for Config {
@ -107,6 +112,7 @@ impl Default for Config {
cd_with_abbreviations: false, cd_with_abbreviations: false,
case_sensitive_completions: false, case_sensitive_completions: false,
enable_external_completion: true, enable_external_completion: true,
trim_strategy: TRIM_STRATEGY_DEFAULT,
} }
} }
} }
@ -131,6 +137,30 @@ pub enum HistoryFileFormat {
PlainText, 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<String>,
},
}
impl Value { impl Value {
pub fn into_config(self) -> Result<Config, ShellError> { pub fn into_config(self) -> Result<Config, ShellError> {
let v = self.as_record(); let v = self.as_record();
@ -348,6 +378,7 @@ impl Value {
eprintln!("$config.enable_external_completion is not a bool") eprintln!("$config.enable_external_completion is not a bool")
} }
} }
"table_trim" => config.trim_strategy = try_parse_trim_strategy(value, &config)?,
x => { x => {
eprintln!("$config.{} is an unknown config setting", 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<TrimStrategy, ShellError> {
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<TrimStrategy> {
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<HashMap<String, Value>, ShellError> { fn create_map(value: &Value, config: &Config) -> Result<HashMap<String, Value>, ShellError> {
let (cols, inner_vals) = value.as_record()?; let (cols, inner_vals) = value.as_record()?;
let mut hm: HashMap<String, Value> = HashMap::new(); let mut hm: HashMap<String, Value> = HashMap::new();

View file

@ -19,6 +19,4 @@ unicode-width = "0.1.8"
strip-ansi-escapes = "0.1.1" strip-ansi-escapes = "0.1.1"
ansi-str = { git = "https://github.com/zhiburt/ansi-str", rev = "655cd8125a032286082794690c2cc6dc835345b4" } ansi-str = { git = "https://github.com/zhiburt/ansi-str", rev = "655cd8125a032286082794690c2cc6dc835345b4" }
atty = "0.2.14" atty = "0.2.14"
tabled = { git = "https://github.com/zhiburt/tabled", rev = "e3cbdea5b81edda8b3cc7b9f64f98fecae7db423", features = [ tabled = { git = "https://github.com/zhiburt/tabled", branch = "master", features = ["color"] }
"color",
] }

View file

@ -1,7 +1,7 @@
mod table; mod table;
mod table_theme; mod table_theme;
mod textstyle; mod textstyle;
mod wrap; mod width_control;
pub use table::{draw_table, Table}; pub use table::{draw_table, Table};
pub use table_theme::TableTheme; pub use table_theme::TableTheme;

View file

@ -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 std::collections::HashMap;
use nu_ansi_term::Style;
use nu_protocol::{Config, FooterMode, TrimStrategy};
use tabled::{ use tabled::{
builder::Builder, builder::Builder,
formatting_settings::AlignmentStrategy, formatting_settings::AlignmentStrategy,
object::{Cell, Columns, Rows}, object::{Cell, Columns, Rows},
papergrid, papergrid,
style::BorderColor, 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)] #[derive(Debug)]
@ -39,27 +44,89 @@ pub fn draw_table(
color_hm: &HashMap<String, Style>, color_hm: &HashMap<String, Style>,
config: &Config, config: &Config,
) -> Option<String> { ) -> Option<String> {
// Remove the edges, if used let termwidth = fix_termwidth(termwidth, &table.theme)?;
let (headers, data) = crate::wrap::wrap(&table.headers, &table.data, 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() { let headers = if headers.is_empty() {
None None
} else { } else {
Some(headers) Some(headers)
}; };
let alignments = build_alignment_map(&table.data);
let theme = &table.theme; let theme = &table.theme;
let with_header = headers.is_some(); let with_header = headers.is_some();
let with_footer = with_header && need_footer(config, data.len() as u64); 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 = build_table(data, headers, Some(alignments), config, with_footer);
let table = load_theme(table, color_hm, theme, with_footer, with_header); 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()) 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<Vec<StyledString>>,
count_headers: usize,
) -> Vec<Vec<String>> {
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<StyledString>) -> Vec<String> {
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<StyledString>]) -> Vec<Vec<Alignment>> { fn build_alignment_map(data: &[Vec<StyledString>]) -> Vec<Vec<Alignment>> {
let mut v = vec![Vec::new(); data.len()]; let mut v = vec![Vec::new(); data.len()];
for (i, row) in data.iter().enumerate() { for (i, row) in data.iter().enumerate() {
@ -195,3 +262,89 @@ impl TableOption for RemoveHeaderLine {
grid.set_split_line(1, papergrid::Line::default()); 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::<tabled::width::PriorityMax>();
w.change(grid)
}
TrimStrategy::Truncate { suffix } => {
let mut w =
Width::truncate(self.termwidth).priority::<tabled::width::PriorityMax>();
if let Some(suffix) = suffix {
w = w.suffix(suffix);
}
w.change(grid);
}
};
}
}
fn table_fix_lengths(
headers: &[StyledString],
data: &[Vec<StyledString>],
) -> (Vec<StyledString>, Vec<Vec<StyledString>>) {
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<StyledString>]) -> usize {
let mut length = headers.len();
for row in data {
length = std::cmp::max(length, row.len());
}
length
}

View file

@ -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<StyledString>,
data: &mut [Vec<StyledString>],
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<StyledString>],
termwidth: usize,
) -> Option<usize> {
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<usize> {
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<StyledString>]) -> Vec<usize> {
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<usize> {
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', " ")
}

View file

@ -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<Subline>,
pub width: usize,
}
#[derive(Debug, Clone)]
pub struct WrappedLine {
pub line: String,
pub width: usize,
}
#[derive(Debug, Clone)]
pub struct WrappedCell {
pub lines: Vec<WrappedLine>,
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<str> {
// 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<Vec<Subline>> {
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::<Vec<_>>()
})
.collect::<Vec<_>>()
}
pub fn column_width(input: &[Vec<Subline>]) -> 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<Subline> {
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<Item = Subline>,
color_hm: &HashMap<String, Style>,
re_leading: &regex::Regex,
re_trailing: &regex::Regex,
) -> (Vec<WrappedLine>, usize) {
let mut lines = vec![];
let mut current_line: Vec<Subline> = 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, &current_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, &current_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(&current_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(&current_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<StyledString>],
termwidth: usize,
theme: &TableTheme,
) -> Option<(Vec<String>, Vec<Vec<String>>)> {
// 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<beginsp>^\s+)").expect("error with leading space regex");
let re_trailing =
regex::Regex::new(r"(?P<endsp>\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<Vec<Subline>>,
pub style: TextStyle,
}
fn split_lines(
headers: &[StyledString],
data: &[Vec<StyledString>],
) -> (Vec<ContentLines>, Vec<Vec<ContentLines>>) {
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<ContentLines>]) -> Vec<usize> {
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<ContentLines>,
data_splited: Vec<Vec<ContentLines>>,
max_column_width: usize,
re_leading: &regex::Regex,
re_trailing: &regex::Regex,
) -> (Vec<String>, Vec<Vec<String>>) {
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::<Vec<_>>()
.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::<Vec<_>>()
.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<ContentLines>,
data: &mut [Vec<ContentLines>],
) {
// 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<usize> {
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', " ")
}

View file

@ -257,6 +257,15 @@ let-env config = {
case_sensitive_completions: false # set to true to enable case-sensitive completions 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 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: { hooks: {
pre_prompt: [{ pre_prompt: [{
$nothing # replace with source code to run before the prompt is shown $nothing # replace with source code to run before the prompt is shown