mirror of
https://github.com/nushell/nushell
synced 2024-12-25 20:43:09 +00:00
Add a very silly table
This commit is contained in:
parent
0694245ccd
commit
26d50ebcd5
13 changed files with 1840 additions and 0 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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"
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
125
crates/nu-command/src/table.rs
Normal file
125
crates/nu-command/src/table.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
22
crates/nu-table/.gitignore
vendored
Normal 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/*
|
18
crates/nu-table/Cargo.toml
Normal file
18
crates/nu-table/Cargo.toml
Normal 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"
|
5
crates/nu-table/src/lib.rs
Normal file
5
crates/nu-table/src/lib.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod table;
|
||||
mod wrap;
|
||||
|
||||
pub use table::{draw_table, StyledString, Table, TextStyle, Theme};
|
||||
pub use wrap::Alignment;
|
86
crates/nu-table/src/main.rs
Normal file
86
crates/nu-table/src/main.rs
Normal 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
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
274
crates/nu-table/src/wrap.rs
Normal 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: ®ex::Regex,
|
||||
re_trailing: ®ex::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(¤t_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(¤t_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)
|
||||
}
|
Loading…
Reference in a new issue