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 => {
153 TerminalSetupAvailability::Offered
154 }
155 TerminalType::TerminalApp
156 | TerminalType::Xterm
157 | TerminalType::WindowsTerminal
158 | TerminalType::Hyper
159 | TerminalType::Tabby
160 | TerminalType::Unknown => TerminalSetupAvailability::GuidanceOnly,
161 }
162 }
163
164 pub fn should_offer_terminal_setup(&self) -> bool {
166 matches!(self.terminal_setup_availability(), TerminalSetupAvailability::Offered)
167 }
168
169 pub fn config_path(&self) -> Result<PathBuf> {
171 let home_dir = dirs::home_dir().context("Failed to determine home directory")?;
172
173 let path = match self {
174 TerminalType::Ghostty => {
175 if cfg!(target_os = "windows") {
176 let appdata =
177 env::var("APPDATA").context("APPDATA environment variable not set")?;
178 PathBuf::from(appdata).join("ghostty").join("config")
179 } else {
180 home_dir.join(".config").join("ghostty").join("config")
181 }
182 }
183 TerminalType::Kitty => {
184 if cfg!(target_os = "windows") {
185 let appdata =
186 env::var("APPDATA").context("APPDATA environment variable not set")?;
187 PathBuf::from(appdata).join("kitty").join("kitty.conf")
188 } else {
189 home_dir.join(".config").join("kitty").join("kitty.conf")
190 }
191 }
192 TerminalType::Alacritty => {
193 if cfg!(target_os = "windows") {
194 let appdata =
195 env::var("APPDATA").context("APPDATA environment variable not set")?;
196 PathBuf::from(appdata).join("alacritty").join("alacritty.toml")
197 } else {
198 home_dir.join(".config").join("alacritty").join("alacritty.toml")
199 }
200 }
201 TerminalType::WezTerm => home_dir.join(".wezterm.lua"),
202 TerminalType::TerminalApp => {
203 if cfg!(target_os = "macos") {
204 home_dir.join("Library").join("Preferences").join("com.apple.Terminal.plist")
205 } else {
206 anyhow::bail!("Terminal.app is only available on macOS")
207 }
208 }
209 TerminalType::Xterm => home_dir.join(".Xresources"),
210 TerminalType::Zed => {
211 if cfg!(target_os = "windows") {
212 let appdata =
213 env::var("APPDATA").context("APPDATA environment variable not set")?;
214 PathBuf::from(appdata).join("Zed").join("settings.json")
215 } else if cfg!(target_os = "macos") {
216 home_dir
217 .join("Library")
218 .join("Application Support")
219 .join("Zed")
220 .join("settings.json")
221 } else {
222 home_dir.join(".config").join("zed").join("settings.json")
223 }
224 }
225 TerminalType::Warp => {
226 if cfg!(target_os = "macos") {
227 home_dir.join(".warp")
228 } else {
229 home_dir.join(".config").join("warp")
230 }
231 }
232 TerminalType::ITerm2 => {
233 if cfg!(target_os = "macos") {
234 home_dir.join("Library").join("Preferences").join("com.googlecode.iterm2.plist")
235 } else {
236 anyhow::bail!("iTerm2 is only available on macOS")
237 }
238 }
239 TerminalType::VSCode => {
240 if cfg!(target_os = "windows") {
241 let appdata =
242 env::var("APPDATA").context("APPDATA environment variable not set")?;
243 PathBuf::from(appdata).join("Code").join("User").join("settings.json")
244 } else if cfg!(target_os = "macos") {
245 home_dir
246 .join("Library")
247 .join("Application Support")
248 .join("Code")
249 .join("User")
250 .join("settings.json")
251 } else {
252 home_dir.join(".config").join("Code").join("User").join("settings.json")
253 }
254 }
255 TerminalType::WindowsTerminal => {
256 if cfg!(target_os = "windows") {
257 let local_appdata = env::var("LOCALAPPDATA")
258 .context("LOCALAPPDATA environment variable not set")?;
259 PathBuf::from(local_appdata)
260 .join("Packages")
261 .join("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
262 .join("LocalState")
263 .join("settings.json")
264 } else {
265 anyhow::bail!("Windows Terminal is only available on Windows")
266 }
267 }
268 TerminalType::Hyper => home_dir.join(".hyper.js"),
269 TerminalType::Tabby => {
270 if cfg!(target_os = "windows") {
271 let appdata =
272 env::var("APPDATA").context("APPDATA environment variable not set")?;
273 PathBuf::from(appdata).join("tabby").join("config.yaml")
274 } else if cfg!(target_os = "macos") {
275 home_dir
276 .join("Library")
277 .join("Application Support")
278 .join("tabby")
279 .join("config.yaml")
280 } else {
281 home_dir.join(".config").join("tabby").join("config.yaml")
282 }
283 }
284 TerminalType::Unknown => {
285 anyhow::bail!("Cannot determine config path for unknown terminal")
286 }
287 };
288
289 Ok(path)
290 }
291
292 pub fn name(&self) -> &'static str {
294 match self {
295 TerminalType::Ghostty => "Ghostty",
296 TerminalType::Kitty => "Kitty",
297 TerminalType::Alacritty => "Alacritty",
298 TerminalType::WezTerm => "WezTerm",
299 TerminalType::TerminalApp => "Terminal.app",
300 TerminalType::Xterm => "xterm",
301 TerminalType::Zed => "Zed",
302 TerminalType::Warp => "Warp",
303 TerminalType::ITerm2 => "iTerm2",
304 TerminalType::VSCode => "VS Code",
305 TerminalType::WindowsTerminal => "Windows Terminal",
306 TerminalType::Hyper => "Hyper",
307 TerminalType::Tabby => "Tabby",
308 TerminalType::Unknown => "Unknown",
309 }
310 }
311
312 pub fn requires_manual_setup(&self) -> bool {
314 self.should_offer_terminal_setup()
315 }
316}
317
318impl TerminalFeature {
319 pub fn name(&self) -> &'static str {
321 match self {
322 TerminalFeature::Multiline => "Shift+Enter Multiline Input",
323 TerminalFeature::CopyPaste => "Enhanced Copy/Paste",
324 TerminalFeature::ShellIntegration => "Shell Integration",
325 TerminalFeature::ThemeSync => "Theme Synchronization",
326 TerminalFeature::Notifications => "System Notifications",
327 }
328 }
329}
330
331pub fn is_ghostty_terminal(term_program: Option<&str>, term: Option<&str>) -> bool {
333 terminal_name_contains(term_program, "ghostty") || terminal_name_contains(term, "ghostty")
334}
335
336fn terminal_name_contains(value: Option<&str>, needle: &str) -> bool {
337 value.map(|value| value.to_ascii_lowercase().contains(needle)).unwrap_or(false)
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn terminal_feature_support_matches_expectations() {
346 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::Multiline));
347 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::CopyPaste));
348 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::ShellIntegration));
349 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::ThemeSync));
350 assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::Notifications));
351
352 assert!(TerminalType::VSCode.supports_feature(TerminalFeature::Multiline));
353 assert!(TerminalType::VSCode.supports_feature(TerminalFeature::Notifications));
354 assert!(!TerminalType::VSCode.supports_feature(TerminalFeature::CopyPaste));
355
356 assert!(TerminalType::Zed.supports_feature(TerminalFeature::Multiline));
357 assert!(TerminalType::Zed.supports_feature(TerminalFeature::ThemeSync));
358 assert!(TerminalType::Zed.supports_feature(TerminalFeature::Notifications));
359
360 assert!(TerminalType::Warp.supports_feature(TerminalFeature::Notifications));
361
362 assert!(!TerminalType::Unknown.supports_feature(TerminalFeature::Multiline));
363 assert!(!TerminalType::Unknown.supports_feature(TerminalFeature::Notifications));
364 }
365
366 #[test]
367 fn terminal_names_match_current_labels() {
368 assert_eq!(TerminalType::Kitty.name(), "Kitty");
369 assert_eq!(TerminalType::Alacritty.name(), "Alacritty");
370 assert_eq!(TerminalType::VSCode.name(), "VS Code");
371 }
372
373 #[test]
374 fn manual_setup_detection_matches_offer_state() {
375 assert!(TerminalType::VSCode.requires_manual_setup());
376 assert!(!TerminalType::ITerm2.requires_manual_setup());
377 assert!(!TerminalType::Kitty.requires_manual_setup());
378 }
379
380 #[test]
381 fn native_multiline_terminals_are_not_offered_setup() {
382 assert!(TerminalType::WezTerm.has_native_multiline_support());
383 assert!(!TerminalType::WezTerm.should_offer_terminal_setup());
384 assert!(TerminalType::ITerm2.has_native_multiline_support());
385 assert!(!TerminalType::ITerm2.should_offer_terminal_setup());
386 assert!(TerminalType::Warp.has_native_multiline_support());
387 assert!(!TerminalType::Warp.should_offer_terminal_setup());
388 }
389
390 #[test]
391 fn supported_setup_terminals_are_offered_setup() {
392 assert!(TerminalType::VSCode.should_offer_terminal_setup());
393 assert!(TerminalType::Alacritty.should_offer_terminal_setup());
394 assert!(TerminalType::Zed.should_offer_terminal_setup());
395 assert!(!TerminalType::WindowsTerminal.should_offer_terminal_setup());
396 assert!(!TerminalType::Hyper.should_offer_terminal_setup());
397 assert!(!TerminalType::Tabby.should_offer_terminal_setup());
398 }
399
400 #[test]
401 fn ghostty_helper_matches_term_program_or_term() {
402 assert!(is_ghostty_terminal(Some("Ghostty"), None));
403 assert!(is_ghostty_terminal(None, Some("xterm-ghostty")));
404 assert!(!is_ghostty_terminal(Some("WezTerm"), Some("xterm-256color")));
405 }
406}