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>>>> =
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
53pub fn supports_unicode_box_drawing() -> bool {
59 if read_env_var("VTCODE_NO_UNICODE").is_some() {
61 return false;
62 }
63
64 if let Some(term) = read_env_var("TERM") {
66 let term_lower = term.to_lowercase();
67
68 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 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 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 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 false
111}
112
113pub 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 set_var("VTCODE_NO_UNICODE", "1");
180 assert!(!supports_unicode_box_drawing());
181 remove_var("VTCODE_NO_UNICODE");
182
183 set_var("TERM", "xterm-256color");
185 assert!(supports_unicode_box_drawing());
186
187 set_var("LANG", "en_US.UTF-8");
189 assert!(supports_unicode_box_drawing());
190
191 set_var("TERM", "dumb");
193 assert!(!supports_unicode_box_drawing());
194
195 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 set_var("TERM", "xterm-256color");
216 let border_type = get_border_type();
217 assert!(matches!(border_type, ratatui::widgets::BorderType::Rounded));
218
219 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}