1use std::{ffi::OsString, path::Path};
4
5use omp_core::{Str, fmts};
6
7#[cfg(unix)]
8mod platform {
9 use std::path::PathBuf;
10
11 pub(super) fn tty_path() -> Option<PathBuf> {
12 crate::tty::override_path().or_else(|| nix::unistd::ttyname(std::io::stdin()).ok())
13 }
14}
15
16#[cfg(windows)]
17mod platform {
18 use windows_sys::Win32::{
19 Foundation::INVALID_HANDLE_VALUE,
20 System::Console::{GetConsoleMode, GetStdHandle, STD_INPUT_HANDLE},
21 };
22
23 pub(super) fn has_console() -> bool {
24 let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
25 if handle.is_null() || handle == INVALID_HANDLE_VALUE {
26 return false;
27 }
28 let mut mode = 0;
29 unsafe { GetConsoleMode(handle, &mut mode) != 0 }
30 }
31}
32
33pub const UNKNOWN_TERMINAL_ID: &str = "unknown";
36
37#[must_use]
43pub fn terminal_id() -> Str {
44 #[cfg(unix)]
45 {
46 let tty_path = platform::tty_path();
47 terminal_id_with(tty_path.as_deref(), |name| std::env::var_os(name))
48 }
49 #[cfg(windows)]
50 {
51 let id = terminal_id_with(None, |name| std::env::var_os(name));
52 if id != UNKNOWN_TERMINAL_ID || !platform::has_console() {
53 return id;
54 }
55 "console".into()
56 }
57}
58
59#[must_use]
64pub fn terminal_id_with(
65 tty_path: Option<&Path>,
66 mut env: impl FnMut(&str) -> Option<OsString>,
67) -> Str {
68 if let Some(id) = tty_path.and_then(normalize_tty_path) {
69 return id;
70 }
71
72 if let Some(pane) = nonempty_env(&mut env, "ZELLIJ_PANE_ID") {
73 if let Some(session) = nonempty_env(&mut env, "ZELLIJ_SESSION_NAME") {
74 let session = session.replace(['/', '\\'], "-");
75 return fmts!("zellij-{session}-{pane}");
76 }
77 return fmts!("zellij-{pane}");
78 }
79
80 for (name, prefix) in [
81 ("TMUX_PANE", "tmux"),
82 ("CMUX_SURFACE_ID", "cmux"),
83 ("KITTY_WINDOW_ID", "kitty"),
84 ("WEZTERM_PANE", "wezterm"),
85 ("TERM_SESSION_ID", "apple"),
86 ("WT_SESSION", "wt"),
87 ] {
88 if let Some(value) = nonempty_env(&mut env, name) {
89 return fmts!("{prefix}-{value}");
90 }
91 }
92
93 UNKNOWN_TERMINAL_ID.into()
94}
95
96fn normalize_tty_path(path: &Path) -> Option<Str> {
97 let path = path.to_str()?;
98 let relative = path.strip_prefix("/dev/")?;
99 if relative.is_empty() {
100 return None;
101 }
102 Some(Str::from(relative.replace('/', "-")))
103}
104
105fn nonempty_env(env: &mut impl FnMut(&str) -> Option<OsString>, name: &str) -> Option<String> {
106 env(name)?
107 .into_string()
108 .ok()
109 .filter(|value| !value.is_empty())
110}
111
112#[cfg(test)]
113mod tests {
114 use std::{collections::HashMap, ffi::OsString, path::Path};
115
116 use super::{UNKNOWN_TERMINAL_ID, terminal_id_with};
117
118 #[test]
119 fn normalizes_posix_tty_paths() {
120 let no_env = |_: &str| None;
121 assert_eq!(terminal_id_with(Some(Path::new("/dev/pts/3")), no_env), "pts-3");
122 assert_eq!(terminal_id_with(Some(Path::new("/dev/ttys004")), no_env), "ttys004");
123 }
124
125 #[test]
126 fn tty_path_precedes_environment() {
127 assert_eq!(
128 terminal_id_with(Some(Path::new("/dev/pts/8")), |_| Some("ignored".into())),
129 "pts-8"
130 );
131 }
132
133 #[test]
134 fn environment_uses_exact_precedence() {
135 let variables = HashMap::from([
136 ("ZELLIJ_PANE_ID", "1"),
137 ("ZELLIJ_SESSION_NAME", "work/tree\\leaf"),
138 ("TMUX_PANE", "%2"),
139 ("CMUX_SURFACE_ID", "3"),
140 ("KITTY_WINDOW_ID", "4"),
141 ("WEZTERM_PANE", "5"),
142 ("TERM_SESSION_ID", "6"),
143 ("WT_SESSION", "7"),
144 ]);
145 let env = |name: &str| variables.get(name).map(OsString::from);
146 assert_eq!(terminal_id_with(None, env), "zellij-work-tree-leaf-1");
147
148 let ordered = [
149 ("TMUX_PANE", "tmux-%2"),
150 ("CMUX_SURFACE_ID", "cmux-3"),
151 ("KITTY_WINDOW_ID", "kitty-4"),
152 ("WEZTERM_PANE", "wezterm-5"),
153 ("TERM_SESSION_ID", "apple-6"),
154 ("WT_SESSION", "wt-7"),
155 ];
156 for (index, &(winner, expected)) in ordered.iter().enumerate() {
157 let env = |name: &str| {
158 ordered[index..]
159 .iter()
160 .find(|&&(candidate, _)| candidate == name)
161 .map(|_| OsString::from(variables[name]))
162 };
163 assert_eq!(terminal_id_with(None, env), expected, "winner: {winner}");
164 }
165 }
166
167 #[test]
168 fn empty_and_non_unicode_values_are_ignored() {
169 let env = |name: &str| match name {
170 "TMUX_PANE" => Some(OsString::new()),
171 "KITTY_WINDOW_ID" => Some("9".into()),
172 _ => None,
173 };
174 assert_eq!(terminal_id_with(None, env), "kitty-9");
175 assert_eq!(terminal_id_with(None, |_| None), UNKNOWN_TERMINAL_ID);
176 }
177}