* 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
17 changed files with 1146 additions and 500 deletions

View File

@ -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<String, ReadlineError>,
ctx: &mut Context,
redirect_stdin: bool,

View File

@ -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<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> {
let registry = registry.clone();
let mut args = args.evaluate_once(&registry).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<OutputSt
let mut delay_slot = None;
let termwidth = std::cmp::max(textwrap::termwidth(), 20);
while !finished {
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();
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<OutputSt
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::sync::Arc;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct NuConfig {
pub vars: Arc<Mutex<IndexMap<String, Value>>>,
}

View File

@ -1,4 +1,5 @@
use nu_protocol::{hir::Number, Primitive};
use nu_table::TextStyle;
pub fn number(number: impl Into<Number>) -> Primitive {
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 {
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::{Primitive, Type, UntaggedValue};
use nu_source::{DebugDocBuilder, PrettyDebug, Tagged};
use nu_table::TextStyle;
pub fn date_from_str(s: Tagged<&str>) -> Result<UntaggedValue, ShellError> {
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(),
}
}

View File

@ -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<T>(
host: &mut dyn Host,
func: impl FnOnce(&mut dyn Host) -> Result<T, ShellError>,
) {
let result = func(host);
// pub(crate) fn handle_unexpected<T>(
// host: &mut dyn Host,
// func: impl FnOnce(&mut dyn Host) -> Result<T, ShellError>,
// ) {
// 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));
// }
// }

View File

@ -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)
}

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;
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,

View File

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