1use anyhow::{Context, Result};
18
19pub trait InputProvider: Send + Sync {
21 fn password(&self, prompt: &str) -> Result<String>;
23
24 fn password_with_confirm(&self, prompt: &str) -> Result<String>;
26
27 fn confirm(&self, prompt: &str, default: bool) -> Result<bool>;
29
30 fn input_text(&self, prompt: &str, default: Option<&str>) -> Result<String>;
32
33 fn select(&self, prompt: &str, items: &[String], default: usize) -> Result<usize>;
35}
36
37pub struct TerminalInput;
41
42fn test_env_fallback(var: &str) -> Option<String> {
46 if std::env::var("NEX_TESTING").is_err() {
47 return None;
48 }
49 std::env::var(var).ok()
50}
51
52impl InputProvider for TerminalInput {
53 fn password(&self, prompt: &str) -> Result<String> {
54 if let Some(pp) = test_env_fallback("NEX_TEST_PASSPHRASE") {
55 return Ok(pp);
56 }
57 dialoguer::Password::new()
58 .with_prompt(prompt)
59 .interact()
60 .context("failed to read password")
61 }
62
63 fn password_with_confirm(&self, prompt: &str) -> Result<String> {
64 if let Some(pp) = test_env_fallback("NEX_TEST_PASSPHRASE") {
65 return Ok(pp);
66 }
67 dialoguer::Password::new()
68 .with_prompt(prompt)
69 .with_confirmation("Confirm", "Values do not match")
70 .interact()
71 .context("failed to read password")
72 }
73
74 fn confirm(&self, prompt: &str, default: bool) -> Result<bool> {
75 if let Some(val) = test_env_fallback("NEX_TEST_CONFIRM") {
76 return Ok(val == "y" || val == "yes" || val == "true");
77 }
78 if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
79 return Ok(default);
80 }
81 dialoguer::Confirm::new()
82 .with_prompt(prompt)
83 .default(default)
84 .interact()
85 .context("failed to read confirmation")
86 }
87
88 fn input_text(&self, prompt: &str, default: Option<&str>) -> Result<String> {
89 if let Some(val) = test_env_fallback("NEX_TEST_INPUT") {
90 return Ok(val);
91 }
92 let mut builder = dialoguer::Input::<String>::new().with_prompt(prompt);
93 if let Some(d) = default {
94 builder = builder.default(d.to_string());
95 }
96 builder.interact_text().context("failed to read input")
97 }
98
99 fn select(&self, prompt: &str, items: &[String], default: usize) -> Result<usize> {
100 if let Some(val) = test_env_fallback("NEX_TEST_SELECT") {
101 return Ok(val.parse().unwrap_or(default));
102 }
103 if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
104 return Ok(default);
105 }
106 dialoguer::Select::new()
107 .with_prompt(prompt)
108 .items(items)
109 .default(default)
110 .interact()
111 .context("failed to read selection")
112 }
113}
114
115#[cfg(test)]
117pub struct ScriptedInput {
118 responses: std::sync::Mutex<std::collections::VecDeque<String>>,
119}
120
121#[cfg(test)]
122impl ScriptedInput {
123 pub fn new(responses: Vec<&str>) -> Self {
124 Self {
125 responses: std::sync::Mutex::new(responses.into_iter().map(String::from).collect()),
126 }
127 }
128
129 fn next_response(&self) -> Result<String> {
130 self.responses
131 .lock()
132 .expect("scripted input lock")
133 .pop_front()
134 .context("ScriptedInput: no more responses queued")
135 }
136}
137
138#[cfg(test)]
139impl InputProvider for ScriptedInput {
140 fn password(&self, _prompt: &str) -> Result<String> {
141 self.next_response()
142 }
143
144 fn password_with_confirm(&self, _prompt: &str) -> Result<String> {
145 self.next_response()
146 }
147
148 fn confirm(&self, _prompt: &str, default: bool) -> Result<bool> {
149 match self.responses.lock().expect("lock").pop_front() {
150 Some(r) => Ok(r == "y" || r == "yes" || r == "true"),
151 None => Ok(default),
152 }
153 }
154
155 fn input_text(&self, _prompt: &str, default: Option<&str>) -> Result<String> {
156 match self.responses.lock().expect("lock").pop_front() {
157 Some(r) if !r.is_empty() => Ok(r),
158 _ => Ok(default.unwrap_or("").to_string()),
159 }
160 }
161
162 fn select(&self, _prompt: &str, _items: &[String], default: usize) -> Result<usize> {
163 match self.responses.lock().expect("lock").pop_front() {
164 Some(r) => Ok(r.parse().unwrap_or(default)),
165 None => Ok(default),
166 }
167 }
168}
169
170static INPUT: std::sync::OnceLock<Box<dyn InputProvider>> = std::sync::OnceLock::new();
172
173pub fn input() -> &'static dyn InputProvider {
175 INPUT.get_or_init(|| Box::new(TerminalInput)).as_ref()
176}
177
178#[cfg(test)]
180pub fn set_input(provider: Box<dyn InputProvider>) {
181 let _ = INPUT.set(provider);
182}