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 std::collections::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_no_unicode = env::var("VTCODE_NO_UNICODE").ok();
140
141        // Test with VTCODE_NO_UNICODE set (should disable Unicode)
142        set_var("VTCODE_NO_UNICODE", "1");
143        assert!(!supports_unicode_box_drawing());
144        remove_var("VTCODE_NO_UNICODE");
145
146        // Test with modern terminal
147        set_var("TERM", "xterm-256color");
148        assert!(supports_unicode_box_drawing());
149
150        // Test with UTF-8 locale
151        set_var("LANG", "en_US.UTF-8");
152        assert!(supports_unicode_box_drawing());
153
154        // Test with basic terminal
155        set_var("TERM", "dumb");
156        assert!(!supports_unicode_box_drawing());
157
158        // Test with no locale info (should default to false for safety)
159        remove_var("TERM");
160        remove_var("LANG");
161        remove_var("LC_ALL");
162        assert!(!supports_unicode_box_drawing());
163
164        // Restore original values
165        match original_term {
166            Some(val) => set_var("TERM", &val),
167            None => clear_var("TERM"),
168        }
169        match original_lang {
170            Some(val) => set_var("LANG", &val),
171            None => clear_var("LANG"),
172        }
173        match original_lc_all {
174            Some(val) => set_var("LC_ALL", &val),
175            None => clear_var("LC_ALL"),
176        }
177        match original_no_unicode {
178            Some(val) => set_var("VTCODE_NO_UNICODE", &val),
179            None => clear_var("VTCODE_NO_UNICODE"),
180        }
181    }
182
183    #[test]
184    fn test_get_border_type() {
185        // Save original TERM
186        let original_term = env::var("TERM").ok();
187
188        // Test with Unicode-supporting terminal
189        set_var("TERM", "xterm-256color");
190        let border_type = get_border_type();
191        assert!(matches!(border_type, ratatui::widgets::BorderType::Rounded));
192
193        // Test with basic terminal
194        set_var("TERM", "dumb");
195        let border_type = get_border_type();
196        assert!(matches!(border_type, ratatui::widgets::BorderType::Plain));
197
198        // Restore original TERM
199        match original_term {
200            Some(val) => set_var("TERM", &val),
201            None => clear_var("TERM"),
202        }
203    }
204}