use anyhow::{Context, Result};
pub trait InputProvider: Send + Sync {
fn password(&self, prompt: &str) -> Result<String>;
fn password_with_confirm(&self, prompt: &str) -> Result<String>;
fn confirm(&self, prompt: &str, default: bool) -> Result<bool>;
fn input_text(&self, prompt: &str, default: Option<&str>) -> Result<String>;
fn select(&self, prompt: &str, items: &[String], default: usize) -> Result<usize>;
}
pub struct TerminalInput;
fn test_env_fallback(var: &str) -> Option<String> {
if std::env::var("NEX_TESTING").is_err() {
return None;
}
std::env::var(var).ok()
}
impl InputProvider for TerminalInput {
fn password(&self, prompt: &str) -> Result<String> {
if let Some(pp) = test_env_fallback("NEX_TEST_PASSPHRASE") {
return Ok(pp);
}
dialoguer::Password::new()
.with_prompt(prompt)
.interact()
.context("failed to read password")
}
fn password_with_confirm(&self, prompt: &str) -> Result<String> {
if let Some(pp) = test_env_fallback("NEX_TEST_PASSPHRASE") {
return Ok(pp);
}
dialoguer::Password::new()
.with_prompt(prompt)
.with_confirmation("Confirm", "Values do not match")
.interact()
.context("failed to read password")
}
fn confirm(&self, prompt: &str, default: bool) -> Result<bool> {
if let Some(val) = test_env_fallback("NEX_TEST_CONFIRM") {
return Ok(val == "y" || val == "yes" || val == "true");
}
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return Ok(default);
}
dialoguer::Confirm::new()
.with_prompt(prompt)
.default(default)
.interact()
.context("failed to read confirmation")
}
fn input_text(&self, prompt: &str, default: Option<&str>) -> Result<String> {
if let Some(val) = test_env_fallback("NEX_TEST_INPUT") {
return Ok(val);
}
let mut builder = dialoguer::Input::<String>::new().with_prompt(prompt);
if let Some(d) = default {
builder = builder.default(d.to_string());
}
builder.interact_text().context("failed to read input")
}
fn select(&self, prompt: &str, items: &[String], default: usize) -> Result<usize> {
if let Some(val) = test_env_fallback("NEX_TEST_SELECT") {
return Ok(val.parse().unwrap_or(default));
}
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return Ok(default);
}
dialoguer::Select::new()
.with_prompt(prompt)
.items(items)
.default(default)
.interact()
.context("failed to read selection")
}
}
#[cfg(test)]
#[allow(dead_code)]
pub struct ScriptedInput {
responses: std::sync::Mutex<std::collections::VecDeque<String>>,
}
#[cfg(test)]
#[allow(dead_code)]
impl ScriptedInput {
pub fn new(responses: Vec<&str>) -> Self {
Self {
responses: std::sync::Mutex::new(responses.into_iter().map(String::from).collect()),
}
}
fn next_response(&self) -> Result<String> {
self.responses
.lock()
.expect("scripted input lock")
.pop_front()
.context("ScriptedInput: no more responses queued")
}
}
#[cfg(test)]
impl InputProvider for ScriptedInput {
fn password(&self, _prompt: &str) -> Result<String> {
self.next_response()
}
fn password_with_confirm(&self, _prompt: &str) -> Result<String> {
self.next_response()
}
fn confirm(&self, _prompt: &str, default: bool) -> Result<bool> {
match self.responses.lock().expect("lock").pop_front() {
Some(r) => Ok(r == "y" || r == "yes" || r == "true"),
None => Ok(default),
}
}
fn input_text(&self, _prompt: &str, default: Option<&str>) -> Result<String> {
match self.responses.lock().expect("lock").pop_front() {
Some(r) if !r.is_empty() => Ok(r),
_ => Ok(default.unwrap_or("").to_string()),
}
}
fn select(&self, _prompt: &str, _items: &[String], default: usize) -> Result<usize> {
match self.responses.lock().expect("lock").pop_front() {
Some(r) => Ok(r.parse().unwrap_or(default)),
None => Ok(default),
}
}
}
static INPUT: std::sync::OnceLock<Box<dyn InputProvider>> = std::sync::OnceLock::new();
pub fn input() -> &'static dyn InputProvider {
INPUT.get_or_init(|| Box::new(TerminalInput)).as_ref()
}
#[cfg(test)]
#[allow(dead_code)]
pub fn set_input(provider: Box<dyn InputProvider>) {
let _ = INPUT.set(provider);
}