use anyhow::{Context, Result};
use std::env;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalType {
Ghostty,
Kitty,
Alacritty,
WezTerm,
TerminalApp,
Xterm,
Zed,
Warp,
ITerm2,
VSCode,
WindowsTerminal,
Hyper,
Tabby,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalFeature {
Multiline,
CopyPaste,
ShellIntegration,
ThemeSync,
Notifications,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalSetupAvailability {
NativeSupport,
Offered,
GuidanceOnly,
}
impl TerminalType {
pub fn detect() -> Result<Self> {
if let Ok(term_program) = env::var("TERM_PROGRAM") {
let term_lower = term_program.to_lowercase();
if term_lower.contains("ghostty") {
return Ok(TerminalType::Ghostty);
} else if term_lower.contains("wezterm") {
return Ok(TerminalType::WezTerm);
} else if term_lower.contains("apple_terminal") {
return Ok(TerminalType::TerminalApp);
} else if term_lower.contains("iterm") {
return Ok(TerminalType::ITerm2);
} else if term_lower.contains("vscode") {
return Ok(TerminalType::VSCode);
} else if term_lower.contains("warp") {
return Ok(TerminalType::Warp);
} else if term_lower.contains("hyper") {
return Ok(TerminalType::Hyper);
} else if term_lower.contains("tabby") {
return Ok(TerminalType::Tabby);
}
}
if env::var("KITTY_WINDOW_ID").is_ok() || env::var("KITTY_PID").is_ok() {
return Ok(TerminalType::Kitty);
}
if env::var("ALACRITTY_SOCKET").is_ok() || env::var("ALACRITTY_LOG").is_ok() {
return Ok(TerminalType::Alacritty);
}
if env::var("ZED_TERMINAL").is_ok() {
return Ok(TerminalType::Zed);
}
if env::var("WT_SESSION").is_ok() || env::var("WT_PROFILE_ID").is_ok() {
return Ok(TerminalType::WindowsTerminal);
}
if let Ok(term) = env::var("TERM") {
let term_lower = term.to_lowercase();
if term_lower.contains("kitty") {
return Ok(TerminalType::Kitty);
} else if term_lower.contains("alacritty") {
return Ok(TerminalType::Alacritty);
} else if term_lower.contains("xterm") {
return Ok(TerminalType::Xterm);
}
}
Ok(TerminalType::Unknown)
}
pub fn supports_feature(&self, feature: TerminalFeature) -> bool {
match (self, feature) {
(TerminalType::Ghostty, _) => true,
(TerminalType::Kitty, _) => true,
(TerminalType::Alacritty, _) => true,
(TerminalType::WezTerm, _) => true,
(TerminalType::TerminalApp, TerminalFeature::Multiline) => true,
(TerminalType::TerminalApp, TerminalFeature::ShellIntegration) => true,
(TerminalType::TerminalApp, TerminalFeature::Notifications) => true,
(TerminalType::TerminalApp, _) => false,
(TerminalType::Xterm, TerminalFeature::Multiline) => true,
(TerminalType::Xterm, TerminalFeature::Notifications) => true,
(TerminalType::Xterm, _) => false,
(TerminalType::Zed, TerminalFeature::Multiline) => true,
(TerminalType::Zed, TerminalFeature::ThemeSync) => true,
(TerminalType::Zed, TerminalFeature::Notifications) => true,
(TerminalType::Zed, _) => false,
(TerminalType::Warp, TerminalFeature::Multiline) => true,
(TerminalType::Warp, TerminalFeature::Notifications) => true,
(TerminalType::Warp, _) => false,
(TerminalType::ITerm2, _) => true,
(TerminalType::VSCode, TerminalFeature::Multiline) => true,
(TerminalType::VSCode, TerminalFeature::Notifications) => true,
(TerminalType::VSCode, _) => false,
(TerminalType::WindowsTerminal, _) => true,
(TerminalType::Hyper, _) => true,
(TerminalType::Tabby, _) => true,
(TerminalType::Unknown, _) => false,
}
}
pub fn has_native_multiline_support(&self) -> bool {
matches!(
self,
TerminalType::Ghostty
| TerminalType::Kitty
| TerminalType::WezTerm
| TerminalType::ITerm2
| TerminalType::Warp
)
}
pub fn terminal_setup_availability(&self) -> TerminalSetupAvailability {
match self {
TerminalType::Ghostty
| TerminalType::Kitty
| TerminalType::WezTerm
| TerminalType::ITerm2
| TerminalType::Warp => TerminalSetupAvailability::NativeSupport,
TerminalType::Alacritty | TerminalType::Zed | TerminalType::VSCode => {
TerminalSetupAvailability::Offered
}
TerminalType::TerminalApp
| TerminalType::Xterm
| TerminalType::WindowsTerminal
| TerminalType::Hyper
| TerminalType::Tabby
| TerminalType::Unknown => TerminalSetupAvailability::GuidanceOnly,
}
}
pub fn should_offer_terminal_setup(&self) -> bool {
matches!(
self.terminal_setup_availability(),
TerminalSetupAvailability::Offered
)
}
pub fn config_path(&self) -> Result<PathBuf> {
let home_dir = dirs::home_dir().context("Failed to determine home directory")?;
let path = match self {
TerminalType::Ghostty => {
if cfg!(target_os = "windows") {
let appdata =
env::var("APPDATA").context("APPDATA environment variable not set")?;
PathBuf::from(appdata).join("ghostty").join("config")
} else {
home_dir.join(".config").join("ghostty").join("config")
}
}
TerminalType::Kitty => {
if cfg!(target_os = "windows") {
let appdata =
env::var("APPDATA").context("APPDATA environment variable not set")?;
PathBuf::from(appdata).join("kitty").join("kitty.conf")
} else {
home_dir.join(".config").join("kitty").join("kitty.conf")
}
}
TerminalType::Alacritty => {
if cfg!(target_os = "windows") {
let appdata =
env::var("APPDATA").context("APPDATA environment variable not set")?;
PathBuf::from(appdata)
.join("alacritty")
.join("alacritty.toml")
} else {
home_dir
.join(".config")
.join("alacritty")
.join("alacritty.toml")
}
}
TerminalType::WezTerm => home_dir.join(".wezterm.lua"),
TerminalType::TerminalApp => {
if cfg!(target_os = "macos") {
home_dir
.join("Library")
.join("Preferences")
.join("com.apple.Terminal.plist")
} else {
anyhow::bail!("Terminal.app is only available on macOS")
}
}
TerminalType::Xterm => home_dir.join(".Xresources"),
TerminalType::Zed => {
if cfg!(target_os = "windows") {
let appdata =
env::var("APPDATA").context("APPDATA environment variable not set")?;
PathBuf::from(appdata).join("Zed").join("settings.json")
} else if cfg!(target_os = "macos") {
home_dir
.join("Library")
.join("Application Support")
.join("Zed")
.join("settings.json")
} else {
home_dir.join(".config").join("zed").join("settings.json")
}
}
TerminalType::Warp => {
if cfg!(target_os = "macos") {
home_dir.join(".warp")
} else {
home_dir.join(".config").join("warp")
}
}
TerminalType::ITerm2 => {
if cfg!(target_os = "macos") {
home_dir
.join("Library")
.join("Preferences")
.join("com.googlecode.iterm2.plist")
} else {
anyhow::bail!("iTerm2 is only available on macOS")
}
}
TerminalType::VSCode => {
if cfg!(target_os = "windows") {
let appdata =
env::var("APPDATA").context("APPDATA environment variable not set")?;
PathBuf::from(appdata)
.join("Code")
.join("User")
.join("settings.json")
} else if cfg!(target_os = "macos") {
home_dir
.join("Library")
.join("Application Support")
.join("Code")
.join("User")
.join("settings.json")
} else {
home_dir
.join(".config")
.join("Code")
.join("User")
.join("settings.json")
}
}
TerminalType::WindowsTerminal => {
if cfg!(target_os = "windows") {
let local_appdata = env::var("LOCALAPPDATA")
.context("LOCALAPPDATA environment variable not set")?;
PathBuf::from(local_appdata)
.join("Packages")
.join("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
.join("LocalState")
.join("settings.json")
} else {
anyhow::bail!("Windows Terminal is only available on Windows")
}
}
TerminalType::Hyper => home_dir.join(".hyper.js"),
TerminalType::Tabby => {
if cfg!(target_os = "windows") {
let appdata =
env::var("APPDATA").context("APPDATA environment variable not set")?;
PathBuf::from(appdata).join("tabby").join("config.yaml")
} else if cfg!(target_os = "macos") {
home_dir
.join("Library")
.join("Application Support")
.join("tabby")
.join("config.yaml")
} else {
home_dir.join(".config").join("tabby").join("config.yaml")
}
}
TerminalType::Unknown => {
anyhow::bail!("Cannot determine config path for unknown terminal")
}
};
Ok(path)
}
pub fn name(&self) -> &'static str {
match self {
TerminalType::Ghostty => "Ghostty",
TerminalType::Kitty => "Kitty",
TerminalType::Alacritty => "Alacritty",
TerminalType::WezTerm => "WezTerm",
TerminalType::TerminalApp => "Terminal.app",
TerminalType::Xterm => "xterm",
TerminalType::Zed => "Zed",
TerminalType::Warp => "Warp",
TerminalType::ITerm2 => "iTerm2",
TerminalType::VSCode => "VS Code",
TerminalType::WindowsTerminal => "Windows Terminal",
TerminalType::Hyper => "Hyper",
TerminalType::Tabby => "Tabby",
TerminalType::Unknown => "Unknown",
}
}
pub fn requires_manual_setup(&self) -> bool {
self.should_offer_terminal_setup()
}
}
impl TerminalFeature {
pub fn name(&self) -> &'static str {
match self {
TerminalFeature::Multiline => "Shift+Enter Multiline Input",
TerminalFeature::CopyPaste => "Enhanced Copy/Paste",
TerminalFeature::ShellIntegration => "Shell Integration",
TerminalFeature::ThemeSync => "Theme Synchronization",
TerminalFeature::Notifications => "System Notifications",
}
}
}
pub fn is_ghostty_terminal(term_program: Option<&str>, term: Option<&str>) -> bool {
terminal_name_contains(term_program, "ghostty") || terminal_name_contains(term, "ghostty")
}
fn terminal_name_contains(value: Option<&str>, needle: &str) -> bool {
value
.map(|value| value.to_ascii_lowercase().contains(needle))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminal_feature_support_matches_expectations() {
assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::Multiline));
assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::CopyPaste));
assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::ShellIntegration));
assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::ThemeSync));
assert!(TerminalType::Ghostty.supports_feature(TerminalFeature::Notifications));
assert!(TerminalType::VSCode.supports_feature(TerminalFeature::Multiline));
assert!(TerminalType::VSCode.supports_feature(TerminalFeature::Notifications));
assert!(!TerminalType::VSCode.supports_feature(TerminalFeature::CopyPaste));
assert!(TerminalType::Zed.supports_feature(TerminalFeature::Multiline));
assert!(TerminalType::Zed.supports_feature(TerminalFeature::ThemeSync));
assert!(TerminalType::Zed.supports_feature(TerminalFeature::Notifications));
assert!(TerminalType::Warp.supports_feature(TerminalFeature::Notifications));
assert!(!TerminalType::Unknown.supports_feature(TerminalFeature::Multiline));
assert!(!TerminalType::Unknown.supports_feature(TerminalFeature::Notifications));
}
#[test]
fn terminal_names_match_current_labels() {
assert_eq!(TerminalType::Kitty.name(), "Kitty");
assert_eq!(TerminalType::Alacritty.name(), "Alacritty");
assert_eq!(TerminalType::VSCode.name(), "VS Code");
}
#[test]
fn manual_setup_detection_matches_offer_state() {
assert!(TerminalType::VSCode.requires_manual_setup());
assert!(!TerminalType::ITerm2.requires_manual_setup());
assert!(!TerminalType::Kitty.requires_manual_setup());
}
#[test]
fn native_multiline_terminals_are_not_offered_setup() {
assert!(TerminalType::WezTerm.has_native_multiline_support());
assert!(!TerminalType::WezTerm.should_offer_terminal_setup());
assert!(TerminalType::ITerm2.has_native_multiline_support());
assert!(!TerminalType::ITerm2.should_offer_terminal_setup());
assert!(TerminalType::Warp.has_native_multiline_support());
assert!(!TerminalType::Warp.should_offer_terminal_setup());
}
#[test]
fn supported_setup_terminals_are_offered_setup() {
assert!(TerminalType::VSCode.should_offer_terminal_setup());
assert!(TerminalType::Alacritty.should_offer_terminal_setup());
assert!(TerminalType::Zed.should_offer_terminal_setup());
assert!(!TerminalType::WindowsTerminal.should_offer_terminal_setup());
assert!(!TerminalType::Hyper.should_offer_terminal_setup());
assert!(!TerminalType::Tabby.should_offer_terminal_setup());
}
#[test]
fn ghostty_helper_matches_term_program_or_term() {
assert!(is_ghostty_terminal(Some("Ghostty"), None));
assert!(is_ghostty_terminal(None, Some("xterm-ghostty")));
assert!(!is_ghostty_terminal(
Some("WezTerm"),
Some("xterm-256color")
));
}
}