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