From 64f5f9c6f597f674db5a1e7ce57b1fc12931f764 Mon Sep 17 00:00:00 2001 From: Spenser Black Date: Tue, 2 Feb 2021 17:09:26 -0500 Subject: [PATCH] Allow confirm with no choice (#101) * Add interact_opt Confirm example * Execute private _interact_on fn Makes `Confirm::interact_on` call private fn `Confirm::_interact_on`, which will allow for optionally allowing the user to quit. * Read key instead of char Allows handling of escape key * Change return type to Result> Changes the return type of `Confirm::_interact_on`, and handles the rendered answer when it is `None`. * Handle quitting Confirm in private fn * Add optional interactions for Confirm Allows the user to answer neither "yes" nor "no" * Make exit clear confirm when waiting for newline * Display cancel instead of n/a * Set example confirm default to yes Sets the default value to "yes" for the optional confirm that waits for a newline, to simplify viewing behavior of a "quit" input when waiting for newline. Per discussion in #101 * Only set None value to default if quit forbidden Prevents the value of confirm prompt from being converted to the default value when no value selected *only if* returning a None value is allowed. * Show no selected value when it is None Removes the "cancel" output from an optional confirm. Per discussion in #101 --- examples/confirm.rs | 22 +++++++++++++ src/prompts/confirm.rs | 70 +++++++++++++++++++++++++++++++++--------- src/theme.rs | 44 +++++++++++++++++--------- 3 files changed, 108 insertions(+), 28 deletions(-) diff --git a/examples/confirm.rs b/examples/confirm.rs index 5b5b70ce..3b1d3ca0 100644 --- a/examples/confirm.rs +++ b/examples/confirm.rs @@ -45,4 +45,26 @@ fn main() { } else { println!("nevermind then :("); } + + match Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Do you really really really really want to continue?") + .interact_opt() + .unwrap() + { + Some(true) => println!("Looks like you want to continue"), + Some(false) => println!("nevermind then :("), + None => println!("Ok, we can start over later"), + } + + match Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Do you really really really really really want to continue?") + .default(true) + .wait_for_newline(true) + .interact_opt() + .unwrap() + { + Some(true) => println!("Looks like you want to continue"), + Some(false) => println!("nevermind then :("), + None => println!("Ok, we can start over later"), + } } diff --git a/src/prompts/confirm.rs b/src/prompts/confirm.rs index 14ab36dc..98de93c7 100644 --- a/src/prompts/confirm.rs +++ b/src/prompts/confirm.rs @@ -2,7 +2,7 @@ use std::io; use crate::theme::{SimpleTheme, TermThemeRenderer, Theme}; -use console::Term; +use console::{Key, Term}; /// Renders a confirm prompt. /// @@ -119,6 +119,15 @@ impl<'a> Confirm<'a> { self.interact_on(&Term::stderr()) } + /// Enables user interaction and returns the result. + /// + /// This method is similar to [interact_on_opt](#method.interact_on_opt) except for the fact that it does not allow selection of the terminal. + /// The dialog is rendered on stderr. + /// Result contains `Some(bool)` if user answered "yes" or "no" or `None` if user cancelled with 'Esc' or 'q'. + pub fn interact_opt(&self) -> io::Result> { + self.interact_on_opt(&Term::stderr()) + } + /// Like [interact](#method.interact) but allows a specific terminal to be set. /// /// ## Examples @@ -135,6 +144,34 @@ impl<'a> Confirm<'a> { /// # } /// ``` pub fn interact_on(&self, term: &Term) -> io::Result { + self._interact_on(term, false)? + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case")) + } + + /// Like [interact_opt](#method.interact_opt) but allows a specific terminal to be set. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Confirm; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let confirmation = Confirm::new() + /// .interact_on_opt(&Term::stdout())?; + /// + /// match confirmation { + /// Some(answer) => println!("User answered {}", if answer { "yes" } else { "no " }), + /// None => println!("User did not answer") + /// } + /// + /// Ok(()) + /// } + /// ``` + pub fn interact_on_opt(&self, term: &Term) -> io::Result> { + self._interact_on(term, true) + } + + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result> { let mut render = TermThemeRenderer::new(term, self.theme); let default_if_show = if self.show_default { @@ -156,24 +193,28 @@ impl<'a> Confirm<'a> { let mut value = default_if_show; loop { - let input = term.read_char()?; + let input = term.read_key()?; match input { - 'y' | 'Y' => { + Key::Char('y') | Key::Char('Y') => { value = Some(true); } - 'n' | 'N' => { + Key::Char('n') | Key::Char('N') => { value = Some(false); } - '\n' | '\r' => { - value = value.or(self.default); + Key::Enter => { + if !allow_quit { + value = value.or(self.default); + } - if let Some(val) = value { - rv = val; + if value.is_some() || allow_quit { + rv = value; break; - } else { - continue; } + continue; + } + Key::Escape | Key::Char('q') if allow_quit => { + value = None; } _ => { continue; @@ -187,11 +228,12 @@ impl<'a> Confirm<'a> { // Default behavior: matches continuously on every keystroke, // and does not wait for user to hit the Enter key. loop { - let input = term.read_char()?; + let input = term.read_key()?; let value = match input { - 'y' | 'Y' => true, - 'n' | 'N' => false, - '\n' | '\r' if self.default.is_some() => self.default.unwrap(), + Key::Char('y') | Key::Char('Y') => Some(true), + Key::Char('n') | Key::Char('N') => Some(false), + Key::Enter if self.default.is_some() => Some(self.default.unwrap()), + Key::Escape | Key::Char('q') if allow_quit => None, _ => { continue; } diff --git a/src/theme.rs b/src/theme.rs index 9e8dfee9..667b8890 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -40,12 +40,21 @@ pub trait Theme { &self, f: &mut dyn fmt::Write, prompt: &str, - selection: bool, + selection: Option, ) -> fmt::Result { - if prompt.is_empty() { - write!(f, "{}", if selection { "yes" } else { "no" }) - } else { - write!(f, "{} {}", &prompt, if selection { "yes" } else { "no" }) + let selection = selection.map(|b| if b { "yes" } else { "no" }); + + match selection { + Some(selection) if prompt.is_empty() => { + write!(f, "{}", selection) + } + Some(selection) => { + write!(f, "{} {}", &prompt, selection) + } + None if prompt.is_empty() => Ok(()), + None => { + write!(f, "{}", &prompt) + } } } @@ -366,7 +375,7 @@ impl Theme for ColorfulTheme { &self, f: &mut dyn fmt::Write, prompt: &str, - selection: bool, + selection: Option, ) -> fmt::Result { if !prompt.is_empty() { write!( @@ -376,14 +385,21 @@ impl Theme for ColorfulTheme { self.prompt_style.apply_to(prompt) )?; } + let selection = selection.map(|b| if b { "yes" } else { "no" }); - write!( - f, - "{} {}", - &self.success_suffix, - self.values_style - .apply_to(if selection { "yes" } else { "no" }) - ) + match selection { + Some(selection) => { + write!( + f, + "{} {}", + &self.success_suffix, + self.values_style.apply_to(selection) + ) + } + None => { + write!(f, "{}", &self.success_suffix) + } + } } /// Formats an input prompt after selection. @@ -607,7 +623,7 @@ impl<'a> TermThemeRenderer<'a> { self.write_formatted_str(|this, buf| this.theme.format_confirm_prompt(buf, prompt, default)) } - pub fn confirm_prompt_selection(&mut self, prompt: &str, sel: bool) -> io::Result<()> { + pub fn confirm_prompt_selection(&mut self, prompt: &str, sel: Option) -> io::Result<()> { self.write_formatted_prompt(|this, buf| { this.theme.format_confirm_prompt_selection(buf, prompt, sel) })