Remapping ThinkPad Keys with udev hwdb

ThinkPad T series (also X1 Carbon) laptops have a great keyboard that I’ve been using delightfully for years. However, there’s a minor issue with its keyboard layout: they replaced the Menu key with a PrtSc. In my day to day work, I almost always accidentally hit that key while using my lovely Ctrl and Alt keys, upon which my laptop happily plays a shatter sound, flashes my screen white for a split second and spawns a screenshot file under my Pictures/ (thank you, GNOME). Whereas when I wanted to use my Menu key, it’s nowhere to be found.

However, there’s still an Insert key lying quietly in the top-right corner, which I never used (except for checking if some app even supports it). So why not make my old Insert PrtSc and my old PrtSc the new Menu?

Moreover, there are also 4 special keys (Fn + F9F12) that could have been my media keys, but are by default strange things like Settings and Search. Why not map them to media keys as well?

Things I tried

xmodmap was the go-to tool for remapping keys, and it works on the X11 server level so the only thing you need to care about is the X11 key symbols (no key codes, scan codes and other nightmares). However, naturally this tool won’t work under Wayland (which supports fractional scaling etc), and it cannot get automatically loaded by my GNOME 3 (even with autostart). Some say it’s been deprecated, and people should use xkb instead, so no luck here.

The other way is to modify the xkb key symbol database. Although it doesn’t provide any means of overriding in /etc/, you can directly edit the files under /usr/share/X11/xkb/symbols/. The interesting files are pc for the standard keys, and inet for the special keys, and here’s the patch I’ve been using for two years:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
diff --git a/pc b/pc.zhnew
index 0199713..33631cb 100644
--- a/pc
+++ b/pc.zhnew
@@ -29,7 +29,7 @@ xkb_symbols "pc105" {
key <RTSH> { [Shift_R] };
key <RCTL> { [Control_R] };
key <RWIN> { [Super_R] };
- key <MENU> { [Menu] };
+ key <PRSC> { [Menu] };

// Beginning of modifier mappings.
modifier_map Shift { Shift_L, Shift_R };
@@ -64,7 +64,7 @@ xkb_symbols "pc105" {

hidden partial alphanumeric_keys
xkb_symbols "editing" {
- key <PRSC> {
+ key <INS> {
type= "PC_ALT_LEVEL2",
symbols[Group1]= [Print, Sys_Req]
};
@@ -73,7 +73,7 @@ xkb_symbols "editing" {
type= "PC_CONTROL_LEVEL2",
symbols[Group1]= [Pause, Break]
};
- key <INS> { [Insert] };
+ key <MENU> { [Insert] };
key <HOME> { [Home] };
key <PGUP> { [Prior] };
key <DELE> { [Delete] };

diff --git a/inet b/inet.zhnew
index 755597e..f4b86d6 100644
--- a/inet
+++ b/inet.zhnew
@@ -121,13 +121,13 @@ xkb_symbols "evdev" {

// key <I120> { [] }; // KEY_MACRO
key <I126> { [plusminus] };
- key <I128> { [XF86LaunchA] };
+ key <I173> { [XF86LaunchA] };
key <I147> { [XF86MenuKB] };
key <I148> { [XF86Calculator] };
// key <I149> { [] }; // KEY_SETUP
key <I150> { [XF86Sleep] };
key <I151> { [XF86WakeUp] };
- key <I152> { [XF86Explorer] };
+ key <I171> { [XF86Explorer] };
key <I153> { [XF86Send] };
// key <I154> { [] }; // KEY_DELETEFILE
key <I155> { [XF86Xfer] };
@@ -146,15 +146,15 @@ xkb_symbols "evdev" {
// key <I168> { [] }; // KEY_CLOSECD (opposite of eject)
key <I169> { [XF86Eject] };
key <I170> { [XF86Eject, XF86Eject] };
- key <I171> { [XF86AudioNext] };
- key <I172> { [XF86AudioPlay, XF86AudioPause] };
- key <I173> { [XF86AudioPrev] };
- key <I174> { [XF86AudioStop, XF86Eject] };
+ key <I152> { [XF86AudioNext] };
+ key <I179> { [XF86AudioPlay, XF86AudioPause] };
+ key <I128> { [XF86AudioPrev] };
+ key <I225> { [XF86AudioStop, XF86Eject] };
key <I175> { [XF86AudioRecord] };
key <I176> { [XF86AudioRewind] };
key <I177> { [XF86Phone] };
// key <I178> { [] }; // KEY_ISO
- key <I179> { [XF86Tools] };
+ key <I172> { [XF86Tools] };
key <I180> { [XF86HomePage] };
key <I181> { [XF86Reload] };
key <I182> { [XF86Close] };
@@ -188,7 +188,7 @@ xkb_symbols "evdev" {
// key <I222> { [] }; // KEY_QUESTION
key <I223> { [XF86Mail] };
key <I224> { [XF86Messenger] }; // KEY_CHAT
- key <I225> { [XF86Search] };
+ key <I174> { [XF86Search] };
key <I226> { [XF86Go] }; // KEY_CONNECT
key <I227> { [XF86Finance] };
key <I228> { [XF86Game] }; // KEY_SPORT

It works perfectly, without any overhead. The only problem with it, is that because these things live inside /usr/, which is managed by pacman, it is reverted to packaged version every time xkeyboard-config is updated, and it actually does get updated sometimes. In that case I’ll find me suddenly making screenshots again, and need to patch those files and reboot for things to work.

Using udev hwdb

Recently when I was doing some brief research about new ThinkPads, I came across the ArchWiki for ThinkPad T480, which mentioned using something called hwdb to add support for its two special buttons. It looked promising, and I finally took some hours today to figure it out for my own remapping.

The hwdb in udev works on a much lower level: it maps the scan codes from your keyboard to standard key codes, and /etc/udev/hwdb.d/ provides a means of customization, which allows overriding the way scan codes are mapped. Some more detail can be found out on Arch Wiki.

And here is the final hwdb file I came up with:

1
2
3
4
5
6
7
8
9
10
11
# /etc/udev/hwdb.d/90-zh-thinkpad.hwdb

evdev:atkbd:dmi:bvn*:bvr*:bd*:svnLENOVO*:pn*:pvrThinkPad*
KEYBOARD_KEY_b7=compose
KEYBOARD_KEY_d2=sysrq

evdev:name:ThinkPad Extra Buttons:dmi:bvn*:bvr*:bd*:svnLENOVO*:pn*:pvrThinkPad*
KEYBOARD_KEY_1c=playpause
KEYBOARD_KEY_1d=stopcd
KEYBOARD_KEY_1e=previoussong
KEYBOARD_KEY_1f=nextsong

The hwdb rules we need to write consists of two parts: matching and mapping. The matching expression is a shell glob that matches the device, where as the mapping maps the scan code (in hex) to key code macro names in kernel’s include/uapi/linux/input-event-codes.h. man hwdb provided some simple example, but actually comments in the built-in hwdb file provides much more details about this file.

In order to find out the scan codes for my keys, I tried both methods in Arch Wiki. The traditional showkey --scancodes didn’t work well for me, requiring switching to a tty, and was printing multiple bytes of hex for a single keystroke of mine. In contrast, evtest was just the right tool. Just execute sudo evtest in a terminal and select something like AT Translated Set 2 keyboard for the builtin keyboard (mine is 3), and you can test your keystrokes to find out its scan code in output like (MSC_SCAN), value <SCAN CODE HERE>.

Another caveat is that, for the special keys on ThinkPad keyboard, unlike regular keys they are not listed under the AT Translated Set 2 keyboard, but actually under another input device named ThinkPad Extra Buttons. It took me some time to realize this, and I also tried showkey which disappointed me again.

After we’ve got the scan codes from evtest and key codes from input-event-codes.h, it’s time to write the rule. We need the matching part for the two devices, whose format specification is available in the comment inside systemd’s bulitin hwdb file. The exact info for your current machine can be obtained from cat /sys/class/dmi/id/modalias, and combining with other existing ThinkPad rules in /usr/lib/udev/hwdb.d/60-keyboard.hwdb I derived mine successfully.

The actual rules read by udev upon boot is a compiled binary file called hwdb.bin, so one will need to compile the configuration files into binary with sudo systemd-hwdb update. To make the changes take effect immediately, run sudo udevadm trigger, and finally, try out the new key mapping!

One last thing… the Menu key

Most of my key mapping worked, except for my new Menu key. I double checked the scan code and the key code name – they both seemed correct. In input-event-codes.h, I also found KEY_OPITON and KEY_CONTEXT_MENU, but neither of them worked as Menu key as well.

So I tried xev. Interestingly, it printed XF86MenuKB as the key symbol on the X11 level, instead of Menu. This must be something with the X11 key symbol database. I did some grep, played around for around half an hour, and finally found out the answer when I expanded my search into /usr/share/X11/xkb/keycodes/evdev:

1
2
3
alias <MENU> = <COMP>;
...
<I147> = 147; // #define KEY_MENU 139

Combined with the output from xmodmap -pke | grep Menu:

1
2
keycode 135 = Menu NoSymbol Menu
keycode 147 = XF86MenuKB NoSymbol XF86MenuKB

I surprisingly found out that my key code KEY_MENU was mapped to key symbol XF86MenuKB. And to map to key symbol Menu, I actually need to map my scan code to key code KEY_COMP.

So one last change, save, and sudo systemd-hwdb update && sudo udevadm trigger. Hooray! All my keys are working flawlessly now, and I don’t need to worry about package updates anymore (just forget about xkeyboard-config). Although the obscure and scattered documentation put me through these tedious trial-and-error attempts, it still kinda excited me when I finally got my keys remapped correctly, after all these years.