use std::sync::OnceLock; use ab_glyph::{FontArc, PxScale}; use chrono::Local; use image::{Rgb, RgbImage}; use imageproc::{ drawing::{draw_filled_rect_mut, draw_hollow_rect_mut, draw_text_mut}, rect::Rect, }; use sysinfo::{Components, Disks, System}; use crate::types::SystemView; pub(crate) const DISPLAY_WIDTH: u32 = 960; pub(crate) const DISPLAY_HEIGHT: u32 = 376; pub(crate) fn collect_system_view() -> SystemView { fn mount_usage(disks: &Disks, mount_point: &str) -> (String, String, String) { disks .iter() .find(|disk| disk.mount_point() == std::path::Path::new(mount_point)) .map(|disk| { let total = disk.total_space(); let used = total.saturating_sub(disk.available_space()); let usage_percent = if total == 0 { 0.0 } else { used as f64 / total as f64 * 100.0 }; ( format!("{usage_percent:.0}"), format_bytes(used), format_bytes(total), ) }) .unwrap_or_else(|| ("n/a".into(), "n/a".into(), "n/a".into())) } let mut sys = System::new_all(); sys.refresh_all(); let load_avg = System::load_average(); let total_memory = sys.total_memory(); let used_memory = sys.used_memory(); let total_swap = sys.total_swap(); let used_swap = sys.used_swap(); let mut disks = Disks::new(); disks.refresh(false); let disk_total: u64 = disks.iter().map(|disk| disk.total_space()).sum(); let disk_used: u64 = disks .iter() .map(|disk| disk.total_space().saturating_sub(disk.available_space())) .sum(); let mut components = Components::new(); components.refresh(false); let (cache_usage_percent, cache_used, cache_total) = mount_usage(&disks, "/mnt/cache"); let (user_usage_percent, user_used, user_total) = mount_usage(&disks, "/mnt/user"); SystemView { cache_usage_percent, cache_used, cache_total, user_usage_percent, user_used, user_total, cpu_usage_percent: format!("{:.1}", sys.global_cpu_usage()), load_avg_one: format!("{:.2}", load_avg.one), load_avg_five: format!("{:.2}", load_avg.five), load_avg_fifteen: format!("{:.2}", load_avg.fifteen), mem_usage_percent: format!("{:.1}", percentage(used_memory, total_memory)), mem_used: format_bytes(used_memory), mem_total: format_bytes(total_memory), swap_usage_percent: format!("{:.1}", percentage(used_swap, total_swap)), swap_used: format_bytes(used_swap), swap_total: format_bytes(total_swap), disk_usage_percent: format!("{:.1}", percentage(disk_used, disk_total)), disk_used: format_bytes(disk_used), disk_total: format_bytes(disk_total), cpu_count: sys.cpus().len(), process_count: sys.processes().len(), uptime: format_uptime(System::uptime()), temperature_cpu: component_temperature(&components, "Tctl"), temperature_gpu: component_temperature(&components, "amdgpu"), } } fn component_temperature(components: &Components, needle: &str) -> Option { components .iter() .find(|component| component.label().contains(needle)) .and_then(|component| component.temperature()) .map(|value| format!("{value:.1} °C")) } fn percentage(used: u64, total: u64) -> f64 { if total == 0 { 0.0 } else { used as f64 * 100.0 / total as f64 } } fn format_uptime(seconds: u64) -> String { let days = seconds / 86_400; let hours = (seconds % 86_400) / 3_600; let minutes = (seconds % 3_600) / 60; if days == 0 { format!("{hours:02}:{minutes:02}") } else if days == 1 { format!("1 day, {hours:02}:{minutes:02}") } else { format!("{days} days, {hours:02}:{minutes:02}") } } fn format_bytes(bytes: u64) -> String { const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"]; const THRESHOLD: f64 = 1024.0; if bytes == 0 { return "0 B".to_string(); } let mut size = bytes as f64; let mut unit_index = 0; while size >= THRESHOLD && unit_index < UNITS.len() - 1 { size /= THRESHOLD; unit_index += 1; } if unit_index > 0 { format!("{size:.2} {}", UNITS[unit_index]) } else { format!("{} {}", size, UNITS[unit_index]) } } pub(crate) fn render_system_panel() -> RgbImage { let system = collect_system_view(); let mut canvas = RgbImage::from_pixel(DISPLAY_WIDTH, DISPLAY_HEIGHT, Rgb([10, 14, 18])); let host_name = System::host_name().unwrap_or_else(|| "Unraid".into()); let timestamp = Local::now().format("%d.%m.%Y %H:%M:%S").to_string(); draw_filled_rect_mut( &mut canvas, Rect::at(0, 0).of_size(DISPLAY_WIDTH, 84), Rgb([18, 27, 36]), ); draw_filled_rect_mut( &mut canvas, Rect::at(0, 84).of_size(DISPLAY_WIDTH, DISPLAY_HEIGHT - 84), Rgb([8, 11, 15]), ); draw_text_line( &mut canvas, Rgb([216, 253, 114]), 28, 20, 36.0, "AOOSTAR Native Specs", ); draw_text_line( &mut canvas, Rgb([146, 163, 181]), 30, 58, 20.0, &format!("{host_name} | {timestamp}"), ); let cards = [ ( 24, 106, "CPU", format!("{} %", system.cpu_usage_percent), format!( "Load {} / {} / {}", system.load_avg_one, system.load_avg_five, system.load_avg_fifteen ), Rgb([124, 231, 191]), ), ( 332, 106, "Memory", format!("{} %", system.mem_usage_percent), format!("{} / {}", system.mem_used, system.mem_total), Rgb([125, 196, 255]), ), ( 640, 106, "Storage", format!( "C {}% U {}%", system.cache_usage_percent, system.user_usage_percent ), format!( "/mnt/cache {} / {} | /mnt/user {} / {}", system.cache_used, system.cache_total, system.user_used, system.user_total ), Rgb([255, 199, 115]), ), ( 24, 236, "Swap", format!("{} %", system.swap_usage_percent), format!("{} / {}", system.swap_used, system.swap_total), Rgb([194, 167, 255]), ), ( 332, 236, "Temperatures", format!("CPU {}", system.temperature_cpu.as_deref().unwrap_or("-")), format!("GPU {}", system.temperature_gpu.as_deref().unwrap_or("-")), Rgb([255, 127, 115]), ), ( 640, 236, "System", system.uptime.clone(), format!("{} CPU / {} proc", system.cpu_count, system.process_count), Rgb([216, 253, 114]), ), ]; for (x, y, title, value, meta, accent) in cards { draw_metric_card(&mut canvas, x, y, title, &value, &meta, accent); } canvas } pub(crate) fn overlay_system_specs(canvas: &mut RgbImage) { let system = collect_system_view(); let host_name = System::host_name().unwrap_or_else(|| "Unraid".into()); let timestamp = Local::now().format("%H:%M:%S").to_string(); draw_filled_rect_mut(canvas, Rect::at(18, 16).of_size(924, 54), Rgb([12, 18, 24])); draw_hollow_rect_mut(canvas, Rect::at(18, 16).of_size(924, 54), Rgb([36, 50, 67])); draw_text_line( canvas, Rgb([216, 253, 114]), 34, 28, 22.0, &format!( "{host_name} CPU {}% RAM {}% C {}% U {}%", system.cpu_usage_percent, system.mem_usage_percent, system.cache_usage_percent, system.user_usage_percent ), ); draw_text_line( canvas, Rgb([146, 163, 181]), 34, 52, 16.0, &format!( "Load {} / {} / {} Temp {} / {} Uptime {} {}", system.load_avg_one, system.load_avg_five, system.load_avg_fifteen, system.temperature_cpu.as_deref().unwrap_or("-"), system.temperature_gpu.as_deref().unwrap_or("-"), system.uptime, timestamp ), ); draw_filled_rect_mut( canvas, Rect::at(18, 306).of_size(924, 52), Rgb([12, 18, 24]), ); draw_hollow_rect_mut( canvas, Rect::at(18, 306).of_size(924, 52), Rgb([36, 50, 67]), ); draw_text_line( canvas, Rgb([237, 242, 247]), 34, 320, 18.0, &format!( "Cache {} / {} User {} / {} Proc {} CPU {} GPU {}", system.cache_used, system.cache_total, system.user_used, system.user_total, system.process_count, system.temperature_cpu.as_deref().unwrap_or("-"), system.temperature_gpu.as_deref().unwrap_or("-"), ), ); } fn draw_metric_card( canvas: &mut RgbImage, x: i32, y: i32, title: &str, value: &str, meta: &str, accent: Rgb, ) { draw_filled_rect_mut(canvas, Rect::at(x, y).of_size(284, 112), Rgb([16, 22, 30])); draw_hollow_rect_mut(canvas, Rect::at(x, y).of_size(284, 112), Rgb([36, 50, 67])); draw_filled_rect_mut(canvas, Rect::at(x + 1, y + 1).of_size(6, 110), accent); draw_text_line(canvas, accent, x + 20, y + 16, 19.0, title); draw_text_line(canvas, Rgb([237, 242, 247]), x + 20, y + 44, 28.0, value); draw_text_line(canvas, Rgb([146, 163, 181]), x + 20, y + 82, 18.0, meta); } fn draw_text_line(canvas: &mut RgbImage, color: Rgb, x: i32, y: i32, size: f32, text: &str) { draw_text_mut( canvas, color, x, y, PxScale { x: size, y: size }, display_font(), text, ); } fn display_font() -> &'static FontArc { static FONT: OnceLock = OnceLock::new(); FONT.get_or_init(|| { FontArc::try_from_slice(include_bytes!("../../../fonts/DejaVuSans.ttf")) .expect("embedded display font must be valid") }) }