diff --git a/Cargo.lock b/Cargo.lock index 82f923de85..3534a77293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ "getrandom 0.2.6", "once_cell", - "version_check 0.9.4", + "version_check", ] [[package]] @@ -69,22 +69,19 @@ 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" +name = "ansi-str" +version = "0.2.0" +source = "git+https://github.com/zhiburt/ansi-str?rev=655cd8125a032286082794690c2cc6dc835345b4#655cd8125a032286082794690c2cc6dc835345b4" dependencies = [ - "heapless 0.5.6", - "nom 4.2.3", + "ansi_rs", ] [[package]] -name = "ansi-str" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d04a41a1463228d6eed971b9cdadd86b5577c69c09fd2379c57fe5e159071" +name = "ansi_rs" +version = "0.1.0" +source = "git+https://gitlab.com/zhiburt/ansi_rs#f45b8378e5ac7bafc0ca2d2748505fe92e2af6d6" dependencies = [ - "ansi-parser", + "nom 7.1.1", ] [[package]] @@ -164,18 +161,6 @@ 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" @@ -317,7 +302,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -326,7 +311,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -386,6 +371,12 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + [[package]] name = "bytemuck" version = "1.9.1" @@ -724,7 +715,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ - "generic-array 0.14.5", + "generic-array", "typenum", ] @@ -856,7 +847,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -1329,24 +1320,6 @@ 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" @@ -1354,7 +1327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", - "version_check 0.9.4", + "version_check", ] [[package]] @@ -1483,15 +1456,6 @@ 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" @@ -1535,18 +1499,6 @@ 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" @@ -1554,7 +1506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a08e755adbc0ad283725b29f4a4883deee15336f372d5f61fae59efec40f983" dependencies = [ "atomic-polyfill", - "hash32 0.2.1", + "hash32", "rustc_version 0.4.0", "spin", "stable_deref_trait", @@ -2412,16 +2364,6 @@ 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" @@ -2722,7 +2664,7 @@ dependencies = [ name = "nu-pretty-hex" version = "0.65.1" dependencies = [ - "heapless 0.7.13", + "heapless", "nu-ansi-term", "rand 0.8.5", ] @@ -2771,6 +2713,7 @@ dependencies = [ "nu-protocol", "regex", "strip-ansi-escapes", + "tabled", "unicode-width", ] @@ -3092,6 +3035,17 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" +[[package]] +name = "papergrid" +version = "0.4.0" +source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423" +dependencies = [ + "ansi-str", + "bytecount", + "strip-ansi-escapes", + "unicode-width", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -3544,7 +3498,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "version_check 0.9.4", + "version_check", ] [[package]] @@ -3555,7 +3509,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "version_check 0.9.4", + "version_check", ] [[package]] @@ -4658,6 +4612,29 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "tabled" +version = "0.7.0" +source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423" +dependencies = [ + "ansi-str", + "papergrid", + "tabled_derive", + "unicode-width", +] + +[[package]] +name = "tabled_derive" +version = "0.3.0" +source = "git+https://github.com/zhiburt/tabled?rev=e3cbdea5b81edda8b3cc7b9f64f98fecae7db423#e3cbdea5b81edda8b3cc7b9f64f98fecae7db423" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -4952,7 +4929,7 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" dependencies = [ - "version_check 0.9.4", + "version_check", ] [[package]] @@ -5080,12 +5057,6 @@ 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-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 31dd18c530..062c44b19a 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -173,7 +173,8 @@ impl Command for Table { theme: load_theme_from_config(config), }; - let result = nu_table::draw_table(&table, term_width, &color_hm, config); + let result = nu_table::draw_table(&table, term_width, &color_hm, config) + .unwrap_or_else(|| format!("Couldn't fit table into {} columns!", term_width)); Ok(Value::String { val: result, @@ -556,7 +557,8 @@ impl Iterator for PagingTableCreator { match table { Ok(Some(table)) => { - let result = nu_table::draw_table(&table, term_width, &color_hm, &self.config); + let result = nu_table::draw_table(&table, term_width, &color_hm, &self.config) + .unwrap_or_else(|| format!("Couldn't fit table into {} columns!", term_width)); Some(Ok(result.as_bytes().to_vec())) } diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml index 28cb9d8fbc..11a9e11cf8 100644 --- a/crates/nu-table/Cargo.toml +++ b/crates/nu-table/Cargo.toml @@ -17,5 +17,8 @@ nu-protocol = { path = "../nu-protocol", version = "0.65.1" } regex = "1.4" unicode-width = "0.1.8" strip-ansi-escapes = "0.1.1" -ansi-str = "0.2.0" +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", +] } diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs index 4a98a1c3fb..22491ae9ed 100644 --- a/crates/nu-table/src/lib.rs +++ b/crates/nu-table/src/lib.rs @@ -5,5 +5,4 @@ mod wrap; pub use table::{draw_table, Table}; pub use table_theme::TableTheme; -pub use textstyle::{StyledString, TextStyle}; -pub use wrap::Alignment; +pub use textstyle::{Alignment, StyledString, TextStyle}; diff --git a/crates/nu-table/src/main.rs b/crates/nu-table/src/main.rs index 3fe9bdb3bb..f85ada3924 100644 --- a/crates/nu-table/src/main.rs +++ b/crates/nu-table/src/main.rs @@ -29,7 +29,8 @@ fn main() { // get the default config let config = Config::default(); // Capture the table as a string - let output_table = draw_table(&table, width, &color_hm, &config); + let output_table = draw_table(&table, width, &color_hm, &config) + .unwrap_or_else(|| format!("Couldn't fit table into {} columns!", width)); // Draw the table println!("{}", output_table) } diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index da02f1d011..0724faac25 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -1,16 +1,16 @@ use crate::table_theme::TableTheme; -use crate::wrap::{column_width, split_sublines, wrap, Alignment, Subline, WrappedCell}; -use crate::{StyledString, TextStyle}; -use nu_ansi_term::{AnsiString, AnsiStrings, Style}; +use crate::StyledString; +use nu_ansi_term::Style; use nu_protocol::{Config, FooterMode}; use std::collections::HashMap; -use std::fmt::Write; - -enum SeparatorPosition { - Top, - Middle, - Bottom, -} +use tabled::{ + builder::Builder, + formatting_settings::AlignmentStrategy, + object::{Cell, Columns, Rows}, + papergrid, + style::BorderColor, + Alignment, Modify, TableOption, +}; #[derive(Debug)] pub struct Table { @@ -33,669 +33,176 @@ impl Table { } } -#[derive(Debug)] -pub struct ProcessedTable { - pub headers: Vec, - pub data: Vec>, - pub theme: TableTheme, -} - -#[derive(Debug)] -pub struct ProcessedCell { - pub contents: Vec>, - pub style: TextStyle, -} - -#[derive(Debug)] -pub struct WrappedTable { - pub column_widths: Vec, - pub headers: Vec, - pub data: Vec>, - pub theme: TableTheme, - pub footer: Vec, -} - -impl WrappedTable { - fn print_separator( - &self, - separator_position: SeparatorPosition, - color_hm: &HashMap, - ) -> String { - let column_count = self.column_widths.len(); - let mut output: Vec = Vec::new(); - let sep_color = color_hm - .get("separator") - .unwrap_or(&Style::default()) - .to_owned(); - - match separator_position { - SeparatorPosition::Top => { - for column in self.column_widths.iter().enumerate() { - if column.0 == 0 && self.theme.print_left_border { - output.push(sep_color.paint(self.theme.top_left.to_string())); - } - - for _ in 0..*column.1 { - output.push(sep_color.paint(self.theme.top_horizontal.to_string())); - } - - output.push(sep_color.paint(self.theme.top_horizontal.to_string())); - output.push(sep_color.paint(self.theme.top_horizontal.to_string())); - if column.0 == column_count - 1 { - if self.theme.print_right_border { - output.push(sep_color.paint(self.theme.top_right.to_string())); - } - } else { - output.push(sep_color.paint(self.theme.top_center.to_string())); - } - } - output.push(AnsiString::from("\n".to_string())); - } - SeparatorPosition::Middle => { - for column in self.column_widths.iter().enumerate() { - if column.0 == 0 && self.theme.print_left_border { - output.push(sep_color.paint(self.theme.middle_left.to_string())); - } - - for _ in 0..*column.1 { - output.push(sep_color.paint(self.theme.middle_horizontal.to_string())); - } - - output.push(sep_color.paint(self.theme.middle_horizontal.to_string())); - output.push(sep_color.paint(self.theme.middle_horizontal.to_string())); - - if column.0 == column_count - 1 { - if self.theme.print_right_border { - output.push(sep_color.paint(self.theme.middle_right.to_string())); - } - } else { - output.push(sep_color.paint(self.theme.center.to_string())); - } - } - output.push(AnsiString::from("\n".to_string())); - } - SeparatorPosition::Bottom => { - for column in self.column_widths.iter().enumerate() { - if column.0 == 0 && self.theme.print_left_border { - output.push(sep_color.paint(self.theme.bottom_left.to_string())); - } - for _ in 0..*column.1 { - output.push(sep_color.paint(self.theme.bottom_horizontal.to_string())); - } - output.push(sep_color.paint(self.theme.bottom_horizontal.to_string())); - output.push(sep_color.paint(self.theme.bottom_horizontal.to_string())); - - if column.0 == column_count - 1 { - if self.theme.print_right_border { - output.push(sep_color.paint(self.theme.bottom_right.to_string())); - } - } else { - output.push(sep_color.paint(self.theme.bottom_center.to_string())); - } - } - } - } - AnsiStrings(&output[..]).to_string() - } - - fn print_cell_contents( - &self, - cells: &[WrappedCell], - color_hm: &HashMap, - ) -> String { - let sep_color = color_hm - .get("separator") - .unwrap_or(&Style::default()) - .to_owned(); - - let mut total_output = String::new(); - - for current_line in 0.. { - let mut lines_printed = 0; - - let mut output = String::new(); - if self.theme.print_left_border { - output.push_str( - &sep_color - .paint(&self.theme.left_vertical.to_string()) - .to_string(), - ); - } - - for column in cells.iter().enumerate() { - if let Some(line) = (column.1).lines.get(current_line) { - let remainder = self.column_widths[column.0] - line.width; - output.push(' '); - - match column.1.style.alignment { - Alignment::Left => { - if let Some(color) = column.1.style.color_style { - output.push_str(&color.paint(&line.line).to_string()); - } else { - output.push_str(&line.line); - } - for _ in 0..remainder { - output.push(' '); - } - } - Alignment::Center => { - for _ in 0..remainder / 2 { - output.push(' '); - } - if let Some(color) = column.1.style.color_style { - output.push_str(&color.paint(&line.line).to_string()); - } else { - output.push_str(&line.line); - } - for _ in 0..(remainder / 2 + remainder % 2) { - output.push(' '); - } - } - Alignment::Right => { - for _ in 0..remainder { - output.push(' '); - } - if let Some(color) = column.1.style.color_style { - output.push_str(&color.paint(&line.line).to_string()); - } else { - output.push_str(&line.line); - } - } - } - output.push(' '); - lines_printed += 1; - } else { - for _ in 0..self.column_widths[column.0] + 2 { - output.push(' '); - } - } - if column.0 < cells.len() - 1 { - output.push_str( - &sep_color - .paint(&self.theme.center_vertical.to_string()) - .to_string(), - ); - } else if self.theme.print_right_border { - output.push_str( - &sep_color - .paint(&self.theme.right_vertical.to_string()) - .to_string(), - ); - } - } - - if lines_printed == 0 { - break; - } - - writeln!(&mut total_output, "{}", output).expect("writing should be done to buffer"); - } - total_output - } - - fn print_table(&self, color_hm: &HashMap, config: &Config) -> String { - let mut output = String::new(); - - // TODO: This may be unnecessary after JTs changes. Let's remove it and see. - // #[cfg(windows)] - // { - // let _ = nu_ansi_term::enable_ansi_support(); - // } - - if self.data.is_empty() { - return output; - } - - // The top border - if self.theme.print_top_border { - output.push_str(&self.print_separator(SeparatorPosition::Top, color_hm)); - } - - // The header - let skip_headers = (self.headers.len() == 2 && self.headers[1].max_width == 0) - || (self.headers.len() == 1 && self.headers[0].max_width == 0); - - if !self.headers.is_empty() && !skip_headers { - output.push_str(&self.print_cell_contents(&self.headers, color_hm)); - } - - // The middle section - let mut first_row = true; - for row in &self.data { - if !first_row { - if self.theme.separate_rows { - output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm)); - } - } else { - first_row = false; - - if self.theme.separate_header && !self.headers.is_empty() && !skip_headers { - output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm)); - } - } - - output.push_str(&self.print_cell_contents(row, color_hm)); - } - - match config.footer_mode { - FooterMode::Always => { - if self.theme.separate_header && !self.headers.is_empty() && !skip_headers { - output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm)); - } - - if !self.headers.is_empty() && !skip_headers { - output.push_str(&self.print_cell_contents(&self.footer, color_hm)); - } - } - FooterMode::RowCount(r) => { - if self.data.len() as u64 > r { - if self.theme.separate_header && !self.headers.is_empty() && !skip_headers { - output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm)); - } - - if !self.headers.is_empty() && !skip_headers { - output.push_str(&self.print_cell_contents(&self.footer, color_hm)); - } - } - } - _ => {} // Never and Auto aka auto get eaten and nothing happens - } - - // The table finish - if self.theme.print_bottom_border { - output.push_str(&self.print_separator(SeparatorPosition::Bottom, color_hm)); - } - - // 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 - if let Ok(bytes) = strip_ansi_escapes::strip(&output) { - String::from_utf8_lossy(&bytes).to_string() - } else { - output - } - } else { - // Draw the table with ansi colors - output - } - } -} - -fn process_table(table: &Table) -> ProcessedTable { - let mut processed_data = vec![]; - for row in &table.data { - let mut out_row = vec![]; - for column in row { - let cleaned = clean(&column.contents); - out_row.push(ProcessedCell { - contents: split_sublines(&cleaned), - style: column.style, - }); - } - processed_data.push(out_row); - } - - let mut processed_headers = vec![]; - for header in &table.headers { - let cleaned = clean(&header.contents); - processed_headers.push(ProcessedCell { - contents: split_sublines(&cleaned), - style: header.style, - }); - } - - ProcessedTable { - headers: processed_headers, - data: processed_data, - theme: table.theme.clone(), - } -} - -fn clean(input: &str) -> String { - let input = input.replace('\r', ""); - - input.replace('\t', " ") -} - -fn get_max_column_widths(processed_table: &ProcessedTable) -> Vec { - use std::cmp::max; - - let mut max_num_columns = 0; - - max_num_columns = max(max_num_columns, processed_table.headers.len()); - - for row in &processed_table.data { - max_num_columns = max(max_num_columns, row.len()); - } - - let mut output = vec![0; max_num_columns]; - - for column in processed_table.headers.iter().enumerate() { - output[column.0] = max(output[column.0], column_width(&column.1.contents)); - } - - for row in &processed_table.data { - for column in row.iter().enumerate() { - output[column.0] = max(output[column.0], column_width(&column.1.contents)); - } - } - - output -} - -pub fn maybe_truncate_columns(termwidth: usize, processed_table: &mut ProcessedTable) { - // 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 < processed_table.headers.len() { - processed_table.headers.truncate(max_num_of_columns); - - for entry in processed_table.data.iter_mut() { - entry.truncate(max_num_of_columns); - } - - processed_table.headers.push(ProcessedCell { - contents: vec![vec![Subline { - subline: "...".to_string(), - width: 3, - }]], - style: TextStyle::basic_center(), - }); - - for entry in processed_table.data.iter_mut() { - entry.push(ProcessedCell { - contents: vec![vec![Subline { - subline: "...".to_string(), - width: 3, - }]], - style: TextStyle::basic_center(), - }); // ellipsis is centred - } - } -} - pub fn draw_table( table: &Table, termwidth: usize, color_hm: &HashMap, config: &Config, -) -> String { +) -> Option { // Remove the edges, if used - let edges_width = if table.theme.print_left_border && table.theme.print_right_border { - 3 - } else if table.theme.print_left_border || table.theme.print_right_border { - 1 + let (headers, data) = crate::wrap::wrap(&table.headers, &table.data, termwidth, &table.theme)?; + let headers = if headers.is_empty() { + None } else { - 0 + Some(headers) }; - if termwidth < edges_width { - return format!("Couldn't fit table into {} columns!", termwidth); - } + let alignments = build_alignment_map(&table.data); - let raw_termwidth = termwidth; - let termwidth = termwidth - edges_width; + let theme = &table.theme; - let mut processed_table = process_table(table); + let with_header = headers.is_some(); + let with_footer = with_header && need_footer(config, data.len() as u64); - let max_per_column = get_max_column_widths(&processed_table); + let table = build_table(data, headers, Some(alignments), config, with_footer); + let table = load_theme(table, color_hm, theme, with_footer, with_header); - maybe_truncate_columns(termwidth, &mut processed_table); - - let headers_len = processed_table.headers.len(); - - // fix the length of the table if there are no headers: - let headers_len = if headers_len == 0 { - if !table.data.is_empty() && !table.data[0].is_empty() { - table.data[0].len() - } else { - return String::new(); - } - } else { - 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 = match max_column_width { - None => return format!("Couldn't fit table into {} columns!", raw_termwidth), - Some(max_column_width) => 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 wrapped_table = match max_column_width { - None => return format!("Couldn't fit table into {} columns!", raw_termwidth), - Some(max_column_width) => wrap_cells( - processed_table, - max_column_width, - color_hm, - &re_leading, - &re_trailing, - ), - }; - - wrapped_table.print_table(color_hm, config) + print_table(table, termwidth) } -fn wrap_cells( - processed_table: ProcessedTable, - max_column_width: usize, +fn print_table(table: tabled::Table, term_width: usize) -> Option { + let s = table.to_string(); + + let width = s.lines().next().map(papergrid::string_width).unwrap_or(0); + if width > term_width { + return None; + } + + Some(s) +} + +fn build_alignment_map(data: &[Vec]) -> Vec> { + let mut v = vec![Vec::new(); data.len()]; + for (i, row) in data.iter().enumerate() { + let mut row_alignments = Vec::with_capacity(row.len()); + for col in row { + row_alignments.push(Alignment::Horizontal(col.style.alignment)); + } + + v[i] = row_alignments; + } + + v +} + +fn build_table( + data: Vec>, + headers: Option>, + alignment_map: Option>>, + config: &Config, + need_footer: bool, +) -> tabled::Table { + let header_present = headers.is_some(); + let mut builder = Builder::from(data); + + if let Some(headers) = headers { + builder = builder.set_columns(headers.clone()); + + if need_footer { + builder = builder.add_record(headers); + } + } + + let mut table = builder.build(); + + table = table.with( + Modify::new(Rows::new(1..)) + .with(Alignment::left()) + .with(AlignmentStrategy::PerLine), + ); + + if !config.disable_table_indexes { + table = table.with(Modify::new(Columns::first()).with(Alignment::right())); + } + + if header_present { + table = table.with(Modify::new(Rows::first()).with(Alignment::center())); + } + + if let Some(alignments) = alignment_map { + table = apply_alignments(table, alignments, header_present); + } + + table +} + +fn apply_alignments( + mut table: tabled::Table, + alignment: Vec>, + header_present: bool, +) -> tabled::Table { + let offset = if header_present { 1 } else { 0 }; + for (row, alignments) in alignment.into_iter().enumerate() { + for (col, alignment) in alignments.into_iter().enumerate() { + table = table.with(Modify::new(Cell(row + offset, col)).with(alignment)); + } + } + + table +} + +fn load_theme( + mut table: tabled::Table, color_hm: &HashMap, - re_leading: ®ex::Regex, - re_trailing: ®ex::Regex, -) -> WrappedTable { - let mut column_widths = vec![ - 0; - std::cmp::max( - processed_table.headers.len(), - if !processed_table.data.is_empty() { - processed_table.data[0].len() - } else { - 0 - } - ) - ]; - let mut output_headers = vec![]; - for header in processed_table.headers.into_iter().enumerate() { - let mut wrapped = WrappedCell { - lines: vec![], - max_width: 0, - style: header.1.style, - }; + theme: &TableTheme, + with_footer: bool, + with_header: bool, +) -> tabled::Table { + table = table.with(theme.theme.clone()); - for contents in header.1.contents.into_iter() { - let (mut lines, inner_max_width) = wrap( - max_column_width, - contents.into_iter(), - color_hm, - re_leading, - re_trailing, - ); - wrapped.lines.append(&mut lines); - if inner_max_width > wrapped.max_width { - wrapped.max_width = inner_max_width; - } + if let Some(color) = color_hm.get("separator") { + let color = color.paint(" ").to_string(); + if let Ok(color) = BorderColor::try_from(color) { + table = table.with(color); } - if column_widths[header.0] < wrapped.max_width { - column_widths[header.0] = wrapped.max_width; - } - output_headers.push(wrapped); } - let mut output_data = vec![]; - for row in processed_table.data.into_iter() { - let mut output_row = vec![]; - for column in row.into_iter().enumerate() { - let mut wrapped = WrappedCell { - lines: vec![], - max_width: 0, - style: column.1.style, - }; - for contents in column.1.contents.into_iter() { - let (mut lines, inner_max_width) = wrap( - max_column_width, - contents.into_iter(), - color_hm, - re_leading, - re_trailing, - ); - wrapped.lines.append(&mut lines); - if inner_max_width > wrapped.max_width { - wrapped.max_width = inner_max_width; - } - } - if column_widths[column.0] < wrapped.max_width { - column_widths[column.0] = wrapped.max_width; - } - output_row.push(wrapped); - } - output_data.push(output_row); + if with_footer { + table = table.with(FooterStyle).with( + Modify::new(Rows::last()) + .with(Alignment::center()) + .with(AlignmentStrategy::PerCell), + ); } - let mut footer = vec![ - WrappedCell { - lines: vec![], - max_width: 0, - style: TextStyle { - ..Default::default() - }, - }; - output_headers.len() - ]; - footer.clone_from_slice(&output_headers[..]); + if !with_header { + table = table.with(RemoveHeaderLine); + } - WrappedTable { - column_widths, - headers: output_headers, - data: output_data, - theme: processed_table.theme, - footer, + table +} + +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) +} + +struct FooterStyle; + +impl TableOption for FooterStyle { + fn change(&mut self, grid: &mut papergrid::Grid) { + if grid.count_columns() == 0 || grid.count_rows() == 0 { + return; + } + + let mut line = papergrid::Line::default(); + + let border = grid.get_border((0, 0)); + line.left = border.left_bottom_corner; + line.intersection = border.right_bottom_corner; + line.horizontal = border.bottom; + + let border = grid.get_border((0, grid.count_columns() - 1)); + line.right = border.right_bottom_corner; + + grid.set_split_line(grid.count_rows() - 1, line); } } -struct ColumnSpace { - num_overages: usize, - underage_sum: usize, - overage_separator_sum: usize, -} +struct RemoveHeaderLine; -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) - } +impl TableOption for RemoveHeaderLine { + fn change(&mut self, grid: &mut papergrid::Grid) { + grid.set_split_line(1, papergrid::Line::default()); } } diff --git a/crates/nu-table/src/table_theme.rs b/crates/nu-table/src/table_theme.rs index 00d96f3190..2c0fa9a6db 100644 --- a/crates/nu-table/src/table_theme.rs +++ b/crates/nu-table/src/table_theme.rs @@ -1,329 +1,130 @@ +use tabled::{style::StyleConfig, Style}; + #[derive(Debug, Clone)] pub struct TableTheme { - pub top_left: char, - pub middle_left: char, - pub bottom_left: char, - pub top_center: char, - pub center: char, - pub bottom_center: char, - pub top_right: char, - pub middle_right: char, - pub bottom_right: char, - pub top_horizontal: char, - pub middle_horizontal: char, - pub bottom_horizontal: char, - pub left_vertical: char, - pub center_vertical: char, - pub right_vertical: char, - - pub separate_header: bool, - pub separate_rows: bool, - - pub print_left_border: bool, - pub print_right_border: bool, - pub print_top_border: bool, - pub print_bottom_border: bool, + pub(crate) theme: StyleConfig, + pub(crate) is_left_set: bool, + pub(crate) is_right_set: bool, } impl TableTheme { - #[allow(unused)] pub fn basic() -> TableTheme { - TableTheme { - top_left: '+', - middle_left: '+', - bottom_left: '+', - top_center: '+', - center: '+', - bottom_center: '+', - top_right: '+', - middle_right: '+', - bottom_right: '+', - top_horizontal: '-', - middle_horizontal: '-', - bottom_horizontal: '-', - left_vertical: '|', - center_vertical: '|', - right_vertical: '|', - - separate_header: true, - separate_rows: true, - - print_left_border: true, - print_right_border: true, - print_top_border: true, - print_bottom_border: true, + Self { + theme: Style::ascii().into(), + is_left_set: true, + is_right_set: true, } } - #[allow(unused)] pub fn thin() -> TableTheme { - TableTheme { - top_left: '┌', - middle_left: '├', - bottom_left: '└', - top_center: '┬', - center: '┼', - bottom_center: '┴', - top_right: '┐', - middle_right: '┤', - bottom_right: '┘', - - top_horizontal: '─', - middle_horizontal: '─', - bottom_horizontal: '─', - - left_vertical: '│', - center_vertical: '│', - right_vertical: '│', - - separate_header: true, - separate_rows: true, - - print_left_border: true, - print_right_border: true, - print_top_border: true, - print_bottom_border: true, + Self { + theme: Style::modern().into(), + is_left_set: true, + is_right_set: true, } } - #[allow(unused)] pub fn light() -> TableTheme { - TableTheme { - top_left: ' ', - middle_left: '─', - bottom_left: ' ', - top_center: ' ', - center: '─', - bottom_center: ' ', - top_right: ' ', - middle_right: '─', - bottom_right: ' ', - - top_horizontal: ' ', - middle_horizontal: '─', - bottom_horizontal: ' ', - - left_vertical: ' ', - center_vertical: ' ', - right_vertical: ' ', - - separate_header: true, - separate_rows: false, - - print_left_border: true, - print_right_border: true, - print_top_border: false, - print_bottom_border: true, + Self { + theme: Style::blank().header('─').into(), + is_left_set: false, + is_right_set: false, } } - #[allow(unused)] pub fn compact() -> TableTheme { - TableTheme { - top_left: '─', - middle_left: '─', - bottom_left: '─', - top_center: '┬', - center: '┼', - bottom_center: '┴', - top_right: '─', - middle_right: '─', - bottom_right: '─', - top_horizontal: '─', - middle_horizontal: '─', - bottom_horizontal: '─', - - left_vertical: ' ', - center_vertical: '│', - right_vertical: ' ', - - separate_header: true, - separate_rows: false, - - print_left_border: false, - print_right_border: false, - print_top_border: true, - print_bottom_border: true, + Self { + theme: Style::modern() + .left_off() + .right_off() + .horizontal_off() + .into(), + is_left_set: false, + is_right_set: false, } } - #[allow(unused)] pub fn with_love() -> TableTheme { - TableTheme { - top_left: '❤', - middle_left: '❤', - bottom_left: '❤', - top_center: '❤', - center: '❤', - bottom_center: '❤', - top_right: '❤', - middle_right: '❤', - bottom_right: '❤', - top_horizontal: '❤', - middle_horizontal: '❤', - bottom_horizontal: '❤', - - left_vertical: ' ', - center_vertical: '❤', - right_vertical: ' ', - - separate_header: true, - separate_rows: false, - - print_left_border: false, - print_right_border: false, - print_top_border: true, - print_bottom_border: true, + Self { + theme: Style::psql() + .header('❤') + .top('❤') + .bottom('❤') + .vertical('❤') + .into(), + is_left_set: false, + is_right_set: false, } } - #[allow(unused)] pub fn compact_double() -> TableTheme { - TableTheme { - top_left: '═', - middle_left: '═', - bottom_left: '═', - top_center: '╦', - center: '╬', - bottom_center: '╩', - top_right: '═', - middle_right: '═', - bottom_right: '═', - top_horizontal: '═', - middle_horizontal: '═', - bottom_horizontal: '═', - - left_vertical: ' ', - center_vertical: '║', - right_vertical: ' ', - - separate_header: true, - separate_rows: false, - - print_left_border: false, - print_right_border: false, - print_top_border: true, - print_bottom_border: true, + Self { + theme: Style::psql() + .header('═') + .top('═') + .bottom('═') + .vertical('║') + .top_intersection('╦') + .bottom_intersection('╩') + .header_intersection('╬') + .into(), + is_left_set: false, + is_right_set: false, } } - #[allow(unused)] pub fn rounded() -> TableTheme { - TableTheme { - top_left: '╭', - middle_left: '├', - bottom_left: '╰', - top_center: '┬', - center: '┼', - bottom_center: '┴', - top_right: '╮', - middle_right: '┤', - bottom_right: '╯', - top_horizontal: '─', - middle_horizontal: '─', - bottom_horizontal: '─', - - left_vertical: '│', - center_vertical: '│', - right_vertical: '│', - - separate_header: true, - separate_rows: false, - - print_left_border: true, - print_right_border: true, - print_top_border: true, - print_bottom_border: true, + Self { + theme: Style::rounded().into(), + is_left_set: true, + is_right_set: true, } } - #[allow(unused)] pub fn reinforced() -> TableTheme { - TableTheme { - top_left: '┏', - middle_left: '├', - bottom_left: '┗', - top_center: '┬', - center: '┼', - bottom_center: '┴', - top_right: '┓', - middle_right: '┤', - bottom_right: '┛', - top_horizontal: '─', - middle_horizontal: '─', - bottom_horizontal: '─', - - left_vertical: '│', - center_vertical: '│', - right_vertical: '│', - - separate_header: true, - separate_rows: false, - - print_left_border: true, - print_right_border: true, - print_top_border: true, - print_bottom_border: true, + Self { + theme: Style::modern() + .top_left_corner('┏') + .top_right_corner('┓') + .bottom_left_corner('┗') + .bottom_right_corner('┛') + .horizontal_off() + .into(), + is_left_set: true, + is_right_set: true, } } - #[allow(unused)] pub fn heavy() -> TableTheme { - TableTheme { - top_left: '┏', - middle_left: '┣', - bottom_left: '┗', - top_center: '┳', - center: '╋', - bottom_center: '┻', - top_right: '┓', - middle_right: '┫', - bottom_right: '┛', - top_horizontal: '━', - middle_horizontal: '━', - bottom_horizontal: '━', - - left_vertical: '┃', - center_vertical: '┃', - right_vertical: '┃', - - separate_header: true, - separate_rows: false, - - print_left_border: true, - print_right_border: true, - print_top_border: true, - print_bottom_border: true, + Self { + theme: Style::modern() + .header('━') + .top('━') + .bottom('━') + .vertical('┃') + .left('┃') + .right('┃') + .left_intersection('┣') + .right_intersection('┫') + .bottom_intersection('┻') + .top_intersection('┳') + .top_left_corner('┏') + .top_right_corner('┓') + .bottom_left_corner('┗') + .bottom_right_corner('┛') + .header_intersection('╋') + .horizontal_off() + .into(), + is_left_set: true, + is_right_set: true, } } - #[allow(unused)] + pub fn none() -> TableTheme { - TableTheme { - top_left: ' ', - middle_left: ' ', - bottom_left: ' ', - top_center: ' ', - center: ' ', - bottom_center: ' ', - top_right: ' ', - middle_right: ' ', - bottom_right: ' ', - - top_horizontal: ' ', - middle_horizontal: ' ', - bottom_horizontal: ' ', - - left_vertical: ' ', - center_vertical: ' ', - right_vertical: ' ', - - separate_header: false, - separate_rows: false, - - print_left_border: false, - print_right_border: false, - print_top_border: false, - print_bottom_border: false, + Self { + theme: Style::blank().into(), + is_left_set: false, + is_right_set: false, } } } diff --git a/crates/nu-table/src/textstyle.rs b/crates/nu-table/src/textstyle.rs index 821615c989..1f60982d95 100644 --- a/crates/nu-table/src/textstyle.rs +++ b/crates/nu-table/src/textstyle.rs @@ -1,6 +1,7 @@ -use crate::wrap::Alignment; use nu_ansi_term::{Color, Style}; +pub type Alignment = tabled::AlignmentHorizontal; + #[derive(Debug, Clone, Copy)] pub struct TextStyle { pub alignment: Alignment, @@ -239,7 +240,7 @@ impl Default for TextStyle { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct StyledString { pub contents: String, pub style: TextStyle, diff --git a/crates/nu-table/src/wrap.rs b/crates/nu-table/src/wrap.rs index 058b02ac49..79f5759115 100644 --- a/crates/nu-table/src/wrap.rs +++ b/crates/nu-table/src/wrap.rs @@ -1,18 +1,12 @@ 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::{fmt::Display, iter::Iterator}; +use std::iter::Iterator; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; -#[derive(Debug, Clone, Copy)] -pub enum Alignment { - Left, - Center, - Right, -} - #[derive(Debug)] pub struct Subline { pub subline: String, @@ -39,21 +33,6 @@ pub struct WrappedCell { pub style: TextStyle, } -impl Display for Line { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut first = true; - for subline in &self.sublines { - if !first { - write!(f, " ")?; - } else { - first = false; - } - write!(f, "{}", subline.subline)?; - } - Ok(()) - } -} - /// Removes ANSI escape codes and some ASCII control characters /// /// Keeps `\n` removes `\r`, `\t` etc. @@ -181,7 +160,7 @@ fn split_word(cell_width: usize, word: &str) -> Vec { output } -pub fn wrap( +pub fn wrap_content( cell_width: usize, mut input: impl Iterator, color_hm: &HashMap, @@ -318,3 +297,353 @@ pub fn wrap( (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', " ") +}