Building ASCII Saver: Lessons from My First macOS Screensaver

Inspired by a post I’d bookmarked on Reddit years ago, ASCII Saver started as a simple idea: take the camera feed and render it as ASCII characters on screen. How hard could it be?

Quite hard, as it turns out. macOS screensavers run in a sandboxed process (legacyScreenSaver) that can’t access the camera directly. That constraint shaped the entire architecture.

Two Processes, One Frame Buffer

The solution is a two-process design:

The two processes communicate through Darwin notifications (start, stop, heartbeat) and share frame data through a seqlock-protected mmap’d file at /tmp/ASCIISaver/framebuffer.bin. Darwin notifications were chosen specifically because they’re one of the few IPC mechanisms not blocked by the legacyScreenSaver sandbox.

The Seqlock Pattern

The frame buffer uses a seqlock for lock-free sharing. The writer increments a sequence number before and after writing. The reader checks that the sequence number is even (no write in progress) and unchanged after reading. If either check fails, it retries. No mutexes, no blocking, no priority inversion.

// Writer side (simplified)
header.sequence &+= 1   // odd = write in progress
// … write frame data …
header.sequence &+= 1   // even = write complete

Camera Permissions and Hardened Runtime

The most frustrating bug was the camera permission prompt never appearing. The agent requested camera access, the Info.plist had NSCameraUsageDescription, the code was signed — but macOS silently denied access without ever showing a prompt.

The missing piece was the com.apple.security.device.camera entitlement. With hardened runtime enabled (required for notarisation), macOS won’t even ask the user for camera permission unless the binary explicitly declares the entitlement. No entitlement, no prompt, no error — just a silent denial.

What I’d Do Differently

If I were starting fresh, I’d skip the Xcode workspace with three targets and a Swift package (there was a target for a test viewer for my convenience). The final version has two targets and a flat file structure. Simpler to build, simpler to sign, simpler to debug.

I’d also set up code signing properly from day one. Unsigned builds work fine in development, but the moment you need TCC permissions (camera, microphone, accessibility), everything falls apart without a valid signature.

The Result

Five colour modes (Classic, Matrix, Amber, Raw Feed, Silhouette), four retro effects (scanlines, phosphor persistence, glitch, interference), and a configuration panel — all running at up to 60 FPS from a 320×180 camera feed.

ASCII Saver on GitHub · Product page