Skip to main content

git_stk/
prompt.rs

1use std::io::{self, BufRead, Write};
2
3use anyhow::{Context, Result};
4
5pub fn confirm(prompt: &str) -> Result<bool> {
6    print!("{prompt}");
7    io::stdout().flush().context("failed to flush stdout")?;
8
9    let mut answer = String::new();
10    io::stdin()
11        .lock()
12        .read_line(&mut answer)
13        .context("failed to read confirmation")?;
14
15    Ok(matches!(answer.trim(), "y" | "Y" | "yes" | "Yes" | "YES"))
16}
17
18/// Like [`confirm`], but a bare Enter - or EOF, i.e. a non-interactive run -
19/// counts as yes. For prompts whose safe default is to proceed.
20pub fn confirm_default_yes(prompt: &str) -> Result<bool> {
21    print!("{prompt}");
22    io::stdout().flush().context("failed to flush stdout")?;
23
24    let mut answer = String::new();
25    io::stdin()
26        .lock()
27        .read_line(&mut answer)
28        .context("failed to read confirmation")?;
29
30    Ok(matches!(
31        answer.trim().to_ascii_lowercase().as_str(),
32        "" | "y" | "yes"
33    ))
34}
35
36/// Number the options and read a 1-based choice from stdin. EOF or input
37/// that is not a valid number picks nothing, so non-interactive callers
38/// fall through to their error path.
39pub fn pick(title: &str, options: &[String]) -> Result<Option<usize>> {
40    anstream::eprintln!("{title}");
41    for (index, option) in options.iter().enumerate() {
42        let number = crate::style::paint(crate::style::DIM, &format!("{}.", index + 1));
43        anstream::eprintln!("  {number} {option}");
44    }
45    eprint!("pick [1-{}]: ", options.len());
46    io::stderr().flush().context("failed to flush stderr")?;
47
48    let mut answer = String::new();
49    io::stdin()
50        .lock()
51        .read_line(&mut answer)
52        .context("failed to read choice")?;
53
54    Ok(answer
55        .trim()
56        .parse::<usize>()
57        .ok()
58        .filter(|choice| (1..=options.len()).contains(choice))
59        .map(|choice| choice - 1))
60}