Two monitors. Two or three apps open across them, all day.
Terminal and the simulator live on the left screen. Cursor lives on the right.
Nearly everything in that setup is keyboard-driven. I have spent years shaving the mouse out of it — launchers, window snapping, clipboard, tmux, a Hyper key on Caps Lock.
Then I watched my own hands for a day. To get from the IDE to the terminal, I reached for the mouse. Every single time.
One gesture. Dozens of times a day. The last thing I hadn’t solved.
What macOS actually gives you
⌘Tab switches between apps in most-recently-used order. That order has nothing to do with where anything sits.
Press it once, you get the last app. Press it twice, the one before that. Neither answers the question you are actually asking, which is the window on my left.
`⌘“ cycles windows inside one app. Genuinely useful. Also not this.
Both model history. My screen is geometry. Nothing built in bridges those two things.
Every tool wanted to rearrange my desktop
I went looking for something off the shelf before writing anything.
yabai, AeroSpace, Amethyst. All good software. All tiling window managers.
Install one and it takes ownership of your layout. Windows get retiled, resized, arranged into a tree you now have to think about.
That is the trade they offer. For a lot of people it is worth taking.
I did not want my layout managed. My windows are where I put them, at sizes I chose. I wanted focus moved and nothing else touched.
Rectangle moves and resizes. Raycast’s window switcher is a list you filter by typing — still a name, still not a direction.
Nothing did only the focus part.
Borrowing tmux’s model
tmux solved this a long time ago. prefix + h/j/k/l moves to the pane in that physical direction.
Nothing about history. The pane that is there.
That is the part worth stealing — not the keys, the mental model. You already know where your windows are. The shortcut should just agree with you.
Two tools, because one can’t do it
All my keyboard customisation already lives in Karabiner-Elements, where Caps Lock is remapped to Hyper — ⌃⌥⇧⌘ held together, a modifier combination no app claims.
Karabiner cannot do this alone, and it took me a minute to accept that. It remaps keys. It has exactly two outputs: synthesized key events, and shell commands.
Neither of those knows where a window is. Ask Karabiner to focus the window to your left and there is nothing to ask — it holds no model of the screen.
Hammerspoon is the mirror image. It reads window geometry through the Accessibility API, and it can focus a window without touching its frame. Focus only, no retiling. Exactly the missing half.
So each tool does the part it is good at. Karabiner owns the keyboard. Hammerspoon owns the screen.
They need a wire between them. This is the nice trick.
F13 through F18 exist in the HID specification and on no Apple keyboard. That makes them private.
Karabiner emits them. Hammerspoon listens for them. No physical key you own will ever produce one.
F19 was already spoken for. It carries `⌘“ through to Ghostty’s quick terminal, using the same trick.
The keys I settled on
Caps + ←↓↑→ moves focus in that direction. Caps + , and Caps + . jump between displays outright.
The selection keeps candidate windows inside a 45° cone of the direction you pressed, then takes the nearest by centre-to-centre distance.
The cone is what makes it feel right.
A naive “any window further left” test picks absurd targets — a window far above and slightly left beats the obvious neighbour beside you.
With tmux panes you never notice. Panes tile and share edges. Floating macOS windows break that assumption immediately.
When nothing sits in that direction, focus falls through to the display in that direction. So crossing monitors is the same gesture as moving one window over. You do not switch modes in your head.
Why arrows and not h/j/k/l? Because Hyper+j, Hyper+k and Hyper+l were already bound — journaling, a kaizen journal, a docs dashboard. Only h was free.
Arrows cost two keystrokes and evicted nothing. Home row would have cost three bindings I use every day. That trade was not close.
Worth checking before you pick keys: Hyper on Caps Lock already contains Shift. So Hyper+Shift+arrow is not a distinguishable chord — there is no shifted variant to reach for. Display switching needed its own two keys rather than being a modifier away.
Two details that made it feel finished
The first working version was correct and still felt wrong.
Focus moved, and I could not tell where it had gone. Two monitors, several windows, no visible answer to the only question that mattered: which one is live now?
So on every keypress, everything except the focused window dims. Down over 0.18s, held 1.2s, back over 0.35s. Both monitors, with a hole cut exactly where the focused window sits.
It is deliberately temporary. A permanent border becomes wallpaper in about a minute — your eye stops reporting it. The dim fires on the transition, which is the only moment the information is worth anything.
The second detail is smaller and I underestimated it completely.
The pointer stayed where it was.
I would move focus to the left monitor, type happily, then go to click something — and the click landed on the right monitor, where the cursor had been sitting all along.
Now the pointer warps to the centre of the newly focused window. Keyboard path only.
Warping the cursor while someone is actively using the mouse would be hostile. Mouse-driven focus changes do nothing at all.
Roughly 520 lines of Lua and six Karabiner rules, all told. Twenty-four unit tests, but only on the geometry — the pure arithmetic that decides which window is west of which.
What it cost, honestly
Three things bit hard enough to name.
Karabiner rules declare which modifiers they consume. mandatory modifiers get stripped from the outgoing event. optional ones ride along untouched.
Mine were optional. So F13 arrived with ⇧⌃⌥⌘ still held, while Hammerspoon listened for a bare F13.
Carbon’s hotkey API matches modifiers exactly. Not as a subset.
Nothing fired. And every test I had written called the Lua function directly, which skips Karabiner entirely. The one test that would have caught it was pressing the key.
The dim overlay kept getting stranded on screen.
Reloading Hammerspoon re-sources every file with fresh empty locals. The previous overlay stayed visible with nothing left referencing it — unhideable, because the handle was gone.
Startup now tears down before it sets up.
And one overlay cannot cover two monitors. With Displays have separate Spaces enabled, which is the default, a window physically cannot span displays.
macOS does not error on this. It renders your overlay on one screen and silently drops the rest. One canvas per screen fixes it.
None of that is visible in daily use. It is what the last mile costs.
My hand stays on the keyboard now.