Skip to main content

vtcode_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/// Detects if the current terminal supports Unicode box drawing characters
44///
45/// This function checks various environment variables and terminal settings
46/// to determine if Unicode characters can be safely displayed without
47/// appearing as broken ANSI sequences.
48pub fn supports_unicode_box_drawing() -> bool {
49    // Check if explicitly disabled via environment variable
50    if read_env_var("VTCODE_NO_UNICODE").is_some() {
51        return false;
52    }
53
54    // Check terminal type - many terminals support Unicode
55    if let Some(term) = read_env_var("TERM") {
56        let term_lower = term.to_lowercase();
57
58        // Modern terminals that definitely support Unicode
59        if term_lower.contains("unicode")
60            || term_lower.contains("utf")
61            || term_lower.contains("xterm-256color")
62            || term_lower.contains("screen-256color")
63            || term_lower.contains("tmux-256color")
64            || term_lower.contains("alacritty")
65            || term_lower.contains("wezterm")
66            || term_lower.contains("kitty")
67            || term_lower.contains("iterm")
68            || term_lower.contains("hyper")
69        {
70            return true;
71        }
72
73        // Older or basic terminal types that likely don't support Unicode well
74        if term_lower.contains("dumb")
75            || term_lower.contains("basic")
76            || term_lower == "xterm"
77            || term_lower == "screen"
78        {
79            return false;
80        }
81    }
82
83    // Check LANG environment variable for UTF-8 locale
84    if let Some(lang) = read_env_var("LANG")
85        && (lang.to_lowercase().contains("utf-8") || lang.to_lowercase().contains("utf8"))
86    {
87        return true;
88    }
89
90    // Check LC_ALL and LC_CTYPE for UTF-8
91    for var in &["LC_ALL", "LC_CTYPE"] {
92        if let Some(locale) = read_env_var(var)
93            && (locale.to_lowercase().contains("utf-8") || locale.to_lowercase().contains("utf8"))
94        {
95            return true;
96        }
97    }
98
99    // Default to plain ASCII for safety - prevents broken Unicode display
100    false
101}
102
103/// Gets the appropriate border type based on terminal capabilities
104///
105/// Returns `BorderType::Rounded` if Unicode is supported, otherwise
106/// returns `BorderType::Plain` for maximum compatibility.
107pub fn get_border_type() -> ratatui::widgets::BorderType {
108    if supports_unicode_box_drawing() {
109        ratatui::widgets::BorderType::Rounded
110    } else {
111        ratatui::widgets::BorderType::Plain
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    #[inline]
119    fn set_var(key: &str, value: &str) {
120        test_env_overrides::set(key, Some(value));
121    }
122    #[inline]
123    fn remove_var(key: &str) {
124        test_env_overrides::set(key, None);
125    }
126    #[inline]
127    fn clear_var(key: &str) {
128        test_env_overrides::clear(key);
129    }
130
131    #[test]
132    fn test_supports_unicode_box_drawing() {
133        // Test with different environment variable combinations
134
135        // Save original values
136        let original_term = env::var("TERM").ok();
137        let original_lang = env::var("LANG").ok();
138        let original_lc_all = env::var("LC_ALL").ok();
139        let original_lc_ctype = env::var("LC_CTYPE").ok();
140        let original_no_unicode = env::var("VTCODE_NO_UNICODE").ok();
141
142        // Test with VTCODE_NO_UNICODE set (should disable Unicode)
143        set_var("VTCODE_NO_UNICODE", "1");
144        assert!(!supports_unicode_box_drawing());
145        remove_var("VTCODE_NO_UNICODE");
146
147        // Test with modern terminal
148        set_var("TERM", "xterm-256color");
149        assert!(supports_unicode_box_drawing());
150
151        // Test with UTF-8 locale
152        set_var("LANG", "en_US.UTF-8");
153        assert!(supports_unicode_box_drawing());
154
155        // Test with basic terminal
156        set_var("TERM", "dumb");
157        assert!(!supports_unicode_box_drawing());
158
159        // Test with no locale info (should default to false for safety)
160        remove_var("TERM");
161        remove_var("LANG");
162        remove_var("LC_ALL");
163        remove_var("LC_CTYPE");
164        assert!(!supports_unicode_box_drawing());
165
166        // Restore original values
167        match original_term {
168            Some(val) => set_var("TERM", &val),
169            None => clear_var("TERM"),
170        }
171        match original_lang {
172            Some(val) => set_var("LANG", &val),
173            None => clear_var("LANG"),
174        }
175        match original_lc_all {
176            Some(val) => set_var("LC_ALL", &val),
177            None => clear_var("LC_ALL"),
178        }
179        match original_lc_ctype {
180            Some(val) => set_var("LC_CTYPE", &val),
181            None => clear_var("LC_CTYPE"),
182        }
183        match original_no_unicode {
184            Some(val) => set_var("VTCODE_NO_UNICODE", &val),
185            None => clear_var("VTCODE_NO_UNICODE"),
186        }
187    }
188
189    #[test]
190    fn test_get_border_type() {
191        // Save original TERM
192        let original_term = env::var("TERM").ok();
193
194        // Test with Unicode-supporting terminal
195        set_var("TERM", "xterm-256color");
196        let border_type = get_border_type();
197        assert!(matches!(border_type, ratatui::widgets::BorderType::Rounded));
198
199        // Test with basic terminal
200        set_var("TERM", "dumb");
201        let border_type = get_border_type();
202        assert!(matches!(border_type, ratatui::widgets::BorderType::Plain));
203
204        // Restore original TERM
205        match original_term {
206            Some(val) => set_var("TERM", &val),
207            None => clear_var("TERM"),
208        }
209    }
210}