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
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 #[inline]
153 fn set_var(key: &str, value: &str) {
154 test_env_overrides::set(key, Some(value));
155 }
156 #[inline]
157 fn remove_var(key: &str) {
158 test_env_overrides::set(key, None);
159 }
160 #[inline]
161 fn clear_var(key: &str) {
162 test_env_overrides::clear(key);
163 }
164
165 #[test]
166 fn test_supports_unicode_box_drawing() {
167 let original_term = env::var("TERM").ok();
171 let original_lang = env::var("LANG").ok();
172 let original_lc_all = env::var("LC_ALL").ok();
173 let original_lc_ctype = env::var("LC_CTYPE").ok();
174 let original_no_unicode = env::var("VTCODE_NO_UNICODE").ok();
175
176 set_var("VTCODE_NO_UNICODE", "1");
178 assert!(!supports_unicode_box_drawing());
179 remove_var("VTCODE_NO_UNICODE");
180
181 set_var("TERM", "xterm-256color");
183 assert!(supports_unicode_box_drawing());
184
185 set_var("LANG", "en_US.UTF-8");
187 assert!(supports_unicode_box_drawing());
188
189 set_var("TERM", "dumb");
191 assert!(!supports_unicode_box_drawing());
192
193 remove_var("TERM");
195 remove_var("LANG");
196 remove_var("LC_ALL");
197 remove_var("LC_CTYPE");
198 assert!(!supports_unicode_box_drawing());
199
200 match original_term {
202 Some(val) => set_var("TERM", &val),
203 None => clear_var("TERM"),
204 }
205 match original_lang {
206 Some(val) => set_var("LANG", &val),
207 None => clear_var("LANG"),
208 }
209 match original_lc_all {
210 Some(val) => set_var("LC_ALL", &val),
211 None => clear_var("LC_ALL"),
212 }
213 match original_lc_ctype {
214 Some(val) => set_var("LC_CTYPE", &val),
215 None => clear_var("LC_CTYPE"),
216 }
217 match original_no_unicode {
218 Some(val) => set_var("VTCODE_NO_UNICODE", &val),
219 None => clear_var("VTCODE_NO_UNICODE"),
220 }
221 }
222
223 #[test]
224 fn test_get_border_type() {
225 let original_term = env::var("TERM").ok();
227
228 set_var("TERM", "xterm-256color");
230 let border_type = get_border_type();
231 assert!(matches!(border_type, ratatui::widgets::BorderType::Rounded));
232
233 set_var("TERM", "dumb");
235 let border_type = get_border_type();
236 assert!(matches!(border_type, ratatui::widgets::BorderType::Plain));
237
238 match original_term {
240 Some(val) => set_var("TERM", &val),
241 None => clear_var("TERM"),
242 }
243 }
244
245 #[test]
246 fn queued_input_edit_binding_switches_for_tmux() {
247 let original_tmux = env::var("TMUX").ok();
248 let original_term = env::var("TERM").ok();
249
250 remove_var("TMUX");
251 set_var("TERM", "xterm-256color");
252 assert!(!queued_input_edit_uses_shift_left());
253
254 set_var("TMUX", "/tmp/tmux-1000/default,123,0");
255 assert!(queued_input_edit_uses_shift_left());
256
257 remove_var("TMUX");
258 set_var("TERM", "tmux-256color");
259 assert!(queued_input_edit_uses_shift_left());
260
261 match original_tmux {
262 Some(val) => set_var("TMUX", &val),
263 None => clear_var("TMUX"),
264 }
265 match original_term {
266 Some(val) => set_var("TERM", &val),
267 None => clear_var("TERM"),
268 }
269 }
270}