Swap built-in keyboard Capslock and Ctrl keys.

Intro#

If I only use Gnome or KDE Plasma, I can turn on the keycode swapping toggle in the Gnome Tweaks or the KDE Settings. However, I’d like to make the swap also persistent in TTYs and Window Managers. I need to modify the keycode at the system level, not the desktop environment level. To achieve this, I chose to use udev.

Another reason is that the Desktop Environment settings will apply to any connected keyboards. However, my USB keyboard has its custom layout and doesn’t need to be modified. The udev solution can apply key mappings to specific keyboards and remain others untouched.

How#

udev provides a way to edit custom hwdb. The config file lies in the folder /etc/udev/hwdb.d/. The file name should be something like 60-keyboard.hwdb. The content might look like this.

1
2
3
evdev:<input_device_id glob pattern>
 KEYBOARD_KEY_<scancode>=<keycode>
 KEYBOARD_KEY_<scancode>=<keycode>

Scancode#

First, I need to determine the hardware scancode that the keyboard reports when capslock or ctrl is pressed. The tool I used is showkey.

1
2
3
4
5
6
# showkey --scancodes
...
0x3a 0xba  # <-- capslock
0x3a 0xba
0x1d 0x9d  # <-- leftctrl
0x1d 0x9d

So, the scancode of capslock is 0x3a and the scancode of leftctrl is 0x1d.

Keycode#

Keycodes are defined in /usr/include/linux/input-event-codes.h. Also available at quirk-keymap-list.txt.

So, the keycodes are capslock and leftctrl respectively.

Mapping#

Before remapping, the scancodes and keycodes mappings are as follows.

scancodekeycode
0x3dcapslock
0x1dleftctrl

After custom mapping, the mapping table should be like this.

scancodekeycode
0x3dleftctrl
0x1dcapslock

Hardware ID#

If don’t mind applying the modification to all the built-in keyboards no matter what model or serial number it is. It is ok to not determine the hardware ID.

The built-in keyboard device ID should start with atkbd:dmi:. To check the actual ID, use the evemu-describe command evemu tool. You need to select the keyboard device by its name. For example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# evemu-describe
Available devices:
/dev/input/event0:	Lid Switch
/dev/input/event1:	Power Button
/dev/input/event2:	Sleep Button
/dev/input/event3:	Power Button
/dev/input/event4:	AT Translated Set 2 keyboard
...
Select the device event number [0-20]: 4
# EVEMU 1.3
# Kernel: 6.1.6-zen1-2-zen
# DMI: dmi:bvnDellInc.:bvr2.21.0:bd06/02/2022:br2.21:svnDellInc.:pnXPS139360:pvr:rvnDellInc.:rn:rvr:cvnDellInc.:ct10:cvr:sku082A:
# Input device name: "AT Translated Set 2 keyboard"
# Input device ID: bus 0x11 vendor 0x01 product 0x01 version 0xab41
# ...

In the above example, the device ID should be:

1
dmi:bvnDellInc.:bvr2.21.0:bd06/02/2022:br2.21:svnDellInc.:pnXPS139360:pvr:rvnDellInc.:rn:rvr:cvnDellInc.:ct10:cvr:sku082A:

A general glob pattern for any AT keyboard should be:

1
evdev:atkbd:dmi:bvn*:bvr*:bd*:svn*:pn*:pvr*

Or evdev:atkbd:dmi:* for short.

Config#

Edit the /etc/udev/hwdb.d/90-custom-keyboard.hwdb config file. Keycodes need to be lowercase.

1
2
3
evdev:atkbd:dmi:bvn*:bvr*:bd*:svn*:pn*:pvr*
 KEYBOARD_KEY_3a=leftctrl
 KEYBOARD_KEY_1d=capslock

Update hwdb#

1
# systemd-hwdb update

To automatically update the hwdb. A reboot should automatically trigger systemd-hwdb-update.service. Or manually trigger systemd-hwdb-update.service.

1
# systemctl start systemd-hwdb-update.service

The service will not run if /etc dir has not been changed.

Reload#

If already rebooted. The changes should be already applied and working. If manually updated the hwdb. Also need to manually reload the hwdb.

1
# udevadm trigger

Verify#

To verify the change, can use udevadm info (#ref). The real path of input event device /dev/input/event4 can be obtained by evemu-describe.

1
2
3
4
5
6
# udevadm info /dev/input/event4 | grep KEYBOARD_KEY
E: KEYBOARD_KEY_1d=capslock
E: KEYBOARD_KEY_3a=leftctrl
E: KEYBOARD_KEY_81=playpause
E: KEYBOARD_KEY_82=stopcd
...

Reference#