1use crate::terminal::capabilities::iterm2::ITerm2Protocol;
10use crate::terminal::capabilities::*;
11use std::fmt::{Display, Formatter};
12
13#[derive(Debug, Copy, Clone, Eq, PartialEq)]
15pub enum TerminalProgram {
16 Dumb,
18 Ansi,
20 ITerm2,
22 Terminology,
24 Kitty,
26 WezTerm,
28 VSCode,
30 Ghostty,
32 Alacritty,
34 Foot,
36 Konsole,
38 AppleTerminal,
40 Warp,
42 Rio,
44 Hyper,
46 Contour,
48 Mlterm,
50 WindowsTerminal,
52}
53
54impl Display for TerminalProgram {
55 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56 let name = match *self {
57 TerminalProgram::Dumb => "dumb",
58 TerminalProgram::Ansi => "ansi",
59 TerminalProgram::ITerm2 => "iTerm2",
60 TerminalProgram::Terminology => "Terminology",
61 TerminalProgram::Kitty => "kitty",
62 TerminalProgram::WezTerm => "WezTerm",
63 TerminalProgram::VSCode => "vscode",
64 TerminalProgram::Ghostty => "ghostty",
65 TerminalProgram::Alacritty => "Alacritty",
66 TerminalProgram::Foot => "foot",
67 TerminalProgram::Konsole => "Konsole",
68 TerminalProgram::AppleTerminal => "Apple Terminal",
69 TerminalProgram::Warp => "Warp",
70 TerminalProgram::Rio => "Rio",
71 TerminalProgram::Hyper => "Hyper",
72 TerminalProgram::Contour => "Contour",
73 TerminalProgram::Mlterm => "mlterm",
74 TerminalProgram::WindowsTerminal => "Windows Terminal",
75 };
76 write!(f, "{name}")
77 }
78}
79
80fn get_term_program_major_minor_version() -> Option<(u16, u16)> {
85 let value = std::env::var("TERM_PROGRAM_VERSION").ok()?;
86 let mut parts = value.split('.').take(2);
87 let major = parts.next()?.parse().ok()?;
88 let minor = parts.next()?.parse().ok()?;
89 Some((major, minor))
90}
91
92impl TerminalProgram {
93 fn detect_term() -> Option<Self> {
94 let term = std::env::var("TERM").ok();
95 let t = term.as_deref()?;
96 match t {
97 "wezterm" => Some(Self::WezTerm),
98 "xterm-kitty" => Some(Self::Kitty),
99 "xterm-ghostty" => Some(Self::Ghostty),
100 "alacritty" | "xterm-alacritty" => Some(Self::Alacritty),
101 "foot" | "foot-extra" | "xterm-foot" => Some(Self::Foot),
102 "rio" | "xterm-rio" => Some(Self::Rio),
103 _ if t.starts_with("mlterm") => Some(Self::Mlterm),
104 _ => None,
105 }
106 }
107
108 fn detect_term_program() -> Option<Self> {
109 match std::env::var("TERM_PROGRAM").ok().as_deref() {
110 Some("WezTerm") => Some(Self::WezTerm),
111 Some("iTerm.app") => Some(Self::ITerm2),
112 Some("ghostty") => Some(Self::Ghostty),
113 Some("Apple_Terminal") => Some(Self::AppleTerminal),
114 Some("WarpTerminal") => Some(Self::Warp),
115 Some("Hyper") => Some(Self::Hyper),
116 Some("alacritty") => Some(Self::Alacritty),
117 Some("rio") => Some(Self::Rio),
118 Some("vscode")
119 if get_term_program_major_minor_version()
120 .is_some_and(|version| (1, 80) <= version) =>
121 {
122 Some(Self::VSCode)
123 }
124 _ => None,
125 }
126 }
127
128 fn detect_secondary_env() -> Option<Self> {
131 if std::env::var_os("WT_SESSION").is_some() {
132 return Some(Self::WindowsTerminal);
133 }
134 if std::env::var_os("KONSOLE_VERSION").is_some() {
135 return Some(Self::Konsole);
136 }
137 if let Ok(value) = std::env::var("TERMINAL_EMULATOR") {
138 if value.eq_ignore_ascii_case("contour") {
139 return Some(Self::Contour);
140 }
141 }
142 if matches!(std::env::var("TERMINOLOGY").ok().as_deref(), Some("1")) {
143 return Some(Self::Terminology);
144 }
145 None
146 }
147
148 pub fn detect() -> Self {
160 Self::detect_term()
161 .or_else(Self::detect_term_program)
162 .or_else(Self::detect_secondary_env)
163 .unwrap_or(Self::Ansi)
164 }
165
166 pub fn capabilities(self) -> TerminalCapabilities {
168 let ansi = TerminalCapabilities {
169 style: Some(StyleCapability::Ansi),
170 image: None,
171 marks: None,
172 };
173 let kitty = || ImageCapability::Kitty(self::kitty::KittyGraphicsProtocol);
174 let iterm2 = || ImageCapability::ITerm2(ITerm2Protocol);
175 #[cfg(feature = "sixel")]
176 let sixel = || ImageCapability::Sixel(self::sixel::SixelProtocol);
177 match self {
178 TerminalProgram::Dumb => TerminalCapabilities::default(),
179 TerminalProgram::Ansi
180 | TerminalProgram::Alacritty
181 | TerminalProgram::Konsole
182 | TerminalProgram::AppleTerminal
183 | TerminalProgram::Warp
184 | TerminalProgram::Hyper => ansi,
185 TerminalProgram::ITerm2 => ansi
186 .with_mark_capability(MarkCapability::ITerm2(ITerm2Protocol))
187 .with_image_capability(iterm2()),
188 TerminalProgram::VSCode => ansi.with_image_capability(iterm2()),
189 TerminalProgram::Terminology => {
190 ansi.with_image_capability(ImageCapability::Terminology(terminology::Terminology))
191 }
192 TerminalProgram::Kitty
193 | TerminalProgram::WezTerm
194 | TerminalProgram::Ghostty
195 | TerminalProgram::Rio => ansi.with_image_capability(kitty()),
196 TerminalProgram::Foot
199 | TerminalProgram::Contour
200 | TerminalProgram::Mlterm
201 | TerminalProgram::WindowsTerminal => {
202 #[cfg(feature = "sixel")]
203 {
204 ansi.with_image_capability(sixel())
205 }
206 #[cfg(not(feature = "sixel"))]
207 {
208 ansi
209 }
210 }
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use crate::terminal::TerminalProgram;
218
219 use temp_env::with_vars;
220
221 #[test]
222 pub fn detect_term_kitty() {
223 with_vars(vec![("TERM", Some("xterm-kitty"))], || {
224 assert_eq!(TerminalProgram::detect(), TerminalProgram::Kitty)
225 })
226 }
227
228 #[test]
229 pub fn detect_term_wezterm() {
230 with_vars(vec![("TERM", Some("wezterm"))], || {
231 assert_eq!(TerminalProgram::detect(), TerminalProgram::WezTerm)
232 })
233 }
234
235 #[test]
236 pub fn detect_term_program_wezterm() {
237 with_vars(
238 vec![
239 ("TERM", Some("xterm-256color")),
240 ("TERM_PROGRAM", Some("WezTerm")),
241 ],
242 || assert_eq!(TerminalProgram::detect(), TerminalProgram::WezTerm),
243 )
244 }
245
246 #[test]
247 pub fn detect_term_program_iterm2() {
248 with_vars(
249 vec![
250 ("TERM", Some("xterm-256color")),
251 ("TERM_PROGRAM", Some("iTerm.app")),
252 ],
253 || assert_eq!(TerminalProgram::detect(), TerminalProgram::ITerm2),
254 )
255 }
256
257 #[test]
258 pub fn detect_terminology() {
259 with_vars(
260 vec![
261 ("TERM", Some("xterm-256color")),
262 ("TERM_PROGRAM", None),
263 ("TERMINOLOGY", Some("1")),
264 ],
265 || assert_eq!(TerminalProgram::detect(), TerminalProgram::Terminology),
266 );
267 with_vars(
268 vec![
269 ("TERM", Some("xterm-256color")),
270 ("TERM_PROGRAM", None),
271 ("TERMINOLOGY", Some("0")),
272 ],
273 || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ansi),
274 );
275 }
276
277 #[test]
278 pub fn detect_term_ghostty() {
279 with_vars(vec![("TERM", Some("xterm-ghostty"))], || {
280 assert_eq!(TerminalProgram::detect(), TerminalProgram::Ghostty)
281 })
282 }
283
284 #[test]
285 pub fn detect_term_program_ghostty() {
286 with_vars(
287 vec![
288 ("TERM", Some("xterm-256color")),
289 ("TERM_PROGRAM", Some("ghostty")),
290 ],
291 || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ghostty),
292 )
293 }
294
295 #[test]
296 pub fn detect_ansi() {
297 with_vars(
298 vec![
299 ("TERM", Some("xterm-256color")),
300 ("TERM_PROGRAM", None),
301 ("TERMINOLOGY", None),
302 ],
303 || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ansi),
304 )
305 }
306
307 #[test]
309 #[allow(non_snake_case)]
310 pub fn GH_230_detect_nested_kitty_from_iterm2() {
311 with_vars(
312 vec![
313 ("TERM_PROGRAM", Some("iTerm.app")),
314 ("TERM", Some("xterm-kitty")),
315 ],
316 || assert_eq!(TerminalProgram::detect(), TerminalProgram::Kitty),
317 )
318 }
319
320 fn assert_detects(env: Vec<(&str, Option<&str>)>, expected: TerminalProgram) {
323 with_vars(env, || assert_eq!(TerminalProgram::detect(), expected));
324 }
325
326 #[test]
327 fn detect_alacritty_via_term() {
328 assert_detects(
329 vec![("TERM", Some("alacritty"))],
330 TerminalProgram::Alacritty,
331 );
332 }
333
334 #[test]
335 fn detect_alacritty_via_term_program() {
336 assert_detects(
337 vec![
338 ("TERM", Some("xterm-256color")),
339 ("TERM_PROGRAM", Some("alacritty")),
340 ],
341 TerminalProgram::Alacritty,
342 );
343 }
344
345 #[test]
346 fn detect_foot() {
347 assert_detects(vec![("TERM", Some("foot"))], TerminalProgram::Foot);
348 }
349
350 #[test]
351 fn detect_rio_via_term() {
352 assert_detects(vec![("TERM", Some("rio"))], TerminalProgram::Rio);
353 }
354
355 #[test]
356 fn detect_rio_via_term_program() {
357 assert_detects(
358 vec![
359 ("TERM", Some("xterm-256color")),
360 ("TERM_PROGRAM", Some("rio")),
361 ],
362 TerminalProgram::Rio,
363 );
364 }
365
366 #[test]
367 fn detect_mlterm() {
368 assert_detects(vec![("TERM", Some("mlterm"))], TerminalProgram::Mlterm);
369 }
370
371 #[test]
372 fn detect_warp() {
373 assert_detects(
374 vec![
375 ("TERM", Some("xterm-256color")),
376 ("TERM_PROGRAM", Some("WarpTerminal")),
377 ],
378 TerminalProgram::Warp,
379 );
380 }
381
382 #[test]
383 fn detect_hyper() {
384 assert_detects(
385 vec![
386 ("TERM", Some("xterm-256color")),
387 ("TERM_PROGRAM", Some("Hyper")),
388 ],
389 TerminalProgram::Hyper,
390 );
391 }
392
393 #[test]
394 fn detect_apple_terminal() {
395 assert_detects(
396 vec![
397 ("TERM", Some("xterm-256color")),
398 ("TERM_PROGRAM", Some("Apple_Terminal")),
399 ],
400 TerminalProgram::AppleTerminal,
401 );
402 }
403
404 #[test]
405 fn detect_windows_terminal() {
406 assert_detects(
407 vec![
408 ("TERM", Some("xterm-256color")),
409 ("TERM_PROGRAM", None),
410 ("WT_SESSION", Some("abc-123")),
411 ],
412 TerminalProgram::WindowsTerminal,
413 );
414 }
415
416 #[test]
417 fn detect_konsole() {
418 assert_detects(
419 vec![
420 ("TERM", Some("xterm-256color")),
421 ("TERM_PROGRAM", None),
422 ("KONSOLE_VERSION", Some("240100")),
423 ],
424 TerminalProgram::Konsole,
425 );
426 }
427
428 #[test]
429 fn detect_contour() {
430 assert_detects(
431 vec![
432 ("TERM", Some("xterm-256color")),
433 ("TERM_PROGRAM", None),
434 ("TERMINAL_EMULATOR", Some("contour")),
435 ],
436 TerminalProgram::Contour,
437 );
438 }
439}