Skip to main content

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}