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}