Skip to main content

choir_cli/
prompt.rs

1//! The one place in this binary that may ask a person a question.
2//!
3//! Everything else here must complete without a terminal, because an
4//! agent driving this CLI has none and a prompt hangs it with no error
5//! and nothing in a log — that is the rule `no_tty_block.rs` asserts,
6//! and this module is its single named exemption.
7//!
8//! What earns the exemption is the shape of [`ask`], not the question
9//! it happens to carry. It never blocks: with no terminal on stdin it
10//! returns `None` immediately, and every caller is obliged to turn that
11//! into a refusal naming the flag that supplies the answer. So the
12//! branch a source scan is really looking for — "wait forever for input
13//! that is not coming" — does not exist here, and cannot be added
14//! without deleting the first line of the function.
15//!
16//! One question is asked today: the account name (D75). An invite that
17//! leaves the seat open is the ordinary kind, and that name is the one
18//! string about a person the op log can never withdraw, so defaulting it
19//! from `$USER` or the invite id would be picking it on their behalf and
20//! calling it a convenience.
21//!
22//! # Examples
23//!
24//! ```
25//! // Under `cargo test` stdin is not a terminal, so this never blocks.
26//! assert_eq!(choir_cli::prompt::ask("ignored"), None);
27//! ```
28
29use std::io::{IsTerminal, Write};
30
31/// Asks `question` on stderr and reads one non-empty line from stdin.
32///
33/// `None` when there is no terminal to ask, or when stdin reaches end of
34/// file. The question goes to stderr so that a caller's own answer stays
35/// the only thing on stdout.
36#[must_use]
37pub fn ask(question: &str) -> Option<String> {
38    if !std::io::stdin().is_terminal() {
39        return None;
40    }
41    loop {
42        eprint!("{question} ");
43        let _ = std::io::stderr().flush();
44        let mut line = String::new();
45        if std::io::stdin().read_line(&mut line).ok()? == 0 {
46            return None;
47        }
48        let answer = line.trim().to_string();
49        if !answer.is_empty() {
50            return Some(answer);
51        }
52    }
53}