Skip to main content

vtcode_ui/tui/core_tui/session/
terminal_capabilities.rs

1//! Terminal capability detection for optimal rendering
2//!
3//! This module provides utilities to detect terminal capabilities such as
4//! Unicode support, color support, and other features to ensure optimal
5//! rendering across different terminal environments.
6
7use std::env;
8
9#[cfg(test)]
10mod test_env_overrides {
11    use hashbrown::HashMap;
12    use std::sync::{LazyLock, Mutex};
13
14    static OVERRIDES: LazyLock<Mutex<HashMap<String, Option<String>>>> =
15        LazyLock::new(|| Mutex::new(HashMap::new()));
16
17    pub(super) fn get(key: &str) -> Option<Option<String>> {
18        OVERRIDES.lock().ok().and_then(|map| map.get(key).cloned())
19    }
20
21    pub(super) fn set(key: &str, value: Option<&str>) {
22        if let Ok(mut map) = OVERRIDES.lock() {
23            map.insert(key.to_string(), value.map(ToString::to_string));
24        }
25    }
26
27    pub(super) fn clear(key: &str) {
28        if let Ok(mut map) = OVERRIDES.lock() {
29            map.remove(key);
30        }
31    }
32}
33
34fn read_env_var(key: &str) -> Option<String> {
35    #[cfg(test)]
36    if let Some(override_value) = test_env_overrides::get(key) {
37        return override_value;
38    }
39
40    env::var(key).ok()
41}
42
43#[cfg(test)]
44pub(crate) fn set_test_env_override(key: &str, value: Option<&str>) {
45    test_env_overrides::set(key, value);
46}
47
48#[cfg(test)]
49pub(crate) fn clear_test_env_override(key: &str) {
50    test_env_overrides::clear(key);
51}
52
53/// Detects if the current terminal supports Unicode box drawing characters
54///
55/// This function checks various environment variables and terminal settings
56/// to determine if Unicode characters can be safely displayed without
57/// appearing as broken ANSI sequences.
58pub fn supports_unicode_box_drawing() -> bool {
59    // Check if explicitly disabled via environment variable
60    if read_env_var("VTCODE_NO_UNICODE").is_some() {
61        return false;
62    }
63
64    // Check terminal type - many terminals support Unicode
65    if let Some(term) = read_env_var("TERM") {
66        let term_lower = term.to_lowercase();
67
68        // Modern terminals that definitely support Unicode
69        if term_lower.contains("unicode")
70            || term_lower.contains("utf")
71            || term_lower.contains("xterm-256color")
72            || term_lower.contains("screen-256color")
73            || term_lower.contains("tmux-256color")
74            || term_lower.contains("alacritty")
75            || term_lower.contains("wezterm")
76            || term_lower.contains("kitty")
77            || term_lower.contains("iterm")
78            || term_lower.contains("hyper")
79        {
80            return true;
81        }
82
83        // Older or basic terminal types that likely don't support Unicode well
84        if term_lower.contains("dumb")
85            || term_lower.contains("basic")
86            || term_lower == "xterm"
87            || term_lower == "screen"
88        {
89            return false;
90        }
91    }
92
93    // Check LANG environment variable for UTF-8 locale
94    if let Some(lang) = read_env_var("LANG")
95        && (lang.to_lowercase().contains("utf-8") || lang.to_lowercase().contains("utf8"))
96    {
97        return true;
98    }
99
100    // Check LC_ALL and LC_CTYPE for UTF-8
101    for var in &["LC_ALL", "LC_CTYPE"] {
102        if let Some(locale) = read_env_var(var)
103            && (locale.to_lowercase().contains("utf-8") || locale.to_lowercase().contains("utf8"))
104        {
105            return true;
106        }
107    }
108
109    // Default to plain ASCII for safety - prevents broken Unicode display
110    false
111}
112
113/// Gets the appropriate border type based on terminal capabilities
114///
115/// Returns `BorderType::Rounded` if Unicode is supported, otherwise
116/// returns `BorderType::Plain` for maximum compatibility.
117pub fn get_border_type() -> ratatui::widgets::BorderType {
118    if supports_unicode_box_drawing() {
119        ratatui::widgets::BorderType::Rounded
120    } else {
121        ratatui::widgets::BorderType::Plain
122    }
123}
124
125pub(crate) fn queued_input_edit_uses_shift_left() -> bool {
126    if read_env_var("TMUX").is_some() {
127        return true;
128    }
129
130    read_env_var("TERM")
131        .map(|term| term.to_lowercase().contains("tmux"))
132        .unwrap_or(false)
133}
134
135pub(crate) fn queued_input_edit_hint() -> &'static str {
136    if queued_input_edit_uses_shift_left() {
137        if cfg!(target_os = "macos") {
138            "⇧ + ← edit"
139        } else {
140            "Shift + ← edit"
141        }
142    } else if cfg!(target_os = "macos") {
143        "⌥ + ↑ edit"
144    } else {
145        "Alt + ↑ edit"
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::sync::{LazyLock, Mutex};
153
154    static TERMINAL_ENV_TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
155
156    #[inline]
157    fn set_var(key: &str, value: &str) {
158        test_env_overrides::set(key, Some(value));
159    }
160    #[inline]
161    fn remove_var(key: &str) {
162        test_env_overrides::set(key, None);
163    }
164    #[inline]
165    fn clear_var(key: &str) {
166        test_env_overrides::clear(key);
167    }
168
169    #[test]
170    fn test_supports_unicode_box_drawing() {
171        let _guard = TERMINAL_ENV_TEST_LOCK
172            .lock()
173            .expect("terminal env test lock");
174        for key in ["TERM", "LANG", "LC_ALL", "LC_CTYPE", "VTCODE_NO_UNICODE"] {
175            clear_var(key);
176        }
177
178        // Test with VTCODE_NO_UNICODE set (should disable Unicode)
179        set_var("VTCODE_NO_UNICODE", "1");
180        assert!(!supports_unicode_box_drawing());
181        remove_var("VTCODE_NO_UNICODE");
182
183        // Test with modern terminal
184        set_var("TERM", "xterm-256color");
185        assert!(supports_unicode_box_drawing());
186
187        // Test with UTF-8 locale
188        set_var("LANG", "en_US.UTF-8");
189        assert!(supports_unicode_box_drawing());
190
191        // Test with basic terminal
192        set_var("TERM", "dumb");
193        assert!(!supports_unicode_box_drawing());
194
195        // Test with no locale info (should default to false for safety)
196        remove_var("TERM");
197        remove_var("LANG");
198        remove_var("LC_ALL");
199        remove_var("LC_CTYPE");
200        assert!(!supports_unicode_box_drawing());
201
202        for key in ["TERM", "LANG", "LC_ALL", "LC_CTYPE", "VTCODE_NO_UNICODE"] {
203            clear_var(key);
204        }
205    }
206
207    #[test]
208    fn test_get_border_type() {
209        let _guard = TERMINAL_ENV_TEST_LOCK
210            .lock()
211            .expect("terminal env test lock");
212        clear_var("TERM");
213
214        // Test with Unicode-supporting terminal
215        set_var("TERM", "xterm-256color");
216        let border_type = get_border_type();
217        assert!(matches!(border_type, ratatui::widgets::BorderType::Rounded));
218
219        // Test with basic terminal
220        set_var("TERM", "dumb");
221        let border_type = get_border_type();
222        assert!(matches!(border_type, ratatui::widgets::BorderType::Plain));
223
224        clear_var("TERM");
225    }
226
227    #[test]
228    fn queued_input_edit_binding_switches_for_tmux() {
229        let _guard = TERMINAL_ENV_TEST_LOCK
230            .lock()
231            .expect("terminal env test lock");
232        clear_var("TMUX");
233        clear_var("TERM");
234        set_var("TERM", "xterm-256color");
235        assert!(!queued_input_edit_uses_shift_left());
236
237        set_var("TMUX", "/tmp/tmux-1000/default,123,0");
238        assert!(queued_input_edit_uses_shift_left());
239
240        remove_var("TMUX");
241        set_var("TERM", "tmux-256color");
242        assert!(queued_input_edit_uses_shift_left());
243
244        clear_var("TMUX");
245        clear_var("TERM");
246    }
247}