use std::io::{IsTerminal as _, Write as _};
pub trait Prompter {
fn ask(&mut self, question: &str) -> bool;
fn interactive(&self) -> bool;
}
pub struct Terminal;
impl Prompter for Terminal {
fn ask(&mut self, question: &str) -> bool {
eprint!("{question} [y/N] ");
let _ = std::io::stderr().flush();
let mut line = String::new();
std::io::stdin().read_line(&mut line).is_ok()
&& matches!(line.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}
fn interactive(&self) -> bool {
std::io::stdin().is_terminal()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InstallAnswer {
pub download: bool,
pub build_index: bool,
}
pub fn install_questions(
downloads: bool,
index_planned: bool,
has_concerns: bool,
assume_yes: bool,
p: &mut dyn Prompter,
) -> InstallAnswer {
if assume_yes {
return InstallAnswer {
download: downloads,
build_index: index_planned,
};
}
if downloads && !p.ask("Continue?") {
return InstallAnswer {
download: false,
build_index: false,
};
}
let build_index = index_planned
&& if downloads && !has_concerns {
true
} else if downloads {
p.ask("Build the blind index as well?")
} else {
p.ask("Build the blind index now?")
};
InstallAnswer {
download: downloads,
build_index,
}
}
pub fn build_question(has_concerns: bool, assume_yes: bool, p: &mut dyn Prompter) -> bool {
if !has_concerns || assume_yes || !p.interactive() {
return true;
}
p.ask("Build it?")
}
#[cfg(test)]
pub mod tests {
use super::*;
pub struct Scripted {
pub answers: Vec<bool>,
pub asked: Vec<String>,
pub tty: bool,
}
impl Scripted {
pub fn new(answers: &[bool], tty: bool) -> Self {
Self {
answers: answers.iter().rev().copied().collect(),
asked: Vec::new(),
tty,
}
}
}
impl Prompter for Scripted {
fn ask(&mut self, q: &str) -> bool {
self.asked.push(q.to_string());
self.answers.pop().unwrap_or(false)
}
fn interactive(&self) -> bool {
self.tty
}
}
fn ans(download: bool, build_index: bool) -> InstallAnswer {
InstallAnswer {
download,
build_index,
}
}
#[test]
fn yes_skips_every_question() {
let mut p = Scripted::new(&[], false);
assert_eq!(
install_questions(true, true, true, true, &mut p),
ans(true, true)
);
assert_eq!(
install_questions(false, true, false, true, &mut p),
ans(false, true)
);
assert!(build_question(true, true, &mut p));
assert!(p.asked.is_empty());
}
#[test]
fn an_ordinary_index_rides_on_the_download_question() {
let mut p = Scripted::new(&[true], true);
assert_eq!(
install_questions(true, true, false, false, &mut p),
ans(true, true)
);
assert_eq!(p.asked, ["Continue?"]);
}
#[test]
fn a_big_index_gets_its_own_question_and_can_be_declined() {
let mut p = Scripted::new(&[true, false], true);
assert_eq!(
install_questions(true, true, true, false, &mut p),
ans(true, false)
);
assert_eq!(p.asked.len(), 2);
let mut p = Scripted::new(&[true, true], true);
assert_eq!(
install_questions(true, true, true, false, &mut p),
ans(true, true)
);
}
#[test]
fn declining_the_download_declines_everything() {
let mut p = Scripted::new(&[false], true);
assert_eq!(
install_questions(true, true, true, false, &mut p),
ans(false, false)
);
assert_eq!(p.asked.len(), 1, "no second question after a no");
}
#[test]
fn end_of_input_without_yes_cancels_as_before() {
let mut p = Scripted::new(&[], false);
assert_eq!(
install_questions(true, true, false, false, &mut p),
ans(false, false)
);
let mut p = Scripted::new(&[], false);
assert_eq!(
install_questions(false, true, false, false, &mut p),
ans(false, false)
);
}
#[test]
fn index_only_installs_ask_once() {
let mut p = Scripted::new(&[true], true);
assert_eq!(
install_questions(false, true, false, false, &mut p),
ans(false, true)
);
assert_eq!(p.asked, ["Build the blind index now?"]);
}
#[test]
fn index_build_asks_only_about_big_builds_and_only_at_a_terminal() {
let mut p = Scripted::new(&[], true);
assert!(
build_question(false, false, &mut p),
"ordinary: no question"
);
assert!(p.asked.is_empty());
let mut p = Scripted::new(&[false], true);
assert!(!build_question(true, false, &mut p));
let mut p = Scripted::new(&[], false);
assert!(build_question(true, false, &mut p), "scripts keep working");
assert!(p.asked.is_empty());
}
}