atomcode_tuix/
terminal.rs1use std::io::IsTerminal;
3
4#[derive(Default)]
11pub struct EnvView {
12 pub is_stdout_tty: bool,
13 pub no_color: bool,
14 pub term: Option<String>,
15 pub colorterm: Option<String>,
16 pub force_ascii: bool,
21 pub force_unicode: bool,
26 pub lang: Option<String>,
27 pub lc_all: Option<String>,
28 pub is_windows: bool,
32 pub wt_session: Option<String>,
35 pub term_program: Option<String>,
39}
40
41impl EnvView {
42 pub fn probe() -> Self {
43 Self {
44 is_stdout_tty: std::io::stdout().is_terminal(),
45 no_color: std::env::var("NO_COLOR").is_ok(),
46 term: std::env::var("TERM").ok(),
47 colorterm: std::env::var("COLORTERM").ok(),
48 force_ascii: std::env::var("ATOMCODE_ASCII").is_ok(),
49 force_unicode: std::env::var("ATOMCODE_UNICODE").is_ok(),
50 lang: std::env::var("LANG").ok(),
51 lc_all: std::env::var("LC_ALL").ok(),
52 is_windows: cfg!(target_os = "windows"),
53 wt_session: std::env::var("WT_SESSION").ok(),
54 term_program: std::env::var("TERM_PROGRAM").ok(),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy)]
60pub struct TerminalCaps {
61 pub tty: bool,
63 pub colors: bool,
65 pub spinner: bool,
67 pub bracketed_paste: bool,
69 pub raw_mode: bool,
71 pub scroll_region: bool,
77 pub unicode_symbols: bool,
85}
86
87impl TerminalCaps {
88 pub fn from_env(env: EnvView) -> Self {
89 let is_dumb = env.term.as_deref() == Some("dumb");
90 let tty = env.is_stdout_tty;
91
92 let locale = env.lc_all.as_deref().or(env.lang.as_deref()).unwrap_or("");
97 let ascii_locale = matches!(locale, "C" | "POSIX" | "ANSI_X3.4-1968");
98
99 let on_modern_emulator = env.wt_session.is_some() || env.term_program.is_some();
113 let windows_legacy_console = env.is_windows && !on_modern_emulator;
114
115 let unicode_symbols = if env.force_unicode {
116 true
117 } else {
118 !env.force_ascii && !is_dumb && !ascii_locale && !windows_legacy_console
119 };
120
121 Self {
122 tty,
123 colors: tty && !env.no_color && !is_dumb,
124 spinner: tty && !is_dumb,
125 bracketed_paste: tty && !is_dumb,
126 raw_mode: tty && !is_dumb,
127 scroll_region: tty && !is_dumb,
128 unicode_symbols,
129 }
130 }
131
132 pub fn probe() -> Self {
133 Self::from_env(EnvView::probe())
134 }
135
136 pub fn prompt_chevron(&self) -> &'static str {
141 if self.unicode_symbols {
142 "\u{276f} "
143 } else {
144 "> "
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn env() -> EnvView {
158 EnvView {
159 is_stdout_tty: true,
160 no_color: false,
161 term: Some("xterm-256color".to_string()),
162 colorterm: Some("truecolor".to_string()),
163 force_ascii: false,
164 force_unicode: false,
165 lang: Some("en_US.UTF-8".to_string()),
166 lc_all: None,
167 is_windows: false,
168 wt_session: None,
169 term_program: None,
170 }
171 }
172
173 #[test]
174 fn no_color_env_disables_colors() {
175 let caps = TerminalCaps::from_env(EnvView {
176 no_color: true,
177 ..env()
178 });
179 assert!(!caps.colors);
180 assert!(caps.tty);
181 assert!(caps.spinner); }
183
184 #[test]
185 fn non_tty_forces_plain_mode() {
186 let caps = TerminalCaps::from_env(EnvView {
187 is_stdout_tty: false,
188 term: Some("xterm".to_string()),
189 colorterm: None,
190 ..env()
191 });
192 assert!(!caps.tty);
193 assert!(!caps.colors);
194 assert!(!caps.spinner);
195 assert!(!caps.bracketed_paste);
196 assert!(!caps.raw_mode);
197 }
198
199 #[test]
200 fn dumb_term_disables_spinner_and_colors() {
201 let caps = TerminalCaps::from_env(EnvView {
202 term: Some("dumb".to_string()),
203 colorterm: None,
204 ..env()
205 });
206 assert!(caps.tty);
207 assert!(!caps.colors);
208 assert!(!caps.spinner);
209 assert!(!caps.unicode_symbols, "dumb TERM forces ASCII fallback");
210 }
211
212 #[test]
213 fn atomcode_ascii_env_forces_ascii() {
214 let caps = TerminalCaps::from_env(EnvView {
215 force_ascii: true,
216 ..env()
217 });
218 assert!(!caps.unicode_symbols);
219 assert_eq!(caps.prompt_chevron(), "> ");
220 }
221
222 #[test]
223 fn c_locale_forces_ascii() {
224 let caps = TerminalCaps::from_env(EnvView {
225 colorterm: None,
226 lang: Some("C".to_string()),
227 ..env()
228 });
229 assert!(!caps.unicode_symbols, "LANG=C → ASCII fallback");
230 }
231
232 #[test]
233 fn lc_all_wins_over_lang() {
234 let caps = TerminalCaps::from_env(EnvView {
236 colorterm: None,
237 lc_all: Some("C".to_string()),
238 ..env()
239 });
240 assert!(!caps.unicode_symbols);
241 }
242
243 #[test]
244 fn utf8_locale_keeps_unicode() {
245 let caps = TerminalCaps::from_env(EnvView {
246 lang: Some("zh_CN.UTF-8".to_string()),
247 ..env()
248 });
249 assert!(caps.unicode_symbols);
250 assert_eq!(caps.prompt_chevron(), "\u{276f} ");
251 }
252
253 #[test]
254 fn tty_xterm_gets_everything() {
255 let caps = TerminalCaps::from_env(env());
256 assert!(caps.tty);
257 assert!(caps.colors);
258 assert!(caps.spinner);
259 assert!(caps.bracketed_paste);
260 assert!(caps.raw_mode);
261 assert!(caps.unicode_symbols);
262 }
263
264 #[test]
269 fn windows_legacy_console_falls_back_to_ascii() {
270 let caps = TerminalCaps::from_env(EnvView {
271 is_windows: true,
272 ..env()
273 });
274 assert!(
275 !caps.unicode_symbols,
276 "bare Windows (no WT_SESSION / TERM_PROGRAM) → ASCII fallback to avoid ▢ tofu"
277 );
278 assert_eq!(caps.prompt_chevron(), "> ");
279 }
280
281 #[test]
282 fn windows_terminal_keeps_unicode() {
283 let caps = TerminalCaps::from_env(EnvView {
284 is_windows: true,
285 wt_session: Some("00000000-0000-0000-0000-000000000000".to_string()),
286 ..env()
287 });
288 assert!(caps.unicode_symbols, "Windows Terminal has Cascadia Code");
289 }
290
291 #[test]
292 fn windows_vscode_keeps_unicode() {
293 let caps = TerminalCaps::from_env(EnvView {
294 is_windows: true,
295 term_program: Some("vscode".to_string()),
296 ..env()
297 });
298 assert!(caps.unicode_symbols, "VS Code's integrated terminal is fine");
299 }
300
301 #[test]
302 fn force_unicode_overrides_windows_fallback() {
303 let caps = TerminalCaps::from_env(EnvView {
305 is_windows: true,
306 force_unicode: true,
307 ..env()
308 });
309 assert!(caps.unicode_symbols);
310 }
311
312 #[test]
313 fn force_ascii_beats_force_unicode_when_both_set() {
314 let caps = TerminalCaps::from_env(EnvView {
317 force_ascii: true,
318 force_unicode: true,
319 ..env()
320 });
321 assert!(
322 caps.unicode_symbols,
323 "force_unicode currently wins — ATOMCODE_UNICODE is the explicit opt-in escape hatch"
324 );
325 }
329}