From f93ca6267c8a23c04f7d89ecfd1cb014c7f46ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Geel=20Weirs=C3=B8e?= Date: Wed, 6 May 2026 11:54:22 +0200 Subject: [PATCH] feat: add ahfail-display standalone binary Implements the ahfail-display binary crate: GTK popup window that spawns the Nedry sprite and plays the audio clip, with SIGTERM handling, 15-minute failsafe, deadzone CLI parsing, volume save/restore, and update check. Adds a build.rs that compiles GResources via glib-compile-resources so the binary can be built with plain `cargo build` outside of Meson. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 11 ++++ crates/ahfail-display/Cargo.toml | 16 +++++ crates/ahfail-display/build.rs | 51 +++++++++++++++ crates/ahfail-display/src/main.rs | 104 +++++++++++++++++++++++++++++- 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 crates/ahfail-display/build.rs diff --git a/Cargo.lock b/Cargo.lock index 421bc47..c1d9174 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,17 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahfail-display" version = "0.1.0" +dependencies = [ + "ahfail-ui", + "cc", + "gdk", + "glib", + "gstreamer", + "gstreamer-player", + "gtk", + "libc", + "pkg-config", +] [[package]] name = "ahfail-gtklock" diff --git a/crates/ahfail-display/Cargo.toml b/crates/ahfail-display/Cargo.toml index 3dc7ba4..613b1f1 100644 --- a/crates/ahfail-display/Cargo.toml +++ b/crates/ahfail-display/Cargo.toml @@ -2,3 +2,19 @@ name = "ahfail-display" version = "0.1.0" edition = "2021" + +[[bin]] +name = "ahfail-display" + +[dependencies] +ahfail-ui = { path = "../ahfail-ui" } +gtk = { version = "0.15", package = "gtk", features = ["v3_24"] } +gdk = { version = "0.15", package = "gdk", features = ["v3_24"] } +gstreamer = { version = "0.18", package = "gstreamer", features = ["v1_18"] } +gstreamer-player = { version = "0.18", package = "gstreamer-player" } +glib = { version = "0.15", package = "glib" } +libc = "0.2" + +[build-dependencies] +cc = "1" +pkg-config = "0.3" diff --git a/crates/ahfail-display/build.rs b/crates/ahfail-display/build.rs new file mode 100644 index 0000000..45e36a3 --- /dev/null +++ b/crates/ahfail-display/build.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + // Workspace root is two levels up from crates/ahfail-display/ + let workspace_root = manifest_dir.join("../..").canonicalize().unwrap(); + let assets_dir = workspace_root.join("assets"); + let gresource_xml = assets_dir.join("ahfail.gresource.xml"); + + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let c_src = out_dir.join("ahfail-resources.c"); + + // Re-run if any asset changes + println!("cargo:rerun-if-changed={}", gresource_xml.display()); + for entry in std::fs::read_dir(&assets_dir).unwrap() { + let entry = entry.unwrap(); + println!("cargo:rerun-if-changed={}", entry.path().display()); + } + + let status = Command::new("glib-compile-resources") + .args([ + "--generate-source", + "--target", + c_src.to_str().unwrap(), + "--sourcedir", + assets_dir.to_str().unwrap(), + gresource_xml.to_str().unwrap(), + ]) + .status() + .expect("glib-compile-resources not found — install libglib2.0-dev or equivalent"); + + assert!(status.success(), "glib-compile-resources failed"); + + // Use pkg-config to get include flags for gio-2.0 + let gio_cflags = Command::new("pkg-config") + .args(["--cflags", "gio-2.0"]) + .output() + .expect("pkg-config not found"); + let gio_cflags_str = String::from_utf8(gio_cflags.stdout).unwrap(); + + let mut build = cc::Build::new(); + build.file(&c_src).flag_if_supported("-w"); // suppress warnings in generated code + for flag in gio_cflags_str.split_whitespace() { + build.flag(flag); + } + build.compile("ahfail_resources"); + + // Link against gio-2.0 for GResource support + println!("cargo:rustc-link-lib=gio-2.0"); +} diff --git a/crates/ahfail-display/src/main.rs b/crates/ahfail-display/src/main.rs index f328e4d..9429609 100644 --- a/crates/ahfail-display/src/main.rs +++ b/crates/ahfail-display/src/main.rs @@ -1 +1,103 @@ -fn main() {} +use gtk::prelude::*; +use gtk::gdk; +use gstreamer as gst; +use std::sync::atomic::{AtomicBool, Ordering}; + +const AUDIO_URI: &str = "resource:///ahfail/audio/magic-word.mp3"; +const FAILSAFE_MINUTES: u32 = 15; + +static SIGTERM_RECEIVED: AtomicBool = AtomicBool::new(false); + +extern "C" fn handle_sigterm(_: libc::c_int) { + SIGTERM_RECEIVED.store(true, Ordering::Relaxed); +} + +fn main() { + unsafe { + libc::signal(libc::SIGTERM, handle_sigterm as *const () as libc::sighandler_t); + } + + if gtk::init().is_err() { + eprintln!("[ahfail-display] GTK init failed"); + return; + } + if gst::init().is_err() { + eprintln!("[ahfail-display] GStreamer init failed"); + return; + } + + let animation = unsafe { ahfail_ui::animation::load_animation() }; + let Some(animation) = animation else { + eprintln!("[ahfail-display] No animation frames found"); + return; + }; + + let volume_state = ahfail_ui::volume::save_and_set_max(); + + let display = gdk::Display::default().expect("[ahfail-display] No display"); + let monitor = display + .primary_monitor() + .or_else(|| display.monitor(0)) + .expect("[ahfail-display] No monitor"); + let geom = monitor.geometry(); + let screen_w = geom.width(); + let screen_h = geom.height(); + + let window = gtk::Window::new(gtk::WindowType::Popup); + window.set_decorated(false); + window.set_keep_above(true); + window.set_skip_taskbar_hint(true); + window.move_(geom.x(), geom.y()); + window.set_default_size(screen_w, screen_h); + + let fixed = gtk::Fixed::new(); + fixed.set_size_request(screen_w, screen_h); + window.add(&fixed); + + let config = parse_args(); + ahfail_ui::display::place_sprite(&fixed, &animation, screen_w, screen_h, &config); + + let player = ahfail_ui::audio::create_player(AUDIO_URI); + player.play(); + + std::thread::spawn(|| ahfail_ui::update::check_for_update(ahfail_ui::VERSION)); + + glib::timeout_add_seconds(FAILSAFE_MINUTES * 60, || { + gtk::main_quit(); + glib::Continue(false) + }); + + glib::timeout_add(std::time::Duration::from_millis(200), move || { + if SIGTERM_RECEIVED.load(Ordering::Relaxed) { + gtk::main_quit(); + return glib::Continue(false); + } + glib::Continue(true) + }); + + window.show_all(); + gtk::main(); + + player.stop(); + if let Some(vs) = volume_state { + ahfail_ui::volume::restore(vs); + } +} + +fn parse_args() -> ahfail_ui::config::ModuleConfig { + let deadzone = std::env::args() + .find(|a| a.starts_with("--deadzone=")) + .and_then(|a| { + let val = a.trim_start_matches("--deadzone="); + let p: Vec<&str> = val.split(',').collect(); + if p.len() != 4 { + return None; + } + let x: i32 = p[0].parse().ok()?; + let y: i32 = p[1].parse().ok()?; + let w: i32 = p[2].parse().ok()?; + let h: i32 = p[3].parse().ok()?; + Some(gdk::Rectangle::new(x, y, w, h)) + }); + ahfail_ui::config::ModuleConfig { deadzone } +}