Skip to content

Commit

Permalink
Allow confirm with no choice (#101)
Browse files Browse the repository at this point in the history
* 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<Option<bool>>

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
  • Loading branch information
spenserblack authored Feb 2, 2021
1 parent 0250090 commit 64f5f9c
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 28 deletions.
22 changes: 22 additions & 0 deletions examples/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}
}
70 changes: 56 additions & 14 deletions src/prompts/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::io;

use crate::theme::{SimpleTheme, TermThemeRenderer, Theme};

use console::Term;
use console::{Key, Term};

/// Renders a confirm prompt.
///
Expand Down Expand Up @@ -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<Option<bool>> {
self.interact_on_opt(&Term::stderr())
}

/// Like [interact](#method.interact) but allows a specific terminal to be set.
///
/// ## Examples
Expand All @@ -135,6 +144,34 @@ impl<'a> Confirm<'a> {
/// # }
/// ```
pub fn interact_on(&self, term: &Term) -> io::Result<bool> {
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<Option<bool>> {
self._interact_on(term, true)
}

fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<bool>> {
let mut render = TermThemeRenderer::new(term, self.theme);

let default_if_show = if self.show_default {
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
44 changes: 30 additions & 14 deletions src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,21 @@ pub trait Theme {
&self,
f: &mut dyn fmt::Write,
prompt: &str,
selection: bool,
selection: Option<bool>,
) -> 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)
}
}
}

Expand Down Expand Up @@ -366,7 +375,7 @@ impl Theme for ColorfulTheme {
&self,
f: &mut dyn fmt::Write,
prompt: &str,
selection: bool,
selection: Option<bool>,
) -> fmt::Result {
if !prompt.is_empty() {
write!(
Expand All @@ -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.
Expand Down Expand Up @@ -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<bool>) -> io::Result<()> {
self.write_formatted_prompt(|this, buf| {
this.theme.format_confirm_prompt_selection(buf, prompt, sel)
})
Expand Down

0 comments on commit 64f5f9c

Please sign in to comment.