use std::cell::Cell;
use std::io::{BufRead, Write};
use crate::error::{MindError, Result};
pub struct Choice<T> {
pub keys: &'static [&'static str],
pub value: T,
}
fn rejection<T>(choices: &[Choice<T>]) -> String {
let names: Vec<&str> = choices
.iter()
.filter_map(|c| c.keys.first().copied())
.collect();
let list = match names.as_slice() {
[] => String::new(),
[one] => (*one).to_string(),
[first, second] => format!("{first} or {second}"),
[rest @ .., last] => format!("{}, or {last}", rest.join(", ")),
};
format!("please answer {list} (Enter for the default)")
}
pub fn ask_from<T: Copy, R: BufRead, W: Write>(
input: &mut R,
output: &mut W,
prompt: &str,
choices: &[Choice<T>],
default: T,
) -> Result<T> {
loop {
write!(output, "{prompt}").map_err(|e| MindError::io("<stdout>", e))?;
output.flush().map_err(|e| MindError::io("<stdout>", e))?;
let mut line = String::new();
let read = input
.read_line(&mut line)
.map_err(|e| MindError::io("<stdin>", e))?;
if read == 0 {
writeln!(output).map_err(|e| MindError::io("<stdout>", e))?;
return Ok(default);
}
let reply = line.trim().to_ascii_lowercase();
if reply.is_empty() {
return Ok(default);
}
if let Some(c) = choices.iter().find(|c| c.keys.contains(&reply.as_str())) {
return Ok(c.value);
}
writeln!(output, "{}", rejection(choices)).map_err(|e| MindError::io("<stdout>", e))?;
}
}
thread_local! {
static CAPTURED: Cell<bool> = const { Cell::new(false) };
static REFUSED: Cell<bool> = const { Cell::new(false) };
}
pub fn set_captured(on: bool) {
if on {
REFUSED.with(|r| r.set(false));
}
CAPTURED.with(|c| c.set(on));
}
pub fn captured() -> bool {
CAPTURED.with(Cell::get)
}
pub fn refused() -> bool {
REFUSED.with(Cell::get)
}
pub fn guard(action: &str) -> Result<()> {
if CAPTURED.with(Cell::get) {
REFUSED.with(|r| r.set(true));
return Err(MindError::ConfirmationRequired {
action: action.to_string(),
});
}
Ok(())
}
pub fn ask<T: Copy>(prompt: &str, choices: &[Choice<T>], default: T, action: &str) -> Result<T> {
guard(action)?;
if !crate::hook::is_tty() {
return Err(MindError::ConfirmationRequired {
action: action.to_string(),
});
}
crate::progress::suspend(|| {
let stdin = std::io::stdin();
let mut input = stdin.lock();
let mut output = std::io::stdout();
ask_from(&mut input, &mut output, prompt, choices, default)
})
}
pub const YES_NO: [Choice<bool>; 2] = [
Choice {
keys: &["y", "yes"],
value: true,
},
Choice {
keys: &["n", "no"],
value: false,
},
];
#[cfg(test)]
mod tests {
use super::*;
fn run(input: &str, default: bool) -> (bool, String) {
let mut reader = std::io::Cursor::new(input.as_bytes().to_vec());
let mut out: Vec<u8> = Vec::new();
let v = ask_from(&mut reader, &mut out, "go? [Y/n] ", &YES_NO, default).unwrap();
(v, String::from_utf8(out).unwrap())
}
#[test]
fn listed_replies_are_accepted_in_any_case() {
assert!(run("y\n", false).0);
assert!(run(" YES \n", false).0);
assert!(!run("n\n", true).0);
assert!(!run("No\n", true).0);
}
#[test]
fn a_bare_enter_takes_the_default() {
assert!(run("\n", true).0);
assert!(!run("\n", false).0);
}
#[test]
fn an_unrecognized_reply_is_rejected_and_asked_again() {
let (v, out) = run("maybe\nn\n", true);
assert!(!v, "the second, valid reply decides");
assert_eq!(out.matches("go? [Y/n] ").count(), 2, "{out}");
assert!(out.contains("please answer y or n"), "{out}");
}
#[test]
fn eof_takes_the_default() {
assert!(run("", true).0);
assert!(!run("", false).0);
assert!(run("huh\n", true).0);
}
#[test]
fn a_captured_verb_refuses_to_prompt_and_says_so() {
set_captured(true);
assert!(!refused(), "entering capture starts clean");
assert!(guard("x").is_err());
assert!(refused());
let err = ask("go? [Y/n] ", &YES_NO, true, "running a hook").unwrap_err();
assert!(
matches!(err, MindError::ConfirmationRequired { ref action } if action == "running a hook"),
"{err}"
);
set_captured(false);
assert!(guard("x").is_ok(), "outside capture a prompt may ask");
assert!(refused(), "the flag survives until the next capture");
set_captured(true);
assert!(!refused());
set_captured(false);
}
#[test]
fn the_rejection_names_every_choice() {
let three = [
Choice {
keys: &["y"],
value: 0,
},
Choice {
keys: &["n"],
value: 1,
},
Choice {
keys: &["a"],
value: 2,
},
];
assert_eq!(
rejection(&three),
"please answer y, n, or a (Enter for the default)"
);
}
}