vtcode_tui/core_tui/session/
terminal_capabilities.rs1use 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
43pub fn supports_unicode_box_drawing() -> bool {
49 if read_env_var("VTCODE_NO_UNICODE").is_some() {
51 return false;
52 }
53
54 if let Some(term) = read_env_var("TERM") {
56 let term_lower = term.to_lowercase();
57
58 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 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 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 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 false
101}
102
103pub 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 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 set_var("VTCODE_NO_UNICODE", "1");
144 assert!(!supports_unicode_box_drawing());
145 remove_var("VTCODE_NO_UNICODE");
146
147 set_var("TERM", "xterm-256color");
149 assert!(supports_unicode_box_drawing());
150
151 set_var("LANG", "en_US.UTF-8");
153 assert!(supports_unicode_box_drawing());
154
155 set_var("TERM", "dumb");
157 assert!(!supports_unicode_box_drawing());
158
159 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 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 let original_term = env::var("TERM").ok();
193
194 set_var("TERM", "xterm-256color");
196 let border_type = get_border_type();
197 assert!(matches!(border_type, ratatui::widgets::BorderType::Rounded));
198
199 set_var("TERM", "dumb");
201 let border_type = get_border_type();
202 assert!(matches!(border_type, ratatui::widgets::BorderType::Plain));
203
204 match original_term {
206 Some(val) => set_var("TERM", &val),
207 None => clear_var("TERM"),
208 }
209 }
210}