Tools for frictionless navigation
I’m one of those people who works with multiple open apps and tabs, and was becoming frustrated trying to find apps, windows or tabs. The obvious solution is to just have less stuff open, but the nature of my work is that I’m frequently multi-tasking or working across small chunks of available time. Sadly, it’s not practical to always be on a maker schedule so I needed a different plan.
First attempt
My first thought was to use Hammerspoon and create a set of hot keys for the things I cared about most. I found a neat approach here so repurposed that for the applications I use frequently.
local hyper = { "cmd", "alt", "ctrl", "shift" }
local applicationHotkeys = {
b = 'Google Chrome',
t = 'Ghostty',
s = 'Slack',
m = 'Spotify',
}
for key, app in pairs(applicationHotkeys) do
hs.hotkey.bind(hyper, key, function()
hs.application.launchOrFocus(app)
end)
endThis is fine for applications, but I also wanted to be able to launch specific URLs (like our Jira backlog), so I added some hotkeys for that to. I already used Karabiner-Elements so I mapped capslock to be the hyper key (cmd+alt+ctrl+shift) and could now immediately switch to Slack with hyper-s or launch Jira with hyper-j.
This was fine, but that only gave me quick access into the things I used all the time, and I had to do quite a lot of fiddling to stop my Jira hotkey from opening a new tab each time I switched to it.
Contexts
What I really wanted was a way to separate and distinguish betwen the contexts of kinds of applications, websites and work I do:
- Mail and Calendar
- Social / chat
- Jira / Jira Product Discovery
- Terminal
- Notes
- General web browsing
- Music
Spaces is already built-in on my Mac, and felt like the obvious choice, but it only solved the problem of separating different contexts into their own workspaces. I still had the problem of moving between the windows within a workspace. And given that I already have too many years of muscle memory from working in vim, something that supported keyboard navigation (with vim keys) would be the best solution for me.
Luckily that already exists in the Aerospace, an i3-like tiling window manager for MacOS.
Aerospace
I’ve been using Aerospace for a while now and it’s fast become second nature. This is great because it means I no longer have to pause, think or reach for the mouse when switching between tasks or applications. I got to this point by:
- Defining a clear mental model for the purpose of each workspace, and the applications and types of work that live in each. Aerospace allows you to define any key (alphanumeric) to a workspace, meaning I can use mnemonics for workspaces (as apposed to only numeric workspace numbers)
- Binding all of my workspace / window management keys within the scope of the
hyperkey, and within that, sticking tovimkeybindings where possible. This means that my mental model for moving right is alwayslwithin whatever context I am working - Using application detection to automatically move applications into the workspace I expect them to be in. So if (for example) I launch Ghostty while I’m in my browser workspace, Ghostty loads and is immediately moved to my terminal workspace. It may seem counter-intuitive in the moment, but applications tend to remain loaded for way longer than my immediate need for it in that moment, so in the long run, having it where I expect to find it means less cognitive load wondering where that terminal is.
In a way, I’m striving for mise en place on my computer.
Config
My full config is available on github as part of my Nix configs, but I’ll go through some of the relevant bits below.
As part of this configuration I changed my mapping of the hyper key to be cmd+alt+ctrl so that I could then use shift as a secondary modifier.
Aerospace also allows you to define different binding modes - with each defined mode having it’s own set of bindings. All of the following bindings are for the default [mode.main.binding] mode.
Layout
Aerospace has three layout modes - floating where no special layouts get applied, tiling that tiles windows either next to one another (vertical) or above one another (horizontal), and accordion where the windows are as wide as possible, with a small portion of non-focused windows visible on the side (when choosing vertical) or above and below (when choosing horizontal).
alt-cmd-ctrl-slash = 'layout tiles horizontal vertical'
alt-cmd-ctrl-comma = 'layout accordion horizontal vertical'
alt-cmd-ctrl-period = 'layout floating tiling'Just a note: floating only applies to the currently focused window / application, not to everything in the workspace.
Window resizing
When in tiling mode, the windows adjust to take up equal amounts of space. I sometimes want to do adjustments to this:
alt-cmd-ctrl-minus = 'resize smart -50'
alt-cmd-ctrl-equal = 'resize smart +50'Moving (focus and position)
The expected vim motion keys hjkl are mapped to hyper and hyper+shift to move my focus between windows and to move window positions within the workspace:
alt-cmd-ctrl-h = 'focus left'
alt-cmd-ctrl-j = 'focus down'
alt-cmd-ctrl-k = 'focus up'
alt-cmd-ctrl-l = 'focus right'
alt-cmd-ctrl-shift-h = 'move left'
alt-cmd-ctrl-shift-j = 'move down'
alt-cmd-ctrl-shift-k = 'move up'
alt-cmd-ctrl-shift-l = 'move right'Workspaces
Aerospace defaults to workspaces [0-9] and [a-z]. When you have an external monitor attached, that becomes workspace 10. You do not need to have all the workspaces show up, and I’ve explicitly listed only the alphabetic workspaces I use:
alt-cmd-ctrl-1 = 'workspace 1' # Mail and Calendar
alt-cmd-ctrl-2 = 'workspace 2' # Socials: Slack, Telegram, Discord, WhatsApp
alt-cmd-ctrl-3 = 'workspace 3' # Work: Jira
alt-cmd-ctrl-4 = 'workspace 4'
alt-cmd-ctrl-5 = 'workspace 5'
alt-cmd-ctrl-6 = 'workspace 6'
alt-cmd-ctrl-7 = 'workspace 7'
alt-cmd-ctrl-8 = 'workspace 8'
alt-cmd-ctrl-9 = 'workspace 9'
alt-cmd-ctrl-b = 'workspace B' # Browser: Chrome, Firefox
alt-cmd-ctrl-e = 'workspace E' # External monitor
alt-cmd-ctrl-f = 'workspace F' # Finder
alt-cmd-ctrl-m = 'workspace M' # Music: Spotify
alt-cmd-ctrl-n = 'workspace N' # Notes: Obsidian
alt-cmd-ctrl-t = 'workspace T' # Terminal: GhosttyI use the exact same config between my work and personal machines, the only difference being the applications that live in each workspace. So at home I have WhatsApp and Discord in workspace 2, but at work it is Slack and Telegram. However, whichever machine I am on, if I want to switch to a chat/messaging context, it’s always in workspace 2.
I also use a mix of numeric and alphabetic workspaces. I can’t remember my reasoning for this, but if I were to post-rationalise it, numeric workspaces are for my daily drivers (Calendar, Slack, Jira), while alphabetic are for supporting applications.
I then use shift to move a window into that workspace:
alt-cmd-ctrl-shift-1 = 'move-node-to-workspace 1'
alt-cmd-ctrl-shift-2 = 'move-node-to-workspace 2'
alt-cmd-ctrl-shift-3 = 'move-node-to-workspace 3'
alt-cmd-ctrl-shift-4 = 'move-node-to-workspace 4'
alt-cmd-ctrl-shift-5 = 'move-node-to-workspace 5'
alt-cmd-ctrl-shift-6 = 'move-node-to-workspace 6'
alt-cmd-ctrl-shift-7 = 'move-node-to-workspace 7'
alt-cmd-ctrl-shift-8 = 'move-node-to-workspace 8'
alt-cmd-ctrl-shift-9 = 'move-node-to-workspace 9'
alt-cmd-ctrl-shift-b = 'move-node-to-workspace B'
alt-cmd-ctrl-shift-e = 'move-node-to-workspace E'
alt-cmd-ctrl-shift-f = 'move-node-to-workspace F'
alt-cmd-ctrl-shift-m = 'move-node-to-workspace M'
alt-cmd-ctrl-shift-n = 'move-node-to-workspace N'
alt-cmd-ctrl-shift-t = 'move-node-to-workspace T'I’ll explain more about E for my external monitor a bit later.
The other binding I use is for toggling between the two most recent workspaces:
alt-cmd-ctrl-tab = 'workspace-back-and-forth'Service mode
Service mode is a special mode within Aerospace for some advanced window management. I use it for nesting windows, resetting the layout (when I inevitably mess it up) and doing a mass window close.
Getting into service mode is a keybind withing the main mode:
alt-cmd-ctrl-shift-semicolon = 'mode service'When entering service mode, all of the bindings for the main mode are cleared, and you now only have access to the bindings in this mode. All of these settings are within the [mode.service.binding] section of the config.
I sometimes want a very specific tiled layout, such as stacking two windows vertically within a horizontal tiled layout.
By way of example, here is a screenshot of me editing this post. I have three Ghostty windows - Neovim on the left, Hugo in the middle and git on the right.

I would prefer that my Neovim window was a lot wider, so I expand that with hyper +:

The final step is to join the Hugo and git windows into their own vertical stack. To do that I would:
- Change focus to the Hugo window:
hyperl - Enter service mode:
hypershift; - Join the current window with the one on the right:
hypershiftl - Using a join command exits service mode automatically, so I just need to move back to Neovim on the left:
hyperh

The full keybindings for window stacking is also related to vim motions:
alt-cmd-ctrl-shift-h = ['join-with left', 'mode main']
alt-cmd-ctrl-shift-j = ['join-with down', 'mode main']
alt-cmd-ctrl-shift-k = ['join-with up', 'mode main']
alt-cmd-ctrl-shift-l = ['join-with right', 'mode main']If it all goes horribly wrong, I might want to reset the layout. Enter service mode and just hit r:
r = ['flatten-workspace-tree', 'mode main']And if I want to close everything except the current window, enter service mode and just hit backspace. This is only for windows in the current workspace.
backspace = ['close-all-windows-but-current', 'mode main']Window and Workspace management
The last part of the config is to ensure that workspaces and applications are consistently loaded where I expect them to be. These configs exist at the top level of the Aerospace config file.
[workspace-to-monitor-force-assignment]
E = ['secondary', 'main']I use the mnemonic E to designate my external monitor. This says that the E workspace must defauilt to the secondary (external) monitor, and if not available, fall back to the main monitor. With this setting the windows I have on the external remain in their dedicated workspace when I unplug, and return when I plug back in.
Finally, you can detect when windows load, and force them into a specific workspace or layout mode. Aerospace has a command line utility to list all currently running applications:
aerospace list-appswhich then outputs the currently running applications and their identifiers:
12760 | com.apple.UserNotificationCenter | UserNotificationCenter
1779 | com.apple.finder | Finder
2712 | com.hnc.Discord | Discord
6695 | com.apple.Preview | Preview
1885 | com.spotify.client | Spotify
1775 | com.mitchellh.ghostty | Ghostty
2285 | org.mozilla.firefox | Firefox
2627 | md.obsidian | Obsidian
2750 | net.whatsapp.WhatsApp | WhatsAppMove ghostty to the T workspace:
[[on-window-detected]]
if.app-id = 'com.mitchellh.ghostty'
run = ['move-node-to-workspace T']Always run Preview in floating mode:
[[on-window-detected]]
if.app-id = 'com.apple.Preview'
run = 'layout floating'Alfred
I’ve been a long time user of Alfred so I created some keyword triggers for workflows to open specific URLs I access all the time. So for example, cmd space jpd opens my teams project in Jira Product Discovery. I don’t use the dock, keep it hidden at all times, and launch applications through Alfred anyway, so it’s the same workflow for me to open the URL as it is for opening any other application.