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