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}