349 lines
10 KiB
Rust
349 lines
10 KiB
Rust
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<String> {
|
|
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<u8>,
|
|
) {
|
|
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<u8>, 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<FontArc> = OnceLock::new();
|
|
FONT.get_or_init(|| {
|
|
FontArc::try_from_slice(include_bytes!("../../../fonts/DejaVuSans.ttf"))
|
|
.expect("embedded display font must be valid")
|
|
})
|
|
}
|