docs: rewrite readme; add macOS install script
All checks were successful
Test / test (push) Successful in 6m2s

- 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 <noreply@anthropic.com>
This commit is contained in:
Asger Geel Weirsøe
2026-05-06 15:16:26 +02:00
parent e41ae1cd7f
commit f951cb9c6d
2 changed files with 213 additions and 67 deletions

168
readme.md
View File

@@ -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 Nedrys “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.
---
## Linux — Wayland (gtklock)
### Install dependencies (Arch)
1. **Install Dependencies:**
```bash
sudo pacman -S meson ninja rust gtk3 gstreamer gst-plugins-base gst-plugins-good
sudo pacman -S meson ninja rust gtk3 gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gtklock
```
2. **Build:**
### Build and install
```bash
meson setup builddir
meson compile -C builddir
```
3. **Install:**
```bash
sudo meson install -C builddir
```
## Usage
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 `gtklock` with the module path:
### 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`.
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
```

98
scripts/install-macos.sh Executable file
View File

@@ -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."