* WIP

* Get ready to land nu-table

* Remove unwrap
This commit is contained in:
Jonathan Turner 2020-06-19 20:41:53 -07:00 committed by GitHub
parent 77e02ac1c1
commit fcbaefed52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1146 additions and 500 deletions

9
Cargo.lock generated
View file

@ -2472,6 +2472,7 @@ dependencies = [
"nu-plugin", "nu-plugin",
"nu-protocol", "nu-protocol",
"nu-source", "nu-source",
"nu-table",
"nu-test-support", "nu-test-support",
"nu-value-ext", "nu-value-ext",
"num-bigint", "num-bigint",
@ -2611,6 +2612,14 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "nu-table"
version = "0.15.1"
dependencies = [
"ansi_term 0.12.1",
"unicode-width",
]
[[package]] [[package]]
name = "nu-test-support" name = "nu-test-support"
version = "0.15.1" version = "0.15.1"

View file

@ -17,6 +17,7 @@ nu-errors = { version = "0.15.1", path = "../nu-errors" }
nu-parser = { version = "0.15.1", path = "../nu-parser" } nu-parser = { version = "0.15.1", path = "../nu-parser" }
nu-value-ext = { version = "0.15.1", path = "../nu-value-ext" } nu-value-ext = { version = "0.15.1", path = "../nu-value-ext" }
nu-test-support = { version = "0.15.1", path = "../nu-test-support" } 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" ansi_term = "0.12.1"
app_dirs = "1.2.1" app_dirs = "1.2.1"

View file

@ -739,7 +739,7 @@ fn chomp_newline(s: &str) -> &str {
} }
} }
enum LineResult { pub enum LineResult {
Success(String), Success(String),
Error(String, ShellError), Error(String, ShellError),
CtrlC, 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 /// 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<String, ReadlineError>, readline: Result<String, ReadlineError>,
ctx: &mut Context, ctx: &mut Context,
redirect_stdin: bool, redirect_stdin: bool,

View file

@ -1,8 +1,9 @@
use crate::commands::WholeStreamCommand; use crate::commands::WholeStreamCommand;
use crate::format::TableView; use crate::data::value::{format_leaf, style_leaf};
use crate::prelude::*; use crate::prelude::*;
use nu_errors::ShellError; use nu_errors::ShellError;
use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value};
use nu_table::{draw_table, Alignment, StyledString, TextStyle, Theme};
use std::time::Instant; use std::time::Instant;
const STREAM_PAGE_SIZE: usize = 1000; const STREAM_PAGE_SIZE: usize = 1000;
@ -38,12 +39,175 @@ impl WholeStreamCommand for Table {
} }
} }
fn str_to_color(s: String) -> Option<ansi_term::Color> {
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<StyledString> = 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<StyledString>,
starting_idx: usize,
) -> Vec<Vec<StyledString>> {
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<StyledString> = 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<OutputStream, ShellError> { async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputStream, ShellError> {
let registry = registry.clone(); let registry = registry.clone();
let mut args = args.evaluate_once(&registry).await?; let mut args = args.evaluate_once(&registry).await?;
let mut finished = false; let mut finished = false;
let host = args.host.clone(); // let host = args.host.clone();
let mut start_number = match args.get("start_number") { let mut start_number = match args.get("start_number") {
Some(Value { Some(Value {
value: UntaggedValue::Primitive(Primitive::Int(i)), value: UntaggedValue::Primitive(Primitive::Int(i)),
@ -64,6 +228,8 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputSt
let mut delay_slot = None; let mut delay_slot = None;
let termwidth = std::cmp::max(textwrap::termwidth(), 20);
while !finished { while !finished {
let mut new_input: VecDeque<Value> = VecDeque::new(); let mut new_input: VecDeque<Value> = VecDeque::new();
@ -113,12 +279,9 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputSt
let input: Vec<Value> = new_input.into(); let input: Vec<Value> = new_input.into();
if !input.is_empty() { if !input.is_empty() {
let mut host = host.lock(); let t = from_list(&input, start_number);
let view = TableView::from_list(&input, start_number);
if let Some(view) = view { draw_table(&t, termwidth);
handle_unexpected(&mut *host, |host| crate::format::print_view(&view, host));
}
} }
start_number += input.len(); start_number += input.len();
@ -126,15 +289,3 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result<OutputSt
Ok(OutputStream::empty()) Ok(OutputStream::empty())
} }
#[cfg(test)]
mod tests {
use super::Table;
#[test]
fn examples_work_as_expected() {
use crate::examples::test as test_examples;
test_examples(Table {})
}
}

View file

@ -6,7 +6,7 @@ use parking_lot::Mutex;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct NuConfig { pub struct NuConfig {
pub vars: Arc<Mutex<IndexMap<String, Value>>>, pub vars: Arc<Mutex<IndexMap<String, Value>>>,
} }

View file

@ -1,4 +1,5 @@
use nu_protocol::{hir::Number, Primitive}; use nu_protocol::{hir::Number, Primitive};
use nu_table::TextStyle;
pub fn number(number: impl Into<Number>) -> Primitive { pub fn number(number: impl Into<Number>) -> Primitive {
let number = number.into(); let number = number.into();
@ -9,9 +10,9 @@ pub fn number(number: impl Into<Number>) -> Primitive {
} }
} }
pub fn style_primitive(primitive: &Primitive) -> &'static str { pub fn style_primitive(primitive: &Primitive) -> TextStyle {
match primitive { match primitive {
Primitive::Int(_) | Primitive::Bytes(_) | Primitive::Decimal(_) => "r", Primitive::Int(_) | Primitive::Bytes(_) | Primitive::Decimal(_) => TextStyle::basic_right(),
_ => "", _ => TextStyle::basic(),
} }
} }

View file

@ -7,6 +7,7 @@ use nu_protocol::hir::Operator;
use nu_protocol::ShellTypeName; use nu_protocol::ShellTypeName;
use nu_protocol::{Primitive, Type, UntaggedValue}; use nu_protocol::{Primitive, Type, UntaggedValue};
use nu_source::{DebugDocBuilder, PrettyDebug, Tagged}; use nu_source::{DebugDocBuilder, PrettyDebug, Tagged};
use nu_table::TextStyle;
pub fn date_from_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> { pub fn date_from_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> {
let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| { 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() 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() { match value.into() {
UntaggedValue::Primitive(p) => style_primitive(p), UntaggedValue::Primitive(p) => style_primitive(p),
_ => "", _ => TextStyle::basic(),
} }
} }

View file

@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
#[cfg(test)] #[cfg(test)]
use indexmap::IndexMap; use indexmap::IndexMap;
use nu_errors::ShellError; // use nu_errors::ShellError;
use std::ffi::OsString; use std::ffi::OsString;
use std::fmt::Debug; use std::fmt::Debug;
@ -200,13 +200,13 @@ impl Host for FakeHost {
} }
} }
pub(crate) fn handle_unexpected<T>( // pub(crate) fn handle_unexpected<T>(
host: &mut dyn Host, // host: &mut dyn Host,
func: impl FnOnce(&mut dyn Host) -> Result<T, ShellError>, // func: impl FnOnce(&mut dyn Host) -> Result<T, ShellError>,
) { // ) {
let result = func(host); // let result = func(host);
if let Err(err) = result { // if let Err(err) = result {
host.stderr(&format!("Something unexpected happened:\n{:?}", err)); // host.stderr(&format!("Something unexpected happened:\n{:?}", err));
} // }
} // }

View file

@ -1,14 +1,6 @@
pub(crate) mod table;
use crate::prelude::*; use crate::prelude::*;
use nu_errors::ShellError; use nu_errors::ShellError;
pub(crate) use table::TableView;
pub(crate) trait RenderView { pub(crate) trait RenderView {
fn render_view(&self, host: &mut dyn Host) -> Result<(), ShellError>; 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)
}

View file

@ -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<Vec<(String, &'static str)>>;
#[derive(Debug, new)]
pub struct TableView {
// List of header cell values:
headers: Vec<String>,
// 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<TableView> {
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<String>, 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<usize> {
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<String>, 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<String>,
mut entries: Entries,
max_per_column: Vec<usize>,
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<Cell> = 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<color::Color> {
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<Value>) -> Vec<Attr> {
let mut v: Vec<Attr> = 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<Attr> {
match s.as_str() {
"b" | "bold" => Some(Attr::Bold),
"i" | "italic" | "italics" => Some(Attr::Italic(true)),
"u" | "underline" | "underlined" => Some(Attr::Underline(true)),
_ => None,
}
}

View file

@ -32,7 +32,8 @@ pub mod utils;
mod examples; mod examples;
pub use crate::cli::{ 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::{ pub use crate::commands::command::{
whole_stream_command, CommandArgs, EvaluatedWholeStreamCommandArgs, WholeStreamCommand, whole_stream_command, CommandArgs, EvaluatedWholeStreamCommandArgs, WholeStreamCommand,

View file

@ -77,7 +77,7 @@ pub(crate) use crate::context::CommandRegistry;
pub(crate) use crate::context::Context; pub(crate) use crate::context::Context;
pub(crate) use crate::data::config; pub(crate) use crate::data::config;
pub(crate) use crate::data::value; 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::env::Host;
pub(crate) use crate::shell::filesystem_shell::FilesystemShell; pub(crate) use crate::shell::filesystem_shell::FilesystemShell;
pub(crate) use crate::shell::help_shell::HelpShell; pub(crate) use crate::shell::help_shell::HelpShell;

View file

@ -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"

View file

@ -0,0 +1,5 @@
mod table;
mod wrap;
pub use table::{draw_table, StyledString, Table, TextStyle, Theme};
pub use wrap::Alignment;

View file

@ -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::<usize>().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);
}

View file

@ -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<StyledString>,
pub data: Vec<Vec<StyledString>>,
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<ansi_term::Colour>,
}
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<StyledString>, data: Vec<Vec<StyledString>>, theme: Theme) -> Table {
Table {
headers,
data,
theme,
}
}
}
#[derive(Debug)]
pub struct ProcessedTable<'a> {
pub headers: Vec<ProcessedCell<'a>>,
pub data: Vec<Vec<ProcessedCell<'a>>>,
pub theme: Theme,
}
#[derive(Debug)]
pub struct ProcessedCell<'a> {
pub contents: Vec<Subline<'a>>,
pub style: TextStyle,
}
#[derive(Debug)]
pub struct WrappedTable {
pub column_widths: Vec<usize>,
pub headers: Vec<WrappedCell>,
pub data: Vec<Vec<WrappedCell>>,
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::<String>()
);
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::<String>()
);
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::<String>()
);
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::<Vec<_>>(),
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::<Vec<_>>(),
style: header.style.clone(),
});
}
ProcessedTable {
headers: processed_headers,
data: processed_data,
theme: table.theme.clone(),
}
}
fn get_max_column_widths(processed_table: &ProcessedTable) -> Vec<usize> {
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
}
}
}

226
crates/nu-table/src/wrap.rs Normal file
View file

@ -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<Subline<'a>>,
pub width: usize,
}
#[derive(Debug)]
pub struct WrappedLine {
pub line: String,
pub width: usize,
}
#[derive(Debug)]
pub struct WrappedCell {
pub lines: Vec<WrappedLine>,
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<Item = Subline> {
input.split_terminator(' ').map(|x| Subline {
subline: x,
width: UnicodeWidthStr::width(x),
})
}
pub fn column_width<'a>(input: impl Iterator<Item = &'a Subline<'a>>) -> 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<Subline<'a>> {
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<Item = Subline<'a>>,
style: TextStyle,
) -> WrappedCell {
let mut lines = vec![];
let mut current_line: Vec<Subline> = vec![];
let mut current_width = 0;
let mut first = true;
let mut max_width = 0;
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, &current_line[0].subline);
for subline in sublines {
let width = subline.width;
lines.push(Line {
sublines: vec![subline],
width,
});
}
first = true;
current_width = item.width;
current_line = vec![item];
} else {
if !current_line.is_empty() {
lines.push(Line {
sublines: current_line,
width: current_width,
});
}
first = true;
current_width = item.width;
current_line = vec![item];
max_width = std::cmp::max(max_width, current_width);
}
} else {
current_width += item.width;
current_line.push(item);
}
}
None => {
if current_width > cell_width {
// We need to break up the last word
let sublines = split_word(cell_width, &current_line[0].subline);
for subline in sublines {
let width = subline.width;
lines.push(Line {
sublines: vec![subline],
width,
});
}
} else if current_width > 0 {
lines.push(Line {
sublines: current_line,
width: current_width,
});
}
break;
}
}
}
let mut current_max = 0;
let mut output = vec![];
for line in lines {
let mut current_line_width = 0;
let mut first = true;
let mut current_line = String::new();
for subline in line.sublines {
if !first {
current_line_width += 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,
}
}