From f951cb9c6da2641852c5bdfa3917bc534230e935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Geel=20Weirs=C3=B8e?= Date: Wed, 6 May 2026 15:16:26 +0200 Subject: [PATCH] docs: rewrite readme; add macOS install script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clarify that the sprite is the author's face on Nedry's body - Explicit Wayland (gtklock module) vs X11/macOS (PAM module) comparison table with an explanation of how each integration works - Accurate per-distro PAM paths (standard/Fedora multilib/Debian multiarch) - scripts/install-macos.sh: one-shot installer for macOS — checks Homebrew, installs brew deps, builds from source, copies binaries, and patches /etc/pam.d/screensaverui (or screensaver) after the last auth line Co-Authored-By: Claude Sonnet 4.6 --- readme.md | 182 +++++++++++++++++++++++++-------------- scripts/install-macos.sh | 98 +++++++++++++++++++++ 2 files changed, 213 insertions(+), 67 deletions(-) create mode 100755 scripts/install-macos.sh diff --git a/readme.md b/readme.md index d0ffcad..2576f6c 100644 --- a/readme.md +++ b/readme.md @@ -1,38 +1,43 @@ # Ah ah ah, you didn't say the magic word -This `gtklock` module listens for failed unlock attempts and recreates Dennis Nedry’s “ah ah ah” lockout scene from Jurassic Park. +On a failed lock-screen unlock attempt, this spawns a looping animation of **the author's face photoshopped onto Dennis Nedry's body** and plays the "ah ah ah, you didn't say the magic word" clip from Jurassic Park. Each wrong guess adds another sprite at a random screen position. Volume is forced to 100% on the first failure and restored when the screen unlocks. -* **Animation:** Spawns a looping "Nedry" sprite at a random location on the screen. -* **Audio:** Plays the "ah ah ah, you didn't say the magic word" clip. -* **Safety:** Sprites avoid overlapping a configurable "deadzone" (e.g., your login box). -* **Performance:** Uses pre-warmed audio players for low latency +## Platform support -## Requirements +The integration method differs by display server: -* **Build:** Meson, Ninja, Rust (Cargo), GTK+3 development headers. -* **Runtime:** `gtklock`, `gstreamer`, `gst-plugins-base`, `gst-plugins-good` (for audio playback). +| Platform | Display server | Module loaded | How it works | +|---|---|---|---| +| Linux | **Wayland** | `ahfail-module.so` | Loaded directly by `gtklock` via its module API | +| Linux | **X11** | `libahfail_pam.so` + `ahfail-display` | PAM cleanup hook spawns the display binary after each failed auth | +| macOS | — | `libahfail_pam.so` + `ahfail-display` | Same PAM approach, hooks into the screensaver PAM stack | -## Build & Install +The **gtklock module** is Wayland-only — it uses gtklock's internal window API to overlay sprites directly on the lock screen. The **PAM module** works on X11 and macOS by registering a cleanup callback that fires on each failed authentication attempt; it double-forks a standalone display binary (`ahfail-display`) that runs independently of the PAM stack. -1. **Install Dependencies:** - ```bash - sudo pacman -S meson ninja rust gtk3 gstreamer gst-plugins-base gst-plugins-good - ``` +--- -2. **Build:** - ```bash - meson setup builddir - meson compile -C builddir - ``` +## Linux — Wayland (gtklock) -3. **Install:** - ```bash - sudo meson install -C builddir - ``` +### Install dependencies (Arch) -## Usage +```bash +sudo pacman -S meson ninja rust gtk3 gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gtklock +``` -Run `gtklock` with the module path: +### Build and install + +```bash +meson setup builddir +meson compile -C builddir +sudo meson install -C builddir +``` + +Installs: +- `/usr/lib/gtklock/ahfail-module.so` — gtklock module +- `/usr/lib/ahfail/libahfail_pam.so` — PAM module (for X11) +- `/usr/lib/ahfail/ahfail-display` — standalone display binary + +### Run ```bash gtklock -m /usr/lib/gtklock/ahfail-module.so @@ -40,38 +45,76 @@ gtklock -m /usr/lib/gtklock/ahfail-module.so ### Arguments -* `--deadzone=X,Y,W,H`: Defines a rectangle where sprites will *not* spawn (e.g., to keep your password field visible). - ```bash - gtklock -m ahfail-module.so -- --deadzone=860,440,200,200 - ``` - *(Note the `--` separator before module arguments)* - -* `--audio-uri=URI`: Override the default audio clip. - ```bash - gtklock -m ahfail-module.so -- --audio-uri=file:///home/user/custom.mp3 - ``` - -## macOS (build from source) - -### Prerequisites +Pass module arguments after `--`: ```bash -brew install gtk+3 gstreamer gst-plugins-base gst-plugins-good meson ninja rust +gtklock -m /usr/lib/gtklock/ahfail-module.so -- --deadzone=860,440,200,200 ``` -### Build +- `--deadzone=X,Y,W,H` — rectangle where sprites will not spawn (keep your password field clear) +- `--audio-uri=URI` — override the default audio clip, e.g. `file:///home/user/custom.mp3` + +--- + +## Linux — X11 (i3lock, xscreensaver, etc.) + +Add the PAM module to your screen locker's PAM service file. Use the full path — `$(libdir)/ahfail` is not in PAM's default search path. + +**Arch / standard:** +``` +auth optional /usr/lib/ahfail/libahfail_pam.so +``` + +**Fedora / RHEL (multilib):** +``` +auth optional /usr/lib64/ahfail/libahfail_pam.so +``` + +**Debian / Ubuntu (multiarch):** +``` +auth optional /usr/lib/x86_64-linux-gnu/ahfail/libahfail_pam.so +``` + +Common service files: `/etc/pam.d/i3lock`, `/etc/pam.d/xscreensaver`, `/etc/pam.d/lightdm`. + +Optionally pass the display binary path as an argument if it is not at the default location: + +``` +auth optional /usr/lib/ahfail/libahfail_pam.so display_path=/usr/lib/ahfail/ahfail-display +``` + +--- + +## macOS + +### Quick install (recommended) + +```bash +git clone https://gitea.weircon.dk/agw/gtk-ahfail.git +cd gtk-ahfail +bash scripts/install-macos.sh +``` + +The script installs Homebrew dependencies, builds from source, copies binaries to `/usr/local/lib/ahfail/`, and patches the screensaver PAM configuration automatically. + +### Manual install + +#### Prerequisites + +```bash +brew install gtk+3 gstreamer gst-plugins-base gst-plugins-good meson ninja +``` + +Rust: install via [rustup.rs](https://rustup.rs) if not already present. + +#### Build ```bash meson setup builddir meson compile -C builddir ``` -Produces: -- `builddir/ahfail-module.so` — gtklock module (Wayland/Linux only) -- `builddir/libahfail_pam.so` — PAM module (macOS + X11 Linux) -- `builddir/ahfail-display` — display binary (spawned by PAM module) - -### Install +#### Install ```bash sudo mkdir -p /usr/local/lib/ahfail @@ -79,33 +122,38 @@ sudo cp builddir/libahfail_pam.so /usr/local/lib/ahfail/ sudo cp builddir/ahfail-display /usr/local/lib/ahfail/ ``` -### Configure PAM (macOS) +#### Configure PAM -Find your screen locker's PAM service file. On macOS 13+ the screensaver uses `/etc/pam.d/screensaverui`; on older versions it may be `/etc/pam.d/screensaver`. Add the following line after the existing `auth` entries (requires `sudo`): +Find the screensaver PAM service file: +- macOS 13 (Ventura) and later: `/etc/pam.d/screensaverui` +- macOS 12 and earlier: `/etc/pam.d/screensaver` + +Add this line after the existing `auth` entries (requires `sudo`): ``` auth optional /usr/local/lib/ahfail/libahfail_pam.so ``` -### Configure PAM (Linux/X11) - -Add to `/etc/pam.d/gtklock` (or `i3lock`, `xscreensaver`, etc.). Use the full path because `$(libdir)/ahfail` is not in PAM's default search path: - -``` -auth optional /usr/lib/ahfail/libahfail_pam.so -``` - -On Fedora/RHEL replace `/usr/lib` with `/usr/lib64`. - -## Development - -* **Run Tests:** `cargo test` -* **Benchmarks:** `cargo test --test benchmarks -- --nocapture` -* **Linting:** `cargo clippy` +--- ## Customization -To change the default sprite or audio: -1. Replace files in `assets/`. -2. Update `assets/ahfail.gresource.xml`. -3. Rebuild with Meson. +The sprite is the author's face on Nedry's body. To use your own: + +1. Replace the sprite frames in `assets/sprites/` with your own PNG sequence. +2. Update `assets/ahfail.gresource.xml` if you add or remove files. +3. Rebuild with Meson. + +To replace the audio clip, swap `assets/audio/magic-word.mp3` and rebuild. + +--- + +## Development + +```bash +cargo test # unit + integration tests (needs display) +cargo test --test benchmarks -- --nocapture # sprite placement benchmarks +cargo clippy # lint +meson setup builddir && meson compile -C builddir # full build including .so +xvfb-run meson test -C builddir --verbose # Meson tests headless +``` diff --git a/scripts/install-macos.sh b/scripts/install-macos.sh new file mode 100755 index 0000000..00a350c --- /dev/null +++ b/scripts/install-macos.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# install-macos.sh — build and install ahfail on macOS +# Run from the repository root: bash scripts/install-macos.sh +set -euo pipefail + +INSTALL_DIR="/usr/local/lib/ahfail" +BOLD=$(tput bold 2>/dev/null || true) +RESET=$(tput sgr0 2>/dev/null || true) + +step() { echo "${BOLD}==> $*${RESET}"; } +die() { echo "ERROR: $*" >&2; exit 1; } + +[[ "$(uname)" == "Darwin" ]] || die "This script is for macOS only." + +# ── 1. Homebrew ─────────────────────────────────────────────────────────────── +if ! command -v brew &>/dev/null; then + die "Homebrew is required. Install it from https://brew.sh then re-run this script." +fi + +step "Installing Homebrew dependencies..." +brew install --quiet gtk+3 gstreamer gst-plugins-base gst-plugins-good meson ninja + +# ── 2. Rust ─────────────────────────────────────────────────────────────────── +if ! command -v cargo &>/dev/null; then + step "Installing Rust via rustup..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path + # shellcheck source=/dev/null + source "$HOME/.cargo/env" +fi + +# ── 3. Build ────────────────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +cd "$PROJECT_DIR" + +step "Building ahfail..." +if [[ -d builddir ]]; then + meson setup builddir --wipe +else + meson setup builddir +fi +meson compile -C builddir + +# ── 4. Install binaries ─────────────────────────────────────────────────────── +step "Installing binaries to ${INSTALL_DIR}/ (requires sudo)..." +sudo mkdir -p "$INSTALL_DIR" +sudo cp builddir/libahfail_pam.so "$INSTALL_DIR/" +sudo cp builddir/ahfail-display "$INSTALL_DIR/" +sudo chmod 755 "$INSTALL_DIR/libahfail_pam.so" "$INSTALL_DIR/ahfail-display" + +# ── 5. Configure PAM ───────────────────────────────────────────────────────── +step "Configuring PAM..." + +# macOS 13+ uses screensaverui; 12 and earlier uses screensaver +if [[ -f /etc/pam.d/screensaverui ]]; then + PAM_FILE="/etc/pam.d/screensaverui" +elif [[ -f /etc/pam.d/screensaver ]]; then + PAM_FILE="/etc/pam.d/screensaver" +else + echo "" + echo " Could not find a screensaver PAM file. Add this line manually" + echo " to the appropriate file in /etc/pam.d/ (after the auth entries):" + echo "" + echo " auth optional ${INSTALL_DIR}/libahfail_pam.so" + echo "" + echo "Done (PAM not configured automatically)." + exit 0 +fi + +PAM_LINE="auth optional ${INSTALL_DIR}/libahfail_pam.so" + +if grep -qF "$PAM_LINE" "$PAM_FILE"; then + echo " PAM already configured in ${PAM_FILE}." +else + # Insert after the last 'auth' line so we stay in the auth block. + sudo python3 - "$PAM_FILE" "$PAM_LINE" <<'PYEOF' +import sys + +pam_file, new_line = sys.argv[1], sys.argv[2] + "\n" +lines = open(pam_file).readlines() + +if new_line in lines: + sys.exit(0) + +# Find the last line that starts with 'auth' +last_auth = max( + (i for i, l in enumerate(lines) if l.strip().startswith("auth")), + default=len(lines) - 1, +) +lines.insert(last_auth + 1, new_line) +open(pam_file, "w").writelines(lines) +PYEOF + echo " Added to ${PAM_FILE}." +fi + +# ── Done ────────────────────────────────────────────────────────────────────── +echo "" +echo "${BOLD}Done.${RESET} Lock your screen and type the wrong password to test."