pub fn running_process_names() -> Vec<String> {
#[cfg(target_os = "linux")]
{
linux_comms()
}
#[cfg(not(target_os = "linux"))]
{
ps_comms()
}
}
#[cfg(target_os = "linux")]
fn linux_comms() -> Vec<String> {
let mut out = Vec::new();
if let Ok(rd) = std::fs::read_dir("/proc") {
for e in rd.flatten() {
let is_pid = e
.file_name()
.to_str()
.map(|s| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()))
.unwrap_or(false);
if is_pid {
if let Ok(comm) = std::fs::read_to_string(e.path().join("comm")) {
out.push(comm.trim().to_string());
}
}
}
}
out
}
#[cfg(not(target_os = "linux"))]
fn ps_comms() -> Vec<String> {
match std::process::Command::new("ps")
.args(["-Ao", "comm="])
.output()
{
Ok(o) => String::from_utf8_lossy(&o.stdout)
.lines()
.map(|l| l.trim().rsplit('/').next().unwrap_or("").to_string())
.filter(|s| !s.is_empty())
.collect(),
Err(_) => Vec::new(),
}
}
pub fn tool_running(tool: &str, comms: &[String]) -> bool {
let want = match tool {
"claude-code" => "claude",
"codex" => "codex",
_ => return false,
};
comms.iter().any(|c| c == want)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_exact_binary_name_only() {
let comms = vec![
"codex".to_string(),
"bash".to_string(),
"some-claude-helper".to_string(), ];
assert!(tool_running("codex", &comms));
assert!(!tool_running("claude-code", &comms), "no exact 'claude'");
assert!(!tool_running("unknown", &comms));
}
#[test]
fn matches_claude_when_present() {
let comms = vec!["claude".to_string(), "node".to_string()];
assert!(tool_running("claude-code", &comms));
assert!(!tool_running("codex", &comms));
}
}