Keyboard To Launch Control

We all know that science can go wrong, and that of all science that can go wrong rocket science has lots of potential to go the wrongest. Usually you would want to launch your rocket smoothly, on a trajectory pointing in the general direction of “up”, and then control it for a good long while after. But sometimes things do not turn out quite as planned, and you get an explosion instead.

Computer science is a lot like rocket science in that respect, except that our explosions tend to be somewhat less spectacular.

We have a problem

A laptop keyboard. The windows key has disintegrated, the keycap is lying beside flipped on its top side and the scissor mechanic that stabilizes the key is in pieces.

(Why yes keyboards are filthy!) So our window manager key (otherwise known as the Windows key, Super, Meta, or Mod4) has just. Exploded. Pressed it, let go, had pieces fly off exploded.

We are now short a key that is needed to do absolutely anything with our computer (because tiling window manager), but we can’t fix the key. Tiny plastic pieces broke off, and it simply will not go back together again.

Luckily there is a key on the keyboard that we do not use, and it’s even in the photo! It’s the ><| key, also known as 102nd. (We’ll get back to that.)

Now we cannot simply tell our computer to use that key for window manager functions because it’s a character key that, on this layout, sends the same characters as some other keys. That means we will somehow have to inform this wondrous device of crystal magic that this key in particular should now be a different key, kthx.

Mapping, sans geography

But to do that we’ll have to understand a bit about how keyboards work. This will be somewhat simplified, but we don’t need to know the exact details for this adventure.

When a key on the keyboard is pressed, say the 6 key, the keyboard does not send the character 6. Some keyboards of old did in fact do that, but this hasn’t been standard practice for many years because it makes changing keyboard layouts hard to impossible. After all it’d be the keyboard that has to do the change, and reprogramming the keyboard does not sound like good times. Instead keyboards send what is called a scancode that identifies a key, but has no further meaning. All the scancode is supposed to do is be the same across all keyboards of a given type—in this case, PC keyboards. All PC keyboards, even if they’ve got very different print on the keys, will send the same scancode for that physical key. Pressing a key sends a scancode, releasing the key sends a different scancode. That’s all keyboards do.

This makes it possible—and in fact necessary—to translate from scancodes to characters or other things. The translation takes scancodes as inputs, and gives keycodes as outputs. Where a scancode for the character 6 on the US layout may be 7, the keycode can be something entirely different. On Linux the X11 keycode for this key is 15, with symbolic name <AE06>. Most importantly, that’s not a character 6 yet.

There’s another step of translation involved, from keycodes to keysyms. We need this extra step because pressing the 6 key while holding down shift will still only send the scancode 7, only that this time we know the shift key is held down because it has sent its own scancode. This fact of “shift is held down” is encoded in modifier bits that the computer keeps track of. Pressing shift will set the “shift” modifier bit to 1, releasing shift will reset it to 0. (We’ll get back to that too.) The translation from keycodes to keysyms takes these modifiers into account, and finally produces the character 6 for the key 6 we pressed. (Or it may have produced ^ if shift was held. Or ¼ if AltGr and shift were held on us-altgr-intl layout. Or …)

We can change how either translation operates to remap keys, and both have their advantages and disadvantages.

I know that code

Possibly the easiest way to remap a key is to ignore that anything other than X11 exists, and remap it using a modified xkb layout. This obviously has the downside of ignoring that anything other than X11 exists, but for this particular case it’s not too bad. Never used the windows key on a virtual console anyway.

First we’ll need a description of the current layout. We’ll get that from the X server by running setxkbmap -print, and for this machine it gives

xkb_keymap {
	xkb_keycodes  { include "evdev+aliases(qwerty)"	};
	xkb_types     { include "complete"	};
	xkb_compat    { include "complete"	};
	xkb_symbols   { include "pc+us(altgr-intl)+inet(evdev)+ctrl(nocaps)"	};
	xkb_geometry  { include "pc(pc104)"	};
};

If we run xev and press what’s left of the windows key, we’ll see that it has its own keycode and keysym: 133 and Super_L, respectively. Pressing this key sets the state bit for Mod4, releasing it resets that bit. If that sounds a lot like Shift acts, that’s because both in fact use the same mechanism. Pressing Shift does not directly set a state bit, instead it is translated (ignoring all state bits) to its own keysym, and that keysym then sets the state bit when the X server processes it. Likewise letting go of the key will clear the bit when the X server processes the corresponding event for that.

Super, or Mod4, already has a keysym associated that does all the modifier bit handling. This means that all we have to do is change the layout a tiny bit such that the ><| key always translates to a Super_L keysym instead. Since this is about symbols, we’ll add a line to the xkb_symbols block:

xkb_keymap {
	xkb_keycodes  { include "evdev+aliases(qwerty)" };
	xkb_types     { include "complete"      };
	xkb_compat    { include "complete"      };
	xkb_symbols   {
		include "pc+us(altgr-intl)+inet(evdev)+ctrl(nocaps)"
		key <LSGT> { [ Super_L, Super_L, Super_L, Super_L ] };
	};
	xkb_geometry  { include "pc(pc104)"     };
};

<LSGT> is the X11 internal name for the 102nd key. Now there’s four occurences of Super_L. As we’ve noted earlier each keycode can translate to different keysyms, depending on which modifier bits are set. The list here is what does that: the first element is used when no modifier bits are set, the second is used when shift is set, the third is for AltGr, and the fourth is for AltGr and shift simultaneously. Since we want the key to ignore all of these we translate it to the same keysym regardless of modifiers.

This description can now be saved to a file (say, exploded.xkb) and loaded into the X server with xkbcomp exploded.xkb $DISPLAY. And voilà: the ><| key functions like the windows did before its failed launch attempt. Success!

Unfortunately changing the X keyboard layout, as already mentioned, does not work very well outside of X. It also doesn’t work very well with USB keyboards, because those will use the same layout—and maybe we want to use that key for something different there? There are ways around both of those problems, but ideally we’d remap that one key only on that one exact keyboard.

Changing the X11 keyboard layout messed with the translation of keycodes to keysyms. To affect everything on the system, even stuff that’s not run inside X, we’re better off messing with the translation from scancodes to keycodes instead. And because that translation is done by the kernel, and the kernel has easy access to hardware information, we can even do it for just the broken keyboard. Sadly it also looks a lot more arcane, but it really isn’t!

This doesn’t scan though

Translation of scancodes to keycodes goes through the kernel, and the translation can be modified by adding entries to the udev hardware database. This database is made up of a bunch of files with a .hwdb extension and usually resides in /etc/udev/hwdb.d, but that can be different on different systems.

hwdb files consist of a number of blocks, each containing a match pattern that determines to which hardware devices to block applies, and a number of indented key-value pairs that describe properties of said device. We’ll use this to change the translation from scancodes to keycodes, which is conveniently done by assigning to a device a key-value pair like KEYBOARD_KEY_07=z. This particular example would translate the key 6 to the keycode that is in the US layout translated to z. (If this sounds like an odd way to write it, then welcome to the world of hardware specifications.)

To write a block we will of course need to know two things: which hardware device we’re talking about, and what the scancode of the ><| key is. Getting the hardware id of the keyboard is most easily done with evemu-describe, which will list all input devices and ask which to describe. We only have one keyboard attached and it is described as an AT keyboard with identifier dmi:bvnLENOVO:..., and since it’s unlikely that we’ll ever find another Lenovo AT keyboard to plug into this thing we’ll just use the simplest match pattern: evdev:atkbd:dmi:bvnLENOVO:*.

Next for the scancode. We’ll get this one out of evtest, which also lists all devices and asks for which to read from. Using the same keyboard as the source and pressing the ><| key, we get the scancode 56:

Event: time 1628119925.557511, type 4 (EV_MSC), code 4 (MSC_SCAN), value 56
Event: time 1628119925.557511, type 1 (EV_KEY), code 86 (KEY_102ND), value 0
Event: time 1628119925.557511, -------------- SYN_REPORT ------------

The scancode given here is already in hexadecimal as expected by hwdb, so we can just copy it over. Also, see that KEY_102ND in there? Yeah, that’s the name of that key. Because it’s the 102nd key on the keyboard and doesn’t have any function on the US keyboards, what for not being a thing there. Brilliant naming, right?

So we end up with a hwdb file that looks like this:

evdev:atkbd:dmi:bvnLENOVO:*
 KEYBOARD_KEY_56=leftmeta

Here leftmeta is the name for the windows key. The list of all key names is available, and there are quite a few unexpected entries in there. (Ever needed a bass boost key on your computer? We got you covered!)

Now it’s just a matter of saving this file, running udevadm trigger to reload the translation table, and we have a fix that works outside of X.

Neat!