diff --git a/src/lib.rs b/src/lib.rs index b231d4a..eeda96a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use nu_plugin::{LabeledError, Plugin}; use nu_protocol::{PluginSignature, SyntaxShape}; mod confirm; +mod multiselect; mod password; mod prompt; mod select; @@ -64,6 +65,27 @@ impl Plugin for DialogPlugin { None, ) .category(nu_protocol::Category::Misc), + PluginSignature::build("ask multiselect") + .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.", + ) + .switch("abortable", "If set users can abort the prompt.", None) + .named( + "prompt", + SyntaxShape::String, + "An optional prompt that can be shown to the user for the selection.", + None, + ) + .named( + "default", + SyntaxShape::String, + "The default selections as a comma separated string of indices", + None, + ) + .category(nu_protocol::Category::Misc), PluginSignature::build("ask password") .usage("Prompt the user with a password input.") .named( @@ -95,6 +117,7 @@ impl Plugin for DialogPlugin { match name { "ask confirm" => self.confirm(call, input), "ask select" => self.select(call, input), + "ask multiselect" => self.multiselect(call, input), "ask password" => self.password(call, input), "ask" => Err(LabeledError { diff --git a/src/multiselect.rs b/src/multiselect.rs new file mode 100644 index 0000000..20b7b0d --- /dev/null +++ b/src/multiselect.rs @@ -0,0 +1,58 @@ +use dialoguer::MultiSelect; +use nu_plugin::{EvaluatedCall, LabeledError}; +use nu_protocol::{Span, Value}; + +use crate::{prompt::UserPrompt, DialogPlugin}; + +impl DialogPlugin { + pub(crate) fn multiselect( + &self, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let options: Vec = call.req(0)?; + + let mut select = MultiSelect::new(); + select.items(&options); + + if let Some(prompt) = call.get_flag::("prompt")? { + select.with_prompt(prompt); + } + if let Some(def) = call.get_flag::("default")? { + let defaults = def + .split(',') + .map(|v| v.trim()) + .filter_map(|v| v.parse::().ok()) + .collect::>(); + let check_states = (0..options.len()) + .map(|i| defaults.contains(&i)) + .collect::>(); + select.defaults(&check_states); + } + + if call.has_flag("abortable") { + if let Some(selection) = select.ask_opt(call.head)? { + Ok(map_selection(selection, options, call.head)) + } else { + Ok(Value::Nothing { span: call.head }) + } + } else { + let selection = select.ask(call.head)?; + + Ok(map_selection(selection, options, call.head)) + } + } +} + +fn map_selection(selection: Vec, options: Vec, span: Span) -> Value { + let selected_items = options + .into_iter() + .enumerate() + .filter(|(i, _)| selection.contains(i)) + .map(|(_, val)| Value::String { val, span }) + .collect::>(); + Value::List { + vals: selected_items, + span, + } +} diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 3c7a366..f18f2e7 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -1,6 +1,6 @@ use std::io; -use dialoguer::Password; +use dialoguer::{MultiSelect, Password}; use nu_plugin::LabeledError; mod generic_select; pub use generic_select::GenericSelect; @@ -39,6 +39,19 @@ impl<'a> UserPrompt for Password<'a> { } } +impl<'a> UserPrompt for MultiSelect<'a> { + type Output = Vec; + + fn ask(&self, span: Span) -> Result { + self.interact().map_err(|e| create_labeled_error(e, span)) + } + + fn ask_opt(&self, span: Span) -> Result, LabeledError> { + self.interact_opt() + .map_err(|e| create_labeled_error(e, span)) + } +} + fn create_labeled_error(e: io::Error, span: Span) -> LabeledError { LabeledError { label: "Failed to prompt user".into(),