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 currentUserKeyMapping
,reset
resets the currentUserKeyMapping
to default, orexport
outputs the currentUserKeyMapping
in a format for creating a LaunchAgentdefault
displays the defaultFnFunctionUsageMap
output fromioreg
-
keymap.sh [n=keycode n=keycode...]
where:n
is 1 to 12 for the keyboard function keys, andkeycode
is a hex key code starting with0x
-
keymap.sh [n=function n=function...]
where:n
as above, is 1 to 12 for the keyboard function keys, andfunction
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 ScrLk47
is F14 and Pause48
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:
- 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"
- Then match this output to either the
mac_new
ormac_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.
- Edit the script so that
this_mac
uses either themac_new
ormac_old
variable:- if neither fit your Mac, go ahead and edit the variable
this_mac
directly, remembering that the first array value0
is not used, e.g.this_mac=(0 0x1 0x2...)
.
- if neither fit your Mac, go ahead and edit the variable
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) |
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.