532 lines
15 KiB
Rust
532 lines
15 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 normalize_specs_mode(mode: &str) -> &'static str {
|
|
match mode {
|
|
"meters" => "meters",
|
|
_ => "cards",
|
|
}
|
|
}
|
|
|
|
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(mode: &str) -> RgbImage {
|
|
match normalize_specs_mode(mode) {
|
|
"meters" => render_system_panel_meters(),
|
|
_ => render_system_panel_cards(),
|
|
}
|
|
}
|
|
|
|
fn render_system_panel_cards() -> 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!(
|
|
"{} cores | {} procs",
|
|
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
|
|
}
|
|
|
|
fn render_system_panel_meters() -> RgbImage {
|
|
let system = collect_system_view();
|
|
let mut canvas = RgbImage::from_pixel(DISPLAY_WIDTH, DISPLAY_HEIGHT, Rgb([9, 13, 17]));
|
|
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, 72),
|
|
Rgb([15, 23, 30]),
|
|
);
|
|
draw_text_line(
|
|
&mut canvas,
|
|
Rgb([216, 253, 114]),
|
|
26,
|
|
18,
|
|
32.0,
|
|
"AOOSTAR System Monitor",
|
|
);
|
|
draw_text_line(
|
|
&mut canvas,
|
|
Rgb([146, 163, 181]),
|
|
28,
|
|
48,
|
|
18.0,
|
|
&format!("{host_name} | {timestamp}"),
|
|
);
|
|
|
|
draw_meter_row(
|
|
&mut canvas,
|
|
26,
|
|
102,
|
|
"CPU",
|
|
&format!("{}%", system.cpu_usage_percent),
|
|
parse_metric_percent(&system.cpu_usage_percent),
|
|
Rgb([110, 231, 183]),
|
|
&format!(
|
|
"Load {} / {} / {} {} cores {} procs",
|
|
system.load_avg_one,
|
|
system.load_avg_five,
|
|
system.load_avg_fifteen,
|
|
system.cpu_count,
|
|
system.process_count
|
|
),
|
|
);
|
|
draw_meter_row(
|
|
&mut canvas,
|
|
26,
|
|
170,
|
|
"RAM",
|
|
&format!("{}%", system.mem_usage_percent),
|
|
parse_metric_percent(&system.mem_usage_percent),
|
|
Rgb([120, 189, 247]),
|
|
&format!("{} / {}", system.mem_used, system.mem_total),
|
|
);
|
|
draw_meter_row(
|
|
&mut canvas,
|
|
26,
|
|
238,
|
|
"GPU",
|
|
system.temperature_gpu.as_deref().unwrap_or("-"),
|
|
normalize_temperature_bar(system.temperature_gpu.as_deref()),
|
|
Rgb([241, 132, 121]),
|
|
&format!(
|
|
"Temp meter CPU {} Cache {}% User {}%",
|
|
system.temperature_cpu.as_deref().unwrap_or("-"),
|
|
system.cache_usage_percent,
|
|
system.user_usage_percent
|
|
),
|
|
);
|
|
|
|
draw_filled_rect_mut(
|
|
&mut canvas,
|
|
Rect::at(26, 308).of_size(908, 42),
|
|
Rgb([12, 18, 24]),
|
|
);
|
|
draw_hollow_rect_mut(
|
|
&mut canvas,
|
|
Rect::at(26, 308).of_size(908, 42),
|
|
Rgb([31, 44, 58]),
|
|
);
|
|
draw_text_line(
|
|
&mut canvas,
|
|
Rgb([146, 163, 181]),
|
|
42,
|
|
321,
|
|
18.0,
|
|
&format!(
|
|
"Swap {}% {} / {} Cache {} / {} User {} / {} Uptime {}",
|
|
system.swap_usage_percent,
|
|
system.swap_used,
|
|
system.swap_total,
|
|
system.cache_used,
|
|
system.cache_total,
|
|
system.user_used,
|
|
system.user_total,
|
|
system.uptime
|
|
),
|
|
);
|
|
|
|
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_meter_row(
|
|
canvas: &mut RgbImage,
|
|
x: i32,
|
|
y: i32,
|
|
label: &str,
|
|
value: &str,
|
|
percent: f32,
|
|
accent: Rgb<u8>,
|
|
meta: &str,
|
|
) {
|
|
draw_text_line(canvas, accent, x, y, 19.0, label);
|
|
draw_text_line(canvas, Rgb([237, 242, 247]), x + 112, y, 19.0, value);
|
|
draw_meter_bar(canvas, x, y + 28, 840, 20, percent, accent);
|
|
draw_text_line(canvas, Rgb([146, 163, 181]), x, y + 56, 17.0, meta);
|
|
}
|
|
|
|
fn draw_meter_bar(
|
|
canvas: &mut RgbImage,
|
|
x: i32,
|
|
y: i32,
|
|
width: u32,
|
|
height: u32,
|
|
percent: f32,
|
|
accent: Rgb<u8>,
|
|
) {
|
|
draw_filled_rect_mut(
|
|
canvas,
|
|
Rect::at(x, y).of_size(width, height),
|
|
Rgb([19, 27, 35]),
|
|
);
|
|
draw_hollow_rect_mut(
|
|
canvas,
|
|
Rect::at(x, y).of_size(width, height),
|
|
Rgb([36, 50, 67]),
|
|
);
|
|
let fill_width =
|
|
((width.saturating_sub(2)) as f32 * (percent.clamp(0.0, 100.0) / 100.0)).round() as u32;
|
|
if fill_width > 0 {
|
|
draw_filled_rect_mut(
|
|
canvas,
|
|
Rect::at(x + 1, y + 1).of_size(fill_width, height.saturating_sub(2)),
|
|
accent,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn parse_metric_percent(value: &str) -> f32 {
|
|
value.parse::<f32>().unwrap_or(0.0)
|
|
}
|
|
|
|
fn normalize_temperature_bar(value: Option<&str>) -> f32 {
|
|
let Some(raw) = value else {
|
|
return 0.0;
|
|
};
|
|
let numeric = raw
|
|
.replace(" °C", "")
|
|
.replace('C', "")
|
|
.trim()
|
|
.parse::<f32>()
|
|
.unwrap_or(0.0);
|
|
(numeric / 100.0 * 100.0).clamp(0.0, 100.0)
|
|
}
|
|
|
|
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")
|
|
})
|
|
}
|