From 2b89653be64e3638c4c72eaff60b52c3696d730d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Geel=20Weirs=C3=B8e?= Date: Wed, 6 May 2026 09:25:39 +0200 Subject: [PATCH] refactor: remove dead utils/bench.rs Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 69 ++++++ GEMINI.md | 56 +++++ crates/ahfail-gtklock/src/utils/bench.rs | 12 - meson.build | 2 +- src/config.rs | 37 --- src/context.rs | 110 --------- src/handler.rs | 165 ------------- src/lib.rs | 193 --------------- src/state.rs | 26 -- src/utils/bench.rs | 12 - tests/ahfail_tests.rs | 299 ----------------------- tests/benchmarks.rs | 57 ----- 12 files changed, 126 insertions(+), 912 deletions(-) create mode 100644 CLAUDE.md create mode 100644 GEMINI.md delete mode 100644 crates/ahfail-gtklock/src/utils/bench.rs delete mode 100644 src/config.rs delete mode 100644 src/context.rs delete mode 100644 src/handler.rs delete mode 100644 src/lib.rs delete mode 100644 src/state.rs delete mode 100644 src/utils/bench.rs delete mode 100644 tests/ahfail_tests.rs delete mode 100644 tests/benchmarks.rs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..44fa104 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Is + +`ahfail` is a [`gtklock`](https://github.com/jovanlanik/gtklock) module. It compiles to `ahfail-module.so`, which gtklock loads at runtime. On each failed unlock attempt (`PW_FAILURE`), it spawns an animated "Nedry" sprite at a random screen location and plays an audio clip ("ah ah ah, you didn't say the magic word"). + +## Build System + +The build is a two-stage hybrid: + +1. **Meson** compiles GResources from `assets/ahfail.gresource.xml` into C, invokes Cargo to produce `libahfail_module.a`, then links everything into `ahfail-module.so`. +2. **Cargo** handles only the Rust crate — it produces a `staticlib` that Meson links. + +Always use Meson for the final shared object; `cargo build` alone does not produce the loadable module. + +```bash +# First-time setup +meson setup builddir + +# Build +meson compile -C builddir + +# Run Rust tests only (no GTK display required) +cargo test + +# Run Meson tests (loads the .so, requires GTK) +meson test -C builddir + +# Manual integration test +gtklock -d -m builddir/ahfail-module.so -- --deadzone=X,Y,W,H +``` + +## Architecture + +All FFI entry points live in `src/lib.rs` — these are the `extern "C"` functions that gtklock calls (`on_activation`, `on_window_create`, `on_window_destroy`, `on_idle_hide`, etc.). + +**Module lifecycle:** +- `on_activation` — called once at startup; initialises GTK/GStreamer, loads all sprite frames into a `PixbufSimpleAnim`, stores them in `MODULE_STATE`. +- `on_window_create` — called per monitor; calls `WindowHandler::create`, which creates a `gtk::Fixed` overlay, a pool of pre-warmed GStreamer players, and wires up a signal on `error_label` that fires on each failed attempt. +- `on_window_destroy` / `on_idle_hide` — clean up sprites and stop players. + +**Key types:** + +| Type | File | Purpose | +|------|------|---------| +| `MODULE_STATE` | `src/state.rs` | Thread-local `RefCell` holding the shared animation, audio URI, and deadzone config | +| `WindowData` | `src/state.rs` | Heap-allocated per-window state (sprites, player pool, GTK signal handle) | +| `WindowContext` | `src/context.rs` | Safe wrapper around the raw `*mut Window` pointer passed from C | +| `Window` / `GtkLock` | `src/context.rs` | `#[repr(C)]` mirrors of gtklock's internal structs — must match `include/gtklock-module.h` | +| `WindowHandler` | `src/handler.rs` | Sprite placement logic (random position, deadzone avoidance with retries) and GStreamer player management | +| `ModuleConfig` | `src/config.rs` | Parses the `--deadzone=x,y,w,h` CLI argument via glib's `GOptionEntry` | + +**Assets** are embedded as GResources at build time. At runtime they are accessed via `resource:///ahfail/sprites/...` and `resource:///ahfail/audio/magic-word.mp3`. + +## Safety Conventions + +- All `unsafe` pointer work on `*mut Window` goes through `WindowContext` — never dereference the raw pointer outside that wrapper. +- `WindowData` is heap-allocated via `Box::into_raw` and reclaimed via `Box::from_raw` in `WindowContext::take_data`. Do not free it any other way. +- The `module_data` flexible array field on `Window` is a C ABI contract with gtklock — index 0 is this module's slot. + +## Tests + +`tests/ahfail_tests.rs` — integration tests that construct mock `Window` / `GtkLock` structs directly to exercise handler logic without a running gtklock process. Run with `cargo test`. + +`tests/benchmarks.rs` — criterion benchmarks for sprite placement. + +`tests/module_test.c` — C smoke test built by Meson that dlopen-style verifies the exported symbols exist. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..c4c30c9 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,56 @@ +# GEMINI.md + +## Project Overview + +This project is a `gtklock` module named `ahfail`, written in Rust (using `gtk-rs`) with a Meson build system. + +The module listens for failed unlock attempts (`PW_FAILURE`) in `gtklock`. Upon failure, it: +1. Spawns a looping "Nedry" sprite animation at a random screen location. +2. Plays an audio clip ("ah ah ah, you didn't say the magic word"). +3. Avoids placing sprites in a user-configurable "deadzone". + +All assets (images and audio) are compiled into the module binary as GResources. + +## Build Architecture + +* **Meson:** The primary build system. It handles: + * Compiling GResources (`assets/ahfail.gresource.xml`). + * Invoking Cargo to build the Rust code as a static library (`libahfail_module.a`). + * Linking the Rust static library, GResources, and C dependencies into the final shared object (`ahfail-module.so`). +* **Cargo:** Handles the Rust source code, dependencies (`gtk`, `gdk`, `gstreamer`), and tests. + +## Key Files + +* `src/lib.rs`: FFI entry points (`on_activation`, `on_window_create`, etc.) exported to C. +* `src/handler.rs`: Main logic for sprite placement and audio playback. +* `src/config.rs`: Argument parsing logic. +* `tests/ahfail_tests.rs`: Comprehensive integration tests mocking `gtklock` behavior. +* `meson.build`: Build definition bridging C and Rust. + +## Building and Running + +### Prerequisites +* Meson, Ninja, Rust (Cargo) +* `gtk3` development headers +* `gstreamer` + `gst-plugins-base` + `gst-plugins-good` (runtime) + +### Commands +```bash +# Setup +meson setup builddir + +# Build +meson compile -C builddir + +# Test (Rust logic) +cargo test + +# Run (Manual test) +gtklock -d -m builddir/ahfail-module.so -- --deadzone=X,Y,W,H +``` + +## Development Conventions + +* **Safety:** Use `WindowContext` wrappers in `src/context.rs` to handle unsafe `Window` pointers. +* **State:** `MODULE_STATE` (thread-local) holds global config/assets. `WindowData` (heap-allocated) holds per-window sprites and players. +* **Tests:** `tests/ahfail_tests.rs` contains integration tests that mock `gtklock` structures. Run with `cargo test`. diff --git a/crates/ahfail-gtklock/src/utils/bench.rs b/crates/ahfail-gtklock/src/utils/bench.rs deleted file mode 100644 index 99f7fc8..0000000 --- a/crates/ahfail-gtklock/src/utils/bench.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::time::Instant; - -pub fn time_execution(name: &str, f: F) -> T -where - F: FnOnce() -> T, -{ - let start = Instant::now(); - let result = f(); - let duration = start.elapsed(); - println!("[benchmark] {}: {:?}", name, duration); - result -} diff --git a/meson.build b/meson.build index 52f8123..5ecad45 100644 --- a/meson.build +++ b/meson.build @@ -22,7 +22,7 @@ resources = gnome.compile_resources( cargo_target = custom_target( 'ahfail-cargo-build', - input: ['src/lib.rs', 'Cargo.toml'], + input: ['crates/ahfail-gtklock/src/lib.rs', 'Cargo.toml'], output: ['libahfail_module.a'], command: [ 'sh', '-c', 'cargo build --release -p ahfail-gtklock --target-dir "@OUTDIR@/target" && cp "@OUTDIR@/target/release/libahfail_module.a" "@OUTPUT@"' diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 797fdff..0000000 --- a/src/config.rs +++ /dev/null @@ -1,37 +0,0 @@ -use gtk::{glib, gdk}; -use std::ffi::CStr; -use std::ptr; - -// Storage for the command line argument string -pub static mut DEADZONE_ARG: *mut std::os::raw::c_char = ptr::null_mut(); - -pub const DEADZONE_LONG: &[u8] = b"deadzone\0"; -pub const DEADZONE_DESC: &[u8] = b"Area to avoid spawning sprites (x,y,w,h)\0"; -pub const DEADZONE_ARG_DESC: &[u8] = b"x,y,w,h\0"; - -pub struct ModuleConfig { - pub deadzone: Option, -} - -impl ModuleConfig { - pub unsafe fn from_args() -> Self { - let mut deadzone = None; - if !DEADZONE_ARG.is_null() { - let c_str = CStr::from_ptr(DEADZONE_ARG); - if let Ok(s) = c_str.to_str() { - let parts: Vec<&str> = s.split(',').collect(); - if parts.len() == 4 { - if let (Ok(x), Ok(y), Ok(w), Ok(h)) = (parts[0].parse(), parts[1].parse(), parts[2].parse(), parts[3].parse()) { - deadzone = Some(gdk::Rectangle::new(x, y, w, h)); - println!("[ahfail] Configured deadzone: {:?}", deadzone); - } else { - eprintln!("[ahfail] Invalid numbers in deadzone argument: {}", s); - } - } else { - eprintln!("[ahfail] Invalid format for deadzone argument (expected x,y,w,h): {}", s); - } - } - } - Self { deadzone } - } -} diff --git a/src/context.rs b/src/context.rs deleted file mode 100644 index 45a7bf0..0000000 --- a/src/context.rs +++ /dev/null @@ -1,110 +0,0 @@ -use gtk::prelude::*; // Keep this if extension traits are needed, otherwise remove. Window uses it. -use gtk::{glib, gdk}; // Window uses gdk, glib. -use gtk::gdk::Monitor; -use std::ffi::c_void; -use std::marker::PhantomData; -use std::ptr; // Import ptr -use glib::translate::from_glib_none; -use crate::state::WindowData; - -#[repr(C)] -pub struct __IncompleteArrayField(PhantomData); - -impl __IncompleteArrayField { - pub const fn new() -> Self { - Self(PhantomData) - } - - /// # Safety - /// Returns a raw pointer to the field. The caller must ensure the struct was allocated - /// with enough space for this flexible array member. - pub unsafe fn as_ptr(&self) -> *mut T { - self as *const _ as *mut T - } -} - -impl Default for __IncompleteArrayField { - fn default() -> Self { - Self::new() - } -} - -#[repr(C)] -pub struct Window { - pub monitor: *mut gdk::ffi::GdkMonitor, - pub window: *mut gtk::ffi::GtkWidget, - pub overlay: *mut gtk::ffi::GtkOverlay, - pub window_box: *mut gtk::ffi::GtkWidget, - pub body_revealer: *mut gtk::ffi::GtkWidget, - pub body_grid: *mut gtk::ffi::GtkWidget, - pub input_label: *mut gtk::ffi::GtkWidget, - pub input_field: *mut gtk::ffi::GtkWidget, - pub message_revealer: *mut gtk::ffi::GtkWidget, - pub message_scrolled_window: *mut gtk::ffi::GtkWidget, - pub message_box: *mut gtk::ffi::GtkWidget, - pub unlock_button: *mut gtk::ffi::GtkWidget, - pub error_label: *mut gtk::ffi::GtkWidget, - pub warning_label: *mut gtk::ffi::GtkWidget, - pub info_box: *mut gtk::ffi::GtkWidget, - pub time_box: *mut gtk::ffi::GtkWidget, - pub clock_label: *mut gtk::ffi::GtkWidget, - pub date_label: *mut gtk::ffi::GtkWidget, - pub module_data: __IncompleteArrayField<*mut c_void>, -} - -#[repr(C)] -pub struct GtkLock { - pub windows: *mut glib::ffi::GArray, -} - -// Safe wrapper for Window pointer operations -pub struct WindowContext(*mut Window); - -impl WindowContext { - /// # Safety - /// `ctx` must be a valid pointer to a `Window` struct. - pub unsafe fn new(ctx: *mut Window) -> Option { - if ctx.is_null() { - None - } else { - Some(Self(ctx)) - } - } - - pub unsafe fn get_error_label(&self) -> gtk::Label { - from_glib_none((*self.0).error_label as *mut gtk::ffi::GtkLabel) - } - - pub unsafe fn get_overlay(&self) -> gtk::Overlay { - from_glib_none((*self.0).overlay as *mut gtk::ffi::GtkOverlay) - } - - pub unsafe fn get_monitor(&self) -> Monitor { - from_glib_none((*self.0).monitor as *mut gdk::ffi::GdkMonitor) - } - - /// # Safety - /// The caller must ensure `module_data` has been initialized and points to a valid `WindowData`. - pub unsafe fn set_data(&self, data: Box) { - let raw_ptr = Box::into_raw(data); - (*(*self.0).module_data.as_ptr()) = raw_ptr as *mut c_void; - } - - pub unsafe fn get_data_ptr(&self) -> *mut WindowData { - *(*self.0).module_data.as_ptr() as *mut WindowData - } - - /// # Safety - /// The caller takes ownership of the data and must drop it. - pub unsafe fn take_data(&self) -> Option> { - let ptr_ref = (*self.0).module_data.as_ptr(); - let ptr = *ptr_ref; - if ptr.is_null() { - None - } else { - *ptr_ref = ptr::null_mut(); // Clear it - Some(Box::from_raw(ptr as *mut WindowData)) - } - } -} - diff --git a/src/handler.rs b/src/handler.rs deleted file mode 100644 index fc1717d..0000000 --- a/src/handler.rs +++ /dev/null @@ -1,165 +0,0 @@ -use gtk::prelude::*; -use gtk::{glib, gdk, gdk_pixbuf, gio}; -use gstreamer as gst; -use gstreamer_player as gst_player; -use rand::Rng; -use crate::state::{MODULE_STATE, WindowData}; -use crate::context::WindowContext; - -const SPRITE_MARGIN: i32 = 100; -const SPRITE_SCALE: f64 = 0.6; -const PLAYER_POOL_SIZE: usize = 3; -const RETRY_ATTEMPTS: usize = 10; - -pub struct WindowHandler; - -impl WindowHandler { - pub unsafe fn create(ctx: &WindowContext) { - println!("[ahfail] on_window_create called"); - - let error_label = ctx.get_error_label(); - let overlay = ctx.get_overlay(); - let monitor = ctx.get_monitor(); - - let geom = monitor.geometry(); - let screen_w = geom.width(); - let screen_h = geom.height(); - - let fixed = gtk::Fixed::new(); - fixed.set_size_request(screen_w, screen_h); - fixed.set_halign(gtk::Align::Fill); - fixed.set_valign(gtk::Align::Fill); - fixed.set_hexpand(true); - fixed.set_vexpand(true); - - overlay.add_overlay(&fixed); - overlay.set_overlay_pass_through(&fixed, true); - - let mut ready_players = Vec::new(); - MODULE_STATE.with(|state| { - if let Some(audio_uri) = &state.borrow().audio_uri { - for _ in 0..PLAYER_POOL_SIZE { - ready_players.push(Self::create_player(audio_uri)); - } - } - }); - - let data = Box::new(WindowData { - sprites: Vec::new(), - active_players: Vec::new(), - ready_players, - fixed: fixed.clone(), - signal_id: None, - }); - - ctx.set_data(data); - let ptr_addr = ctx.get_data_ptr() as usize; - - let signal_id = error_label.connect_notify(Some("label"), move |label, _| { - let text = label.text(); - let text_str = text.as_str(); - if text_str.is_empty() { - return; - } - println!("[ahfail] Error label changed to: '{}'", text_str); - - MODULE_STATE.with(|state| { - let state = state.borrow(); - if let (Some(animation), Some(audio_uri)) = (&state.animation, &state.audio_uri) { - let data = unsafe { &mut *(ptr_addr as *mut WindowData) }; - - let image = gtk::Image::from_animation(animation); - image.show(); - - let sprite_w = animation.width(); - let sprite_h = animation.height(); - - let mut rng = rand::thread_rng(); - - let safe_w = screen_w - SPRITE_MARGIN; - let safe_h = screen_h - SPRITE_MARGIN; - - let max_x = if safe_w > sprite_w { safe_w - sprite_w } else { 0 }; - let max_y = if safe_h > sprite_h { safe_h - sprite_h } else { 0 }; - - let mut x = 0; - let mut y = 0; - let mut found_safe_spot = false; - - for _ in 0..RETRY_ATTEMPTS { - x = rng.gen_range(0..=max_x); - y = rng.gen_range(0..=max_y); - - if let Some(deadzone) = &state.config.deadzone { - let sprite_rect = gdk::Rectangle::new(x, y, sprite_w, sprite_h); - if deadzone.intersect(&sprite_rect).is_none() { - found_safe_spot = true; - break; - } - } else { - found_safe_spot = true; - break; - } - } - - if !found_safe_spot { - println!("[ahfail] Could not find safe spot after retries, placing anyway"); - } - - println!("[ahfail] Placing sprite at ({}, {})", x, y); - data.fixed.put(&image, x, y); - data.sprites.push(image); - - let player = if let Some(p) = data.ready_players.pop() { - p - } else { - Self::create_player(audio_uri) - }; - - player.play(); - data.active_players.push(player); - - let new_player = Self::create_player(audio_uri); - data.ready_players.push(new_player); - } - }); - }); - - unsafe { - (*ctx.get_data_ptr()).signal_id = Some(signal_id); - } - } - - pub unsafe fn destroy(ctx: &WindowContext) { - if let Some(data) = ctx.take_data() { - if let Some(signal_id) = data.signal_id { - let error_label = ctx.get_error_label(); - error_label.disconnect(signal_id); - } - - for sprite in data.sprites { - sprite.destroy(); - } - for player in data.active_players { - player.stop(); - } - for player in data.ready_players { - player.stop(); - } - data.fixed.destroy(); - } - } - - fn create_player(uri: &str) -> gst_player::Player { - let player = gst_player::Player::new(None, None); - player.set_uri(Some(uri)); - player.connect_end_of_stream(glib::clone!(@weak player => move |_| { - player.seek(gst::ClockTime::from_seconds(0)); - })); - player.connect_error(|_, err| { - eprintln!("[ahfail] GStreamer Player Error: {}", err); - }); - player - } -} - diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index d18837d..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,193 +0,0 @@ -pub mod config; -pub mod context; -pub mod state; -pub mod handler; - -use gtk::prelude::*; -use gtk::{glib, gdk_pixbuf, gio}; -use gtk::gdk_pixbuf::InterpType; -use gstreamer as gst; -use std::ffi::{c_void, CStr}; -use glib::translate::from_glib_none; -use std::os::raw::{c_char, c_int, c_uint}; -use std::ptr; - -// Re-export types for external use (e.g. integration tests) -pub use config::{ModuleConfig, DEADZONE_ARG, DEADZONE_LONG, DEADZONE_DESC, DEADZONE_ARG_DESC}; -pub use context::{Window, GtkLock, WindowContext, __IncompleteArrayField}; -pub use state::{MODULE_STATE, WindowData, ModuleState}; -pub use handler::WindowHandler; - -// Scale factor to reduce the image size and avoid cropping/overlap -const SPRITE_SCALE: f64 = 0.6; - -#[no_mangle] -pub static module_name: [c_char; 7] = [ - b'a' as c_char, - b'h' as c_char, - b'f' as c_char, - b'a' as c_char, - b'i' as c_char, - b'l' as c_char, - 0, -]; - -#[no_mangle] -pub static module_major_version: c_uint = 4; - -#[no_mangle] -pub static module_minor_version: c_uint = 0; - -#[no_mangle] -pub static mut module_entries: [glib::ffi::GOptionEntry; 3] = [ - glib::ffi::GOptionEntry { - long_name: DEADZONE_LONG.as_ptr() as *const c_char, - short_name: 0, - flags: 0, - arg: glib::ffi::G_OPTION_ARG_STRING, - arg_data: unsafe { &raw mut DEADZONE_ARG as *mut _ }, - description: DEADZONE_DESC.as_ptr() as *const c_char, - arg_description: DEADZONE_ARG_DESC.as_ptr() as *const c_char - }, - glib::ffi::GOptionEntry { long_name: ptr::null(), short_name: 0, flags: 0, arg: 0, arg_data: ptr::null_mut(), description: ptr::null(), arg_description: ptr::null() }, - glib::ffi::GOptionEntry { long_name: ptr::null(), short_name: 0, flags: 0, arg: 0, arg_data: ptr::null_mut(), description: ptr::null(), arg_description: ptr::null() }, -]; - -extern "C" { - fn ahfail_get_resource() -> *mut gio::ffi::GResource; -} - -// Helper for tests to inspect window data -/// # Safety -/// `ctx` must be a valid Window pointer. -pub unsafe fn get_window_data_ref(ctx: *mut Window) -> Option<&'static state::WindowData> { - let ptr = *(*ctx).module_data.as_ptr(); - if ptr.is_null() { - None - } else { - Some(&*(ptr as *mut state::WindowData)) - } -} - -/// # Safety -/// This function is called by the C host. -#[no_mangle] -pub unsafe extern "C" fn on_activation(_gtklock: *mut GtkLock, _id: c_int) { - println!("[ahfail] on_activation called"); - if let Err(e) = gtk::init() { - eprintln!("Failed to initialize GTK bindings: {}", e); - return; - } - if let Err(e) = gst::init() { - eprintln!("Failed to initialize GStreamer: {}", e); - return; - } - - let resource_ptr = ahfail_get_resource(); - if !resource_ptr.is_null() { - let resource = from_glib_none::<_, gio::Resource>(resource_ptr); - gio::resources_register(&resource); - } - - // Load frames - let mut loaded_frames: Vec = Vec::new(); - match gio::resources_enumerate_children("/ahfail/sprites", gio::ResourceLookupFlags::NONE) { - Ok(mut frames) => { - frames.sort(); - for frame_path in frames { - let full_path = format!("/ahfail/sprites/{}", frame_path); - match gdk_pixbuf::Pixbuf::from_resource(&full_path) { - Ok(pixbuf) => { - let w = (pixbuf.width() as f64 * SPRITE_SCALE) as i32; - let h = (pixbuf.height() as f64 * SPRITE_SCALE) as i32; - if let Some(scaled) = pixbuf.scale_simple(w, h, InterpType::Bilinear) { - loaded_frames.push(scaled); - } else { - loaded_frames.push(pixbuf); - } - }, - Err(e) => eprintln!("Failed to load sprite frame {}: {}", full_path, e), - } - } - }, - Err(e) => eprintln!("Failed to enumerate sprites: {}", e), - } - - let anim_opt = if !loaded_frames.is_empty() { - let first = &loaded_frames[0]; - let anim = gdk_pixbuf::PixbufSimpleAnim::new(first.width(), first.height(), 12.0); - anim.set_loop(true); - for frame in loaded_frames { - anim.add_frame(&frame); - } - Some(anim.upcast()) - } else { - None - }; - - let config = ModuleConfig::from_args(); - - MODULE_STATE.with(|state| { - let mut state = state.borrow_mut(); - state.animation = anim_opt; - state.audio_uri = Some("resource:///ahfail/audio/magic-word.mp3".to_string()); - state.config = config; - }); -} - -/// # Safety -/// This function is called by the C host. -#[no_mangle] -pub unsafe extern "C" fn on_window_create(_gtklock: *mut GtkLock, ctx: *mut Window) { - if let Some(win_ctx) = WindowContext::new(ctx) { - WindowHandler::create(&win_ctx); - } -} - -/// # Safety -/// This function is called by the C host. -#[no_mangle] -pub unsafe extern "C" fn on_window_destroy(_gtklock: *mut GtkLock, ctx: *mut Window) { - if let Some(win_ctx) = WindowContext::new(ctx) { - WindowHandler::destroy(&win_ctx); - } -} - -/// # Safety -/// This function is called by the C host. -#[no_mangle] -pub unsafe extern "C" fn on_idle_hide(gtklock: *mut GtkLock) { - if gtklock.is_null() { return; } - let garray = (*gtklock).windows; - if !garray.is_null() { - let len = (*garray).len; - let data_ptr = (*garray).data as *mut *mut Window; - let windows = std::slice::from_raw_parts(data_ptr, len as usize); - - for &window_ptr in windows { - on_window_destroy(ptr::null_mut(), window_ptr); - } - } -} - -/// # Safety -/// This function is called by the C host. -#[no_mangle] -pub unsafe extern "C" fn on_focus_change(_gtklock: *mut GtkLock, _win: *mut Window, _old: *mut Window) {} - -/// # Safety -/// This function is called by the C host. -#[no_mangle] -pub unsafe extern "C" fn on_idle_show(_gtklock: *mut GtkLock) {} - -/// # Safety -/// This function is called by the C host. -#[no_mangle] -pub unsafe extern "C" fn g_module_unload(_module: *mut c_void) { - MODULE_STATE.with(|state| { - let mut state = state.borrow_mut(); - state.animation = None; - state.audio_uri = None; - state.config.deadzone = None; - }); -} \ No newline at end of file diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index 1367b21..0000000 --- a/src/state.rs +++ /dev/null @@ -1,26 +0,0 @@ -use gtk::{glib, gdk_pixbuf}; -use gstreamer_player as gst_player; -use std::cell::RefCell; -use crate::config::ModuleConfig; - -pub struct ModuleState { - pub animation: Option, - pub audio_uri: Option, - pub config: ModuleConfig, -} - -pub struct WindowData { - pub sprites: Vec, - pub active_players: Vec, - pub ready_players: Vec, - pub fixed: gtk::Fixed, - pub signal_id: Option, -} - -thread_local! { - pub static MODULE_STATE: RefCell = const { RefCell::new(ModuleState { - animation: None, - audio_uri: None, - config: ModuleConfig { deadzone: None }, - }) }; -} diff --git a/src/utils/bench.rs b/src/utils/bench.rs deleted file mode 100644 index 99f7fc8..0000000 --- a/src/utils/bench.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::time::Instant; - -pub fn time_execution(name: &str, f: F) -> T -where - F: FnOnce() -> T, -{ - let start = Instant::now(); - let result = f(); - let duration = start.elapsed(); - println!("[benchmark] {}: {:?}", name, duration); - result -} diff --git a/tests/ahfail_tests.rs b/tests/ahfail_tests.rs deleted file mode 100644 index 26106df..0000000 --- a/tests/ahfail_tests.rs +++ /dev/null @@ -1,299 +0,0 @@ -use ahfail_module::*; -use gtk::prelude::*; -use gtk::{glib, gdk_pixbuf, gdk}; -use gstreamer as gst; -use std::ptr; -use std::ffi::c_void; -use gtk::glib::translate::{ToGlibPtr, Stash}; - -// Mock implementation of the resource getter for tests -#[no_mangle] -extern "C" fn ahfail_get_resource() -> *mut gtk::gio::ffi::GResource { - ptr::null_mut() -} - -// Helper struct to simulate the C flexible array member allocation -#[repr(C)] -struct WindowWithStorage { - window: Window, - // Reserve space for the 'module_data' flexible array (1 pointer) - storage: *mut c_void, -} - -fn setup_test_environment() { - static ONCE: std::sync::Once = std::sync::Once::new(); - ONCE.call_once(|| { - let _ = gtk::init(); - let _ = gst::init(); - }); -} - -fn flush_events() { - while gtk::events_pending() { - gtk::main_iteration(); - } -} - -fn create_mock_context() -> (WindowWithStorage, gtk::Window, gtk::Label, gtk::Overlay, gdk::Display) { - setup_test_environment(); - - let error_label = gtk::Label::new(Some("")); - let overlay = gtk::Overlay::new(); - let window_widget = gtk::Window::new(gtk::WindowType::Toplevel); - let display = gdk::Display::default().expect("No display available for testing"); - let monitor = display.monitor(0).expect("No monitor available"); - - // Explicitly type the Stash to avoid ambiguity - let monitor_stash: Stash<*mut gdk::ffi::GdkMonitor, _> = monitor.to_glib_none(); - let window_stash: Stash<*mut gtk::ffi::GtkWindow, _> = window_widget.to_glib_none(); - let overlay_stash: Stash<*mut gtk::ffi::GtkOverlay, _> = overlay.to_glib_none(); - let label_stash: Stash<*mut gtk::ffi::GtkLabel, _> = error_label.to_glib_none(); - - let win = Window { - monitor: monitor_stash.0, - window: window_stash.0 as *mut gtk::ffi::GtkWidget, - overlay: overlay_stash.0, - window_box: ptr::null_mut(), - body_revealer: ptr::null_mut(), - body_grid: ptr::null_mut(), - input_label: ptr::null_mut(), - input_field: ptr::null_mut(), - message_revealer: ptr::null_mut(), - message_scrolled_window: ptr::null_mut(), - message_box: ptr::null_mut(), - unlock_button: ptr::null_mut(), - error_label: label_stash.0 as *mut gtk::ffi::GtkWidget, - warning_label: ptr::null_mut(), - info_box: ptr::null_mut(), - time_box: ptr::null_mut(), - clock_label: ptr::null_mut(), - date_label: ptr::null_mut(), - module_data: __IncompleteArrayField::new(), - }; - - let storage = WindowWithStorage { - window: win, - storage: ptr::null_mut(), - }; - - (storage, window_widget, error_label, overlay, display) -} - -fn inject_test_state() { - let pixbuf = gdk_pixbuf::Pixbuf::new(gdk_pixbuf::Colorspace::Rgb, false, 8, 1, 1).unwrap(); - let anim = gdk_pixbuf::PixbufSimpleAnim::new(1, 1, 1.0); - anim.add_frame(&pixbuf); - - // Use a minimal valid WAV (silent) to avoid GStreamer errors - let wav_base64 = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQEAAAAA"; - let data_uri = format!("data:audio/wav;base64,{}", wav_base64); - - MODULE_STATE.with(|state| { - let mut state = state.borrow_mut(); - state.animation = Some(anim.upcast()); - state.audio_uri = Some(data_uri); - }); -} - -fn run_test_01_initialization() { - unsafe { - on_activation(ptr::null_mut(), 0); - } - MODULE_STATE.with(|state| { - let state = state.borrow(); - // It should have set the audio URI to the default - assert_eq!(state.audio_uri.as_deref(), Some("resource:///ahfail/audio/magic-word.mp3")); - }); -} - -fn run_test_02_window_create_null() { - unsafe { - on_window_create(ptr::null_mut(), ptr::null_mut()); - } -} - -fn run_test_03_window_create_valid() { - let (mut win_storage, _w, _l, _o, _) = create_mock_context(); - let ctx_ptr = &mut win_storage.window as *mut Window; - - unsafe { - on_window_create(ptr::null_mut(), ctx_ptr); - let data = get_window_data_ref(ctx_ptr); - assert!(data.is_some(), "WindowData should be initialized"); - - on_window_destroy(ptr::null_mut(), ctx_ptr); - flush_events(); - } -} - -fn run_test_04_trigger_effect_sprite() { - let (mut win_storage, _w, label, _o, _) = create_mock_context(); - let ctx_ptr = &mut win_storage.window as *mut Window; - inject_test_state(); - - unsafe { - on_window_create(ptr::null_mut(), ctx_ptr); - label.set_text("Error"); - flush_events(); - - let data = get_window_data_ref(ctx_ptr).unwrap(); - assert_eq!(data.sprites.len(), 1); - - on_window_destroy(ptr::null_mut(), ctx_ptr); - flush_events(); - } -} - -fn run_test_05_trigger_effect_audio() { - let (mut win_storage, _w, label, _o, _) = create_mock_context(); - let ctx_ptr = &mut win_storage.window as *mut Window; - inject_test_state(); - - unsafe { - on_window_create(ptr::null_mut(), ctx_ptr); - label.set_text("Error"); - flush_events(); - - let data = get_window_data_ref(ctx_ptr).unwrap(); - assert_eq!(data.active_players.len(), 1); - - on_window_destroy(ptr::null_mut(), ctx_ptr); - flush_events(); - } -} - -fn run_test_06_multiple_triggers() { - let (mut win_storage, _w, label, _o, _) = create_mock_context(); - let ctx_ptr = &mut win_storage.window as *mut Window; - inject_test_state(); - - unsafe { - on_window_create(ptr::null_mut(), ctx_ptr); - for _ in 0..5 { - label.set_text("Error"); - flush_events(); - } - - let data = get_window_data_ref(ctx_ptr).unwrap(); - assert_eq!(data.sprites.len(), 5); - assert_eq!(data.active_players.len(), 5); - - on_window_destroy(ptr::null_mut(), ctx_ptr); - flush_events(); - } -} - -fn run_test_07_window_cleanup() { - let (mut win_storage, _w, label, _o, _) = create_mock_context(); - let ctx_ptr = &mut win_storage.window as *mut Window; - inject_test_state(); - - unsafe { - on_window_create(ptr::null_mut(), ctx_ptr); - label.set_text("Error"); - flush_events(); - - assert!(get_window_data_ref(ctx_ptr).is_some()); - on_window_destroy(ptr::null_mut(), ctx_ptr); - assert!(get_window_data_ref(ctx_ptr).is_none()); - flush_events(); - } -} - -fn run_test_08_multiple_windows() { - let (mut win1, _w1, label1, _o1, _) = create_mock_context(); - let (mut win2, _w2, label2, _o2, _) = create_mock_context(); - let ctx1 = &mut win1.window as *mut Window; - let ctx2 = &mut win2.window as *mut Window; - inject_test_state(); - - unsafe { - on_window_create(ptr::null_mut(), ctx1); - on_window_create(ptr::null_mut(), ctx2); - - label1.set_text("E1"); - flush_events(); - - label2.set_text("E2"); - label2.set_text("E2 again"); - flush_events(); - - let data1 = get_window_data_ref(ctx1).unwrap(); - let data2 = get_window_data_ref(ctx2).unwrap(); - - assert_eq!(data1.sprites.len(), 1); - assert_eq!(data2.sprites.len(), 2); - - on_window_destroy(ptr::null_mut(), ctx1); - on_window_destroy(ptr::null_mut(), ctx2); - flush_events(); - } -} - -fn run_test_09_idle_hide_cleanup() { - let (mut win_storage, _w, label, _o, _) = create_mock_context(); - let ctx_ptr = &mut win_storage.window as *mut Window; - inject_test_state(); - - unsafe { - on_window_create(ptr::null_mut(), ctx_ptr); - label.set_text("Error"); - flush_events(); - - // Mock GtkLock struct - let mut windows_array = glib::ffi::g_array_new(0, 0, std::mem::size_of::<*mut Window>() as u32); - glib::ffi::g_array_append_vals(windows_array, &ctx_ptr as *const _ as *const c_void, 1); - - let mut lock = GtkLock { - windows: windows_array, - }; - - on_idle_hide(&mut lock); - - assert!(get_window_data_ref(ctx_ptr).is_none()); - glib::ffi::g_array_free(windows_array, 1); - flush_events(); - } -} - -fn run_test_10_module_unload() { - inject_test_state(); - MODULE_STATE.with(|state| { - assert!(state.borrow().audio_uri.is_some()); - }); - unsafe { - g_module_unload(ptr::null_mut()); - } - MODULE_STATE.with(|state| { - let state = state.borrow(); - assert!(state.animation.is_none()); - assert!(state.audio_uri.is_none()); - }); -} - -#[test] -fn run_all_tests_sequentially() { - println!("Running test 01..."); - run_test_01_initialization(); - println!("Running test 02..."); - run_test_02_window_create_null(); - println!("Running test 03..."); - run_test_03_window_create_valid(); - println!("Running test 04..."); - run_test_04_trigger_effect_sprite(); - println!("Running test 05..."); - run_test_05_trigger_effect_audio(); - println!("Running test 06..."); - run_test_06_multiple_triggers(); - println!("Running test 07..."); - run_test_07_window_cleanup(); - println!("Running test 08..."); - run_test_08_multiple_windows(); - println!("Running test 09..."); - run_test_09_idle_hide_cleanup(); - println!("Running test 10..."); - run_test_10_module_unload(); - - // Final flush - flush_events(); -} \ No newline at end of file diff --git a/tests/benchmarks.rs b/tests/benchmarks.rs deleted file mode 100644 index fa86cd1..0000000 --- a/tests/benchmarks.rs +++ /dev/null @@ -1,57 +0,0 @@ -use gtk::prelude::*; -use gtk::{gdk, gdk_pixbuf}; -use gstreamer as gst; -use gstreamer_player as gst_player; -use std::time::Instant; - -#[cfg(test)] -mod benchmarks { - use super::*; - use std::sync::Once; - - static INIT: Once = Once::new(); - - fn init() { - INIT.call_once(|| { - gtk::init().unwrap(); - gst::init().unwrap(); - }); - } - - #[test] - fn bench_pixbuf_loading() { - init(); - let start = Instant::now(); - // Create a 1x1 pixbuf to simulate loading - let _pixbuf = gdk_pixbuf::Pixbuf::new(gdk_pixbuf::Colorspace::Rgb, false, 8, 220, 220).unwrap(); - println!("Pixbuf creation: {:?}", start.elapsed()); - } - - #[test] - fn bench_animation_creation() { - init(); - let start = Instant::now(); - let _anim = gdk_pixbuf::PixbufSimpleAnim::new(220, 220, 12.0); - println!("Animation creation: {:?}", start.elapsed()); - } - - #[test] - fn bench_player_creation() { - init(); - let start = Instant::now(); - let _player = gst_player::Player::new(None, None); - println!("GstPlayer creation: {:?}", start.elapsed()); - } - - #[test] - fn bench_coord_calculation() { - use rand::Rng; - let start = Instant::now(); - let mut rng = rand::thread_rng(); - for _ in 0..1000 { - let _x = rng.gen_range(0..1920); - let _y = rng.gen_range(0..1080); - } - println!("1000 RNG calculations: {:?}", start.elapsed()); - } -}