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    /// Terminology.
28    ///
29    /// See <http://terminolo.gy> for more information.
30    Terminology,
31    /// Kitty.
32    ///
33    /// kitty is a fast, featureful, GPU-based terminal emulator with a lot of extensions to the
34    /// terminal protocol.
35    ///
36    /// See <https://sw.kovidgoyal.net/kitty/> for more information.
37    Kitty,
38    /// WezTerm
39    ///
40    /// WezTerm is a GPU-accelerated cross-platform terminal emulator and multiplexer.  It's highly
41    /// customizable and supports some terminal extensions.
42    ///
43    /// See <https://wezfurlong.org/wezterm/> for more information.
44    WezTerm,
45    /// The built-in terminal in VSCode.
46    VSCode,
47    /// Ghostty.
48    ///
49    /// See <https://mitchellh.com/ghostty> for more information.
50    Ghostty,
51}
52
53impl Display for TerminalProgram {
54    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
55        let name = match *self {
56            TerminalProgram::Dumb => "dumb",
57            TerminalProgram::Ansi => "ansi",
58            TerminalProgram::ITerm2 => "iTerm2",
59            TerminalProgram::Terminology => "Terminology",
60            TerminalProgram::Kitty => "kitty",
61            TerminalProgram::WezTerm => "WezTerm",
62            TerminalProgram::VSCode => "vscode",
63            TerminalProgram::Ghostty => "ghostty",
64        };
65        write!(f, "{name}")
66    }
67}
68
69impl TerminalProgram {
70    fn detect_term() -> Option<Self> {
71        match std::env::var("TERM").ok().as_deref() {
72            Some("wezterm") => Some(Self::WezTerm),
73            Some("xterm-kitty") => Some(Self::Kitty),
74            Some("xterm-ghostty") => Some(Self::Ghostty),
75            _ => None,
76        }
77    }
78
79    fn detect_term_program() -> Option<Self> {
80        match std::env::var("TERM_PROGRAM").ok().as_deref() {
81            Some("WezTerm") => Some(Self::WezTerm),
82            Some("iTerm.app") => Some(Self::ITerm2),
83            Some("ghostty") => Some(Self::Ghostty),
84            Some("vscode") => Some(Self::VSCode),
85            _ => None,
86        }
87    }
88
89    /// Attempt to detect the terminal program mdcat is running on.
90    ///
91    /// This function looks at various environment variables to identify the terminal program.
92    ///
93    /// It first looks at `$TERM` to determine the terminal program, then at `$TERM_PROGRAM`.
94    ///
95    /// If `$TERM` is set to anything other than `xterm-256colors` it's definitely accurate, since
96    /// it points to the terminfo entry to use.  `$TERM` also propagates across most boundaries
97    /// (e.g. `sudo`, `ssh`), and thus the most reliable place to check.
98    ///
99    /// However, `$TERM` only works if the terminal has a dedicated entry in terminfo database. Many
100    /// terminal programs avoid this complexity (even WezTerm only sets `$TERM` if explicitly
101    /// configured to do so), so `mdcat` proceeds to look at other variables.  However, these are
102    /// generally not as reliable as `$TERM`, because they often do not propagate across SSH or
103    /// sudo, and may leak if one terminal program is started from another one.
104    ///
105    /// # Returns
106    ///
107    /// - [`TerminalProgram::Kitty`] if `$TERM` is `xterm-kitty`.
108    /// - [`TerminalProgram::WezTerm`] if `$TERM` is `wezterm`.
109    /// - [`TerminalProgram::WezTerm`] if `$TERM_PROGRAM` is `WezTerm`.
110    /// - [`TerminalProgram::ITerm2`] if `$TERM_PROGRAM` is `iTerm.app`.
111    /// - [`TerminalProgram::Ghostty`] if `$TERM` is `xterm-ghostty`.
112    /// - [`TerminalProgram::Ghostty`] if `$TERM_PROGRAM` is `ghostty`.
113    /// - [`TerminalProgram::Ansi`] otherwise.
114    pub fn detect() -> Self {
115        Self::detect_term()
116            .or_else(Self::detect_term_program)
117            .unwrap_or(Self::Ansi)
118    }
119
120    /// Get the capabilities of this terminal emulator.
121    pub fn capabilities(self) -> TerminalCapabilities {
122        let ansi = TerminalCapabilities {
123            style: Some(StyleCapability::Ansi),
124            image: None,
125            marks: None,
126        };
127        match self {
128            TerminalProgram::Dumb => TerminalCapabilities::default(),
129            TerminalProgram::Ansi => ansi,
130            TerminalProgram::ITerm2 => ansi
131                .with_mark_capability(MarkCapability::ITerm2(ITerm2Protocol))
132                .with_image_capability(ImageCapability::ITerm2(ITerm2Protocol)),
133            TerminalProgram::Terminology => ansi,
134            TerminalProgram::Kitty => ansi
135                .with_image_capability(ImageCapability::Kitty(self::kitty::KittyGraphicsProtocol)),
136            TerminalProgram::WezTerm => ansi
137                .with_image_capability(ImageCapability::Kitty(self::kitty::KittyGraphicsProtocol)),
138            TerminalProgram::VSCode => ansi,
139            TerminalProgram::Ghostty => ansi
140                .with_image_capability(ImageCapability::Kitty(self::kitty::KittyGraphicsProtocol)),
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use crate::terminal::TerminalProgram;
148
149    use temp_env::with_vars;
150
151    #[test]
152    pub fn detect_term_kitty() {
153        with_vars(vec![("TERM", Some("xterm-kitty"))], || {
154            assert_eq!(TerminalProgram::detect(), TerminalProgram::Kitty)
155        })
156    }
157
158    #[test]
159    pub fn detect_term_wezterm() {
160        with_vars(vec![("TERM", Some("wezterm"))], || {
161            assert_eq!(TerminalProgram::detect(), TerminalProgram::WezTerm)
162        })
163    }
164
165    #[test]
166    pub fn detect_term_program_wezterm() {
167        with_vars(
168            vec![
169                ("TERM", Some("xterm-256color")),
170                ("TERM_PROGRAM", Some("WezTerm")),
171            ],
172            || assert_eq!(TerminalProgram::detect(), TerminalProgram::WezTerm),
173        )
174    }
175
176    #[test]
177    pub fn detect_term_program_iterm2() {
178        with_vars(
179            vec![
180                ("TERM", Some("xterm-256color")),
181                ("TERM_PROGRAM", Some("iTerm.app")),
182            ],
183            || assert_eq!(TerminalProgram::detect(), TerminalProgram::ITerm2),
184        )
185    }
186
187    #[test]
188    pub fn does_not_detect_terminology() {
189        with_vars(
190            vec![
191                ("TERM", Some("xterm-256color")),
192                ("TERM_PROGRAM", None),
193                ("TERMINOLOGY", Some("1")),
194            ],
195            || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ansi),
196        );
197        with_vars(
198            vec![
199                ("TERM", Some("xterm-256color")),
200                ("TERM_PROGRAM", None),
201                ("TERMINOLOGY", Some("0")),
202            ],
203            || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ansi),
204        );
205    }
206
207    #[test]
208    pub fn detect_term_program_vscode() {
209        with_vars(
210            vec![
211                ("TERM", Some("xterm-256color")),
212                ("TERM_PROGRAM", Some("vscode")),
213            ],
214            || assert_eq!(TerminalProgram::detect(), TerminalProgram::VSCode),
215        )
216    }
217
218    #[test]
219    pub fn vscode_and_terminology_have_no_image_capability() {
220        assert!(TerminalProgram::VSCode.capabilities().image.is_none());
221        assert!(TerminalProgram::Terminology.capabilities().image.is_none());
222    }
223
224    #[test]
225    pub fn detect_term_ghostty() {
226        with_vars(vec![("TERM", Some("xterm-ghostty"))], || {
227            assert_eq!(TerminalProgram::detect(), TerminalProgram::Ghostty)
228        })
229    }
230
231    #[test]
232    pub fn detect_term_program_ghostty() {
233        with_vars(
234            vec![
235                ("TERM", Some("xterm-256color")),
236                ("TERM_PROGRAM", Some("ghostty")),
237            ],
238            || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ghostty),
239        )
240    }
241
242    #[test]
243    pub fn detect_ansi() {
244        with_vars(
245            vec![
246                ("TERM", Some("xterm-256color")),
247                ("TERM_PROGRAM", None),
248                ("TERMINOLOGY", None),
249            ],
250            || assert_eq!(TerminalProgram::detect(), TerminalProgram::Ansi),
251        )
252    }
253
254    /// Regression test for <https://github.com/swsnr/mdcat/issues/230>
255    #[test]
256    #[allow(non_snake_case)]
257    pub fn GH_230_detect_nested_kitty_from_iterm2() {
258        with_vars(
259            vec![
260                ("TERM_PROGRAM", Some("iTerm.app")),
261                ("TERM", Some("xterm-kitty")),
262            ],
263            || assert_eq!(TerminalProgram::detect(), TerminalProgram::Kitty),
264        )
265    }
266}