diff --git a/Cargo.lock b/Cargo.lock index be5b08a..1ee236e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,7 +3,11 @@ version = 4 [[package]] -name = "ahfail" +name = "ahfail-display" +version = "0.1.0" + +[[package]] +name = "ahfail-gtklock" version = "0.1.0" dependencies = [ "gdk", @@ -14,11 +18,18 @@ dependencies = [ "gstreamer-player", "gtk", "libc", - "once_cell", "pkg-config", "rand", ] +[[package]] +name = "ahfail-pam" +version = "0.1.0" + +[[package]] +name = "ahfail-ui" +version = "0.1.0" + [[package]] name = "anyhow" version = "1.0.100" diff --git a/Cargo.toml b/Cargo.toml index 6709b7d..41c085e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,8 @@ -[package] -name = "ahfail" -version = "0.1.0" -edition = "2021" - -[lib] -name = "ahfail_module" -crate-type = ["staticlib", "rlib"] - -[dependencies] -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" } -gio = { version = "0.15", package = "gio" } -gdk-pixbuf = "0.15" -libc = "0.2" -once_cell = "1.10" -rand = "0.8" - -[build-dependencies] -pkg-config = "0.3" +[workspace] +members = [ + "crates/ahfail-ui", + "crates/ahfail-gtklock", + "crates/ahfail-pam", + "crates/ahfail-display", +] +resolver = "2" diff --git a/crates/ahfail-display/Cargo.toml b/crates/ahfail-display/Cargo.toml new file mode 100644 index 0000000..3dc7ba4 --- /dev/null +++ b/crates/ahfail-display/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "ahfail-display" +version = "0.1.0" +edition = "2021" diff --git a/crates/ahfail-display/src/main.rs b/crates/ahfail-display/src/main.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/crates/ahfail-display/src/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/crates/ahfail-gtklock/Cargo.toml b/crates/ahfail-gtklock/Cargo.toml new file mode 100644 index 0000000..5581e4b --- /dev/null +++ b/crates/ahfail-gtklock/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ahfail-gtklock" +version = "0.1.0" +edition = "2021" + +[lib] +name = "ahfail_module" +crate-type = ["staticlib", "rlib"] + +[dependencies] +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" } +gio = { version = "0.15", package = "gio" } +gdk-pixbuf = "0.15" +libc = "0.2" +rand = "0.8" + +[build-dependencies] +pkg-config = "0.3" diff --git a/crates/ahfail-gtklock/src/config.rs b/crates/ahfail-gtklock/src/config.rs new file mode 100644 index 0000000..797fdff --- /dev/null +++ b/crates/ahfail-gtklock/src/config.rs @@ -0,0 +1,37 @@ +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/crates/ahfail-gtklock/src/context.rs b/crates/ahfail-gtklock/src/context.rs new file mode 100644 index 0000000..45a7bf0 --- /dev/null +++ b/crates/ahfail-gtklock/src/context.rs @@ -0,0 +1,110 @@ +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/crates/ahfail-gtklock/src/handler.rs b/crates/ahfail-gtklock/src/handler.rs new file mode 100644 index 0000000..fc1717d --- /dev/null +++ b/crates/ahfail-gtklock/src/handler.rs @@ -0,0 +1,165 @@ +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/crates/ahfail-gtklock/src/lib.rs b/crates/ahfail-gtklock/src/lib.rs new file mode 100644 index 0000000..d18837d --- /dev/null +++ b/crates/ahfail-gtklock/src/lib.rs @@ -0,0 +1,193 @@ +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/crates/ahfail-gtklock/src/state.rs b/crates/ahfail-gtklock/src/state.rs new file mode 100644 index 0000000..1367b21 --- /dev/null +++ b/crates/ahfail-gtklock/src/state.rs @@ -0,0 +1,26 @@ +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/crates/ahfail-gtklock/src/utils/bench.rs b/crates/ahfail-gtklock/src/utils/bench.rs new file mode 100644 index 0000000..99f7fc8 --- /dev/null +++ b/crates/ahfail-gtklock/src/utils/bench.rs @@ -0,0 +1,12 @@ +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/crates/ahfail-gtklock/tests/ahfail_tests.rs b/crates/ahfail-gtklock/tests/ahfail_tests.rs new file mode 100644 index 0000000..26106df --- /dev/null +++ b/crates/ahfail-gtklock/tests/ahfail_tests.rs @@ -0,0 +1,299 @@ +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/crates/ahfail-gtklock/tests/benchmarks.rs b/crates/ahfail-gtklock/tests/benchmarks.rs new file mode 100644 index 0000000..fa86cd1 --- /dev/null +++ b/crates/ahfail-gtklock/tests/benchmarks.rs @@ -0,0 +1,57 @@ +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()); + } +} diff --git a/crates/ahfail-pam/Cargo.toml b/crates/ahfail-pam/Cargo.toml new file mode 100644 index 0000000..fedf775 --- /dev/null +++ b/crates/ahfail-pam/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "ahfail-pam" +version = "0.1.0" +edition = "2021" diff --git a/crates/ahfail-pam/src/lib.rs b/crates/ahfail-pam/src/lib.rs new file mode 100644 index 0000000..2200a7e --- /dev/null +++ b/crates/ahfail-pam/src/lib.rs @@ -0,0 +1 @@ +pub fn placeholder() {} diff --git a/crates/ahfail-ui/Cargo.toml b/crates/ahfail-ui/Cargo.toml new file mode 100644 index 0000000..669fbe8 --- /dev/null +++ b/crates/ahfail-ui/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "ahfail-ui" +version = "0.1.0" +edition = "2021" diff --git a/crates/ahfail-ui/src/lib.rs b/crates/ahfail-ui/src/lib.rs new file mode 100644 index 0000000..2200a7e --- /dev/null +++ b/crates/ahfail-ui/src/lib.rs @@ -0,0 +1 @@ +pub fn placeholder() {} diff --git a/meson.build b/meson.build index 75b2429..52f8123 100644 --- a/meson.build +++ b/meson.build @@ -25,7 +25,7 @@ cargo_target = custom_target( input: ['src/lib.rs', 'Cargo.toml'], output: ['libahfail_module.a'], command: [ - 'sh', '-c', 'cargo build --release --target-dir "@OUTDIR@/target" && cp "@OUTDIR@/target/release/libahfail_module.a" "@OUTPUT@"' + 'sh', '-c', 'cargo build --release -p ahfail-gtklock --target-dir "@OUTDIR@/target" && cp "@OUTDIR@/target/release/libahfail_module.a" "@OUTPUT@"' ], build_by_default: true )