Keyboard with wrong keys

Key mapping for external PC keyboard on Mac

This post is to document my steps to use external Windows keyboard on Mac with custom key mapping.

Background

Apple’s magic keyboard does not support multi-device so I have to repurpose my Logitech K810 keyboard with MacBook. Logitech K810 is and old model with Windows key layout even though it also supports MacOS.

The bottom row has the same number of keys as magic keyboard but with different keys. For example, magic keyboard has cmd key (aka GUI keys) besides space bar but K810 has Alt. Magic keyboard has Option key next to cmd key and K810 has Windows key on the left and Ctrl key on the right. The position of left Ctrl and FN is different on magic keyboard too. Since I rely on shortcuts on Magic keyboard, I therefore decide configure key mapping on the Windows key, Alt keys and right ctrl key to match magic keyboard.

K810
K810 Layout

In a nutshell, I need to remap some keys on an external Windows keyboard connected to MacOS (Monterey).

What is not working

In MacOS, you could modify keys from keyboard preference as below. However, in my case I need to keep left ctrl key and overwrite the right ctrl key. MacOS does not distinguish them unfortunately. The native method is not flexible so I did not bother.

Use MacOS keyboard preference to modify keys

In the mean time, I hate to install a third-party application (such as Karabiner-Elements) just for the purpose of key mapping. Even though I appreciate the efforts by the community, I just don’t find it a neat solution for the additional dependency.

Challenges with key mapping

I then came across a developer’s post on using a utility called hidutil to map keys. This is promising after I tested using this command to swap key a/A and b/B.

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

The command above takes effect immediately, on all keyboards. In my case I only need mapping on an external keyboard, not the built-in keyboard. Luckily hidutil has –matching switch where I can specify ProductID and VendorID. I can find both of them from About This Mac -> System Report -> Hardware -> Bluetooth:

Vendor ID and Product ID

From this technical note, we can find out the usage IDs of the keys in the table at the bottom of the page. Then use the Usage ID as the last two bytes of the source and destination values of the mapping expression above. For example, 0x700000004 stands for key A/a, and 0x700000005 stands for key B/b. In my case, after going through this table, my key mapping is:

Source Key on K810Source Code on K810Destination KeyDestination Code
Left Alt0x7000000E2Left GUI (cmd)0x7000000E3
Right Alt0x7000000E6Right GUI (cmd)0x7000000E7
Left Win0x7000000E3Left Alt (option)0x7000000E2
Right Ctrl0x7000000E4Right Alt (option)0x7000000E6
Fn+F120x700000045Globe0xFF00000003

There is even a website that helps you generate the usage ID. I put together a command, which is working, on the external keyboard.

hidutil property --matching '{"ProductID":0xB319,"VendorID":0x046D}' --set '{"UserKeyMapping":[{"HIDKeyboardModifierMappingSrc":0x7000000E2,"HIDKeyboardModifierMappingDst":0x7000000E3},{"HIDKeyboardModifierMappingSrc":0x7000000E6,"HIDKeyboardModifierMappingDst":0x7000000E7},
{"HIDKeyboardModifierMappingSrc":0x7000000E3,"HIDKeyboardModifierMappingDst":0x7000000E2},
{"HIDKeyboardModifierMappingSrc":0x7000000E4,"HIDKeyboardModifierMappingDst":0x7000000E6},
{"HIDKeyboardModifierMappingSrc":0x700000045,"HIDKeyboardModifierMappingDst":0xFF00000003}
]}'

The other way to confirm that key mapping is working, is to look up key mapping with the following command:

hidutil property --matching '{"ProductID":0xB319,"VendorID":0x046D}' --get "UserKeyMapping"

To wipe out the mapping, we can use the following command:

hidutil property --matching '{"ProductID":0xB319,"VendorID":0x046D}' --set '{"UserKeyMapping":[]}'

Note that we have to use the same matching expression. However, I noticed that the command only works while the bluetooth keyboard is connected. When I reboot the MacBook the mapping goes away as well.

The solution to key mapping

Even though I could run this command after reboot using a plist file in ~/Library/LaunchAgents/, as the reference blog suggests, it will not work because I cannot guarantee the bluetooth keyboard is connected before MacOS executes it. Also, I might disconnect and re-connect the keyboard and I want to ensure that the system runs that command whenever the keyboard gets connected.

In the Karabiner Element community, some developers touched on this. Basically we can customize the plist file to tell MacOS when to fire the hidutil command, by using “LaunchEvents” key in the XML. There is an example for USB keyboard. For myself, I edit the plist file to be:

<?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.hidutilKeyMapping</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/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
                },
                { 
                    "HIDKeyboardModifierMappingSrc":0x7000000E3,
                    "HIDKeyboardModifierMappingDst":0x7000000E2
                },
                { 
                    "HIDKeyboardModifierMappingSrc":0x7000000E4,
                    "HIDKeyboardModifierMappingDst":0x7000000E6
                },
                {
                    "HIDKeyboardModifierMappingSrc":0x700000045,
                    "HIDKeyboardModifierMappingDst":0xFF00000003
                }
            ]
        }</string>
    </array>

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

Store the file above as ~/Library/LaunchAgents/com.local.hidutilKeyMapping.plist (create LaunchAgents directory if it doesn’t exist) and then run the following to load it:

launchctl load ~/Library/LaunchAgents/com.local.hidutilKeyMapping.plist

This works perfectly. The dict section (line 7 to line 23) of the file is based on this document as the GitHub comment suggests. The LaunchEvents section narrows down the type of events that will trigger the program with arguments. It triggers whenever a bluetooth device with specific ProductID and VendorID connects to Mac.

We can still use hidutil command from the previous section to ensure that the custom key mapping remain in effect:

Note that the output above has usage values converted to decimal from hexadecimal, with destination on top and source at the bottom.

I like this solution as it is light-weight, targeting a specific event, highly customizable and resolves an issue in a very specific circumstance.

Final words

K380 layout

After all this customization effort, I came across the Logitech K380 model (current alternative to K810), whose layout is exactly the way I customized K810 to be. So I ordered the new K380 model for just $29.99 during the week of black Friday. Remembering that I had my K810 for $94.98 back in 2015, what’s the point of all this….