use std::collections::VecDeque;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use regex::Regex;
use resvg::{tiny_skia, usvg};
use saudade::EventCtx;
use saudade::{
App, Button, Color, Container, Event, Image, Key, Label, MouseButton, NamedKey, Painter,
PopupRequest, Rect, Theme, Widget, WindowConfig,
};
use sysinfo::{Disks, Product, System};
const CONTENT_WIDTH: i32 = 320;
const LOGO_X: i32 = 16;
const LOGO_Y: i32 = 12;
const LOGO_W: i32 = 56;
const LOGO_H: i32 = 56;
const LOGO_HIT: Rect = Rect::new(LOGO_X, LOGO_Y, LOGO_W, LOGO_H);
const EASTER_EGG_CLICKS: usize = 5;
const EASTER_EGG_WINDOW: Duration = Duration::from_secs(2);
struct SystemInfo {
machine: String,
operating_system: String,
vendor: Option<String>,
model: Option<String>,
cpu: String,
memory_line: String,
disk_line: String,
kernel: String,
uptime: String,
}
fn main() {
let info = gather_system_info();
let about = build_about_box(&info);
let height = about.bounds().h;
let root = AboutWithEasterEgg::new(about);
App::new(
WindowConfig::new("About This Computer", CONTENT_WIDTH, height),
root,
)
.with_theme(Theme::windows_31())
.run();
}
const RULE_X: i32 = 16;
const RULE_W: i32 = CONTENT_WIDTH - 2 * RULE_X;
const CONTENT_RIGHT: i32 = RULE_X + RULE_W;
const KEY_X: i32 = RULE_X;
const VALUE_X: i32 = 90;
const ROW_H: i32 = 18;
const RULE_GAP: i32 = 8;
fn build_about_box(info: &SystemInfo) -> Container {
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 software: [(&str, &str); 2] = [("System", &info.kernel), ("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));
root.push(Label::new(machine_box, info.machine.clone()).with_size(20.0));
root.push(Label::new(os_box, info.operating_system.clone()));
push_rows(&mut root, hardware_top, &hardware);
push_rows(&mut root, software_top, &software);
root.push(
Button::new(ok, "Close")
.default(true)
.on_click(|cx| cx.close()),
);
root
}
fn push_rows(root: &mut Container, top_y: i32, rows: &[(&str, &str)]) {
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;
}
}
fn build_os_logo(x: i32, y: i32, w: i32, h: i32) -> Image {
let svg = logo_svg_for(&System::distribution_id());
match rasterize_logo(svg, w, h) {
Some(pixels) => Image::from_pixels(x, y, w, h, pixels),
None => Image::new(x, y, w, h),
}
}
fn logo_svg_for(distribution_id: &str) -> &'static str {
const APPLE: &str = include_str!("../assets/os/apple.svg");
const ARCH: &str = include_str!("../assets/os/arch.svg");
const DEBIAN: &str = include_str!("../assets/os/debian.svg");
const FEDORA: &str = include_str!("../assets/os/fedora.svg");
const FREEBSD: &str = include_str!("../assets/os/freebsd.svg");
const MINT: &str = include_str!("../assets/os/linuxmint.svg");
const MANJARO: &str = include_str!("../assets/os/manjaro.svg");
const NETBSD: &str = include_str!("../assets/os/netbsd.svg");
const NIXOS: &str = include_str!("../assets/os/nixos.svg");
const OPENBSD: &str = include_str!("../assets/os/openbsd.svg");
const OPENSUSE: &str = include_str!("../assets/os/opensuse.svg");
const TUX: &str = include_str!("../assets/os/tux.svg");
const UBUNTU: &str = include_str!("../assets/os/ubuntu.svg");
const WINDOWS: &str = include_str!("../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,
}
}
fn rasterize_logo(svg: &str, w: i32, h: i32) -> Option<Vec<u32>> {
if w <= 0 || h <= 0 {
return None;
}
let (w, h) = (w as u32, h as u32);
let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).ok()?;
let mut pixmap = tiny_skia::Pixmap::new(w, h)?;
let size = tree.size();
let scale = (w as f32 / size.width()).min(h as f32 / size.height());
let tx = (w as f32 - size.width() * scale) * 0.5;
let ty = (h as f32 - size.height() * scale) * 0.5;
let transform = tiny_skia::Transform::from_scale(scale, scale).post_translate(tx, ty);
resvg::render(&tree, transform, &mut pixmap.as_mut());
let mut out = vec![0u32; (w * h) as usize];
for (px, chunk) in out.iter_mut().zip(pixmap.data().chunks_exact(4)) {
let (r, g, b, a) = (chunk[0], chunk[1], chunk[2], chunk[3]);
if a == 0 {
continue;
}
let inv = 255 - a as u32;
let comp = |c: u8| (c as u32 + inv).min(255);
*px = 0xFF00_0000 | (comp(r) << 16) | (comp(g) << 8) | comp(b);
}
Some(out)
}
struct AboutWithEasterEgg {
about: Container,
snake: Option<SnakeGame>,
logo_clicks: Vec<Instant>,
}
impl AboutWithEasterEgg {
fn new(about: Container) -> Self {
Self {
about,
snake: None,
logo_clicks: Vec::new(),
}
}
fn register_logo_click(&mut self, now: Instant) -> bool {
self.logo_clicks
.retain(|t| now.duration_since(*t) <= EASTER_EGG_WINDOW);
self.logo_clicks.push(now);
self.logo_clicks.len() >= EASTER_EGG_CLICKS
}
}
impl Widget for AboutWithEasterEgg {
fn bounds(&self) -> Rect {
self.about.bounds()
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
match &mut self.snake {
Some(game) => game.paint(painter, self.about.bounds()),
None => self.about.paint(painter, theme),
}
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
if let Some(game) = self.snake.as_mut() {
if let Event::KeyDown {
key: Key::Named(NamedKey::Escape),
..
} = event
{
self.snake = None;
ctx.request_paint();
return;
}
game.event(event, ctx);
return;
}
if let Event::PointerDown {
pos,
button: MouseButton::Left,
} = event
&& LOGO_HIT.contains(*pos)
&& self.register_logo_click(Instant::now())
{
self.snake = Some(SnakeGame::new(self.about.bounds()));
self.logo_clicks.clear();
ctx.request_paint();
return;
}
self.about.event(event, ctx);
}
fn layout(&mut self, bounds: Rect) {
self.about.layout(bounds);
}
fn popup_request(&self) -> Option<PopupRequest> {
if self.snake.is_some() {
None
} else {
self.about.popup_request()
}
}
fn wants_ticks(&self) -> bool {
self.snake.is_some()
}
}
const CELL: i32 = 10;
const STEP_INTERVAL: Duration = Duration::from_millis(110);
const HUD_TOP: i32 = 20;
const HUD_BOTTOM: i32 = 18;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Direction {
Up,
Down,
Left,
Right,
}
impl Direction {
fn delta(self) -> (i32, i32) {
match self {
Direction::Up => (0, -1),
Direction::Down => (0, 1),
Direction::Left => (-1, 0),
Direction::Right => (1, 0),
}
}
fn opposite(self) -> Direction {
match self {
Direction::Up => Direction::Down,
Direction::Down => Direction::Up,
Direction::Left => Direction::Right,
Direction::Right => Direction::Left,
}
}
}
struct SnakeGame {
grid_w: i32,
grid_h: i32,
offset_x: i32,
offset_y: i32,
body: VecDeque<(i32, i32)>,
direction: Direction,
pending: VecDeque<Direction>,
food: (i32, i32),
rng_state: u64,
last_step: Instant,
game_over: bool,
score: u32,
}
fn play_rect(bounds: Rect) -> Rect {
Rect::new(
bounds.x,
bounds.y + HUD_TOP,
bounds.w,
(bounds.h - HUD_TOP - HUD_BOTTOM).max(CELL),
)
}
impl SnakeGame {
fn new(bounds: Rect) -> Self {
let play = play_rect(bounds);
let grid_w = (play.w / CELL).max(8);
let grid_h = (play.h / CELL).max(8);
let offset_x = (play.w - grid_w * CELL) / 2;
let offset_y = (play.h - grid_h * CELL) / 2;
let mut body = VecDeque::new();
let cx = grid_w / 2;
let cy = grid_h / 2;
body.push_back((cx, cy));
body.push_back((cx - 1, cy));
body.push_back((cx - 2, cy));
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let seed = nanos ^ (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
let mut g = Self {
grid_w,
grid_h,
offset_x,
offset_y,
body,
direction: Direction::Right,
pending: VecDeque::new(),
food: (0, 0),
rng_state: seed.max(1),
last_step: Instant::now(),
game_over: false,
score: 0,
};
g.place_food();
g
}
const MAX_PENDING: usize = 2;
fn queue_direction(&mut self, dir: Direction) {
let last = self.pending.back().copied().unwrap_or(self.direction);
if dir == last || dir == last.opposite() {
return;
}
if self.pending.len() >= Self::MAX_PENDING {
return;
}
self.pending.push_back(dir);
}
fn rand_u32(&mut self) -> u32 {
let mut x = self.rng_state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.rng_state = x;
(x >> 32) as u32
}
fn place_food(&mut self) {
loop {
let x = (self.rand_u32() % self.grid_w as u32) as i32;
let y = (self.rand_u32() % self.grid_h as u32) as i32;
if !self.body.iter().any(|c| *c == (x, y)) {
self.food = (x, y);
return;
}
}
}
fn step(&mut self) {
if let Some(next) = self.pending.pop_front() {
self.direction = next;
}
let &(hx, hy) = self.body.front().expect("snake body is non-empty");
let (dx, dy) = self.direction.delta();
let head = (hx + dx, hy + dy);
let hit_wall = head.0 < 0 || head.0 >= self.grid_w || head.1 < 0 || head.1 >= self.grid_h;
let will_grow = head == self.food;
let hit_self = self
.body
.iter()
.enumerate()
.any(|(i, c)| *c == head && (will_grow || i + 1 < self.body.len()));
if hit_wall || hit_self {
self.game_over = true;
return;
}
self.body.push_front(head);
if will_grow {
self.score += 1;
self.place_food();
} else {
self.body.pop_back();
}
}
fn reset(&mut self) {
self.body.clear();
let cx = self.grid_w / 2;
let cy = self.grid_h / 2;
self.body.push_back((cx, cy));
self.body.push_back((cx - 1, cy));
self.body.push_back((cx - 2, cy));
self.direction = Direction::Right;
self.pending.clear();
self.place_food();
self.last_step = Instant::now();
self.game_over = false;
self.score = 0;
}
fn paint(&self, painter: &mut Painter, bounds: Rect) {
let play = play_rect(bounds);
let title_bar = Rect::new(bounds.x, bounds.y, bounds.w, HUD_TOP);
painter.fill_rect(title_bar, Color::LIGHT_GRAY);
painter.h_line(bounds.x, bounds.y + HUD_TOP - 1, bounds.w, Color::BLACK);
let score_text = format!("SCORE {}", self.score);
painter.text(bounds.x + 8, bounds.y + 4, &score_text, 11.0, Color::BLACK);
painter.fill_rect(play, Color::WHITE);
let cell_rect = |gx: i32, gy: i32| {
Rect::new(
play.x + self.offset_x + gx * CELL,
play.y + self.offset_y + gy * CELL,
CELL,
CELL,
)
};
let food = cell_rect(self.food.0, self.food.1);
painter.fill_rect(food.inset(1), Color::RED);
for (i, &(x, y)) in self.body.iter().enumerate() {
let r = cell_rect(x, y).inset(1);
let color = if i == 0 { Color::BLACK } else { Color::NAVY };
painter.fill_rect(r, color);
}
let hint_bar = Rect::new(
bounds.x,
bounds.y + bounds.h - HUD_BOTTOM,
bounds.w,
HUD_BOTTOM,
);
painter.fill_rect(hint_bar, Color::LIGHT_GRAY);
painter.h_line(bounds.x, hint_bar.y, bounds.w, Color::BLACK);
let hint = "ESC back \u{2022} SPACE restart";
painter.text(bounds.x + 8, hint_bar.y + 4, hint, 10.0, Color::DARK_GRAY);
if self.game_over {
let banner = Rect::new(play.x + play.w / 2 - 80, play.y + play.h / 2 - 18, 160, 36);
painter.fill_rect(banner, Color::WHITE);
painter.stroke_rect(banner, Color::BLACK);
painter.text_centered(banner, "GAME OVER", 16.0, Color::RED);
}
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
match event {
Event::KeyDown { key, .. } => match key {
Key::Named(NamedKey::Left) => {
self.queue_direction(Direction::Left);
}
Key::Named(NamedKey::Right) => {
self.queue_direction(Direction::Right);
}
Key::Named(NamedKey::Up) => {
self.queue_direction(Direction::Up);
}
Key::Named(NamedKey::Down) => {
self.queue_direction(Direction::Down);
}
Key::Named(NamedKey::Space) if self.game_over => {
self.reset();
ctx.request_paint();
}
_ => {}
},
Event::Tick => {
if self.game_over {
return;
}
let now = Instant::now();
if now.duration_since(self.last_step) >= STEP_INTERVAL {
self.last_step = now;
self.step();
ctx.request_paint();
}
}
_ => {}
}
}
}
fn format_bytes(bytes: u64) -> String {
const KB: f64 = 1000.0;
const MB: f64 = KB * 1000.0;
const GB: f64 = MB * 1000.0;
let bytes_f = bytes as f64;
if bytes_f >= GB {
format!("{:.1} GB", bytes_f / GB)
} else if bytes_f >= MB {
format!("{:.1} MB", bytes_f / MB)
} else if bytes_f >= KB {
format!("{:.1} kB", bytes_f / KB)
} else {
format!("{} B", bytes)
}
}
fn format_binary_bytes(bytes: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
let bytes_f = bytes as f64;
if bytes_f >= GB {
format!("{:.1} GiB", bytes_f / GB)
} else if bytes_f >= MB {
format!("{:.1} MiB", bytes_f / MB)
} else if bytes_f >= KB {
format!("{:.1} KiB", bytes_f / KB)
} else {
format!("{} B", bytes)
}
}
fn gather_system_info() -> SystemInfo {
let mut sys = System::new_all();
sys.refresh_all();
let operating_system = System::long_os_version()
.or_else(System::os_version)
.unwrap_or_else(|| "Unknown OS".to_string());
let cpu = sys
.cpus()
.first()
.map(|cpu| {
let brand = cpu.brand().trim();
let freq = cpu.frequency();
if freq > 0 {
format!("{} @ {} MHz", brand, freq)
} else {
brand.to_string()
}
})
.unwrap_or_else(|| "Unknown CPU".to_string());
let memory_line = format_binary_bytes(sys.total_memory());
let disks = Disks::new_with_refreshed_list();
let mut total_disk = 0u64;
let mut avail_disk = 0u64;
for disk in disks.list() {
total_disk += disk.total_space();
avail_disk += disk.available_space();
}
let disk_line = if total_disk > 0 {
format!(
"{} Free of {}",
format_bytes(avail_disk),
format_bytes(total_disk)
)
} else {
"Disk information unavailable".to_string()
};
let uptime = seconds_to_string(System::uptime());
let vendor = Product::vendor_name().map(|v| v.trim().to_string());
let family = Product::family()
.map(|m| m.trim().to_string())
.filter(|m| !m.is_empty());
let machine = match &family {
Some(model) => prettify_model(model),
None => short_hostname(),
};
SystemInfo {
machine,
operating_system,
vendor,
model: Product::name(),
cpu,
memory_line,
disk_line,
kernel: System::kernel_long_version(),
uptime,
}
}
fn short_hostname() -> String {
match System::host_name() {
Some(host) => host.split('.').next().unwrap_or(&host).to_string(),
None => "Computer".to_string(),
}
}
fn prettify_model(model: &str) -> String {
const RULES: &[(&str, &str)] = &[
(r"^MacBookPro", "MacBook Pro"),
(r"^MacBookAir", "MacBook Air"),
(r"^MacBook", "MacBook"),
(r"^Macmini", "Mac mini"),
(r"^MacStudio", "Mac Studio"),
(r"^MacPro", "Mac Pro"),
(r"^iMacPro", "iMac Pro"),
(r"^iMac", "iMac"),
];
for (pattern, name) in RULES {
if Regex::new(pattern).is_ok_and(|re| re.is_match(model)) {
return (*name).to_string();
}
}
model.to_string()
}
fn seconds_to_string(seconds: u64) -> String {
let days = seconds / 86_400;
let hours = (seconds % 86_400) / 3_600;
let minutes = (seconds % 3_600) / 60;
let mut result = String::new();
if days > 0 {
result.push_str(&format!("{}d", days));
}
if hours > 0 {
if !result.is_empty() {
result.push(' ');
}
result.push_str(&format!("{}h", hours));
}
if minutes > 0 || result.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(&format!("{}m", minutes));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prettify_model_maps_apple_identifiers() {
assert_eq!(prettify_model("MacBookPro18,3"), "MacBook Pro");
assert_eq!(prettify_model("MacBookAir10,1"), "MacBook Air");
assert_eq!(prettify_model("MacBook10,1"), "MacBook");
assert_eq!(prettify_model("Macmini9,1"), "Mac mini");
assert_eq!(prettify_model("iMac21,1"), "iMac");
assert_eq!(prettify_model("iMacPro1,1"), "iMac Pro");
}
#[test]
fn prettify_model_passes_through_unknown_identifiers() {
assert_eq!(prettify_model("20AN"), "20AN");
assert_eq!(prettify_model("Mac14,2"), "Mac14,2");
}
}