use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Multiplexer {
Tmux,
Herdr,
None,
}
impl Multiplexer {
pub(crate) fn detect() -> Self {
static CACHE: OnceLock<Multiplexer> = OnceLock::new();
*CACHE.get_or_init(detect_uncached)
}
pub(crate) fn detect_from(present: impl Fn(&str) -> bool) -> Self {
if present("HERDR_ENV")
|| present("HERDR_PANE_ID")
|| present("HERDR_TAB_ID")
|| present("HERDR_WORKSPACE_ID")
{
Self::Herdr
} else if present("TMUX") || present("TMUX_PANE") {
Self::Tmux
} else {
Self::None
}
}
}
fn detect_uncached() -> Multiplexer {
let mux = Multiplexer::detect_from(|v| std::env::var_os(v).is_some());
if mux == Multiplexer::Herdr && std::env::var_os("TMUX").is_some() {
return disambiguate_nested(&tmux_pane_pids(), parent_pid(), std::process::id());
}
mux
}
fn disambiguate_nested(tmux_pane_pids: &[u32], parent: u32, self_pid: u32) -> Multiplexer {
if tmux_pane_pids.contains(&parent) || tmux_pane_pids.contains(&self_pid) {
Multiplexer::Tmux
} else {
Multiplexer::Herdr
}
}
fn tmux_pane_pids() -> Vec<u32> {
let output = std::process::Command::new("tmux")
.args(["list-panes", "-a", "-F", "#{pane_pid}"])
.output();
match output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.filter_map(|l| l.trim().parse::<u32>().ok())
.collect(),
_ => Vec::new(),
}
}
#[cfg(unix)]
fn parent_pid() -> u32 {
unsafe { libc::getppid() as u32 }
}
#[cfg(not(unix))]
fn parent_pid() -> u32 {
0
}
#[cfg(test)]
mod tests {
use super::*;
fn present<'a>(vars: &'a [&'a str]) -> impl Fn(&str) -> bool + 'a {
move |v| vars.contains(&v)
}
#[test]
fn plain_tmux_detects_tmux() {
assert_eq!(
Multiplexer::detect_from(present(&["TMUX"])),
Multiplexer::Tmux
);
assert_eq!(
Multiplexer::detect_from(present(&["TMUX", "TMUX_PANE"])),
Multiplexer::Tmux
);
}
#[test]
fn plain_herdr_detects_herdr() {
assert_eq!(
Multiplexer::detect_from(present(&["HERDR_ENV"])),
Multiplexer::Herdr
);
assert_eq!(
Multiplexer::detect_from(present(&["HERDR_PANE_ID"])),
Multiplexer::Herdr
);
assert_eq!(
Multiplexer::detect_from(present(&["HERDR_TAB_ID", "HERDR_WORKSPACE_ID"])),
Multiplexer::Herdr
);
}
#[test]
fn plain_terminal_is_none() {
assert_eq!(Multiplexer::detect_from(present(&[])), Multiplexer::None);
assert_eq!(
Multiplexer::detect_from(present(&["WEZTERM_PANE"])),
Multiplexer::None
);
}
#[test]
fn nested_tmux_pane_detects_tmux() {
assert_eq!(
disambiguate_nested(&[100, 200, 300], 200, 400),
Multiplexer::Tmux
);
assert_eq!(
disambiguate_nested(&[100, 200, 300], 0, 300),
Multiplexer::Tmux
);
}
#[test]
fn nested_herdr_pane_detects_herdr() {
assert_eq!(
disambiguate_nested(&[100, 200, 300], 500, 600),
Multiplexer::Herdr
);
assert_eq!(disambiguate_nested(&[], 500, 600), Multiplexer::Herdr);
}
#[cfg(unix)]
#[test]
fn parent_pid_is_nonzero() {
assert_ne!(parent_pid(), 0);
}
#[test]
#[ignore]
fn probe_detect() {
println!("detect() => {:?}", Multiplexer::detect());
println!(
" self_pid={} parent_pid={} tmux_pane_pids={:?}",
std::process::id(),
parent_pid(),
tmux_pane_pids(),
);
}
}