Skip to main content

pulldown_cmark_mdcat/terminal/
detect.rs

1// Copyright 2018-2020 Sebastian Wiesner <sebastian@swsnr.de>
2
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! Detect the terminal application mdcat is running on.
8
9use crate::terminal::capabilities::iterm2::ITerm2Protocol;
10use crate::terminal::capabilities::*;
11use std::fmt::{Display, Formatter};
12
13/// A terminal application.
14#[derive(Debug, Copy, Clone, Eq, PartialEq)]
15pub enum TerminalProgram {
16    /// A dumb terminal which does not support any formatting.
17    Dumb,
18    /// A plain ANSI terminal which supports only standard ANSI formatting.
19    Ansi,
20    /// iTerm2.
21    ///
22    /// iTerm2 is a powerful macOS terminal emulator with many formatting features, including images
23    /// and inline links.
24    ///
25    /// See <https://www.iterm2.com> for more information.
26    ITerm2,
27    /// Kitty.
28    ///
29    /// kitty is a fast, featureful, GPU-based terminal emulator with a lot of extensions to the
30    /// terminal protocol.
31    ///
32    /// See <https://sw.kovidgoyal.net/kitty/> for more information.
33    Kitty,
34    /// WezTerm
35    ///
36    /// WezTerm is a GPU-accelerated cross-platform terminal emulator and multiplexer.  It's highly
37    /// customizable and supports some terminal extensions.
38    ///
39    /// See <https://wezfurlong.org/wezterm/> for more information.
40    WezTerm,
41    /// The built-in terminal in VSCode.
42    VSCode,
43    /// Ghostty.
44    ///
45    /// See <https://mitchellh.com/ghostty> for more information.
46    Ghostty,
47    /// foot.
48    ///
49    /// See <https://codeberg.org/dnkl/foot> for mor information.
50    Foot,
51    /// xterm.
52    ///
53    /// See <https://invisible-island.net/xterm/xterm.html> for mor information.
54    Xterm,
55}
56
57impl Display for TerminalProgram {
58    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
59        let name = match *self {
60            TerminalProgram::Dumb => "dumb",
61            TerminalProgram::Ansi => "ansi",
62            TerminalProgram::ITerm2 => "iTerm2",
63            TerminalProgram::Kitty => "kitty",
64            TerminalProgram::WezTerm => "WezTerm",
65            TerminalProgram::VSCode => "vscode",
66            TerminalProgram::Ghostty => "ghostty",
67            TerminalProgram::Foot => "foot",
68            TerminalProgram::Xterm => "xterm",
69        };
70        write!(f, "{name}")
71    }
72}
73
74impl TerminalProgram {
75    fn detect_term() -> Option<Self> {
76        if let Ok(v) = std::env::var("XTERM_VERSION") {
77            if !v.is_empty() {
78                return Some(Self::Xterm);
79            }
80        }
81
82        match std::env::var("TERM").ok().as_deref() {
83            Some("wezterm") => Some(Self::WezTerm),
84            Some("xterm-kitty") => Some(Self::Kitty),
85            Some("xterm-ghostty") => Some(Self::Ghostty),
86            Some("foot") => Some(Self::Foot),
87            _ => None,
88        }
89    }
90
91    fn detect_term_program() -> Option<Self> {
92        match std::env::var("TERM_PROGRAM").ok().as_deref() {
93            Some("WezTerm") => Some(Self::WezTerm),
94            Some("iTerm.app") => Some(Self::ITerm2),
95            Some("ghostty") => Some(Self::Ghostty),
96            Some("vscode") => Some(Self::VSCode),
97            _ => None,
98        }
99    }
100
101    /// Attempt to detect the terminal program mdcat is running on.
102    ///
103    /// This function looks at various environment variables to identify the terminal program.
104    ///
105    /// It first looks at `$TERM` to determine the terminal program, then at `$TERM_PROGRAM`.
106    ///
107    /// If `$TERM` is set to anything other than `xterm-256colors` it's definitely accurate, since
108    /// it points to the terminfo entry to use.  `$TERM` also propagates across most boundaries
109    /// (e.g. `sudo`, `ssh`), and thus the most reliable place to check.
110    ///
111    /// However, `$TERM` only works if the terminal has a dedicated entry in terminfo database. Many
112    /// terminal programs avoid this complexity (even WezTerm only sets `$TERM` if explicitly
113    /// configured to do so), so `mdcat` proceeds to look at other variables.  However, these are
114    /// generally not as reliable as `$TERM`, because they often do not propagate across SSH or
115    /// sudo, and may leak if one terminal program is started from another one.
116    ///
117    /// # Returns
118    ///
119    /// - [`TerminalProgram::Kitty`] if `$TERM` is `xterm-kitty`.
120    /// - [`TerminalProgram::WezTerm`] if `$TERM` is `wezterm`.
121    /// - [`TerminalProgram::WezTerm`] if `$TERM_PROGRAM` is `WezTerm`.
122    /// - [`TerminalProgram::ITerm2`] if `$TERM_PROGRAM` is `iTerm.app`.
123    /// - [`TerminalProgram::Ghostty`] if `$TERM` is `xterm-ghostty`.
124    /// - [`TerminalProgram::Ghostty`] if `$TERM_PROGRAM` is `ghostty`.
125    /// - [`TerminalProgram::Ansi`] otherwise.
126    pub fn detect() -> Self {
127        Self::detect_term()
128            .or_else(Self::detect_term_program)
129            .unwrap_or(Self::Ansi)
130    }
131
132    /// Get the capabilities of this terminal emulator.
133    pub fn capabilities(self) -> TerminalCapabilities {
134        let ansi = TerminalCapabilities {
135            style: Some(StyleCapability::Ansi),
136            image: None,
137            marks: None,
138        };
139        match self {
140            TerminalProgram::Dumb => TerminalCapabilities::default(),
141            TerminalProgram::Ansi => ansi,
142            TerminalProgram::ITerm2 => ansi
143                .with_mark_capability(MarkCapability::ITerm2(ITerm2Protocol))
144                .with_image_capability(ImageCapability::ITerm2(ITerm2Protocol)),
145            TerminalProgram::Kitty => ansi
146                .with_image_capability(ImageCapability::Kitty(self::kitty::KittyGraphicsProtocol)),
147            TerminalProgram::WezTerm => ansi
148                .with_image_capability(ImageCapability::Kitty(self::kitty::KittyGraphicsProtocol)),
149            TerminalProgram::VSCode => ansi,
150            TerminalProgram::Ghostty => ansi
151                .with_image_capability(ImageCapability::Kitty(self::kitty::KittyGraphicsProtocol)),
152            #[cfg(feature = "sixel")]
153            TerminalProgram::Foot => {
154                ansi.with_image_capability(ImageCapability::Sixel(self::sixel::SixelProtocol))
155            }
156            #[cfg(not(feature = "sixel"))]
157            TerminalProgram::Foot => ansi,
158            #[cfg(feature = "sixel")]
159            TerminalProgram::Xterm => {
160                ansi.with_image_capability(ImageCapability::Sixel(self::sixel::SixelProtocol))
161            }
162            #[cfg(not(feature = "sixel"))]
163            TerminalProgram::Xterm => ansi,
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use crate::terminal::TerminalProgram;
171
172    use temp_env::with_vars;
173
174    #[test]
175    pub fn detect_term_kitty() {
176        with_vars(vec![("TERM", Some("xterm-kitty"))], || {
177            assert_eq!(TerminalProgram::detect(), TerminalProgram::Kitty)
178        })
179    }
180
181    #[test]
182    pub fn detect_term_wezterm() {
183        with_vars(vec![("TERM", Some("wezterm"))], || {
184            assert_eq!(TerminalProgram::detect(), TerminalProgram::WezTerm)
185        })
186    }
187
188    #[test]
189    pub fn detect_term_program_wezterm() {
190        with_vars(
191            vec![
192                ("TERM", Some("xterm-256color")),
193                ("TERM_PROGRAM", Some("WezTerm")),
194            ],
195            || assert_eq!(TerminalProgram::detect(), TerminalProgram::WezTerm),
196        )
197    }
198
199    #[test]
200    pub fn detect_term_program_iterm2() {
201        with_vars(
202            vec![
203                ("TERM", Some("xterm-256color")),
204                ("TERM_PROGRAM", Some("iTerm.app")),
205            ],
206            || assert_eq!(TerminalProgram::detect(), TerminalProgram::ITerm2),
207        )
208    }
209
210    #[test]
211    pub fn detect_term_program_vscode() {
212        with_vars(
213            vec![
214                ("TERM", Some("xterm-256color")),
215                ("TERM_PROGRAM", Some("vscode")),
216            ],
217            || assert_eq!(TerminalProgram::detect(), TerminalProgram::VSCode),
218        )
219    }
220
221    #[test]
222    pub fn vscode_has_no_image_capability() {
223        assert!(TerminalProgram::VSCode.capabilities().image.is_none());
224    }
225
226    #[test]
227    pub fn detect_term_ghostty() {
228        with_vars(vec![("TERM", Some("xterm-ghostty"))], || {
229            assert_eq!(TerminalProgram::detect(), TerminalProgram::Ghostty)
230        })
231    }
232
233    #[test]
234    pub fn detect_term_program_ghostty() {
235        with_vars(
236            vec![
237                ("TERM", Some("xterm-256color")),
238                ("TERM_PROGRAM", Some("ghostty")),
239            ],
240            || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ghostty),
241        )
242    }
243
244    #[test]
245    pub fn detect_ansi() {
246        with_vars(
247            vec![("TERM", Some("xterm-256color")), ("TERM_PROGRAM", None)],
248            || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ansi),
249        )
250    }
251
252    /// Regression test for <https://github.com/swsnr/mdcat/issues/230>
253    #[test]
254    #[allow(non_snake_case)]
255    pub fn GH_230_detect_nested_kitty_from_iterm2() {
256        with_vars(
257            vec![
258                ("TERM_PROGRAM", Some("iTerm.app")),
259                ("TERM", Some("xterm-kitty")),
260            ],
261            || assert_eq!(TerminalProgram::detect(), TerminalProgram::Kitty),
262        )
263    }
264
265    #[test]
266    pub fn detect_term_foot() {
267        with_vars(vec![("TERM", Some("foot"))], || {
268            assert_eq!(TerminalProgram::detect(), TerminalProgram::Foot)
269        })
270    }
271
272    #[test]
273    pub fn detect_term_xterm() {
274        with_vars(vec![("XTERM_VERSION", Some("XTerm(410)"))], || {
275            assert_eq!(TerminalProgram::detect(), TerminalProgram::Xterm)
276        })
277    }
278}