SpaceFn implemented in the Xorg input driver

A few years ago, the user spiceBar proposed the SpaceFn keyboard layout on geekhack. The idea of SpaceFn is to let the space key work as both a regular space and a modifier. If you just press space normally, then a space appears. If you hold down space and press another key, then something different happens. For instance, you can configure Space+h/j/k/l to work as arrow keys so it's no longer necessary to move the hands away from home row in order to use the arrow keys. The spacebar is always right under your thumb, so no advanced pinky gymnastics are needed in order to reach an out-of-the-way Fn key.

I have implemented SpaceFn for Linux by modifying the Xorg input driver: https://github.com/ljosa/xf86-input-evdev-spacefn. This page describe the implementation and how to build, install, and configure it.

How it works

The SpaceFn implementation intecepts and buffers some keypresses until it knows what to do with them. As an example, suppose you press down space and then, soon after, press down H. The driver does not yet know whether you intended to (1) type a space followed by an H or (2) press the left arrow key (Space + H), so it marks that space has been pressed down and buffers the H.

There are several ways in which the driver can reach a decision about your intention and release the keypresses in the buffer:

  1. You release the spacebar. The driver decides that you intended to type a space followed by the keys in the buffer, even though you had pressed the next key before releasing space. (This is called a "rollover" and happens frequently when typing quickly.)
  2. You release a non-space key while the buffer is empty. This must mean that you pressed the non-space key before you pressed down the spacebar, so you intended to type that other letter followed by a space. (This is another form of rollover.)
  3. You release a non-space key while the buffer is not empty. The driver assumes that you intended to modify the keys in the buffer, so it ensures that a modifier keypress has been sent, then sends the keypress event(s) in the buffer, and finally sends the release event.
  4. It has been a while (200 ms) since the non-space key was added to the buffer. The driver assumes that you intended to modify the key, so it ensures that a modifier keypress has been sent, then sends the keypress event(s) in the buffer.
  5. A non-space key is pressed and it's already been a while (150 ms) since the spacebar was pressed down. The driver assumes that you intended to modify the key, so it doesn't buffer it. Instead, the driver ensures that a modifier keypress has been sent, then sends the keypress event for the non-space key.

These rules seem to work well, but they are not perfect:

This algorithm seems to work very well for me, but people type in different ways, so if you try it and find that it causes mistakes when you type, I'd love to hear from you. We may have to adjust the timeouts or revise the algorithm.

Other places SpaceFn could be implemented

The Xorg input driver is perhaps not the ideal place to implement SpaceFn.

I tried to write a Linux kernel module, but modifying and changing keyboard events in the keyboard notifier chain turned out to affect only the virtual terminals and not evdev, so it didn't work for X. And I didn't find anywhere else in the Linux kernel that looked like it might work and be accessible to a kernel module.

In Xorg, perhaps xkb would be a more logical place for this functionality, but I didn't immediately see where it would go. So for now it's in the input driver.

Some people have tried to implement SpaceFn with xcape, a program that is typically used to make left control act as escape when pressed and released on its own. The program is just an X client, so that is simpler than installing a driver: all you have to do is run it. Xcape works by eavesdropping on X events with X's Record extension. If it sees that the modifier has been pressed and then released without any other keypresses in-between, then xcape synthesizes a keypress with Xtest.

That works fairly well for the left-control-as-escape case because the key is normally used as a modifier; when it is pressed alone, it's a fairly unusual situation, and it's not too much of a chore to release the key before pressing something else. But it doesn't work well at all for space because one uses the space bar all the time when typing quickly. When typing quickly, one often "rolls over," pressing the next key before releasing the previous key. If Space+h is left arrow, then typing "come here" quickly comes out as "come←ere" and you see "comereh".

I tried to improve xcape by making it send the synthetic space event unless another key has been both pressed and released while space is held. That worked in the sense that it generated the missing space in the use case we discussed, but the keypress event for "h" still had the modifier bit set, so it ended up as a "←". And "come ←ere" is not much better than "come←ere".

The fundamental problem is that the SpaceFn implementation needs to be able to intercept and modify and/or prevent keyboard events, and Xrecord/Xtest can't do that. There used to be an extension called Xevie for intercepting events, but it has been removed years ago.

Building and installing the input driver

I build the modified input driver for Ubuntu 16.04 under Docker so I don't have to clutter my system with all the dependencies. Here is the Dockerfile: https://gist.github.com/ljosa/b626f9fdd86424ce9923530ab561fbaf

Build the container with

docker build -t spacefn-build .

where . refers to the directory where the Dockerfile is located. You can then check out the xf86-input-evdev-spacefn repo and build it:

git clone git@github.com:ljosa/xf86-input-evdev-spacefn.git
docker run --rm=true -v `pwd`:/src spacefn-build

You should end up with a file called evdev_drv.so in the current directory. Copy this file to the right place to install it:

sudo cp evdev_drv.so /usr/lib/xorg/modules/input/evdev_drv.so

Note that copying the file will kill the X server. It will restart with the modified driver.

Configuring

The implementation will send keycode 135 ("menu") when space is being used as a modifier. You probably already have that key as Mod5, so you can just use Xmodmap to bind the keycode to Mode_switch and then add whatever you want as the second pair of keysyms on the letter keys:

keycode 135 = Mode_switch Mode_switch Mode_switch Mode_switch
keycode 56 = b B space space    
keycode  43 = h H Left Left
keycode  44 = j J Down Down
keycode  45 = k K Up Up
keycode  46 = l L Right Right
keycode  57 = n N Next Next
keycode  58 = m M Prior Prior
keycode  30 = u U Home Home
keycode  32 = o O End End
keycode  10 = 1 exclam F1 F1
keycode  11 = 2 at F2 F2
keycode  12 = 3 numbersign F3 F3
keycode  13 = 4 dollar F4 F4
keycode  14 = 5 percent F5 F5
keycode  15 = 6 asciicircum F6 F6
keycode  16 = 7 ampersand F7 F7
keycode  17 = 8 asterisk F8 F8
keycode  18 = 9 parenleft F9 F9
keycode  19 = 0 parenright F10 F10
keycode  20 = minus underscore F11 F11
keycode  21 = equal plus F12 F12   

If you put this in your \~/.Xmodmap, then it should be read when you log in. You can read it immediately with xmodmap - < ~/.Xmodmap.


Last updated: 2017-12-29