[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:

  1. 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.
  2. On external keyboards, make the function keys act like the laptop keyboard.
  3. On external keyboards, the numpad should act like "numlock off". For example, 4 should be Left Arrow.
  4. 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)
  5. 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'
# First put in the numpad keys, which I got from 
# https://hidutil-generator.netlify.app/
# https://www.freebsddiary.org/APC/usb_hid_usages.php
# also see https://github.com/ivangreene/keymap
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
    },
"""

# Function keys:
# 1. set "Use F1, F2, etc. keys as standard function keys"
# 2. turn f1, f2, f3, and f10, f11, f12 into media keys
# 3. fn should reverse the meaning so fn+f1 will be f1 not media
key_mappings = [] # [src, dst]

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 += ']}'

# print(output)
process = subprocess.run(["hidutil", "property", "--set", output],
                         capture_output=True)
# print(process.stdout)

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:

# see the current mapping; optionally use --matching
# but they will be decimal instead of hexadecimal
hidutil property --get "UserKeyMapping"

# erase the current mapping; optionally use --matching
# hidutil property --set '{"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.

Labels: ,

3 comments:

Pete wrote at Thursday, April 13, 2023 at 11:01:00 AM PDT

Serious question:

How much time would you say you have spent fiddling with key remapping as compared to the time you would have spent just reaching your pinky to the bottom left whenever you need an alternate function key action? Is the ROI really there?

Also, how lost are you when you use someone else's laptop or keyboard and it doesn't act like yours?

The world is moving towards a "servers are cattle not pets" mindset, but I find many of the devs leading that push turn their laptop into pets with such fervor that they're nearly non-functional if they are ever forced to start over and lose all the doodads and whizzbangs they're accustomed to. Beware that trap.

Anonymous wrote at Thursday, April 13, 2023 at 11:09:00 AM PDT

Amit is saving the rest of us time and discomfort too. thank you!

Amit wrote at Thursday, April 13, 2023 at 12:54:00 PM PDT

Pete: it's a good question! I'm not expecting my configuration tweaking to save time. I think my configuring uses a whole lot of time.

In this case, I use several keyboards, and they all have keys in different places. I'm trying to make them work more similarly, because I find it more comfortable. I don't want to be looking at the keyboard every time I want to press Option or Command or Volume Up.

I don't feel particularly lost at someone else's laptop (something I do very rarely). I go back to looking for the Volume Up key.

I'm the same way with my home. I'll put my soap or towel in a certain place, not because it's the same as everyone else's house, but because that's where I want it to be. I know the world is moving towards everyone having identical homes and identical cars etc, and that might be more efficient, but I am not trying to live my life to maximize efficiency. If I'm in somebody else's house or in a hotel room, I'll look around for the towel. I haven't found it to be a big problem, and I don't find myself "non-functional" if the towel or soap or something else is in a different place.