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 <noreply@anthropic.com>
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -11,6 +11,17 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ahfail-display"
|
name = "ahfail-display"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ahfail-ui",
|
||||||
|
"cc",
|
||||||
|
"gdk",
|
||||||
|
"glib",
|
||||||
|
"gstreamer",
|
||||||
|
"gstreamer-player",
|
||||||
|
"gtk",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahfail-gtklock"
|
name = "ahfail-gtklock"
|
||||||
|
|||||||
@@ -2,3 +2,19 @@
|
|||||||
name = "ahfail-display"
|
name = "ahfail-display"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
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"
|
||||||
|
|||||||
51
crates/ahfail-display/build.rs
Normal file
51
crates/ahfail-display/build.rs
Normal file
@@ -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");
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user