[Update: 13-Apr-2023 with some of the background context and some things I learned from hackernews]
[Update: 18-Dec-2023] This hidutil stopped working for me in MacOS 13.6 and 14.2. Also see other people having problems too.
[Update: 28-Jan-2024] Some people say hidutil works again in Mac OS 14.3.
On my Mac, I've used KeyRemap4Macbook, Karabiner, Karabiner Elements, and FunctionFlip for some of my key remapping needs. The main things I want:
- On the laptop keyboard, some media keys should be function keys but other media keys should stay media keys. When using fn I want to get the other version of the key.
- On external keyboards, make the function keys act like the laptop keyboard.
- On external keyboards, the numpad should act like "numlock off". For example, 4 should be Left Arrow.
- On external keyboards, the Windows key should act as Option, and the Alt key should act as Command. This can usually be set in the System Preferences. (Except it doesn't work on one of my keyboards)
- I also use those same external keyboards with my Windows and Linux machines, so I prefer to make these changes through software rather than firmware.
I recently learned about hidutil. It's built-in but has no user-friendly UI. Someone has created this config generator for it; I learned about it from Rakhesh Sasidharan's blog post. Using that generator, I can change a media/function key to do something else, but I can't seem to set fn+key. I instead learned how to do that from Adam Strzelecki's blog post. Also from the comments in Adam's blog post, I learned that there's a way to have a different configuration for each type of keyboard using the --matching flag.
I have three keyboards but at first I'm going to try using a single hidutil configuration for all three, and then later I will refine it as needed. I ended up writing a Python program to run hidutil with my desired configuration, a combination of the keys I can set using the config generator website and the keys I can set using Adam's blog post:
import subprocess, json output = '{"UserKeyMapping":[\n' output += """ {"HIDKeyboardModifierMappingSrc": 0x700000059, "HIDKeyboardModifierMappingDst": 0x70000004D }, {"HIDKeyboardModifierMappingSrc": 0x70000005A, "HIDKeyboardModifierMappingDst": 0x700000051 }, {"HIDKeyboardModifierMappingSrc": 0x70000005B, "HIDKeyboardModifierMappingDst": 0x70000004E }, {"HIDKeyboardModifierMappingSrc": 0x70000005C, "HIDKeyboardModifierMappingDst": 0x700000050 }, {"HIDKeyboardModifierMappingSrc": 0x70000005E, "HIDKeyboardModifierMappingDst": 0x70000004F }, {"HIDKeyboardModifierMappingSrc": 0x70000005F, "HIDKeyboardModifierMappingDst": 0x70000004A }, {"HIDKeyboardModifierMappingSrc": 0x700000060, "HIDKeyboardModifierMappingDst": 0x700000052 }, {"HIDKeyboardModifierMappingSrc": 0x700000061, "HIDKeyboardModifierMappingDst": 0x70000004B }, {"HIDKeyboardModifierMappingSrc": 0x700000063, "HIDKeyboardModifierMappingDst": 0x70000004C }, """ key_mappings = [] function_keys = subprocess.check_output( """ioreg -l | grep FnFunctionUsageMap | grep -Eo '0x[0-9a-fA-F]+,0x[0-9a-fA-F]+' """, shell=True).decode('utf-8').split('\n') for fn_key in [1, 2, 3, 10, 11, 12]: src_key, dst_key = function_keys[fn_key-1].replace("0x", "").split(",") src_key = '0x' + src_key[:4] + '0000' + src_key[4:] dst_key = '0x' + dst_key[:4] + '0000' + dst_key[4:] key_mappings.append((src_key, dst_key)) key_mappings.append((dst_key, src_key)) output += ',\n'.join([ ' {"HIDKeyboardModifierMappingSrc": ' + src + ', "HIDKeyboardModifierMappingDst": ' + dst + '}' for (src, dst) in key_mappings]) output += ']}' process = subprocess.run(["hidutil", "property", "--set", output], capture_output=True)
The script handles most of what I want. One remaining mystery is that the fn versions are incomplete — fn+F7, fn+F8, fn+F9 work for me as media keys, but fn+F4, fn+F5, fn+F6 don't work for me as apple-specific media keys. One missing feature is that on one external keyboard, I want to map the Windows key to Option, and the Alt key to Cmd. System Preferences sets these to right Option/Cmd, and I want the left keys to map to left keys and right keys to map to right keys. My workaround is to to it with firmware, and toggle that on/off when switching operating systems, but I'd like to figure out how to use hidutil to handle this too.
Other useful commands:
hidutil property --get "UserKeyMapping"
I currently run this manually on boot, but a launchctl file can be used to make it run automatically on boot. If using per-device configurations, they don't apply unless you run this while the keyboard is plugged in. That's inconvenient. Digi Hunch's blog post covers how to automatically load that configuration when the keyboard is plugged in.