I recently encountered some problem on making a program called ScreenKey for SerenityOS, it displays the user keyboard inputs in a window, for example your typing sequence Ctrl, C then Enter.

I do this, because I send patches fixing text editor bugs in SerenityOS, and it’s more convincing to record a video demonstrating the "before/after" fixing the bug so that my patches can be merged. Because they are all related to text editor, it’s necessary to show your keyboard inputs so others will know how to reproduce the bug.

BUT, there is no such keyboard echo program in SerenityOS (running in qemu), I have to use an external application on my host machine and capture my full computer screen, which is not my desire. So I decided to write one :^)

The first thing is to get input keys, it can implemented by reading from the keyboard input device.

I write a keyboard echo program keycho.c, it merely prints key codes in hex and decimal, you can visit here for the key code mapping in linux. Try compiling this piece of code to see if it actually works. Note that it only works and tested on linux systems.

keycho.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <sys/select.h>
#include <linux/input.h>

const char *const evval2str[3] = {
    "Released",
    "Pressed",
    "Repeated"
};

int main(int argc, char *argv[])
{
    /* no canonical mode and don't echo input characters */
    struct termios t;
    tcgetattr(STDIN_FILENO, &t);
    t.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &t);

    const char *kbd_input_path = "/dev/input/by-path/platform-i8042-serio-0-event-kbd";
    int kbdfd = open(kbd_input_path, O_RDONLY | O_CLOEXEC);
    if (kbdfd == -1) {
        fprintf(stderr, "Error on opening keyboard input\n");
        exit(EXIT_FAILURE);
    }

    struct input_event ev;
    while (1) {
        int nread = read(kbdfd, &ev, sizeof(struct input_event));
        if (nread == sizeof(struct input_event)) {
            if (ev.type == EV_KEY && ev.value >= 0 && ev.value <= 2)
                printf("%s 0x%04x (%d)\n", evval2str[ev.value], (int)ev.code, (int)ev.code);
        }
    }

    return 0;
}

So it works ── only in terminal. I want some GUI application, so there must be a UI thread (main thread), we cannot read() key inputs in the UI thread because it will BLOCK the application! You might want to make the keyboard file descriptor non-blocking O_NONBLOCK ── but at a cost of wasting cpu cycles on looping. It’s obvious and necessary to have a new thread reading those keys!

The basic idea of the application is simple:

  • Create a thread listening on keyboard inputs, it appends those keycodes to a shared queue.
  • The main thread renders key codes in the shared queue.
  • A timer is reset immediately after painting key codes, and queue is cleared when timer triggered.

HOWEVER, here’s the problem, if we invoke something like join() in the main thread when the application is about to exit, it will wait for the listening thread to terminate, but the listener is BLOCKING on read()!

The self-pipe trick can solve this problem. It’s a well-known "trick", very portable and cross-platform ── works across all different kinds of Unix systems.

  1. Create a pipe and make both ends non-blocking.
  2. Add the read end of the pipe to the read file descriptors set of I/O multiplexor, like select() or poll().
  3. Write a byte to the write end of the pipe when a signal handler is called. Also check if the read end of the pipe is ready when select() / poll() returns successfully.

For better illustration, here’s a C++ application in Qt-like API for displaying key press.

SomeWidget
class SomeWidget : public Widget {
public:
    SomeWidget();
    virtual ~SomeWidget();

    virtual void on_paint();

private:
    int m_kbd_fd { -1 };
    int m_pipe_fds[2] { -1 };
    Thread* m_listener { nullptr };
    Mutex m_mutex { };
    Queue<KeyCode> m_keycodes { };
    Timer* m_timer { };
}
SomeWidget Constructor
SomeWidget::SomeWidget()
{
    m_kbd_fd = open("/path/to/keyboard/input/device", O_RDONLY | O_CLOEXEC);

    /* Create a pipe */
    int rc = pipe(m_pipe_fds);
    assert(rc != -1);

    /* Make both pipe ends non-blocking */
    int flags = fcntl(m_pipe_fds[0], F_GETFL);
    assert(flags != -1);
    flags |= O_NONBLOCK;
    rc = fcntl(m_pipe_fds[0], F_SETFL, flags);
    assert(rc != -1);

    flags = fcntl(m_pipe_fds[1], F_GETFL);
    assert(flags != -1);
    flags |= O_NONBLOCK;
    rc = fcntl(m_pipe_fds[1], F_SETFL, flags);
    assert(rc != -1);

    /* Create a listening thread */
    m_listener = Thread::create([this]() {
        /* Add keyboard fd and read end of pipe to readfds */
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(m_kbdfd, &readfds);
        FD_SET(m_pipefds[0], &readfds);

        while (true) {
            int rc = select(2, &readfds, NULL, NULL, NULL);
            if (rc == -1)
                Thread::exit(1);

            if (FD_ISSET(m_pipefds[0], &readfds)) {
                /* About to quit */
                Thread::exit(0);
            } else if (FD_ISSET(m_kbdfd, &readfds)) {
                /* Read keycode */
                KeyEvent ev;
                int nread = read(m_kbdfd, &ev, sizeof(KeyEvent));
                if (nread == sizeof(KeyEvent)) {
                    Thread::lock(m_mutex);
                    m_keycodes.push(event.key);
                    Thread::run_on_main([this]() {
                        update();   // will trigger on_paint()
                    };
                    Thread::unlock(m_mutex);
                } else if (nread == 0) {
                    continue;
                } else {
                    Thread::exit(1);
                }
            }
        }
    });

    /* Clear key codes if no more keyboard inputs after 2s */
    m_timer = Timer::create(2000, [this]() {
        Thread::lock(m_mutex);
        m_keycodes.clear();
        Thread::unlock(m_mutex);
    })
}

Write a byte to the pipe to notify the listening thread, so that join() will return.

SomeWidget Destructor
SomeWidget::~SomeWidget()
{
    /* Notify the listener to quit, it's a non-blocking pipe, so it immediately returns */
    char c = 'x';
    write(m_pipe_fds[1], &c, sizeof(char));

    /* Wait for the listener to join */
    Thread::join(m_listner);

    close(m_kbd_fd);
    close(m_pipe_fds[0]);
    close(m_pipe_fds[1]);
}
Draw keycodes
void SomeWidget::on_paint()
{
    /* Paint keycodes */
    for (KeyCode const& kc : m_keycodes)
        paint_text(kc, some_font);

    /* Reset timer */
    m_timer->reset();
}

Now the application exits gracefully :D

Until next time!