use saudade::{Container, Font, Label, Painter, Rect, SvgImage, Theme, Widget, include_svg};
pub const MIN_CONTENT_WIDTH: i32 = 320;
pub const MACHINE_FONT_SIZE: f32 = 20.0;
pub const LOGO_X: i32 = 16;
pub const LOGO_Y: i32 = 12;
pub const LOGO_W: i32 = 56;
pub const LOGO_H: i32 = 56;
pub const LOGO_HIT: Rect = Rect::new(LOGO_X, LOGO_Y, LOGO_W, LOGO_H);
const RULE_X: i32 = 16;
const KEY_X: i32 = RULE_X;
const VALUE_X: i32 = 90;
const ROW_H: i32 = 18;
const RULE_GAP: i32 = 8;
pub struct SystemInfo {
pub machine: String,
pub operating_system: String,
pub vendor: Option<String>,
pub model: Option<String>,
pub cpu: String,
pub memory_line: String,
pub disk_line: String,
pub kernel: String,
pub window_manager: Option<String>,
pub packages: Option<u32>,
pub uptime: String,
pub distribution_id: String,
}
pub fn compute_content_width(info: &SystemInfo, family: Option<&str>, body_size: f32) -> i32 {
let Some(font) = Font::load_system() else {
return MIN_CONTENT_WIDTH;
};
content_width_with_font(info, family, body_size, &font)
}
pub fn content_width_with_font(
info: &SystemInfo,
family: Option<&str>,
body_size: f32,
font: &Font,
) -> i32 {
let needed_for = |text: &str, size: f32| -> i32 {
let (w, _) = font.measure(text, size);
VALUE_X + w.ceil() as i32 + RULE_X
};
let mut required = MIN_CONTENT_WIDTH;
if let Some(family) = family.map(str::trim).filter(|s| !s.is_empty()) {
required = required.max(needed_for(family, MACHINE_FONT_SIZE));
}
let mut values: Vec<&str> = vec![
info.operating_system.as_str(),
info.cpu.as_str(),
info.memory_line.as_str(),
info.disk_line.as_str(),
info.kernel.as_str(),
info.uptime.as_str(),
];
if let Some(v) = &info.vendor {
values.push(v);
}
if let Some(m) = &info.model {
values.push(m);
}
if let Some(wm) = &info.window_manager {
values.push(wm);
}
let pkg_str;
if let Some(n) = info.packages {
pkg_str = n.to_string();
values.push(&pkg_str);
}
for v in values {
required = required.max(needed_for(v, body_size));
}
required
}
pub fn build_about_box(info: &SystemInfo, content_width: i32) -> Container {
let rule_w = content_width - 2 * RULE_X;
let content_right = RULE_X + rule_w;
let mut hardware: Vec<(&str, &str)> = Vec::new();
if let Some(vendor) = &info.vendor {
hardware.push(("Vendor", vendor));
}
if let Some(model) = &info.model {
hardware.push(("Model", model));
}
hardware.push(("CPU", &info.cpu));
hardware.push(("Memory", &info.memory_line));
hardware.push(("Disk", &info.disk_line));
let mut software: Vec<(&str, &str)> = Vec::new();
software.push(("System", &info.kernel));
if let Some(wm) = &info.window_manager {
software.push(("WM", wm));
}
let packages_str;
if let Some(n) = info.packages {
packages_str = n.to_string();
software.push(("Packages", &packages_str));
}
software.push(("Uptime", &info.uptime));
let ok_w = 72;
let ok_h = 24;
let machine_top = 24;
let machine_box = Rect::new(VALUE_X, machine_top, rule_w, 26);
let os_box = Rect::new(VALUE_X, machine_top + 24, rule_w, ROW_H);
let rule1_y = os_box.bottom() + RULE_GAP;
let hardware_top = rule1_y + 13;
let rule2_y = hardware_top + hardware.len() as i32 * ROW_H + RULE_GAP;
let software_top = rule2_y + 13;
let software_bottom = software_top + software.len() as i32 * ROW_H;
let ok = Rect::new(
content_right / 2 - ok_w / 2,
software_bottom + RULE_GAP * 2,
ok_w,
ok_h,
);
let mut root = Container::new(content_width, ok.bottom() + 12);
root.push(build_os_logo(
LOGO_X,
LOGO_Y,
LOGO_W,
LOGO_H,
&info.distribution_id,
));
root.push(Label::new(machine_box, info.machine.clone()).with_size(MACHINE_FONT_SIZE));
root.push(Label::new(os_box, info.operating_system.clone()));
push_rows(&mut root, hardware_top, &hardware, content_right);
push_rows(&mut root, software_top, &software, content_right);
root.push(
saudade::Button::new(ok, "Close")
.default(true)
.on_click(|cx| cx.close()),
);
root
}
fn push_rows(root: &mut Container, top_y: i32, rows: &[(&str, &str)], content_right: i32) {
let value_w = content_right - VALUE_X;
let mut y = top_y;
for (key, value) in rows {
root.push(Label::new(
Rect::new(KEY_X, y, VALUE_X - KEY_X - 6, ROW_H),
format!("{key}:"),
));
root.push(Label::new(
Rect::new(VALUE_X, y, value_w, ROW_H),
(*value).to_string(),
));
y += ROW_H;
}
}
pub fn build_os_logo(x: i32, y: i32, w: i32, h: i32, distribution_id: &str) -> Logo {
Logo {
rect: Rect::new(x, y, w, h),
image: logo_svg_for(distribution_id),
}
}
pub struct Logo {
rect: Rect,
image: SvgImage,
}
impl Widget for Logo {
fn bounds(&self) -> Rect {
self.rect
}
fn paint(&mut self, painter: &mut Painter, _theme: &Theme) {
painter.draw_svg(&self.image, self.rect);
}
}
pub fn logo_svg_for(distribution_id: &str) -> SvgImage {
const APPLE: SvgImage = include_svg!("assets/os/apple.svg");
const ARCH: SvgImage = include_svg!("assets/os/arch.svg");
const DEBIAN: SvgImage = include_svg!("assets/os/debian.svg");
const FEDORA: SvgImage = include_svg!("assets/os/fedora.svg");
const FREEBSD: SvgImage = include_svg!("assets/os/freebsd.svg");
const MINT: SvgImage = include_svg!("assets/os/linuxmint.svg");
const MANJARO: SvgImage = include_svg!("assets/os/manjaro.svg");
const NETBSD: SvgImage = include_svg!("assets/os/netbsd.svg");
const NIXOS: SvgImage = include_svg!("assets/os/nixos.svg");
const OPENBSD: SvgImage = include_svg!("assets/os/openbsd.svg");
const OPENSUSE: SvgImage = include_svg!("assets/os/opensuse.svg");
#[allow(deprecated)]
const TUX: SvgImage = include_svg!("assets/os/tux.svg");
const UBUNTU: SvgImage = include_svg!("assets/os/ubuntu.svg");
const WINDOWS: SvgImage = include_svg!("assets/os/windows.svg");
match distribution_id {
"ubuntu" => UBUNTU,
"debian" => DEBIAN,
"fedora" => FEDORA,
"arch" => ARCH,
"manjaro" => MANJARO,
"linuxmint" => MINT,
"nixos" => NIXOS,
"macos" => APPLE,
"windows" => WINDOWS,
"freebsd" => FREEBSD,
"openbsd" => OPENBSD,
"netbsd" => NETBSD,
id if id.starts_with("opensuse") => OPENSUSE,
_ => TUX,
}
}