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 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}