use std::collections::VecDeque;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use regex::Regex;
use retrofetch::host;
use retrofetch::ui::{self, SystemInfo};
use saudade::EventCtx;
use saudade::{
App, Color, Container, Event, Key, MouseButton, NamedKey, Painter, PopupRequest, Rect, Theme,
Widget, WindowConfig,
};
use sysinfo::System;
const EASTER_EGG_CLICKS: usize = 5;
const EASTER_EGG_WINDOW: Duration = Duration::from_secs(2);
fn main() {
let info = gather_system_info();
let theme = Theme::windows_31();
let content_width =
ui::compute_content_width(&info, host::product_family().as_deref(), theme.font_size);
let about = ui::build_about_box(&info, content_width);
let height = about.bounds().h;
let root = AboutWithEasterEgg::new(about);
App::new(
WindowConfig::new("About This Computer", content_width, height),
root,
)
.with_theme(theme)
.run();
}
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
&& ui::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_quantity(value: f64, unit: &str) -> String {
let s = format!("{:.1}", value);
let trimmed = s.strip_suffix(".0").unwrap_or(&s);
format!("{} {}", trimmed, unit)
}
fn format_bytes(bytes: u64) -> String {
const KB: f64 = 1000.0;
const MB: f64 = KB * 1000.0;
const GB: f64 = MB * 1000.0;
const TB: f64 = GB * 1000.0;
const PB: f64 = TB * 1000.0;
let bytes_f = bytes as f64;
if bytes_f >= PB {
format_quantity(bytes_f / PB, "PB")
} else if bytes_f >= TB {
format_quantity(bytes_f / TB, "TB")
} else if bytes_f >= GB {
format_quantity(bytes_f / GB, "GB")
} else if bytes_f >= MB {
format_quantity(bytes_f / MB, "MB")
} else if bytes_f >= KB {
format_quantity(bytes_f / KB, "kB")
} else {
format!("{} B", bytes)
}
}
fn round_installed_memory(bytes: u64) -> u64 {
const GIB: u64 = 1024 * 1024 * 1024;
if bytes > 4 * GIB {
bytes.div_ceil(2 * GIB) * 2 * GIB
} else if bytes > 2 * GIB {
bytes.div_ceil(GIB) * GIB
} else {
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;
const TB: f64 = GB * 1024.0;
const PB: f64 = TB * 1024.0;
let bytes_f = bytes as f64;
if bytes_f >= PB {
format_quantity(bytes_f / PB, "PiB")
} else if bytes_f >= TB {
format_quantity(bytes_f / TB, "TiB")
} else if bytes_f >= GB {
format_quantity(bytes_f / GB, "GiB")
} else if bytes_f >= MB {
format_quantity(bytes_f / MB, "MiB")
} else if bytes_f >= KB {
format_quantity(bytes_f / KB, "KiB")
} else {
format!("{} B", bytes)
}
}
fn gather_system_info() -> SystemInfo {
let mut sys = System::new_all();
sys.refresh_all();
let operating_system = host::long_os_version()
.or_else(host::os_version)
.unwrap_or_else(|| "Unknown OS".to_string());
let cpu = match host::cpu_brand(&sys) {
Some(brand) => {
let trimmed = brand.trim();
match host::cpu_frequency_mhz(&sys) {
Some(freq) if freq > 0 => format!("{} @ {} MHz", trimmed, freq),
_ => trimmed.to_string(),
}
}
None => "Unknown CPU".to_string(),
};
let memory_line = format_binary_bytes(round_installed_memory(host::total_memory_bytes(&sys)));
let (total_disk, avail_disk) = host::disk_totals();
let disk_line = if total_disk > 0 {
format!(
"{} ({} free)",
format_bytes(total_disk),
format_bytes(avail_disk)
)
} else {
"Disk information unavailable".to_string()
};
let uptime = seconds_to_string(host::uptime_seconds());
let vendor = host::product_vendor_name().map(|v| v.trim().to_string());
let family = host::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: host::product_name(),
cpu,
memory_line,
disk_line,
kernel: host::kernel_long_version(),
window_manager: host::window_manager(),
packages: host::installed_package_count(),
uptime,
distribution_id: System::distribution_id(),
}
}
fn short_hostname() -> String {
match host::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");
}
#[test]
fn round_installed_memory_snaps_to_plausible_sizes() {
const GIB: u64 = 1024 * 1024 * 1024;
assert_eq!(round_installed_memory(16_959_840_256), 16 * GIB);
assert_eq!(round_installed_memory(16 * GIB), 16 * GIB);
assert_eq!(round_installed_memory(8 * GIB), 8 * GIB);
assert_eq!(round_installed_memory(6 * GIB - 200 * 1024 * 1024), 6 * GIB);
assert_eq!(round_installed_memory(3 * GIB - 100 * 1024 * 1024), 3 * GIB);
assert_eq!(round_installed_memory(900_000_000), 900_000_000);
}
}