Skip to main content

git_cli/
prompt.rs

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}