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}