Skip to content

Instantly share code, notes, and snippets.

@paultheman
Last active July 6, 2024 23:13
Show Gist options
  • Save paultheman/808be117d447c490a29d6405975d41bd to your computer and use it in GitHub Desktop.
Save paultheman/808be117d447c490a29d6405975d41bd to your computer and use it in GitHub Desktop.
Guide on how to remap Keyboard keys on macOS

Guide on how to remap Keyboard keys on macOS

Update: since macOS 14.2 hidutil requires root privileges.

If you have a mac with an INT (ISO) keyboard you might want to change the ± key to ~. During my research I found that the information on this topic is not at all centralized. I prefer this option because it does not involve installing new software.

With macOS 10.12 Sierra Apple introduced hidutil as a tool to remap keyboard keys. See TN2450.

1. Lets list our HID devices:

hidutil list

You will get something similar as below. See the "VendorID" and "ProductID". In this example we will be modifying the internal MacBook keyboard. hidutil_list

2. hidutil offers us a --match option which I advise you to use

If you don't use the --match option you will remap all the keyboards, which I doubt you want. The syntax is as follows:

hidutil property --match '{"VendorID":0x5ac,"ProductID":0x342}' --get 'UserKeyMapping'

If no 'UserKeyMapping' dictionary is defined the values are NULL, otherwise you will receive the Src and Dst of the keys (values are decimal):

hidutil_match_get

3. Use the hidutil-generator to see the codes that you would like to change

This also works for fn key and VolumeUp VolumeDown. If you have another device, that's not a keyboard but sends HID keyboard signals you can remap these also!

4. Test your new keymapping

sudo hidutil property --match '{"VendorID":0x5ac,"ProductID":0x342}' --set '{"UserKeyMapping":[{"HIDKeyboardModifierMappingSrc":0x700000064,"HIDKeyboardModifierMappingDst":0x700000035},{"HIDKeyboardModifierMappingSrc":0x700000035,"HIDKeyboardModifierMappingDst":0x7000000E1}]}'

Remember you can always go back to default using --set '{"UserKeyMapping":[]}'

5. Once you are happy with the changes you will need to create a launchagent so the UserKeyMapping is applied at every login

5.1 Paste the code from the hidutil-generator into a file named com.local.KeyRemapping.plist at /Users/[USERNAME]/Library/LaunchAgents/com.local.KeyRemapping.plist

IMPORTANT If you are remapping a bluetooth keyboard you will need to add some lines to the plist file. See here and below for a removable keyboard.

<?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>local.KeyRemapping</string>
    <key>LaunchEvents</key>
    <dict>
        <key>com.apple.iokit.matching</key>
        <dict>
            <key>com.apple.bluetooth.hostController</key>
            <dict>
                <key>IOProviderClass</key>
                <string>IOBluetoothHCIController</string>
                <key>idProduct</key>
                <integer>B319</integer>
                <key>idVendor</key>
                <integer>046D</integer>
                <key>IOMatchLaunchStream</key>
                <true/>
            </dict>
        </dict>
    </dict> 
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/sudo</string>
        <string>/usr/bin/hidutil</string>
        <string>property</string>
        <string>--matching</string>
        <string>{"ProductID":0xB319,"VendorID":0x046D}</string>
        <string>--set</string>
        <string>{
            "UserKeyMapping": [
                { 
                    "HIDKeyboardModifierMappingSrc":0x7000000E2,
                    "HIDKeyboardModifierMappingDst":0x7000000E3
                },
                { 
                    "HIDKeyboardModifierMappingSrc":0x7000000E6,
                    "HIDKeyboardModifierMappingDst":0x7000000E7
                }
            ]
        }</string>
    </array>

    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

If it is a removable keyboard you need to add the following:

<key>LaunchEvents</key> 
    <dict>
    <key>com.apple.iokit.matching</key>
    <dict>
        <key>com.apple.usb.device</key>
        <dict>
            <key>IOProviderClass</key>
            <string>IOUSBDevice</string> 
            <key>idProduct</key>
            <string>*</string>
            <key>idVendor</key>
            <string>*</string>
            <key>IOMatchLaunchStream</key>
        <true/>
        </dict>
    </dict>
</dict>
<key>ProgramArguments</key>
...   

5.2 Create the launch agent, here is the new syntax for it:

launchctl bootstrap gui/<user's UID> /Users/[USERNAME]/Library/LaunchAgents/com.local.KeyRemapping.plist
To get your UID type echo $(id) or id -u <username>

The most complete guide for launchctl that I found is here.
Check it if you ever want to bootout/unload the launch agent.
The explanations for the domain are here:
system is privileged (runs services as root) and requires root for interaction
user/<uid> runs as that user, but does not require that the user be logged in
gui/<uid> runs as that user, but is only active when the user is logged in at the GUI

5.3 Edit the sudoers file to allow the LaunchAgent to run as root

Since macOS 14.2 hidutil needs root privileges for key remapping
sudo visudo
add the following line
<USERNAME> ALL = (root) NOPASSWD: /usr/bin/hidutil


How it works

With the goal of also being able to remap Mouse buttons or Scroll wheel I investigated further the remapping mechanism. Unfortunately I was unable to find a solution to remapping anything other than keys.

The code for IOHIDFamily is open source on github.
Here we can see the folllwing function Key IOHIDKeyboardFilter::remapKey(Key key) in IOHIDFamily/IOHIDEventSystemPlugIns /IOHIDKeyboardFilter.mm, according to Adam Strzelecki this function seems to do the actual remapping.

So, for hidutil we have the MSB specifying the Usage Page of the HID Usage Tables. For example the 0x7 points to the KeyboardOrKeypad Page. Apple VendorDefined Usage Tables are here. If we would like to remap apple specific keys we would have to use the 0xFF01 as MSB.
Example (F4 is remapped as Dashboard):
{"HIDKeyboardModifierMappingSrc":0x00070000003d, "HIDKeyboardModifierMappingDst":0xff0100000002}

It seems that my mouse is identified as 'Usage Page' 1 'Usage' 2 (kHIDUsage_GD_Mouse) and also 'Usage Page' 12 (0x0C) 'Usage' 1 (kHIDUsage_Csmr_ConsumerControl). We can see this if we run hidutil list --match '{"VendorID":0x46d, "ProductID":0xc526}'. I believe kHIDUsage_Csmr_ConsumerControl is the horizontal scroll that's available for my mouse.

mouse

Although hidutil offers a dump option for system, clients and services I was unable make very much sense of it.

In order to list the HID descriptor I used osx-hid-inspector you can find a binary in the dist folder.

osx-hid-inspector

kHIDUsage_GD_X and kHIDUsage_GD_Y seem to be the Mouse X and Y positions.
kHIDUsage_GD_Wheel is the vertical scroll wheel.
kHIDUsage_Csmr_ACPan is the horizontal scroll.
usage_page 9 is referring to kHIDUsage_Button_x basically buttons 1 to 16. I only identified 4 buttons.

I wanted to get a look with Wireshark at how the HID data looks. Here is what I found:
The keyboard key codes are matching with the HID Usage Tables.
This is LSB first so our number is 0x40000. Actually it's not... during the HID handshake it is specified which bytes should be read and how many, in our case we are interested only in the 04 data from a maximum of 2 bytes. This matches precisely with the HID specification 0x04 = a or A.

kbd_wireshark

Regarding the mouse trace I was able to identify the following:
0x01 -> Left Mouse Button
0x02 -> Right Mouse Button
0x03 -> Both Buttons pressed
0x04 -> Middle Mouse Button

The 0x38 Wheel seems to give the following values:
0x01 -> Scroll Up (Value is signed, only 7 bits are relevant meaning 127)
0xFF -> Scroll Down (Value is signed, only 7 bits are relevant meaning -127)

According to the HID Usage tables:

Wheel Control is defined as below:
Usage Page (Generic Desktop) (0x01)
Usage (Wheel) (0x38)
Logical Minimum (-127)
Logical Maximum (127)
Report Count (1)
Report Size (8)
Input (Data, Var, Rel)

The 0x238 AC Pan according to the HID Tables is a Linear Control (LC) "Set the horizontal offset of the display in the document."

The raw values are similar to the ones for the Wheel:
0x01 -> Scroll right
0xFF -> Scroll left

Here are some trace screenshots:

Scroll_Down

Scroll_L

LR_mb


Final Words

If you would like to get a quick look at keyboard/mouse button codes you could run sniffMK. I was however unable to get the Scroll codes from it. Before I tried Karabiner Elements but it also did not output the Scroll events.

The source code for hidutil is on github.
It seems that there are other options that can be used but require the tool to be compiled with #define internal macro.

const char monitorUsage[] =
"\nMonitor HID Event System events\n"
"\nUsage:\n\n"
"  hidutil monitor [ --predicate <predicate> ][ --show <varibles> ][ --type <event type> ][ --matching <matching> ][ --children ]\n"
"\nFlags:\n\n"
"  -p  --predicate.............filter events output based on predicate\n"
"  -s  --show..................show only specified variables\n"
"  -t  --type..................filter by event type, takes integer or string\n"
"  -c  --children..............show child events\n"
"  -v  --verbose...............print service enumerated/terminated info\n"
"  -o  --output................output to file\n"
"      --client................event system clien type (admin, passive, ratecontrolled, simple, monitor)\n"
"      --serialize.............output events in serialized binary format\n"

MATCHING_HELP
"\nExamples:\n\n"
"  hidutil monitor --info keyboard\n"
"  hidutil monitor --type keyboard\n"
"  hidutil monitor --predicate 'typestr contains \"digit\"' --children\n"
"  hidutil monitor --predicate 'usagepage == 7 and latency > 100'\n"
"  hidutil monitor --show 'timestamp sender typestr usagepage usage'\n"
"  hidutil monitor --matching '{\"ProductID\":0x54c,\"VendorID\":746}'\n";
const char reportUsage[] =
"\nMonitor HID device reports\n"
"\nUsage:\n\n"
"  hidutil report [--get <reportID> ][ --set <reportID> <bytes> ][ --type <reportType> ][ --matching <matching> ] [ --verbose ]\n"
"\nFlags:\n\n"
"  -g  --get...................get report for report ID. Use --type to specify report type\n"
"  -s  --set...................set report with report ID and data. Use --type to specify report type\n"
"  -t  --type..................report type (input, output, feature), defaults to feature\n"
"  -v  --verbose...............print device enumerated/terminated info\n"
MATCHING_HELP
"\nExamples:\n\n"
"  hidutil report\n"
"  hidutil report --get 01 --type feature\n"
"  hidutil report --set 02 0a 0b 0c --type output\n"
"  hidutil report --matching '{\"PrimaryUsagePage\":1,\"PrimaryUsage\":6}'\n";

The following points remain open:

  • Compile hidutil with #define internal
  • Find out how the 0x38 Wheel is parsed... the byte 0x38 is not found in the Wireshark trace
  • Is it even possible to remap the Scroll/Mouse buttons using hidutil?
@paultheman
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment