mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 16:33:37 +01:00
parent
892a416211
commit
a56abb6502
53
Cargo.lock
generated
53
Cargo.lock
generated
@ -607,6 +607,12 @@ dependencies = [
|
|||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.59"
|
version = "1.0.59"
|
||||||
@ -940,6 +946,22 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2fcdc3c9cf8ee446222e8ee8691a6d21b563b8fe1a64b1873080db7b5b23cf0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"lazy_static 1.4.0",
|
||||||
|
"libc",
|
||||||
|
"mio 0.7.0",
|
||||||
|
"parking_lot 0.11.0",
|
||||||
|
"signal-hook",
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossterm_winapi"
|
name = "crossterm_winapi"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@ -2850,6 +2872,7 @@ dependencies = [
|
|||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
"nu-value-ext",
|
"nu-value-ext",
|
||||||
"nu_plugin_binaryview",
|
"nu_plugin_binaryview",
|
||||||
|
"nu_plugin_chart",
|
||||||
"nu_plugin_fetch",
|
"nu_plugin_fetch",
|
||||||
"nu_plugin_from_bson",
|
"nu_plugin_from_bson",
|
||||||
"nu_plugin_from_sqlite",
|
"nu_plugin_from_sqlite",
|
||||||
@ -3116,7 +3139,7 @@ name = "nu_plugin_binaryview"
|
|||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term 0.12.1",
|
"ansi_term 0.12.1",
|
||||||
"crossterm",
|
"crossterm 0.18.0",
|
||||||
"image",
|
"image",
|
||||||
"neso",
|
"neso",
|
||||||
"nu-errors",
|
"nu-errors",
|
||||||
@ -3127,6 +3150,21 @@ dependencies = [
|
|||||||
"rawkey",
|
"rawkey",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu_plugin_chart"
|
||||||
|
version = "0.20.0"
|
||||||
|
dependencies = [
|
||||||
|
"crossterm 0.18.0",
|
||||||
|
"nu-cli",
|
||||||
|
"nu-data",
|
||||||
|
"nu-errors",
|
||||||
|
"nu-plugin",
|
||||||
|
"nu-protocol",
|
||||||
|
"nu-source",
|
||||||
|
"nu-value-ext",
|
||||||
|
"tui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu_plugin_fetch"
|
name = "nu_plugin_fetch"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
@ -5471,6 +5509,19 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tui"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2eaeee894a1e9b90f80aa466fe59154fdb471980b5e104d8836fcea309ae17e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cassowary",
|
||||||
|
"crossterm 0.17.8",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typed-arena"
|
name = "typed-arena"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
|
50
Cargo.toml
50
Cargo.toml
@ -27,6 +27,7 @@ nu-protocol = {version = "0.20.0", path = "./crates/nu-protocol"}
|
|||||||
nu-source = {version = "0.20.0", path = "./crates/nu-source"}
|
nu-source = {version = "0.20.0", path = "./crates/nu-source"}
|
||||||
nu-value-ext = {version = "0.20.0", path = "./crates/nu-value-ext"}
|
nu-value-ext = {version = "0.20.0", path = "./crates/nu-value-ext"}
|
||||||
|
|
||||||
|
nu_plugin_chart = {version = "0.20.0", path = "./crates/nu_plugin_chart", optional = true}
|
||||||
nu_plugin_binaryview = {version = "0.20.0", path = "./crates/nu_plugin_binaryview", optional = true}
|
nu_plugin_binaryview = {version = "0.20.0", path = "./crates/nu_plugin_binaryview", optional = true}
|
||||||
nu_plugin_fetch = {version = "0.20.0", path = "./crates/nu_plugin_fetch", optional = true}
|
nu_plugin_fetch = {version = "0.20.0", path = "./crates/nu_plugin_fetch", optional = true}
|
||||||
nu_plugin_from_bson = {version = "0.20.0", path = "./crates/nu_plugin_from_bson", optional = true}
|
nu_plugin_from_bson = {version = "0.20.0", path = "./crates/nu_plugin_from_bson", optional = true}
|
||||||
@ -59,6 +60,16 @@ serde = {version = "1.0.115", features = ["derive"]}
|
|||||||
toml = "0.5.6"
|
toml = "0.5.6"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
ctrlc-support = ["nu-cli/ctrlc"]
|
||||||
|
directories-support = ["nu-cli/directories", "nu-cli/dirs", "nu-data/directories", "nu-data/dirs"]
|
||||||
|
git-support = ["nu-cli/git2"]
|
||||||
|
ptree-support = ["nu-cli/ptree"]
|
||||||
|
rich-benchmark = ["nu-cli/rich-benchmark"]
|
||||||
|
rustyline-support = ["nu-cli/rustyline-support"]
|
||||||
|
term-support = ["nu-cli/term"]
|
||||||
|
uuid-support = ["nu-cli/uuid_crate"]
|
||||||
|
which-support = ["nu-cli/ichwh", "nu-cli/which"]
|
||||||
|
|
||||||
default = [
|
default = [
|
||||||
"sys",
|
"sys",
|
||||||
"ps",
|
"ps",
|
||||||
@ -77,38 +88,30 @@ default = [
|
|||||||
"fetch",
|
"fetch",
|
||||||
"rich-benchmark",
|
"rich-benchmark",
|
||||||
]
|
]
|
||||||
extra = ["default", "binaryview", "tree", "clipboard-cli", "trash-support", "start", "bson", "sqlite", "s3"]
|
extra = ["default", "binaryview", "tree", "clipboard-cli", "trash-support", "start", "bson", "sqlite", "s3", "chart"]
|
||||||
stable = ["default"]
|
stable = ["default"]
|
||||||
|
|
||||||
# Default
|
trace = ["nu-parser/trace"]
|
||||||
|
|
||||||
|
# Stable (Default)
|
||||||
inc = ["nu_plugin_inc"]
|
inc = ["nu_plugin_inc"]
|
||||||
ps = ["nu_plugin_ps"]
|
ps = ["nu_plugin_ps"]
|
||||||
sys = ["nu_plugin_sys"]
|
sys = ["nu_plugin_sys"]
|
||||||
textview = ["nu_plugin_textview"]
|
textview = ["nu_plugin_textview"]
|
||||||
|
|
||||||
# Stable
|
|
||||||
binaryview = ["nu_plugin_binaryview"]
|
|
||||||
bson = ["nu_plugin_from_bson", "nu_plugin_to_bson"]
|
|
||||||
fetch = ["nu_plugin_fetch"]
|
fetch = ["nu_plugin_fetch"]
|
||||||
match = ["nu_plugin_match"]
|
match = ["nu_plugin_match"]
|
||||||
post = ["nu_plugin_post"]
|
post = ["nu_plugin_post"]
|
||||||
|
|
||||||
|
# Extra
|
||||||
|
bson = ["nu_plugin_from_bson", "nu_plugin_to_bson"]
|
||||||
|
chart = ["nu_plugin_chart"]
|
||||||
|
binaryview = ["nu_plugin_binaryview"]
|
||||||
|
clipboard-cli = ["nu-cli/clipboard-cli"]
|
||||||
|
trash-support = ["nu-cli/trash-support"]
|
||||||
|
start = ["nu_plugin_start"]
|
||||||
|
tree = ["nu_plugin_tree"]
|
||||||
s3 = ["nu_plugin_s3"]
|
s3 = ["nu_plugin_s3"]
|
||||||
sqlite = ["nu_plugin_from_sqlite", "nu_plugin_to_sqlite"]
|
sqlite = ["nu_plugin_from_sqlite", "nu_plugin_to_sqlite"]
|
||||||
start = ["nu_plugin_start"]
|
|
||||||
trace = ["nu-parser/trace"]
|
|
||||||
tree = ["nu_plugin_tree"]
|
|
||||||
|
|
||||||
clipboard-cli = ["nu-cli/clipboard-cli"]
|
|
||||||
ctrlc-support = ["nu-cli/ctrlc"]
|
|
||||||
directories-support = ["nu-cli/directories", "nu-cli/dirs", "nu-data/directories", "nu-data/dirs"]
|
|
||||||
git-support = ["nu-cli/git2"]
|
|
||||||
ptree-support = ["nu-cli/ptree"]
|
|
||||||
rich-benchmark = ["nu-cli/rich-benchmark"]
|
|
||||||
rustyline-support = ["nu-cli/rustyline-support"]
|
|
||||||
term-support = ["nu-cli/term"]
|
|
||||||
trash-support = ["nu-cli/trash-support"]
|
|
||||||
uuid-support = ["nu-cli/uuid_crate"]
|
|
||||||
which-support = ["nu-cli/ichwh", "nu-cli/which"]
|
|
||||||
|
|
||||||
# Core plugins that ship with `cargo install nu` by default
|
# Core plugins that ship with `cargo install nu` by default
|
||||||
# Currently, Cargo limits us to installing only one binary
|
# Currently, Cargo limits us to installing only one binary
|
||||||
@ -170,6 +173,11 @@ name = "nu_plugin_extra_s3"
|
|||||||
path = "src/plugins/nu_plugin_extra_s3.rs"
|
path = "src/plugins/nu_plugin_extra_s3.rs"
|
||||||
required-features = ["s3"]
|
required-features = ["s3"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "nu_plugin_extra_chart"
|
||||||
|
path = "src/plugins/nu_plugin_extra_chart.rs"
|
||||||
|
required-features = ["chart"]
|
||||||
|
|
||||||
# Main nu binary
|
# Main nu binary
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "nu"
|
name = "nu"
|
||||||
|
@ -112,8 +112,9 @@ pub async fn histogram(
|
|||||||
nu_data::utils::Operation {
|
nu_data::utils::Operation {
|
||||||
grouper: Some(Box::new(move |_, _| Ok(String::from("frequencies")))),
|
grouper: Some(Box::new(move |_, _| Ok(String::from("frequencies")))),
|
||||||
splitter: Some(splitter(column_grouper)),
|
splitter: Some(splitter(column_grouper)),
|
||||||
format: None,
|
format: &None,
|
||||||
eval: &evaluate_with,
|
eval: &evaluate_with,
|
||||||
|
reduction: &nu_data::utils::Reduction::Count,
|
||||||
},
|
},
|
||||||
&name,
|
&name,
|
||||||
)?;
|
)?;
|
||||||
@ -123,17 +124,33 @@ pub async fn histogram(
|
|||||||
|
|
||||||
Ok(futures::stream::iter(
|
Ok(futures::stream::iter(
|
||||||
results
|
results
|
||||||
.percentages
|
.data
|
||||||
.table_entries()
|
.table_entries()
|
||||||
.map(move |value| {
|
.cloned()
|
||||||
let values = value.table_entries().cloned().collect::<Vec<_>>();
|
|
||||||
let occurrences = values.len();
|
|
||||||
|
|
||||||
(occurrences, values[occurrences - 1].clone())
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(move |(occurrences, value)| {
|
.zip(
|
||||||
|
results
|
||||||
|
.percentages
|
||||||
|
.table_entries()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter(),
|
||||||
|
)
|
||||||
|
.map(move |(counts, percentages)| {
|
||||||
|
let percentage = percentages
|
||||||
|
.table_entries()
|
||||||
|
.cloned()
|
||||||
|
.last()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
UntaggedValue::decimal_from_float(0.0, name.span).into_value(&name)
|
||||||
|
});
|
||||||
|
let value = counts
|
||||||
|
.table_entries()
|
||||||
|
.cloned()
|
||||||
|
.last()
|
||||||
|
.unwrap_or_else(|| UntaggedValue::int(0).into_value(&name));
|
||||||
|
|
||||||
let mut fact = TaggedDictBuilder::new(&name);
|
let mut fact = TaggedDictBuilder::new(&name);
|
||||||
let column_value = labels
|
let column_value = labels
|
||||||
.get(idx)
|
.get(idx)
|
||||||
@ -147,19 +164,19 @@ pub async fn histogram(
|
|||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
fact.insert_value(&column.item, column_value);
|
fact.insert_value(&column.item, column_value);
|
||||||
fact.insert_untagged("occurrences", UntaggedValue::int(occurrences));
|
fact.insert_untagged("count", value);
|
||||||
|
|
||||||
let percentage = format!(
|
let fmt_percentage = format!(
|
||||||
"{}%",
|
"{}%",
|
||||||
// Some(2) < the number of digits
|
// Some(2) < the number of digits
|
||||||
// true < group the digits
|
// true < group the digits
|
||||||
crate::commands::str_::from::action(&value, &name, Some(2), true)?
|
crate::commands::str_::from::action(&percentage, &name, Some(2), true)?
|
||||||
.as_string()?
|
.as_string()?
|
||||||
);
|
);
|
||||||
fact.insert_untagged("percentage", UntaggedValue::string(percentage));
|
fact.insert_untagged("percentage", UntaggedValue::string(fmt_percentage));
|
||||||
|
|
||||||
let string = std::iter::repeat("*")
|
let string = std::iter::repeat("*")
|
||||||
.take(value.as_u64().map_err(|_| {
|
.take(percentage.as_u64().map_err(|_| {
|
||||||
ShellError::labeled_error("expected a number", "expected a number", &name)
|
ShellError::labeled_error("expected a number", "expected a number", &name)
|
||||||
})? as usize)
|
})? as usize)
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
|
@ -54,7 +54,7 @@ fn summarizes_by_values() {
|
|||||||
| get rusty_at
|
| get rusty_at
|
||||||
| histogram
|
| histogram
|
||||||
| where value == "Estados Unidos"
|
| where value == "Estados Unidos"
|
||||||
| get occurrences
|
| get count
|
||||||
| echo $it
|
| echo $it
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
@ -93,20 +93,19 @@ fn help() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn occurrences() {
|
fn count() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: ".", pipeline(
|
cwd: ".", pipeline(
|
||||||
r#"
|
r#"
|
||||||
echo "[{"bit":1},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":1}]"
|
echo [[bit]; [1] [0] [0] [0] [0] [0] [0] [1]]
|
||||||
| from json
|
|
||||||
| histogram bit
|
| histogram bit
|
||||||
| sort-by occurrences
|
| sort-by count
|
||||||
| reject frequency
|
| reject frequency
|
||||||
| to json
|
| to json
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
let bit_json = r#"[{"bit":"1","occurrences":2,"percentage":"33.33%"},{"bit":"0","occurrences":6,"percentage":"100.00%"}]"#;
|
let bit_json = r#"[{"bit":"1","count":2,"percentage":"33.33%"},{"bit":"0","count":6,"percentage":"100.00%"}]"#;
|
||||||
|
|
||||||
assert_eq!(actual.out, bit_json);
|
assert_eq!(actual.out, bit_json);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
use crate::value::compute_values;
|
use crate::value::unsafe_compute_values;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use nu_errors::ShellError;
|
use nu_errors::ShellError;
|
||||||
use nu_protocol::hir::Operator;
|
use nu_protocol::hir::Operator;
|
||||||
@ -23,6 +23,14 @@ impl Labels {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn at_split(&self, idx: usize) -> Option<&str> {
|
||||||
|
if let Some(k) = self.y.get(idx) {
|
||||||
|
Some(&k[..])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn grouped(&self) -> impl Iterator<Item = &String> {
|
pub fn grouped(&self) -> impl Iterator<Item = &String> {
|
||||||
self.x.iter()
|
self.x.iter()
|
||||||
}
|
}
|
||||||
@ -51,7 +59,7 @@ fn formula(
|
|||||||
calculator: Box<dyn Fn(Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static>,
|
calculator: Box<dyn Fn(Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static>,
|
||||||
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
|
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
|
||||||
Box::new(move |acc, datax| -> Result<Value, ShellError> {
|
Box::new(move |acc, datax| -> Result<Value, ShellError> {
|
||||||
let result = match compute_values(Operator::Multiply, &acc, &acc_begin) {
|
let result = match unsafe_compute_values(Operator::Multiply, &acc, &acc_begin) {
|
||||||
Ok(v) => v.into_untagged_value(),
|
Ok(v) => v.into_untagged_value(),
|
||||||
Err((left_type, right_type)) => {
|
Err((left_type, right_type)) => {
|
||||||
return Err(ShellError::coerce_error(
|
return Err(ShellError::coerce_error(
|
||||||
@ -62,7 +70,8 @@ fn formula(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match calculator(datax) {
|
match calculator(datax) {
|
||||||
Ok(total) => Ok(match compute_values(Operator::Plus, &result, &total) {
|
Ok(total) => Ok(
|
||||||
|
match unsafe_compute_values(Operator::Plus, &result, &total) {
|
||||||
Ok(v) => v.into_untagged_value(),
|
Ok(v) => v.into_untagged_value(),
|
||||||
Err((left_type, right_type)) => {
|
Err((left_type, right_type)) => {
|
||||||
return Err(ShellError::coerce_error(
|
return Err(ShellError::coerce_error(
|
||||||
@ -70,35 +79,91 @@ fn formula(
|
|||||||
right_type.spanned_unknown(),
|
right_type.spanned_unknown(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
Err(reason) => Err(reason),
|
Err(reason) => Err(reason),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reducer_for(
|
pub fn reducer_for(
|
||||||
command: Reduction,
|
command: &Reduction,
|
||||||
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
|
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
|
||||||
match command {
|
match command {
|
||||||
Reduction::Accumulate => Box::new(formula(
|
Reduction::Accumulate => Box::new(formula(
|
||||||
UntaggedValue::int(1).into_untagged_value(),
|
UntaggedValue::int(1).into_untagged_value(),
|
||||||
Box::new(sum),
|
Box::new(sum),
|
||||||
)),
|
)),
|
||||||
_ => Box::new(formula(
|
Reduction::Count => Box::new(formula(
|
||||||
UntaggedValue::int(0).into_untagged_value(),
|
UntaggedValue::int(0).into_untagged_value(),
|
||||||
Box::new(sum),
|
Box::new(sum),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max(values: &Value, tag: impl Into<Tag>) -> Result<&Value, ShellError> {
|
pub fn max(values: &Value, tag: impl Into<Tag>) -> Result<Value, ShellError> {
|
||||||
let tag = tag.into();
|
let tag = tag.into();
|
||||||
|
|
||||||
values
|
let mut x = UntaggedValue::int(0);
|
||||||
.table_entries()
|
|
||||||
.filter_map(|dataset| dataset.table_entries().max())
|
for split in values.table_entries() {
|
||||||
.max()
|
match split.value {
|
||||||
.ok_or_else(|| ShellError::labeled_error("err", "err", &tag))
|
UntaggedValue::Table(ref values) => {
|
||||||
|
let inner = inner_max(values)?;
|
||||||
|
|
||||||
|
if let Ok(greater_than) =
|
||||||
|
crate::value::compare_values(Operator::GreaterThan, &inner.value, &x)
|
||||||
|
{
|
||||||
|
if greater_than {
|
||||||
|
x = inner.value.clone();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(ShellError::unexpected(format!(
|
||||||
|
"Could not compare\nleft: {:?}\nright: {:?}",
|
||||||
|
inner.value, x
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(ShellError::labeled_error(
|
||||||
|
"Attempted to compute the sum of a value that cannot be summed.",
|
||||||
|
"value appears here",
|
||||||
|
split.tag.span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(x.into_value(&tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner_max(data: &[Value]) -> Result<Value, ShellError> {
|
||||||
|
let mut biggest = data
|
||||||
|
.first()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ShellError::unexpected("Cannot perform aggregate math operation on empty data")
|
||||||
|
})?
|
||||||
|
.value
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
for value in data.iter() {
|
||||||
|
if let Ok(greater_than) =
|
||||||
|
crate::value::compare_values(Operator::GreaterThan, &value.value, &biggest)
|
||||||
|
{
|
||||||
|
if greater_than {
|
||||||
|
biggest = value.value.clone();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(ShellError::unexpected(format!(
|
||||||
|
"Could not compare\nleft: {:?}\nright: {:?}",
|
||||||
|
biggest, value.value
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value {
|
||||||
|
value: biggest,
|
||||||
|
tag: Tag::unknown(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> {
|
pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> {
|
||||||
@ -107,7 +172,7 @@ pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> {
|
|||||||
for value in data {
|
for value in data {
|
||||||
match value.value {
|
match value.value {
|
||||||
UntaggedValue::Primitive(_) => {
|
UntaggedValue::Primitive(_) => {
|
||||||
acc = match compute_values(Operator::Plus, &acc, &value) {
|
acc = match unsafe_compute_values(Operator::Plus, &acc, &value) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err((left_type, right_type)) => {
|
Err((left_type, right_type)) => {
|
||||||
return Err(ShellError::coerce_error(
|
return Err(ShellError::coerce_error(
|
||||||
@ -133,19 +198,12 @@ pub fn sort_columns(
|
|||||||
values: &[String],
|
values: &[String],
|
||||||
format: &Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
|
format: &Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
|
||||||
) -> Result<Vec<String>, ShellError> {
|
) -> Result<Vec<String>, ShellError> {
|
||||||
let mut keys = vec![];
|
let mut keys = values.to_vec();
|
||||||
|
|
||||||
if let Some(fmt) = format {
|
|
||||||
for k in values.iter() {
|
|
||||||
let k = k.clone().tagged_unknown();
|
|
||||||
let v = crate::value::Date::naive_from_str(k.borrow_tagged())?.into_untagged_value();
|
|
||||||
keys.push(fmt(&v, k.to_string())?);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
keys = values.to_vec();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if format.is_none() {
|
||||||
keys.sort();
|
keys.sort();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,17 +225,13 @@ pub fn sort(planes: &Labels, values: &Value, tag: impl Into<Tag>) -> Result<Valu
|
|||||||
let grouped = groups.get_data_by_key(key.borrow_spanned());
|
let grouped = groups.get_data_by_key(key.borrow_spanned());
|
||||||
|
|
||||||
if let Some(grouped) = grouped {
|
if let Some(grouped) = grouped {
|
||||||
y.push(grouped.table_entries().cloned().collect::<Vec<_>>());
|
y.push(grouped);
|
||||||
} else {
|
} else {
|
||||||
let empty = UntaggedValue::table(&[]).into_value(&tag);
|
y.push(UntaggedValue::Table(vec![]).into_value(&tag));
|
||||||
y.push(empty.table_entries().cloned().collect::<Vec<_>>());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
x.push(
|
x.push(UntaggedValue::table(&y).into_value(&tag));
|
||||||
UntaggedValue::table(&y.iter().cloned().flatten().collect::<Vec<Value>>())
|
|
||||||
.into_value(&tag),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(UntaggedValue::table(&x).into_value(&tag))
|
Ok(UntaggedValue::table(&x).into_value(&tag))
|
||||||
@ -195,17 +249,27 @@ pub fn evaluate(
|
|||||||
let mut y = vec![];
|
let mut y = vec![];
|
||||||
|
|
||||||
for (idx, subset) in split.table_entries().enumerate() {
|
for (idx, subset) in split.table_entries().enumerate() {
|
||||||
let mut set = vec![];
|
if let UntaggedValue::Table(values) = &subset.value {
|
||||||
|
|
||||||
if let Some(ref evaluator) = evaluator {
|
if let Some(ref evaluator) = evaluator {
|
||||||
let value = evaluator(idx, subset)?;
|
let mut evaluations = vec![];
|
||||||
|
|
||||||
set.push(value);
|
for set in values.iter() {
|
||||||
} else {
|
evaluations.push(evaluator(idx, set)?);
|
||||||
set.push(UntaggedValue::int(1).into_value(&tag));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
y.push(UntaggedValue::table(&set).into_value(&tag));
|
y.push(UntaggedValue::Table(evaluations).into_value(&tag));
|
||||||
|
} else {
|
||||||
|
y.push(
|
||||||
|
UntaggedValue::Table(
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|_| UntaggedValue::int(1).into_value(&tag))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.into_value(&tag),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
x.push(UntaggedValue::table(&y).into_value(&tag));
|
x.push(UntaggedValue::table(&y).into_value(&tag));
|
||||||
@ -215,14 +279,17 @@ pub fn evaluate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub enum Reduction {
|
pub enum Reduction {
|
||||||
#[allow(dead_code)]
|
|
||||||
Count,
|
Count,
|
||||||
Accumulate,
|
Accumulate,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reduce(values: &Value, tag: impl Into<Tag>) -> Result<Value, ShellError> {
|
pub fn reduce(
|
||||||
|
values: &Value,
|
||||||
|
reduction_with: &Reduction,
|
||||||
|
tag: impl Into<Tag>,
|
||||||
|
) -> Result<Value, ShellError> {
|
||||||
let tag = tag.into();
|
let tag = tag.into();
|
||||||
let reduce_with = reducer_for(Reduction::Accumulate);
|
let reduce_with = reducer_for(reduction_with);
|
||||||
|
|
||||||
let mut datasets = vec![];
|
let mut datasets = vec![];
|
||||||
for dataset in values.table_entries() {
|
for dataset in values.table_entries() {
|
||||||
@ -255,8 +322,8 @@ pub fn percentages(
|
|||||||
.filter_map(|s| {
|
.filter_map(|s| {
|
||||||
let hundred = UntaggedValue::decimal_from_float(100.0, tag.span);
|
let hundred = UntaggedValue::decimal_from_float(100.0, tag.span);
|
||||||
|
|
||||||
match compute_values(Operator::Divide, &hundred, &maxima) {
|
match unsafe_compute_values(Operator::Divide, &hundred, &maxima) {
|
||||||
Ok(v) => match compute_values(Operator::Multiply, &s, &v) {
|
Ok(v) => match unsafe_compute_values(Operator::Multiply, &s, &v) {
|
||||||
Ok(v) => Some(v.into_untagged_value()),
|
Ok(v) => Some(v.into_untagged_value()),
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
},
|
},
|
||||||
|
@ -6,6 +6,7 @@ mod internal;
|
|||||||
pub use crate::utils::group::group;
|
pub use crate::utils::group::group;
|
||||||
pub use crate::utils::split::split;
|
pub use crate::utils::split::split;
|
||||||
|
|
||||||
|
pub use crate::utils::internal::Reduction;
|
||||||
use crate::utils::internal::*;
|
use crate::utils::internal::*;
|
||||||
|
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
@ -27,8 +28,9 @@ pub struct Model {
|
|||||||
pub struct Operation<'a> {
|
pub struct Operation<'a> {
|
||||||
pub grouper: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
|
pub grouper: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
|
||||||
pub splitter: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
|
pub splitter: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
|
||||||
pub format: Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
|
pub format: &'a Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
|
||||||
pub eval: &'a Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
|
pub eval: &'a Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
|
||||||
|
pub reduction: &'a Reduction,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report(
|
pub fn report(
|
||||||
@ -46,19 +48,17 @@ pub fn report(
|
|||||||
.map(|(key, _)| key.clone())
|
.map(|(key, _)| key.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let x = if options.format.is_some() {
|
let x = sort_columns(&x, &options.format)?;
|
||||||
sort_columns(&x, &options.format)
|
|
||||||
} else {
|
|
||||||
sort_columns(&x, &None)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let mut y = splitted
|
let mut y = splitted
|
||||||
.row_entries()
|
.row_entries()
|
||||||
.map(|(key, _)| key.clone())
|
.map(|(key, _)| key.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
y.sort();
|
y.sort();
|
||||||
|
|
||||||
let planes = Labels { x, y };
|
let planes = Labels { x, y };
|
||||||
|
|
||||||
let sorted = sort(&planes, &splitted, &tag)?;
|
let sorted = sort(&planes, &splitted, &tag)?;
|
||||||
|
|
||||||
let evaluated = evaluate(
|
let evaluated = evaluate(
|
||||||
@ -72,11 +72,11 @@ pub fn report(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let group_labels = planes.grouping_total();
|
let group_labels = planes.grouping_total();
|
||||||
|
let split_labels = planes.splits_total();
|
||||||
|
|
||||||
let reduced = reduce(&evaluated, &tag)?;
|
let reduced = reduce(&evaluated, options.reduction, &tag)?;
|
||||||
|
|
||||||
let max = max(&reduced, &tag)?.clone();
|
let maxima = max(&reduced, &tag)?;
|
||||||
let maxima = max.clone();
|
|
||||||
|
|
||||||
let percents = percentages(&maxima, &reduced, &tag)?;
|
let percents = percentages(&maxima, &reduced, &tag)?;
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ pub fn report(
|
|||||||
},
|
},
|
||||||
Range {
|
Range {
|
||||||
start: UntaggedValue::int(0).into_untagged_value(),
|
start: UntaggedValue::int(0).into_untagged_value(),
|
||||||
end: max,
|
end: split_labels,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
data: reduced,
|
data: reduced,
|
||||||
@ -99,7 +99,6 @@ pub fn report(
|
|||||||
|
|
||||||
pub mod helpers {
|
pub mod helpers {
|
||||||
use super::Model;
|
use super::Model;
|
||||||
use bigdecimal::BigDecimal;
|
|
||||||
use indexmap::indexmap;
|
use indexmap::indexmap;
|
||||||
use nu_errors::ShellError;
|
use nu_errors::ShellError;
|
||||||
use nu_protocol::{UntaggedValue, Value};
|
use nu_protocol::{UntaggedValue, Value};
|
||||||
@ -113,10 +112,6 @@ pub mod helpers {
|
|||||||
UntaggedValue::int(s).into_untagged_value()
|
UntaggedValue::int(s).into_untagged_value()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decimal(f: impl Into<BigDecimal>) -> Value {
|
|
||||||
UntaggedValue::decimal(f.into()).into_untagged_value()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decimal_from_float(f: f64, span: Span) -> Value {
|
pub fn decimal_from_float(f: f64, span: Span) -> Value {
|
||||||
UntaggedValue::decimal_from_float(f, span).into_untagged_value()
|
UntaggedValue::decimal_from_float(f, span).into_untagged_value()
|
||||||
}
|
}
|
||||||
@ -216,9 +211,12 @@ pub mod helpers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn date_formatter(
|
pub fn date_formatter(
|
||||||
fmt: &'static str,
|
fmt: String,
|
||||||
) -> Box<dyn Fn(&Value, String) -> Result<String, ShellError>> {
|
) -> Box<dyn Fn(&Value, String) -> Result<String, ShellError>> {
|
||||||
Box::new(move |date: &Value, _: String| date.format(&fmt))
|
Box::new(move |date: &Value, _: String| {
|
||||||
|
let fmt = fmt.clone();
|
||||||
|
date.format(&fmt)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_without_checking_percentages(report_a: Model, report_b: Model) {
|
pub fn assert_without_checking_percentages(report_a: Model, report_b: Model) {
|
||||||
@ -232,23 +230,24 @@ pub mod helpers {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::helpers::{
|
use super::helpers::{
|
||||||
assert_without_checking_percentages, committers, date_formatter, decimal,
|
assert_without_checking_percentages, committers, date_formatter, decimal_from_float, int,
|
||||||
decimal_from_float, int, table,
|
table,
|
||||||
};
|
};
|
||||||
use super::{report, Labels, Model, Operation, Range};
|
use super::{report, Labels, Model, Operation, Range, Reduction};
|
||||||
use nu_errors::ShellError;
|
use nu_errors::ShellError;
|
||||||
use nu_protocol::Value;
|
use nu_protocol::Value;
|
||||||
use nu_source::{Span, Tag, TaggedItem};
|
use nu_source::{Span, Tag, TaggedItem};
|
||||||
use nu_value_ext::ValueExt;
|
use nu_value_ext::ValueExt;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prepares_report_using_accumulating_value() {
|
fn prepares_report_using_counting_value() {
|
||||||
let committers = table(&committers());
|
let committers = table(&committers());
|
||||||
|
|
||||||
let by_date = Box::new(move |_, row: &Value| {
|
let by_date = Box::new(move |_, row: &Value| {
|
||||||
let key = String::from("date").tagged_unknown();
|
let key = String::from("date").tagged_unknown();
|
||||||
let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
|
let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
|
||||||
|
|
||||||
let callback = date_formatter("%Y-%m-%d");
|
let callback = date_formatter("%Y-%m-%d".to_string());
|
||||||
callback(&key, "nothing".to_string())
|
callback(&key, "nothing".to_string())
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -261,7 +260,7 @@ mod tests {
|
|||||||
let options = Operation {
|
let options = Operation {
|
||||||
grouper: Some(by_date),
|
grouper: Some(by_date),
|
||||||
splitter: Some(by_country),
|
splitter: Some(by_country),
|
||||||
format: Some(date_formatter("%Y-%m-%d")),
|
format: &None,
|
||||||
eval: /* value to be used for accumulation */ &Some(Box::new(move |_, value: &Value| {
|
eval: /* value to be used for accumulation */ &Some(Box::new(move |_, value: &Value| {
|
||||||
let chickens_key = String::from("chickens").tagged_unknown();
|
let chickens_key = String::from("chickens").tagged_unknown();
|
||||||
|
|
||||||
@ -275,6 +274,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})),
|
})),
|
||||||
|
reduction: &Reduction::Count
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_without_checking_percentages(
|
assert_without_checking_percentages(
|
||||||
@ -295,29 +295,29 @@ mod tests {
|
|||||||
},
|
},
|
||||||
Range {
|
Range {
|
||||||
start: int(0),
|
start: int(0),
|
||||||
end: int(60),
|
end: int(3),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
data: table(&[
|
data: table(&[
|
||||||
table(&[int(10), int(30), int(60)]),
|
table(&[int(10), int(20), int(30)]),
|
||||||
table(&[int(5), int(15), int(30)]),
|
table(&[int(5), int(10), int(15)]),
|
||||||
table(&[int(2), int(6), int(12)]),
|
table(&[int(2), int(4), int(6)]),
|
||||||
]),
|
]),
|
||||||
percentages: table(&[
|
percentages: table(&[
|
||||||
|
table(&[
|
||||||
|
decimal_from_float(33.33, Span::unknown()),
|
||||||
|
decimal_from_float(66.66, Span::unknown()),
|
||||||
|
decimal_from_float(99.99, Span::unknown()),
|
||||||
|
]),
|
||||||
table(&[
|
table(&[
|
||||||
decimal_from_float(16.66, Span::unknown()),
|
decimal_from_float(16.66, Span::unknown()),
|
||||||
decimal(50),
|
decimal_from_float(33.33, Span::unknown()),
|
||||||
decimal(100),
|
decimal_from_float(49.99, Span::unknown()),
|
||||||
]),
|
]),
|
||||||
table(&[
|
table(&[
|
||||||
decimal_from_float(8.33, Span::unknown()),
|
decimal_from_float(6.66, Span::unknown()),
|
||||||
decimal(25),
|
decimal_from_float(13.33, Span::unknown()),
|
||||||
decimal(50),
|
decimal_from_float(19.99, Span::unknown()),
|
||||||
]),
|
|
||||||
table(&[
|
|
||||||
decimal_from_float(3.33, Span::unknown()),
|
|
||||||
decimal(10),
|
|
||||||
decimal(20),
|
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
@ -16,7 +16,7 @@ pub fn split(
|
|||||||
let mut out = TaggedDictBuilder::new(&tag);
|
let mut out = TaggedDictBuilder::new(&tag);
|
||||||
|
|
||||||
if splitter.is_none() {
|
if splitter.is_none() {
|
||||||
out.insert_untagged("table", UntaggedValue::table(&[value.clone()]));
|
out.insert_untagged("table", value.clone());
|
||||||
return Ok(out.into_value());
|
return Ok(out.into_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use crate::value::{UntaggedValue, Value};
|
use crate::value::{UntaggedValue, Value};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum RowValueIter<'a> {
|
pub enum RowValueIter<'a> {
|
||||||
Empty,
|
Empty,
|
||||||
Entries(indexmap::map::Iter<'a, String, Value>),
|
Entries(indexmap::map::Iter<'a, String, Value>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum TableValueIter<'a> {
|
pub enum TableValueIter<'a> {
|
||||||
Empty,
|
Empty,
|
||||||
Entries(std::slice::Iter<'a, Value>),
|
Entries(std::slice::Iter<'a, Value>),
|
||||||
|
@ -11,7 +11,7 @@ doctest = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.12.1"
|
ansi_term = "0.12.1"
|
||||||
crossterm = "0.17"
|
crossterm = "0.18"
|
||||||
image = {version = "0.22.4", default_features = false, features = ["png_codec", "jpeg"]}
|
image = {version = "0.22.4", default_features = false, features = ["png_codec", "jpeg"]}
|
||||||
neso = "0.5.0"
|
neso = "0.5.0"
|
||||||
nu-errors = {path = "../nu-errors", version = "0.20.0"}
|
nu-errors = {path = "../nu-errors", version = "0.20.0"}
|
||||||
|
22
crates/nu_plugin_chart/Cargo.toml
Normal file
22
crates/nu_plugin_chart/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["The Nu Project Contributors"]
|
||||||
|
description = "A plugin to display charts"
|
||||||
|
edition = "2018"
|
||||||
|
license = "MIT"
|
||||||
|
name = "nu_plugin_chart"
|
||||||
|
version = "0.20.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nu-data = {path = "../nu-data", version = "0.20.0"}
|
||||||
|
nu-errors = {path = "../nu-errors", version = "0.20.0"}
|
||||||
|
nu-plugin = {path = "../nu-plugin", version = "0.20.0"}
|
||||||
|
nu-protocol = {path = "../nu-protocol", version = "0.20.0"}
|
||||||
|
nu-source = {path = "../nu-source", version = "0.20.0"}
|
||||||
|
nu-cli = {path = "../nu-cli", version = "0.20.0"}
|
||||||
|
nu-value-ext = {path = "../nu-value-ext", version = "0.20.0"}
|
||||||
|
|
||||||
|
crossterm = "0.18"
|
||||||
|
tui = {version = "0.12.0", default-features = false, features = ["crossterm"]}
|
155
crates/nu_plugin_chart/src/chart.rs
Normal file
155
crates/nu_plugin_chart/src/chart.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
use nu_errors::ShellError;
|
||||||
|
use nu_protocol::Value;
|
||||||
|
use nu_source::Tagged;
|
||||||
|
|
||||||
|
use tui::{
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
widgets::{BarChart as TuiBarChart, Block, Borders},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum Columns {
|
||||||
|
One(Tagged<String>),
|
||||||
|
Two(Tagged<String>, Tagged<String>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub struct Chart {
|
||||||
|
pub reduction: nu_data::utils::Reduction,
|
||||||
|
pub columns: Columns,
|
||||||
|
pub eval: Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
|
||||||
|
pub format: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Chart {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Chart {
|
||||||
|
pub fn new() -> Chart {
|
||||||
|
Chart {
|
||||||
|
reduction: nu_data::utils::Reduction::Count,
|
||||||
|
columns: Columns::None,
|
||||||
|
eval: None,
|
||||||
|
format: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BarChart<'a> {
|
||||||
|
pub title: &'a str,
|
||||||
|
pub data: Vec<(&'a str, u64)>,
|
||||||
|
pub enhanced_graphics: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BarChart<'a> {
|
||||||
|
pub fn from_model(model: &'a nu_data::utils::Model) -> Result<BarChart<'a>, ShellError> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
let mut data_points = Vec::new();
|
||||||
|
|
||||||
|
for percentages in model
|
||||||
|
.percentages
|
||||||
|
.table_entries()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
{
|
||||||
|
let mut percentages_collected = vec![];
|
||||||
|
|
||||||
|
for percentage in percentages
|
||||||
|
.table_entries()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
{
|
||||||
|
percentages_collected.push(percentage.as_u64()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
data_points.push(percentages_collected);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mark_in = if model.labels.y.len() <= 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(model.labels.y.len() as f64 / 2.0).floor() as usize
|
||||||
|
};
|
||||||
|
|
||||||
|
for idx in 0..model.labels.x.len() {
|
||||||
|
let mut current = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let label = if current == mark_in {
|
||||||
|
model
|
||||||
|
.labels
|
||||||
|
.at(idx)
|
||||||
|
.ok_or_else(|| ShellError::untagged_runtime_error("Could not load data"))?
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
let percentages_collected = data_points
|
||||||
|
.get(current)
|
||||||
|
.ok_or_else(|| ShellError::untagged_runtime_error("Could not load data"))?;
|
||||||
|
|
||||||
|
data.push((
|
||||||
|
label,
|
||||||
|
*percentages_collected
|
||||||
|
.get(idx)
|
||||||
|
.ok_or_else(|| ShellError::untagged_runtime_error("Could not load data"))?,
|
||||||
|
));
|
||||||
|
|
||||||
|
current += 1;
|
||||||
|
|
||||||
|
if current == model.labels.y.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(BarChart {
|
||||||
|
title: "Bar Chart",
|
||||||
|
data: (&data[..]).to_vec(),
|
||||||
|
enhanced_graphics: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw<T>(&mut self, ui: &mut tui::Terminal<T>) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
T: tui::backend::Backend,
|
||||||
|
{
|
||||||
|
ui.draw(|f| {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(2)
|
||||||
|
.constraints([Constraint::Percentage(100)].as_ref())
|
||||||
|
.split(f.size());
|
||||||
|
|
||||||
|
let barchart = TuiBarChart::default()
|
||||||
|
.block(Block::default().title("Chart").borders(Borders::ALL))
|
||||||
|
.data(&self.data)
|
||||||
|
.bar_width(9)
|
||||||
|
.bar_style(Style::default().fg(Color::Green))
|
||||||
|
.value_style(
|
||||||
|
Style::default()
|
||||||
|
.bg(Color::Green)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
);
|
||||||
|
|
||||||
|
f.render_widget(barchart, chunks[0]);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_right(&mut self) {
|
||||||
|
let one_bar = self.data.remove(0);
|
||||||
|
self.data.push(one_bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_left(&mut self) {
|
||||||
|
if let Some(one_bar) = self.data.pop() {
|
||||||
|
self.data.insert(0, one_bar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
crates/nu_plugin_chart/src/lib.rs
Normal file
4
crates/nu_plugin_chart/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mod chart;
|
||||||
|
mod nu;
|
||||||
|
|
||||||
|
pub use chart::Chart;
|
6
crates/nu_plugin_chart/src/main.rs
Normal file
6
crates/nu_plugin_chart/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use nu_plugin::serve_plugin;
|
||||||
|
use nu_plugin_chart::Chart;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
serve_plugin(&mut Chart::new());
|
||||||
|
}
|
330
crates/nu_plugin_chart/src/nu/mod.rs
Normal file
330
crates/nu_plugin_chart/src/nu/mod.rs
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
use nu_errors::ShellError;
|
||||||
|
use nu_plugin::Plugin;
|
||||||
|
use nu_protocol::{CallInfo, ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value};
|
||||||
|
use nu_source::TaggedItem;
|
||||||
|
use nu_value_ext::ValueExt;
|
||||||
|
|
||||||
|
use crate::chart::{BarChart, Chart, Columns};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
io::{stdout, Write},
|
||||||
|
sync::mpsc,
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tui::{backend::CrosstermBackend, Terminal};
|
||||||
|
|
||||||
|
enum Event<I> {
|
||||||
|
Input(I),
|
||||||
|
Tick,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display(model: &nu_data::utils::Model) -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut app = BarChart::from_model(&model)?;
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
let mut stdout = stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
let tick_rate = Duration::from_millis(250);
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut last_tick = Instant::now();
|
||||||
|
loop {
|
||||||
|
if event::poll(tick_rate - last_tick.elapsed()).is_ok() {
|
||||||
|
if let Ok(CEvent::Key(key)) = event::read() {
|
||||||
|
let _ = tx.send(Event::Input(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last_tick.elapsed() >= tick_rate {
|
||||||
|
let _ = tx.send(Event::Tick);
|
||||||
|
last_tick = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal.clear()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
app.draw(&mut terminal)?;
|
||||||
|
|
||||||
|
match rx.recv()? {
|
||||||
|
Event::Input(event) => match event.code {
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(
|
||||||
|
terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
KeyCode::Left => app.on_left(),
|
||||||
|
KeyCode::Right => app.on_right(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Event::Tick => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for Chart {
|
||||||
|
fn config(&mut self) -> Result<Signature, ShellError> {
|
||||||
|
Ok(Signature::build("chart")
|
||||||
|
.desc("Displays bar charts")
|
||||||
|
.switch("acc", "accumuate values", Some('a'))
|
||||||
|
.optional(
|
||||||
|
"columns",
|
||||||
|
SyntaxShape::Any,
|
||||||
|
"the columns to chart [x-axis y-axis]",
|
||||||
|
)
|
||||||
|
.named(
|
||||||
|
"use",
|
||||||
|
SyntaxShape::ColumnPath,
|
||||||
|
"column to use for evaluation",
|
||||||
|
Some('u'),
|
||||||
|
)
|
||||||
|
.named(
|
||||||
|
"format",
|
||||||
|
SyntaxShape::String,
|
||||||
|
"Specify date and time formatting",
|
||||||
|
Some('f'),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sink(&mut self, call_info: CallInfo, input: Vec<Value>) {
|
||||||
|
if let Some(Value {
|
||||||
|
value: UntaggedValue::Primitive(Primitive::Boolean(true)),
|
||||||
|
..
|
||||||
|
}) = call_info.args.get("acc")
|
||||||
|
{
|
||||||
|
self.reduction = nu_data::utils::Reduction::Accumulate;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.run(call_info, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Chart {
|
||||||
|
fn run(&mut self, call_info: CallInfo, input: Vec<Value>) -> Result<(), ShellError> {
|
||||||
|
let args = call_info.args;
|
||||||
|
let name = call_info.name_tag;
|
||||||
|
|
||||||
|
self.eval = if let Some(path) = args.get("use") {
|
||||||
|
Some(evaluator(path.as_column_path()?.item))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
self.format = if let Some(fmt) = args.get("format") {
|
||||||
|
Some(fmt.as_string()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
for arg in args.positional_iter() {
|
||||||
|
match arg {
|
||||||
|
Value {
|
||||||
|
value: UntaggedValue::Primitive(Primitive::String(column)),
|
||||||
|
tag,
|
||||||
|
} => {
|
||||||
|
let column = column.clone();
|
||||||
|
self.columns = Columns::One(column.tagged(tag));
|
||||||
|
}
|
||||||
|
Value {
|
||||||
|
value: UntaggedValue::Table(arguments),
|
||||||
|
tag,
|
||||||
|
} => {
|
||||||
|
if arguments.len() > 1 {
|
||||||
|
let col1 = arguments
|
||||||
|
.get(0)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ShellError::labeled_error(
|
||||||
|
"expected file and replace strings eg) [find replace]",
|
||||||
|
"missing find-replace values",
|
||||||
|
tag,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.as_string()?
|
||||||
|
.tagged(tag);
|
||||||
|
|
||||||
|
let col2 = arguments
|
||||||
|
.get(1)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ShellError::labeled_error(
|
||||||
|
"expected file and replace strings eg) [find replace]",
|
||||||
|
"missing find-replace values",
|
||||||
|
tag,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.as_string()?
|
||||||
|
.tagged(tag);
|
||||||
|
|
||||||
|
self.columns = Columns::Two(col1, col2);
|
||||||
|
} else {
|
||||||
|
let col1 = arguments
|
||||||
|
.get(0)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ShellError::labeled_error(
|
||||||
|
"expected file and replace strings eg) [find replace]",
|
||||||
|
"missing find-replace values",
|
||||||
|
tag,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.as_string()?
|
||||||
|
.tagged(tag);
|
||||||
|
|
||||||
|
self.columns = Columns::One(col1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = UntaggedValue::table(&input).into_value(&name);
|
||||||
|
|
||||||
|
match &self.columns {
|
||||||
|
Columns::Two(col1, col2) => {
|
||||||
|
let key = col1.clone();
|
||||||
|
let fmt = self.format.clone();
|
||||||
|
|
||||||
|
let grouper = Box::new(move |_: usize, row: &Value| {
|
||||||
|
let key = key.clone();
|
||||||
|
let fmt = fmt.clone();
|
||||||
|
|
||||||
|
match row.get_data_by_key(key.borrow_spanned()) {
|
||||||
|
Some(key) => {
|
||||||
|
if let Some(fmt) = fmt {
|
||||||
|
let callback = nu_data::utils::helpers::date_formatter(fmt);
|
||||||
|
callback(&key, "nothing".to_string())
|
||||||
|
} else {
|
||||||
|
nu_value_ext::as_string(&key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(ShellError::labeled_error(
|
||||||
|
"unknown column",
|
||||||
|
"unknown column",
|
||||||
|
key.tag(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let key = col2.clone();
|
||||||
|
let splitter = Box::new(move |_: usize, row: &Value| {
|
||||||
|
let key = key.clone();
|
||||||
|
|
||||||
|
match row.get_data_by_key(key.borrow_spanned()) {
|
||||||
|
Some(key) => nu_value_ext::as_string(&key),
|
||||||
|
None => Err(ShellError::labeled_error(
|
||||||
|
"unknown column",
|
||||||
|
"unknown column",
|
||||||
|
key.tag(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let formatter = if self.format.is_some() {
|
||||||
|
let default = String::from("%b-%Y");
|
||||||
|
|
||||||
|
let string_fmt = self.format.as_ref().unwrap_or_else(|| &default);
|
||||||
|
|
||||||
|
Some(nu_data::utils::helpers::date_formatter(
|
||||||
|
string_fmt.to_string(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = nu_data::utils::Operation {
|
||||||
|
grouper: Some(grouper),
|
||||||
|
splitter: Some(splitter),
|
||||||
|
format: &formatter,
|
||||||
|
eval: &self.eval,
|
||||||
|
reduction: &self.reduction,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = display(&nu_data::utils::report(&data, options, &name)?);
|
||||||
|
}
|
||||||
|
Columns::One(col) => {
|
||||||
|
let key = col.clone();
|
||||||
|
let fmt = self.format.clone();
|
||||||
|
|
||||||
|
let grouper = Box::new(move |_: usize, row: &Value| {
|
||||||
|
let key = key.clone();
|
||||||
|
let fmt = fmt.clone();
|
||||||
|
|
||||||
|
match row.get_data_by_key(key.borrow_spanned()) {
|
||||||
|
Some(key) => {
|
||||||
|
if let Some(fmt) = fmt {
|
||||||
|
let callback = nu_data::utils::helpers::date_formatter(fmt);
|
||||||
|
callback(&key, "nothing".to_string())
|
||||||
|
} else {
|
||||||
|
nu_value_ext::as_string(&key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(ShellError::labeled_error(
|
||||||
|
"unknown column",
|
||||||
|
"unknown column",
|
||||||
|
key.tag(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let formatter = if self.format.is_some() {
|
||||||
|
let default = String::from("%b-%Y");
|
||||||
|
|
||||||
|
let string_fmt = self.format.as_ref().unwrap_or_else(|| &default);
|
||||||
|
|
||||||
|
Some(nu_data::utils::helpers::date_formatter(
|
||||||
|
string_fmt.to_string(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = nu_data::utils::Operation {
|
||||||
|
grouper: Some(grouper),
|
||||||
|
splitter: None,
|
||||||
|
format: &formatter,
|
||||||
|
eval: &self.eval,
|
||||||
|
reduction: &self.reduction,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = display(&nu_data::utils::report(&data, options, &name)?);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluator(by: ColumnPath) -> Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send> {
|
||||||
|
Box::new(move |_: usize, value: &Value| {
|
||||||
|
let path = by.clone();
|
||||||
|
|
||||||
|
let eval = nu_value_ext::get_data_by_column_path(value, &path, move |_, _, error| error);
|
||||||
|
|
||||||
|
match eval {
|
||||||
|
Ok(with_value) => Ok(with_value),
|
||||||
|
Err(reason) => Err(reason),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -35,7 +35,7 @@ If we now want to see how often the different numbers were generated, we can use
|
|||||||
```shell
|
```shell
|
||||||
> open random_numbers.csv | histogram "random numbers"
|
> open random_numbers.csv | histogram "random numbers"
|
||||||
───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
|
───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
# │ random numbers │ occurrences │ percentage │ frequency
|
# │ random numbers │ count │ percentage │ frequency
|
||||||
───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
|
───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
0 │ 0 │ 8 │ 57.14% │ *********************************************************
|
0 │ 0 │ 8 │ 57.14% │ *********************************************************
|
||||||
1 │ 1 │ 14 │ 100.00% │ ****************************************************************************************************
|
1 │ 1 │ 14 │ 100.00% │ ****************************************************************************************************
|
||||||
@ -51,7 +51,7 @@ We can also set the name of the second column or sort the table:
|
|||||||
```shell
|
```shell
|
||||||
> open random_numbers.csv | histogram "random numbers" probability
|
> open random_numbers.csv | histogram "random numbers" probability
|
||||||
───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
|
───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
# │ random numbers │ occurrences │ percentage │ probability
|
# │ random numbers │ count │ percentage │ probability
|
||||||
───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
|
───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
0 │ 0 │ 8 │ 57.14% │ *********************************************************
|
0 │ 0 │ 8 │ 57.14% │ *********************************************************
|
||||||
1 │ 1 │ 14 │ 100.00% │ ****************************************************************************************************
|
1 │ 1 │ 14 │ 100.00% │ ****************************************************************************************************
|
||||||
@ -66,7 +66,7 @@ We can also set the name of the second column or sort the table:
|
|||||||
```shell
|
```shell
|
||||||
> open random_numbers.csv | histogram "random numbers" probability | sort-by probability
|
> open random_numbers.csv | histogram "random numbers" probability | sort-by probability
|
||||||
───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
|
───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
# │ random numbers │ occurrences │ percentage │ probability
|
# │ random numbers │ count │ percentage │ probability
|
||||||
───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
|
───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
0 │ 4 │ 3 │ 21.43% │ *********************
|
0 │ 4 │ 3 │ 21.43% │ *********************
|
||||||
1 │ 3 │ 6 │ 42.86% │ ******************************************
|
1 │ 3 │ 6 │ 42.86% │ ******************************************
|
||||||
@ -81,9 +81,9 @@ We can also set the name of the second column or sort the table:
|
|||||||
Of course, histogram operations are not restricted to just analyzing numbers in files, you can also analyze your directories
|
Of course, histogram operations are not restricted to just analyzing numbers in files, you can also analyze your directories
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
> ls -la | histogram type | sort-by occurrences
|
> ls -la | histogram type | sort-by count
|
||||||
───┬─────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
|
───┬─────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
# │ type │ occurrences │ percentage │ frequency
|
# │ type │ count │ percentage │ frequency
|
||||||
───┼─────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
|
───┼─────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
0 │ Dir │ 5 │ 4.76% │ ****
|
0 │ Dir │ 5 │ 4.76% │ ****
|
||||||
1 │ Symlink │ 28 │ 26.67% │ **************************
|
1 │ Symlink │ 28 │ 26.67% │ **************************
|
||||||
|
6
src/plugins/nu_plugin_extra_chart.rs
Normal file
6
src/plugins/nu_plugin_extra_chart.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use nu_plugin::serve_plugin;
|
||||||
|
use nu_plugin_chart::Chart;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
serve_plugin(&mut Chart::new());
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user