1use anyhow::{Context, Result};
10use dialoguer::{Confirm, Input, MultiSelect, Select};
11
12use crate::interactive;
13
14pub fn input<S: Into<String>>(prompt: S, default: Option<&str>) -> Result<String> {
18 let prompt_str = prompt.into();
19
20 if interactive::is_non_interactive() {
21 return default.map(|s| s.to_string()).ok_or_else(|| {
22 anyhow::anyhow!(
23 "{} is required in non-interactive mode",
24 prompt_str.trim_end_matches(':')
25 )
26 });
27 }
28
29 let mut input: Input<String> = Input::new();
30 input = input.with_prompt(&prompt_str);
31
32 if let Some(d) = default {
33 input = input.default(d.to_string());
34 }
35
36 input
37 .interact_text()
38 .context("Failed to read input from terminal")
39}
40
41pub fn input_validated<S, V>(prompt: S, default: Option<&str>, validator: V) -> Result<String>
45where
46 S: Into<String>,
47 V: Fn(&String) -> Result<(), &'static str> + Clone,
48{
49 let prompt_str = prompt.into();
50
51 if interactive::is_non_interactive() {
52 return default.map(|s| s.to_string()).ok_or_else(|| {
53 anyhow::anyhow!(
54 "{} is required in non-interactive mode",
55 prompt_str.trim_end_matches(':')
56 )
57 });
58 }
59
60 let mut input: Input<String> = Input::new();
61 input = input.with_prompt(&prompt_str).validate_with(validator);
62
63 if let Some(d) = default {
64 input = input.default(d.to_string());
65 }
66
67 input
68 .interact_text()
69 .context("Failed to read input from terminal")
70}
71
72pub fn select<S: Into<String>>(prompt: S, items: &[String]) -> Result<usize> {
76 let prompt_str = prompt.into();
77
78 if interactive::is_non_interactive() {
79 anyhow::bail!(
80 "Selection required for '{}' but running in non-interactive mode",
81 prompt_str
82 );
83 }
84
85 Select::new()
86 .with_prompt(&prompt_str)
87 .items(items)
88 .default(0)
89 .interact()
90 .context("Failed to read selection from terminal")
91}
92
93pub fn select_with_default<S: Into<String>>(
97 prompt: S,
98 items: &[String],
99 default: usize,
100) -> Result<usize> {
101 if interactive::is_non_interactive() {
102 return Ok(default);
103 }
104
105 let prompt_str = prompt.into();
106
107 Select::new()
108 .with_prompt(&prompt_str)
109 .items(items)
110 .default(default)
111 .interact()
112 .context("Failed to read selection from terminal")
113}
114
115pub fn multi_select<S: Into<String>>(prompt: S, items: &[String]) -> Result<Vec<usize>> {
119 let prompt_str = prompt.into();
120
121 if interactive::is_non_interactive() {
122 anyhow::bail!(
123 "Multi-selection required for '{}' but running in non-interactive mode",
124 prompt_str
125 );
126 }
127
128 MultiSelect::new()
129 .with_prompt(&prompt_str)
130 .items(items)
131 .interact()
132 .context("Failed to read multi-selection from terminal")
133}
134
135pub fn confirm<S: Into<String>>(prompt: S, default: bool) -> Result<bool> {
140 if interactive::is_yes() {
142 return Ok(true);
143 }
144
145 if interactive::is_non_interactive() {
147 return Ok(false);
148 }
149
150 let prompt_str = prompt.into();
151
152 Confirm::new()
153 .with_prompt(&prompt_str)
154 .default(default)
155 .interact()
156 .context("Failed to read confirmation from terminal")
157}
158
159pub fn confirm_destructive<S: Into<String>>(prompt: S) -> Result<bool> {
165 if interactive::is_yes() {
167 return Ok(true);
168 }
169
170 if interactive::is_non_interactive() {
172 return Ok(false);
173 }
174
175 let prompt_str = prompt.into();
176
177 Confirm::new()
178 .with_prompt(&prompt_str)
179 .default(false) .interact()
181 .context("Failed to read confirmation from terminal")
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn reset_state() {
190 interactive::init(false, false);
191 }
192
193 fn set_non_interactive() {
194 interactive::init(true, false);
195 }
196
197 fn set_yes_mode() {
198 interactive::init(false, true);
199 }
200
201 fn set_non_interactive_with_yes() {
202 interactive::init(true, true);
203 }
204
205 #[test]
208 fn test_input_non_interactive_with_default() {
209 set_non_interactive();
210 let result = input("Enter name:", Some("default_value"));
211 assert!(result.is_ok());
212 assert_eq!(result.unwrap(), "default_value");
213 reset_state();
214 }
215
216 #[test]
217 fn test_input_non_interactive_without_default() {
218 set_non_interactive();
219 let result = input("Enter name:", None);
220 assert!(result.is_err());
221 let err = result.unwrap_err().to_string();
222 assert!(err.contains("required"));
223 assert!(err.contains("non-interactive"));
224 reset_state();
225 }
226
227 #[test]
228 fn test_input_validated_non_interactive_with_default() {
229 set_non_interactive();
230 let result = input_validated("Enter email:", Some("test@example.com"), |_| Ok(()));
231 assert!(result.is_ok());
232 assert_eq!(result.unwrap(), "test@example.com");
233 reset_state();
234 }
235
236 #[test]
237 fn test_input_validated_non_interactive_without_default() {
238 set_non_interactive();
239 let result = input_validated::<_, fn(&String) -> Result<(), &'static str>>(
240 "Enter email:",
241 None,
242 |_| Ok(()),
243 );
244 assert!(result.is_err());
245 reset_state();
246 }
247
248 #[test]
251 fn test_select_non_interactive_fails() {
252 set_non_interactive();
253 let items = vec!["Option 1".to_string(), "Option 2".to_string()];
254 let result = select("Choose:", &items);
255 assert!(result.is_err());
256 let err = result.unwrap_err().to_string();
257 assert!(err.contains("non-interactive"));
258 reset_state();
259 }
260
261 #[test]
262 fn test_select_with_default_non_interactive() {
263 set_non_interactive();
264 let items = vec!["Option 1".to_string(), "Option 2".to_string()];
265 let result = select_with_default("Choose:", &items, 1);
266 assert!(result.is_ok());
267 assert_eq!(result.unwrap(), 1);
268 reset_state();
269 }
270
271 #[test]
274 fn test_multi_select_non_interactive_fails() {
275 set_non_interactive();
276 let items = vec!["Option 1".to_string(), "Option 2".to_string()];
277 let result = multi_select("Choose multiple:", &items);
278 assert!(result.is_err());
279 let err = result.unwrap_err().to_string();
280 assert!(err.contains("non-interactive"));
281 reset_state();
282 }
283
284 #[test]
287 fn test_confirm_yes_mode() {
288 set_yes_mode();
289 let result = confirm("Proceed?", false);
290 assert!(result.is_ok());
291 assert!(result.unwrap()); reset_state();
293 }
294
295 #[test]
296 fn test_confirm_non_interactive_no_yes() {
297 set_non_interactive();
298 let result = confirm("Proceed?", true);
299 assert!(result.is_ok());
300 assert!(!result.unwrap()); reset_state();
302 }
303
304 #[test]
305 fn test_confirm_non_interactive_with_yes() {
306 set_non_interactive_with_yes();
307 let result = confirm("Proceed?", false);
308 assert!(result.is_ok());
309 assert!(result.unwrap()); reset_state();
311 }
312
313 #[test]
316 fn test_confirm_destructive_yes_mode() {
317 set_yes_mode();
318 let result = confirm_destructive("Delete all?");
319 assert!(result.is_ok());
320 assert!(result.unwrap());
321 reset_state();
322 }
323
324 #[test]
325 fn test_confirm_destructive_non_interactive_no_yes() {
326 set_non_interactive();
327 let result = confirm_destructive("Delete all?");
328 assert!(result.is_ok());
329 assert!(!result.unwrap()); reset_state();
331 }
332
333 #[test]
334 fn test_confirm_destructive_non_interactive_with_yes() {
335 set_non_interactive_with_yes();
336 let result = confirm_destructive("Delete all?");
337 assert!(result.is_ok());
338 assert!(result.unwrap());
339 reset_state();
340 }
341
342 #[test]
345 fn test_input_trims_colon_in_error() {
346 set_non_interactive();
347 let result = input("Enter name:", None);
348 let err = result.unwrap_err().to_string();
349 assert!(err.contains("Enter name"));
351 assert!(!err.contains("Enter name:"));
352 reset_state();
353 }
354}