1use anyhow::{Context, Result};
4use std::env;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TerminalType {
10 Ghostty,
11 Kitty,
12 Alacritty,
13 WezTerm,
14 TerminalApp,
15 Xterm,
16 Zed,
17 Warp,
18 ITerm2,
19 VSCode,
20 WindowsTerminal,
21 Hyper,
22 Tabby,
23 Unknown,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum TerminalFeature {
29 Multiline,
30 CopyPaste,
31 ShellIntegration,
32 ThemeSync,
33 Notifications,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum TerminalSetupAvailability {
39 NativeSupport,
40 Offered,
41 GuidanceOnly,
42}
43
44impl TerminalType {
45 pub fn detect() -> Result<Self> {
47 if let Ok(term_program) = env::var("TERM_PROGRAM") {
48 let term_lower = term_program.to_lowercase();
49
50 if term_lower.contains("ghostty") {
51 return Ok(TerminalType::Ghostty);
52 } else if term_lower.contains("wezterm") {
53 return Ok(TerminalType::WezTerm);
54 } else if term_lower.contains("apple_terminal") {
55 return Ok(TerminalType::TerminalApp);
56 } else if term_lower.contains("iterm") {
57 return Ok(TerminalType::ITerm2);
58 } else if term_lower.contains("vscode") {
59 return Ok(TerminalType::VSCode);
60 } else if term_lower.contains("warp") {
61 return Ok(TerminalType::Warp);
62 } else if term_lower.contains("hyper") {
63 return Ok(TerminalType::Hyper);
64 } else if term_lower.contains("tabby") {
65 return Ok(TerminalType::Tabby);
66 }
67 }
68
69 if env::var("KITTY_WINDOW_ID").is_ok() || env::var("KITTY_PID").is_ok() {
70 return Ok(TerminalType::Kitty);
71 }
72
73 if env::var("ALACRITTY_SOCKET").is_ok() || env::var("ALACRITTY_LOG").is_ok() {
74 return Ok(TerminalType::Alacritty);
75 }
76
77 if env::var("ZED_TERMINAL").is_ok() {
78 return Ok(TerminalType::Zed);
79 }
80
81 if env::var("WT_SESSION").is_ok() || env::var("WT_PROFILE_ID").is_ok() {
82 return Ok(TerminalType::WindowsTerminal);
83 }
84
85 if let Ok(term) = env::var("TERM") {
86 let term_lower = term.to_lowercase();
87
88 if term_lower.contains("kitty") {
89 return Ok(TerminalType::Kitty);
90 } else if term_lower.contains("alacritty") {
91 return Ok(TerminalType::Alacritty);
92 } else if term_lower.contains("xterm") {
93 return Ok(TerminalType::Xterm);
94 }
95 }
96
97 Ok(TerminalType::Unknown)
98 }
99
100 pub fn supports_feature(&self, feature: TerminalFeature) -> bool {
102 match (self, feature) {
103 (TerminalType::Ghostty, _) => true,
104 (TerminalType::Kitty, _) => true,
105 (TerminalType::Alacritty, _) => true,
106 (TerminalType::WezTerm, _) => true,
107 (TerminalType::TerminalApp, TerminalFeature::Multiline) => true,
108 (TerminalType::TerminalApp, TerminalFeature::ShellIntegration) => true,
109 (TerminalType::TerminalApp, TerminalFeature::Notifications) => true,
110 (TerminalType::TerminalApp, _) => false,
111 (TerminalType::Xterm, TerminalFeature::Multiline) => true,
112 (TerminalType::Xterm, TerminalFeature::Notifications) => true,
113 (TerminalType::Xterm, _) => false,
114 (TerminalType::Zed, TerminalFeature::Multiline) => true,
115 (TerminalType::Zed, TerminalFeature::ThemeSync) => true,
116 (TerminalType::Zed, TerminalFeature::Notifications) => true,
117 (TerminalType::Zed, _) => false,
118 (TerminalType::Warp, TerminalFeature::Multiline) => true,
119 (TerminalType::Warp, TerminalFeature::Notifications) => true,
120 (TerminalType::Warp, _) => false,
121 (TerminalType::ITerm2, _) => true,
122 (TerminalType::VSCode, TerminalFeature::Multiline) => true,
123 (TerminalType::VSCode, TerminalFeature::Notifications) => true,
124 (TerminalType::VSCode, _) => false,
125 (TerminalType::WindowsTerminal, _) => true,
126 (TerminalType::Hyper, _) => true,
127 (TerminalType::Tabby, _) => true,
128 (TerminalType::Unknown, _) => false,
129 }
130 }
131
132 pub fn has_native_multiline_support(&self) -> bool {
134 matches!(
135 self,
136 TerminalType::Ghostty
137 | TerminalType::Kitty
138 | TerminalType::WezTerm
139 | TerminalType::ITerm2
140 | TerminalType::Warp
141 )
142 }
143
144 pub fn terminal_setup_availability(&self) -> TerminalSetupAvailability {
146 match self {
147 TerminalType::Ghostty
148 | TerminalType::Kitty
149 | TerminalType::WezTerm
150 | TerminalType::ITerm2
151 | TerminalType::Warp => TerminalSetupAvailability::NativeSupport,
152 TerminalType::Alacritty | TerminalType::Zed | TerminalType::VSCode => TerminalSetupAvailability::Offered,
153 TerminalType::TerminalApp
154 | TerminalType::Xterm
155 | TerminalType::WindowsTerminal
156 | TerminalType::Hyper
157 | TerminalType::Tabby
158 | TerminalType::Unknown => TerminalSetupAvailability::GuidanceOnly,
159 }
160 }
161
162 pub fn should_offer_terminal_setup(&self) -> bool {
164 matches!(self.terminal_setup_availability(), TerminalSetupAvailability::Offered)
165 }
166
167 pub fn config_path(&self) -> Result<PathBuf> {
169 let home_dir = dirs::home_dir().context("Failed to determine home directory")?;
170
171 let path = match self {
172 TerminalType::Ghostty => {
173 if cfg!(target_os = "windows") {
174 let appdata = env::var("APPDATA").context("APPDATA environment variable not set")?;
175 PathBuf::from(appdata).join("ghostty").join("config")
176 } else {
177 home_dir.join(".config").join("ghostty").join("config")
178 }
179 }
180 TerminalType::Kitty => {
181 if cfg!(target_os = "windows") {
182 let appdata = env::var("APPDATA").context("APPDATA environment variable not set")?;
183 PathBuf::from(appdata).join("kitty").join("kitty.conf")
184 } else {
185 home_dir.join(".config").join("kitty").join("kitty.conf")
186 }
187 }
188 TerminalType::Alacritty => {
189 if cfg!(target_os = "windows") {
190 let appdata = env::var("APPDATA").context("APPDATA environment variable not set")?;
191 PathBuf::from(appdata).join("alacritty").join("alacritty.toml")
192 } else {
193 home_dir.join(".config").join("alacritty").join("alacritty.toml")
194 }
195 }
196 TerminalType::WezTerm => home_dir.join(".wezterm.lua"),
197 TerminalType::TerminalApp => {
198 if cfg!(target_os = "macos") {
199 home_dir.join("Library").join("Preferences").join("com.apple.Terminal.plist")
200 } else {
201 anyhow::bail!("Terminal.app is only available on macOS")
202 }
203 }
204 TerminalType::Xterm => home_dir.join(".Xresources"),
205 TerminalType::Zed => {
206 if cfg!(target_os = "windows") {
207 let appdata = env::var("APPDATA").context("APPDATA environment variable not set")?;
208 PathBuf::from(appdata).join("Zed").join("settings.json")
209 } else if cfg!(target_os = "macos") {
210 home_dir
211 .join("Library")
212 .join("Application Support")
213 .join("Zed")
214 .join("settings.json")
215 } else {
216 home_dir.join(".config").join("zed").join("settings.json")
217 }
218 }
219 TerminalType::Warp => {
220 if cfg!(target_os = "macos") {
221 home_dir.join(".warp")
222 } else {
223 home_dir.join(".config").join("warp")
224 }
225 }
226 TerminalType::ITerm2 => {
227 if cfg!(target_os = "macos") {
228 home_dir.join("Library").join("Preferences").join("com.googlecode.iterm2.plist")
229 } else {
230 anyhow::bail!("iTerm2 is only available on macOS")
231 }
232 }
233 TerminalType::VSCode => {
234 if cfg!(target_os = "windows") {
235 let appdata = env::var("APPDATA").context("APPDATA environment variable not set")?;
236 PathBuf::from(appdata).join("Code").join("User").join("settings.json")
237 } else if cfg!(target_os = "macos") {
238 home_dir
239 .join("Library")
240 .join("Application Support")
241 .join("Code")
242 .join("User")
243 .join("settings.json")
244 } else {
245 home_dir.join(".config").join("Code").join("User").join("settings.json")
246 }
247 }
248 TerminalType::WindowsTerminal => {
249 if cfg!(target_os = "windows") {
250 let local_appdata =
251 env::var("LOCALAPPDATA").context("LOCALAPPDATA environment variable not set")?;
252 PathBuf::from(local_appdata)
253 .join("Packages")
254 .join("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
255 .join("LocalState")
256 .join("settings.json")
257 } else {
258 anyhow::bail!("Windows Terminal is only available on Windows")
259 }
260 }
261 TerminalType::Hyper => home_dir.join(".hyper.js"),
262 TerminalType::Tabby => {
263 if cfg!(target_os = "windows") {
264 let appdata = env::var("APPDATA").context("APPDATA environment variable not set")?;
265 PathBuf::from(appdata).join("tabby").join("config.yaml")
266 } else if cfg!(target_os = "macos") {
267 home_dir
268 .join("Library")
269 .join("Application Support")
270 .join("tabby")
271 .join("config.yaml")
272 } else {
273 home_dir.join(".config").join("tabby").join("config.yaml")
274 }
275 }
276 TerminalType::Unknown => {
277 anyhow::bail!("Cannot determine config path for unknown terminal")
278 }
279 };
280
281 Ok(path)
282 }
283
284 pub fn name(&self) -> &'static str {
286 match self {
287 TerminalType::Ghostty => "Ghostty",
288 TerminalType::Kitty => "Kitty",
289 TerminalType::Alacritty => "Alacritty",
290 TerminalType::WezTerm => "WezTerm",
291 TerminalType::TerminalApp => "Terminal.app",
292 TerminalType::Xterm => "xterm",
293 TerminalType::Zed => "Zed",
294 TerminalType::Warp => "Warp",
295 TerminalType::ITerm2 => "iTerm2",
296 TerminalType::VSCode => "VS Code",
297 TerminalType::WindowsTerminal => "Windows Terminal",
298 TerminalType::Hyper => "Hyper",
299 TerminalType::Tabby => "Tabby",
300 TerminalType::Unknown => "Unknown",
301 }
302 }
303
304 pub fn requires_manual_setup(&self) -> bool {
306 self.should_offer_terminal_setup()
307 }
308}
309
310impl TerminalFeature {
311 pub fn name(&self) -> &'static str {
313 match self {
314 TerminalFeature::Multiline => "Shift+Enter Multiline Input",
315 TerminalFeature::CopyPaste => "Enhanced Copy/Paste",
316 TerminalFeature::ShellIntegration => "Shell Integration",
317 TerminalFeature::ThemeSync => "Theme Synchronization",
318 TerminalFeature::Notifications => "System Notifications",
319 }
320 }
321}
322
323pub fn is_ghostty_terminal(term_program: Option<&str>, term: Option<&str>) -> bool {
325 terminal_name_contains(term_program, "ghostty") || terminal_name_contains(term, "ghostty")
326}
327
328fn terminal_name_contains(value: Option<&str>, needle: &str) -> bool {
329 value.map(|value| value.to_ascii_lowercase().contains(needle)).unwrap_or(false)
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn terminal_feature_support_matches_expectations() {
338 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::Multiline));
339 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::CopyPaste));
340 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::ShellIntegration));
341 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::ThemeSync));
342 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::Notifications));
343
344 assert!(TerminalType::VSCode.supports_feature(TerminalFeature::Multiline));
345 assert!(TerminalType::VSCode.supports_feature(TerminalFeature::Notifications));
346 assert!(!TerminalType::VSCode.supports_feature(TerminalFeature::CopyPaste));
347
348 assert!(TerminalType::Zed.supports_feature(TerminalFeature::Multiline));
349 assert!(TerminalType::Zed.supports_feature(TerminalFeature::ThemeSync));
350 assert!(TerminalType::Zed.supports_feature(TerminalFeature::Notifications));
351
352 assert!(TerminalType::Warp.supports_feature(TerminalFeature::Notifications));
353
354 assert!(!TerminalType::Unknown.supports_feature(TerminalFeature::Multiline));
355 assert!(!TerminalType::Unknown.supports_feature(TerminalFeature::Notifications));
356 }
357
358 #[test]
359 fn terminal_names_match_current_labels() {
360 assert_eq!(TerminalType::Kitty.name(), "Kitty");
361 assert_eq!(TerminalType::Alacritty.name(), "Alacritty");
362 assert_eq!(TerminalType::VSCode.name(), "VS Code");
363 }
364
365 #[test]
366 fn manual_setup_detection_matches_offer_state() {
367 assert!(TerminalType::VSCode.requires_manual_setup());
368 assert!(!TerminalType::ITerm2.requires_manual_setup());
369 assert!(!TerminalType::Kitty.requires_manual_setup());
370 }
371
372 #[test]
373 fn native_multiline_terminals_are_not_offered_setup() {
374 assert!(TerminalType::WezTerm.has_native_multiline_support());
375 assert!(!TerminalType::WezTerm.should_offer_terminal_setup());
376 assert!(TerminalType::ITerm2.has_native_multiline_support());
377 assert!(!TerminalType::ITerm2.should_offer_terminal_setup());
378 assert!(TerminalType::Warp.has_native_multiline_support());
379 assert!(!TerminalType::Warp.should_offer_terminal_setup());
380 }
381
382 #[test]
383 fn supported_setup_terminals_are_offered_setup() {
384 assert!(TerminalType::VSCode.should_offer_terminal_setup());
385 assert!(TerminalType::Alacritty.should_offer_terminal_setup());
386 assert!(TerminalType::Zed.should_offer_terminal_setup());
387 assert!(!TerminalType::WindowsTerminal.should_offer_terminal_setup());
388 assert!(!TerminalType::Hyper.should_offer_terminal_setup());
389 assert!(!TerminalType::Tabby.should_offer_terminal_setup());
390 }
391
392 #[test]
393 fn ghostty_helper_matches_term_program_or_term() {
394 assert!(is_ghostty_terminal(Some("Ghostty"), None));
395 assert!(is_ghostty_terminal(None, Some("xterm-ghostty")));
396 assert!(!is_ghostty_terminal(Some("WezTerm"), Some("xterm-256color")));
397 }
398}