Add a very silly table

This commit is contained in:
JT 2021-09-10 14:27:12 +12:00
parent 0694245ccd
commit 26d50ebcd5
13 changed files with 1840 additions and 0 deletions

37
Cargo.lock generated
View File

@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
@ -148,6 +157,7 @@ dependencies = [
"nu-engine",
"nu-parser",
"nu-protocol",
"nu-table",
"pretty_assertions",
"reedline",
"tempfile",
@ -285,6 +295,7 @@ dependencies = [
"glob",
"nu-engine",
"nu-protocol",
"nu-table",
]
[[package]]
@ -310,6 +321,15 @@ dependencies = [
"codespan-reporting",
]
[[package]]
name = "nu-table"
version = "0.36.0"
dependencies = [
"nu-ansi-term",
"regex",
"unicode-width",
]
[[package]]
name = "num-integer"
version = "0.1.44"
@ -494,12 +514,29 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "regex"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remove_dir_all"
version = "0.5.3"

View File

@ -16,6 +16,7 @@ nu-command = { path="./crates/nu-command" }
nu-engine = { path="./crates/nu-engine" }
nu-parser = { path="./crates/nu-parser" }
nu-protocol = { path = "./crates/nu-protocol" }
nu-table = { path = "./crates/nu-table" }
# mimalloc = { version = "*", default-features = false }

View File

@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
nu-protocol = { path = "../nu-protocol" }
nu-engine = { path = "../nu-engine" }
nu-table = { path = "../nu-table" }
# Potential dependencies for extras
glob = "0.3.0"

View File

@ -7,6 +7,7 @@ use nu_protocol::{
use crate::{
where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv, Ls,
Table,
};
pub fn create_default_context() -> Rc<RefCell<EngineState>> {
@ -45,6 +46,8 @@ pub fn create_default_context() -> Rc<RefCell<EngineState>> {
working_set.add_decl(Box::new(Ls));
working_set.add_decl(Box::new(Table));
let sig = Signature::build("exit");
working_set.add_decl(sig.predeclare());
let sig = Signature::build("vars");

View File

@ -11,6 +11,7 @@ mod length;
mod let_;
mod let_env;
mod ls;
mod table;
mod where_;
pub use alias::Alias;
@ -26,3 +27,4 @@ pub use length::Length;
pub use let_::Let;
pub use let_env::LetEnv;
pub use ls::Ls;
pub use table::Table;

View File

@ -0,0 +1,125 @@
use std::collections::HashMap;
use nu_protocol::ast::{Call, PathMember};
use nu_protocol::engine::{Command, EvaluationContext};
use nu_protocol::{Signature, Span, Value};
use nu_table::StyledString;
pub struct Table;
//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one.
impl Command for Table {
fn name(&self) -> &str {
"table"
}
fn usage(&self) -> &str {
"Render the table."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("table")
}
fn run(
&self,
_context: &EvaluationContext,
call: &Call,
input: Value,
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
match input {
Value::List { vals, .. } => {
let table = convert_to_table(vals);
if let Some(table) = table {
let result = nu_table::draw_table(&table, 80, &HashMap::new());
Ok(Value::String {
val: result,
span: call.head,
})
} else {
Ok(Value::Nothing { span: call.head })
}
}
Value::Stream { stream, .. } => {
let table = convert_to_table(stream);
if let Some(table) = table {
let result = nu_table::draw_table(&table, 80, &HashMap::new());
Ok(Value::String {
val: result,
span: call.head,
})
} else {
Ok(Value::Nothing { span: call.head })
}
}
x => Ok(x),
}
}
}
fn convert_to_table(iter: impl IntoIterator<Item = Value>) -> Option<nu_table::Table> {
let mut iter = iter.into_iter().peekable();
if let Some(first) = iter.peek() {
let mut headers = first.columns();
headers.insert(0, "#".into());
let mut data = vec![];
for (row_num, item) in iter.enumerate() {
let mut row = vec![row_num.to_string()];
for header in headers.iter().skip(1) {
let result = item.clone().follow_cell_path(&[PathMember::String {
val: header.into(),
span: Span::unknown(),
}]);
match result {
Ok(value) => row.push(value.into_string()),
Err(_) => row.push(String::new()),
}
}
data.push(row);
}
Some(nu_table::Table {
headers: headers
.into_iter()
.map(|x| StyledString {
contents: x,
style: nu_table::TextStyle::default_header(),
})
.collect(),
data: data
.into_iter()
.map(|x| {
x.into_iter()
.enumerate()
.map(|(col, y)| {
if col == 0 {
StyledString {
contents: y,
style: nu_table::TextStyle::default_header(),
}
} else {
StyledString {
contents: y,
style: nu_table::TextStyle::basic_left(),
}
}
})
.collect::<Vec<StyledString>>()
})
.collect(),
theme: nu_table::Theme::rounded(),
})
} else {
None
}
}

View File

@ -288,6 +288,13 @@ impl Value {
pub fn is_true(&self) -> bool {
matches!(self, Value::Bool { val: true, .. })
}
pub fn columns(&self) -> Vec<String> {
match self {
Value::Record { cols, .. } => cols.clone(),
_ => vec![],
}
}
}
impl PartialEq for Value {

22
crates/nu-table/.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
/target
/scratch
**/*.rs.bk
history.txt
tests/fixtures/nuplayground
crates/*/target
# Debian/Ubuntu
debian/.debhelper/
debian/debhelper-build-stamp
debian/files
debian/nu.substvars
debian/nu/
# macOS junk
.DS_Store
# JetBrains' IDE items
.idea/*
# VSCode's IDE items
.vscode/*

View File

@ -0,0 +1,18 @@
[package]
authors = ["The Nu Project Contributors"]
description = "Nushell table printing"
edition = "2018"
license = "MIT"
name = "nu-table"
version = "0.36.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "table"
path = "src/main.rs"
[dependencies]
nu-ansi-term = "0.36.0"
regex = "1.4"
unicode-width = "0.1.8"

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,86 @@
use nu_table::{draw_table, StyledString, Table, TextStyle, Theme};
use std::collections::HashMap;
fn main() {
let args: Vec<_> = std::env::args().collect();
let mut width = 0;
if args.len() > 1 {
// Width in terminal characters
width = args[1].parse::<usize>().expect("Need a width in columns");
}
if width < 4 {
println!("Width must be greater than or equal to 4, setting width to 80");
width = 80;
}
// The mocked up table data
let (table_headers, row_data) = make_table_data();
// The table headers
let headers = vec_of_str_to_vec_of_styledstr(&table_headers, true);
// The table rows
let rows = vec_of_str_to_vec_of_styledstr(&row_data, false);
// The table itself
let table = Table::new(headers, vec![rows; 3], Theme::rounded());
// FIXME: Config isn't available from here so just put these here to compile
let color_hm: HashMap<String, nu_ansi_term::Style> = HashMap::new();
// Capture the table as a string
let output_table = draw_table(&table, width, &color_hm);
// Draw the table
println!("{}", output_table)
}
fn make_table_data() -> (Vec<&'static str>, Vec<&'static str>) {
let table_headers = vec![
"category",
"description",
"emoji",
"ios_version",
"unicode_version",
"aliases",
"tags",
"category2",
"description2",
"emoji2",
"ios_version2",
"unicode_version2",
"aliases2",
"tags2",
];
let row_data = vec![
"Smileys & Emotion",
"grinning face",
"😀",
"6",
"6.1",
"grinning",
"smile",
"Smileys & Emotion",
"grinning face",
"😀",
"6",
"6.1",
"grinning",
"smile",
];
(table_headers, row_data)
}
fn vec_of_str_to_vec_of_styledstr(data: &[&str], is_header: bool) -> Vec<StyledString> {
let mut v = vec![];
for x in data {
if is_header {
v.push(StyledString::new(
String::from(*x),
TextStyle::default_header(),
))
} else {
v.push(StyledString::new(String::from(*x), TextStyle::basic_left()))
}
}
v
}

1259
crates/nu-table/src/table.rs Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,274 @@
use crate::table::TextStyle;
use nu_ansi_term::Style;
use std::collections::HashMap;
use std::{fmt::Display, iter::Iterator};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy)]
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) -> Vec<Vec<Subline>> {
input
.split_terminator('\n')
.map(|line| {
line.split_terminator(' ')
.map(|x| Subline {
subline: x,
width: {
// We've tried UnicodeWidthStr::width(x), UnicodeSegmentation::graphemes(x, true).count()
// and x.chars().count() with all types of combinations. Currently, it appears that
// getting the max of char count and Unicode width seems to produce the best layout.
// However, it's not perfect.
let c = x.chars().count();
let u = UnicodeWidthStr::width(x);
std::cmp::max(c, u)
},
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
pub fn column_width(input: &[Vec<Subline>]) -> usize {
let mut max = 0;
for line in input {
let mut total = 0;
let mut first = true;
for inp in line {
if !first {
// Account for the space
total += 1;
} else {
first = false;
}
total += inp.width;
}
if total > max {
max = total;
}
}
max
}
fn split_word(cell_width: usize, word: &str) -> Vec<Subline> {
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>>,
color_hm: &HashMap<String, Style>,
re_leading: &regex::Regex,
re_trailing: &regex::Regex,
) -> (Vec<WrappedLine>, usize) {
let mut lines = vec![];
let mut current_line: Vec<Subline> = vec![];
let mut current_width = 0;
let mut first = true;
let mut max_width = 0;
let lead_trail_space_bg_color = color_hm
.get("leading_trailing_space_bg")
.unwrap_or(&Style::default())
.to_owned();
loop {
match input.next() {
Some(item) => {
if !first {
current_width += 1;
} else {
first = false;
}
if item.width + current_width > cell_width {
// If this is a really long single word, we need to split the word
if current_line.len() == 1 && current_width > cell_width {
max_width = cell_width;
let sublines = split_word(cell_width, current_line[0].subline);
for subline in sublines {
let width = subline.width;
lines.push(Line {
sublines: vec![subline],
width,
});
}
first = true;
current_width = item.width;
current_line = vec![item];
} else {
if !current_line.is_empty() {
lines.push(Line {
sublines: current_line,
width: current_width,
});
}
first = true;
current_width = item.width;
current_line = vec![item];
max_width = std::cmp::max(max_width, current_width);
}
} else {
current_width += item.width;
current_line.push(item);
}
}
None => {
if current_width > cell_width {
// We need to break up the last word
let sublines = split_word(cell_width, current_line[0].subline);
for subline in sublines {
let width = subline.width;
lines.push(Line {
sublines: vec![subline],
width,
});
}
} else if current_width > 0 {
lines.push(Line {
sublines: current_line,
width: current_width,
});
}
break;
}
}
}
let mut current_max = 0;
let mut output = vec![];
for line in lines {
let mut current_line_width = 0;
let mut first = true;
let mut current_line = String::new();
for subline in line.sublines {
if !first {
current_line_width += 1 + subline.width;
current_line.push(' ');
} else {
first = false;
current_line_width = subline.width;
}
current_line.push_str(subline.subline);
}
if current_line_width > current_max {
current_max = current_line_width;
}
// highlight leading and trailing spaces so they stand out.
let mut bg_color_string = Style::default().prefix().to_string();
// right now config settings can only set foreground colors so, in this
// instance we take the foreground color and make it a background color
if let Some(bg) = lead_trail_space_bg_color.foreground {
bg_color_string = Style::default().on(bg).prefix().to_string()
};
if let Some(leading_match) = re_leading.find(&current_line.clone()) {
String::insert_str(
&mut current_line,
leading_match.end(),
nu_ansi_term::ansi::RESET,
);
String::insert_str(&mut current_line, leading_match.start(), &bg_color_string);
}
if let Some(trailing_match) = re_trailing.find(&current_line.clone()) {
String::insert_str(&mut current_line, trailing_match.start(), &bg_color_string);
current_line += nu_ansi_term::ansi::RESET;
}
output.push(WrappedLine {
line: current_line,
width: current_line_width,
});
}
(output, current_max)
}