refined statmon
This commit is contained in:
1
hypr/scripts/statmon/.env
Normal file
1
hypr/scripts/statmon/.env
Normal file
@@ -0,0 +1 @@
|
||||
OPENWEATHER_API_KEY=1d88683051588bb011bfd5be2192ecc0
|
||||
984
hypr/scripts/statmon/Cargo.lock
generated
984
hypr/scripts/statmon/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,3 +7,7 @@ edition = "2024"
|
||||
sysinfo = "0.30"
|
||||
crossterm = "0.27"
|
||||
chrono = "0.4"
|
||||
dotenvy = "0.15"
|
||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
127
hypr/scripts/statmon/birthdays.json
Normal file
127
hypr/scripts/statmon/birthdays.json
Normal 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
|
||||
}
|
||||
]
|
||||
@@ -1,27 +1,54 @@
|
||||
use chrono::Local;
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use crossterm::{
|
||||
cursor, execute,
|
||||
style::{Color, ResetColor, SetForegroundColor},
|
||||
terminal::{Clear, ClearType},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fs,
|
||||
env, fs,
|
||||
io::{stdout, Write},
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use sysinfo::{
|
||||
Components, CpuRefreshKind, MemoryRefreshKind, Networks, ProcessRefreshKind,
|
||||
RefreshKind, System,
|
||||
Components, CpuRefreshKind, MemoryRefreshKind, Networks, ProcessRefreshKind, RefreshKind,
|
||||
System,
|
||||
};
|
||||
|
||||
// --- BTOP Theme RGB Constants ---
|
||||
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 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) {
|
||||
let _ = Command::new("notify-send")
|
||||
@@ -41,19 +68,209 @@ fn make_bar(pct: f32, width: usize) -> String {
|
||||
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) {
|
||||
let bat_paths = ["/sys/class/power_supply/BAT0", "/sys/class/power_supply/BAT1"];
|
||||
for path in bat_paths {
|
||||
if Path::new(path).exists() {
|
||||
let cap_str = fs::read_to_string(format!("{}/capacity", path))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
let status = fs::read_to_string(format!("{}/status", path))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let cap_str = fs::read_to_string(format!("{}/capacity", path)).unwrap_or_default().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>() {
|
||||
return (format!("{}% ({})", cap, status), cap);
|
||||
}
|
||||
@@ -67,17 +284,9 @@ fn get_mouse_battery() -> String {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
|
||||
|
||||
if name.starts_with("hid") || name.starts_with("mouse") {
|
||||
let cap = fs::read_to_string(path.join("capacity"))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
let status = fs::read_to_string(path.join("status"))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let cap = fs::read_to_string(path.join("capacity")).unwrap_or_default().trim().to_string();
|
||||
let status = fs::read_to_string(path.join("status")).unwrap_or_default().trim().to_string();
|
||||
if !cap.is_empty() {
|
||||
let status_str = if status.is_empty() { "Wireless".to_string() } else { status };
|
||||
return format!("{}% ({})", cap, status_str);
|
||||
@@ -96,23 +305,6 @@ fn get_cpu_temp(components: &Components) -> (String, f32) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -120,7 +312,6 @@ fn get_gpu_info() -> (String, f32) {
|
||||
for card_num in 0..4 {
|
||||
let card_path = format!("/sys/class/drm/card{}/device", card_num);
|
||||
let path = Path::new(&card_path);
|
||||
|
||||
if path.exists() {
|
||||
let busy_file = if path.join("gpu_busy_percent").exists() {
|
||||
Some(path.join("gpu_busy_percent"))
|
||||
@@ -131,23 +322,9 @@ fn get_gpu_info() -> (String, f32) {
|
||||
};
|
||||
|
||||
if let Some(bf) = busy_file {
|
||||
let busy_pct = fs::read_to_string(bf)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.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;
|
||||
let busy_pct = fs::read_to_string(bf).unwrap_or_default().trim().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 {
|
||||
(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 {
|
||||
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 {
|
||||
let r_sectors: u64 = parts[5].parse().unwrap_or(0);
|
||||
let w_sectors: u64 = parts[9].parse().unwrap_or(0);
|
||||
total_read += r_sectors;
|
||||
total_write += w_sectors;
|
||||
total_read += parts[5].parse::<u64>().unwrap_or(0);
|
||||
total_write += parts[9].parse::<u64>().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
} else { 0.0 };
|
||||
|
||||
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)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
} else { 0.0 };
|
||||
|
||||
*last_read_sectors = total_read;
|
||||
*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) {
|
||||
if let Ok(output) = Command::new("systemctl").args(["--failed", "--quiet"]).output() {
|
||||
if output.status.success() {
|
||||
if let Ok(stdout) = String::from_utf8(output.stdout) {
|
||||
let lines: Vec<&str> = stdout.lines().filter(|l| !l.trim().is_empty()).collect();
|
||||
let count = lines.len() as u32;
|
||||
if count == 0 {
|
||||
return ("0 units".to_string(), 0);
|
||||
fn get_network_speed(networks: &Networks, elapsed_secs: f32) -> String {
|
||||
let mut rx_bytes = 0u64;
|
||||
let mut tx_bytes = 0u64;
|
||||
|
||||
for (_name, net) in networks {
|
||||
rx_bytes += net.received();
|
||||
tx_bytes += net.transmitted();
|
||||
}
|
||||
|
||||
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 {
|
||||
return (format!("{} unit(s) failed", count), count);
|
||||
"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>> {
|
||||
execute!(stdout, SetForegroundColor(FG_MAIN))?;
|
||||
write!(stdout, " {:<14}: ", label)?;
|
||||
write!(stdout, " {:<16}: ", label)?;
|
||||
execute!(stdout, SetForegroundColor(val_color))?;
|
||||
writeln!(stdout, "{}", value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(
|
||||
RefreshKind::new()
|
||||
.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 components = Components::new_with_refreshed_list();
|
||||
|
||||
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_write_sectors = 0u64;
|
||||
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));
|
||||
|
||||
loop {
|
||||
let elapsed_secs = last_tick.elapsed().as_secs_f32();
|
||||
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();
|
||||
networks.refresh();
|
||||
components.refresh();
|
||||
|
||||
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 (temp_str, temp_val) = get_cpu_temp(&components);
|
||||
let (gpu_str, _) = get_gpu_info();
|
||||
let nvme_temp = get_nvme_temp();
|
||||
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
|
||||
let total_mem = sys.total_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 };
|
||||
|
||||
// Swap Metrics
|
||||
let total_swap = sys.total_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_bar_val = format!("{} {:.0}% ({}MB / {}MB)", make_bar(swap_pct, 16), swap_pct, used_swap, total_swap);
|
||||
|
||||
// CPU Metrics
|
||||
let global_cpu = sys.global_cpu_info().cpu_usage();
|
||||
let cpus = sys.cpus();
|
||||
let num_cores = cpus.len() as f32;
|
||||
|
||||
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);
|
||||
// Top 5 Processes
|
||||
let mut processes: Vec<_> = sys.processes().values().collect();
|
||||
processes.sort_by(|a, b| b.cpu_usage().partial_cmp(&a.cpu_usage()).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let top_processes: Vec<_> = processes.into_iter().take(5).collect();
|
||||
|
||||
// Render Screen
|
||||
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 };
|
||||
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_bar_val = format!("{} {:.1}%", make_bar(global_cpu, 16), global_cpu);
|
||||
let cpu_color = if global_cpu_pct >= 85.0 { ALERT } else if global_cpu_pct >= 70.0 { ACCENT } else { FG_MAIN };
|
||||
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_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)?;
|
||||
|
||||
let swap_color = if swap_pct >= 70.0 { ALERT } else if swap_pct >= 30.0 { ACCENT } else { FG_MAIN };
|
||||
let swap_bar_val = format!("{} {:.0}% ({}MB / {}MB)", make_bar(swap_pct, 16), swap_pct, used_swap, total_swap);
|
||||
let swap_color = if swap_pct >= 80.0 { ALERT } else { FG_MAIN };
|
||||
print_line(&mut stdout, "Swap Usage", &swap_bar_val, swap_color)?;
|
||||
|
||||
print_line(&mut stdout, "Radeon GPU", &gpu_str, ACCENT)?;
|
||||
print_line(&mut stdout, "NVMe Temp", &nvme_temp, FG_MAIN)?;
|
||||
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))?;
|
||||
println!("-----------------------------------------------------------------------------------------------");
|
||||
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))?;
|
||||
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 {
|
||||
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);
|
||||
writeln!(stdout, " {:<7} {:<25} {:<10} {:<12}", pid, name_truncated, cpu_p, 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))?;
|
||||
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)?;
|
||||
stdout.flush()?;
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
|
||||
Reference in New Issue
Block a user