MacOS Keymap Guide
My full configuration of key mappings across MacOS, enabling a mostly keyboard focused experience.
Justin Wyne / November 22, 2023 / Updated October 4, 2024
Introduction
Ever since I learned vim in my college operating systems course, I've been hooked on keyboard efficiency. Vim accomplishes this well for text editing, but the desire to have that feeling of keyboard-only speed expanded to the rest of computing was a natural consequence.
Over the years, I've gone overboard trying to eliminate the use of a mouse completely; using browser extensions like cvim, vimperator, qutebrowser, emulating mouse movements through key presses, and several others. But practically speaking, you will end up needing to use a mouse in a modern computing environment anyway. So after many attempts at overdoing it, I've settled back on a practical balance between keymap optimization and additions of a few mouse bindings. Below are the results of that journey.
A warning on practicality
The amount of time spent on this, is arguably not a worthwhile one. If someone else has already done the work for you, you can skip some of the trials and tribulations of those errors and get to something meaningful more quickly. However, the implementat of the concepts here tend to be very personal; specific to your own workflows and preferences. It will take a lot of work and especially time to arrive at something that feels good to you. I've invested time into tuning all of this over a decade because I enjoy it. However, the time spent on that probably is likely much more than the time I've saved. If you attempt to do the same, it will undoubtedly become a rabbit hole for you as well.
Objective
To achieve balance without excess as mentioned above, there are a few main principles I stick to:
- Portability - I want the setup to be easily transferrable between multiple computers and environments, so I choose to implement everything strictly with software and not rely on hardware, like specific keyboards. More on this in the Software section.
- Home row first - Try to put as many of the commonly used shortcuts into the home row to minimize finger travel distance.
- Reduce right hand movement - Allow for the most common tasks to be completed in both standard hand positions: Both hands on keyboard + Left hand on keyboard and right hand on mouse.
- Avoid conflicts - Many programs follow the same standard conventions on keybindings. The approaches below try to leverage those conventions while also staying out of the way of application specific shortcuts of software that I use most often.
Software
I acknowledge the allure of mechanical keyboards and their custom keys, layouts, and personalization software (QMK/VIA). But, I often take my laptop with me while traveling or even around the house. I don't want to have to carry around a keyboard with me or feel like I'm missing out on functionality when I don't have it with me. So I've chosen to stick to software solutions that work on any keyboard.
- Karabiner-Elements is my go-to workhorse for creating custom keybindings. It handles the basic remapping of the Caps Lock key to both Control and Escape, vim-like arrow keys, as well as mouse bindings for pausing music.
- Hammerspoon is what I use for basic window management.
- Raycast is a great replacement for the default Spotlight app on Macs. It also has a great plugin for switching Karabiner profiles.
- Yet Another Dotfiles Manager (yadm) is perfect for versioning and backing up configurations for everything mentioned above.
- HomeRow is a new addition that I primarily use for keyboard-based scrolling, but it also supports really powerful OS-level click navigation.
- JiTouch is used for navigating tabs with trackpad gestures.
Basics
Caps Lock → Ctrl/Esc
The first thing I do on any new computer is remap the caps lock key to ctrl and esc.
- ctrl triggers when caps lock is held and pressed with another key
- escape triggers when pressed and released on its own
Karabiner modification
1{2"description": "⇪ Caps Lock → [ Control with other keys, ESC if pressed alone ]",3"manipulators": [4{5"from": {6"key_code": "caps_lock",7"modifiers": { "optional": ["any"] }8},9"to": [{ "key_code": "left_control" }],10"to_if_alone": [{ "key_code": "escape" }],11"type": "basic"12}13]14}
Vim Arrow Keys
Now that we have the contorl key in the right place, the second-most used of all these bindings is putting arrow keys on the home row. I've found that it is quite reliable and doesn't conflict with existing key mappings.
Key | Description |
---|---|
ctrl h | Left |
ctrl j | Down |
ctrl k | Up |
ctrl l | Right |
Karabiner Elements Configuration:
1{2"description": "Control` `h/j/k/l to Arrows",3"manipulators": [45"from": {6"key_code": "h",7"modifiers": {8"mandatory": ["control"],9"optional": ["caps_lock"]10}11},12"to": [13{14"key_code": "left_arrow"15}16],17"type": "basic"18},1920"from": {21"key_code": "j",22"modifiers": {23"mandatory": ["control"],24"optional": ["caps_lock"]25}26},27"to": [28{29"key_code": "down_arrow"30}31],32"type": "basic"33},3435"from": {36"key_code": "k",37"modifiers": {38"mandatory": ["control"],39"optional": ["caps_lock"]40}41},42"to": [43{44"key_code": "up_arrow"45}46],47"type": "basic"48},4950"from": {51"key_code": "l",52"modifiers": {53"mandatory": ["control"],54"optional": ["caps_lock"]55}56},57"to": [58{59"key_code": "right_arrow"60}61],62"type": "basic"63}64]65}
Application Launching
For launching apps, I hold semicolon ; and tap a letter to open my most commonly used apps.
I chose ; as the meta key because it's easy to reach and doesn't conflict with any other shortcuts I use.
Also, the choice you pick here shouldn't often have another key that follows it naturally and rolls into another key. For example, another alpha character. Because it will trigger accidentally when you're typing other words quickly and press the next key before you raise the original.
Home Row
Key | Description |
---|---|
Home Row | |
; a | Zoom |
; s | Slack |
; d | Code |
; f | Fantastical |
; h | Home |
; j | 1Password |
Top Row | |
; e | Reminders |
; r | Brave |
; i | WezTerm - Read more here |
; o | Obsidian |
Bottom Row | |
; m | Music |
Karabiner Elements Configuration:
1{2"description": "Semicolon as modifier layer",3"manipulators": [4{5"from": {6"key_code": "semicolon"7},8"to": [9{10"set_variable": {11"name": "semicolon_modifier",12"value": 113}14}15],16"to_after_key_up": [17{18"set_variable": {19"name": "semicolon_modifier",20"value": 021}22}23],24"to_if_alone": [25{26"key_code": "semicolon"27}28],29"type": "basic"30},31{32"conditions": [33{34"name": "semicolon_modifier",35"type": "variable_if",36"value": 137}38],39"from": {40"key_code": "h"41},42"to": [43{44"shell_command": "open -a Home"45}46],47"type": "basic"48},49{50"conditions": [51{52"name": "semicolon_modifier",53"type": "variable_if",54"value": 155}56],57"from": {58"key_code": "m"59},60"to": [61{62"shell_command": "open -a Music"63}64],65"type": "basic"66},67{68"conditions": [69{70"name": "semicolon_modifier",71"type": "variable_if",72"value": 173}74],75"from": {76"key_code": "t"77},78"to": [79{80"shell_command": "open -a Telegram"81}82],83"type": "basic"84},85{86"conditions": [87{88"name": "semicolon_modifier",89"type": "variable_if",90"value": 191}92],93"from": {94"key_code": "r"95},96"to": [97{98"shell_command": "open -a Brave\\ Browser"99}100],101"type": "basic"102},103{104"conditions": [105{106"name": "semicolon_modifier",107"type": "variable_if",108"value": 1109}110],111"from": {112"key_code": "j"113},114"to": [115{116"shell_command": "open -a 1Password"117}118],119"type": "basic"120},121{122"conditions": [123{124"name": "semicolon_modifier",125"type": "variable_if",126"value": 1127}128],129"from": {130"key_code": "e"131},132"to": [133{134"shell_command": "open -a Reminders"135}136],137"type": "basic"138},139{140"conditions": [141{142"name": "semicolon_modifier",143"type": "variable_if",144"value": 1145}146],147"from": {148"key_code": "f"149},150"to": [151{152"shell_command": "open -a Fantastical"153}154],155"type": "basic"156},157{158"conditions": [159{160"name": "semicolon_modifier",161"type": "variable_if",162"value": 1163}164],165"from": {166"key_code": "d"167},168"to": [169{170"shell_command": "open -a Visual\\ Studio\\ Code\\ -\\ Insiders"171}172],173"type": "basic"174},175{176"conditions": [177{178"name": "semicolon_modifier",179"type": "variable_if",180"value": 1181}182],183"from": {184"key_code": "o"185},186"to": [187{188"shell_command": "open -a Obsidian"189}190],191"type": "basic"192},193{194"conditions": [195{196"name": "semicolon_modifier",197"type": "variable_if",198"value": 1199}200],201"from": {202"key_code": "i"203},204"to": [205{206"shell_command": "open -a iterm"207}208],209"type": "basic"210},211{212"conditions": [213{214"name": "semicolon_modifier",215"type": "variable_if",216"value": 1217}218],219"from": {220"key_code": "a"221},222"to": [223{224"shell_command": "open -a zoom.us"225}226],227"type": "basic"228},229{230"conditions": [231{232"name": "semicolon_modifier",233"type": "variable_if",234"value": 1235}236],237"from": {238"key_code": "s"239},240"to": [241{242"shell_command": "open -a slack"243}244],245"type": "basic"246}247]248}
Window Management
The next two are accomplished with a Hammerspoon script.
I've tried a bunch of different window management including tiling window managers like Aerospace and Yabai, quadrant-based tools like SizeUp, and other. But the only thing that's stuck for me is this basic grid-based script in Hammerspoon.
It's simple enough to use, but also flexible for a lot of the one-off use cases.
Resizing windows
Key | Description |
---|---|
ctrl cmd h | Shrink Left |
ctrl cmd j | Grow Down |
ctrl cmd k | Shrink Up |
ctrl cmd l | Grow Right |
Moving windows
Key | Description |
---|---|
ctrl opt h | Shrink Left |
ctrl opt j | Grow Down |
ctrl opt k | Shrink Up |
ctrl opt l | Grow Right |
Hammerspoon Configuration
1-- GRID2hs.window.animationDuration=0.23local hotkey = require "hs.hotkey"4local grid = require "hs.grid"56grid.MARGINX = 207grid.MARGINY = 208grid.GRIDHEIGHT = 49grid.GRIDWIDTH = 61011local mod_resize = {"ctrl", "cmd"}12local mod_move = {"ctrl", "alt"}1314-- Move Window15hotkey.bind(mod_move, 'j', grid.pushWindowDown)16hotkey.bind(mod_move, 'k', grid.pushWindowUp)17hotkey.bind(mod_move, 'h', grid.pushWindowLeft)18hotkey.bind(mod_move, 'l', grid.pushWindowRight)1920-- Resize Window21hotkey.bind(mod_resize, 'k', grid.resizeWindowShorter)22hotkey.bind(mod_resize, 'j', grid.resizeWindowTaller)23hotkey.bind(mod_resize, 'l', grid.resizeWindowWider)24hotkey.bind(mod_resize, 'h', grid.resizeWindowThinner)
Navigating Tabs
Tabs are already quite accessible with the nearly universal keyboard shortcut of cmd shift [ and cmd shift ].
To add compatibility with the principle of supporting both hand positions, I also change the forward/back buttons on the mouse to tab switching. I rarely use the default forward/back functionality on the mouse, and switching tabs is much more common for me.
Mouse Button | Remapped to |
---|---|
Mouse Forward | cmd shift [ (move left one tab) |
Mouse Backward | cmd shift ] (move right one tab) |
Karabiner Elements snippet:
1{2"description": "Mouse forward/back to switch tabs",3"manipulators": [4{5"from": {6"modifiers": {},7"pointing_button": "button4"8},9"to": [10{11"key_code": "close_bracket",12"modifiers": ["command", "shift"]13}14],15"type": "basic"16},17{18"from": {19"modifiers": {},20"pointing_button": "button5"21},22"to": [23{24"key_code": "open_bracket",25"modifiers": ["command", "shift"]26}27],28"type": "basic"29}30]31}
Navigating Text
These may not look or feel quite as natural, but they are the same bindings as in the terminal. Except, f and b are swapped for word jumps instead of character jumps.
Key | Description |
---|---|
ctrl a | Move to beginning of line (MacOS default) |
ctrl e | Move to end of line (MacOS default) |
ctrl f | Move forward one word (Overrides MacOS default of one char) |
ctrl b | Move backward one word (Overrides MacOS default of one char) |
ctrl w | Delete backward one word (Exclude terminals) |
1{2"description": "Control w/f/b - for text navigation",3"manipulators": [4{5"from": {6"key_code": "f",7"modifiers": { "mandatory": ["control"] }8},9"to": [10{11"key_code": "right_arrow",12"modifiers": ["option"]13}14],15"type": "basic"16},17{18"from": {19"key_code": "b",20"modifiers": { "mandatory": ["control"] }21},22"to": [23{24"key_code": "left_arrow",25"modifiers": ["option"]26}27],28"type": "basic"29},30{31"conditions": [32{33"bundle_identifiers": [34"^com\\.googlecode\\.iterm2$",35"^com\\.github\\.wez\\.wezterm$",36"^com\\.microsoft\\.VSCode$"37],38"type": "frontmost_application_unless"39}40],41"from": {42"key_code": "w",43"modifiers": { "mandatory": ["control"] }44},45"to": [46{47"key_code": "delete_or_backspace",48"modifiers": ["option"]49}50],51"type": "basic"52}53]54}
Navigating the Menu Bar
Accessing the menu bar from the keyboard is really handy for quick lookups such as calendar, weather, and system performance. The benefit of this is that you don't have to switch applications and can quickly get back to what you were doing because the popup will disappear.
Key | Description |
---|---|
ctrl cmd 7 | Fantastical Menu Bar |
ctrl cmd 8 | iStat Combined Menu Bar |
ctrl cmd 9 | iStat Calendar Menu Bar |
ctrl cmd 0 | iStat Weather Menu Bar |
ctrl cmd - | iStat Battery Menu Bar |
ctrl cmd = | MacOS Notification Center |
cmd ctrl i | MacOS Expose |
cmd ctrl o | MacOS Show Desktop |
Future Ideas
NixOS Darwin
Conclusion
As I mentioned above, these are just some of the things I've found useful, they are highly personal, and likely to change often. Let me know if you have any questions or comments. I'd love to hear about your setup and how you use it.