Native Wayland screen magnifier utility.
⚠ Status: Maggie is built for Linux only. It's developed and tested primarily on my own setup, using the Niri compositor. It's Wayland-only — X11 is not supported, and other compositors (Sway, Hyprland, KWin, GNOME…) are untested and may not work correctly (feel free to report any compat issues).
Maggie is a frozen-frame screen magnifier: it captures the screen exactly once at startup via zwlr_screencopy and shows a fullscreen, cursor-following, pixelated view of that frame as a layer-shell overlay — no live capture, no compositor window rules required. It runs natively on Wayland compositors exposing wlr-layer-shell + wlr-screencopy, such as minimal desktop environments and tiling window managers in the spirit of Niri.
Rendering is GPU-accelerated via EGL + OpenGL ES 2 (nearest-neighbor, crisp magnifier look), with a fully functional CPU bilinear fallback when EGL/GLES2 is unavailable.
Here's what Maggie can do today:
- Keyboard zoom:
1–9switch zoom levels instantly. - Scroll-wheel zoom:
levelsmode (steps through 1–9) orfactormode (10 % steps, clamped to 1×–32×); wheel direction is reversed by default, flippable via theinvert_scroll_zoomconfig option; high-resolutionvalue120deltas supported. - Cursor-following:
snap(instant, default),ease(smooth exponential), orinertia(with momentum glide), tracked at sub-pixel precision and clamped at the capture edges. - GPU rendering: EGL + OpenGL ES 2 with nearest-neighbor sampling at 2× buffer scale; automatic, permanent CPU fallback (bilinear) if GPU init fails.
- OSD key legend:
Ktoggles an on-screen legend that stays in the corner farthest from the cursor. - Fullscreen screenshot:
Fsaves the frozen frame as a PNG (default~/Pictures/maggie_%Y%m%d_%H%M%S.png). - Configuration: RON file at
~/.config/maggie/config.ron. - CLI:
-z/--zoom <level>initial zoom,-d/--debugverbose logging,--help,--version. - Quit:
Q,Escape, or right mouse button.
| Key | Action |
|---|---|
1–9 |
Set zoom level |
| Mouse wheel | Zoom in/out (mode + direction configurable) |
F |
Save fullscreen screenshot |
K |
Toggle OSD legend |
A |
Anti-aliasing toggle (stub, inert) |
C |
Configuration window (stub, inert) |
S / W |
Manual region / window screenshot (stub, inert) |
Q, Escape, RMB |
Quit |
maggie # start with defaults
maggie -z 3 # start at zoom level 3
maggie -d # verbose debug logging to stdoutgit clone https://github.com/hced/maggie.git
cd maggie
cargo build --release
cp target/release/maggie /usr/local/bin/System dependencies (Debian/Ubuntu): libwayland-dev and libxkbcommon-dev. Prefer a pre-built binary? Check the Releases page — tag pushes build and publish a maggie-linux.tar.gz automatically.
Release builds target x86-64-v3 (AVX2/FMA — every mainstream CPU from the last decade, set in .cargo/config.toml), so the render loops auto-vectorize. On pre-v3 hardware, build with just build-generic (or RUSTFLAGS="-C target-cpu=x86-64" cargo build --release). For a build tuned to your exact CPU, use just build-native — fastest, but the binary only runs on identical-or-newer CPUs.
The repo includes a justfile with developer conveniences: just build, just run, just tests, just check, just lint — and a full release workflow (just release, just push-release-tag).
Maggie reads its configuration from ~/.config/maggie/config.ron (Rusty Object Notation). If the file is absent, it falls back to sensible defaults. Options:
default_zoom— initial zoom level (floating point).cursor_follow—snap(default) |ease|inertia.scroll_zoom_mode—levels(default) |factor.invert_scroll_zoom— boolean, defaultfalse.show_osd— boolean, defaulttrue.keybindings— bindings for all in-app functions.screenshot_path— default~/Pictures.screenshot_filename_pattern— supports%Y %m %d %H %M %Stokens, defaultmaggie_%Y%m%d_%H%M%S.png.
Newer options carry #[serde(default)], so config files written before they existed remain loadable. Configuration is currently load-only — edit the file manually (see Limitations).
Maggie is a single-binary Wayland client built on wayland-client + smithay-client-toolkit.
- Frozen-frame model — the screen is captured exactly once at startup via
zwlr_screencopy; the SHM buffer (XRGB8888/ARGB8888) is converted to RGBA with a stride-aware row copy honoringy_invert. Failed captures retry up to 3 times, then the overlay renders black. - Capture-before-content — the overlay is committed at startup but presents no image data until the first frame arrives, so the initial screencopy never contains the overlay — avoiding the Droste-effect self-feedback that plagues live-capture magnifiers.
- Fullscreen layer-shell overlay —
Layer::Overlaywith all anchors and an exclusive zone of −1 ("dont care"), so the compositor hands it the full physical screen instead of shrinking it around bars/docks. The surface re-asserts its size on every configure and redraws immediately. Keyboard interactivity ison-demand, keeping compositor-level global keybindings alive. - Lazy GPU init — the
wl_egl_windowis created at the first configure, when the real output size is known, so the very first presented buffer is already fullscreen. EGL is loaded dynamically (khronos-egl); GLES2 bindings are generated at build time bygl_generator. - Swap interval 0 — frame redraws never block the event loop, so input works during panning animations.
- Event-driven input — pointer motion at sub-pixel precision, wheel deltas, and keyboard events are handled in a
blocking_dispatchloop; panning animations are driven bywl_surfaceframe callbacks.
| Module | Responsibility |
|---|---|
src/main.rs |
CLI parsing (clap), tracing setup, entry point |
src/engine.rs |
Core state machine: Wayland globals, layer-shell surface, screencopy handling, input dispatch, view math (zoom/centering/clamping, ease/inertia), draw orchestration, screenshot saving |
src/capture.rs |
Screenshot output path generation (~ expansion, filename tokens, directory creation) |
src/render.rs |
RgbaBuffer, CPU bilinear renderer and nearest-neighbor scaling |
src/gpu.rs |
EGL/GLES2 renderer: shader compilation, textured-quad draw, OSD pass, lazy init, resize |
src/osd.rs |
5×7 bitmap font, OSD sprite construction, farthest-corner placement (unit-tested) |
src/input.rs |
Legacy keysym → Action dispatch layer (actual key handling lives in engine.rs) |
src/config.rs |
RON config schema, defaults, load_config / save_config |
- Which GPU renders? On Wayland the compositor decides, not the app:
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND)hands clients the compositor's client-buffer device, and there is no client-side way to pick another GPU. Maggie logs the actual device at startup —EGL GPU: <vendor> — <renderer>— so routing is always verifiable at a glance. - Hybrid laptops (iGPU + dGPU): if the log shows the integrated GPU but you want the discrete one, the lever is the compositor. For niri:
debug { render-drm-device "/dev/dri/renderD129" }(use your dGPU's render node) renders everything — the compositor and all EGL clients — on the discrete GPU while the panel stays on the iGPU (niri's multi-GPU/PRIME copy path). Verify withnvidia-smi(compositor and clients will show real VRAM usage) or theEGL GPUlog. Expect higher power draw. - Panning smoothness: motion-driven redraws are coalesced to ~120 Hz, and the fullscreen surface is marked opaque so compositors occlusion-cull whatever is underneath — compositor load is roughly independent of the app beneath the magnifier, so a constantly repainting app (e.g. a browser) no longer steals frames.
Broader compositor and distro support is a direction I'd like to take Maggie in, but none of it is implemented yet:
| Target | Status |
|---|---|
| Niri | Tested |
| Sway | Not implemented |
| Hyprland | Not implemented |
| GNOME (Mutter) | Not implemented |
| KDE (KWin) | Not implemented |
The frozen-frame + layer-shell design is compositor-agnostic at the protocol level, but each environment needs its own capture path (GNOME notably lacks zwlr_screencopy), and layering/anchoring behavior differs between compositors.
Planned or under consideration (per SPEC.md — not commitments):
- Manual selection screenshot (
S) — drag a rectangular region; nudge its sides with the arrow keys. Stub. - Window selection screenshot (
W) — grid of available windows; click to capture and save one. Stub. - Configuration window (
C) — live config editing with instant application, per-setting reset, and persistence. Stub. - Anti-aliasing toggle (
A) — nearest-neighbor / bilinear switch. Stub. - Write-on-change config persistence —
save_configexists but is unused; runtime adjustments never reach disk. - Selection-mode cancellation — Escape should cancel an in-progress
S/Wselection instead of quitting. - Legacy mode bindings — the obsolete Center Cursor / Edge Pan / Miniature Window modes (
Ctrl+C/Ctrl+E/Ctrl+M) are pending redefinition or removal.
Tag pushes (v*) trigger the CI workflow, which builds a --release binary, runs the test suite, and publishes a packaged archive with checksums to a GitHub Release.
What Maggie doesn't do (yet):
- No live capture mode — the screen is captured once at startup; the view is frozen until exit (by design).
- Config is load-only — runtime changes never reach disk; edit
config.ronmanually. - Only
Fscreenshot works —SandWare not implemented;Escapequits rather than cancelling. AandCare inert — bound, but log "not yet implemented".- Linux / Wayland-only — other compositors untested; X11 unsupported.
H. Cederblad
