Skip to main content

vtcode_tui/utils/
tty.rs

1//! TTY detection and capability utilities using crossterm's IsTty trait.
2//!
3//! This module provides safe and convenient TTY detection across the codebase,
4//! abstracting away platform differences for TTY detection.
5//!
6//! # Usage
7//!
8//! ```rust
9//! use vtcode_tui::utils::tty::TtyExt;
10//! use std::io;
11//!
12//! // Check if stdout is a TTY
13//! if io::stdout().is_tty_ext() {
14//!     // Apply terminal-specific features
15//! }
16//!
17//! // Check if stdin is a TTY
18//! if io::stdin().is_tty_ext() {
19//!     // Interactive input available
20//! }
21//! ```
22
23use crossterm::tty::IsTty;
24use std::io;
25use vtcode_commons::color_policy::no_color_env_active;
26
27/// Extension trait for TTY detection on standard I/O streams.
28///
29/// This trait extends crossterm's `IsTty` to provide convenient methods
30/// for checking TTY capabilities with better error handling.
31pub trait TtyExt {
32    /// Returns `true` if this stream is connected to a terminal.
33    ///
34    /// This is a convenience wrapper around crossterm's `IsTty` trait
35    /// that provides consistent behavior across the codebase.
36    fn is_tty_ext(&self) -> bool;
37
38    /// Returns `true` if this stream supports ANSI color codes.
39    ///
40    /// This checks both TTY status and common environment variables
41    /// that might disable color output.
42    fn supports_color(&self) -> bool;
43
44    /// Returns `true` if this stream supports interactive features.
45    ///
46    /// Interactive features include cursor movement, color, and other
47    /// terminal capabilities that require a real terminal.
48    fn is_interactive(&self) -> bool;
49}
50
51impl TtyExt for io::Stdout {
52    fn is_tty_ext(&self) -> bool {
53        self.is_tty()
54    }
55
56    fn supports_color(&self) -> bool {
57        if !self.is_tty() {
58            return false;
59        }
60
61        // Check NO_COLOR with strict non-empty semantics.
62        if no_color_env_active() {
63            return false;
64        }
65
66        // Check for FORCE_COLOR environment variable
67        if std::env::var_os("FORCE_COLOR").is_some() {
68            return true;
69        }
70
71        true
72    }
73
74    fn is_interactive(&self) -> bool {
75        self.is_tty() && self.supports_color()
76    }
77}
78
79impl TtyExt for io::Stderr {
80    fn is_tty_ext(&self) -> bool {
81        self.is_tty()
82    }
83
84    fn supports_color(&self) -> bool {
85        if !self.is_tty() {
86            return false;
87        }
88
89        // Check NO_COLOR with strict non-empty semantics.
90        if no_color_env_active() {
91            return false;
92        }
93
94        // Check for FORCE_COLOR environment variable
95        if std::env::var_os("FORCE_COLOR").is_some() {
96            return true;
97        }
98
99        true
100    }
101
102    fn is_interactive(&self) -> bool {
103        self.is_tty() && self.supports_color()
104    }
105}
106
107impl TtyExt for io::Stdin {
108    fn is_tty_ext(&self) -> bool {
109        self.is_tty()
110    }
111
112    fn supports_color(&self) -> bool {
113        // Stdin doesn't output color, but we check if it's interactive
114        self.is_tty()
115    }
116
117    fn is_interactive(&self) -> bool {
118        self.is_tty()
119    }
120}
121
122/// TTY capabilities that can be queried for feature detection.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct TtyCapabilities {
125    /// Whether the terminal supports ANSI color codes.
126    pub color: bool,
127    /// Whether the terminal supports cursor movement and manipulation.
128    pub cursor: bool,
129    /// Whether the terminal supports bracketed paste mode.
130    pub bracketed_paste: bool,
131    /// Whether the terminal supports focus change events.
132    pub focus_events: bool,
133    /// Whether the terminal supports mouse input.
134    pub mouse: bool,
135    /// Whether the terminal supports keyboard enhancement flags.
136    pub keyboard_enhancement: bool,
137}
138
139impl TtyCapabilities {
140    /// Detect the capabilities of the current terminal.
141    ///
142    /// This function queries the terminal to determine which features
143    /// are available. It should be called once at application startup
144    /// and the results cached for later use.
145    ///
146    /// # Returns
147    ///
148    /// Returns `Some(TtyCapabilities)` if stderr is a TTY, otherwise `None`.
149    pub fn detect() -> Option<Self> {
150        let stderr = io::stderr();
151        if !stderr.is_tty() {
152            return None;
153        }
154
155        Some(Self {
156            color: stderr.supports_color(),
157            cursor: true,               // All TTYs support basic cursor movement
158            bracketed_paste: true,      // Assume support, will fail gracefully if not
159            focus_events: true,         // Assume support, will fail gracefully if not
160            mouse: true,                // Assume support, will fail gracefully if not
161            keyboard_enhancement: true, // Assume support, will fail gracefully if not
162        })
163    }
164
165    /// Returns `true` if the terminal supports all advanced features.
166    pub fn is_fully_featured(&self) -> bool {
167        self.color
168            && self.cursor
169            && self.bracketed_paste
170            && self.focus_events
171            && self.mouse
172            && self.keyboard_enhancement
173    }
174
175    /// Returns `true` if the terminal supports basic TUI features.
176    pub fn is_basic_tui(&self) -> bool {
177        self.color && self.cursor
178    }
179}
180
181/// Check if the application is running in an interactive TTY context.
182///
183/// This is useful for deciding whether to use rich terminal features
184/// or fall back to plain text output.
185pub fn is_interactive_session() -> bool {
186    io::stderr().is_tty() && io::stdin().is_tty()
187}
188
189/// Get the current terminal dimensions.
190///
191/// Returns `Some((width, height))` if the terminal size can be determined,
192/// otherwise `None`.
193pub fn terminal_size() -> Option<(u16, u16)> {
194    crossterm::terminal::size().ok()
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_tty_detection() {
203        // These tests verify the TTY detection logic works
204        // Note: In actual test environments, these may vary
205        let stdout = io::stdout();
206        let stderr = io::stderr();
207        let stdin = io::stdin();
208
209        // Just verify the methods don't panic
210        let _ = stdout.is_tty();
211        let _ = stderr.is_tty();
212        let _ = stdin.is_tty();
213    }
214
215    #[test]
216    fn test_capabilities_detection() {
217        // Test that capability detection doesn't panic
218        let caps = TtyCapabilities::detect();
219        // In a test environment, this might be None
220        // Just verify the method works
221        let _ = caps.is_some() || caps.is_none();
222    }
223
224    #[test]
225    fn test_interactive_session() {
226        // Test interactive session detection
227        let _ = is_interactive_session();
228    }
229}