Another approach is to change the mapping from scancodes to keycodes with udev. I needed this more low-level change to get my capslock-escape working in VS Code. This answer is based on https://wiki.archlinux.org/title/Map_scancodes_to_keycodes
Gathering keyboard information
First, identify the keyboard device. I've used cat /proc/bus/input/devices which outputs:
I: Bus=0011 Vendor=0001 Product=0001 Version=ab83
N: Name="AT Translated Set 2 keyboard"
P: Phys=isa0060/serio0/input0
S: Sysfs=/devices/platform/i8042/serio0/input/input3
U: Uniq=
H: Handlers=sysrq kbd event3 leds
B: PROP=0
B: EV=120013
B: KEY=402000000 3803078f800d001 feffffdfffefffff fbfffffffffffffe
B: MSC=10
B: LED=7
Note the eventX here. Then we can get the modalias of the device with cat /sys/class/input/event3/device/modalias:
input:b0011v0001p0001eAB83-e0,1,4,11,14,k71,72,73,74,75,76,77,79,7A,7B,7C,7D,7E,7F,80,8C,8E,8F,9B,9C,9D,9E,9F,A3,A4,A5,A6,AC,AD,B7,B8,B9,D9,E2,ram4,l0,1,2,sfw
We will later use the input:b0011v0001p0001eAB83 identifier.
Now, we want to find out which scancode we want to map to which keycode. My example will map the capslock to an escape (scancode may depend on the keyboard hardware). Run evtest /dev/input/event3 and then press the key (eg capslock):
Event: time 1732003836.156077, type 4 (EV_MSC), code 4 (MSC_SCAN), value 3a
Event: time 1732003836.156077, type 1 (EV_KEY), code 58 (KEY_CAPSLOCK), value 0
Event: time 1732003836.156077, -------------- SYN_REPORT ------------
We note the MSC_SCAN value, in this case the scancode is 3a.
In my example, our target keycode is esc, here is a list of other keycodes.
Update scancode to keycode mapping
There is a default mapping file at /usr/lib/udev/hwdb.d/60-keyboard.hwdb. Create a new file with a higher number, eg /usr/lib/udev/hwdb.d/70-keyboard-custom.hwdb:
# evdev:<modalias identifier>*
# KEYBOARD_KEY_<scancode>=<target keycode>
evdev:input:b0011v0001p0001eAB83*
KEYBOARD_KEY_3a=esc
After saving the file run systemd-hwdb update and udevadm trigger to recompile and reload the hardware database index.
Now, when the pressed capslock yields the 3a scancode, udev will translate it to an esc keycode. Essentially, we have two escape keys now. To achieve the reverse mapping (escape to capslock), follow the same steps to identify the escape scancode and map it to the capslock keycode.