Back to posts
MacOS Keymap Guide

MacOS Keymap Guide

My full configuration of key mappings across MacOS, enabling a mostly keyboard focused experience.

Justin Wyne / November 23, 2023

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

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.

`1234567890-=deletetabQWERTYUIOP[]\ctrl / escASDFGHJKL;'returnshiftZXCVBNM,./shiftfnctrloptcmdspacecmdopt
KeyDescription
ctrl hLeft
ctrl jDown
ctrl kUp
ctrl lRight

Karabiner Elements Configuration:

karabiner.json 
1
{
2
"description": "Control` `h/j/k/l to Arrows",
3
"manipulators": [
4
5
"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
},
19
20
"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
},
34
35
"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
},
49
50
"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.

`1234567890-=deletetabQWERTYUIOP[]\ctrl / escASDFGHJKL;'returnshiftZXCVBNM,./shiftfnctrloptcmdspacecmdopt

Home Row

KeyDescription
Home Row
; aZoom
; sSlack
; dCode
; fFantastical
; hHome
; j1Password
Top Row
; eReminders
; rBrave
; iWezTerm - Read more here
; oObsidian
Bottom Row
; mMusic

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": 1
13
}
14
}
15
],
16
"to_after_key_up": [
17
{
18
"set_variable": {
19
"name": "semicolon_modifier",
20
"value": 0
21
}
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": 1
37
}
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": 1
55
}
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": 1
73
}
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": 1
91
}
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": 1
109
}
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": 1
127
}
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": 1
145
}
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": 1
163
}
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": 1
181
}
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": 1
199
}
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": 1
217
}
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": 1
235
}
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

`1234567890-=deletetabQWERTYUIOP[]\ctrl / escASDFGHJKL;'returnshiftZXCVBNM,./shiftfnctrloptcmdspacecmdopt
KeyDescription
ctrl cmd hShrink Left
ctrl cmd jGrow Down
ctrl cmd kShrink Up
ctrl cmd lGrow Right

Moving windows

`1234567890-=deletetabQWERTYUIOP[]\ctrl / escASDFGHJKL;'returnshiftZXCVBNM,./shiftfnctrloptcmdspacecmdopt
KeyDescription
ctrl opt hShrink Left
ctrl opt jGrow Down
ctrl opt kShrink Up
ctrl opt lGrow Right

Hammerspoon Configuration

init.lua 
1
-- GRID
2
hs.window.animationDuration=0.2
3
local hotkey = require "hs.hotkey"
4
local grid = require "hs.grid"
5
6
grid.MARGINX = 20
7
grid.MARGINY = 20
8
grid.GRIDHEIGHT = 4
9
grid.GRIDWIDTH = 6
10
11
local mod_resize = {"ctrl", "cmd"}
12
local mod_move = {"ctrl", "alt"}
13
14
-- Move Window
15
hotkey.bind(mod_move, 'j', grid.pushWindowDown)
16
hotkey.bind(mod_move, 'k', grid.pushWindowUp)
17
hotkey.bind(mod_move, 'h', grid.pushWindowLeft)
18
hotkey.bind(mod_move, 'l', grid.pushWindowRight)
19
20
-- Resize Window
21
hotkey.bind(mod_resize, 'k', grid.resizeWindowShorter)
22
hotkey.bind(mod_resize, 'j', grid.resizeWindowTaller)
23
hotkey.bind(mod_resize, 'l', grid.resizeWindowWider)
24
hotkey.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 ButtonRemapped to
Mouse Forwardcmd shift [
(move left one tab)
Mouse Backwardcmd 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.

`1234567890-=deletetabQWERTYUIOP[]\ctrl / escASDFGHJKL;'returnshiftZXCVBNM,./shiftfnctrloptcmdspacecmdopt
KeyDescription
ctrl aMove to beginning of line
(MacOS default)
ctrl eMove to end of line
(MacOS default)
ctrl fMove forward one word
(Overrides MacOS default of one char)
ctrl bMove backward one word
(Overrides MacOS default of one char)
ctrl wDelete backward one word
(Exclude terminals)
karabiner.json 
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.

`1234567890-=deletetabQWERTYUIOP[]\ctrl / escASDFGHJKL;'returnshiftZXCVBNM,./shiftfnctrloptcmdspacecmdopt
KeyDescription
ctrl cmd 7Fantastical Menu Bar
ctrl cmd 8iStat Combined Menu Bar
ctrl cmd 9iStat Calendar Menu Bar
ctrl cmd 0iStat Weather Menu Bar
ctrl cmd -iStat Battery Menu Bar
ctrl cmd =MacOS Notification Center
cmd ctrl iMacOS Expose
cmd ctrl oMacOS Show Desktop

Settings

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.