1use std::io::{self, BufRead, Write};
2
3#[derive(Debug, thiserror::Error)]
4pub enum PromptError {
5 #[error("aborted")]
6 Aborted,
7 #[error(transparent)]
8 Io(#[from] io::Error),
9}
10
11pub fn confirm(prompt: &str) -> Result<bool, PromptError> {
12 let stdin = io::stdin();
13 let mut input = stdin.lock();
14 let mut output = io::stdout();
15 confirm_with_io(prompt, &mut input, &mut output)
16}
17
18pub fn confirm_with_io(
19 prompt: &str,
20 input: &mut impl BufRead,
21 output: &mut impl Write,
22) -> Result<bool, PromptError> {
23 write!(output, "{prompt}")?;
24 output.flush()?;
25
26 let mut line = String::new();
27 input.read_line(&mut line)?;
28 let trimmed = line.trim_end_matches(['\n', '\r']);
29 Ok(matches!(trimmed, "y" | "Y"))
30}
31
32pub fn confirm_or_abort(prompt: &str) -> Result<(), PromptError> {
33 let stdin = io::stdin();
34 let mut input = stdin.lock();
35 let mut output = io::stdout();
36 confirm_or_abort_with_io(prompt, &mut input, &mut output)
37}
38
39pub fn confirm_or_abort_with_io(
40 prompt: &str,
41 input: &mut impl BufRead,
42 output: &mut impl Write,
43) -> Result<(), PromptError> {
44 if confirm_with_io(prompt, input, output)? {
45 return Ok(());
46 }
47
48 writeln!(output, "🚫 Aborted")?;
49 Err(PromptError::Aborted)
50}
51
52pub fn select_menu_with_io(
53 prompt: &str,
54 valid_choices: &[&str],
55 default_choice: &str,
56 input: &mut impl BufRead,
57 output: &mut impl Write,
58) -> Result<String, PromptError> {
59 write!(output, "{prompt}")?;
60 output.flush()?;
61
62 let mut line = String::new();
63 input.read_line(&mut line)?;
64 let trimmed = line.trim_end_matches(['\n', '\r']).trim();
65 let choice = if trimmed.is_empty() {
66 default_choice
67 } else {
68 trimmed
69 };
70
71 if valid_choices.contains(&choice) {
72 return Ok(choice.to_string());
73 }
74
75 writeln!(output, "🚫 Aborted")?;
76 Err(PromptError::Aborted)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use pretty_assertions::{assert_eq, assert_ne};
83 use std::io::Cursor;
84
85 #[test]
86 fn confirm_accepts_only_y_or_uppercase_y() {
87 let mut out: Vec<u8> = Vec::new();
88
89 let mut input = Cursor::new("y\n");
90 assert_eq!(confirm_with_io("?", &mut input, &mut out).unwrap(), true);
91
92 let mut input = Cursor::new("Y\n");
93 assert_eq!(confirm_with_io("?", &mut input, &mut out).unwrap(), true);
94
95 let mut input = Cursor::new("yes\n");
96 assert_eq!(confirm_with_io("?", &mut input, &mut out).unwrap(), false);
97
98 let mut input = Cursor::new("\n");
99 assert_eq!(confirm_with_io("?", &mut input, &mut out).unwrap(), false);
100 }
101
102 #[test]
103 fn confirm_or_abort_prints_aborted_and_errors_on_decline() {
104 let mut out: Vec<u8> = Vec::new();
105 let mut input = Cursor::new("n\n");
106
107 let err = confirm_or_abort_with_io("prompt ", &mut input, &mut out).unwrap_err();
108 assert_ne!(out.len(), 0);
109 assert_eq!(String::from_utf8_lossy(&out), "prompt 🚫 Aborted\n");
110 assert!(matches!(err, PromptError::Aborted));
111 }
112
113 #[test]
114 fn select_menu_defaults_and_aborts_on_invalid() {
115 let mut out: Vec<u8> = Vec::new();
116
117 let mut input = Cursor::new("\n");
118 let v = select_menu_with_io("choose ", &["1", "2"], "2", &mut input, &mut out).unwrap();
119 assert_eq!(v, "2");
120
121 let mut out: Vec<u8> = Vec::new();
122 let mut input = Cursor::new("nope\n");
123 let err =
124 select_menu_with_io("choose ", &["1", "2"], "2", &mut input, &mut out).unwrap_err();
125 assert!(matches!(err, PromptError::Aborted));
126 assert_eq!(String::from_utf8_lossy(&out), "choose 🚫 Aborted\n");
127 }
128}