use serde_json::{json, Value};
use std::process::Command;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ProcCounts {
pub total: u32,
pub running: u32,
pub sleeping: u32,
pub zombie: u32,
pub dwait: u32,
pub stopped: u32,
}
pub fn read_proc_counts() -> Option<ProcCounts> {
let out = Command::new("ps").args(["-eo", "stat="]).output().ok()?;
if !out.status.success() {
return None;
}
let text = std::str::from_utf8(&out.stdout).ok()?;
Some(tally(text))
}
pub fn tally(text: &str) -> ProcCounts {
let mut c = ProcCounts::default();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
c.total += 1;
match line.as_bytes()[0] {
b'R' => c.running += 1,
b'S' | b'I' => c.sleeping += 1, b'Z' => c.zombie += 1,
b'D' => c.dwait += 1,
b'T' | b't' => c.stopped += 1, _ => {}
}
}
c
}
pub fn process_count(format: &str, warn_zombie: bool) -> Option<Vec<Value>> {
let c = read_proc_counts()?;
let contents = render_format(format, &c);
let mut groups = vec![
Value::String("process_count".into()),
Value::String("proc".into()),
];
if warn_zombie && c.zombie > 0 {
groups.insert(0, Value::String("process_count_zombie".into()));
}
Some(vec![json!({
"contents": contents,
"highlight_groups": groups,
"divider_highlight_group": "background:divider",
})])
}
fn render_format(fmt: &str, c: &ProcCounts) -> String {
fmt.replace("{total}", &c.total.to_string())
.replace("{running}", &c.running.to_string())
.replace("{sleeping}", &c.sleeping.to_string())
.replace("{zombie}", &c.zombie.to_string())
.replace("{dwait}", &c.dwait.to_string())
.replace("{stopped}", &c.stopped.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tally_empty_input_yields_zero_counts() {
let c = tally("");
assert_eq!(c, ProcCounts::default());
assert_eq!(c.total, 0);
}
#[test]
fn tally_counts_each_state_code() {
let text = "R\nS\nS\nZ\nD\nT\nI\nR\n";
let c = tally(text);
assert_eq!(c.total, 8);
assert_eq!(c.running, 2);
assert_eq!(c.sleeping, 3);
assert_eq!(c.zombie, 1);
assert_eq!(c.dwait, 1);
assert_eq!(c.stopped, 1);
}
#[test]
fn tally_ignores_modifier_suffixes() {
let text = "R+\nS<\nSs\nD<L\n";
let c = tally(text);
assert_eq!(c.total, 4);
assert_eq!(c.running, 1);
assert_eq!(c.sleeping, 2);
assert_eq!(c.dwait, 1);
}
#[test]
fn tally_skips_blank_lines() {
let text = "R\n\n\nS\n";
let c = tally(text);
assert_eq!(c.total, 2);
assert_eq!(c.running, 1);
assert_eq!(c.sleeping, 1);
}
#[test]
fn tally_unknown_state_does_not_increment_category_but_does_total() {
let c = tally("X\nY\n?\n");
assert_eq!(c.total, 3);
assert_eq!(c.running, 0);
assert_eq!(c.sleeping, 0);
assert_eq!(c.zombie, 0);
assert_eq!(c.dwait, 0);
assert_eq!(c.stopped, 0);
}
#[test]
fn tally_handles_lowercase_t_for_tracing_stop() {
let c = tally("T\nt\nT\n");
assert_eq!(c.stopped, 3);
}
#[test]
fn render_format_substitutes_all_tokens() {
let c = ProcCounts {
total: 100,
running: 3,
sleeping: 90,
zombie: 1,
dwait: 2,
stopped: 4,
};
let out = render_format(
"{total} {running}R {sleeping}S {zombie}Z {dwait}D {stopped}T",
&c,
);
assert_eq!(out, "100 3R 90S 1Z 2D 4T");
}
#[test]
fn render_format_leaves_unknown_tokens_intact() {
let c = ProcCounts {
total: 5,
..Default::default()
};
let s = render_format("{total} {cpu}", &c);
assert_eq!(s, "5 {cpu}");
}
#[test]
fn read_proc_counts_real_probe_has_a_total() {
let c = read_proc_counts().expect("ps must be available on POSIX CI");
assert!(
c.total > 0,
"ps reported no processes — impossible while this test is running"
);
}
#[test]
fn process_count_returns_one_chunk_when_ps_available() {
let out = process_count("{total}", false).expect("ps available");
assert_eq!(out.len(), 1);
let contents = out[0]["contents"].as_str().unwrap_or("");
assert!(
contents.chars().all(|c| c.is_ascii_digit()),
"contents should be a numeric string, got {contents:?}"
);
}
#[test]
fn process_count_warn_zombie_adds_zombie_highlight_group() {
let c = ProcCounts {
total: 5,
zombie: 1,
..Default::default()
};
let mut groups: Vec<String> = vec!["process_count".into(), "proc".into()];
let warn_zombie = true;
if warn_zombie && c.zombie > 0 {
groups.insert(0, "process_count_zombie".into());
}
assert_eq!(groups[0], "process_count_zombie");
assert_eq!(groups.len(), 3);
}
}