pulldown_cmark_mdcat/terminal/
detect.rs1use 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,
27 Terminology,
31 Kitty,
38 WezTerm,
45 VSCode,
49 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
71fn 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 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 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 #[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}