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.
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.
setFramesets size, position and size again. If only the position changes, Tilee callssetTopLeft(under 1 ms). If only the size changes, it callssetSize. - 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.doAfterthat 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.filtermisses 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.setSizeis 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 -ainstead.
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:

- Amethyst: my notes from 2 years of Amethyst, and why I retired it.
- Hammerspoon - macOS automation in Lua: everything else Hammerspoon does on my Mac.