refined statmon

This commit is contained in:
2026-08-12 07:56:02 -07:00
parent 58d07d012f
commit 6366cdd40d
5 changed files with 1466 additions and 235 deletions

View File

@@ -0,0 +1 @@
OPENWEATHER_API_KEY=1d88683051588bb011bfd5be2192ecc0

File diff suppressed because it is too large Load Diff

View File

@@ -7,3 +7,7 @@ edition = "2024"
sysinfo = "0.30" sysinfo = "0.30"
crossterm = "0.27" crossterm = "0.27"
chrono = "0.4" chrono = "0.4"
dotenvy = "0.15"
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@@ -0,0 +1,127 @@
[
{
"name": "Marc",
"month": 1,
"day": 2
},
{
"name": "Wedding",
"month": 1,
"day": 29
},
{
"name": "Yvonne",
"month": 2,
"day": 4
},
{
"name": "Mutti und Papa Hochzeitstag",
"month": 2,
"day": 8
},
{
"name": "Patrick Ryu",
"month": 4,
"day": 1
},
{
"name": "Isabell",
"month": 4,
"day": 5
},
{
"name": "Avy",
"month": 4,
"day": 6
},
{
"name": "Florian Schaedlich",
"month": 4,
"day": 28
},
{
"name": "Frank Schaedlich",
"month": 5,
"day": 9
},
{
"name": "Bruce",
"month": 6,
"day": 14
},
{
"name": "Martin",
"month": 6,
"day": 21
},
{
"name": "Philipp",
"month": 7,
"day": 16
},
{
"name": "Zen",
"month": 8,
"day": 1
},
{
"name": "Max",
"month": 8,
"day": 4
},
{
"name": "Nadine",
"month": 8,
"day": 6
},
{
"name": "Mario",
"month": 9,
"day": 1
},
{
"name": "Lara",
"month": 9,
"day": 4
},
{
"name": "Cal",
"month": 9,
"day": 24
},
{
"name": "Andreas",
"month": 9,
"day": 25
},
{
"name": "Papa",
"month": 9,
"day": 29
},
{
"name": "Noelle",
"month": 10,
"day": 2
},
{
"name": "Ingo",
"month": 10,
"day": 4
},
{
"name": "Oma",
"month": 10,
"day": 14
},
{
"name": "Bruce Passing",
"month": 10,
"day": 26
},
{
"name": "Sylvia",
"month": 11,
"day": 5
}
]

View File

@@ -1,27 +1,54 @@
use chrono::Local; use chrono::{Datelike, Local, NaiveDate};
use crossterm::{ use crossterm::{
cursor, execute, cursor, execute,
style::{Color, ResetColor, SetForegroundColor}, style::{Color, ResetColor, SetForegroundColor},
terminal::{Clear, ClearType}, terminal::{Clear, ClearType},
}; };
use serde::Deserialize;
use std::{ use std::{
fs, env, fs,
io::{stdout, Write}, io::{stdout, Write},
path::Path, path::{Path, PathBuf},
process::Command, process::Command,
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use sysinfo::{ use sysinfo::{
Components, CpuRefreshKind, MemoryRefreshKind, Networks, ProcessRefreshKind, Components, CpuRefreshKind, MemoryRefreshKind, Networks, ProcessRefreshKind, RefreshKind,
RefreshKind, System, System,
}; };
// --- BTOP Theme RGB Constants --- // --- BTOP Theme RGB Constants ---
const FG_MAIN: Color = Color::Rgb { r: 157, g: 132, b: 98 }; // #9d8462 const FG_MAIN: Color = Color::Rgb { r: 157, g: 132, b: 98 }; // #9d8462
const ACCENT: Color = Color::Rgb { r: 255, g: 145, b: 0 }; // #ff9100 const ACCENT: Color = Color::Rgb { r: 255, g: 145, b: 0 }; // #ff9100
const ALERT: Color = Color::Rgb { r: 255, g: 0, b: 0 }; // #ff0000 const ALERT: Color = Color::Rgb { r: 255, g: 0, b: 0 }; // #ff0000
const METER_BG: Color = Color::Rgb { r: 48, g: 51, b: 64 }; // #303340
// --- Location Settings ---
const ZIP_CODE: &str = "95662,US";
const LAT: f64 = 38.67995;
const LON: f64 = -121.29892;
#[derive(Deserialize, Debug, Clone)]
struct Birthday {
name: String,
month: u32,
day: u32,
}
struct WeatherState {
aqi_desc: String,
aqi_val: u64,
today_summary: String,
tomorrow_summary: String,
}
fn get_config_dir() -> PathBuf {
if let Ok(home) = env::var("HOME") {
PathBuf::from(home).join(".config/hypr/scripts/statmon")
} else {
PathBuf::from(".")
}
}
fn send_notification(summary: &str, body: &str, urgency: &str) { fn send_notification(summary: &str, body: &str, urgency: &str) {
let _ = Command::new("notify-send") let _ = Command::new("notify-send")
@@ -41,19 +68,209 @@ fn make_bar(pct: f32, width: usize) -> String {
format!("[{}{}]", "".repeat(filled), "".repeat(empty)) format!("[{}{}]", "".repeat(filled), "".repeat(empty))
} }
fn fetch_air_quality(api_key: &str) -> (String, u64) {
if api_key.is_empty() {
return ("API Key Missing".to_string(), 0);
}
let url = format!(
"http://api.openweathermap.org/data/2.5/air_pollution?lat={}&lon={}&appid={}",
LAT, LON, api_key
);
if let Ok(resp) = reqwest::blocking::get(&url) {
if let Ok(json) = resp.json::<serde_json::Value>() {
if let Some(aqi) = json["list"][0]["main"]["aqi"].as_u64() {
let desc = match aqi {
1 => "Good (1)",
2 => "Fair (2)",
3 => "Moderate (3)",
4 => "Poor (4)",
5 => "Very Poor (5)",
_ => "Unknown",
};
return (desc.to_string(), aqi);
}
}
}
("Unavailable".to_string(), 0)
}
fn fetch_weather_and_forecast(api_key: &str) -> (String, String) {
if api_key.is_empty() {
return ("API Key Missing".to_string(), "API Key Missing".to_string());
}
let url = format!(
"https://api.openweathermap.org/data/2.5/forecast?zip={}&appid={}&units=imperial",
ZIP_CODE, api_key
);
let mut today_str = "N/A".to_string();
let mut tomorrow_str = "N/A".to_string();
if let Ok(resp) = reqwest::blocking::get(&url) {
if let Ok(json) = resp.json::<serde_json::Value>() {
if let Some(list) = json["list"].as_array() {
let mut max_pop_today = 0.0f64;
let mut max_pop_tomorrow = 0.0f64;
let mut today_rain = false;
let mut tomorrow_rain = false;
let now = Local::now().date_naive();
let tomorrow = now + chrono::Days::new(1);
for item in list {
if let Some(dt_txt) = item["dt_txt"].as_str() {
if let Ok(date) = NaiveDate::parse_from_str(&dt_txt[..10], "%Y-%m-%d") {
let pop = item["pop"].as_f64().unwrap_or(0.0);
let weather_main = item["weather"][0]["main"].as_str().unwrap_or("");
if date == now {
if pop > max_pop_today { max_pop_today = pop; }
if weather_main.to_lowercase().contains("rain") || pop > 0.3 {
today_rain = true;
}
} else if date == tomorrow {
if pop > max_pop_tomorrow { max_pop_tomorrow = pop; }
if weather_main.to_lowercase().contains("rain") || pop > 0.3 {
tomorrow_rain = true;
}
}
}
}
}
today_str = format!(
"{} (Rain Chance: {:.0}%)",
if today_rain { "Rain Expected" } else { "Clear/No Major Rain" },
max_pop_today * 100.0
);
tomorrow_str = format!(
"{} (Rain Chance: {:.0}%)",
if tomorrow_rain { "Rain Expected" } else { "Clear/No Major Rain" },
max_pop_tomorrow * 100.0
);
}
}
}
(today_str, tomorrow_str)
}
fn get_upcoming_birthdays() -> Vec<String> {
let mut upcoming = Vec::new();
let bday_file = get_config_dir().join("birthdays.json");
if let Ok(content) = fs::read_to_string(&bday_file) {
if let Ok(birthdays) = serde_json::from_str::<Vec<Birthday>>(&content) {
let today = Local::now().date_naive();
for b in birthdays {
for i in 0..=2 {
let check_date = today + chrono::Days::new(i);
if check_date.month() == b.month && check_date.day() == b.day {
let label = match i {
0 => "TODAY!".to_string(),
1 => "Tomorrow".to_string(),
_ => format!("In {} days", i),
};
upcoming.push(format!("{} - {} ({}/{})", b.name, label, b.month, b.day));
}
}
}
}
}
upcoming
}
fn render_calendar_and_birthdays(
stdout: &mut std::io::Stdout,
upcoming_birthdays: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
let now = Local::now();
let year = now.year();
let month = now.month();
let today = now.day();
let first_of_month = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
let days_in_month = if month == 12 {
NaiveDate::from_ymd_opt(year + 1, 1, 1)
} else {
NaiveDate::from_ymd_opt(year, month + 1, 1)
}
.unwrap()
.signed_duration_since(first_of_month)
.num_days() as u32;
let start_weekday = first_of_month.weekday().num_days_from_sunday();
// Side-by-Side Headers
execute!(stdout, SetForegroundColor(ACCENT))?;
write!(stdout, " {:<27}", format!("CALENDAR ({} {})", now.format("%B"), year))?;
execute!(stdout, SetForegroundColor(ACCENT))?;
writeln!(stdout, "UPCOMING EVENTS")?;
execute!(stdout, SetForegroundColor(FG_MAIN))?;
write!(stdout, " Su Mo Tu We Th Fr Sa ")?;
writeln!(stdout, "----------------------------------")?;
// Build Calendar Lines
let mut calendar_lines: Vec<String> = Vec::new();
let mut current_line = String::new();
for _ in 0..start_weekday {
current_line.push_str(" ");
}
let mut current_col = start_weekday;
for day in 1..=days_in_month {
if day == today {
current_line.push_str(&format!("\x1b[38;2;255;0;0m{:>2}\x1b[0m ", day));
} else {
current_line.push_str(&format!("{:>2} ", day));
}
current_col += 1;
if current_col % 7 == 0 || day == days_in_month {
if day == days_in_month && current_col % 7 != 0 {
let missing_spaces = ((7 - (current_col % 7)) * 3) as usize;
current_line.push_str(&" ".repeat(missing_spaces));
}
calendar_lines.push(current_line.clone());
current_line.clear();
}
}
let total_rows = calendar_lines.len().max(upcoming_birthdays.len().max(1));
for i in 0..total_rows {
execute!(stdout, SetForegroundColor(FG_MAIN))?;
if i < calendar_lines.len() {
write!(stdout, " {} ", calendar_lines[i])?;
} else {
write!(stdout, " ")?;
}
write!(stdout, " ")?;
if i < upcoming_birthdays.len() {
execute!(stdout, SetForegroundColor(ACCENT))?;
writeln!(stdout, "• {}", upcoming_birthdays[i])?;
} else if i == 0 && upcoming_birthdays.is_empty() {
execute!(stdout, SetForegroundColor(FG_MAIN))?;
writeln!(stdout, "No events in next 2 days")?;
} else {
writeln!(stdout)?;
}
}
Ok(())
}
fn get_battery_info() -> (String, u32) { fn get_battery_info() -> (String, u32) {
let bat_paths = ["/sys/class/power_supply/BAT0", "/sys/class/power_supply/BAT1"]; let bat_paths = ["/sys/class/power_supply/BAT0", "/sys/class/power_supply/BAT1"];
for path in bat_paths { for path in bat_paths {
if Path::new(path).exists() { if Path::new(path).exists() {
let cap_str = fs::read_to_string(format!("{}/capacity", path)) let cap_str = fs::read_to_string(format!("{}/capacity", path)).unwrap_or_default().trim().to_string();
.unwrap_or_default() let status = fs::read_to_string(format!("{}/status", path)).unwrap_or_default().trim().to_string();
.trim()
.to_string();
let status = fs::read_to_string(format!("{}/status", path))
.unwrap_or_default()
.trim()
.to_string();
if let Ok(cap) = cap_str.parse::<u32>() { if let Ok(cap) = cap_str.parse::<u32>() {
return (format!("{}% ({})", cap, status), cap); return (format!("{}% ({})", cap, status), cap);
} }
@@ -67,17 +284,9 @@ fn get_mouse_battery() -> String {
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
if name.starts_with("hid") || name.starts_with("mouse") { if name.starts_with("hid") || name.starts_with("mouse") {
let cap = fs::read_to_string(path.join("capacity")) let cap = fs::read_to_string(path.join("capacity")).unwrap_or_default().trim().to_string();
.unwrap_or_default() let status = fs::read_to_string(path.join("status")).unwrap_or_default().trim().to_string();
.trim()
.to_string();
let status = fs::read_to_string(path.join("status"))
.unwrap_or_default()
.trim()
.to_string();
if !cap.is_empty() { if !cap.is_empty() {
let status_str = if status.is_empty() { "Wireless".to_string() } else { status }; let status_str = if status.is_empty() { "Wireless".to_string() } else { status };
return format!("{}% ({})", cap, status_str); return format!("{}% ({})", cap, status_str);
@@ -96,23 +305,6 @@ fn get_cpu_temp(components: &Components) -> (String, f32) {
return (format!("{:.0}°C", temp), temp); return (format!("{:.0}°C", temp), temp);
} }
} }
if let Ok(entries) = fs::read_dir("/sys/class/hwmon") {
for entry in entries.flatten() {
let path = entry.path();
if let Ok(name) = fs::read_to_string(path.join("name")) {
if name.contains("coretemp") || name.contains("k10temp") || name.contains("cpu") {
if let Ok(temp_str) = fs::read_to_string(path.join("temp1_input")) {
if let Ok(temp_val) = temp_str.trim().parse::<f32>() {
let temp = temp_val / 1000.0;
return (format!("{:.0}°C", temp), temp);
}
}
}
}
}
}
("N/A".to_string(), 0.0) ("N/A".to_string(), 0.0)
} }
@@ -120,7 +312,6 @@ fn get_gpu_info() -> (String, f32) {
for card_num in 0..4 { for card_num in 0..4 {
let card_path = format!("/sys/class/drm/card{}/device", card_num); let card_path = format!("/sys/class/drm/card{}/device", card_num);
let path = Path::new(&card_path); let path = Path::new(&card_path);
if path.exists() { if path.exists() {
let busy_file = if path.join("gpu_busy_percent").exists() { let busy_file = if path.join("gpu_busy_percent").exists() {
Some(path.join("gpu_busy_percent")) Some(path.join("gpu_busy_percent"))
@@ -131,23 +322,9 @@ fn get_gpu_info() -> (String, f32) {
}; };
if let Some(bf) = busy_file { if let Some(bf) = busy_file {
let busy_pct = fs::read_to_string(bf) let busy_pct = fs::read_to_string(bf).unwrap_or_default().trim().parse::<f32>().unwrap_or(0.0);
.unwrap_or_default() let vram_used = fs::read_to_string(path.join("mem_info_vram_used")).unwrap_or_default().trim().parse::<u64>().unwrap_or(0) / 1024 / 1024;
.trim() let vram_total = fs::read_to_string(path.join("mem_info_vram_total")).unwrap_or_default().trim().parse::<u64>().unwrap_or(0) / 1024 / 1024;
.parse::<f32>()
.unwrap_or(0.0);
let vram_used = fs::read_to_string(path.join("mem_info_vram_used"))
.unwrap_or_default()
.trim()
.parse::<u64>()
.unwrap_or(0) / 1024 / 1024;
let vram_total = fs::read_to_string(path.join("mem_info_vram_total"))
.unwrap_or_default()
.trim()
.parse::<u64>()
.unwrap_or(0) / 1024 / 1024;
return if vram_total > 0 { return if vram_total > 0 {
(format!("{} (VRAM: {}MB / {}MB)", make_bar(busy_pct, 16), vram_used, vram_total), busy_pct) (format!("{} (VRAM: {}MB / {}MB)", make_bar(busy_pct, 16), vram_used, vram_total), busy_pct)
@@ -188,25 +365,19 @@ fn get_disk_io(last_read_sectors: &mut u64, last_write_sectors: &mut u64, elapse
if parts.len() >= 10 { if parts.len() >= 10 {
let dev_name = parts[2]; let dev_name = parts[2];
if dev_name.starts_with("nvme") && dev_name.ends_with("n1") || dev_name.starts_with("sd") && dev_name.len() == 3 { if dev_name.starts_with("nvme") && dev_name.ends_with("n1") || dev_name.starts_with("sd") && dev_name.len() == 3 {
let r_sectors: u64 = parts[5].parse().unwrap_or(0); total_read += parts[5].parse::<u64>().unwrap_or(0);
let w_sectors: u64 = parts[9].parse().unwrap_or(0); total_write += parts[9].parse::<u64>().unwrap_or(0);
total_read += r_sectors;
total_write += w_sectors;
} }
} }
} }
let read_mb = if *last_read_sectors > 0 && total_read >= *last_read_sectors { let read_mb = if *last_read_sectors > 0 && total_read >= *last_read_sectors {
((total_read - *last_read_sectors) as f32 * 512.0) / (1024.0 * 1024.0 * elapsed_secs) ((total_read - *last_read_sectors) as f32 * 512.0) / (1024.0 * 1024.0 * elapsed_secs)
} else { } else { 0.0 };
0.0
};
let write_mb = if *last_write_sectors > 0 && total_write >= *last_write_sectors { let write_mb = if *last_write_sectors > 0 && total_write >= *last_write_sectors {
((total_write - *last_write_sectors) as f32 * 512.0) / (1024.0 * 1024.0 * elapsed_secs) ((total_write - *last_write_sectors) as f32 * 512.0) / (1024.0 * 1024.0 * elapsed_secs)
} else { } else { 0.0 };
0.0
};
*last_read_sectors = total_read; *last_read_sectors = total_read;
*last_write_sectors = total_write; *last_write_sectors = total_write;
@@ -217,32 +388,46 @@ fn get_disk_io(last_read_sectors: &mut u64, last_write_sectors: &mut u64, elapse
} }
} }
fn get_systemd_failed() -> (String, u32) { fn get_network_speed(networks: &Networks, elapsed_secs: f32) -> String {
if let Ok(output) = Command::new("systemctl").args(["--failed", "--quiet"]).output() { let mut rx_bytes = 0u64;
if output.status.success() { let mut tx_bytes = 0u64;
if let Ok(stdout) = String::from_utf8(output.stdout) {
let lines: Vec<&str> = stdout.lines().filter(|l| !l.trim().is_empty()).collect(); for (_name, net) in networks {
let count = lines.len() as u32; rx_bytes += net.received();
if count == 0 { tx_bytes += net.transmitted();
return ("0 units".to_string(), 0); }
} else {
return (format!("{} unit(s) failed", count), count); let rx_kb = (rx_bytes as f32 / 1024.0) / elapsed_secs;
} let tx_kb = (tx_bytes as f32 / 1024.0) / elapsed_secs;
}
} format!("{:.0} KB/s ↑ {:.0} KB/s", rx_kb, tx_kb)
}
fn get_failed_systemd_units() -> String {
if let Ok(output) = Command::new("systemctl").arg("list-units").arg("--state=failed").arg("--quiet").output() {
let stdout_str = String::from_utf8_lossy(&output.stdout);
let count = stdout_str.lines().filter(|line| line.contains("loaded")).count();
format!("{} units", count)
} else {
"0 units".to_string()
} }
("0 units".to_string(), 0)
} }
fn print_line(stdout: &mut std::io::Stdout, label: &str, value: &str, val_color: Color) -> Result<(), Box<dyn std::error::Error>> { fn print_line(stdout: &mut std::io::Stdout, label: &str, value: &str, val_color: Color) -> Result<(), Box<dyn std::error::Error>> {
execute!(stdout, SetForegroundColor(FG_MAIN))?; execute!(stdout, SetForegroundColor(FG_MAIN))?;
write!(stdout, " {:<14}: ", label)?; write!(stdout, " {:<16}: ", label)?;
execute!(stdout, SetForegroundColor(val_color))?; execute!(stdout, SetForegroundColor(val_color))?;
writeln!(stdout, "{}", value)?; writeln!(stdout, "{}", value)?;
Ok(()) Ok(())
} }
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load .env from ~/.config/hypr/scripts/statmon/.env
let env_path = get_config_dir().join(".env");
let _ = dotenvy::from_path(&env_path);
let api_key = env::var("OPENWEATHER_API_KEY").unwrap_or_default();
let mut sys = System::new_with_specifics( let mut sys = System::new_with_specifics(
RefreshKind::new() RefreshKind::new()
.with_cpu(CpuRefreshKind::everything()) .with_cpu(CpuRefreshKind::everything())
@@ -252,147 +437,96 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut networks = Networks::new_with_refreshed_list(); let mut networks = Networks::new_with_refreshed_list();
let mut components = Components::new_with_refreshed_list(); let mut components = Components::new_with_refreshed_list();
let mut stdout = stdout(); let mut stdout = stdout();
let alert_threshold = Duration::from_secs(10);
let mut core_max_start: Option<Instant> = None;
let mut core_max_alerted = false;
let mut temp_high_start: Option<Instant> = None;
let mut temp_high_alerted = false;
let mut mem_high_start: Option<Instant> = None;
let mut mem_high_alerted = false;
let mut bat_notified_50 = false;
let mut bat_notified_25 = false;
let mut last_read_sectors = 0u64; let mut last_read_sectors = 0u64;
let mut last_write_sectors = 0u64; let mut last_write_sectors = 0u64;
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
let mut last_aqi_fetch = Instant::now() - Duration::from_secs(600);
let mut last_weather_fetch = Instant::now() - Duration::from_secs(3600);
let mut weather = WeatherState {
aqi_desc: "Fetching...".to_string(),
aqi_val: 1,
today_summary: "Fetching...".to_string(),
tomorrow_summary: "Fetching...".to_string(),
};
let mut upcoming_birthdays: Vec<String>;
thread::sleep(Duration::from_millis(1000)); thread::sleep(Duration::from_millis(1000));
loop { loop {
let elapsed_secs = last_tick.elapsed().as_secs_f32(); let elapsed_secs = last_tick.elapsed().as_secs_f32();
last_tick = Instant::now(); last_tick = Instant::now();
// 10-Minute Air Quality Check
if last_aqi_fetch.elapsed() >= Duration::from_secs(600) {
let (aqi_desc, aqi_val) = fetch_air_quality(&api_key);
weather.aqi_desc = aqi_desc;
weather.aqi_val = aqi_val;
if aqi_val >= 5 {
send_notification("Air Quality Warning", &format!("Air quality in 95662 is concerning: {}", weather.aqi_desc), "critical");
}
last_aqi_fetch = Instant::now();
}
// 60-Minute Weather Check
if last_weather_fetch.elapsed() >= Duration::from_secs(3600) {
let (today_s, tomorrow_s) = fetch_weather_and_forecast(&api_key);
weather.today_summary = today_s;
weather.tomorrow_summary = tomorrow_s;
last_weather_fetch = Instant::now();
}
// Always check birthdays on every refresh loop
upcoming_birthdays = get_upcoming_birthdays();
sys.refresh_all(); sys.refresh_all();
networks.refresh(); networks.refresh();
components.refresh(); components.refresh();
let time_str = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); let time_str = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let (bat_str, bat_pct) = get_battery_info(); let (bat_str, _) = get_battery_info();
let mouse_bat_str = get_mouse_battery(); let mouse_bat_str = get_mouse_battery();
let (temp_str, temp_val) = get_cpu_temp(&components); let (temp_str, temp_val) = get_cpu_temp(&components);
let (gpu_str, _) = get_gpu_info(); let (gpu_str, _) = get_gpu_info();
let nvme_temp = get_nvme_temp(); let nvme_temp = get_nvme_temp();
let disk_io = get_disk_io(&mut last_read_sectors, &mut last_write_sectors, elapsed_secs); let disk_io = get_disk_io(&mut last_read_sectors, &mut last_write_sectors, elapsed_secs);
let (failed_sysd, failed_count) = get_systemd_failed(); let net_speed = get_network_speed(&networks, elapsed_secs);
let systemd_units = get_failed_systemd_units();
// CPU Metrics
let global_cpu_pct = sys.global_cpu_info().cpu_usage();
let cpu_bar_val = format!("{} {:.1}%", make_bar(global_cpu_pct, 16), global_cpu_pct);
let high_cores: Vec<usize> = sys.cpus().iter().enumerate()
.filter_map(|(idx, cpu)| if cpu.cpu_usage() >= 80.0 { Some(idx) } else { None })
.collect();
let high_cores_str = if high_cores.is_empty() {
"All under 80%".to_string()
} else {
format!("Cores: {:?}", high_cores)
};
// Memory Metrics // Memory Metrics
let total_mem = sys.total_memory() / 1024 / 1024; let total_mem = sys.total_memory() / 1024 / 1024;
let used_mem = sys.used_memory() / 1024 / 1024; let used_mem = sys.used_memory() / 1024 / 1024;
let mem_pct = if total_mem > 0 { (used_mem as f32 * 100.0) / total_mem as f32 } else { 0.0 }; let mem_pct = if total_mem > 0 { (used_mem as f32 * 100.0) / total_mem as f32 } else { 0.0 };
// Swap Metrics
let total_swap = sys.total_swap() / 1024 / 1024; let total_swap = sys.total_swap() / 1024 / 1024;
let used_swap = sys.used_swap() / 1024 / 1024; let used_swap = sys.used_swap() / 1024 / 1024;
let swap_pct = if total_swap > 0 { (used_swap as f32 * 100.0) / total_swap as f32 } else { 0.0 }; let swap_pct = if total_swap > 0 { (used_swap as f32 * 100.0) / total_swap as f32 } else { 0.0 };
let swap_bar_val = format!("{} {:.0}% ({}MB / {}MB)", make_bar(swap_pct, 16), swap_pct, used_swap, total_swap);
// CPU Metrics // Top 5 Processes
let global_cpu = sys.global_cpu_info().cpu_usage(); let mut processes: Vec<_> = sys.processes().values().collect();
let cpus = sys.cpus(); processes.sort_by(|a, b| b.cpu_usage().partial_cmp(&a.cpu_usage()).unwrap_or(std::cmp::Ordering::Equal));
let num_cores = cpus.len() as f32; let top_processes: Vec<_> = processes.into_iter().take(5).collect();
let mut has_core_at_100 = false;
let mut core_str = String::new();
for (i, cpu) in cpus.iter().enumerate() {
let usage = cpu.cpu_usage();
if usage >= 99.0 {
has_core_at_100 = true;
}
if usage >= 80.0 {
core_str.push_str(&format!("C{}: {:.0}% | ", i, usage));
}
}
let (final_core_str, core_color) = if core_str.is_empty() {
("All under 80%".to_string(), METER_BG)
} else {
if core_str.ends_with(" | ") {
core_str.truncate(core_str.len() - 3);
}
(core_str, ALERT)
};
// --- NOTIFICATIONS ---
if bat_pct <= 25 && !bat_notified_25 {
send_notification("Low Battery Warning", &format!("Battery is down to {}%!", bat_pct), "critical");
bat_notified_25 = true;
} else if bat_pct <= 50 && bat_pct > 25 && !bat_notified_50 {
send_notification("Battery Notice", &format!("Battery dropped below 50% ({}%).", bat_pct), "normal");
bat_notified_50 = true;
} else if bat_pct > 55 {
bat_notified_50 = false;
bat_notified_25 = false;
}
if has_core_at_100 {
let start = core_max_start.get_or_insert_with(Instant::now);
if start.elapsed() >= alert_threshold && !core_max_alerted {
send_notification("CPU Warning", "A CPU core has been at 100% usage for over 10 seconds!", "critical");
core_max_alerted = true;
}
} else {
core_max_start = None;
core_max_alerted = false;
}
if temp_val >= 85.0 {
let start = temp_high_start.get_or_insert_with(Instant::now);
if start.elapsed() >= alert_threshold && !temp_high_alerted {
send_notification("High Temperature Alert", &format!("CPU Temperature is at {:.0}°C for over 10s!", temp_val), "critical");
temp_high_alerted = true;
}
} else {
temp_high_start = None;
temp_high_alerted = false;
}
if mem_pct >= 80.0 {
let start = mem_high_start.get_or_insert_with(Instant::now);
if start.elapsed() >= alert_threshold && !mem_high_alerted {
send_notification("High Memory Usage", &format!("RAM usage has been above 80% ({:.0}%) for over 10s!", mem_pct), "critical");
mem_high_alerted = true;
}
} else {
mem_high_start = None;
mem_high_alerted = false;
}
// Network Bandwidth Calculations
let mut rx_bytes = 0;
let mut tx_bytes = 0;
for (_iface, data) in &networks {
rx_bytes += data.received();
tx_bytes += data.transmitted();
}
let down_kb = ((rx_bytes / 1024) as f32 / elapsed_secs) as u64;
let up_kb = ((tx_bytes / 1024) as f32 / elapsed_secs) as u64;
// --- Flat Process List Logic ---
let my_pid = std::process::id();
let mut procs: Vec<_> = sys
.processes()
.values()
.filter(|p| p.pid().as_u32() != my_pid)
.collect();
// Sort by individual process CPU usage descending
procs.sort_by(|a, b| b.cpu_usage().partial_cmp(&a.cpu_usage()).unwrap_or(std::cmp::Ordering::Equal));
let top_5 = procs.into_iter().take(5);
// Render Screen // Render Screen
execute!(stdout, cursor::MoveTo(0, 0), Clear(ClearType::FromCursorDown))?; execute!(stdout, cursor::MoveTo(0, 0), Clear(ClearType::FromCursorDown))?;
@@ -411,63 +545,58 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let temp_color = if temp_val >= 85.0 { ALERT } else if temp_val >= 70.0 { ACCENT } else { FG_MAIN }; let temp_color = if temp_val >= 85.0 { ALERT } else if temp_val >= 70.0 { ACCENT } else { FG_MAIN };
print_line(&mut stdout, "CPU Temp", &temp_str, temp_color)?; print_line(&mut stdout, "CPU Temp", &temp_str, temp_color)?;
let cpu_color = if global_cpu >= 85.0 { ALERT } else if global_cpu >= 60.0 { ACCENT } else { FG_MAIN }; let cpu_color = if global_cpu_pct >= 85.0 { ALERT } else if global_cpu_pct >= 70.0 { ACCENT } else { FG_MAIN };
let cpu_bar_val = format!("{} {:.1}%", make_bar(global_cpu, 16), global_cpu);
print_line(&mut stdout, "CPU Usage", &cpu_bar_val, cpu_color)?; print_line(&mut stdout, "CPU Usage", &cpu_bar_val, cpu_color)?;
print_line(&mut stdout, "High Cores", &final_core_str, core_color)?; let core_color = if high_cores.is_empty() { FG_MAIN } else { ALERT };
print_line(&mut stdout, "High Cores", &high_cores_str, core_color)?;
let mem_color = if mem_pct >= 90.0 { ALERT } else if mem_pct >= 70.0 { ACCENT } else { FG_MAIN }; let mem_color = if mem_pct >= 90.0 { ALERT } else if mem_pct >= 70.0 { ACCENT } else { FG_MAIN };
let mem_bar_val = format!("{} {:.0}% ({}MB / {}MB)", make_bar(mem_pct, 16), mem_pct, used_mem, total_mem); let mem_bar_val = format!("{} {:.0}% ({}MB / {}MB)", make_bar(mem_pct, 16), mem_pct, used_mem, total_mem);
print_line(&mut stdout, "Memory Usage", &mem_bar_val, mem_color)?; print_line(&mut stdout, "Memory Usage", &mem_bar_val, mem_color)?;
let swap_color = if swap_pct >= 70.0 { ALERT } else if swap_pct >= 30.0 { ACCENT } else { FG_MAIN }; let swap_color = if swap_pct >= 80.0 { ALERT } else { FG_MAIN };
let swap_bar_val = format!("{} {:.0}% ({}MB / {}MB)", make_bar(swap_pct, 16), swap_pct, used_swap, total_swap);
print_line(&mut stdout, "Swap Usage", &swap_bar_val, swap_color)?; print_line(&mut stdout, "Swap Usage", &swap_bar_val, swap_color)?;
print_line(&mut stdout, "Radeon GPU", &gpu_str, ACCENT)?; print_line(&mut stdout, "Radeon GPU", &gpu_str, ACCENT)?;
print_line(&mut stdout, "NVMe Temp", &nvme_temp, FG_MAIN)?; print_line(&mut stdout, "NVMe Temp", &nvme_temp, FG_MAIN)?;
print_line(&mut stdout, "Disk I/O", &disk_io, ACCENT)?; print_line(&mut stdout, "Disk I/O", &disk_io, ACCENT)?;
print_line(&mut stdout, "Network", &format!("{} KB/s ↑ {} KB/s", down_kb, up_kb), ACCENT)?; print_line(&mut stdout, "Network", &net_speed, FG_MAIN)?;
print_line(&mut stdout, "Systemd Units", &systemd_units, FG_MAIN)?;
let sysd_color = if failed_count > 0 { ALERT } else { FG_MAIN };
print_line(&mut stdout, "Systemd Units", &failed_sysd, sysd_color)?;
// Render Top 5 Table // Processes Section
execute!(stdout, SetForegroundColor(FG_MAIN))?; execute!(stdout, SetForegroundColor(FG_MAIN))?;
println!("-----------------------------------------------------------------------------------------------"); println!("-----------------------------------------------------------------------------------------------");
execute!(stdout, SetForegroundColor(ACCENT))?; execute!(stdout, SetForegroundColor(ACCENT))?;
println!(" {:<8} {:<30} {:<12} {:<12}", "PID", "NAME", "CPU %", "MEM (MB)"); println!(" {:<7} {:<25} {:<10} {:<12}", "PID", "NAME", "CPU %", "MEM (MB)");
execute!(stdout, SetForegroundColor(FG_MAIN))?; execute!(stdout, SetForegroundColor(FG_MAIN))?;
println!("-----------------------------------------------------------------------------------------------"); println!("-----------------------------------------------------------------------------------------------");
for proc in top_processes {
let pid = proc.pid().to_string();
let name = proc.name();
let name_truncated = if name.len() > 24 { format!("{}", &name[..23]) } else { name.to_string() };
let cpu_p = format!("{:.1}", proc.cpu_usage());
let mem_mb = format!("{:.1}", proc.memory() as f32 / 1024.0 / 1024.0);
for p in top_5 { writeln!(stdout, " {:<7} {:<25} {:<10} {:<12}", pid, name_truncated, cpu_p, mem_mb)?;
let raw_name = p.name();
let display_name = if raw_name.len() > 28 {
format!("{}...", &raw_name[..25])
} else {
raw_name.to_string()
};
// Normalize individual process CPU % across logical cores
let cpu_normalized = p.cpu_usage() / num_cores;
// Direct KiB to MB conversion for single process memory
let mem_mb = p.memory() / 1024;
let row_color = if cpu_normalized >= 25.0 { ALERT } else if cpu_normalized >= 10.0 { ACCENT } else { FG_MAIN };
execute!(stdout, SetForegroundColor(row_color))?;
println!(" {:<8} {:<30} {:<12.1} {:<12}", p.pid().as_u32(), display_name, cpu_normalized, mem_mb);
} }
// Air Quality & Weather Section
execute!(stdout, SetForegroundColor(FG_MAIN))?;
println!("-----------------------------------------------------------------------------------------------");
let aqi_color = if weather.aqi_val >= 4 { ALERT } else if weather.aqi_val == 3 { ACCENT } else { FG_MAIN };
print_line(&mut stdout, "Air Quality (95662)", &weather.aqi_desc, aqi_color)?;
print_line(&mut stdout, "Weather Today", &weather.today_summary, FG_MAIN)?;
print_line(&mut stdout, "Weather Tomorrow", &weather.tomorrow_summary, FG_MAIN)?;
// Calendar & Birthdays Section
execute!(stdout, SetForegroundColor(FG_MAIN))?;
println!("-----------------------------------------------------------------------------------------------");
render_calendar_and_birthdays(&mut stdout, &upcoming_birthdays)?;
execute!(stdout, SetForegroundColor(FG_MAIN))?; execute!(stdout, SetForegroundColor(FG_MAIN))?;
println!("==============================================================================================="); println!("===============================================================================================");
if global_cpu > 85.0 || mem_pct > 90.0 || temp_val > 85.0 {
execute!(stdout, SetForegroundColor(ALERT))?;
println!("\n WARNING: HIGH RESOURCE USAGE DETECTED! ");
}
execute!(stdout, ResetColor)?; execute!(stdout, ResetColor)?;
stdout.flush()?; stdout.flush()?;
thread::sleep(Duration::from_secs(2)); thread::sleep(Duration::from_secs(2));