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    prompt_yes_no(prompt, false)
7}
8
9/// Like [`confirm`], but a bare Enter - or EOF, i.e. a non-interactive run -
10/// counts as yes. For prompts whose safe default is to proceed.
11pub fn confirm_default_yes(prompt: &str) -> Result<bool> {
12    prompt_yes_no(prompt, true)
13}
14
15/// Read a yes/no answer. Unrecognized input re-prompts rather than silently
16/// taking the default - so a stray line (type-ahead buffered while a provider
17/// CLI was still printing) is not misread as the answer to a destructive
18/// prompt. A bare Enter takes `default_yes`; EOF (a non-interactive run with
19/// nothing left to read) also takes it, so piped and scripted callers work
20/// unchanged.
21fn prompt_yes_no(prompt: &str, default_yes: bool) -> Result<bool> {
22    loop {
23        print!("{prompt}");
24        io::stdout().flush().context("failed to flush stdout")?;
25
26        let mut answer = String::new();
27        let read = io::stdin()
28            .lock()
29            .read_line(&mut answer)
30            .context("failed to read confirmation")?;
31        if read == 0 {
32            // EOF: nothing left to read, so take the default.
33            return Ok(default_yes);
34        }
35
36        match answer.trim().to_ascii_lowercase().as_str() {
37            "y" | "yes" => return Ok(true),
38            "n" | "no" => return Ok(false),
39            "" => return Ok(default_yes),
40            _ => anstream::eprintln!("please answer y or n"),
41        }
42    }
43}
44
45/// Number the options and read a 1-based choice from stdin. EOF or input
46/// that is not a valid number picks nothing, so non-interactive callers
47/// fall through to their error path.
48pub fn pick(title: &str, options: &[String]) -> Result<Option<usize>> {
49    anstream::eprintln!("{title}");
50    for (index, option) in options.iter().enumerate() {
51        let number = crate::style::paint(crate::style::DIM, &format!("{}.", index + 1));
52        anstream::eprintln!("  {number} {option}");
53    }
54    eprint!("pick [1-{}]: ", options.len());
55    io::stderr().flush().context("failed to flush stderr")?;
56
57    let mut answer = String::new();
58    io::stdin()
59        .lock()
60        .read_line(&mut answer)
61        .context("failed to read choice")?;
62
63    Ok(answer
64        .trim()
65        .parse::<usize>()
66        .ok()
67        .filter(|choice| (1..=options.len()).contains(choice))
68        .map(|choice| choice - 1))
69}