# niri-macos: I ported niri's scrollable tiling to macOS, then wrote an autopsy on my own Swift

Source: https://www.michaelchurley.com/blog/niri-macos-scrollable-tiling-swift-ax-port  
Published: 2026-06-22  
Author: Michael C. Hurley  
Tags: niri-macos, niri, swift, macos, tiling, window-manager, accessibility, scrollable-tiling, paperwm, yabai, skhd, spm, ipc, michaelmonetized

![niri-macos scrollable column strip concept](/blog/niri-macos-scrollable-tiling-swift-ax-port/screenshots/scroll-strip-concept.png)

## Who

I wanted niri's scrollable tiling on the Mac without living inside Hammerspoon.

YaLTeR's [niri](https://github.com/YaLTeR/niri) is a Wayland compositor: windows live in columns on an **infinite horizontal strip**; you scroll the strip like a document; opening a window does not crush the ones you already have. PaperWM.spoon already brings that idea to macOS in Lua on Hammerspoon. I wanted the same paradigm as a **native Swift daemon** with Accessibility APIs, spring animations, and a yabai-shaped IPC CLI so skhd can drive it.

If you have ever rewritten a compositor concept as a weekend Accessibility prototype and then left an autopsy in the repo for future-you, this is that diary.

## What

I built **niri-macos**, SPM package `niri-macos`, version **0.1.0** (`niri-macos --version`), public under **michaelmonetized/niri-macos**. Platforms: **macOS 13+**. Zero external Swift packages. Products: library **NiriCore**, daemon **niri-macos**, CLI **niri-msg**.

Stack from the tree: AppKit + CoreGraphics + QuartzCore, `AXObserver` / `AXUIElement` for event-driven window tracking, `CGWindowList` enumeration, `CVDisplayLink` 60fps spring animation, `CGEventTap` gestures, Unix socket IPC at `/tmp/niri-macos.sock` with JSON commands. Config is JSON via `ConfigManager` at `~/.config/niri-macos/config.json` (gaps, outer gaps, preset widths, spring params, scroll thresholds, windowRules Codable). Hotkeys stay external: **skhd** bindings documented in README and `HOTKEYS.md`. Gestures: **Cmd+Shift+scroll** (focus window), **Cmd+scroll** (workspace), **3-finger swipe** (free scroll with momentum).

![Architecture: NiriCore, daemon, niri-msg, skhd](/blog/niri-macos-scrollable-tiling-swift-ax-port/screenshots/architecture-ipc.png)

Surfaces that exist: horizontal layout engine (`LayoutEngine.swift` ~44KB), consume/expel column stacking, center/maximize/preset widths (33/50/66/100%), dynamic workspaces (up/down/create above/below), split groups (horizontal/vertical/quad), multi-monitor isolation (active monitor follows mouse), menubar operations, `niri-msg status` / `list-windows` / `quit`. Tests: **112** `func test*` under `NiriCoreTests` (layout, types, IPC). CI: GitHub Actions on `macos-14` (build, test, release build). HEAD `d21a739`. **Four** commits. ~226KB of Swift.

README still marks Planned: focus ring overlay, window-rule **enforcement** (structs parse; sitrep says not applied), overview mode, sketchybar integration, Homebrew formula, launchd plist. PLAN.md still dreams of KDL like upstream niri; the shipped parser is JSON.

![AUTOPSY.md roast vs sitrep FUNCTIONAL](/blog/niri-macos-scrollable-tiling-swift-ax-port/screenshots/autopsy-vs-sitrep.png)

`AUTOPSY.md` (dated 2026-02-09) is the scar: it calls the early tree a 3,672-line prototype with **two** commits, **zero** tests, seven singletons, hardcoded gaps, and a README that was aspirational fiction. It also credits real spring physics, a thoughtful IPC command set, and clean `ColumnWidth` modeling. `sitrep.md` at the same era (updated for the refactor) says **FUNCTIONAL**: 112 passing tests, JSON config, protocol-based DI, main-thread layout serialization. The June 22 `nightly` commits are where that contradiction resolves in git history.

## Where

Code: [github.com/michaelmonetized/niri-macos](https://github.com/michaelmonetized/niri-macos), **public**. No hosted demo. Run locally: `swift build -c release`, put `niri-macos` on your PATH, grant **Accessibility**, start the daemon, drive it with `niri-msg` / skhd. Socket default `/tmp/niri-macos.sock`. Log default `/tmp/niri-macos.log`.

## When

- **2026-02-06.** `640d314` feat: implement niri scrolling layout paradigm for macOS (initial README/PLAN + core sources).
- **2026-02-08.** `37f34ad` fix that is really a sequel: multi-monitor isolation, discrete scroll, workspace creation, split groups, animation/gestures/AX observer (+2391/−132). AUTOPSY calls the message an undersell.
- **2026-02-09.** AUTOPSY.md examination date; sitrep claims FUNCTIONAL + 112 tests (landed in tree with the later nightly push).
- **2026-06-22.** GitHub repo `created_at`; two `nightly` commits (`f3b1d77`, HEAD `d21a739`) ship NiriCore extraction, ConfigManager, full test suite, workflow, AUTOPSY in-tree, hustlemc/uncap crumbs. Last push `2026-06-22T22:20:39Z`.

![Four-commit arc](/blog/niri-macos-scrollable-tiling-swift-ax-port/screenshots/commit-arc.png)

## Why

I use macOS and I still want niri's rule: **new windows append; existing frames stay**; scroll the strip; isolate per monitor. Hammerspoon is fine. I wanted direct APIs, spring physics I own, and `niri-msg` that feels like talking to yabai while the layout model is niri's.

I also wanted the honesty layer. Shipping AUTOPSY.md next to a polished README is the point: document the Jenga tower, then answer it with tests and a library boundary instead of deleting the roast.

## Engagement Q

Would you rather run scrollable tiling as **native Swift + Accessibility + skhd**, or stay on **PaperWM.spoon** and keep the Lua runtime, and what would make you trust a 0.1.0 WM with four commits and 112 tests?
