use ratatui::style::Color;
pub fn threshold_color(percent: f64) -> Color {
if percent < 60.0 {
Color::Green
} else if percent <= 85.0 {
Color::Yellow
} else {
Color::Red
}
}
pub fn parse_cpu_top(output: &str) -> Option<f64> {
for line in output.lines() {
let trimmed = line.trim();
if trimmed.starts_with("CPU:") {
return parse_cpu_busybox(trimmed);
}
let lower = trimmed.to_lowercase();
if lower.starts_with("%cpu") || lower.starts_with("cpu(s)") {
return parse_cpu_linux_top_line(trimmed);
}
}
None
}
fn parse_cpu_busybox(line: &str) -> Option<f64> {
let words: Vec<&str> = line.split_whitespace().collect();
for i in 0..words.len().saturating_sub(1) {
if words[i + 1] == "idle" {
let pct_str = words[i].trim_end_matches('%');
if let Ok(idle) = pct_str.parse::<f64>() {
return Some((100.0 - idle).clamp(0.0, 100.0));
}
}
}
None
}
fn parse_cpu_linux_top_line(line: &str) -> Option<f64> {
let after_colon = line.split_once(':')?.1;
let fields: Vec<&str> = after_colon.split(',').collect();
for field in fields {
let field = field.trim();
let parts: Vec<&str> = field.splitn(2, [' ', '%']).collect();
if parts.len() == 2 {
let label = parts[1].trim().to_lowercase();
if label == "id" || label == "idle" || label.starts_with("id,") {
let val_str = parts[0].trim_end_matches('%');
if let Ok(idle) = val_str.parse::<f64>() {
return Some((100.0 - idle).clamp(0.0, 100.0));
}
}
}
}
None
}
pub fn parse_cpu_top_macos(output: &str) -> Option<f64> {
for line in output.lines() {
let lower = line.trim().to_lowercase();
if lower.starts_with("cpu usage:") {
for part in line.split(',') {
let part = part.trim();
if part.to_lowercase().ends_with("idle") {
let token = part.split_whitespace().next()?;
let idle: f64 = token.trim_end_matches('%').parse().ok()?;
return Some((100.0 - idle).clamp(0.0, 100.0));
}
}
}
}
None
}
pub fn parse_cpu_proc_stat(output: &str) -> Option<f64> {
let line = output.lines().next()?;
let mut parts = line.split_whitespace();
let label = parts.next()?;
if !label.starts_with("cpu") {
return None;
}
let values: Vec<u64> = parts.filter_map(|s| s.parse().ok()).collect();
if values.len() < 4 {
return None;
}
let idle = values[3] + values.get(4).copied().unwrap_or(0); let total: u64 = values.iter().sum();
if total == 0 {
return None;
}
Some(((total - idle) as f64 / total as f64 * 100.0).clamp(0.0, 100.0))
}
pub fn parse_ram_free(output: &str) -> Option<f64> {
let mem_line = output
.lines()
.find(|l| l.trim_start().starts_with("Mem:"))?;
let fields: Vec<&str> = mem_line.split_whitespace().collect();
let total: f64 = fields.get(1)?.parse().ok()?;
if total == 0.0 {
return None;
}
if let Some(available_str) = fields.get(6) {
let available: f64 = available_str.parse().ok()?;
Some(((total - available) / total * 100.0).clamp(0.0, 100.0))
} else if let Some(free_str) = fields.get(3) {
let free: f64 = free_str.parse().ok()?;
Some(((total - free) / total * 100.0).clamp(0.0, 100.0))
} else {
None
}
}
pub fn parse_ram_vmstat(vm_stat_output: &str, memsize_output: &str) -> Option<f64> {
let total_bytes: f64 = memsize_output.split(':').nth(1)?.trim().parse().ok()?;
if total_bytes == 0.0 {
return None;
}
let page_size: f64 = vm_stat_output
.lines()
.next()
.and_then(|l| {
let idx = l.find("page size of")?;
let rest = &l[idx + "page size of".len()..];
rest.split_whitespace().next()?.parse().ok()
})
.unwrap_or(4096.0);
let mut free_pages: f64 = 0.0;
let mut speculative_pages: f64 = 0.0;
for line in vm_stat_output.lines() {
if let Some(val) = parse_vmstat_line(line, "Pages free:") {
free_pages = val;
} else if let Some(val) = parse_vmstat_line(line, "Pages speculative:") {
speculative_pages = val;
}
}
let available_bytes = (free_pages + speculative_pages) * page_size;
Some(((total_bytes - available_bytes) / total_bytes * 100.0).clamp(0.0, 100.0))
}
fn parse_vmstat_line(line: &str, prefix: &str) -> Option<f64> {
if line.trim_start().starts_with(prefix) {
line.split(':')
.nth(1)?
.trim()
.trim_end_matches('.')
.parse()
.ok()
} else {
None
}
}
pub fn parse_disk_df(output: &str) -> Option<f64> {
let mut lines = output.lines();
let header = lines.next()?;
let data_line = lines.next()?;
let header_fields: Vec<&str> = header.split_whitespace().collect();
let data_fields: Vec<&str> = data_line.split_whitespace().collect();
let pct_col = header_fields.iter().position(|h| {
*h == "Use%" || *h == "Capacity" || h.ends_with("Use%") || h.ends_with("Capacity")
})?;
let pct_str = data_fields.get(pct_col)?;
let pct_str = pct_str.trim_end_matches('%');
pct_str.parse::<f64>().ok().map(|p| p.clamp(0.0, 100.0))
}
pub fn parse_uptime(output: &str) -> Option<String> {
let line = output.lines().next()?;
let up_idx = line.to_lowercase().find(" up ")?;
let after_up = line[up_idx + 4..].trim();
let mut parts: Vec<&str> = Vec::new();
for part in after_up.split(", ") {
if part.trim().contains("user") {
break;
}
parts.push(part.trim());
}
let uptime_str = if parts.is_empty() {
after_up.to_string()
} else {
parts.join(", ")
};
if let Some(days_idx) = uptime_str.find(" days") {
let end = days_idx + 5; Some(uptime_str[..end].to_string())
} else if let Some(day_idx) = uptime_str.find(" day,") {
let end = day_idx + 4; Some(uptime_str[..end].to_string())
} else {
Some(uptime_str)
}
}
pub fn parse_loadavg(output: &str) -> Option<String> {
let line = output.lines().next()?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
Some(format!("{} {} {}", parts[0], parts[1], parts[2]))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cpu_top_ubuntu_procps_ng() {
let out = "%Cpu(s): 2.3 us, 0.7 sy, 0.0 ni, 96.7 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st";
let result = parse_cpu_top(out).expect("should parse");
assert!((result - 3.3).abs() < 0.1, "got {result}");
}
#[test]
fn test_cpu_top_centos_old_format() {
let out = "Cpu(s): 2.3%us, 0.7%sy, 0.0%ni, 96.7%id, 0.3%wa, 0.0%hi, 0.0%si, 0.0%st";
let result = parse_cpu_top(out).expect("should parse");
assert!((result - 3.3).abs() < 0.1, "got {result}");
}
#[test]
fn test_cpu_top_alpine_busybox() {
let out = "CPU: 4% usr 1% sys 0% nic 94% idle 0% io 0% irq 1% sirq";
let result = parse_cpu_top(out).expect("should parse");
assert!((result - 6.0).abs() < 0.1, "got {result}");
}
#[test]
fn test_cpu_top_macos() {
let out = "CPU usage: 3.17% user, 1.56% sys, 95.26% idle";
let result = parse_cpu_top_macos(out).expect("should parse");
assert!((result - 4.74).abs() < 0.1, "got {result}");
}
#[test]
fn test_cpu_top_macos_full_output() {
let out = "Processes: 412 total, 2 running, 410 sleeping, 2178 threads\n\
2024/01/15 14:23:05\n\
Load Avg: 1.52, 1.74, 1.89\n\
CPU usage: 5.71% user, 2.57% sys, 91.71% idle\n\
SharedLibs: 438M resident, 108M data, 24M linkedit.";
let result = parse_cpu_top_macos(out).expect("should parse");
assert!((result - 8.29).abs() < 0.1, "got {result}");
}
#[test]
fn test_cpu_proc_stat() {
let out = "cpu 74608 2520 24433 1117073 6176 4054 0 0 0 0";
let result = parse_cpu_proc_stat(out).expect("should parse");
assert!(result > 0.0 && result < 100.0, "got {result}");
}
#[test]
fn test_cpu_empty_returns_none() {
assert!(parse_cpu_top("").is_none());
assert!(parse_cpu_top_macos("").is_none());
assert!(parse_cpu_proc_stat("").is_none());
}
#[test]
fn test_cpu_unrecognized_returns_none() {
assert!(parse_cpu_top("no cpu info here").is_none());
}
#[test]
fn test_ram_free_modern_7col() {
let out =
" total used free shared buff/cache available\n\
Mem: 8192000000 3145728000 512000000 134217728 4534272000 4915200000\n\
Swap: 2147483648 0 2147483648";
let result = parse_ram_free(out).expect("should parse");
assert!((result - 40.0).abs() < 1.0, "got {result}");
}
#[test]
fn test_ram_free_busybox_4col() {
let out = " total used free\n\
Mem: 1018736 524288 494448";
let result = parse_ram_free(out).expect("should parse");
assert!((result - 51.5).abs() < 1.0, "got {result}");
}
#[test]
fn test_ram_empty_returns_none() {
assert!(parse_ram_free("").is_none());
assert!(parse_ram_vmstat("", "").is_none());
}
#[test]
fn test_ram_vmstat_macos() {
let vm_stat = "Mach Virtual Memory Statistics: (page size of 16384 bytes)\n\
Pages free: 23456.\n\
Pages active: 456789.\n\
Pages inactive: 123456.\n\
Pages speculative: 12345.\n\
Pages throttled: 0.\n\
Pages wired down: 98765.\n";
let memsize = "hw.memsize: 17179869184";
let result = parse_ram_vmstat(vm_stat, memsize).expect("should parse");
assert!(result > 0.0 && result <= 100.0, "got {result}");
}
#[test]
fn test_disk_df_linux() {
let out = "Filesystem 1K-blocks Used Available Use% Mounted on\n\
/dev/sda1 51475068 9000000 39841436 19% /";
let result = parse_disk_df(out).expect("should parse");
assert!((result - 19.0).abs() < 0.1, "got {result}");
}
#[test]
fn test_disk_df_macos() {
let out = "Filesystem 1024-blocks Used Available Capacity iused ifree %iused Mounted on\n\
/dev/disk3s5 994662584 516879368 400765064 57% 5488234 4293478045 0% /";
let result = parse_disk_df(out).expect("should parse");
assert!((result - 57.0).abs() < 0.1, "got {result}");
}
#[test]
fn test_disk_df_100pct() {
let out = "Filesystem 1K-blocks Used Available Use% Mounted on\n\
/dev/sda1 51475068 51475068 0 100% /";
let result = parse_disk_df(out).expect("should parse");
assert!((result - 100.0).abs() < 0.1, "got {result}");
}
#[test]
fn test_disk_empty_returns_none() {
assert!(parse_disk_df("").is_none());
assert!(parse_disk_df("only header\n").is_none());
}
#[test]
fn test_uptime_linux_days() {
let out = " 14:23:45 up 2 days, 3:45, 2 users, load average: 0.15, 0.10, 0.08";
let result = parse_uptime(out).expect("should parse");
assert_eq!(result, "2 days", "expected '2 days' only, got: {result}");
}
#[test]
fn test_uptime_linux_hours() {
let out = " 10:00:00 up 3:45, 1 user, load average: 0.00, 0.01, 0.05";
let result = parse_uptime(out).expect("should parse");
assert!(result.contains("3:45"), "missing '3:45': {result}");
}
#[test]
fn test_uptime_empty_returns_none() {
assert!(parse_uptime("").is_none());
}
#[test]
fn test_loadavg() {
let out = "0.15 0.10 0.08 1/423 12345\n";
let result = parse_loadavg(out).expect("should parse");
assert_eq!(result, "0.15 0.10 0.08");
}
#[test]
fn test_threshold_color() {
assert_eq!(threshold_color(0.0), Color::Green);
assert_eq!(threshold_color(59.9), Color::Green);
assert_eq!(threshold_color(60.0), Color::Yellow);
assert_eq!(threshold_color(85.0), Color::Yellow);
assert_eq!(threshold_color(85.1), Color::Red);
assert_eq!(threshold_color(100.0), Color::Red);
}
}