diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ac966dd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,58 @@
+# Generated by Cargo
+# will have compiled files and executables
+debug
+target
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+
+# Generated by cargo mutants
+# Contains mutation testing data
+**/mutants.out*/
+
+# RustRover
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+.mypy_cache/
+.pytest_cache/
+/.project
+/.pydevproject
+/.settings
+/.cproject
+/.idea
+/.vscode
+
+__pycache__
+/.coverage/
+/.coveragerc
+/install dir
+/work area
+
+/meson-test-run.txt
+/meson-test-run.xml
+/meson-cross-test-run.txt
+/meson-cross-test-run.xml
+
+.DS_Store
+*~
+*.swp
+packagecache
+.wraplock
+/MANIFEST
+/build
+/dist
+/meson.egg-info
+
+/docs/built_docs
+/docs/hotdoc-private*
+
+*.pyc
+/*venv*
+
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..be5b08a
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1002 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ahfail"
+version = "0.1.0"
+dependencies = [
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gstreamer",
+ "gstreamer-player",
+ "gtk",
+ "libc",
+ "once_cell",
+ "pkg-config",
+ "rand",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "atk"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd"
+dependencies = [
+ "atk-sys",
+ "bitflags",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cairo-rs"
+version = "0.15.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc"
+dependencies = [
+ "bitflags",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "thiserror",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gdk"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a"
+dependencies = [
+ "bitflags",
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gio"
+version = "0.15.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.15.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.15.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a"
+dependencies = [
+ "anyhow",
+ "heck 0.4.1",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer"
+version = "0.18.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66363bacf5e4f6eb281564adc2902e44c52ae5c45082423e7439e9012b75456"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "glib",
+ "gstreamer-sys",
+ "libc",
+ "muldiv",
+ "num-integer",
+ "num-rational",
+ "once_cell",
+ "option-operations",
+ "paste",
+ "pretty-hex",
+ "thiserror",
+]
+
+[[package]]
+name = "gstreamer-base"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224f35f36582407caf58ded74854526beeecc23d0cf64b8d1c3e00584ed6863f"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "glib",
+ "gstreamer",
+ "gstreamer-base-sys",
+ "libc",
+]
+
+[[package]]
+name = "gstreamer-base-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a083493c3c340e71fa7c66eebda016e9fafc03eb1b4804cf9b2bad61994b078e"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-player"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f14ee02352ba73cadebe640bfb33f12fe8d03cbcad816a102d55a0251fb99bb"
+dependencies = [
+ "bitflags",
+ "glib",
+ "gstreamer",
+ "gstreamer-player-sys",
+ "gstreamer-video",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gstreamer-player-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f9b674b39a4d0e18710f6e3d2b109f1793d8028ee4e39da3909b55b4529d399"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-sys",
+ "gstreamer-video-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3517a65d3c2e6f8905b456eba5d53bda158d664863aef960b44f651cb7d33e2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-video"
+version = "0.18.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9418adfc72dafa1ad9eb106527ce4804887d101027c4528ec28c7d29cc899519"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "futures-channel",
+ "glib",
+ "gstreamer",
+ "gstreamer-base",
+ "gstreamer-video-sys",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gstreamer-video-sys"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33331b1675e73b5b000c796354278eca7fdde9327015971d9f41afe28b96e0dc"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-base-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0"
+dependencies = [
+ "atk",
+ "bitflags",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "once_cell",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.15.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d"
+dependencies = [
+ "anyhow",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "indexmap"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.177"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "muldiv"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "option-operations"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42b01597916c91a493b1e8a2fde64fec1764be3259abc1f06efc99c274f150a2"
+dependencies = [
+ "paste",
+]
+
+[[package]]
+name = "pango"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f"
+dependencies = [
+ "bitflags",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "pretty-hex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.22.27",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..6709b7d
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,23 @@
+[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"
diff --git a/assets/ahfail.gresource.xml b/assets/ahfail.gresource.xml
new file mode 100644
index 0000000..f28de01
--- /dev/null
+++ b/assets/ahfail.gresource.xml
@@ -0,0 +1,31 @@
+
+
+
+ magic-word.mp3
+ frame__0000.png
+ frame__0001.png
+ frame__0002.png
+ frame__0003.png
+ frame__0004.png
+ frame__0005.png
+ frame__0006.png
+ frame__0007.png
+ frame__0008.png
+ frame__0009.png
+ frame__0010.png
+ frame__0011.png
+ frame__0012.png
+ frame__0013.png
+ frame__0014.png
+ frame__0015.png
+ frame__0016.png
+ frame__0017.png
+ frame__0018.png
+ frame__0019.png
+ frame__0020.png
+ frame__0021.png
+ frame__0022.png
+ frame__0023.png
+ frame__0024.png
+
+
diff --git a/assets/frame__0000.png b/assets/frame__0000.png
new file mode 100644
index 0000000..84efd92
Binary files /dev/null and b/assets/frame__0000.png differ
diff --git a/assets/frame__0001.png b/assets/frame__0001.png
new file mode 100644
index 0000000..0ff72b4
Binary files /dev/null and b/assets/frame__0001.png differ
diff --git a/assets/frame__0002.png b/assets/frame__0002.png
new file mode 100644
index 0000000..224e8a2
Binary files /dev/null and b/assets/frame__0002.png differ
diff --git a/assets/frame__0003.png b/assets/frame__0003.png
new file mode 100644
index 0000000..4085f53
Binary files /dev/null and b/assets/frame__0003.png differ
diff --git a/assets/frame__0004.png b/assets/frame__0004.png
new file mode 100644
index 0000000..d32d359
Binary files /dev/null and b/assets/frame__0004.png differ
diff --git a/assets/frame__0005.png b/assets/frame__0005.png
new file mode 100644
index 0000000..78b96f9
Binary files /dev/null and b/assets/frame__0005.png differ
diff --git a/assets/frame__0006.png b/assets/frame__0006.png
new file mode 100644
index 0000000..d5257a8
Binary files /dev/null and b/assets/frame__0006.png differ
diff --git a/assets/frame__0007.png b/assets/frame__0007.png
new file mode 100644
index 0000000..9664f22
Binary files /dev/null and b/assets/frame__0007.png differ
diff --git a/assets/frame__0008.png b/assets/frame__0008.png
new file mode 100644
index 0000000..767e4e2
Binary files /dev/null and b/assets/frame__0008.png differ
diff --git a/assets/frame__0009.png b/assets/frame__0009.png
new file mode 100644
index 0000000..b4aa2b9
Binary files /dev/null and b/assets/frame__0009.png differ
diff --git a/assets/frame__0010.png b/assets/frame__0010.png
new file mode 100644
index 0000000..1370a8c
Binary files /dev/null and b/assets/frame__0010.png differ
diff --git a/assets/frame__0011.png b/assets/frame__0011.png
new file mode 100644
index 0000000..de91b41
Binary files /dev/null and b/assets/frame__0011.png differ
diff --git a/assets/frame__0012.png b/assets/frame__0012.png
new file mode 100644
index 0000000..e4a18e8
Binary files /dev/null and b/assets/frame__0012.png differ
diff --git a/assets/frame__0013.png b/assets/frame__0013.png
new file mode 100644
index 0000000..2150edd
Binary files /dev/null and b/assets/frame__0013.png differ
diff --git a/assets/frame__0014.png b/assets/frame__0014.png
new file mode 100644
index 0000000..892cafc
Binary files /dev/null and b/assets/frame__0014.png differ
diff --git a/assets/frame__0015.png b/assets/frame__0015.png
new file mode 100644
index 0000000..d685c9e
Binary files /dev/null and b/assets/frame__0015.png differ
diff --git a/assets/frame__0016.png b/assets/frame__0016.png
new file mode 100644
index 0000000..4b3856f
Binary files /dev/null and b/assets/frame__0016.png differ
diff --git a/assets/frame__0017.png b/assets/frame__0017.png
new file mode 100644
index 0000000..caafb5d
Binary files /dev/null and b/assets/frame__0017.png differ
diff --git a/assets/frame__0018.png b/assets/frame__0018.png
new file mode 100644
index 0000000..66f7a23
Binary files /dev/null and b/assets/frame__0018.png differ
diff --git a/assets/frame__0019.png b/assets/frame__0019.png
new file mode 100644
index 0000000..78359fe
Binary files /dev/null and b/assets/frame__0019.png differ
diff --git a/assets/frame__0020.png b/assets/frame__0020.png
new file mode 100644
index 0000000..46e4c11
Binary files /dev/null and b/assets/frame__0020.png differ
diff --git a/assets/frame__0021.png b/assets/frame__0021.png
new file mode 100644
index 0000000..3ae18b2
Binary files /dev/null and b/assets/frame__0021.png differ
diff --git a/assets/frame__0022.png b/assets/frame__0022.png
new file mode 100644
index 0000000..4e4d88a
Binary files /dev/null and b/assets/frame__0022.png differ
diff --git a/assets/frame__0023.png b/assets/frame__0023.png
new file mode 100644
index 0000000..307e40a
Binary files /dev/null and b/assets/frame__0023.png differ
diff --git a/assets/frame__0024.png b/assets/frame__0024.png
new file mode 100644
index 0000000..307e40a
Binary files /dev/null and b/assets/frame__0024.png differ
diff --git a/assets/magic-word.mp3 b/assets/magic-word.mp3
new file mode 100644
index 0000000..2e3750e
Binary files /dev/null and b/assets/magic-word.mp3 differ
diff --git a/include/ahfail/module.h b/include/ahfail/module.h
new file mode 100644
index 0000000..7bf2fa0
--- /dev/null
+++ b/include/ahfail/module.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "gtklock-module.h"
+
+extern const char module_name[];
+extern const guint module_major_version;
+extern const guint module_minor_version;
+extern GOptionEntry module_entries[];
diff --git a/include/gtklock-module.h b/include/gtklock-module.h
new file mode 100644
index 0000000..14e2fb3
--- /dev/null
+++ b/include/gtklock-module.h
@@ -0,0 +1,100 @@
+// gtklock-userinfo-module
+// Copyright (c) 2024 Jovan Lanik
+
+// Module header
+
+#include "gtk/gtk.h"
+
+struct Window {
+ GdkMonitor *monitor;
+
+ GtkWidget *window;
+ GtkWidget *overlay;
+ GtkWidget *window_box;
+ GtkWidget *body_revealer;
+ GtkWidget *body_grid;
+ GtkWidget *input_label;
+ GtkWidget *input_field;
+ GtkWidget *message_revealer;
+ GtkWidget *message_scrolled_window;
+ GtkWidget *message_box;
+ GtkWidget *unlock_button;
+ GtkWidget *error_label;
+ GtkWidget *warning_label;
+ GtkWidget *info_box;
+ GtkWidget *time_box;
+ GtkWidget *clock_label;
+ GtkWidget *date_label;
+
+ void *module_data[];
+};
+
+struct GtkLock {
+ GtkApplication *app;
+ void *lock;
+ pid_t parent;
+
+ GArray *windows;
+ GArray *messages;
+ GArray *errors;
+
+ struct Window *focused_window;
+ gboolean hidden;
+ guint idle_timeout;
+
+ guint draw_time_source;
+ guint idle_hide_source;
+
+ gboolean follow_focus;
+ gboolean use_idle_hide;
+
+ char *time;
+ char *date;
+ char *time_format;
+ char *date_format;
+ char *config_path;
+ char *layout_path;
+ char *lock_command;
+ char *unlock_command;
+
+ GArray *modules;
+};
+
+const gchar *g_module_check_init(GModule *m);
+void g_module_unload(GModule *m);
+void on_activation(struct GtkLock *gtklock, int id);
+void on_locked(struct GtkLock *gtklock);
+void on_output_change(struct GtkLock *gtklock);
+void on_focus_change(struct GtkLock *gtklock, struct Window *win, struct Window *old);
+void on_idle_hide(struct GtkLock *gtklock);
+void on_idle_show(struct GtkLock *gtklock);
+void on_window_create(struct GtkLock *gtklock, struct Window *win);
+void on_window_destroy(struct GtkLock *gtklock, struct Window *win);
+
+/*
+
+MIT Licence
+
+Copyright (c) 2024 Jovan Lanik
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+MIT Licence
+
+*/
+
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..75b2429
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,53 @@
+project(
+ 'ahfail',
+ ['c', 'rust'],
+ version: '0.1.0',
+ meson_version: '>=1.3.0',
+ default_options: ['warning_level=3', 'c_std=c11']
+)
+
+ahfail_inc = include_directories('include')
+cc = meson.get_compiler('c')
+m_dep = cc.find_library('m', required : false)
+gtk_dep = dependency('gtk+-3.0')
+gstplayer_dep = dependency('gstreamer-player-1.0')
+gnome = import('gnome')
+
+resources = gnome.compile_resources(
+ 'ahfail-resources',
+ 'assets/ahfail.gresource.xml',
+ source_dir: 'assets',
+ c_name: 'ahfail'
+)
+
+cargo_target = custom_target(
+ 'ahfail-cargo-build',
+ 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@"'
+ ],
+ build_by_default: true
+)
+
+libahfail = shared_library(
+ 'ahfail-module',
+ resources,
+ link_args: ['-Wl,--whole-archive', meson.current_build_dir() / 'libahfail_module.a', '-Wl,--no-whole-archive'],
+ link_depends: cargo_target,
+ include_directories: ahfail_inc,
+ dependencies: [gtk_dep, gstplayer_dep, m_dep],
+ install: true,
+ install_dir: get_option('libdir') / 'gtklock',
+ name_prefix: ''
+)
+
+smoke = executable(
+ 'module_smoke_test',
+ 'tests/module_test.c',
+ include_directories: ahfail_inc,
+ link_with: libahfail,
+ dependencies: [gtk_dep, gstplayer_dep]
+)
+
+test('module symbols', smoke)
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..797fdff
--- /dev/null
+++ b/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/src/context.rs b/src/context.rs
new file mode 100644
index 0000000..45a7bf0
--- /dev/null
+++ b/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/src/handler.rs b/src/handler.rs
new file mode 100644
index 0000000..fc1717d
--- /dev/null
+++ b/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/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..d18837d
--- /dev/null
+++ b/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/src/state.rs b/src/state.rs
new file mode 100644
index 0000000..1367b21
--- /dev/null
+++ b/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/src/utils/bench.rs b/src/utils/bench.rs
new file mode 100644
index 0000000..99f7fc8
--- /dev/null
+++ b/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/tests/ahfail_tests.rs b/tests/ahfail_tests.rs
new file mode 100644
index 0000000..26106df
--- /dev/null
+++ b/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/tests/benchmarks.rs b/tests/benchmarks.rs
new file mode 100644
index 0000000..fa86cd1
--- /dev/null
+++ b/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/tests/module_test.c b/tests/module_test.c
new file mode 100644
index 0000000..2de46ed
--- /dev/null
+++ b/tests/module_test.c
@@ -0,0 +1,18 @@
+#include
+
+#include "ahfail/module.h"
+
+int main(void) {
+ if (module_name[0] == '\0') {
+ return 1;
+ }
+
+ on_activation(NULL, 42);
+ on_window_create(NULL, NULL);
+ on_focus_change(NULL, NULL, NULL);
+ on_idle_show(NULL);
+ on_idle_hide(NULL);
+ on_window_destroy(NULL, NULL);
+
+ return 0;
+}