diff --git a/Cargo.lock b/Cargo.lock index 0e7db0a..a4863be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,6 +307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" dependencies = [ "console", + "fuzzy-matcher", "shell-words", "tempfile", "zeroize", @@ -394,6 +395,15 @@ dependencies = [ "instant", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "getrandom" version = "0.2.9" @@ -1266,6 +1276,16 @@ dependencies = [ "syn 2.0.13", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.1.45" diff --git a/Cargo.toml b/Cargo.toml index 59339c3..ce2dada 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,6 @@ name = "nu_plugin_dialog" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dialoguer = "0.10.4" +dialoguer = { version = "0.10.4", features = ["fuzzy-select", "completion"] } nu-plugin = "0.78.0" nu-protocol = "0.78.0" diff --git a/src/confirm.rs b/src/confirm.rs index 6a8745b..e2060a3 100644 --- a/src/confirm.rs +++ b/src/confirm.rs @@ -1,7 +1,7 @@ use nu_plugin::{EvaluatedCall, LabeledError}; use nu_protocol::Value; -use crate::DialogPlugin; +use crate::{prompt::UserPrompt, DialogPlugin}; impl DialogPlugin { pub(crate) fn confirm( @@ -18,11 +18,7 @@ impl DialogPlugin { if let Some(val) = default_val { confirm.default(val); } - let result = confirm.interact().map_err(|e| LabeledError { - label: "Failed to prompt user".into(), - msg: e.to_string(), - span: Some(call.head), - })?; + let result = confirm.prompt()?; Ok(Value::Bool { val: result, diff --git a/src/lib.rs b/src/lib.rs index 2fa22d8..5a16e92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ use nu_plugin::{LabeledError, Plugin}; use nu_protocol::{PluginSignature, SyntaxShape}; mod confirm; +mod prompt; +mod select; pub struct DialogPlugin { pub(crate) theme: Box, @@ -38,6 +40,26 @@ impl Plugin for DialogPlugin { None, ) .category(nu_protocol::Category::Misc), + PluginSignature::build("ask select") + .usage("Prompt the user with a selection prompt.") + .required( + "items", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "The items out of which one can be selected.", + ) + .named( + "prompt", + SyntaxShape::String, + "An optional prompt that can be shown to the user for the selection.", + None, + ) + .named( + "default", + SyntaxShape::Number, + "The default selection.", + None, + ) + .category(nu_protocol::Category::Misc), ] } @@ -49,6 +71,7 @@ impl Plugin for DialogPlugin { ) -> Result { match name { "ask confirm" => self.confirm(call, input), + "ask select" => self.select(call, input), "ask" => Err(LabeledError { label: "Missing subcommand".into(), diff --git a/src/prompt/generic_select.rs b/src/prompt/generic_select.rs new file mode 100644 index 0000000..70c8a37 --- /dev/null +++ b/src/prompt/generic_select.rs @@ -0,0 +1,69 @@ +use dialoguer::{theme::Theme, FuzzySelect, Select}; + +use super::{create_labeled_error, UserPrompt}; + +pub enum GenericSelect<'a> { + Fuzzy(FuzzySelect<'a>), + Normal(Select<'a>), +} + +impl<'a> GenericSelect<'a> { + pub fn fuzzy(theme: &'a dyn Theme) -> Self { + Self::Fuzzy(FuzzySelect::with_theme(theme)) + } + + pub fn normal(theme: &'a dyn Theme) -> Self { + Self::Normal(Select::with_theme(theme)) + } + + pub fn items(&mut self, items: &[T]) -> &mut Self { + match self { + GenericSelect::Fuzzy(f) => f.items(items).nop(), + GenericSelect::Normal(n) => n.items(items).nop(), + } + self + } + + pub fn default(&mut self, val: usize) -> &mut Self { + match self { + GenericSelect::Fuzzy(f) => f.default(val).nop(), + GenericSelect::Normal(n) => n.default(val).nop(), + } + self + } + + pub fn with_prompt>(&mut self, prompt: S) -> &mut Self { + match self { + GenericSelect::Fuzzy(f) => f.with_prompt(prompt).nop(), + GenericSelect::Normal(n) => n.with_prompt(prompt).nop(), + } + self + } +} + +impl<'a> UserPrompt for GenericSelect<'a> { + type Output = usize; + + fn prompt(&self) -> Result { + match self { + GenericSelect::Fuzzy(f) => f.interact(), + GenericSelect::Normal(n) => n.interact(), + } + .map_err(create_labeled_error) + } + + fn prompt_opt(&self) -> Result, nu_plugin::LabeledError> { + match self { + GenericSelect::Fuzzy(f) => f.interact_opt(), + GenericSelect::Normal(n) => n.interact_opt(), + } + .map_err(create_labeled_error) + } +} + +trait Nop { + fn nop(&mut self) {} +} + +impl<'a> Nop for Select<'a> {} +impl<'a> Nop for FuzzySelect<'a> {} diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs new file mode 100644 index 0000000..e9349f3 --- /dev/null +++ b/src/prompt/mod.rs @@ -0,0 +1,33 @@ +use std::io; + +use nu_plugin::LabeledError; +mod generic_select; +pub use generic_select::GenericSelect; + +pub trait UserPrompt { + type Output; + + fn prompt(&self) -> Result; + + fn prompt_opt(&self) -> Result, LabeledError>; +} + +impl<'a> UserPrompt for dialoguer::Confirm<'a> { + type Output = bool; + + fn prompt(&self) -> Result { + self.interact().map_err(create_labeled_error) + } + + fn prompt_opt(&self) -> Result, LabeledError> { + self.interact_opt().map_err(create_labeled_error) + } +} + +fn create_labeled_error(e: io::Error) -> LabeledError { + LabeledError { + label: "Failed to prompt user".into(), + msg: e.to_string(), + span: None, + } +} diff --git a/src/select.rs b/src/select.rs new file mode 100644 index 0000000..f883d54 --- /dev/null +++ b/src/select.rs @@ -0,0 +1,39 @@ +use nu_plugin::{EvaluatedCall, LabeledError}; +use nu_protocol::Value; + +use crate::{ + prompt::{GenericSelect, UserPrompt}, + DialogPlugin, +}; + +impl DialogPlugin { + pub(crate) fn select( + &self, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let mut options: Vec = call.req(0)?; + + let mut select = if call.has_flag("fuzzy") { + GenericSelect::fuzzy(&*self.theme) + } else { + GenericSelect::normal(&*self.theme) + }; + select.items(&options); + + if let Some(prompt) = call.get_flag::("prompt")? { + select.with_prompt(prompt); + } + if let Some(def) = call.get_flag::("default")? { + select.default(def); + } + + let selection = select.prompt()?; + let selected_item = options.remove(selection); + + Ok(Value::String { + val: selected_item, + span: call.head, + }) + } +}