Tilee: my own tiling window manager in Hammerspoon

A tiling window manager I built in 1 day with Claude Code, in Lua inside Hammerspoon. It replaces Amethyst and tiles a new window in 0.1 s instead of 9 s.

Tilee is the tiling window manager on my Mac Studio. It replaces Amethyst, which I used for 2 years. Each of my 4 screens shows 1, 2 or 3 equal columns, and every window snaps into its slot. No dragging, no overlap.

It runs as 1 Lua module inside Hammerspoon, which already holds my Mac together (see Hammerspoon - macOS automation in Lua). I built it with Claude Code on 13th October 2026: measurements and analysis in the morning, the spec and the build in the afternoon, live by the evening. The name fits the -ee family, next to Filee: my own file manager for macOS and Dictee: my own local dictation app.

The difference is the reason it exists. Amethyst took 9 s to tile a new window by the afternoon. Tilee takes 0.1 s.

Bar chart: Amethyst tiles a new window in 9.0 s after 3 hours of uptime and 1.9 s after a relaunch, Tilee in 0.1 s. A second chart shows Tilee's own work per action, from 5.5 ms for a swap to 82 ms when a thrown window arrives

Why I replaced Amethyst

Amethyst was solid for layouts, but it got slow. My worst moments were focus changes and Shift+Cmd+4, the swap with the main window. So I measured it: open a TextEdit window and time how long Amethyst takes to put it in its tile.

Amethyst state Time to tile a new window
Running for 3 h, my normal day 8.3 s, 9.0 s, 9.9 s
Just relaunched 1.9 s, 2.1 s

The Mac wasn't the limit. Moving 1 window through the macOS Accessibility API costs 34 ms. Amethyst used 1.3 s of CPU in a 13 s probe; the rest was waiting.

The cause is in its design, not in 1 bug:

  • Every event restarts the work. A new window fires 3 to 5 events, and each one cancels the queued reflow and starts a new one.
  • It reads everything, every time. Each reflow re-reads every window through Accessibility (540 ms for my 32 windows), then sets the frame of every tiled window, not only the ones that changed.
  • It gets worse with uptime. 2 s after a relaunch, 9 s after 3 hours. The issue "Amethyst is extremely slow" (#1496) has been open since 2023, with the same numbers as mine.
  • Ghost windows. Amethyst keeps its own window list and loses track of entries, so a screen kept a gap for a window I had thrown elsewhere.

I looked at the alternatives before building anything. AeroSpace and Rift are fast, but both replace macOS Spaces with their own workspaces and window model. yabai needs System Integrity Protection partly off, which I won't do. A fork of Amethyst means rewriting the core of someone else's 13-year-old Swift codebase.

A tiler in Hammerspoon had the best trade: no new app, no permission dialog, every shortcut kept, and only my desk to support. PaperWM.spoon already proved that a full tiling window manager works in Hammerspoon Lua.

What it does

Feature How it works
3 layouts 1, 2 or 3 equal columns per screen. Opt+Shift+D / A / F, or Shift+Cmd+arrows to cycle
Stacking More windows than columns: the extra ones stack in the last column, the last focused on top
New windows Take the main slot on the left. The others shift right
Same shortcuts Every Amethyst key: focus and throw to screen, swap, focus next, float. The Stream Deck needs no change
Floating A list of apps that never tile (1Password, CleanShot, System Settings), plus Opt+Shift+T per window
Pointer follows focus The pointer jumps to the window I focus with the keyboard
Home screens Terminal, Ghostty and TextMate open on the demo screen; my recording window goes straight there
Switch A menu bar item shows T or A. 1 click, 1 Stream Deck key or 1 URL switches to Amethyst and back

The switch matters during the first week. Turning Tilee on quits Amethyst; turning it off relaunches Amethyst. Both apps holding the same keys at once gives unpredictable results, so only one of them is ever on. If tilee.lua fails to load, Hammerspoon starts Amethyst by itself.

The numbers

Measure Amethyst 0.24.3 Tilee
New window tiled 1.9 s fresh, 9 s after 3 h 0.1 s (median of 5 runs)
Swap with main (Shift+Cmd+4) A full reflow of the screen 5.5 ms of work, 72-171 ms on screen
Layout change, 10 windows A full reflow of the screen 38-103 ms until the screen shows it
Average reflow - 17.5 ms median, 73 ms at the 95th percentile
Slower with uptime Yes, 2 s to 9 s in 3 h Not so far. There is no window list to grow
Ghost windows Yes, after a throw Can't happen: the state is rebuilt at each event

The Tilee numbers come from 2 sources: the automated QA (22 checks, 5 runs) and 317 reflows in the live log during my first evening of real use. "On screen" includes about 20-50 ms for macOS to deliver the key press and the app's own redraw. TextEdit alone takes about 130 ms to resize when a window changes screen. Tilee can't make an app draw faster.

My verdict after the first evening: much better than Amethyst, and extremely responsive.

For business people

A window manager is something I touch 100s of times a day without noticing it. When each touch costs 2 to 9 s, I notice, and every wait breaks my focus.

What changed for me:

  • No waiting. Windows land where they belong as fast as I can press keys.
  • No afternoon slump. Tilee keeps no state that grows over the day, so the Mac feels the same at 18:00 as at 08:00.
  • No relaunch ritual. I no longer restart the window manager to get speed back.
  • It does exactly what I need. 3 layouts, my 4 screens, my shortcuts. When something is missing, I describe it and it's there the same day.

The cost: 1 day of work with Claude Code. The trade: it's personal software. 1 user, 1 desk, no community to catch a macOS change first. For something this central to my day, I take that trade.

For technical people

Stack

Layer Choice
Language Lua, 1,383 lines in 1 module, plus a 520-line QA script
Host Hammerspoon 1.1.1 on macOS 26.5. ~/.hammerspoon/tilee.lua is a symlink to the repo
Events hs.window.filter: created, destroyed, focused, moved, minimised; app launch and quit; screen and Space changes
Window calls Accessibility, through hs.window
State tilee-state.json: on or off, the mode per screen, the window order
Control Hotkeys, a menu bar item, and hammerspoon://tilee?cmd=... URLs for Alfred and the Stream Deck
Log 1 line per reflow, with its time and the number of windows moved

The design choices that make it fast

  • Rebuild, don't track. Before every reflow, Tilee rebuilds the screen's window list from the live windows. There is no list to corrupt, so the ghost window can't happen.
  • Move only what's off its slot. A window within 4 points of its slot stays. A swap moves 2 windows, not 10.
  • 1 Accessibility call instead of 3. setFrame sets size, position and size again. If only the position changes, Tilee calls setTopLeft (under 1 ms). If only the size changes, it calls setSize.
  • A frame cache. Tilee reads a window's frame again only after the window reports a move. A busy app (Notee once took 309 ms to answer) no longer slows every key press.
  • Visible windows first. Windows hidden under the top window of a stacked column move 1 per run-loop pass, after the visible ones. A layout change shows in about 50 ms, and the hotkeys stay responsive while the hidden windows follow.
  • A slow-app guard. Hammerspoon runs on 1 Lua thread, so a hung app would block my dictation key too. If a frame read takes more than 100 ms, Tilee uses that app's last known frames for 10 s.

QA that moves real windows

tilee-qa.lua runs 22 checks in about 2 minutes. It opens TextEdit and Terminal windows, presses the real shortcuts, measures each result, closes its windows and writes a report. Because it takes over the screens, Claude runs it only after I say "go", when I'm away from the desk. Every fix to a reported problem gets a new check.

Lessons from the first evening

The first live run found 6 problems that no read-only check could:

  • Hammerspoon garbage-collects timers. An hs.timer.doAfter that nothing references is collected and never fires. Tilee keeps every timer in a table until it fires.
  • No AppleScript inside a coroutine. It spins the run loop, a callback switches Lua state, and Hammerspoon aborts. That crash took Hammerspoon down once.
  • hs.window.filter misses background apps. It registers an app only when it launches or activates with a focused window. My own apps that start in the background stayed invisible. Tilee registers new apps again 1, 3, 6 and 10 s after launch.
  • setSize is the expensive call. 5 to 20 ms when nothing changes, 100 ms or more when the app lays out its content again.
  • Fixed-size dialogs. A permission prompt that refuses to resize used to hold a whole column. A window that keeps less than half its slot now floats.
  • A consent prompt can freeze Hammerspoon. An AppleScript to Terminal raised the macOS Automation prompt and blocked Hammerspoon, dictation included, for about 2 minutes. The QA now opens Terminal with open -a instead.

The QA ends at 17 to 19 of 22 checks passed. The fails are timing limits I set at 50-150 ms, where macOS key delivery and TextEdit's own resize take longer than Tilee itself.

What's next

  • 1-week trial. Amethyst stays installed, 1 Stream Deck key away.
  • The checks that need a person: sleep and wake, unplugging the prompter screen, a reload with 10 windows open, and the same timings after 8 hours of uptime.
  • Then Amethyst leaves the login items for good.

Further reading

The tool it runs in:

The app it replaces:

The proof that a full tiler works in Hammerspoon:

The tool I built it with:

NicAI
Written by NicAI, Nic's AI assistant, for his personal knowledge base. Researched and drafted by the model, not hand-written by Nic. Verify anything you plan to act on.