Remapping Keys in Linux

Our Goal: Automatic Remapping

This is mainly a reminder so that I don’t forget how I set up automatic key remapping under Linux. Let’s face it: nobody remembers all of the steps to set up a new laptop and I don’t want to spend the same amount of time solving this problem in the future as I just spent on it today.

Our goal today is to set up automatic key remapping whenever a specific USB keyboard is plugged in. We’ll be creating a new udev rule that fires whenever the device is plugged in. That udev rule will call a single script that sets up a minimal environment and then executes a different script (look this really is necessary, OK?) and that second script will actually change the keyboard mapping.

Sidebar: What’s udev? According to the Arch Linux wiki, udev “is a device manager for the Linux kernel. As the successor of devfsd and hotplug, udev primarily manages device nodes in the /dev directory. At the same time, udev also handles all user space events raised while hardware devices are added into the system or removed from it, including firmware loading as required by certain devices.” Basically: it does shit when hardware gets connected.

Identifying the Keyboard

Our first step is to figure out which device is a keyboard. We can execute ls /dev/input/by-id to try to find the keyboard. In my case this was particularly easy and I found the keyboard hanging out in that location with the helpful name usb-Metadot_-_Das_Keyboard_Das_Keyboard-event-kbd.

Once you know the name of the device, you can use udevadm info to get additional fascinating details like the manufacturer’s ID or how many chin whiskers the keyboard has. To do this, we just run udevadm info -a -n /dev/input/by-id/usb-Metadot_-_Das_Keyboard_Das_Keyboard-event-kbd | less (obviously, you should substitute the full path to your own favorite keyboard).

The first page of many in udevadm’s verbose but helpful output:

Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

  looking at device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.4/1-1.4:1.0/0003:24F0:0140.014B/input/input291/event9':

  looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.4/1-1.4:1.0/0003:24F0:0140.014B/input/input291':
    ATTRS{name}=="Metadot - Das Keyboard Das Keyboard"

What we’re looking for in this mountain of garbage text is something that uniquely identifies the keyboard. Now, a reasonable human might assume that something like ATTRS{phys} would be the right thing. I was unable to reliably get results using that attribute value. Instead, I chose the idVendor and idProduct attributes as the target of my udev rule.

Writing the Rules

Now that we’ve found a way to uniquely identify the keyboard (you can match on any of the ATTRS values). We need to write a udev rule that can identify the hardware when it’s connected and then run a script.

I created a new file /etc/udev/rules.d/99-keyboard.rules and dumped the following in it:

SUBSYSTEM=="input", SUBSYSTEMS=="usb", ACTION=="add", ATTRS{idVendor}=="24f0", ATTRS{idProduct}=="0140", RUN+="/usr/local/bin/keyboard-udev"

This rule is trying to match:

  • SUBSYSTEM=="input" any device in the input subsystem,
  • SUBSYSTEMS=="usb" that device had better be a USB device,
  • ACTION=="add" and some joker has just plugged that USB input device into the computer,
  • ATTRS{idVendor}=="24f0" and someone made this product with love,
  • ATTRS{idProduct}=="0140" and it has amazing features unique to products called “0140”.

Once the rule is matched, we’re going to run all scripts that have been added to RUN. In our case, that’s just the `keyboard-udev` script.

The keyboard-udev script

### WARNING: Your distro might not put bash in this location.
###          You could just use `/usr/bin/env bash` instead.

# only needed for debugging
#exec >/tmp/udev.out 2>&1

DISPLAY=:0 ; export DISPLAY;

# Path to lock file

# Lock the file (other atomic alternatives would be "ln" or "mkdir")
exec 9>"$lock"
if ! flock -n 9; then
        notify-send -t 5000 "Keyboard script is already running."
        exit 1

/usr/bin/su jeremiah -c "sleep 3; /usr/local/bin/keyboard;" &

# The lock file will be unlocked when the script ends
echo '' > /tmp/keyboard.lock &

This script sets the HOME, XAUTHORITY, and DISPLAY session variables with just enough information to fool the computer that this script is being executed in an environment that belongs to me. This is necessary since, without changing these values, the execution environment is actually the environment of whichever process is responsible for udev (probably systemd).

Once the environment is set up, the script locks the file /tmp/keyboard.lock. Why? Well, because it turns out that this script will be fired once per device that matches your rules. Since there are 5 hardware devices that match the idVendor and idProduct that we specified, the rule is executed 5 times. While this particular script isn’t destructive, you can probably see why you’d only want the script to run once, not 5 times.

After we’ve set up the execution environment and created the lock file, we get to the good stuff: changing system settings. Well, first we launch the script as the appropriate user using /usr/bin/su jeremiah -c SOME_COMMAND. The su program runs a command a substitute user (here it’s “jeremiah”). The -c parameter is used to specify that the next thing encountered is the command to pass through to the shell running as “jeremiah”. We have to put the command in double quotes because it’s more than a single program to execute.

The keyboard script

The /usr/local/bin/keyboard command is really simple:

setxkbmap -option "ctrl:nocaps,altwin:swap_lalt_lwin,compose:menu"

xmodmap -e "clear mod1"
xmodmap -e "add mod1 = Alt_L"
xmodmap -e "add mod1 = Meta_L"
xmodmap -e "add mod3 = Alt_R"

This sets up my keyboard layout by turning Caps Lock into another control key, swaps the left Alt key with the left Windows key, and then turns the menu key into the Compose key (Compose is how I can type things like λ and ü without using another program).

Wrapping Up

Before you’re done, there are just a few more things that you need to do.

Obviously, you’ll need to make both of those scripts executable. I’m assuming that if you’ve read to this point you either know how to do that or you’re humoring me because we’ve met in real life. (Hint: chmod)

After you’ve written the udev rule and copied and pasted the two scripts (or written your own, whatever), you’ll also need to tell udev that it’s time to pick up the rules. You can easily do that with: sudo udevadm control --reload-rules.

Now you’re all set and, in theory, your keyboard should be remapped whenever you reconnect it.

Photo by Juan Gomez on Unsplash

Original comments follow.

Robert - Aug 1, 2019

Thanks for the example. I copied the pattern and it worked right away! Just out of curiosity, why is the “sleep 3;” required? I tried without that and it stopped working, but I don’t get why.

Jeremiah Peschka - Aug 1, 2019

I’m happy this worked for you! You know, I should’ve documented that because I cannot for the life of me remember. When I first wrote this, I ran across something that explained why the sleep is so important. I’ll try to do some more digging to see if I can find the original source.

Felicia - Jul 4, 2020

EXCELLENT article! I’ve wanted to do something like this with some Thinkpad keyboards that need tweaking. I already had a script to do it, but needed help with the udev part. Thanks very much for demystifying the confusing world of writing custom udev rules!

pgupta - Aug 4, 2020

thank you. I am using a KVM switch and I kept losing my custom xmodmap bindings everytime i switched back to Arch. I kept messing with udev rules but could not get xmodmap to properly reload the bindings until I used your method in this blog post.