vtcode_ui/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>>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
15
16 pub(super) fn get(key: &str) -> Option<Option<String>> {
17 OVERRIDES.lock().unwrap_or_else(|e| e.into_inner()).get(key).cloned()
18 }
19
20 pub(super) fn set(key: &str, value: Option<&str>) {
21 if let Ok(mut map) = OVERRIDES.lock() {
22 map.insert(key.to_string(), value.map(ToString::to_string));
23 }
24 }
25
26 pub(super) fn clear(key: &str) {
27 if let Ok(mut map) = OVERRIDES.lock() {
28 map.remove(key);
29 }
30 }
31}
32
33fn read_env_var(key: &str) -> Option<String> {
34 #[cfg(test)]
35 if let Some(override_value) = test_env_overrides::get(key) {
36 return override_value;
37 }
38
39 env::var(key).ok()
40}
41
42#[cfg(test)]
43pub(crate) fn set_test_env_override(key: &str, value: Option<&str>) {
44 test_env_overrides::set(key, value);
45}
46
47#[cfg(test)]
48pub(crate) fn clear_test_env_override(key: &str) {
49 test_env_overrides::clear(key);
50}
51
52fn supports_unicode_box_drawing() -> bool {
58 if read_env_var("VTCODE_NO_UNICODE").is_some() {
60 return false;
61 }
62
63 if let Some(term) = read_env_var("TERM") {
65 let term_lower = term.to_lowercase();
66
67 if term_lower.contains("unicode")
69 || term_lower.contains("utf")
70 || term_lower.contains("xterm-256color")
71 || term_lower.contains("screen-256color")
72 || term_lower.contains("tmux-256color")
73 || term_lower.contains("alacritty")
74 || term_lower.contains("wezterm")
75 || term_lower.contains("kitty")
76 || term_lower.contains("iterm")
77 || term_lower.contains("hyper")
78 {
79 return true;
80 }
81
82 if term_lower.contains("dumb")
84 || term_lower.contains("basic")
85 || term_lower == "xterm"
86 || term_lower == "screen"
87 {
88 return false;
89 }
90 }
91
92 if let Some(lang) = read_env_var("LANG")
94 && (lang.to_lowercase().contains("utf-8") || lang.to_lowercase().contains("utf8"))
95 {
96 return true;
97 }
98
99 for var in &["LC_ALL", "LC_CTYPE"] {
101 if let Some(locale) = read_env_var(var)
102 && (locale.to_lowercase().contains("utf-8") || locale.to_lowercase().contains("utf8"))
103 {
104 return true;
105 }
106 }
107
108 false
110}
111
112fn supports_rich_unicode_box_drawing() -> bool {
119 if read_env_var("VTCODE_NO_RICH_UNICODE").is_some() {
121 return false;
122 }
123
124 if !supports_unicode_box_drawing() {
126 return false;
127 }
128
129 if let Some(term) = read_env_var("TERM") {
131 let term_lower = term.to_lowercase();
132 return term_lower.contains("alacritty")
133 || term_lower.contains("wezterm")
134 || term_lower.contains("kitty")
135 || term_lower.contains("iterm")
136 || term_lower.contains("hyper")
137 || term_lower.contains("ghostty");
138 }
139
140 false
141}
142
143pub(crate) fn get_border_type() -> ratatui::widgets::BorderType {
149 if supports_rich_unicode_box_drawing() {
150 ratatui::widgets::BorderType::HeavyDoubleDashed
151 } else if supports_unicode_box_drawing() {
152 ratatui::widgets::BorderType::Rounded
153 } else {
154 ratatui::widgets::BorderType::Plain
155 }
156}
157
158pub(crate) fn queued_input_edit_uses_shift_left() -> bool {
159 if read_env_var("TMUX").is_some() {
160 return true;
161 }
162
163 read_env_var("TERM")
164 .map(|term| term.to_lowercase().contains("tmux"))
165 .unwrap_or(false)
166}
167
168pub(crate) fn queued_input_edit_hint() -> &'static str {
169 if queued_input_edit_uses_shift_left() {
170 if cfg!(target_os = "macos") {
171 "⇧ + ← edit"
172 } else {
173 "Shift + ← edit"
174 }
175 } else if cfg!(target_os = "macos") {
176 "⌥ + ↑ edit"
177 } else {
178 "Alt + ↑ edit"
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::sync::{LazyLock, Mutex};
186
187 static TERMINAL_ENV_TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
188
189 #[inline]
190 fn set_var(key: &str, value: &str) {
191 test_env_overrides::set(key, Some(value));
192 }
193 #[inline]
194 fn remove_var(key: &str) {
195 test_env_overrides::set(key, None);
196 }
197 #[inline]
198 fn clear_var(key: &str) {
199 test_env_overrides::clear(key);
200 }
201
202 #[test]
203 fn test_supports_unicode_box_drawing() {
204 let _guard = TERMINAL_ENV_TEST_LOCK.lock().expect("terminal env test lock");
205 for key in ["TERM", "LANG", "LC_ALL", "LC_CTYPE", "VTCODE_NO_UNICODE"] {
206 clear_var(key);
207 }
208
209 set_var("VTCODE_NO_UNICODE", "1");
211 assert!(!supports_unicode_box_drawing());
212 remove_var("VTCODE_NO_UNICODE");
213
214 set_var("TERM", "xterm-256color");
216 assert!(supports_unicode_box_drawing());
217
218 set_var("LANG", "en_US.UTF-8");
220 assert!(supports_unicode_box_drawing());
221
222 set_var("TERM", "dumb");
224 assert!(!supports_unicode_box_drawing());
225
226 remove_var("TERM");
228 remove_var("LANG");
229 remove_var("LC_ALL");
230 remove_var("LC_CTYPE");
231 assert!(!supports_unicode_box_drawing());
232
233 for key in ["TERM", "LANG", "LC_ALL", "LC_CTYPE", "VTCODE_NO_UNICODE"] {
234 clear_var(key);
235 }
236 }
237
238 #[test]
239 fn test_get_border_type() {
240 let _guard = TERMINAL_ENV_TEST_LOCK.lock().expect("terminal env test lock");
241 clear_var("TERM");
242 clear_var("VTCODE_NO_RICH_UNICODE");
243
244 set_var("TERM", "alacritty");
246 let border_type = get_border_type();
247 assert!(matches!(border_type, ratatui::widgets::BorderType::HeavyDoubleDashed));
248
249 set_var("TERM", "xterm-256color");
251 clear_var("VTCODE_NO_RICH_UNICODE");
252 let border_type = get_border_type();
253 assert!(matches!(border_type, ratatui::widgets::BorderType::Rounded));
254
255 set_var("TERM", "dumb");
257 let border_type = get_border_type();
258 assert!(matches!(border_type, ratatui::widgets::BorderType::Plain));
259
260 set_var("TERM", "alacritty");
262 set_var("VTCODE_NO_RICH_UNICODE", "1");
263 let border_type = get_border_type();
264 assert!(matches!(border_type, ratatui::widgets::BorderType::Rounded));
265
266 clear_var("TERM");
267 clear_var("VTCODE_NO_RICH_UNICODE");
268 }
269
270 #[test]
271 fn queued_input_edit_binding_switches_for_tmux() {
272 let _guard = TERMINAL_ENV_TEST_LOCK.lock().expect("terminal env test lock");
273 remove_var("TMUX");
276 remove_var("TERM");
277 set_var("TERM", "xterm-256color");
278 assert!(!queued_input_edit_uses_shift_left());
279
280 set_var("TMUX", "/tmp/tmux-1000/default,123,0");
281 assert!(queued_input_edit_uses_shift_left());
282
283 remove_var("TMUX");
284 set_var("TERM", "tmux-256color");
285 assert!(queued_input_edit_uses_shift_left());
286
287 clear_var("TMUX");
288 clear_var("TERM");
289 }
290}