use crate::detect::{Row, Rows};
const SHELLS: &[&str] = &[
"bash", "zsh", "fish", "dash", "sh", "ksh", "tcsh", "csh", "nu", "elvish", "xonsh",
];
pub fn detect() -> Rows {
let Some(shell) = find_shell_ancestor().or_else(shell_from_env) else {
return Vec::new();
};
let value = match shell_version(&shell) {
Some(v) => format!("{shell} {v}"),
None => shell,
};
vec![Row::val(value)]
}
fn find_shell_ancestor() -> Option<String> {
let mut pid = ppid_of("self")?;
let mut guard = 0u32;
while pid > 1 && guard < 64 {
if let Some(comm) = crate::util::read_trim(&format!("/proc/{pid}/comm")) {
let name = comm.strip_prefix('-').unwrap_or(&comm);
if SHELLS.contains(&name) {
return Some(name.to_string());
}
}
let next = ppid_of(&pid.to_string())?;
if next == pid {
break; }
pid = next;
guard += 1;
}
None
}
fn ppid_of(who: &str) -> Option<u32> {
let stat = crate::util::read_trim(&format!("/proc/{who}/stat"))?;
let after = &stat[stat.rfind(')')? + 1..];
after.split_whitespace().nth(1)?.parse().ok()
}
fn shell_version(shell: &str) -> Option<String> {
let out = crate::util::cmd(shell, &["--version"])?;
let line = out.lines().next()?;
for token in line.split_whitespace() {
if token.starts_with(|c: char| c.is_ascii_digit()) {
let ver: String = token
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
if !ver.is_empty() {
return Some(ver);
}
}
}
None
}
fn shell_from_env() -> Option<String> {
let sh = std::env::var("SHELL").ok()?;
let base = sh.rsplit('/').next()?;
if base.is_empty() {
None
} else {
Some(base.to_string())
}
}