Home

Handling user input in headless programs with libinput

Recently, we’ve been working on a robot at school, and we wanted a way to control it remotely. The remote part is solved by the Linux kernel and its drivers, but I still had to find a way to process keyboard input in our robot’s program.

There are several options for processing input in graphical programs, but for headless applications, you’ll need something different.

Like a lot of other things on Linux, input handling is done by opening a file and calling ioctls on the file descriptor.

libevdev is a wrapper around these ioctls. However, you still need to specify the device paths yourself, so you also need to manually write code for device discovery, using something like libudev.

libinput is an input handling library that was developed for processing user input in Wayland compositors. It has a udev backend, and a simple to use API. Just what I needed.

To start off, we need to include the header files:

#include <libinput.h>
#include <libudev.h>

Next, we create a struct libinput_interface which stores two callbacks, that are used when libinput opens or closes a device file. These are named open_restricted and close_restricted. I didn’t need anything fancy, so I made the simplest possible functions:

int open_callback(const char *path, int flags, void *user_data)
{
    return open(path, flags);
}

void close_callback(int fd, void *user_data)
{
    close(fd);
}

On top of the libinput_interface struct, the libinput_udev_create_context function expects an already initialized udev context. We can easily create one like so:

struct udev *uctx = udev_new();

This is the only line of libudev code we have to write when working with libinput. Do note, that I have omitted any kind of error handling for the sake of conciseness.

Now we can finally create our libinput context:

struct libinput *lctx = libinput_udev_create_context(&linterface, NULL, uctx);

We have to assign the libinput context to a seat: Unless you have an exotic setup, like this, you can just assign it to seat0 which always exists:

libinput_udev_assign_seat(lctx, "seat0");<

We have a fully working libinput context, now it’s time to capture user input. According to the documentation, the library provides a single file descriptor that we must monitor for incoming data, and our program must call libinput_dispatch whenever data is available.

For monitoring the file descriptor for data, we can use the poll API, since it has a POLLIN flag. Our main loop can look something like this:

struct pollfd pfd = {
    .fd = libinput_get_fd(lctx),
    .events = POLLIN,
    .revents = 0,
};

while(poll(&pfd, 1, -1) > -1) {
    libinput_dispatch(lctx); // has to be called after poll()
    struct libinput_event *ev;
    while((ev = libinput_get_event(lctx))) {
        /* do things with ev */
        libinput_event_destroy(ev);
    }
}

libinput_event is a generic event type. We have to check what kind of event it is with libinput_event_get_type. Let’s check for keyboard input:

if(libinput_event_get_type(ev) == LIBINPUT_EVENT_KEYBOARD_KEY) {
    /* handle keyboard event */
}

The event types are listed here.

Keyboard events have the type struct libinput_event_keyboard. We can convert struct libinput_event to this type, and get more information:

struct libinput_event_keyboard *kbev = libinput_event_get_keyboard_event(ev);
if(libinput_event_keyboard_get_key_state(kbev) == LIBINPUT_KEY_STATE_PRESSED) {
    printf("%u was pressed\n", libinput_event_keyboard_get_key(kbev));
}

To build the program, we must link to libinput and libudev. Using pkg-config is the most straightforward way to link them:

cc event_loop.c `pkg-config --libs libudev libinput` -o event_loop

Also make sure your user is in the input group!

There we have it, the skeleton of a libinput event loop, that can be used in a headless environment. Here is the full code for the program:

#include <stdio.h>
#include <poll.h>
#include <fcntl.h>
#include <unistd.h>
#include <libinput.h>
#include <libudev.h>

int open_callback(const char *path, int flags, void *user_data)
{
    return open(path, flags);
}

void close_callback(int fd, void *user_data)
{
    close(fd);
}

int main(void)
{
    struct libinput_interface linterface = {
        .open_restricted = open_callback,
        .close_restricted = close_callback,
    };
    struct udev *uctx = udev_new();
    struct libinput *lctx = libinput_udev_create_context(&linterface, NULL, uctx);

    libinput_udev_assign_seat(lctx, &quot;seat0&quot;);

    struct pollfd pfd = {
        .fd = libinput_get_fd(lctx),
        .events = POLLIN,
        .revents = 0,
    };

    while(poll(&pfd, 1, -1) > -1) {
        libinput_dispatch(lctx); // has to be called after poll()

        struct libinput_event *ev;

        while((ev = libinput_get_event(lctx))) {
            if(libinput_event_get_type(ev) == LIBINPUT_EVENT_KEYBOARD_KEY) {
                struct libinput_event_keyboard *kbev = libinput_event_get_keyboard_event(ev);

                if(libinput_event_keyboard_get_key_state(kbev) == LIBINPUT_KEY_STATE_PRESSED) {
                    printf("%u was pressed\n", libinput_event_keyboard_get_key(kbev));
                }
            }
            libinput_event_destroy(ev);
        }
    }
}