use std::io::{BufRead, IsTerminal};
use std::sync::Mutex;
use demand::{Confirm, Dialog, DialogButton};
use crate::env;
use crate::ui::ctrlc;
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::theme::get_theme;
static MUTEX: Mutex<()> = Mutex::new(());
static SKIP_PROMPT: Mutex<bool> = Mutex::new(false);
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIs)]
pub(crate) enum Confirmation {
Yes,
No,
Unavailable,
}
fn can_prompt_dialog() -> bool {
dialog_prompt_allowed(
console::user_attended_stderr(),
std::io::stdin().is_terminal(),
env::__USAGE.is_some(),
)
}
fn dialog_prompt_allowed(stderr_tty: bool, stdin_tty: bool, usage: bool) -> bool {
stderr_tty && stdin_tty && !usage
}
fn restore_cursor() {
let _ = console::Term::stderr().show_cursor();
}
pub(crate) fn confirm<S: Into<String>>(message: S) -> eyre::Result<Confirmation> {
confirm_with_default(message, true)
}
pub(crate) fn confirm_with_default<S: Into<String>>(
message: S,
default_yes: bool,
) -> eyre::Result<Confirmation> {
let _lock = MUTEX.lock().unwrap(); ctrlc::show_cursor_after_ctrl_c();
if !console::user_attended_stderr() || env::__USAGE.is_some() {
return Ok(Confirmation::Unavailable);
}
let message = message.into();
let _progress_pause = MultiProgressReport::try_get().map(|report| report.pause_progress());
if !std::io::stdin().is_terminal() {
return read_confirm_from_stdin(&message, default_yes);
}
let theme = get_theme();
let result = Confirm::new(message)
.selected(default_yes)
.theme(&theme)
.run()
.inspect_err(|_| restore_cursor())?;
Ok(if result {
Confirmation::Yes
} else {
Confirmation::No
})
}
fn read_confirm_from_stdin(message: &str, default_yes: bool) -> eyre::Result<Confirmation> {
let hint = if default_yes { "[Y/n]" } else { "[y/N]" };
safe_eprintln!("{message} {hint}");
let mut line = String::new();
let read = std::io::stdin().lock().read_line(&mut line)?;
let answer = (read > 0).then_some(line.as_str());
parse_confirm_answer(answer, default_yes)
}
fn parse_confirm_answer(line: Option<&str>, default_yes: bool) -> eyre::Result<Confirmation> {
let Some(line) = line else {
return Ok(Confirmation::No);
};
let answer = line.trim().to_lowercase();
if answer.is_empty() {
return Ok(default_answer(default_yes));
}
if "yes".starts_with(&answer) {
Ok(Confirmation::Yes)
} else if "no".starts_with(&answer) {
Ok(Confirmation::No)
} else {
eyre::bail!("expected y/yes or n/no, got {:?}", line.trim())
}
}
fn default_answer(default_yes: bool) -> Confirmation {
if default_yes {
Confirmation::Yes
} else {
Confirmation::No
}
}
pub(crate) fn confirm_with_all<S: Into<String>>(message: S) -> eyre::Result<Confirmation> {
let _lock = MUTEX.lock().unwrap(); ctrlc::show_cursor_after_ctrl_c();
if !can_prompt_dialog() {
return Ok(Confirmation::Unavailable);
}
let mut skip_prompt = SKIP_PROMPT.lock().unwrap();
if *skip_prompt {
return Ok(Confirmation::Yes);
}
let _progress_pause = MultiProgressReport::try_get().map(|report| report.pause_progress());
let theme = get_theme();
let answer = Dialog::new(message)
.buttons(vec![
DialogButton::new("Yes"),
DialogButton::new("No"),
DialogButton::new("All"),
])
.selected_button(1)
.theme(&theme)
.run()
.inspect_err(|_| restore_cursor())?;
if answer == "All" {
*skip_prompt = true;
}
Ok(dialog_answer(&answer))
}
fn dialog_answer(answer: &str) -> Confirmation {
match answer {
"Yes" | "All" => Confirmation::Yes,
_ => Confirmation::No,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dialog_needs_both_ends_of_the_terminal() {
assert!(dialog_prompt_allowed(true, true, false));
assert!(!dialog_prompt_allowed(false, true, false));
assert!(!dialog_prompt_allowed(true, false, false));
assert!(!dialog_prompt_allowed(false, false, false));
}
#[test]
fn dialog_never_shown_while_generating_usage() {
assert!(!dialog_prompt_allowed(true, true, true));
assert!(!dialog_prompt_allowed(false, true, true));
assert!(!dialog_prompt_allowed(true, false, true));
assert!(!dialog_prompt_allowed(false, false, true));
}
#[test]
fn eof_is_not_consent() {
assert_eq!(parse_confirm_answer(None, true).unwrap(), Confirmation::No);
assert_eq!(parse_confirm_answer(None, false).unwrap(), Confirmation::No);
}
#[test]
fn empty_line_takes_the_default() {
for line in ["", "\n", " \n"] {
assert_eq!(
parse_confirm_answer(Some(line), true).unwrap(),
Confirmation::Yes
);
assert_eq!(
parse_confirm_answer(Some(line), false).unwrap(),
Confirmation::No
);
}
}
#[test]
fn explicit_answers_override_the_default() {
for line in ["y\n", "Y", "ye", "yes", "YES\n"] {
assert_eq!(
parse_confirm_answer(Some(line), false).unwrap(),
Confirmation::Yes
);
}
for line in ["n\n", "N", "no", "No\n"] {
assert_eq!(
parse_confirm_answer(Some(line), true).unwrap(),
Confirmation::No
);
}
}
#[test]
fn all_answers_yes() {
assert_eq!(dialog_answer("Yes"), Confirmation::Yes);
assert_eq!(dialog_answer("All"), Confirmation::Yes);
assert_eq!(dialog_answer("No"), Confirmation::No);
assert_eq!(dialog_answer(""), Confirmation::No);
}
#[test]
fn unrecognized_answers_are_an_error() {
for line in ["maybe", "1", "yep", "sure\n"] {
assert!(parse_confirm_answer(Some(line), true).is_err());
assert!(parse_confirm_answer(Some(line), false).is_err());
}
}
}