const MAX_ANCESTRY_DEPTH: usize = 50;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Terminal {
pub kind: TerminalKind,
pub process_name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TerminalKind {
ITerm2,
TerminalApp,
WezTerm,
Kitty,
Ghostty,
Alacritty,
Warp,
Tabby,
Wave,
GnomeTerminal,
Konsole,
Foot,
Terminator,
Hyper,
Rio,
VSCode,
Cursor,
Unknown,
}
impl TerminalKind {
#[must_use]
pub fn app_name(&self) -> &'static str {
match self {
Self::ITerm2 => "iTerm2",
Self::TerminalApp => "Terminal",
Self::WezTerm => "WezTerm",
Self::Kitty => "kitty",
Self::Ghostty => "Ghostty",
Self::Alacritty => "Alacritty",
Self::Warp => "Warp",
Self::Tabby => "Tabby",
Self::Wave => "Wave",
Self::GnomeTerminal => "gnome-terminal",
Self::Konsole => "konsole",
Self::Foot => "foot",
Self::Terminator => "terminator",
Self::Hyper => "Hyper",
Self::Rio => "Rio",
Self::VSCode => "Code",
Self::Cursor => "Cursor",
Self::Unknown => "unknown",
}
}
}
#[must_use]
pub fn terminal(tty_device: &str) -> Option<Terminal> {
let tty_path = if tty_device.starts_with("/dev/") {
tty_device.to_string()
} else {
format!("/dev/{tty_device}")
};
let start_pid = get_process_on_tty(&tty_path)?;
terminal_from_ancestry(start_pid)
}
#[must_use]
pub fn terminal_from_pid(pid: i32) -> Option<Terminal> {
terminal_from_ancestry(pid)
}
fn get_process_on_tty(tty_path: &str) -> Option<i32> {
let tty_name = tty_path.strip_prefix("/dev/").unwrap_or(tty_path);
prock::list_all_pids()
.into_iter()
.find(|&pid| prock::get_tty(pid).is_some_and(|tty| tty == tty_name))
}
fn terminal_from_ancestry(start_pid: i32) -> Option<Terminal> {
let parent_map = prock::build_parent_map();
let mut current_pid = start_pid;
let mut last_candidate: Option<String> = None;
for _ in 0..MAX_ANCESTRY_DEPTH {
if let Some(path) = prock::get_process_path(current_pid) {
let exec_name = path
.rsplit('/')
.next()
.unwrap_or(&path)
.trim_end_matches(".app");
if let Some((kind, canonical_name)) = map_process_to_terminal(exec_name) {
return Some(Terminal {
kind,
process_name: canonical_name.to_string(),
});
}
if path.contains("iTerm") {
return Some(Terminal {
kind: TerminalKind::ITerm2,
process_name: "iTerm2".to_string(),
});
}
if !is_non_terminal_process(exec_name) {
last_candidate = Some(exec_name.to_string());
}
}
match parent_map.get(¤t_pid) {
Some(&ppid) if ppid > 1 => current_pid = ppid,
_ => break, }
}
last_candidate.map(|name| Terminal {
kind: TerminalKind::Unknown,
process_name: name,
})
}
fn is_non_terminal_process(name: &str) -> bool {
matches!(
name,
"bash"
| "zsh"
| "fish"
| "sh"
| "dash"
| "ksh"
| "tcsh"
| "csh"
| "nu"
| "nushell"
| "pwsh"
| "powershell"
| "elvish"
| "ion"
| "xonsh"
| "login"
| "sshd"
| "ssh"
| "sudo"
| "su"
| "env"
| "direnv"
| "tmux"
| "screen"
| "zellij"
| "launchd"
| "init"
| "systemd"
| "xinit"
| "startx"
)
}
fn map_process_to_terminal(proc_name: &str) -> Option<(TerminalKind, &'static str)> {
match proc_name {
"iTerm2" | "iTerm.app" | "iterm2" => Some((TerminalKind::ITerm2, "iTerm2")),
"Terminal" | "Terminal.app" | "Apple_Terminal" => {
Some((TerminalKind::TerminalApp, "Terminal"))
}
"wezterm" | "wezterm-gui" | "WezTerm" => Some((TerminalKind::WezTerm, "WezTerm")),
"kitty" => Some((TerminalKind::Kitty, "kitty")),
"ghostty" | "Ghostty" => Some((TerminalKind::Ghostty, "Ghostty")),
"alacritty" | "Alacritty" => Some((TerminalKind::Alacritty, "Alacritty")),
"Warp" | "warp" => Some((TerminalKind::Warp, "Warp")),
"Tabby" | "tabby" => Some((TerminalKind::Tabby, "Tabby")),
"Wave" | "waveterm" | "wave" => Some((TerminalKind::Wave, "Wave")),
"gnome-terminal" | "gnome-terminal-" | "gnome-terminal-server" => {
Some((TerminalKind::GnomeTerminal, "gnome-terminal"))
}
"konsole" => Some((TerminalKind::Konsole, "konsole")),
"foot" => Some((TerminalKind::Foot, "foot")),
"terminator" => Some((TerminalKind::Terminator, "terminator")),
"Hyper" | "hyper" => Some((TerminalKind::Hyper, "Hyper")),
"rio" | "Rio" => Some((TerminalKind::Rio, "Rio")),
"Code" | "code" | "Code - Insiders" | "code-insiders" => {
Some((TerminalKind::VSCode, "Code"))
}
"Cursor" | "cursor" => Some((TerminalKind::Cursor, "Cursor")),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_map_process_to_terminal_iterm2() {
assert_eq!(
map_process_to_terminal("iTerm2"),
Some((TerminalKind::ITerm2, "iTerm2"))
);
assert_eq!(
map_process_to_terminal("iterm2"),
Some((TerminalKind::ITerm2, "iTerm2"))
);
}
#[test]
fn test_map_process_to_terminal_wezterm() {
assert_eq!(
map_process_to_terminal("wezterm-gui"),
Some((TerminalKind::WezTerm, "WezTerm"))
);
}
#[test]
fn test_map_process_to_terminal_unknown() {
assert_eq!(map_process_to_terminal("unknown-terminal"), None);
}
#[test]
fn test_terminal_kind_app_name() {
assert_eq!(TerminalKind::ITerm2.app_name(), "iTerm2");
assert_eq!(TerminalKind::Kitty.app_name(), "kitty");
assert_eq!(TerminalKind::Warp.app_name(), "Warp");
assert_eq!(TerminalKind::Tabby.app_name(), "Tabby");
assert_eq!(TerminalKind::Wave.app_name(), "Wave");
}
#[test]
fn test_map_process_to_terminal_warp() {
assert_eq!(
map_process_to_terminal("Warp"),
Some((TerminalKind::Warp, "Warp"))
);
assert_eq!(
map_process_to_terminal("warp"),
Some((TerminalKind::Warp, "Warp"))
);
}
#[test]
fn test_map_process_to_terminal_tabby() {
assert_eq!(
map_process_to_terminal("Tabby"),
Some((TerminalKind::Tabby, "Tabby"))
);
assert_eq!(
map_process_to_terminal("tabby"),
Some((TerminalKind::Tabby, "Tabby"))
);
}
#[test]
fn test_map_process_to_terminal_wave() {
assert_eq!(
map_process_to_terminal("Wave"),
Some((TerminalKind::Wave, "Wave"))
);
assert_eq!(
map_process_to_terminal("waveterm"),
Some((TerminalKind::Wave, "Wave"))
);
assert_eq!(
map_process_to_terminal("wave"),
Some((TerminalKind::Wave, "Wave"))
);
}
#[test]
fn test_detect_terminal_invalid_tty() {
assert!(terminal("ttys999999").is_none());
}
#[test]
fn test_terminal_from_pid_invalid() {
assert!(terminal_from_pid(999_999_999).is_none());
}
#[test]
fn test_terminal_from_pid_current_process() {
let pid = std::process::id() as i32;
let result = terminal_from_pid(pid);
let _ = result;
}
#[test]
fn test_map_process_to_terminal_vscode() {
assert_eq!(
map_process_to_terminal("Code"),
Some((TerminalKind::VSCode, "Code"))
);
assert_eq!(
map_process_to_terminal("code"),
Some((TerminalKind::VSCode, "Code"))
);
assert_eq!(
map_process_to_terminal("Code - Insiders"),
Some((TerminalKind::VSCode, "Code"))
);
}
#[test]
fn test_map_process_to_terminal_cursor() {
assert_eq!(
map_process_to_terminal("Cursor"),
Some((TerminalKind::Cursor, "Cursor"))
);
assert_eq!(
map_process_to_terminal("cursor"),
Some((TerminalKind::Cursor, "Cursor"))
);
}
#[test]
fn test_terminal_kind_app_name_ides() {
assert_eq!(TerminalKind::VSCode.app_name(), "Code");
assert_eq!(TerminalKind::Cursor.app_name(), "Cursor");
}
#[test]
fn test_is_non_terminal_process_shells() {
assert!(is_non_terminal_process("bash"));
assert!(is_non_terminal_process("zsh"));
assert!(is_non_terminal_process("fish"));
assert!(is_non_terminal_process("sh"));
assert!(is_non_terminal_process("nu"));
assert!(is_non_terminal_process("pwsh"));
assert!(is_non_terminal_process("elvish"));
assert!(is_non_terminal_process("ion"));
assert!(is_non_terminal_process("xonsh"));
}
#[test]
fn test_is_non_terminal_process_utilities() {
assert!(is_non_terminal_process("login"));
assert!(is_non_terminal_process("sshd"));
assert!(is_non_terminal_process("sudo"));
assert!(is_non_terminal_process("env"));
assert!(is_non_terminal_process("direnv"));
}
#[test]
fn test_is_non_terminal_process_multiplexers() {
assert!(is_non_terminal_process("tmux"));
assert!(is_non_terminal_process("screen"));
assert!(is_non_terminal_process("zellij"));
}
#[test]
fn test_is_non_terminal_process_launchers() {
assert!(is_non_terminal_process("launchd"));
assert!(is_non_terminal_process("init"));
assert!(is_non_terminal_process("systemd"));
}
#[test]
fn test_is_non_terminal_process_terminals_are_not_excluded() {
assert!(!is_non_terminal_process("alacritty"));
assert!(!is_non_terminal_process("kitty"));
assert!(!is_non_terminal_process("wezterm"));
assert!(!is_non_terminal_process("iTerm2"));
assert!(!is_non_terminal_process("Ghostty"));
assert!(!is_non_terminal_process("my-cool-terminal"));
assert!(!is_non_terminal_process("xterm"));
assert!(!is_non_terminal_process("urxvt"));
}
}