macOS function key remapping with hidutil

Windows vs Mac right Alt

Starting with 10.12 Sierra, macOS comes with the hidden and surprisingly flexible feature allowing remapping arbitrary keyboard keys. This feature is not exposed anywhere in the system UI. Yet it is available to everyone via simple hidutil built-in command controlling IOHIDFamily driver, requiring no hardware modifications or tampering with System Integrity Protection.

I use Mac at home, Windows at work and I often interact with Linux. I use function key shortcuts quite a lot in various file managers and programming IDEs. In the same time, I want my screen brightness to be adjustable on my Mac with a single key press. Finally, on top of that I often type Polish text, which requires using right alt that is hardly reachable with the right thumb as it is farther right than on PC/Windows keyboard. Yes, I am quite demanding.

All what I want could be expressed by the following Mac key remapping, that:

  1. Keeps F1 and F2 for brightness adjustment, but leaves other function keys accessible directly, without fn,
  2. Remaps right to option, so Mac letter modifier is at same place as on PC/Windows keyboard, closer to the space key and much easier reachable with the right thumb.

Function keys on macOS

Magic Keyboard function keys Figure 1: Apple Magic Keyboard function keys depicted in How to use the function keys on your Mac.

By default on macOS, Apple keyboard function keys act as media/OS control keys that modify screen brightness, volume, etc. Standard function keys are accessible holding fn modifier. This is not a problem when working with native macOS apps, rarely using F1-F12, but becomes cumbersome when using some cross platform software, or interacting remotely with the other operating systems and applications relying a lot on standard function keys.

fn behavior can be reversed with System Preferences “Use F1, F2, etc. keys as standard function keys” switch, that makes standard function key behavior primary and media/OS behavior secondary, hence requiring fn modifier. Unfortunately, there is no switch that lets you adjust this behavior individually for the each function key.

There are some macOS applications addressing this, e.g. Function Flip or making function key behavior application specific e.g. Fluor. There are also some programs that let you remap other individual keys e.g. keyremapd of mine.

All of them use rather tricky macOS system level event hooks, and obviously require some helper application running in the background. So far I relied on my own keyremapd solution, but with recent macOS releases it began to act in a quite erratic way. Therefore, without further ado, I started to look for some alternatives.

IOHIDKeyboardFilter and hidutil

macOS since 10.12 Sierra provides quite interesting low-level built-in feature allowing to create custom keyboard remapping without a need for additional software. Everything is a part of IOHIDFamily driver and is nicely described by a Tech Note. Additionally, this driver is open-source, so the underlying details are freely accessible reading IOHIDKeyboardFilter.mm source code.

I believe this feature exists thanks to some smart Apple engineers who are power-users at the same time and realize that a simple System Preferences switch may be not enough for everyone. There is no system UI that exposes this custom keyboard remapping feature though.

Interestingly, both custom remapping and function key mapping with fn happens essentially in the same place in IOHIDKeyboardFilter.mm IOHIDKeyboardFilter::remapKey. Everything is driven by the magic HID page and usage numbers - expressed with 64-bit (or 32-bit) codes. Internally, remapKey applies the mapping according to FnFunctionUsageMap table exposed by the attached USB HID compatible keyboard. This table can be printed with:

$ ioreg -l|grep FnFunctionUsageMap|grep -Eo 0x[0-9a-fA-F]+,0x[0-9a-fA-F]+
0x0007003a,0xff010021 # F1
0x0007003b,0xff010020 # F2
0x0007003c,0xff010010 # F3
0x0007003d,0xff010002 # F4
0x00070040,0x000C00B4 # F7
0x00070041,0x000C00CD # F8
0x00070042,0x000C00B3 # F9
0x00070043,0x000C00E2 # F10
0x00070044,0x000C00EA # F11
0x00070045,0x000C00E9 # F12

NOTE: Output above is for 1st gen Apple Magic Keyboard. Comments starting with # added by me for extra readability. Other keyboards can produce different output.

Left column is 32-bit HID function key code that consists of upper 16-bit HID page number (key group), lower 16-bit HID usage number (individual key code). Right column is 32-bit media key code, also composed of two 16-bit HID page and usage numbers. Meaning of these codes can be found e.g. in FreeBSD USB HID usage table.

Depending on fn state, the left or right key code is emitted by the driver. Everything happens in the software and can be later further customized by UserKeyMapping that is applied right after the fn mapping in IOHIDKeyboardFilter.mm IOHIDKeyboardFilter::remapKey function.

UserKeyMapping details

Custom user key mapping can be provided to IOHIDFamily driver from command line using macOS built-in hidutil e.g. as described in the Tech Note:

$ hidutil property --set '{"UserKeyMapping":[
{"HIDKeyboardModifierMappingSrc":0x700000004,"HIDKeyboardModifierMappingDst":0x700000005},
{"HIDKeyboardModifierMappingSrc":0x700000005,"HIDKeyboardModifierMappingDst":0x700000004}]}'

The example mapping above swaps A and B keys, mapping AB (1st mapping) and BA (2nd mapping). This is very simple example, but UserKeyMapping supports any one-to-one mapping that can be expressed using USB HID usage table codes.

NOTE: UserKeyMapping uses 64-bit values (rather than 32-bit as UserKeyMapping). Upper 32-bit part is HID page number, lower 32-bit is HID usage number. Therefore, 32-bit values provided by FnFunctionUsageMap have to be padded with extra zeros up to 64-bit number.

All above leads me to the following mapping satisfying my demands stated in the beginning:

# Right Command -> Right Alt
# Fn+F3-F4,F7-F10 <-> F3-F4,F7-F10 on Apple Magic Keyboard gen 1
hidutil property --set '{"UserKeyMapping":[
{"HIDKeyboardModifierMappingSrc":0x0007000000e7,
 "HIDKeyboardModifierMappingDst":0x0007000000e6},

{"HIDKeyboardModifierMappingSrc":0x00070000003c,
 "HIDKeyboardModifierMappingDst":0xff0100000010},
{"HIDKeyboardModifierMappingSrc":0xff0100000010,
 "HIDKeyboardModifierMappingDst":0x00070000003c},

{"HIDKeyboardModifierMappingSrc":0x00070000003d,
 "HIDKeyboardModifierMappingDst":0xff0100000002},
{"HIDKeyboardModifierMappingSrc":0xff0100000002,
 "HIDKeyboardModifierMappingDst":0x00070000003d},

{"HIDKeyboardModifierMappingSrc":0x000700000040,
 "HIDKeyboardModifierMappingDst":0x000C000000B4},
{"HIDKeyboardModifierMappingSrc":0x000C000000B4,
 "HIDKeyboardModifierMappingDst":0x000700000040},

{"HIDKeyboardModifierMappingSrc":0x000700000041,
 "HIDKeyboardModifierMappingDst":0x000C000000CD},
{"HIDKeyboardModifierMappingSrc":0x000C000000CD,
 "HIDKeyboardModifierMappingDst":0x000700000041},

{"HIDKeyboardModifierMappingSrc":0x000700000042,
 "HIDKeyboardModifierMappingDst":0x000C000000B3},
{"HIDKeyboardModifierMappingSrc":0x000C000000B3,
 "HIDKeyboardModifierMappingDst":0x000700000042},

{"HIDKeyboardModifierMappingSrc":0x000700000043,
 "HIDKeyboardModifierMappingDst":0x000C000000E2},
{"HIDKeyboardModifierMappingSrc":0x000C000000E2,
 "HIDKeyboardModifierMappingDst":0x000700000043},

{"HIDKeyboardModifierMappingSrc":0x000700000044,
 "HIDKeyboardModifierMappingDst":0x000C000000EA},
{"HIDKeyboardModifierMappingSrc":0x000C000000EA,
 "HIDKeyboardModifierMappingDst":0x000700000044},

{"HIDKeyboardModifierMappingSrc":0x000700000045,
 "HIDKeyboardModifierMappingDst":0x000C000000E9},
{"HIDKeyboardModifierMappingSrc":0x000C000000E9,
 "HIDKeyboardModifierMappingDst":0x000700000045},
]}'

NOTE: This recipe is quite lengthy as I remap each media and function key in both directions.

Such mapping is effective until next reboot, or hidutil call, to make it persistent e.g. active after login, following ~/Library/LaunchAgents/com.nanoant.KeyRemapping.plist launch agent can be used:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.nanoant.KeyRemapping</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/hidutil</string>
        <string>property</string>
        <string>--set</string>
        <string>{"UserKeyMapping":[<!-- put mapping list here -->]}</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

Summary

Originally I wanted to put “easy” in the title of this post, but I am not sure if this solution is really straightforward to everyone. But I believe this is something that should be relatively clear to any programmer or engineer working with various number representations and familiar with command line environment. Personally I find it much more robust than using any third party software. And as long UserKeyMapping remains in IOHIDFamily driver, that remains open-source, this is my preferred approach.

I am aware I am not the first one that “discovers” this nice built-in macOS feature. Nevertheless, I decided to write this “longish” explanation in hope that someone else finds it helpful. I will also appreciate any feedback in the comments section below.