Skip to main content

degenbot_cli/
prompt.rs

1//! The stdin/stdout [`Prompter`] (ADR-051 D4/D2).
2//!
3//! cli-core owns the prompt *policy* as declared data (`PromptPlan`) and the
4//! *message text* each arm asks with; this module owns only the interaction:
5//! ask on stdout, read one line from stdin, and mirror `click.confirm`'s
6//! semantics - a bare Enter yields the default, anything that is not `y`/`yes`
7//! declines, and EOF declines (click raises `Abort` on a closed stdin, which
8//! the arm maps to [`CliError::Aborted`](degenbot_cli_core::CliError::Aborted)).
9//!
10//! No terminal control is used: the progress bar owns stderr, the prompt owns
11//! stdout, so the two never fight over the same draw surface (ADR-051 D9).
12
13use std::io::{BufRead, Write};
14
15use degenbot_cli_core::Prompter;
16
17/// The real stdin/stdout prompter.
18#[derive(Debug, Default, Clone, Copy)]
19pub struct ConsolePrompter;
20
21impl ConsolePrompter {
22    /// A prompter over the process's stdin/stdout.
23    #[must_use]
24    pub const fn new() -> Self {
25        Self
26    }
27}
28
29impl Prompter for ConsolePrompter {
30    fn confirm(&self, message: &str, default: bool) -> bool {
31        let stdin = std::io::stdin();
32        let mut reader = stdin.lock();
33        let stdout = std::io::stdout();
34        let mut writer = stdout.lock();
35        confirm_with(&mut reader, &mut writer, message, default)
36    }
37}
38
39/// Ask `message` on `writer` and read one line from `reader`.
40///
41/// `default` is what a bare Enter yields; a closed stdin (EOF) declines, the
42/// way `click.confirm` aborts on EOF. Split from [`ConsolePrompter`] so the
43/// interaction is unit-testable without touching the process streams.
44#[must_use]
45pub fn confirm_with<R: BufRead, W: Write>(
46    reader: &mut R,
47    writer: &mut W,
48    message: &str,
49    default: bool,
50) -> bool {
51    let suffix = if default { "[Y/n]" } else { "[y/N]" };
52    let _ = write!(writer, "{message} {suffix} ");
53    let _ = writer.flush();
54    let mut line = String::new();
55    match reader.read_line(&mut line) {
56        Ok(0) | Err(_) => false,
57        Ok(_) => match line.trim().to_ascii_lowercase().as_str() {
58            "" => default,
59            "y" | "yes" => true,
60            _ => false,
61        },
62    }
63}