Skip to main content

vtcode_commons/
tty.rs

1//! TTY detection capability shared across VT Code crates.
2//!
3//! This module owns the canonical [`TtyExt`] extension trait for standard I/O
4//! streams. Both `vtcode-core` (`crate::utils::tty`) and `vtcode-ui`
5//! (`crate::tui::utils::tty`) re-export it so historical import paths keep
6//! resolving; extend the contract here instead of forking it per crate.
7//!
8//! # Example
9//!
10//! ```rust
11//! use std::io;
12//! use vtcode_commons::tty::TtyExt;
13//!
14//! if io::stdout().is_tty_ext() {
15//!     // Terminal-specific features available.
16//! }
17//! ```
18
19use std::io;
20use std::io::IsTerminal;
21
22use crate::color_policy::no_color_env_active;
23
24/// Extension trait for TTY detection on standard I/O streams.
25///
26/// This trait extends [`IsTerminal`] with convenient methods for checking TTY
27/// capabilities with consistent `NO_COLOR` / `FORCE_COLOR` handling.
28pub trait TtyExt {
29    /// Returns `true` if this stream is connected to a terminal.
30    fn is_tty_ext(&self) -> bool;
31
32    /// Returns `true` if this stream supports ANSI color codes.
33    ///
34    /// This checks both TTY status and the `NO_COLOR` / `FORCE_COLOR`
35    /// environment variables.
36    fn supports_color(&self) -> bool;
37
38    /// Returns `true` if this stream supports interactive features.
39    ///
40    /// Interactive features include cursor movement, color, and other
41    /// terminal capabilities that require a real terminal.
42    fn is_interactive(&self) -> bool;
43}
44
45impl TtyExt for io::Stdout {
46    fn is_tty_ext(&self) -> bool {
47        self.is_terminal()
48    }
49
50    fn supports_color(&self) -> bool {
51        if !self.is_terminal() {
52            return false;
53        }
54
55        // Check NO_COLOR with strict non-empty semantics.
56        if no_color_env_active() {
57            return false;
58        }
59
60        // Check for FORCE_COLOR environment variable
61        if std::env::var_os("FORCE_COLOR").is_some() {
62            return true;
63        }
64
65        true
66    }
67
68    fn is_interactive(&self) -> bool {
69        self.is_terminal() && self.supports_color()
70    }
71}
72
73impl TtyExt for io::Stderr {
74    fn is_tty_ext(&self) -> bool {
75        self.is_terminal()
76    }
77
78    fn supports_color(&self) -> bool {
79        if !self.is_terminal() {
80            return false;
81        }
82
83        // Check NO_COLOR with strict non-empty semantics.
84        if no_color_env_active() {
85            return false;
86        }
87
88        // Check for FORCE_COLOR environment variable
89        if std::env::var_os("FORCE_COLOR").is_some() {
90            return true;
91        }
92
93        true
94    }
95
96    fn is_interactive(&self) -> bool {
97        self.is_terminal() && self.supports_color()
98    }
99}
100
101impl TtyExt for io::Stdin {
102    fn is_tty_ext(&self) -> bool {
103        self.is_terminal()
104    }
105
106    fn supports_color(&self) -> bool {
107        // Stdin doesn't output color, but we check if it's interactive
108        self.is_terminal()
109    }
110
111    fn is_interactive(&self) -> bool {
112        self.is_terminal()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn interactive_is_tty_and_color_for_output_streams() {
122        // Absolute values depend on the environment (pty vs pipe), so assert
123        // the contract relationship instead: interactive == tty && color.
124        for (interactive, tty, color) in [
125            (io::stdout().is_interactive(), io::stdout().is_tty_ext(), io::stdout().supports_color()),
126            (io::stderr().is_interactive(), io::stderr().is_tty_ext(), io::stderr().supports_color()),
127        ] {
128            assert_eq!(interactive, tty && color);
129        }
130    }
131
132    #[test]
133    fn stdin_answers_are_pure_tty_checks() {
134        // Stdin never emits color: its answers track raw TTY status rather
135        // than consulting NO_COLOR / FORCE_COLOR like the output streams.
136        assert_eq!(io::stdin().supports_color(), io::stdin().is_tty_ext());
137        assert_eq!(io::stdin().is_interactive(), io::stdin().is_tty_ext());
138    }
139}