rskit_cli/prompt/mode.rs
1//! How a [`Prompter`](crate::prompt::Prompter) sources answers.
2
3use std::io::{self, IsTerminal};
4
5/// How the prompter sources answers, resolved once up front.
6///
7/// The decision models *interactive stdio*: prompts are read from stdin but rendered to stderr,
8/// so a session is only interactive when **both** streams are terminals.
9/// If stderr is redirected (`cmd 2>log`) the user would never see the question,
10/// so the mode falls back to [`NonInteractive`] even when stdin is a TTY.
11///
12/// [`NonInteractive`]: PromptMode::NonInteractive
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum PromptMode {
16 /// Interactive stdio (both stdin and stderr are terminals): render prompts and read live,
17 /// typed answers.
18 Interactive,
19 /// Non-interactive stdio (CI, piped, or a redirected prompt sink): never block;
20 /// resolve each question to its declared default.
21 NonInteractive,
22}
23
24impl PromptMode {
25 /// Resolve the mode from the current process's stdin and stderr TTY status.
26 #[must_use]
27 pub fn from_env() -> Self {
28 Self::from_stdio(io::stdin().is_terminal(), io::stderr().is_terminal())
29 }
30
31 /// Resolve the mode from already-known stream TTY statuses (env-free, testable).
32 ///
33 /// Interactive only when both the input (stdin) and the prompt sink (stderr) are terminals,
34 /// so a redirected sink never leaves the user blocked behind an invisible prompt.
35 #[must_use]
36 pub const fn from_stdio(stdin_is_tty: bool, stderr_is_tty: bool) -> Self {
37 if stdin_is_tty && stderr_is_tty {
38 Self::Interactive
39 } else {
40 Self::NonInteractive
41 }
42 }
43
44 /// Whether this mode reads live answers.
45 #[must_use]
46 pub const fn is_interactive(self) -> bool {
47 matches!(self, Self::Interactive)
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::PromptMode;
54
55 #[test]
56 fn interactive_requires_both_stdin_and_stderr_terminals() {
57 assert_eq!(PromptMode::from_stdio(true, true), PromptMode::Interactive);
58 assert_eq!(
59 PromptMode::from_stdio(true, false),
60 PromptMode::NonInteractive
61 );
62 assert_eq!(
63 PromptMode::from_stdio(false, true),
64 PromptMode::NonInteractive
65 );
66 assert_eq!(
67 PromptMode::from_stdio(false, false),
68 PromptMode::NonInteractive
69 );
70 assert!(PromptMode::Interactive.is_interactive());
71 assert!(!PromptMode::NonInteractive.is_interactive());
72 }
73
74 #[test]
75 fn from_env_resolves_against_the_process_stdio() {
76 // Under a test harness stdin/stderr are not TTYs, so the mode resolves to NonInteractive;
77 // the assertion also proves `from_env` is callable.
78 assert_eq!(PromptMode::from_env(), PromptMode::NonInteractive);
79 }
80}