Skip to main content

hematite/ui/
terminal.rs

1use std::env;
2
3/// Known terminal and multiplexer categories derived from environment variables.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum TerminalName {
6    WindowsTerminal,
7    VsCode,
8    Iterm2,
9    AppleTerminal,
10    Ghostty,
11    Kitty,
12    WezTerm,
13    Alacritty,
14    Tmux,
15    Zellij,
16    Unknown,
17}
18
19impl TerminalName {
20    pub fn label(&self) -> &'static str {
21        match self {
22            TerminalName::WindowsTerminal => "WindowsTerminal",
23            TerminalName::VsCode => "VsCode",
24            TerminalName::Iterm2 => "iTerm2",
25            TerminalName::AppleTerminal => "AppleTerminal",
26            TerminalName::Ghostty => "Ghostty",
27            TerminalName::Kitty => "Kitty",
28            TerminalName::WezTerm => "WezTerm",
29            TerminalName::Alacritty => "Alacritty",
30            TerminalName::Tmux => "Tmux",
31            TerminalName::Zellij => "Zellij",
32            TerminalName::Unknown => "Terminal",
33        }
34    }
35}
36
37/// Detects the active terminal environment using heuristics borrowed from Codex-RS.
38pub fn detect_terminal() -> TerminalName {
39    // 1. Multiplexers (probed first as they can wrap other terms)
40    if env::var("ZELLIJ").is_ok() || env::var("ZELLIJ_SESSION_NAME").is_ok() {
41        return TerminalName::Zellij;
42    }
43    if env::var("TMUX").is_ok() {
44        return TerminalName::Tmux;
45    }
46
47    // 2. Explicit TERM_PROGRAM identifiers
48    if let Ok(prog) = env::var("TERM_PROGRAM") {
49        match prog.to_lowercase().as_str() {
50            "vscode" => return TerminalName::VsCode,
51            "iterm.app" => return TerminalName::Iterm2,
52            "apple_terminal" => return TerminalName::AppleTerminal,
53            "ghostty" => return TerminalName::Ghostty,
54            "wezterm" => return TerminalName::WezTerm,
55            _ => {}
56        }
57    }
58
59    // 3. Platform-specific and Capability variables
60    if env::var("WT_SESSION").is_ok() {
61        return TerminalName::WindowsTerminal;
62    }
63    if env::var("KITTY_WINDOW_ID").is_ok() {
64        return TerminalName::Kitty;
65    }
66    if env::var("ALACRITTY_SOCKET").is_ok() {
67        return TerminalName::Alacritty;
68    }
69
70    // 4. TERM fallback
71    if let Ok(term) = env::var("TERM") {
72        let lower = term.to_lowercase();
73        if lower.contains("iterm") {
74            return TerminalName::Iterm2;
75        }
76        if lower.contains("kitty") {
77            return TerminalName::Kitty;
78        }
79        if lower.contains("alacritty") {
80            return TerminalName::Alacritty;
81        }
82        if lower.contains("wezterm") {
83            return TerminalName::WezTerm;
84        }
85    }
86
87    TerminalName::Unknown
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_windows_terminal_detection() {
96        // Clear env noise
97        let old_term = std::env::var("TERM_PROGRAM").ok();
98        std::env::remove_var("TERM_PROGRAM");
99
100        std::env::set_var("WT_SESSION", "test-session");
101        assert_eq!(detect_terminal(), TerminalName::WindowsTerminal);
102
103        std::env::remove_var("WT_SESSION");
104        if let Some(val) = old_term {
105            std::env::set_var("TERM_PROGRAM", val);
106        }
107    }
108
109    #[test]
110    fn test_vscode_detection() {
111        std::env::set_var("TERM_PROGRAM", "vscode");
112        assert_eq!(detect_terminal(), TerminalName::VsCode);
113        std::env::remove_var("TERM_PROGRAM");
114    }
115}