In my last post, “Re-mapping physical function keys on MacBook Pros” I used the hidutil tool to re-map the function keys on a Mac without a touchbar. I’ve since created a script to more easily apply changes.

Background

I learnt more about the hidutil parameters by checking out the HID Usage Tables for USB v1.3 2022, under the Keyboard Page 0x07 and Consumer Page 0x0c.

In addition to these key codes, Apple’s custom key codes in HID Page kHIDPage_AppleVendorKeyboard = 0xff01 is documented in AppleHIDUsageTables.h that is part of the IOHIDFamily open source codebase, specifically:

/* AppleVendor Keyboard Page (0xff01) */
enum {
    kHIDUsage_AppleVendorKeyboard_Spotlight             = 0x0001,
    kHIDUsage_AppleVendorKeyboard_Dashboard             = 0x0002,
    kHIDUsage_AppleVendorKeyboard_Function              = 0x0003,
    kHIDUsage_AppleVendorKeyboard_Launchpad             = 0x0004,
    kHIDUsage_AppleVendorKeyboard_Reserved              = 0x000A,
    kHIDUsage_AppleVendorKeyboard_CapsLockDelayEnable   = 0x000B,
    kHIDUsage_AppleVendorKeyboard_PowerState            = 0x000C,
    kHIDUsage_AppleVendorKeyboard_Expose_All            = 0x0010,
    kHIDUsage_AppleVendorKeyboard_Expose_Desktop        = 0x0011,
    kHIDUsage_AppleVendorKeyboard_Brightness_Up         = 0x0020,
    kHIDUsage_AppleVendorKeyboard_Brightness_Down       = 0x0021,
    kHIDUsage_AppleVendorKeyboard_Language              = 0x0030,
};

Usage

My script below takes three forms of input:

  • keymap.sh [show|reset|default] where:

    • show displays the current UserKeyMapping,
    • reset resets the current UserKeyMapping to default, or
    • export outputs the current UserKeyMapping in a format for creating a LaunchAgent
    • default displays the default FnFunctionUsageMap output from ioreg
  • keymap.sh [n=keycode n=keycode...] where:

    • n is 1 to 12 for the keyboard function keys, and
    • keycode is a hex key code starting with 0x
  • keymap.sh [n=function n=function...] where:

    • n as above, is 1 to 12 for the keyboard function keys, and
    • function is a pre-defined function in the table below:
Function Description
bri_dec decrease display brightness
bri_inc increase display brightness
kbd_dec decrease keyboard backlight brightness
kbd_inc increase keyboard backlight brightness
missionc show mission control / exposé
launchp show launchpad
spotl spotlight search
dict start dictation (hold for Siri)
dnd toggle do not disturb
globe trigger the globe function (by default, emoji picker)
desktop same as F11, which by default, shows the desktop
rew media rewind
play media play or pause
fwd media fast-forward
mute mute
vol_dec decrease volume
vol_inc increase volume
num0 to num9 keypad 0 to 9 keys
num_add keypad add + key
num_sub keypad subtract - key
num_mul keypad multiply * key
num_div keypad divide / key
num_dot keypad . key
num_equ keypad equals = key
num_enter keypad Enter key

In my experimentation on my MacBook:

What does not work:

  • Key codes beyond F20 though they exist in the USB documentation are not recognized on my mac. Similarly, some kHIDUsage_AppleVendorKeyboard codes above do not function for me.
  • PrtSc 0x700000046 is mapped to F13 while ScrLk 47 is F14 and Pause 48 is F15 - this is strange, and is one reason why I use F13 to take a screenshot.
  • The Context Menu key on some Windows keyboards 0x700000065 translates to Command ⌘+Context Menu which cannot be used in keyboard shortcuts.
  • I tried many key codes in the Consumer Page 0xc, but all are simply ignored by macOS, but on Windows, they do work e.g. quick launch application keys, media control keys, browser navigation keys, etc.

What works:

  • All (most?) mac’s already assign F11 to show the desktop, but this can be changed in the keyboard shortcuts.
  • Number pad keys can be assigned to keyboard shortcuts, but only re-map the keypad / number pad keys if you are not using an external 101-key keyboard.
  • I think one can use F13 to F19 (7) + 0 to 9 on the number pad (10) + the four math symbols and . (5) on the number pad - that’s a total of 22 keys to play around with, to either assign Keyboard shortcuts or assign to Shortcut.apps Shortcuts. Capeesh?

Regarding creating a LaunchAgent so that your key mappings are applied automatically upon login... I have not tested my script, and some fancy manipulation is needed. I leave the testing to you.

Referring to my previous Keyboard mapping post, you could do something like this, assuming no such file already exists:

./keymap.sh > ~/Library/LaunchAgents/com.local.KeyRemapping.plist
launchctl load ~/Library/LaunchAgents/com.local.KeyRemapping.plist

Setup

Your keyboard might return different key codes than mine.

Checking the configuration:

  1. Please check what is your default keyboard mapping first with keymap.sh default, e.g.
    "FnFunctionUsageMap" = "0x0007003a,0x00ff0005,0x0007003b,0x00ff0004,0x0007003c,0xff010010,0x0007003d,0x000c0221,0x0007003e,0x000c00cf,0x0007003f,0x0001009b,0x00070040,0x000c00b4,0x00070041,0x000c00cd,0x00070042,0x000c00b3,0x00070043,0x000c00e2,0x00070044,0x000c00ea,0x00070045,0x000c00e9"
  2. Then match this output to either the mac_new or mac_old variable - every other value should match:
    • mac_old is based on my old x86 MacBook Pro and first generation Magic Keyboard,
    • while mac_new suits my new M1 MacBook Pro.
  3. Edit the script so that this_mac uses either the mac_new or mac_old variable:
    • if neither fit your Mac, go ahead and edit the variable this_mac directly, remembering that the first array value 0 is not used, e.g. this_mac=(0 0x1 0x2...).
mac_old=(0 0xff00000005 0xff00000004 0xff0100000010 0xff010004 0xff00000009 0xff00000008 0xc000000b4 0xc000000cd 0xc000000b3 0xc000000e2 0xc000000ea 0xc000000e9)
mac_new=(0 0xff00000005 0xff00000004 0xff0100000010 0xc00000221 0xc000000cf 0x10000009b 0xc000000b4 0xc000000cd 0xc000000b3 0xc000000e2 0xc000000ea 0xc000000e9)
this_mac=(${mac_new[@]})  # change based on "old" or "new" mac / magic keyboard layout

Script

#!/bin/bash
# (c) 2022 C.Y. Wong myByways.com

mac_old=(0 0xff00000005 0xff00000004 0xff0100000010 0xff010004 0xff00000009 0xff00000008 0xc000000b4 0xc000000cd 0xc000000b3 0xc000000e2 0xc000000ea 0xc000000e9)
mac_new=(0 0xff00000005 0xff00000004 0xff0100000010 0xc00000221 0xc000000cf 0x10000009b 0xc000000b4 0xc000000cd 0xc000000b3 0xc000000e2 0xc000000ea 0xc000000e9)

this_mac=(${mac_new[@]})  # change based on "old" or "new" mac / magic keyboard layout

fn_keys=(0 0x70000003a 0x70000003b 0x70000003c 0x70000003d 0x70000003e 0x70000003f 0x700000040 0x700000041 0x700000042 0x700000043 0x700000044 0x700000045 0x700000068 0x700000069 0x70000006a 0x70000006b 0x70000006c 0x70000006d 0x70000006e)
fn_bri_dec=0xff0100000021 # or 0xc00000070
fn_bri_inc=0xff0100000020 # or 0xc0000006f
fn_missionc=0xff0100000010
fn_spotl=0xff0100000001   # or 0xc00000221
fn_dict=0xc000000cf
fn_dnd=0x10000009b
fn_rew=0xc000000b4
fn_play=0xc000000cd
fn_fwd=0xc000000b3
fn_mute=0xc000000e2
fn_vol_inc=0xc000000ea
fn_vol_dec=0xc000000e9
fn_kbd_inc=0xff00000008
fn_kbd_dec=0xff00000009
fn_launchp=0xff0100000004
fn_globe=0xff0100000030
fn_desktop=0x700000044 # or f11
fn_num_div=0x700000054
fn_num_mul=0x700000055
fn_num_sub=0x700000056
fn_num_add=0x700000057
fn_num_enter=0x700000058
fn_num1=0x700000059
fn_num2=0x70000005a
fn_num3=0x70000005b
fn_num4=0x70000005c
fn_num5=0x70000005d
fn_num6=0x70000005e
fn_num7=0x70000005f
fn_num8=0x700000060
fn_num9=0x700000061
fn_num0=0x700000062
fn_num_dot=0x700000063
fn_num_equ=0x700000067

function help() {
  echo "$LESS_TERMCAP_mb$0 $LESS_TERMCAP_md[command]$LESS_TERMCAP_me
  where $LESS_TERMCAP_md[command]$LESS_TERMCAP_me is one of:
    show      display the current user key map
    export    output the current user key map for creating a LaunchAgent
    reset     reset user key map to default
    default   display the default key map (from ioreg)
$LESS_TERMCAP_mb$0 $LESS_TERMCAP_md[n=keycode ...]$LESS_TERMCAP_me
  where ${LESS_TERMCAP_us}n$LESS_TERMCAP_ue is 1 to 12 for thefunction key, and 
    ${LESS_TERMCAP_us}keycode$LESS_TERMCAP_ue is a hex key code starting with 0x
$LESS_TERMCAP_mb$0 $LESS_TERMCAP_md[n=function ...]$LESS_TERMCAP_me
  where ${LESS_TERMCAP_us}n$LESS_TERMCAP_ue is 1 to 12 for the function key, and 
    ${LESS_TERMCAP_us}function$LESS_TERMCAP_ue is one of:
      bri_dec | bri_inc   decrease / increase display brightness
      kbd_dec | kbd_inc   decrease / increase keyboard backlight brightness
      missionc | launchp  show mission control / launchpad
      spotl | dict        spotlight search / dictation (hold for Siri)
      dnd | mute          toggle do not disturb / mute
      rew | play | fwd    rewind / play or pause / fast-forward media
      vol_dec | vol_inc   decrease / increase volume
      globe | desktop     show emoji / desktop (same as F11)
      num0 to num9        number pad keys, along with:
      num_add | num_sub | num_mul | num_div | num_dot | num_equ | num_enter
"
}
function show() {
  echo Current user key map 
  hidutil property -g UserKeyMapping
}
function reset() {
  echo Reset user key map 
  hidutil property -s "{\"UserKeyMapping\":[]}" >/dev/null
}
function default() {
  echo Show default key map 
  ioreg -l | grep -o "\"FnFunctionUsageMap\" = .*"
}
function export() {
  x=$(hidutil property -g UserKeyMapping | sed -E "s/(HIDKeyboardModifierMappingDst) = (.*);/\"\1\": \2,/g;s/(HIDKeyboardModifierMappingSrc) = (.*);/\"\1\": \2/g;s/\(/[/;s/\)/]/")
  echo '<?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.local.KeyRemapping</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/hidutil</string>
        <string>property</string>
        <string>--set</string>
        <string>{"UserKeyMapping":'"$x"'
        }</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>'
}
function map() {
  y=""
  for x in "$@"; do
    x=(${x//=/ })
    x0=${x[0]}
    x1=${x[1]}
    if [[ ${#x[@]} -eq 2 && ( $x0 == [1-9] ||  $x0 == 1[012]) ]]; then
      fn=${this_mac[$x0]}
      [[ "$fn" == "" ]] && echo "Error no such key F$x0" && exit -1
      if [[ "$x1" == f[1-9] || "$x1" == f1[0-9] ]]; then
        kc=${x1:1}
        kc=${fn_keys[$kc]}
        [[ "$kc" == "" ]] && echo "Error mapping F$x0 [$fn] to unknown $x1" && exit -1
        echo "Mapping F$x0 [$fn] to HID $x1 [$kc]"
        y="{\"HIDKeyboardModifierMappingSrc\":$fn,\"HIDKeyboardModifierMappingDst\":$kc},$y"
      elif [[ "$kc" == 0x[0-9A-Fa-f]* && ( ${#kc} -ge 11 && ${#kc} -le 14 ) ]]; then
        echo "Mapping F$x0 [$fn] to HID hex code [$kc]"
        y="{\"HIDKeyboardModifierMappingSrc\":$fn,\"HIDKeyboardModifierMappingDst\":$kc},$y"
      else 
        kc=fn_$x1
        kc=${!kc}
        [[ "$kc" == "" ]] && echo "Error mapping F$x0 [$fn] to unknown $x1" && exit -1
        echo "Mapping F$x0 [$fn] to HID $x1 [$kc]"
        y="{\"HIDKeyboardModifierMappingSrc\":$fn,\"HIDKeyboardModifierMappingDst\":$kc},$y"
      fi
    fi
    hidutil property -s "{\"UserKeyMapping\":[${y%?}]}" >/dev/null
  done
}
function kbd() {
  map 4=kbd_dec 5=kbd_inc
}
if [[ ${#@} -eq 1 && "$1" != *=* ]]; then
  [[ $(LC_ALL=C type -t "$1") == "function" && "$1" != "map" ]] && $1 && exit $?
elif [[ ${#@} -ge 1 ]]; then
  map "$@"
  exit $?
fi
help

I know, I write undecipherable code. I also am known to take shortcuts and do absolutely minimum testing. As always, don’t run scripts you download from the Internet without understanding it first. Good luck with that!

Examples

Command Description
keymap.sh 4=kbd_dec 5=kbd_inc use F3/F4 to control keyboard backlight brightness
keymap.sh 3=launchpad F3 to open launchpad instead of mission control / exposé
keymap.sh 5=f13 6=f14 which is what I used in my last original post - the former to take a screen shot, the latter to run a Shortcuts.app shortcut to toggle dark mode (refer to the screeshots in that post for clarity) Shortcut to toggle appearance of dark / light mode, mapped to F14
keymap.sh 1=0xff00000009 2=0xff00000008 to apply hex key codes
keymap.sh reset to remove all user-defined key codes

Note that each subsequent application of the script wipes out any prior configuration before it!

One final note: The way I call check for match function names LC_ALL=C type -t "$1") allows me to create new functions to quickly apply frequently-used key mapping sets. You can see the example with function kbd(), so that I can just run ./keymap.sh kbd instead of typing it all out. So go ahead and add more of your own.