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)]
117#[allow(dead_code)]
118pub struct ScriptedInput {
119 responses: std::sync::Mutex<std::collections::VecDeque<String>>,
120}
121
122#[cfg(test)]
123#[allow(dead_code)]
124impl ScriptedInput {
125 pub fn new(responses: Vec<&str>) -> Self {
126 Self {
127 responses: std::sync::Mutex::new(responses.into_iter().map(String::from).collect()),
128 }
129 }
130
131 fn next_response(&self) -> Result<String> {
132 self.responses
133 .lock()
134 .expect("scripted input lock")
135 .pop_front()
136 .context("ScriptedInput: no more responses queued")
137 }
138}
139
140#[cfg(test)]
141impl InputProvider for ScriptedInput {
142 fn password(&self, _prompt: &str) -> Result<String> {
143 self.next_response()
144 }
145
146 fn password_with_confirm(&self, _prompt: &str) -> Result<String> {
147 self.next_response()
148 }
149
150 fn confirm(&self, _prompt: &str, default: bool) -> Result<bool> {
151 match self.responses.lock().expect("lock").pop_front() {
152 Some(r) => Ok(r == "y" || r == "yes" || r == "true"),
153 None => Ok(default),
154 }
155 }
156
157 fn input_text(&self, _prompt: &str, default: Option<&str>) -> Result<String> {
158 match self.responses.lock().expect("lock").pop_front() {
159 Some(r) if !r.is_empty() => Ok(r),
160 _ => Ok(default.unwrap_or("").to_string()),
161 }
162 }
163
164 fn select(&self, _prompt: &str, _items: &[String], default: usize) -> Result<usize> {
165 match self.responses.lock().expect("lock").pop_front() {
166 Some(r) => Ok(r.parse().unwrap_or(default)),
167 None => Ok(default),
168 }
169 }
170}
171
172static INPUT: std::sync::OnceLock<Box<dyn InputProvider>> = std::sync::OnceLock::new();
174
175pub fn input() -> &'static dyn InputProvider {
177 INPUT.get_or_init(|| Box::new(TerminalInput)).as_ref()
178}
179
180#[cfg(test)]
182#[allow(dead_code)]
183pub fn set_input(provider: Box<dyn InputProvider>) {
184 let _ = INPUT.set(provider);
185}