
MacOS Keymap Guide
My full configuration of key mappings across MacOS, enabling a mostly keyboard focused experience.
Justin Wyne / November 22, 2023 / Updated March 22, 2026
Ever since I learned Vim in my college operating systems course, I've been hooked on keyboard efficiency. Vim handles text editing beautifully, but that desire for keyboard-only speed naturally expanded to the rest of my computing.
Over the years I went overboard trying to eliminate the mouse entirely; browser extensions like cvim, vimperator, and qutebrowser, emulating mouse movements through key presses, and more. But a mouse is unavoidable in a modern computing environment. After many attempts at overdoing it, I've settled on a practical balance between keyboard optimization and a handful of mouse bindings. Below are the results of that journey.
A word of warning: the time invested in building a setup like this almost certainly exceeds the time it saves. These configurations are also deeply personal; what works for my workflows may not suit yours. I've spent a decade tuning this because I genuinely enjoy it, not because it's efficient. If you go down this path, expect a rabbit hole.
Objective
A few principles guide the setup:
- Portability: Everything is implemented in software, not hardware. I move between machines often and don't want to depend on a specific keyboard. See the Software section for more.
- Home row first: Keep the most common shortcuts within reach of the home row to minimize finger travel.
- Support both hand positions: Common tasks should work whether both hands are on the keyboard, or the left hand is on the keyboard and the right is on the mouse.
- Avoid conflicts: Build on existing conventions rather than fighting them, and stay clear of app-specific shortcuts.
Software
Mechanical keyboards and their customization ecosystems (QMK/VIA) are appealing, but I move around a lot and don't want to depend on a specific keyboard. Everything here works on the built-in laptop keyboard.
- Karabiner-Elements: the workhorse. Handles key remapping, modifier layers, and mouse button remapping.
- Hammerspoon: scriptable macOS automation, used here for window management.
- Raycast: a Spotlight replacement with a great plugin for switching Karabiner profiles.
- yadm: dotfile manager for versioning and syncing all of the above across machines.
- HomeRow: primarily for keyboard-based scrolling, though it also supports powerful OS-level click navigation.
- JiTouch: trackpad gestures for tab navigation.
Basics
Caps Lock → Ctrl/Esc
The first thing I do on any new computer is remap caps lock to dual-purpose ctrl/esc; and also apply the same behavior to the physical left ctrl key itself.
- Held with another key → ctrl
- Pressed and released alone → esc
This means both caps lock and ctrl can fire escape, which is useful coming from Vim where you hit escape constantly.
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
With control in the right place, the next most impactful change is vim-style arrow keys on the home row. ctrl+hjkl is surprisingly conflict-free across most apps.
| Key | Description |
|---|---|
| ctrl h | Left |
| ctrl j | Down |
| ctrl k | Up |
| ctrl l | Right |
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}
Forward Delete
Press = and delete simultaneously to produce a forward delete. Laptop keyboards don't have a dedicated forward delete key, and reaching for fn+delete is awkward. This chord puts it in a reachable spot.
Application Launching
Hold ; and tap a letter to launch any of my most-used apps.
; works well as a modifier for a few reasons: it's easy to reach from the home row, it doesn't conflict with existing shortcuts, and it rarely rolls into another key in natural typing. If your chosen key commonly precedes another character, you'll get false triggers when typing quickly.
| Key | Description |
|---|---|
| Home Row | |
| ; a | Zoom |
| ; s | Slack |
| ; d | VS Code |
| ; f | Fantastical |
| ; g | Forklift |
| ; h | Home |
| ; j | 1Password |
| ; n | Notes |
| Top Row | |
| ; e | Reminders |
| ; r | Arc |
| ; t | Telegram |
| ; i | Ghostty |
| ; o | Obsidian |
| ; u | Music |
| Bottom Row | |
| ; m | Messages |
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
I've tried a lot of window management tools; tiling managers like Aerospace and Yabai, quadrant-based tools like SizeUp, and others. The only thing that's stuck is a simple grid-based Hammerspoon script. It handles the common cases without any ceremony, and it's flexible enough for one-offs.
Resizing windows
Moving windows
| Key | Description |
|---|---|
| ctrl cmd h | Shrink Left |
| ctrl cmd j | Grow Down |
| ctrl cmd k | Shrink Up |
| ctrl cmd l | Grow Right |
| Key | Description |
|---|---|
| ctrl opt h | Nudge Left |
| ctrl opt j | Nudge Down |
| ctrl opt k | Nudge Up |
| ctrl opt l | Nudge Right |
1-- GRID2hs.window.animationDuration=0.23local hotkey = require "hs.hotkey"4local grid = require "hs.grid"56grid.MARGINX = 20 grid.MARGINY = 20 grid.GRIDHEIGHT = 4 grid.GRIDWIDTH = 678local mod_resize = {"ctrl", "cmd"} local mod_move = {"ctrl", "alt"}910-- Move Window hotkey.bind(mod_move, 'j', grid.pushWindowDown)11hotkey.bind(mod_move, 'k', grid.pushWindowUp) hotkey.bind(mod_move, 'h',12grid.pushWindowLeft) hotkey.bind(mod_move, 'l', grid.pushWindowRight)1314-- Resize Window hotkey.bind(mod_resize, 'k', grid.resizeWindowShorter)15hotkey.bind(mod_resize, 'j', grid.resizeWindowTaller) hotkey.bind(mod_resize,16'l', grid.resizeWindowWider) hotkey.bind(mod_resize, 'h',17grid.resizeWindowThinner)18
Navigating Tabs
cmd shift [ and cmd shift ] already work universally for tab switching. For right-hand-on-mouse compatibility, I remap the mouse's forward/back buttons to those same shortcuts. I rarely use forward/back navigation, but switching tabs is constant.
| Mouse Button | Remapped to |
|---|---|
| Mouse Forward | cmd shift [ (move left one tab) |
| Mouse Backward | cmd shift ] (move right one tab) |
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}
Tab Key Modifier Layer
tab also acts as a modifier. Hold it and press h/l to switch tabs; same result as the mouse buttons, but without leaving the keyboard.
- tab alone still sends tab
- tab + h → previous tab
- tab + l → next tab
Trackpad Gestures
JiTouch adds gesture recognition on top of the standard trackpad. "One-Fix" means one finger resting (anchoring) while another performs the gesture.
One-Fix Left-Tap
One-Fix Right-Tap
Two-Fix Double-Tap
| Gesture | Action |
|---|---|
| One-Fix Left-Tap | Previous tab (⇧⌘[) |
| One-Fix Right-Tap | Next tab (⇧⌘]) |
| Two-Fix Index-Double-Tap | Refresh |
The tab gestures mirror the mouse button remapping and keyboard Tab modifier. All three input methods produce the same result, so whichever hand position you're in, tab switching is always one motion away.
Zoom
In Zoom, the mouse forward/back buttons are remapped to mute/unmute audio and video instead of tab switching since you're rarely switching tabs during a call.
| Mouse Button | Action |
|---|---|
| Mouse Forward | Toggle video (cmd shift v) |
| Mouse Backward | Toggle audio (cmd shift a) |
Navigating Text
These mirror terminal readline bindings. The main change: ctrl+f and ctrl+b are mapped to word jumps (opt+arrow) instead of the macOS default of single-character moves, which I find far more useful.
| 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\\.mitchellh\\.ghostty$",37"^com\\.microsoft\\.VSCode$"38],39"type": "frontmost_application_unless"40}41],42"from": {43"key_code": "w",44"modifiers": { "mandatory": ["control"] }45},46"to": [47{48"key_code": "delete_or_backspace",49"modifiers": ["option"]50}51],52"type": "basic"53}54]55}
Navigating the Menu Bar
Keyboard shortcuts for menu bar items are great for quick lookups like calendar, weather, system stats without switching away from your current app. The popup disappears as soon as you're done.
These shortcuts are set directly within each app's preferences (Fantastical, iStat Menus) or in System Settings → Keyboard → Keyboard Shortcuts → App Shortcuts for system-level ones.
| 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 |

Conclusion
This setup is personal and always evolving. If you have questions, suggestions, or a setup of your own, I'd love to hear about it.