diff --git a/Cargo.lock b/Cargo.lock index 65c89cd574..a5e164b7d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2073,7 +2073,6 @@ dependencies = [ "num-bigint", "num-traits 0.2.10", "pretty", - "pretty_assertions", "pretty_env_logger", "ptree", "serde 1.0.103", diff --git a/Cargo.toml b/Cargo.toml index f7c2a6fe5f..9eadc00871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,29 +11,29 @@ repository = "https://github.com/nushell/nushell" homepage = "https://www.nushell.sh" documentation = "https://www.nushell.sh/book/" -[workspace] - -members = [ - "crates/nu-macros", - "crates/nu-errors", - "crates/nu-source", - "crates/nu_plugin_average", - "crates/nu_plugin_binaryview", - "crates/nu_plugin_fetch", - "crates/nu_plugin_inc", - "crates/nu_plugin_match", - "crates/nu_plugin_post", - "crates/nu_plugin_ps", - "crates/nu_plugin_str", - "crates/nu_plugin_sum", - "crates/nu_plugin_sys", - "crates/nu_plugin_textview", - "crates/nu_plugin_tree", - "crates/nu-protocol", - "crates/nu-parser", - "crates/nu-value-ext", - "crates/nu-build" -] +# [workspace] +# +# members = [ +# "crates/nu-macros", +# "crates/nu-errors", +# "crates/nu-source", +# "crates/nu_plugin_average", +# "crates/nu_plugin_binaryview", +# "crates/nu_plugin_fetch", +# "crates/nu_plugin_inc", +# "crates/nu_plugin_match", +# "crates/nu_plugin_post", +# "crates/nu_plugin_ps", +# "crates/nu_plugin_str", +# "crates/nu_plugin_sum", +# "crates/nu_plugin_sys", +# "crates/nu_plugin_textview", +# "crates/nu_plugin_tree", +# "crates/nu-protocol", +# "crates/nu-parser", +# "crates/nu-value-ext", +# "crates/nu-build" +# ] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -174,27 +174,27 @@ path = "src/lib.rs" # unless we use [[bin]], so we use this as a workaround [[bin]] name = "nu_plugin_core_textview" -path = "crates/nu_plugin_textview/src/main.rs" +path = "src/plugins/nu_plugin_core_textview.rs" required-features = ["textview"] [[bin]] name = "nu_plugin_core_inc" -path = "crates/nu_plugin_inc/src/main.rs" +path = "src/plugins/nu_plugin_core_inc.rs" required-features = ["inc"] [[bin]] name = "nu_plugin_core_ps" -path = "crates/nu_plugin_ps/src/main.rs" +path = "src/plugins/nu_plugin_core_ps.rs" required-features = ["ps"] [[bin]] name = "nu_plugin_core_str" -path = "crates/nu_plugin_str/src/main.rs" +path = "src/plugins/nu_plugin_core_str.rs" required-features = ["str"] [[bin]] name = "nu_plugin_core_sys" -path = "crates/nu_plugin_sys/src/main.rs" +path = "src/plugins/nu_plugin_core_sys.rs" required-features = ["sys"] # Main nu binary diff --git a/src/plugins/nu_plugin_core_inc.rs b/src/plugins/nu_plugin_core_inc.rs new file mode 100644 index 0000000000..3fd6ca89e2 --- /dev/null +++ b/src/plugins/nu_plugin_core_inc.rs @@ -0,0 +1,456 @@ +use nu_errors::ShellError; +use nu_protocol::{ + did_you_mean, serve_plugin, CallInfo, ColumnPath, Plugin, Primitive, ReturnSuccess, + ReturnValue, ShellTypeName, Signature, SyntaxShape, UntaggedValue, Value, +}; +use nu_source::{span_for_spanned_list, HasSpan, SpannedItem, Tagged}; +use nu_value_ext::ValueExt; + +enum Action { + SemVerAction(SemVerAction), + Default, +} + +pub enum SemVerAction { + Major, + Minor, + Patch, +} + +struct Inc { + field: Option>, + error: Option, + action: Option, +} + +impl Inc { + fn new() -> Inc { + Inc { + field: None, + error: None, + action: None, + } + } + + fn apply(&self, input: &str) -> Result { + let applied = match &self.action { + Some(Action::SemVerAction(act_on)) => { + let mut ver = match semver::Version::parse(&input) { + Ok(parsed_ver) => parsed_ver, + Err(_) => return Ok(UntaggedValue::string(input.to_string())), + }; + + match act_on { + SemVerAction::Major => ver.increment_major(), + SemVerAction::Minor => ver.increment_minor(), + SemVerAction::Patch => ver.increment_patch(), + } + + UntaggedValue::string(ver.to_string()) + } + Some(Action::Default) | None => match input.parse::() { + Ok(v) => UntaggedValue::string(format!("{}", v + 1)), + Err(_) => UntaggedValue::string(input), + }, + }; + + Ok(applied) + } + + fn for_semver(&mut self, part: SemVerAction) { + if self.permit() { + self.action = Some(Action::SemVerAction(part)); + } else { + self.log_error("can only apply one"); + } + } + + fn permit(&mut self) -> bool { + self.action.is_none() + } + + fn log_error(&mut self, message: &str) { + self.error = Some(message.to_string()); + } + + pub fn usage() -> &'static str { + "Usage: inc field [--major|--minor|--patch]" + } + + fn inc(&self, value: Value) -> Result { + match &value.value { + UntaggedValue::Primitive(Primitive::Int(i)) => { + Ok(UntaggedValue::int(i + 1).into_value(value.tag())) + } + UntaggedValue::Primitive(Primitive::Bytes(b)) => { + Ok(UntaggedValue::bytes(b + 1 as u64).into_value(value.tag())) + } + UntaggedValue::Primitive(Primitive::String(ref s)) => { + Ok(self.apply(&s)?.into_value(value.tag())) + } + UntaggedValue::Table(values) => { + if values.len() == 1 { + Ok(UntaggedValue::Table(vec![self.inc(values[0].clone())?]) + .into_value(value.tag())) + } else { + Err(ShellError::type_error( + "incrementable value", + value.type_name().spanned(value.span()), + )) + } + } + + UntaggedValue::Row(_) => match self.field { + Some(ref f) => { + let fields = f.clone(); + + let replace_for = value.get_data_by_column_path( + &f, + Box::new(move |(obj_source, column_path_tried, _)| { + match did_you_mean(&obj_source, &column_path_tried) { + Some(suggestions) => ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", suggestions[0].1), + span_for_spanned_list(fields.iter().map(|p| p.span)), + ), + None => ShellError::labeled_error( + "Unknown column", + "row does not contain this column", + span_for_spanned_list(fields.iter().map(|p| p.span)), + ), + } + }), + ); + + let got = replace_for?; + let replacement = self.inc(got.clone())?; + + match value.replace_data_at_column_path( + &f, + replacement.value.clone().into_untagged_value(), + ) { + Some(v) => Ok(v), + None => Err(ShellError::labeled_error( + "inc could not find field to replace", + "column name", + value.tag(), + )), + } + } + None => Err(ShellError::untagged_runtime_error( + "inc needs a field when incrementing a column in a table", + )), + }, + _ => Err(ShellError::type_error( + "incrementable value", + value.type_name().spanned(value.span()), + )), + } + } +} + +impl Plugin for Inc { + fn config(&mut self) -> Result { + Ok(Signature::build("inc") + .desc("Increment a value or version. Optionally use the column of a table.") + .switch("major", "increment the major version (eg 1.2.1 -> 2.0.0)") + .switch("minor", "increment the minor version (eg 1.2.1 -> 1.3.0)") + .switch("patch", "increment the patch version (eg 1.2.1 -> 1.2.2)") + .rest(SyntaxShape::ColumnPath, "the column(s) to update") + .filter()) + } + + fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { + if call_info.args.has("major") { + self.for_semver(SemVerAction::Major); + } + if call_info.args.has("minor") { + self.for_semver(SemVerAction::Minor); + } + if call_info.args.has("patch") { + self.for_semver(SemVerAction::Patch); + } + + if let Some(args) = call_info.args.positional { + for arg in args { + match arg { + table @ Value { + value: UntaggedValue::Primitive(Primitive::ColumnPath(_)), + .. + } => { + self.field = Some(table.as_column_path()?); + } + value => { + return Err(ShellError::type_error( + "table", + value.type_name().spanned(value.span()), + )) + } + } + } + } + + if self.action.is_none() { + self.action = Some(Action::Default); + } + + match &self.error { + Some(reason) => Err(ShellError::untagged_runtime_error(format!( + "{}: {}", + reason, + Inc::usage() + ))), + None => Ok(vec![]), + } + } + + fn filter(&mut self, input: Value) -> Result, ShellError> { + Ok(vec![ReturnSuccess::value(self.inc(input)?)]) + } +} + +fn main() { + serve_plugin(&mut Inc::new()); +} + +#[cfg(test)] +mod tests { + + use super::{Inc, SemVerAction}; + use indexmap::IndexMap; + use nu_protocol::{ + CallInfo, EvaluatedArgs, PathMember, ReturnSuccess, UnspannedPathMember, UntaggedValue, + Value, + }; + use nu_protocol::{Plugin, TaggedDictBuilder}; + use nu_source::{Span, Tag}; + + struct CallStub { + positionals: Vec, + flags: IndexMap, + } + + impl CallStub { + fn new() -> CallStub { + CallStub { + positionals: vec![], + flags: indexmap::IndexMap::new(), + } + } + + fn with_long_flag(&mut self, name: &str) -> &mut Self { + self.flags.insert( + name.to_string(), + UntaggedValue::boolean(true).into_value(Tag::unknown()), + ); + self + } + + fn with_parameter(&mut self, name: &str) -> &mut Self { + let fields: Vec = name + .split(".") + .map(|s| { + UnspannedPathMember::String(s.to_string()).into_path_member(Span::unknown()) + }) + .collect(); + + self.positionals + .push(UntaggedValue::column_path(fields).into_untagged_value()); + self + } + + fn create(&self) -> CallInfo { + CallInfo { + args: EvaluatedArgs::new(Some(self.positionals.clone()), Some(self.flags.clone())), + name_tag: Tag::unknown(), + } + } + } + + fn cargo_sample_record(with_version: &str) -> Value { + let mut package = TaggedDictBuilder::new(Tag::unknown()); + package.insert_untagged("version", UntaggedValue::string(with_version)); + package.into_value() + } + + #[test] + fn inc_plugin_configuration_flags_wired() { + let mut plugin = Inc::new(); + + let configured = plugin.config().expect("Can not configure plugin"); + + for action_flag in &["major", "minor", "patch"] { + assert!(configured.named.get(*action_flag).is_some()); + } + } + + #[test] + fn inc_plugin_accepts_major() { + let mut plugin = Inc::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("major").create()) + .is_ok()); + assert!(plugin.action.is_some()); + } + + #[test] + fn inc_plugin_accepts_minor() { + let mut plugin = Inc::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("minor").create()) + .is_ok()); + assert!(plugin.action.is_some()); + } + + #[test] + fn inc_plugin_accepts_patch() { + let mut plugin = Inc::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("patch").create()) + .is_ok()); + assert!(plugin.action.is_some()); + } + + #[test] + fn inc_plugin_accepts_only_one_action() { + let mut plugin = Inc::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("major") + .with_long_flag("minor") + .create(), + ) + .is_err()); + assert_eq!(plugin.error, Some("can only apply one".to_string())); + } + + #[test] + fn inc_plugin_accepts_field() { + let mut plugin = Inc::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_parameter("package.version").create()) + .is_ok()); + + assert_eq!( + plugin + .field + .map(|f| f.iter().map(|f| f.unspanned.clone()).collect()), + Some(vec![ + UnspannedPathMember::String("package".to_string()), + UnspannedPathMember::String("version".to_string()) + ]) + ); + } + + #[test] + fn incs_major() { + let mut inc = Inc::new(); + inc.for_semver(SemVerAction::Major); + assert_eq!(inc.apply("0.1.3").unwrap(), UntaggedValue::string("1.0.0")); + } + + #[test] + fn incs_minor() { + let mut inc = Inc::new(); + inc.for_semver(SemVerAction::Minor); + assert_eq!(inc.apply("0.1.3").unwrap(), UntaggedValue::string("0.2.0")); + } + + #[test] + fn incs_patch() { + let mut inc = Inc::new(); + inc.for_semver(SemVerAction::Patch); + assert_eq!(inc.apply("0.1.3").unwrap(), UntaggedValue::string("0.1.4")); + } + + #[test] + fn inc_plugin_applies_major() { + let mut plugin = Inc::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("major") + .with_parameter("version") + .create() + ) + .is_ok()); + + let subject = cargo_sample_record("0.1.3"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("version")).borrow(), + UntaggedValue::string(String::from("1.0.0")).into_untagged_value() + ), + _ => {} + } + } + + #[test] + fn inc_plugin_applies_minor() { + let mut plugin = Inc::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("minor") + .with_parameter("version") + .create() + ) + .is_ok()); + + let subject = cargo_sample_record("0.1.3"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("version")).borrow(), + UntaggedValue::string(String::from("0.2.0")).into_untagged_value() + ), + _ => {} + } + } + + #[test] + fn inc_plugin_applies_patch() { + let field = String::from("version"); + let mut plugin = Inc::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("patch") + .with_parameter(&field) + .create() + ) + .is_ok()); + + let subject = cargo_sample_record("0.1.3"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&field).borrow(), + UntaggedValue::string(String::from("0.1.4")).into_untagged_value() + ), + _ => {} + } + } +} diff --git a/src/plugins/nu_plugin_core_ps.rs b/src/plugins/nu_plugin_core_ps.rs new file mode 100644 index 0000000000..9cde0460c5 --- /dev/null +++ b/src/plugins/nu_plugin_core_ps.rs @@ -0,0 +1,84 @@ +use futures::executor::block_on; +//use futures::stream::TryStreamExt; + +use futures_util::{StreamExt, TryStreamExt}; +use heim::process::{self as process, Process, ProcessResult}; +use heim::units::{ratio, Ratio}; +use std::usize; + +use nu_errors::ShellError; +use nu_protocol::{ + serve_plugin, CallInfo, Plugin, ReturnSuccess, ReturnValue, Signature, TaggedDictBuilder, + UntaggedValue, Value, +}; +use nu_source::Tag; + +use std::time::Duration; + +struct Ps; +impl Ps { + fn new() -> Ps { + Ps + } +} + +async fn usage(process: Process) -> ProcessResult<(process::Process, Ratio)> { + let usage_1 = process.cpu_usage().await?; + futures_timer::Delay::new(Duration::from_millis(100)).await; + let usage_2 = process.cpu_usage().await?; + + Ok((process, usage_2 - usage_1)) +} + +async fn ps(tag: Tag) -> Vec { + let processes = process::processes() + .map_ok(|process| { + // Note that there is no `.await` here, + // as we want to pass the returned future + // into the `.try_buffer_unordered`. + usage(process) + }) + .try_buffer_unordered(usize::MAX); + pin_utils::pin_mut!(processes); + + let mut output = vec![]; + while let Some(res) = processes.next().await { + if let Ok((process, usage)) = res { + let mut dict = TaggedDictBuilder::new(&tag); + dict.insert_untagged("pid", UntaggedValue::int(process.pid())); + if let Ok(name) = process.name().await { + dict.insert_untagged("name", UntaggedValue::string(name)); + } + if let Ok(status) = process.status().await { + dict.insert_untagged("status", UntaggedValue::string(format!("{:?}", status))); + } + dict.insert_untagged("cpu", UntaggedValue::decimal(usage.get::())); + output.push(dict.into_value()); + } + } + + output +} + +impl Plugin for Ps { + fn config(&mut self) -> Result { + Ok(Signature::build("ps") + .desc("View information about system processes.") + .filter()) + } + + fn begin_filter(&mut self, callinfo: CallInfo) -> Result, ShellError> { + Ok(block_on(ps(callinfo.name_tag)) + .into_iter() + .map(ReturnSuccess::value) + .collect()) + } + + fn filter(&mut self, _: Value) -> Result, ShellError> { + Ok(vec![]) + } +} + +fn main() { + serve_plugin(&mut Ps::new()); +} diff --git a/src/plugins/nu_plugin_core_str.rs b/src/plugins/nu_plugin_core_str.rs new file mode 100644 index 0000000000..b7aaf8ea08 --- /dev/null +++ b/src/plugins/nu_plugin_core_str.rs @@ -0,0 +1,980 @@ +use nu_errors::ShellError; +use nu_protocol::{ + did_you_mean, serve_plugin, CallInfo, ColumnPath, Plugin, Primitive, ReturnSuccess, + ReturnValue, ShellTypeName, Signature, SyntaxShape, UntaggedValue, Value, +}; +use nu_source::{span_for_spanned_list, Tagged}; +use nu_value_ext::ValueExt; + +use regex::Regex; +use std::cmp; + +#[derive(Debug, Eq, PartialEq)] +enum Action { + Downcase, + Upcase, + ToInteger, + Substring(usize, usize), + Replace(ReplaceAction), +} + +#[derive(Debug, Eq, PartialEq)] +enum ReplaceAction { + Direct(String), + FindAndReplace(String, String), +} + +struct Str { + field: Option>, + error: Option, + action: Option, +} + +impl Str { + fn new() -> Str { + Str { + field: None, + error: None, + action: None, + } + } + + fn apply(&self, input: &str) -> Result { + let applied = match self.action.as_ref() { + Some(Action::Downcase) => UntaggedValue::string(input.to_ascii_lowercase()), + Some(Action::Upcase) => UntaggedValue::string(input.to_ascii_uppercase()), + Some(Action::Substring(s, e)) => { + let end: usize = cmp::min(*e, input.len()); + let start: usize = *s; + if start > input.len() - 1 { + UntaggedValue::string("") + } else { + UntaggedValue::string( + &input + .chars() + .skip(start) + .take(end - start) + .collect::(), + ) + } + } + Some(Action::Replace(mode)) => match mode { + ReplaceAction::Direct(replacement) => UntaggedValue::string(replacement.as_str()), + ReplaceAction::FindAndReplace(find, replacement) => { + let regex = Regex::new(find.as_str()); + + match regex { + Ok(re) => UntaggedValue::string( + re.replace(input, replacement.as_str()).to_owned(), + ), + Err(_) => UntaggedValue::string(input), + } + } + }, + Some(Action::ToInteger) => match input.trim() { + other => match other.parse::() { + Ok(v) => UntaggedValue::int(v), + Err(_) => UntaggedValue::string(input), + }, + }, + None => UntaggedValue::string(input), + }; + + Ok(applied) + } + + fn for_field(&mut self, column_path: Tagged) { + self.field = Some(column_path); + } + + fn permit(&mut self) -> bool { + self.action.is_none() + } + + fn log_error(&mut self, message: &str) { + self.error = Some(message.to_string()); + } + + fn for_to_int(&mut self) { + if self.permit() { + self.action = Some(Action::ToInteger); + } else { + self.log_error("can only apply one"); + } + } + + fn for_downcase(&mut self) { + if self.permit() { + self.action = Some(Action::Downcase); + } else { + self.log_error("can only apply one"); + } + } + + fn for_upcase(&mut self) { + if self.permit() { + self.action = Some(Action::Upcase); + } else { + self.log_error("can only apply one"); + } + } + + fn for_substring(&mut self, s: String) { + let v: Vec<&str> = s.split(',').collect(); + let start: usize = match v[0] { + "" => 0, + _ => v[0].trim().parse().unwrap(), + }; + let end: usize = match v[1] { + "" => usize::max_value(), + _ => v[1].trim().parse().unwrap(), + }; + if start > end { + self.log_error("End must be greater than or equal to Start"); + } else if self.permit() { + self.action = Some(Action::Substring(start, end)); + } else { + self.log_error("can only apply one"); + } + } + + fn for_replace(&mut self, mode: ReplaceAction) { + if self.permit() { + self.action = Some(Action::Replace(mode)); + } else { + self.log_error("can only apply one"); + } + } + + pub fn usage() -> &'static str { + "Usage: str field [--downcase|--upcase|--to-int|--substring \"start,end\"|--replace|--find-replace [pattern replacement]]]" + } +} + +impl Str { + fn strutils(&self, value: Value) -> Result { + match &value.value { + UntaggedValue::Primitive(Primitive::String(ref s)) => { + Ok(self.apply(&s)?.into_value(value.tag())) + } + UntaggedValue::Primitive(Primitive::Line(ref s)) => { + Ok(self.apply(&s)?.into_value(value.tag())) + } + UntaggedValue::Row(_) => match self.field { + Some(ref f) => { + let fields = f.clone(); + + let replace_for = + value.get_data_by_column_path( + &f, + Box::new(move |(obj_source, column_path_tried, error)| { + match did_you_mean(&obj_source, &column_path_tried) { + Some(suggestions) => ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", suggestions[0].1), + span_for_spanned_list(fields.iter().map(|p| p.span)), + ), + None => error, + } + }), + ); + + let got = replace_for?; + let replacement = self.strutils(got.clone())?; + + match value.replace_data_at_column_path( + &f, + replacement.value.clone().into_untagged_value(), + ) { + Some(v) => Ok(v), + None => Err(ShellError::labeled_error( + "str could not find field to replace", + "column name", + value.tag(), + )), + } + } + None => Err(ShellError::untagged_runtime_error(format!( + "{}: {}", + "str needs a column when applied to a value in a row", + Str::usage() + ))), + }, + _ => Err(ShellError::labeled_error( + "Unrecognized type in stream", + value.type_name(), + value.tag, + )), + } + } +} + +impl Plugin for Str { + fn config(&mut self) -> Result { + Ok(Signature::build("str") + .desc("Apply string function. Optional use the column of a table") + .switch("downcase", "convert string to lowercase") + .switch("upcase", "convert string to uppercase") + .switch("to-int", "convert string to integer") + .named("replace", SyntaxShape::String, "replaces the string") + .named( + "find-replace", + SyntaxShape::Any, + "finds and replaces [pattern replacement]", + ) + .named( + "substring", + SyntaxShape::String, + "convert string to portion of original, requires \"start,end\"", + ) + .rest(SyntaxShape::ColumnPath, "the column(s) to convert") + .filter()) + } + + fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { + let args = call_info.args; + + if args.has("downcase") { + self.for_downcase(); + } + if args.has("upcase") { + self.for_upcase(); + } + if args.has("to-int") { + self.for_to_int(); + } + if args.has("substring") { + if let Some(start_end) = args.get("substring") { + match start_end { + Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + } => { + self.for_substring(s.to_string()); + } + _ => { + return Err(ShellError::labeled_error( + "Unrecognized type in params", + start_end.type_name(), + &start_end.tag, + )) + } + } + } + } + if args.has("replace") { + if let Some(Value { + value: UntaggedValue::Primitive(Primitive::String(replacement)), + .. + }) = args.get("replace") + { + self.for_replace(ReplaceAction::Direct(replacement.clone())); + } + } + + if args.has("find-replace") { + if let Some(Value { + value: UntaggedValue::Table(arguments), + .. + }) = args.get("find-replace") + { + self.for_replace(ReplaceAction::FindAndReplace( + arguments.get(0).unwrap().as_string()?.to_string(), + arguments.get(1).unwrap().as_string()?.to_string(), + )); + } + } + + if let Some(possible_field) = args.nth(0) { + let possible_field = possible_field.as_column_path()?; + self.for_field(possible_field); + } + + match &self.error { + Some(reason) => Err(ShellError::untagged_runtime_error(format!( + "{}: {}", + reason, + Str::usage() + ))), + None => Ok(vec![]), + } + } + + fn filter(&mut self, input: Value) -> Result, ShellError> { + Ok(vec![ReturnSuccess::value(self.strutils(input)?)]) + } +} + +fn main() { + serve_plugin(&mut Str::new()); +} + +#[cfg(test)] +mod tests { + use super::{Action, ReplaceAction, Str}; + use indexmap::IndexMap; + use nu_protocol::{ + CallInfo, EvaluatedArgs, Plugin, Primitive, ReturnSuccess, TaggedDictBuilder, + UntaggedValue, Value, + }; + use nu_source::Tag; + use nu_value_ext::ValueExt; + use num_bigint::BigInt; + + fn string(input: impl Into) -> Value { + UntaggedValue::string(input.into()).into_untagged_value() + } + + fn table(list: &Vec) -> Value { + UntaggedValue::table(list).into_untagged_value() + } + + fn column_path(paths: &Vec) -> Value { + UntaggedValue::Primitive(Primitive::ColumnPath( + table(&paths.iter().cloned().collect()) + .as_column_path() + .unwrap() + .item, + )) + .into_untagged_value() + } + struct CallStub { + positionals: Vec, + flags: IndexMap, + } + + impl CallStub { + fn new() -> CallStub { + CallStub { + positionals: vec![], + flags: indexmap::IndexMap::new(), + } + } + + fn with_named_parameter(&mut self, name: &str, value: Value) -> &mut Self { + self.flags.insert(name.to_string(), value); + self + } + + fn with_long_flag(&mut self, name: &str) -> &mut Self { + self.flags.insert( + name.to_string(), + UntaggedValue::boolean(true).into_value(Tag::unknown()), + ); + self + } + + fn with_parameter(&mut self, name: &str) -> &mut Self { + let fields: Vec = name + .split(".") + .map(|s| UntaggedValue::string(s.to_string()).into_value(Tag::unknown())) + .collect(); + + self.positionals.push(column_path(&fields)); + self + } + + fn create(&self) -> CallInfo { + CallInfo { + args: EvaluatedArgs::new(Some(self.positionals.clone()), Some(self.flags.clone())), + name_tag: Tag::unknown(), + } + } + } + + fn structured_sample_record(key: &str, value: &str) -> Value { + let mut record = TaggedDictBuilder::new(Tag::unknown()); + record.insert_untagged(key.clone(), UntaggedValue::string(value)); + record.into_value() + } + + fn unstructured_sample_record(value: &str) -> Value { + UntaggedValue::string(value).into_value(Tag::unknown()) + } + + #[test] + fn str_plugin_configuration_flags_wired() { + let mut plugin = Str::new(); + + let configured = plugin.config().unwrap(); + + for action_flag in &[ + "downcase", + "upcase", + "to-int", + "substring", + "replace", + "find-replace", + ] { + assert!(configured.named.get(*action_flag).is_some()); + } + } + + #[test] + fn str_plugin_accepts_downcase() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("downcase").create()) + .is_ok()); + assert_eq!(plugin.action.unwrap(), Action::Downcase); + } + + #[test] + fn str_plugin_accepts_upcase() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("upcase").create()) + .is_ok()); + assert_eq!(plugin.action.unwrap(), Action::Upcase); + } + + #[test] + fn str_plugin_accepts_to_int() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("to-int").create()) + .is_ok()); + assert_eq!(plugin.action.unwrap(), Action::ToInteger); + } + + #[test] + fn str_plugin_accepts_replace() { + let mut plugin = Str::new(); + + let argument = String::from("replace_text"); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("replace", string(&argument)) + .create() + ) + .is_ok()); + + match plugin.action { + Some(Action::Replace(ReplaceAction::Direct(replace_with))) => { + assert_eq!(replace_with, argument) + } + Some(_) | None => panic!("Din't accept."), + } + } + + #[test] + fn str_plugin_accepts_find_replace() { + let mut plugin = Str::new(); + + let search_argument = String::from("kittens"); + let replace_argument = String::from("jotandrehuda"); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter( + "find-replace", + table(&vec![string(&search_argument), string(&replace_argument)]) + ) + .create() + ) + .is_ok()); + + match plugin.action { + Some(Action::Replace(ReplaceAction::FindAndReplace(find_with, replace_with))) => { + assert_eq!(find_with, search_argument); + assert_eq!(replace_with, replace_argument); + } + Some(_) | None => panic!("Din't accept."), + } + } + #[test] + fn str_plugin_accepts_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("package.description") + .create() + ) + .is_ok()); + + let actual = &*plugin.field.unwrap(); + let actual = UntaggedValue::Primitive(Primitive::ColumnPath(actual.clone())); + let actual = actual.into_value(Tag::unknown()); + + assert_eq!( + actual, + column_path(&vec![string("package"), string("description")]) + ) + } + + #[test] + fn str_plugin_accepts_only_one_action() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("upcase") + .with_long_flag("downcase") + .with_long_flag("to-int") + .with_long_flag("substring") + .create(), + ) + .is_err()); + assert_eq!(plugin.error, Some("can only apply one".to_string())); + } + + #[test] + fn str_downcases() { + let mut strutils = Str::new(); + strutils.for_downcase(); + assert_eq!( + strutils.apply("ANDRES").unwrap(), + UntaggedValue::string("andres") + ); + } + + #[test] + fn str_upcases() { + let mut strutils = Str::new(); + strutils.for_upcase(); + assert_eq!( + strutils.apply("andres").unwrap(), + UntaggedValue::string("ANDRES") + ); + } + + #[test] + fn str_to_int() { + let mut strutils = Str::new(); + strutils.for_to_int(); + assert_eq!( + strutils.apply("9999").unwrap(), + UntaggedValue::int(9999 as i64) + ); + } + + #[test] + fn str_replace() { + let mut strutils = Str::new(); + strutils.for_replace(ReplaceAction::Direct("robalino".to_string())); + + assert_eq!( + strutils.apply("andres").unwrap(), + UntaggedValue::string("robalino") + ); + } + + #[test] + fn str_find_replace() { + let mut strutils = Str::new(); + strutils.for_replace(ReplaceAction::FindAndReplace( + "kittens".to_string(), + "jotandrehuda".to_string(), + )); + assert_eq!( + strutils.apply("wykittens").unwrap(), + UntaggedValue::string("wyjotandrehuda") + ); + } + + #[test] + fn str_plugin_applies_upcase_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("upcase") + .with_parameter("name") + .create() + ) + .is_ok()); + + let subject = structured_sample_record("name", "jotandrehuda"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("name")).borrow(), + UntaggedValue::string(String::from("JOTANDREHUDA")).into_untagged_value() + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_upcase_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("upcase").create()) + .is_ok()); + + let subject = unstructured_sample_record("jotandrehuda"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("JOTANDREHUDA")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_downcase_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("downcase") + .with_parameter("name") + .create() + ) + .is_ok()); + + let subject = structured_sample_record("name", "JOTANDREHUDA"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("name")).borrow(), + UntaggedValue::string(String::from("jotandrehuda")).into_untagged_value() + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_downcase_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("downcase").create()) + .is_ok()); + + let subject = unstructured_sample_record("JOTANDREHUDA"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("jotandrehuda")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_to_int_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_long_flag("to-int") + .with_parameter("Nu_birthday") + .create() + ) + .is_ok()); + + let subject = structured_sample_record("Nu_birthday", "10"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("Nu_birthday")).borrow(), + UntaggedValue::int(10).into_untagged_value() + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_to_int_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter(CallStub::new().with_long_flag("to-int").create()) + .is_ok()); + + let subject = unstructured_sample_record("10"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::Int(i)), + .. + }) => assert_eq!(*i, BigInt::from(10)), + _ => {} + } + } + + #[test] + fn str_plugin_applies_substring_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("substring", string("0,1")) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("0123456789"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("0")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_substring_exceeding_string_length() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("substring", string("0,11")) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("0123456789"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("0123456789")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_substring_returns_blank_if_start_exceeds_length() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("substring", string("20,30")) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("0123456789"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_substring_treats_blank_start_as_zero() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("substring", string(",5")) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("0123456789"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("01234")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_substring_treats_blank_end_as_length() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("substring", string("2,")) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("0123456789"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("23456789")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_substring_returns_error_if_start_exceeds_end() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("substring", string("3,1")) + .create() + ) + .is_err()); + assert_eq!( + plugin.error, + Some("End must be greater than or equal to Start".to_string()) + ); + } + + #[test] + fn str_plugin_applies_replace_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("rustconf") + .with_named_parameter("replace", string("22nd August 2019")) + .create() + ) + .is_ok()); + + let subject = structured_sample_record("rustconf", "1st January 1970"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("rustconf")).borrow(), + Value { + value: UntaggedValue::string(String::from("22nd August 2019")), + tag: Tag::unknown() + } + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_replace_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("replace", string("22nd August 2019")) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("1st January 1970"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("22nd August 2019")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_find_replace_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("staff") + .with_named_parameter( + "find-replace", + table(&vec![string("kittens"), string("jotandrehuda")]) + ) + .create() + ) + .is_ok()); + + let subject = structured_sample_record("staff", "wykittens"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("staff")).borrow(), + Value { + value: UntaggedValue::string(String::from("wyjotandrehuda")), + tag: Tag::unknown() + } + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_find_replace_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter( + "find-replace", + table(&vec![string("kittens"), string("jotandrehuda")]) + ) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("wykittens"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("wyjotandrehuda")), + _ => {} + } + } +} diff --git a/src/plugins/nu_plugin_core_sys.rs b/src/plugins/nu_plugin_core_sys.rs new file mode 100644 index 0000000000..c44d98e45c --- /dev/null +++ b/src/plugins/nu_plugin_core_sys.rs @@ -0,0 +1,355 @@ +use std::ffi::OsStr; + +use futures::executor::block_on; +//use futures::stream::StreamExt; +use futures_util::StreamExt; +use heim::units::{frequency, information, thermodynamic_temperature, time}; +use heim::{disk, host, memory, net, sensors}; +use nu_errors::ShellError; +use nu_protocol::{ + serve_plugin, CallInfo, Plugin, ReturnSuccess, ReturnValue, Signature, TaggedDictBuilder, + UntaggedValue, Value, +}; +use nu_source::Tag; + +struct Sys; +impl Sys { + fn new() -> Sys { + Sys + } +} + +async fn cpu(tag: Tag) -> Option { + match futures::future::try_join(heim::cpu::logical_count(), heim::cpu::frequency()).await { + Ok((num_cpu, cpu_speed)) => { + let mut cpu_idx = TaggedDictBuilder::with_capacity(tag, 4); + cpu_idx.insert_untagged("cores", UntaggedValue::int(num_cpu)); + + let current_speed = + (cpu_speed.current().get::() as f64 / 1_000_000_000.0 * 100.0) + .round() + / 100.0; + cpu_idx.insert_untagged("current ghz", UntaggedValue::decimal(current_speed)); + + if let Some(min_speed) = cpu_speed.min() { + let min_speed = + (min_speed.get::() as f64 / 1_000_000_000.0 * 100.0).round() + / 100.0; + cpu_idx.insert_untagged("min ghz", UntaggedValue::decimal(min_speed)); + } + + if let Some(max_speed) = cpu_speed.max() { + let max_speed = + (max_speed.get::() as f64 / 1_000_000_000.0 * 100.0).round() + / 100.0; + cpu_idx.insert_untagged("max ghz", UntaggedValue::decimal(max_speed)); + } + + Some(cpu_idx.into_value()) + } + Err(_) => None, + } +} + +async fn mem(tag: Tag) -> Value { + let mut dict = TaggedDictBuilder::with_capacity(tag, 4); + + let (memory_result, swap_result) = + futures::future::join(memory::memory(), memory::swap()).await; + + if let Ok(memory) = memory_result { + dict.insert_untagged( + "total", + UntaggedValue::bytes(memory.total().get::()), + ); + dict.insert_untagged( + "free", + UntaggedValue::bytes(memory.free().get::()), + ); + } + + if let Ok(swap) = swap_result { + dict.insert_untagged( + "swap total", + UntaggedValue::bytes(swap.total().get::()), + ); + dict.insert_untagged( + "swap free", + UntaggedValue::bytes(swap.free().get::()), + ); + } + + dict.into_value() +} + +async fn host(tag: Tag) -> Value { + let mut dict = TaggedDictBuilder::with_capacity(&tag, 6); + + let (platform_result, uptime_result) = + futures::future::join(host::platform(), host::uptime()).await; + + // OS + if let Ok(platform) = platform_result { + dict.insert_untagged("name", UntaggedValue::string(platform.system())); + dict.insert_untagged("release", UntaggedValue::string(platform.release())); + dict.insert_untagged("hostname", UntaggedValue::string(platform.hostname())); + dict.insert_untagged( + "arch", + UntaggedValue::string(platform.architecture().as_str()), + ); + } + + // Uptime + if let Ok(uptime) = uptime_result { + let mut uptime_dict = TaggedDictBuilder::with_capacity(&tag, 4); + + let uptime = uptime.get::().round() as i64; + let days = uptime / (60 * 60 * 24); + let hours = (uptime - days * 60 * 60 * 24) / (60 * 60); + let minutes = (uptime - days * 60 * 60 * 24 - hours * 60 * 60) / 60; + let seconds = uptime % 60; + + uptime_dict.insert_untagged("days", UntaggedValue::int(days)); + uptime_dict.insert_untagged("hours", UntaggedValue::int(hours)); + uptime_dict.insert_untagged("mins", UntaggedValue::int(minutes)); + uptime_dict.insert_untagged("secs", UntaggedValue::int(seconds)); + + dict.insert_value("uptime", uptime_dict); + } + + // Users + let mut users = host::users(); + let mut user_vec = vec![]; + while let Some(user) = users.next().await { + if let Ok(user) = user { + user_vec.push(Value { + value: UntaggedValue::string(user.username()), + tag: tag.clone(), + }); + } + } + let user_list = UntaggedValue::Table(user_vec); + dict.insert_untagged("users", user_list); + + dict.into_value() +} + +async fn disks(tag: Tag) -> Option { + let mut output = vec![]; + let mut partitions = disk::partitions_physical(); + while let Some(part) = partitions.next().await { + if let Ok(part) = part { + let mut dict = TaggedDictBuilder::with_capacity(&tag, 6); + dict.insert_untagged( + "device", + UntaggedValue::string( + part.device() + .unwrap_or_else(|| OsStr::new("N/A")) + .to_string_lossy(), + ), + ); + + dict.insert_untagged("type", UntaggedValue::string(part.file_system().as_str())); + dict.insert_untagged( + "mount", + UntaggedValue::string(part.mount_point().to_string_lossy()), + ); + + if let Ok(usage) = disk::usage(part.mount_point().to_path_buf()).await { + dict.insert_untagged( + "total", + UntaggedValue::bytes(usage.total().get::()), + ); + dict.insert_untagged( + "used", + UntaggedValue::bytes(usage.used().get::()), + ); + dict.insert_untagged( + "free", + UntaggedValue::bytes(usage.free().get::()), + ); + } + + output.push(dict.into_value()); + } + } + + if !output.is_empty() { + Some(UntaggedValue::Table(output)) + } else { + None + } +} + +async fn battery(tag: Tag) -> Option { + let mut output = vec![]; + + if let Ok(manager) = battery::Manager::new() { + if let Ok(batteries) = manager.batteries() { + for battery in batteries { + if let Ok(battery) = battery { + let mut dict = TaggedDictBuilder::new(&tag); + if let Some(vendor) = battery.vendor() { + dict.insert_untagged("vendor", UntaggedValue::string(vendor)); + } + if let Some(model) = battery.model() { + dict.insert_untagged("model", UntaggedValue::string(model)); + } + if let Some(cycles) = battery.cycle_count() { + dict.insert_untagged("cycles", UntaggedValue::int(cycles)); + } + if let Some(time_to_full) = battery.time_to_full() { + dict.insert_untagged( + "mins to full", + UntaggedValue::decimal( + time_to_full.get::(), + ), + ); + } + if let Some(time_to_empty) = battery.time_to_empty() { + dict.insert_untagged( + "mins to empty", + UntaggedValue::decimal( + time_to_empty.get::(), + ), + ); + } + output.push(dict.into_value()); + } + } + } + } + + if !output.is_empty() { + Some(UntaggedValue::Table(output)) + } else { + None + } +} + +async fn temp(tag: Tag) -> Option { + let mut output = vec![]; + + let mut sensors = sensors::temperatures(); + while let Some(sensor) = sensors.next().await { + if let Ok(sensor) = sensor { + let mut dict = TaggedDictBuilder::new(&tag); + dict.insert_untagged("unit", UntaggedValue::string(sensor.unit())); + if let Some(label) = sensor.label() { + dict.insert_untagged("label", UntaggedValue::string(label)); + } + dict.insert_untagged( + "temp", + UntaggedValue::decimal( + sensor + .current() + .get::(), + ), + ); + if let Some(high) = sensor.high() { + dict.insert_untagged( + "high", + UntaggedValue::decimal(high.get::()), + ); + } + if let Some(critical) = sensor.critical() { + dict.insert_untagged( + "critical", + UntaggedValue::decimal( + critical.get::(), + ), + ); + } + + output.push(dict.into_value()); + } + } + + if !output.is_empty() { + Some(UntaggedValue::Table(output)) + } else { + None + } +} + +async fn net(tag: Tag) -> Option { + let mut output = vec![]; + let mut io_counters = net::io_counters(); + while let Some(nic) = io_counters.next().await { + if let Ok(nic) = nic { + let mut network_idx = TaggedDictBuilder::with_capacity(&tag, 3); + network_idx.insert_untagged("name", UntaggedValue::string(nic.interface())); + network_idx.insert_untagged( + "sent", + UntaggedValue::bytes(nic.bytes_sent().get::()), + ); + network_idx.insert_untagged( + "recv", + UntaggedValue::bytes(nic.bytes_recv().get::()), + ); + output.push(network_idx.into_value()); + } + } + if !output.is_empty() { + Some(UntaggedValue::Table(output)) + } else { + None + } +} + +async fn sysinfo(tag: Tag) -> Vec { + let mut sysinfo = TaggedDictBuilder::with_capacity(&tag, 7); + + let (host, cpu, disks, memory, temp) = futures::future::join5( + host(tag.clone()), + cpu(tag.clone()), + disks(tag.clone()), + mem(tag.clone()), + temp(tag.clone()), + ) + .await; + let (net, battery) = futures::future::join(net(tag.clone()), battery(tag.clone())).await; + + sysinfo.insert_value("host", host); + if let Some(cpu) = cpu { + sysinfo.insert_value("cpu", cpu); + } + if let Some(disks) = disks { + sysinfo.insert_untagged("disks", disks); + } + sysinfo.insert_value("mem", memory); + if let Some(temp) = temp { + sysinfo.insert_untagged("temp", temp); + } + if let Some(net) = net { + sysinfo.insert_untagged("net", net); + } + if let Some(battery) = battery { + sysinfo.insert_untagged("battery", battery); + } + + vec![sysinfo.into_value()] +} + +impl Plugin for Sys { + fn config(&mut self) -> Result { + Ok(Signature::build("sys") + .desc("View information about the current system.") + .filter()) + } + + fn begin_filter(&mut self, callinfo: CallInfo) -> Result, ShellError> { + Ok(block_on(sysinfo(callinfo.name_tag)) + .into_iter() + .map(ReturnSuccess::value) + .collect()) + } + + fn filter(&mut self, _: Value) -> Result, ShellError> { + Ok(vec![]) + } +} + +fn main() { + serve_plugin(&mut Sys::new()); +} diff --git a/src/plugins/nu_plugin_core_textview.rs b/src/plugins/nu_plugin_core_textview.rs new file mode 100644 index 0000000000..71b840569a --- /dev/null +++ b/src/plugins/nu_plugin_core_textview.rs @@ -0,0 +1,296 @@ +use crossterm::{cursor, terminal, RawScreen}; +use crossterm::{InputEvent, KeyEvent}; +use nu_errors::ShellError; +use nu_protocol::{ + outln, serve_plugin, CallInfo, Plugin, Primitive, Signature, UntaggedValue, Value, +}; +use nu_source::AnchorLocation; + +use syntect::easy::HighlightLines; +use syntect::highlighting::{Style, ThemeSet}; +use syntect::parsing::SyntaxSet; + +use std::io::Write; +use std::path::Path; + +enum DrawCommand { + DrawString(Style, String), + NextLine, +} + +struct TextView; + +impl TextView { + fn new() -> TextView { + TextView + } +} + +impl Plugin for TextView { + fn config(&mut self) -> Result { + Ok(Signature::build("textview").desc("Autoview of text data.")) + } + + fn sink(&mut self, _call_info: CallInfo, input: Vec) { + if !input.is_empty() { + view_text_value(&input[0]); + } + } +} + +fn paint_textview( + draw_commands: &Vec, + starting_row: usize, + use_color_buffer: bool, +) -> usize { + let terminal = terminal(); + let cursor = cursor(); + + let size = terminal.terminal_size(); + + // render + let mut pos = 0; + let width = size.0 as usize; + let height = size.1 as usize - 1; + let mut frame_buffer = vec![]; + + for command in draw_commands { + match command { + DrawCommand::DrawString(style, string) => { + for chr in string.chars() { + if chr == '\t' { + for _ in 0..8 { + frame_buffer.push(( + ' ', + style.foreground.r, + style.foreground.g, + style.foreground.b, + )); + } + pos += 8; + } else { + frame_buffer.push(( + chr, + style.foreground.r, + style.foreground.g, + style.foreground.b, + )); + pos += 1; + } + } + } + DrawCommand::NextLine => { + for _ in 0..(width - pos % width) { + frame_buffer.push((' ', 0, 0, 0)); + } + pos += width - pos % width; + } + } + } + + let num_frame_buffer_rows = frame_buffer.len() / width; + let buffer_needs_scrolling = num_frame_buffer_rows > height; + + // display + let mut ansi_strings = vec![]; + let mut normal_chars = vec![]; + + for c in + &frame_buffer[starting_row * width..std::cmp::min(pos, (starting_row + height) * width)] + { + if use_color_buffer { + ansi_strings.push(ansi_term::Colour::RGB(c.1, c.2, c.3).paint(format!("{}", c.0))); + } else { + normal_chars.push(c.0); + } + } + + if buffer_needs_scrolling { + let _ = cursor.goto(0, 0); + } + + if use_color_buffer { + print!("{}", ansi_term::ANSIStrings(&ansi_strings)); + } else { + let s: String = normal_chars.into_iter().collect(); + print!("{}", s); + } + + if buffer_needs_scrolling { + let _ = cursor.goto(0, size.1); + print!( + "{}", + ansi_term::Colour::Blue.paint("[ESC to quit, arrow keys to move]") + ); + } + + let _ = std::io::stdout().flush(); + + num_frame_buffer_rows +} + +fn scroll_view_lines_if_needed(draw_commands: Vec, use_color_buffer: bool) { + let mut starting_row = 0; + + if let Ok(_raw) = RawScreen::into_raw_mode() { + let terminal = terminal(); + let mut size = terminal.terminal_size(); + let height = size.1 as usize - 1; + + let mut max_bottom_line = paint_textview(&draw_commands, starting_row, use_color_buffer); + + // Only scroll if needed + if max_bottom_line > height as usize { + let cursor = cursor(); + let _ = cursor.hide(); + + let input = crossterm::input(); + let mut sync_stdin = input.read_sync(); + + loop { + if let Some(ev) = sync_stdin.next() { + match ev { + InputEvent::Keyboard(k) => match k { + KeyEvent::Esc => { + break; + } + KeyEvent::Up | KeyEvent::Char('k') => { + if starting_row > 0 { + starting_row -= 1; + max_bottom_line = paint_textview( + &draw_commands, + starting_row, + use_color_buffer, + ); + } + } + KeyEvent::Down | KeyEvent::Char('j') => { + if starting_row < (max_bottom_line - height) { + starting_row += 1; + } + max_bottom_line = + paint_textview(&draw_commands, starting_row, use_color_buffer); + } + KeyEvent::PageUp | KeyEvent::Ctrl('b') => { + starting_row -= std::cmp::min(height, starting_row); + max_bottom_line = + paint_textview(&draw_commands, starting_row, use_color_buffer); + } + KeyEvent::PageDown | KeyEvent::Ctrl('f') | KeyEvent::Char(' ') => { + if starting_row < (max_bottom_line - height) { + starting_row += height; + + if starting_row > (max_bottom_line - height) { + starting_row = max_bottom_line - height; + } + } + max_bottom_line = + paint_textview(&draw_commands, starting_row, use_color_buffer); + } + _ => {} + }, + _ => {} + } + } + + let new_size = terminal.terminal_size(); + if size != new_size { + size = new_size; + let _ = terminal.clear(crossterm::ClearType::All); + max_bottom_line = + paint_textview(&draw_commands, starting_row, use_color_buffer); + } + } + let _ = cursor.show(); + + let _ = RawScreen::disable_raw_mode(); + } + } + + outln!(""); +} + +fn scroll_view(s: &str) { + let mut v = vec![]; + for line in s.lines() { + v.push(DrawCommand::DrawString(Style::default(), line.to_string())); + v.push(DrawCommand::NextLine); + } + scroll_view_lines_if_needed(v, false); +} + +fn view_text_value(value: &Value) { + let value_anchor = value.anchor(); + match &value.value { + UntaggedValue::Primitive(Primitive::String(ref s)) => { + if let Some(source) = value_anchor { + let extension: Option = match source { + AnchorLocation::File(file) => { + let path = Path::new(&file); + path.extension().map(|x| x.to_string_lossy().to_string()) + } + AnchorLocation::Url(url) => { + let url = url::Url::parse(&url); + if let Ok(url) = url { + let url = url.clone(); + if let Some(mut segments) = url.path_segments() { + if let Some(file) = segments.next_back() { + let path = Path::new(file); + path.extension().map(|x| x.to_string_lossy().to_string()) + } else { + None + } + } else { + None + } + } else { + None + } + } + //FIXME: this probably isn't correct + AnchorLocation::Source(_source) => None, + }; + + match extension { + Some(extension) => { + // Load these once at the start of your program + let ps: SyntaxSet = syntect::dumps::from_binary(include_bytes!( + "../../assets/syntaxes.bin" + )); + + if let Some(syntax) = ps.find_syntax_by_extension(&extension) { + let ts: ThemeSet = syntect::dumps::from_binary(include_bytes!( + "../../assets/themes.bin" + )); + let mut h = HighlightLines::new(syntax, &ts.themes["OneHalfDark"]); + + let mut v = vec![]; + for line in s.lines() { + let ranges: Vec<(Style, &str)> = h.highlight(line, &ps); + + for range in ranges { + v.push(DrawCommand::DrawString(range.0, range.1.to_string())); + } + + v.push(DrawCommand::NextLine); + } + scroll_view_lines_if_needed(v, true); + } else { + scroll_view(s); + } + } + _ => { + scroll_view(s); + } + } + } else { + scroll_view(s); + } + } + _ => {} + } +} + +fn main() { + serve_plugin(&mut TextView::new()); +}