diff --git a/Cargo.lock b/Cargo.lock index bec2113661..87e87af30b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2472,6 +2472,7 @@ dependencies = [ "nu-plugin", "nu-protocol", "nu-source", + "nu-table", "nu-test-support", "nu-value-ext", "num-bigint", @@ -2611,6 +2612,14 @@ dependencies = [ "termcolor", ] +[[package]] +name = "nu-table" +version = "0.15.1" +dependencies = [ + "ansi_term 0.12.1", + "unicode-width", +] + [[package]] name = "nu-test-support" version = "0.15.1" diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index c55dfa655a..d9c8c36383 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -17,6 +17,7 @@ nu-errors = { version = "0.15.1", path = "../nu-errors" } nu-parser = { version = "0.15.1", path = "../nu-parser" } nu-value-ext = { version = "0.15.1", path = "../nu-value-ext" } nu-test-support = { version = "0.15.1", path = "../nu-test-support" } +nu-table = {version = "0.15.1", path = "../nu-table"} ansi_term = "0.12.1" app_dirs = "1.2.1" diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index ab83a4a9d3..bd529a0a98 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -739,7 +739,7 @@ fn chomp_newline(s: &str) -> &str { } } -enum LineResult { +pub enum LineResult { Success(String), Error(String, ShellError), CtrlC, @@ -747,7 +747,7 @@ enum LineResult { } /// Process the line by parsing the text to turn it into commands, classify those commands so that we understand what is being called in the pipeline, and then run this pipeline -async fn process_line( +pub async fn process_line( readline: Result, ctx: &mut Context, redirect_stdin: bool, diff --git a/crates/nu-cli/src/commands/table.rs b/crates/nu-cli/src/commands/table.rs index ea8f3da027..9953dbda93 100644 --- a/crates/nu-cli/src/commands/table.rs +++ b/crates/nu-cli/src/commands/table.rs @@ -1,8 +1,9 @@ use crate::commands::WholeStreamCommand; -use crate::format::TableView; +use crate::data::value::{format_leaf, style_leaf}; use crate::prelude::*; use nu_errors::ShellError; use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_table::{draw_table, Alignment, StyledString, TextStyle, Theme}; use std::time::Instant; const STREAM_PAGE_SIZE: usize = 1000; @@ -38,12 +39,175 @@ impl WholeStreamCommand for Table { } } +fn str_to_color(s: String) -> Option { + match s.as_str() { + "g" | "green" => Some(ansi_term::Color::Green), + "r" | "red" => Some(ansi_term::Color::Red), + "u" | "blue" => Some(ansi_term::Color::Blue), + "b" | "black" => Some(ansi_term::Color::Black), + "y" | "yellow" => Some(ansi_term::Color::Yellow), + "p" | "purple" => Some(ansi_term::Color::Purple), + "c" | "cyan" => Some(ansi_term::Color::Cyan), + "w" | "white" => Some(ansi_term::Color::White), + _ => None, + } +} + +pub fn from_list(values: &[Value], starting_idx: usize) -> nu_table::Table { + let config = crate::data::config::config(Tag::unknown()); + + let header_style = if let Ok(config) = config { + let header_align = config.get("header_align").map_or(Alignment::Left, |a| { + a.as_string() + .map_or(Alignment::Center, |a| match a.to_lowercase().as_str() { + "center" | "c" => Alignment::Center, + "right" | "r" => Alignment::Right, + _ => Alignment::Center, + }) + }); + + let header_color = match config.get("header_color") { + Some(c) => match c.as_string() { + Ok(color) => str_to_color(color.to_lowercase()).unwrap_or(ansi_term::Color::Green), + _ => ansi_term::Color::Green, + }, + _ => ansi_term::Color::Green, + }; + + let header_bold = match config.get("header_bold") { + Some(b) => match b.as_bool() { + Ok(b) => b, + _ => true, + }, + _ => true, + }; + + TextStyle { + alignment: header_align, + color: Some(header_color), + is_bold: header_bold, + } + } else { + TextStyle::default_header() + }; + + let mut headers: Vec = nu_protocol::merge_descriptors(values) + .into_iter() + .map(|x| StyledString::new(x, header_style.clone())) + .collect(); + let entries = values_to_entries(values, &mut headers, starting_idx); + + nu_table::Table { + headers, + data: entries, + theme: Theme::compact(), + } +} + +fn are_table_indexes_disabled() -> bool { + let config = crate::data::config::config(Tag::unknown()); + match config { + Ok(config) => { + let disable_indexes = config.get("disable_table_indexes"); + disable_indexes.map_or(false, |x| x.as_bool().unwrap_or(false)) + } + _ => false, + } +} + +fn values_to_entries( + values: &[Value], + headers: &mut Vec, + starting_idx: usize, +) -> Vec> { + let disable_indexes = are_table_indexes_disabled(); + let mut entries = vec![]; + + if headers.is_empty() { + headers.push(StyledString::new("".to_string(), TextStyle::basic())); + } + + for (idx, value) in values.iter().enumerate() { + let mut row: Vec = headers + .iter() + .map(|d: &StyledString| { + if d.contents == "" { + match value { + Value { + value: UntaggedValue::Row(..), + .. + } => StyledString::new( + format_leaf(&UntaggedValue::nothing()).plain_string(100_000), + style_leaf(&UntaggedValue::nothing()), + ), + _ => StyledString::new( + format_leaf(value).plain_string(100_000), + style_leaf(value), + ), + } + } else { + match value { + Value { + value: UntaggedValue::Row(..), + .. + } => { + let data = value.get_data(&d.contents); + + StyledString::new( + format_leaf(data.borrow()).plain_string(100_000), + style_leaf(data.borrow()), + ) + } + _ => StyledString::new( + format_leaf(&UntaggedValue::nothing()).plain_string(100_000), + style_leaf(&UntaggedValue::nothing()), + ), + } + } + }) + .collect(); + + // Indices are green, bold, right-aligned: + if !disable_indexes { + row.insert( + 0, + StyledString::new( + (starting_idx + idx).to_string(), + TextStyle { + alignment: Alignment::Center, + color: Some(ansi_term::Color::Green), + is_bold: true, + }, + ), + ); + } + + entries.push(row); + } + + if !disable_indexes { + headers.insert( + 0, + StyledString::new( + "#".to_owned(), + TextStyle { + alignment: Alignment::Center, + color: Some(ansi_term::Color::Green), + is_bold: true, + }, + ), + ); + } + + entries +} + async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result { let registry = registry.clone(); let mut args = args.evaluate_once(®istry).await?; let mut finished = false; - let host = args.host.clone(); + // let host = args.host.clone(); let mut start_number = match args.get("start_number") { Some(Value { value: UntaggedValue::Primitive(Primitive::Int(i)), @@ -64,6 +228,8 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result = VecDeque::new(); @@ -113,12 +279,9 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result = new_input.into(); if !input.is_empty() { - let mut host = host.lock(); - let view = TableView::from_list(&input, start_number); + let t = from_list(&input, start_number); - if let Some(view) = view { - handle_unexpected(&mut *host, |host| crate::format::print_view(&view, host)); - } + draw_table(&t, termwidth); } start_number += input.len(); @@ -126,15 +289,3 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result>>, } diff --git a/crates/nu-cli/src/data/primitive.rs b/crates/nu-cli/src/data/primitive.rs index c352fa3e76..666bd95fd9 100644 --- a/crates/nu-cli/src/data/primitive.rs +++ b/crates/nu-cli/src/data/primitive.rs @@ -1,4 +1,5 @@ use nu_protocol::{hir::Number, Primitive}; +use nu_table::TextStyle; pub fn number(number: impl Into) -> Primitive { let number = number.into(); @@ -9,9 +10,9 @@ pub fn number(number: impl Into) -> Primitive { } } -pub fn style_primitive(primitive: &Primitive) -> &'static str { +pub fn style_primitive(primitive: &Primitive) -> TextStyle { match primitive { - Primitive::Int(_) | Primitive::Bytes(_) | Primitive::Decimal(_) => "r", - _ => "", + Primitive::Int(_) | Primitive::Bytes(_) | Primitive::Decimal(_) => TextStyle::basic_right(), + _ => TextStyle::basic(), } } diff --git a/crates/nu-cli/src/data/value.rs b/crates/nu-cli/src/data/value.rs index 78506c69ff..d4521bf92b 100644 --- a/crates/nu-cli/src/data/value.rs +++ b/crates/nu-cli/src/data/value.rs @@ -7,6 +7,7 @@ use nu_protocol::hir::Operator; use nu_protocol::ShellTypeName; use nu_protocol::{Primitive, Type, UntaggedValue}; use nu_source::{DebugDocBuilder, PrettyDebug, Tagged}; +use nu_table::TextStyle; pub fn date_from_str(s: Tagged<&str>) -> Result { let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| { @@ -160,10 +161,10 @@ pub fn format_leaf<'a>(value: impl Into<&'a UntaggedValue>) -> DebugDocBuilder { InlineShape::from_value(value.into()).format().pretty() } -pub fn style_leaf<'a>(value: impl Into<&'a UntaggedValue>) -> &'static str { +pub fn style_leaf<'a>(value: impl Into<&'a UntaggedValue>) -> TextStyle { match value.into() { UntaggedValue::Primitive(p) => style_primitive(p), - _ => "", + _ => TextStyle::basic(), } } diff --git a/crates/nu-cli/src/env/host.rs b/crates/nu-cli/src/env/host.rs index e6095f6ae7..6a6cce3829 100644 --- a/crates/nu-cli/src/env/host.rs +++ b/crates/nu-cli/src/env/host.rs @@ -1,7 +1,7 @@ use crate::prelude::*; #[cfg(test)] use indexmap::IndexMap; -use nu_errors::ShellError; +// use nu_errors::ShellError; use std::ffi::OsString; use std::fmt::Debug; @@ -200,13 +200,13 @@ impl Host for FakeHost { } } -pub(crate) fn handle_unexpected( - host: &mut dyn Host, - func: impl FnOnce(&mut dyn Host) -> Result, -) { - let result = func(host); +// pub(crate) fn handle_unexpected( +// host: &mut dyn Host, +// func: impl FnOnce(&mut dyn Host) -> Result, +// ) { +// let result = func(host); - if let Err(err) = result { - host.stderr(&format!("Something unexpected happened:\n{:?}", err)); - } -} +// if let Err(err) = result { +// host.stderr(&format!("Something unexpected happened:\n{:?}", err)); +// } +// } diff --git a/crates/nu-cli/src/format.rs b/crates/nu-cli/src/format.rs index 6d7710a146..a4e62f534c 100644 --- a/crates/nu-cli/src/format.rs +++ b/crates/nu-cli/src/format.rs @@ -1,14 +1,6 @@ -pub(crate) mod table; - use crate::prelude::*; use nu_errors::ShellError; -pub(crate) use table::TableView; - pub(crate) trait RenderView { fn render_view(&self, host: &mut dyn Host) -> Result<(), ShellError>; } - -pub(crate) fn print_view(view: &impl RenderView, host: &mut dyn Host) -> Result<(), ShellError> { - view.render_view(host) -} diff --git a/crates/nu-cli/src/format/table.rs b/crates/nu-cli/src/format/table.rs deleted file mode 100644 index 22b6b03739..0000000000 --- a/crates/nu-cli/src/format/table.rs +++ /dev/null @@ -1,453 +0,0 @@ -use crate::data::value::{format_leaf, style_leaf}; -use crate::format::RenderView; -use crate::prelude::*; -use derive_new::new; -use nu_errors::ShellError; -use nu_protocol::{UntaggedValue, Value}; -use textwrap::fill; - -use prettytable::format::{Alignment, FormatBuilder, LinePosition, LineSeparator}; -use prettytable::{color, Attr, Cell, Row, Table}; - -type Entries = Vec>; - -#[derive(Debug, new)] -pub struct TableView { - // List of header cell values: - headers: Vec, - - // List of rows of cells, each containing value and prettytable style-string: - entries: Entries, -} - -enum TableMode { - Light, - Normal, -} - -impl TableView { - pub fn from_list(values: &[Value], starting_idx: usize) -> Option { - if values.is_empty() { - return None; - } - - // Different platforms want different amounts of buffer, not sure why - let termwidth = std::cmp::max(textwrap::termwidth(), 20); - - let mut headers = nu_protocol::merge_descriptors(values); - let mut entries = values_to_entries(values, &mut headers, starting_idx); - let max_per_column = max_per_column(&headers, &entries, values.len()); - - maybe_truncate_columns(&mut headers, &mut entries, termwidth); - 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); - - // Wrap cells as needed - let table_view = wrap_cells( - headers, - entries, - max_per_column, - max_naive_column_width, - max_column_width, - ); - Some(table_view) - } -} - -fn are_table_indexes_disabled() -> bool { - let config = crate::data::config::config(Tag::unknown()); - match config { - Ok(config) => { - let disable_indexes = config.get("disable_table_indexes"); - disable_indexes.map_or(false, |x| x.as_bool().unwrap_or(false)) - } - _ => false, - } -} - -fn values_to_entries(values: &[Value], headers: &mut Vec, starting_idx: usize) -> Entries { - let disable_indexes = are_table_indexes_disabled(); - let mut entries = vec![]; - - if headers.is_empty() { - headers.push("".to_string()); - } - - for (idx, value) in values.iter().enumerate() { - let mut row: Vec<(String, &'static str)> = headers - .iter() - .map(|d: &String| { - if d == "" { - match value { - Value { - value: UntaggedValue::Row(..), - .. - } => ( - format_leaf(&UntaggedValue::nothing()).plain_string(100_000), - style_leaf(&UntaggedValue::nothing()), - ), - _ => (format_leaf(value).plain_string(100_000), style_leaf(value)), - } - } else { - match value { - Value { - value: UntaggedValue::Row(..), - .. - } => { - let data = value.get_data(d); - ( - format_leaf(data.borrow()).plain_string(100_000), - style_leaf(data.borrow()), - ) - } - _ => ( - format_leaf(&UntaggedValue::nothing()).plain_string(100_000), - style_leaf(&UntaggedValue::nothing()), - ), - } - } - }) - .collect(); - - // Indices are green, bold, right-aligned: - if !disable_indexes { - row.insert(0, ((starting_idx + idx).to_string(), "Fgbr")); - } - - entries.push(row); - } - - if !disable_indexes { - headers.insert(0, "#".to_owned()); - } - - entries -} - -#[allow(clippy::ptr_arg)] -fn max_per_column(headers: &[String], entries: &Entries, values_len: usize) -> Vec { - let mut max_per_column = vec![]; - - for i in 0..headers.len() { - let mut current_col_max = 0; - let iter = entries.iter().take(values_len); - - for entry in iter { - let value_length = entry[i].0.chars().count(); - if value_length > current_col_max { - current_col_max = value_length; - } - } - - max_per_column.push(std::cmp::max(current_col_max, headers[i].chars().count())); - } - - max_per_column -} - -fn maybe_truncate_columns(headers: &mut Vec, entries: &mut Entries, 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); - - for entry in entries.iter_mut() { - entry.truncate(max_num_of_columns); - } - - headers.push("...".to_owned()); - - for entry in entries.iter_mut() { - entry.push(("...".to_owned(), "c")); // ellipsis is centred - } - } -} - -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) -> usize { - let ColumnSpace { - num_overages, - underage_sum, - overage_separator_sum, - } = self; - - if *num_overages > 0 { - (termwidth - 1 - *underage_sum - *overage_separator_sum) / *num_overages - } else { - 99999 - } - } -} - -fn wrap_cells( - mut headers: Vec, - mut entries: Entries, - max_per_column: Vec, - max_naive_column_width: usize, - max_column_width: usize, -) -> TableView { - for head in 0..headers.len() { - if max_per_column[head] > max_naive_column_width { - headers[head] = fill(&headers[head], max_column_width); - - for entry in entries.iter_mut() { - entry[head].0 = fill(&entry[head].0, max_column_width); - } - } - } - - TableView { headers, entries } -} - -impl RenderView for TableView { - fn render_view(&self, host: &mut dyn Host) -> Result<(), ShellError> { - if self.entries.is_empty() { - return Ok(()); - } - - let mut table = Table::new(); - - let mut config = crate::data::config::config(Tag::unknown())?; - let header_align = config.get("header_align").map_or(Alignment::LEFT, |a| { - a.as_string() - .map_or(Alignment::LEFT, |a| match a.to_lowercase().as_str() { - "center" | "c" => Alignment::CENTER, - "right" | "r" => Alignment::RIGHT, - _ => Alignment::LEFT, - }) - }); - - let header_color = config.get("header_color").map_or(color::GREEN, |c| { - c.as_string().map_or(color::GREEN, |c| { - str_to_color(c.to_lowercase()).unwrap_or(color::GREEN) - }) - }); - - let header_style = - config - .remove("header_style") - .map_or(vec![Attr::Bold], |y| match y.value { - UntaggedValue::Table(t) => to_style_vec(t), - UntaggedValue::Primitive(p) => vec![p - .into_string(Span::unknown()) - .map_or(Attr::Bold, |s| str_to_style(s).unwrap_or(Attr::Bold))], - _ => vec![Attr::Bold], - }); - - let table_mode = if let Some(s) = config.get("table_mode") { - match s.as_string() { - Ok(typ) if typ == "light" => TableMode::Light, - _ => TableMode::Normal, - } - } else { - TableMode::Normal - }; - - match table_mode { - TableMode::Light => { - table.set_format( - FormatBuilder::new() - .separator(LinePosition::Title, LineSeparator::new('─', '─', ' ', ' ')) - .separator(LinePosition::Bottom, LineSeparator::new(' ', ' ', ' ', ' ')) - .padding(1, 1) - .build(), - ); - } - _ => { - table.set_format( - FormatBuilder::new() - .column_separator('│') - .separator(LinePosition::Top, LineSeparator::new('─', '┬', ' ', ' ')) - .separator(LinePosition::Title, LineSeparator::new('─', '┼', ' ', ' ')) - .separator(LinePosition::Bottom, LineSeparator::new('─', '┴', ' ', ' ')) - .padding(1, 1) - .build(), - ); - } - } - - let skip_headers = (self.headers.len() == 2 && self.headers[1] == "") - || (self.headers.len() == 1 && self.headers[0] == ""); - - let header: Vec = self - .headers - .iter() - .map(|h| { - let mut c = Cell::new_align(h, header_align) - .with_style(Attr::ForegroundColor(header_color)); - for &s in &header_style { - c.style(s); - } - c - }) - .collect(); - - if !skip_headers { - table.set_titles(Row::new(header)); - } - - for row in &self.entries { - table.add_row(Row::new( - row.iter() - .map(|(v, s)| Cell::new(v).style_spec(s)) - .collect(), - )); - } - - table.print_term(&mut *host.out_terminal().ok_or_else(|| ShellError::untagged_runtime_error("Could not open terminal for output"))?) - .map_err(|_| ShellError::untagged_runtime_error("Internal error: could not print to terminal (for unix systems check to make sure TERM is set)"))?; - - Ok(()) - } -} - -fn str_to_color(s: String) -> Option { - match s.as_str() { - "g" | "green" => Some(color::GREEN), - "r" | "red" => Some(color::RED), - "u" | "blue" => Some(color::BLUE), - "b" | "black" => Some(color::BLACK), - "y" | "yellow" => Some(color::YELLOW), - "m" | "magenta" => Some(color::MAGENTA), - "c" | "cyan" => Some(color::CYAN), - "w" | "white" => Some(color::WHITE), - "bg" | "bright green" => Some(color::BRIGHT_GREEN), - "br" | "bright red" => Some(color::BRIGHT_RED), - "bu" | "bright blue" => Some(color::BRIGHT_BLUE), - "by" | "bright yellow" => Some(color::BRIGHT_YELLOW), - "bm" | "bright magenta" => Some(color::BRIGHT_MAGENTA), - "bc" | "bright cyan" => Some(color::BRIGHT_CYAN), - "bw" | "bright white" => Some(color::BRIGHT_WHITE), - _ => None, - } -} - -fn to_style_vec(a: Vec) -> Vec { - let mut v: Vec = Vec::new(); - for t in a { - if let Ok(s) = t.as_string() { - if let Some(r) = str_to_style(s) { - v.push(r); - } - } - } - v -} - -fn str_to_style(s: String) -> Option { - match s.as_str() { - "b" | "bold" => Some(Attr::Bold), - "i" | "italic" | "italics" => Some(Attr::Italic(true)), - "u" | "underline" | "underlined" => Some(Attr::Underline(true)), - _ => None, - } -} diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index 0cb86aa508..a7bb6c7027 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -32,7 +32,8 @@ pub mod utils; mod examples; pub use crate::cli::{ - cli, create_default_context, load_plugins, run_pipeline_standalone, run_vec_of_pipelines, + cli, create_default_context, load_plugins, process_line, run_pipeline_standalone, + run_vec_of_pipelines, LineResult, }; pub use crate::commands::command::{ whole_stream_command, CommandArgs, EvaluatedWholeStreamCommandArgs, WholeStreamCommand, diff --git a/crates/nu-cli/src/prelude.rs b/crates/nu-cli/src/prelude.rs index e8df6041a1..ce58148b6e 100644 --- a/crates/nu-cli/src/prelude.rs +++ b/crates/nu-cli/src/prelude.rs @@ -77,7 +77,7 @@ pub(crate) use crate::context::CommandRegistry; pub(crate) use crate::context::Context; pub(crate) use crate::data::config; pub(crate) use crate::data::value; -pub(crate) use crate::env::host::handle_unexpected; +// pub(crate) use crate::env::host::handle_unexpected; pub(crate) use crate::env::Host; pub(crate) use crate::shell::filesystem_shell::FilesystemShell; pub(crate) use crate::shell::help_shell::HelpShell; diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml new file mode 100644 index 0000000000..f3e25392bd --- /dev/null +++ b/crates/nu-table/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nu-table" +version = "0.15.1" +authors = ["The Nu Project Contributors"] +edition = "2018" +description = "Nushell table printing" +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "table" +path = "src/main.rs" + +[dependencies] +unicode-width = "0.1.7" +ansi_term = "0.12.1" \ No newline at end of file diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs new file mode 100644 index 0000000000..661d7ddde7 --- /dev/null +++ b/crates/nu-table/src/lib.rs @@ -0,0 +1,5 @@ +mod table; +mod wrap; + +pub use table::{draw_table, StyledString, Table, TextStyle, Theme}; +pub use wrap::Alignment; diff --git a/crates/nu-table/src/main.rs b/crates/nu-table/src/main.rs new file mode 100644 index 0000000000..f426a9d02d --- /dev/null +++ b/crates/nu-table/src/main.rs @@ -0,0 +1,23 @@ +use nu_table::{draw_table, StyledString, Table, TextStyle, Theme}; + +fn main() { + let args: Vec<_> = std::env::args().collect(); + + let width = args[1].parse::().expect("Need a width in columns"); + let msg = args[2..] + .iter() + .map(|x| StyledString::new(x.to_owned(), TextStyle::basic())) + .collect(); + + let t = Table::new( + vec![ + StyledString::new("Test me".to_owned(), TextStyle::default_header()), + StyledString::new("Long column name".to_owned(), TextStyle::default_header()), + StyledString::new("Another".to_owned(), TextStyle::default_header()), + ], + vec![msg; 2], + Theme::compact(), + ); + + draw_table(&t, width); +} diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs new file mode 100644 index 0000000000..7ad91e9085 --- /dev/null +++ b/crates/nu-table/src/table.rs @@ -0,0 +1,673 @@ +use crate::wrap::{column_width, split_sublines, wrap, Alignment, Subline, WrappedCell}; + +enum SeparatorPosition { + Top, + Middle, + Bottom, +} + +#[derive(Debug)] +pub struct Table { + pub headers: Vec, + pub data: Vec>, + pub theme: Theme, +} + +#[derive(Debug, Clone)] +pub struct StyledString { + pub contents: String, + pub style: TextStyle, +} + +impl StyledString { + pub fn new(contents: String, style: TextStyle) -> StyledString { + StyledString { contents, style } + } +} + +#[derive(Debug, Clone)] +pub struct TextStyle { + pub is_bold: bool, + pub alignment: Alignment, + pub color: Option, +} + +impl TextStyle { + pub fn basic() -> TextStyle { + TextStyle { + is_bold: false, + alignment: Alignment::Left, + color: None, + } + } + + pub fn basic_right() -> TextStyle { + TextStyle { + is_bold: false, + alignment: Alignment::Right, + color: None, + } + } + + pub fn default_header() -> TextStyle { + TextStyle { + is_bold: true, + alignment: Alignment::Center, + color: Some(ansi_term::Colour::Green), + } + } +} + +#[derive(Debug, Clone)] +pub struct Theme { + 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, +} + +impl Theme { + #[allow(unused)] + pub fn basic() -> Theme { + Theme { + 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, + } + } + #[allow(unused)] + pub fn thin() -> Theme { + Theme { + 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, + } + } + #[allow(unused)] + pub fn light() -> Theme { + Theme { + 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, + } + } + #[allow(unused)] + pub fn compact() -> Theme { + Theme { + 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, + } + } +} + +impl Table { + pub fn new(headers: Vec, data: Vec>, theme: Theme) -> Table { + Table { + headers, + data, + theme, + } + } +} + +#[derive(Debug)] +pub struct ProcessedTable<'a> { + pub headers: Vec>, + pub data: Vec>>, + pub theme: Theme, +} + +#[derive(Debug)] +pub struct ProcessedCell<'a> { + pub contents: Vec>, + pub style: TextStyle, +} + +#[derive(Debug)] +pub struct WrappedTable { + pub column_widths: Vec, + pub headers: Vec, + pub data: Vec>, + pub theme: Theme, +} + +impl WrappedTable { + //TODO: optimize this + fn print_separator(&self, separator_position: SeparatorPosition) { + let column_count = self.column_widths.len(); + + match separator_position { + SeparatorPosition::Top => { + for column in self.column_widths.iter().enumerate() { + if column.0 == 0 && self.theme.print_left_border { + print!("{}", self.theme.top_left); + } + print!( + "{}", + std::iter::repeat(self.theme.top_horizontal) + .take(*column.1) + .collect::() + ); + + print!("{}{}", self.theme.top_horizontal, self.theme.top_horizontal); + if column.0 == column_count - 1 { + if self.theme.print_right_border { + print!("{}", self.theme.top_right); + } + } else { + print!("{}", self.theme.top_center); + } + } + } + SeparatorPosition::Middle => { + for column in self.column_widths.iter().enumerate() { + if column.0 == 0 && self.theme.print_left_border { + print!("{}", self.theme.middle_left); + } + print!( + "{}", + std::iter::repeat(self.theme.middle_horizontal) + .take(*column.1) + .collect::() + ); + + print!( + "{}{}", + self.theme.middle_horizontal, self.theme.middle_horizontal + ); + if column.0 == column_count - 1 { + if self.theme.print_right_border { + print!("{}", self.theme.middle_right); + } + } else { + print!("{}", self.theme.center); + } + } + } + SeparatorPosition::Bottom => { + for column in self.column_widths.iter().enumerate() { + if column.0 == 0 && self.theme.print_left_border { + print!("{}", self.theme.bottom_left); + } + print!( + "{}", + std::iter::repeat(self.theme.bottom_horizontal) + .take(*column.1) + .collect::() + ); + + print!( + "{}{}", + self.theme.bottom_horizontal, self.theme.bottom_horizontal + ); + if column.0 == column_count - 1 { + if self.theme.print_right_border { + print!("{}", self.theme.bottom_right); + } + } else { + print!("{}", self.theme.bottom_center); + } + } + } + } + } + + fn print_cell_contents(&self, cells: &[WrappedCell]) { + for current_line in 0.. { + let mut lines_printed = 0; + + let mut output = if self.theme.print_left_border { + self.theme.left_vertical.to_string() + } else { + String::new() + }; + 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 { + let color = if column.1.style.is_bold { + color.bold() + } else { + color.normal() + }; + + 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 { + let color = if column.1.style.is_bold { + color.bold() + } else { + color.normal() + }; + + 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 { + let color = if column.1.style.is_bold { + color.bold() + } else { + color.normal() + }; + + 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(self.theme.center_vertical); + } else if self.theme.print_right_border { + output.push(self.theme.right_vertical); + } + } + if lines_printed == 0 { + break; + } else { + println!("{}", output); + } + } + } + fn new_print_table(&self) { + if self.data.is_empty() { + return; + } + + self.print_separator(SeparatorPosition::Top); + println!(); + + self.print_cell_contents(&self.headers); + + let mut first_row = true; + + for row in &self.data { + if !first_row { + if self.theme.separate_rows { + self.print_separator(SeparatorPosition::Middle); + println!(); + } + } else { + first_row = false; + + if self.theme.separate_header { + self.print_separator(SeparatorPosition::Middle); + println!(); + } + } + + self.print_cell_contents(row); + } + self.print_separator(SeparatorPosition::Bottom); + println!(); + } +} + +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 { + out_row.push(ProcessedCell { + contents: split_sublines(&column.contents).collect::>(), + style: column.style.clone(), + }); + } + processed_data.push(out_row); + } + + let mut processed_headers = vec![]; + for header in &table.headers { + processed_headers.push(ProcessedCell { + contents: split_sublines(&header.contents).collect::>(), + style: header.style.clone(), + }); + } + + ProcessedTable { + headers: processed_headers, + data: processed_data, + theme: table.theme.clone(), + } +} + +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.iter())); + } + + for row in &processed_table.data { + for column in row.iter().enumerate() { + output[column.0] = max(output[column.0], column_width(column.1.contents.iter())); + } + } + + output +} + +pub fn draw_table(table: &Table, termwidth: usize) { + // Remove the edges, if used + let termwidth = if table.theme.print_left_border && table.theme.print_right_border { + termwidth - 2 + } else if table.theme.print_left_border || table.theme.print_right_border { + termwidth - 1 + } else { + termwidth + }; + + let processed_table = process_table(table); + + let max_per_column = get_max_column_widths(&processed_table); + + // maybe_truncate_columns(&mut headers, &mut entries, termwidth); + let headers_len = table.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); + + let wrapped_table = wrap_cells( + processed_table, + // max_per_column, + // max_naive_column_width, + max_column_width, + ); + + wrapped_table.new_print_table(); +} + +fn wrap_cells(processed_table: ProcessedTable, max_column_width: usize) -> WrappedTable { + let mut column_widths = vec![0; processed_table.headers.len()]; + let mut output_headers = vec![]; + for header in processed_table.headers.into_iter().enumerate() { + let wrapped = wrap( + max_column_width, + header.1.contents.into_iter(), + header.1.style, + ); + 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 wrapped = wrap( + max_column_width, + column.1.contents.into_iter(), + column.1.style, + ); + if column_widths[column.0] < wrapped.max_width { + column_widths[column.0] = wrapped.max_width; + } + output_row.push(wrapped); + } + output_data.push(output_row); + } + + WrappedTable { + column_widths, + headers: output_headers, + data: output_data, + theme: processed_table.theme, + } +} + +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) -> usize { + let ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } = self; + + if *num_overages > 0 { + (termwidth - 1 - *underage_sum - *overage_separator_sum) / *num_overages + } else { + 99999 + } + } +} diff --git a/crates/nu-table/src/wrap.rs b/crates/nu-table/src/wrap.rs new file mode 100644 index 0000000000..bc86eb9f3c --- /dev/null +++ b/crates/nu-table/src/wrap.rs @@ -0,0 +1,226 @@ +use crate::table::TextStyle; +use std::{fmt::Display, iter::Iterator}; +use unicode_width::UnicodeWidthStr; + +#[derive(Debug, Clone)] +pub enum Alignment { + Left, + Center, + Right, +} + +#[derive(Debug)] +pub struct Subline<'a> { + pub subline: &'a str, + pub width: usize, +} + +#[derive(Debug)] +pub struct Line<'a> { + pub sublines: Vec>, + pub width: usize, +} + +#[derive(Debug)] +pub struct WrappedLine { + pub line: String, + pub width: usize, +} + +#[derive(Debug)] +pub struct WrappedCell { + pub lines: Vec, + pub max_width: usize, + + pub style: TextStyle, +} + +impl<'a> Display for Line<'a> { + 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(()) + } +} + +pub fn split_sublines(input: &str) -> impl Iterator { + input.split_terminator(' ').map(|x| Subline { + subline: x, + width: UnicodeWidthStr::width(x), + }) +} + +pub fn column_width<'a>(input: impl Iterator>) -> usize { + let mut total = 0; + + let mut first = true; + for inp in input { + if !first { + // Account for the space + total += 1; + } else { + first = false; + } + + total += inp.width; + } + + total +} + +fn split_word<'a>(cell_width: usize, word: &'a str) -> Vec> { + use unicode_width::UnicodeWidthChar; + + let mut output = vec![]; + let mut current_width = 0; + let mut start_index = 0; + let mut end_index; + + for c in word.char_indices() { + if let Some(width) = c.1.width() { + end_index = c.0; + if current_width + width > cell_width { + output.push(Subline { + subline: &word[start_index..end_index], + width: current_width, + }); + + start_index = c.0; + current_width = width; + } else { + current_width += width; + } + } + } + + if start_index != word.len() { + output.push(Subline { + subline: &word[start_index..], + width: current_width, + }); + } + + output +} + +pub fn wrap<'a>( + cell_width: usize, + mut input: impl Iterator>, + style: TextStyle, +) -> WrappedCell { + let mut lines = vec![]; + let mut current_line: Vec = vec![]; + let mut current_width = 0; + let mut first = true; + let mut max_width = 0; + loop { + // println!("{:?}", current_line); + match input.next() { + Some(item) => { + if !first { + current_width += 1; + } else { + first = false; + } + + if item.width + current_width > cell_width { + // If this is a really long single word, we need to split the word + if current_line.len() == 1 && current_width > cell_width { + max_width = cell_width; + let sublines = split_word(cell_width, ¤t_line[0].subline); + for subline in sublines { + let width = subline.width; + lines.push(Line { + sublines: vec![subline], + width, + }); + } + + first = true; + + current_width = item.width; + current_line = vec![item]; + } else { + if !current_line.is_empty() { + lines.push(Line { + sublines: current_line, + width: current_width, + }); + } + + first = true; + + current_width = item.width; + current_line = vec![item]; + max_width = std::cmp::max(max_width, current_width); + } + } else { + current_width += item.width; + current_line.push(item); + } + } + None => { + if current_width > cell_width { + // We need to break up the last word + let sublines = split_word(cell_width, ¤t_line[0].subline); + for subline in sublines { + let width = subline.width; + lines.push(Line { + sublines: vec![subline], + width, + }); + } + } else if current_width > 0 { + lines.push(Line { + sublines: current_line, + width: current_width, + }); + } + break; + } + } + } + + let mut current_max = 0; + let mut output = vec![]; + + for line in lines { + let mut current_line_width = 0; + let mut first = true; + let mut current_line = String::new(); + + for subline in line.sublines { + if !first { + current_line_width += 1 + subline.width; + current_line.push(' '); + current_line.push_str(subline.subline); + } 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; + } + + output.push(WrappedLine { + line: current_line, + width: current_line_width, + }); + } + + WrappedCell { + lines: output, + max_width: current_max, + style, + } +}