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