Low-level mapping of key combinations in GNU/Linux (remapping the unremappable)

Keyboard by  ainul muttaqin from the Noun Project

This is a “brief” tale about how I used uinput and evdev to remap some keys of my latop’s keyboard. Using this “low-level” approach it is possible to remap “unreamappable” key bindings, such as Alt+Right to End or Alt+Left to Home.

I’ve been using, almost exclusively, an Apple laptop for the last couple of years; among other things, I got very used to keyboard mapping and especially using the combinations Cmd+Left for Home, Cmd+Right for End, Cmd+Up for PageUp and, Cmd+Down for PageDown.

Recently I decided to try to use, again, a GNU/Linux laptop for non-work-related computing activities. I installed Debian testing on a 2017 Razer Blade laptop, a bit old but still very performant; the setup went smooth and everything worked perfectly.

Everything was fine except my fingers tried always to use LeftAlt+Left for Home, LeftAlt+Right for End, and so on. I guess my fingers (and brain) don’t care much which OS is running and what keyboard I am using. When I need to move the cursor to the end of the line my left thumb reaches for the key right before the space bar (which is Alt on this keyboard) and the ring finger of my right-hand presses the Right arrow button.

I assumed that it would have been an easy fix to configure the key-combinations LeftAlt+arrow for Home, End, PageUp, and PageDown. It wasn’t, I spent a few hours tinkering with xmodmap, xbindkeys, and xdotool with not much luck. The closest I got was to emit an End button event when I pressed LeftAlt+Right, but the environment registered that Alt was toggled, so the resulting key combination was Alt+End.

I looked around a little more, on a Wiki page I read that it was impossible to remap the Alt+<Key> combination to another key”; “impossible” shouldn’t be used referring to what can be or not done on a GNU/Linux system.

I realized that it was clear that I needed a “low-level” approach, that is trying to intercept the keyboard events and replace the Alt+<Key> events with a single <OtherKey> event.

Capturing keyboard events

The first step towards the solution of my problem is to capture the events of the keyboard, that is having a program that receives and makes sense of the “raw” keyboard events.

Linux offers a very simple interface for accessing low-level events from input devices: for each device, Linux creates a character device in /dev/input/eventX, the user-level application opens the appropriate file and receives the events by performing blocking or nonblocking reads. Each event is contained in a very simple data structure:

struct input_event {
        struct timeval time;
        unsigned short type;
        unsigned short code;
        unsigned int value;
};

Where time is the timestamp at which the event happened; type is the type of event, for example, a key press; code is the event code, for example, which key was pressed; value is the value the event carries, which in the case of a “key” event is 0 for release, 1 for a keypress, and 2 for autorepeat. Detailed information is available in the official Linux kernel documentation, https://www.kernel.org/doc/html/latest/input/input.html.

So I wrote a simple code for printing out the events generated from the keyboard.

#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <unistd.h>

int main(int agc, char **argv) {
	int fd;
	struct input_event event;

	fd = open(argv[1], O_RDONLY);
	for (;;) {
		read(fd, &event, sizeof(event));
		printf("<%ld.%06ld> type: 0x%x code: %d value: %d\n",
		       event.time.tv_sec, event.time.tv_usec,
		       event.type, event.code, event.value);
	}
}

Before running this code I had to figure out which event character device file (one of those in /dev/input/eventX) was associated with the keyboard. I looked at the content of /proc/bus/input/devices and I found that the keyboard was served by /dev/input/event9:

...
N: Name="Razer Razer Blade Keyboard"
P: Phys=usb-0000:00:14.0-8/input1
S: Sysfs=/devices/pci0000:00/0000:00:14.0/usb1/1-8/1-8:1.1/0003:1532:0224.0003/input/input11
U: Uniq=
H: Handlers=sysrq kbd event9 
B: PROP=0
...

At this point, I could run the previous code using /dev/input/event9 as an argument, the output looked like this:

<1638022392.506256> type: 0x4 code: 4 value: 458763
<1638022392.506256> type: 0x1 code: 35 value: 1
<1638022392.506256> type: 0x0 code: 0 value: 0
<1638022392.574383> type: 0x4 code: 4 value: 458763
<1638022392.574383> type: 0x1 code: 35 value: 0
<1638022392.574383> type: 0x0 code: 0 value: 0
<1638022393.227261> type: 0x4 code: 4 value: 458764
<1638022393.227261> type: 0x1 code: 23 value: 1
<1638022393.227261> type: 0x0 code: 0 value: 0
<1638022393.282272> type: 0x4 code: 4 value: 458764
<1638022393.282272> type: 0x1 code: 23 value: 0
<1638022393.282272> type: 0x0 code: 0 value: 0

This output was generated when I pressed the “h” and “i” keys. Let’s try to make some sense from the output.

Looking at the timestamps it seems like that there’s some grouping, each group is of three events and they are all in the same order: 0x4, 0x1, 0x0.

Macros to make sense out of type and code are defined in /usr/include/linux/input-event-codes.h. Using this file we can now manually decode the output:

type: EV_MSC code: MSC_SCAN
type: EV_KEY code: KEY_H value: 1       // press
type: EV_SYN code: SYN_REPORT value: 0

type: EV_MSC code: MSC_SCAN
type: EV_KEY code: KEY_H value: 0       // release
type: EV_SYN code: SYN_REPORT value: 0

...

Each time I hit a key on the keyboard there are six events, in two groups; each group starts with a “miscellaneous” event containing the scan code, the key pressed or released, and a “synchronization” report for reporting the event.

Creating a virtual keyboard

The next step towards my solution is to create a virtual keyboard that “forwards” the events of the physical keyboard.

Linux offers a module, uinput, for emulating input devices from userspace. libevdev is a library for handling event kernel devices, among the other functionalities it offers API for creating emulated devices using uinput.

Using libevdev and combining the previous code it was very simple to implement a virtual keyboard that just replicates the event of the physical keyboard.

#include <fcntl.h>
#include <linux/input.h>
#include <stdio.h>
#include <unistd.h>
#include <libevdev/libevdev-uinput.h>

int main(int agc, char **argv) {
	struct input_event event;

	int kbd_fd;
	int uinput_fd;

	struct libevdev *kbd_dev;
	struct libevdev_uinput *virtkbd_dev;

	const char *virtkbd_path;
	
	kbd_fd = open(argv[1], O_RDONLY);
	libevdev_new_from_fd(kbd_fd, &kbd_dev);

	uinput_fd = open("/dev/uinput", O_RDWR);	
	libevdev_uinput_create_from_device(kbd_dev,
					   uinput_fd,
					   &virtkbd_dev);

	virtkbd_path = libevdev_uinput_get_devnode(virtkbd_dev);
	printf("Virtual keyboard device: %s\n",
	       virtkbd_path);
	
	for (;;) {
		read(kbd_fd, &event, sizeof(event));
		if (event.type != EV_KEY) {
			continue;
		}
		libevdev_uinput_write_event(virtkbd_dev,
					    event.type,
					    event.code,
					    event.value);
		libevdev_uinput_write_event(virtkbd_dev,
					    EV_SYN,
					    SYN_REPORT,
					    0);
	}
}

Using libevdev_uinput_create_from_device() we create a new “emulated” input device based on the real device, that is having the same characteristics and capabilities to emit the same kind of events.

The code uses libevdev_uinput_write_event() to emit the event using the emulated device; in the main loop all the events that aren’t of type EV_KEY are ignored. All the EV_KEY are verbatim replicated through the emulated device, and also we emit a SYN_REPORT after each EV_KEY.

We can compile this code with:

gcc $(pkg-config libevdev --cflags) replicate-events.c $(pkg-config libevdev --libs) -o replicate-events

When I run this code using the same /dev/input/event9 as an argument, it prints:

Virtual keyboard device: /dev/input/event26

It created a new device and its associated event node in /dev/input/event26, now I could use this as an argument of the previous utility to check that the events are replicated correctly, the output when I typed “hi” looked like this:

<1638027675.383862> type: 0x1 code: 35 value: 1
<1638027675.383862> type: 0x0 code: 0 value: 0
<1638027675.473934> type: 0x1 code: 35 value: 0
<1638027675.473934> type: 0x0 code: 0 value: 0
<1638027675.570950> type: 0x1 code: 23 value: 1
<1638027675.570950> type: 0x0 code: 0 value: 0
<1638027675.647956> type: 0x1 code: 23 value: 0
<1638027675.647956> type: 0x0 code: 0 value: 0

In this case, the miscellaneous events are missing and we can see the same sequence of press and release key events each one followed by a SYN_REPORT.

Remapping Alt+Right to End

Starting from the code for replicating the events, it seems simple to remap the key combinations with some simple changes:

  1. Register locally the press, release events of LeftAlt;
  2. Skip publishing LeftAlt events;
  3. Replace the Right event with End when LeftAlt is pressed down.

This is easily achievable by replacing the main loop in the previous code with:

	int leftalt_down = 0;
	for (;;) {
		read(kbd_fd, &event, sizeof(event));
		if (event.type != EV_KEY) {
			continue;
		}
		if (event.code == KEY_LEFTALT) {
			leftalt_down = event.value;
			continue;
		}
		if (leftalt_down && event.code == KEY_RIGHT) {
			event.code = KEY_END;
		}
		libevdev_uinput_write_event(virtkbd_dev,
					    event.type,
					    event.code,
					    event.value);
		libevdev_uinput_write_event(virtkbd_dev,
					    EV_SYN,
					    SYN_REPORT,
					    0);
	}

When I use the “print-events” utility using as an argument the “virtual keyboard event” I receive the following output when I press LeftAlt+Right:

<1638029144.310424> type: 0x1 code: 107 value: 1
<1638029144.310424> type: 0x0 code: 0 value: 0
<1638029144.390434> type: 0x1 code: 107 value: 0
<1638029144.390434> type: 0x0 code: 0 value: 0

And this is the desired output, the emulated keyboard doesn’t emit the event for the LeftAlt key and emits the press and release of End (code 107).

Unfortunately, when I try it in a text editor (GNU Emacs) the cursor doesn’t move to the end of the line; if I try it in a terminal emulator it prints some gibberish. When using xev to investigate the error I quickly found out that the X11 environment received events for three keys: LeftAlt, Right, End; that’s because X11 uses both keyboards and it receives two press and release (LeftAlt, Right) from the real keyboard and End from the emulated keyboard.

Preventing access to the physical keyboard from other processes

A solution to the previous problem, X11 receiving events from two keyboards, is to prevent other processes to receive events from the physical keyboard. Linux offers the functionality of “grabbing” an event device; when a process grabs a device it becomes the sole recipient for all input events coming from the device.

To do so we can simply use ioctl() using the file descriptor of the open event descriptor with EVIOCGRAB request.

Adding the following two lines of code just before the main loop will grant exclusive access to the keyboard to our application and everything will work as expected (most of the time).

	int grab = 1;
	ioctl(kbd_fd, EVIOCGRAB, &grab);

There’s still a small problem while testing this code I noticed that sometimes just after running it, there are several new lines in the terminal emulator and for a while, the keyboard doesn’t work properly.

That’s because I type the command and press “Enter” to start the application and X11 might not always receive the release of the “Enter” key before my application grabs exclusive access to the keyboard events.

A small sleep, 100ms, before grabbing exclusive access will solve the problem.

	usleep(100000);
	int grab = 1;
	ioctl(kbd_fd, EVIOCGRAB, &grab);

Publishing LeftAlt when not used for remapping keys

There’s (at least) one annoying problem left to solve with the code so far: the LeftAlt will be always ignored and cannot be used for common key combinations; for example, I want to still use it when I use GNU Emacs for the M-x combination.

Ideally, the complete solution should avoid publishing LeftAlt when used for remapping keys and publish it regularly otherwise.

In the new code, when LeftAlt is pressed or released I record it as previously in the leftalt_down variable, but this time I will publish the event.

Whenever the LeftAlt key is pressed if an event for a mappable key is received, I will send a “release” of the LeftAlt before sending a “press” event of the mapped key and a “press” event of LeftAlt key after sending a “release” event of the mapped key.

First of all, I wrote a function for publishing a key event:

void send_key_event(struct libevdev_uinput *dev,
		    unsigned int code, int value) {
	libevdev_uinput_write_event(dev,
				    EV_KEY,
				    code,
				    value);
	libevdev_uinput_write_event(dev,
				    EV_SYN,
				    SYN_REPORT,
				    0);

}

And then I modified the main loop according to the previous description:

	for (;;) {
		read(kbd_fd, &event, sizeof(event));
		if (event.type != EV_KEY) {
			continue;
		}
		if (event.code == KEY_LEFTALT) {
			leftalt_down = event.value;
		}
		if (leftalt_down && event.code == KEY_RIGHT) {
			if (event.value == 1) {
				send_key_event(virtkbd_dev,
					       KEY_LEFTALT,
					       0);
				event.code = KEY_END;
			} else if (event.value == 0) {
				send_key_event(virtkbd_dev,
					       KEY_END,
					       event.value);
				event.code = KEY_LEFTALT;
				event.value = 1;
			}
		}
		send_key_event(virtkbd_dev, event.code, event.value);
	}

As long I am careful to release LeftAlt after releasing Right, this code works well, but if I release LeftAlt before releasing the Right key the code will never send a release event for the End key.

An obvious solution to this problem is to record which key code was used for the “press” event end reuse the same code for the “release” event.

When a “mappable” key is pressed we record which key code to publish, and again, when the LeftAlt is pressed a “release” event of LeftAlt is published before publishing the mapped key code. When the “mappable” key is released we sent the “release” event of the recorded key code.

The revised main loop of the code, looks like:

	int key_right_code = 0;
	for (;;) {
		read(kbd_fd, &event, sizeof(event));
		if (event.type != EV_KEY || event.value > 1) {
			continue;
		}
		if (event.code == KEY_LEFTALT) {
			leftalt_down = event.value;
		}
		if (event.code == KEY_RIGHT) {
			if (event.value == 0) {
				// release
				send_key_event(virtkbd_dev,
					       key_right_code,
					       0);
				if (leftalt_down) {
					send_key_event(virtkbd_dev,
						       KEY_LEFTALT,
						       1);
				}
				continue;
			}
			// press
			key_right_code = KEY_RIGHT;
			if (leftalt_down) {
				key_right_code = KEY_END;
				send_key_event(virtkbd_dev,
					       KEY_LEFTALT,
					       0);
			}
			send_key_event(virtkbd_dev,
				       key_right_code,
				       1);
			continue;
		}
		send_key_event(virtkbd_dev, event.code, event.value);
	}

Conclusions

In the same way it is possible to remap the other “arrow” keys, I’v also disabled “CapsLock” and mapped Alt+C and Alt+V to Copy and Paste.

All the code is available on https://github.com/cjg/heud/tree/main/examples

Happy hacking!

One thought on “Low-level mapping of key combinations in GNU/Linux (remapping the unremappable)

  1. Wow. I just stumbled on this from a web search, and it turns out you only just wrote it.

    This sounds like a really nice way to do things. After 10 years on linux followed by 6 years on macos, whenever I try to go back, it’s the key bindings that stop me from turning the live usb stick into a permanent install. I will have to try this out and see how I find it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s