Skip to main content

soma_cli_core/
terminal.rs

1//! Terminal capability detection and color policy.
2//!
3//! These helpers decide *whether* a stream should receive ANSI styling or
4//! interactive behavior (progress lines, prompts). They do not decide *what*
5//! to print — see [`crate::color`] for that.
6
7use std::io::IsTerminal;
8
9/// Explicit color policy a CLI can expose through a `--color` flag.
10///
11/// `Auto` defers to [`ColorMode::Auto`]'s terminal/`NO_COLOR` detection,
12/// `Always` forces styling on regardless of terminal state, and `Plain`
13/// forces styling off. This mirrors the common `--color=auto|always|never`
14/// convention used across CLIs (and the Aurora CLI token conventions, which
15/// name the "off" state `plain`).
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum ColorMode {
18    #[default]
19    Auto,
20    Always,
21    Plain,
22}
23
24impl ColorMode {
25    /// Parse a `--color` flag value. Accepts `auto`, `always`, and
26    /// `plain`/`never` (both spellings are common in the wild).
27    pub fn parse(value: &str) -> Option<Self> {
28        match value {
29            "auto" => Some(Self::Auto),
30            "always" => Some(Self::Always),
31            "plain" | "never" => Some(Self::Plain),
32            _ => None,
33        }
34    }
35}
36
37/// Resolve whether color should be enabled for a stream, given an explicit
38/// [`ColorMode`] policy and whether that stream is a TTY.
39///
40/// `Auto` also honors the `NO_COLOR` convention (<https://no-color.org>):
41/// any non-empty or empty `NO_COLOR` environment variable disables color.
42pub fn resolve_color(mode: ColorMode, stream_is_tty: bool) -> bool {
43    match mode {
44        ColorMode::Always => true,
45        ColorMode::Plain => false,
46        ColorMode::Auto => stream_is_tty && std::env::var_os("NO_COLOR").is_none(),
47    }
48}
49
50/// Whether stdin is attached to an interactive terminal.
51pub fn is_stdin_terminal() -> bool {
52    std::io::stdin().is_terminal()
53}
54
55/// Whether stdout is attached to an interactive terminal.
56pub fn is_stdout_terminal() -> bool {
57    std::io::stdout().is_terminal()
58}
59
60/// Whether stderr is attached to an interactive terminal.
61pub fn is_stderr_terminal() -> bool {
62    std::io::stderr().is_terminal()
63}
64
65/// Whether stderr output should be colorized under the `Auto` policy: a TTY
66/// and no `NO_COLOR` override.
67pub fn stderr_supports_color() -> bool {
68    resolve_color(ColorMode::Auto, is_stderr_terminal())
69}
70
71/// Whether stdout output should be colorized under the `Auto` policy: a TTY
72/// and no `NO_COLOR` override.
73pub fn stdout_supports_color() -> bool {
74    resolve_color(ColorMode::Auto, is_stdout_terminal())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn parse_accepts_known_values() {
83        assert_eq!(ColorMode::parse("auto"), Some(ColorMode::Auto));
84        assert_eq!(ColorMode::parse("always"), Some(ColorMode::Always));
85        assert_eq!(ColorMode::parse("plain"), Some(ColorMode::Plain));
86        assert_eq!(ColorMode::parse("never"), Some(ColorMode::Plain));
87        assert_eq!(ColorMode::parse("bogus"), None);
88    }
89
90    #[test]
91    fn always_and_plain_ignore_tty_state() {
92        assert!(resolve_color(ColorMode::Always, false));
93        assert!(!resolve_color(ColorMode::Plain, true));
94    }
95
96    #[test]
97    fn auto_requires_tty() {
98        assert!(!resolve_color(ColorMode::Auto, false));
99    }
100
101    #[test]
102    fn auto_respects_no_color_even_on_a_tty() {
103        // Save/restore so this test doesn't leak state into others sharing
104        // the process (NO_COLOR is process-global).
105        let previous = std::env::var_os("NO_COLOR");
106        std::env::set_var("NO_COLOR", "1");
107        let result = resolve_color(ColorMode::Auto, true);
108        match previous {
109            Some(value) => std::env::set_var("NO_COLOR", value),
110            None => std::env::remove_var("NO_COLOR"),
111        }
112        assert!(!result, "NO_COLOR should disable Auto color even on a tty");
113    }
114}