My operating system is Arch Linux and my desktop environment uses Wayland. I've been trying to make a small renderer in the terminal which moves the camera around using WASD. I've disabled canonical terminal input to read the characters as soon as they come in and disabled echo to not print the characters.
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
struct termios orig_term;
void reset_terminal_mode() {
tcsetattr(STDOUT_FILENO, TCSANOW, &orig_term);
}
int main() {
tcgetattr(STDOUT_FILENO, &orig_term);
struct termios new_term = orig_term;
new_term.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDOUT_FILENO, TCSANOW, &new_term);
atexit(reset_terminal_mode);
at_quick_exit(reset_terminal_mode);
char c;
int bytes;
int len;
while (true) {
ioctl(STDOUT_FILENO, FIONREAD, &bytes);
ReadKeyboard:
len = read(STDOUT_FILENO, &c, sizeof(c));
bytes -= len;
if (c == 'q') {
return 0;
}
printf("%c\n", c);
if (bytes > 0) {
goto ReadKeyboard;
}
}
return 0;
}
The issue is, when multiple keys are held down at the same time, the terminal only reads one of them. In addition to this, inputs that are read move the camera once, wait a small amount, and then start moving it in the given direction because of result of auto-repeat. I'm hoping that I can solve both of these issues by being able to detect key up and key down events, though any other methods would also be greatly appreciated.
The terminal is not designed to give you low-level keyboard input. The terminal just gives you characters — the consequences of pressing and releasing some combination of keys in the right order.
Modern terminal emulators are significantly more awesome than what we’ve had before, but in the end they are limited to just giving you characters.
Consequently, getting key release information requires some special magic, like the Linux Input Subsystem.
It is basically a hook, allowing you to see all device input occurring on your system regardless of whether or not your application has input focus. As such it is a protected system requiring access to the “input
” group (usually—it is sometimes called something else).
If you open a terminal you can list the files in /dev/input
and read the fourth column to see what the group name is. You can then add yourself to the that group with the usermod program. Here is how “Flynn” did it:
~ $ ls -l /dev/input |sed -n '4p'
crw-rw---- 1 root input 13, 64 Jun 3 20:50 event0
~ $ sudo usermod -a -G input flynn
[sudo] password for flynn:
~ $ █
After typing his password the task is done, and all that is left is to log out and log back in to get the new superpower.
Glancing again over the evtest.c example code I recommended earlier, I noticed something missing from it — it does not explicitly handle SYN_DROPPED
events, which it should. Philip here on SO has written an example of how to properly parse events. Notice in particular how the SYN_DROPPED
event is handled.
In addition, the entire event sequence should be read — up to and including the EV_SYN
report terminating an event sequence. You are not obligated to pay attention to any of it except the key code and press/release status, but you should be careful not to get behind on reading it.
The evtest program requires you to tell it which event file to observe. This is true of most examples you will find online. There are a plethora of ways you can explore this, including the /proc/bus/input
filesystem, but you can do it directly in code very easily without having to parse a bunch of little text files.
Here is a little module I crufted together (Boost Licensed, which is friendlier than CC-BY-SA) that automatically finds and opens the keyboard(s) and mice on your system. I will likely produce a more polished and complete version at some future date — if I do I’ll put it up on Github and put a link here, but for now this is what you get.
initialize-devices.h
// Copyright 2025 Michael Thomas Greer
// SPDX-License-Identifier: BSL-1.0
#ifndef DUTHOMHAS_INITIALIZE_DEVICES_EXAMPLE_H
#define DUTHOMHAS_INITIALIZE_DEVICES_EXAMPLE_H
//-------------------------------------------------------------------------------------------------
// A convenient helper function
//-------------------------------------------------------------------------------------------------
#include <stdnoreturn.h>
noreturn void rage_quit(const char * message);
//-------------------------------------------------------------------------------------------------
// Keyboard and mouse devices
//-------------------------------------------------------------------------------------------------
// It is entirely possible for more (or fewer) than a single mouse and keyboard to exist on
// a system. This library doesn’t bother to distinguish them, and provides no useful way to
// distinguish them besides their ordering in the lists below (which is consistent with
// `/dev/input/eventN`).
//
#ifndef MAX_OPEN_INPUT_DEVICES
#define MAX_OPEN_INPUT_DEVICES 10
#endif
#include <stdbool.h>
#include <stddef.h>
#include <poll.h>
extern struct pollfd input_devices[MAX_OPEN_INPUT_DEVICES];
extern struct pollfd * keyboards;
extern struct pollfd * mice;
extern int num_devices;
extern int num_keyboards;
extern int num_mice;
void initialize_devices(bool want_mice);
// Initialize keyboard and mouse (if you want_mice) devices.
// Prints an error message to stderr and terminates with exit code 1 on failure to
// find at least a keyboard.
char * get_device_name(int fd, char * name, size_t n);
char * get_device_path(int fd, char * path, size_t n);
// Return information about the device in the argument buffer.
// Returns NULL on failure.
// The device name may be reported as an empty string!
//-------------------------------------------------------------------------------------------------
// Poll/wait for input
//-------------------------------------------------------------------------------------------------
// The poll*() functions find and return the file descriptor for the first device
// with input waiting to be read. You can ask for all devices, just keyboards, just
// mice, or an specific device, keyboard, or mouse.
//
// The timeout is as with poll():
// • INFINITE (or any negative value) means wait for input
// • 0 means test and return immediately
// • >1 means number of milliseconds to wait before giving up and returning
//
// Returns INVALID_FD if no indicated device signals input is available.
//
#ifndef INFINITE
#define INFINITE (-1)
#elif INFINITE != -1
#warning "INFINITE macro is already defined as something other than (-1)!"
#endif
int poll_devices (int timeout_ms); int poll_device (int n, int timeout_ms);
int poll_keyboards(int timeout_ms); int poll_keyboard(int n, int timeout_ms);
int poll_mice (int timeout_ms); int poll_mouse (int n, int timeout_ms);
#endif
initialize-devices.c
// Copyright 2025 Michael Thomas Greer
// SPDX-License-Identifier: BSL-1.0
#define extern
#include "initialize-devices.h"
#undef extern
//-------------------------------------------------------------------------------------------------
// Macro helpers. Most of these are simplified forms that have no place in a header file.
//-------------------------------------------------------------------------------------------------
// A fairly type-agnostic macro swap
#ifndef swap
#define swap(a,b) do { __typeof__(a) _ = a; a = b; b = _; } while (0)
#endif
// Macro helper
#ifndef DUTHOMHAS_FIRST
#define DUTHOMHAS_FIRST(...) DUTHOMHAS_FIRST_(__VA_ARGS__,0)
#define DUTHOMHAS_FIRST_(a,...) a
#endif
// Simplified version of strcats() for concatenating strings
#ifndef strcats
#define strcats(...) strcats_(__VA_ARGS__,"")
#define strcats_(s,a,...) strcat(strcat(s, a), DUTHOMHAS_FIRST(__VA_ARGS__))
#endif
// Simplified version of reentry guard
#ifndef NO_REENTRY
#define NO_REENTRY static int _##__LINE__ = 0; if (!_##__LINE__) _##__LINE__ = 1; else return;
#endif
// File descripter helpers
#ifndef INVALID_FD
#define INVALID_FD (-1)
#endif
#ifndef IS_VALID_FD
#define IS_VALID_FD(fd) ((fd) >= 0)
#endif
//-------------------------------------------------------------------------------------------------
// A convenient helper function
//-------------------------------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
noreturn
void rage_quit(const char * message)
{
fprintf(stderr, "%s\r\n", message);
exit(1);
}
//-------------------------------------------------------------------------------------------------
// Information necessary to find the /dev/input devices we want.
//
// Keyboards are EV=120013 [SYN, KEY, MSC, LED, REP]
// Mice are EV=17 [SYN, KEY, REL, MSC]
// Joysticks, Touchpads, and Pens (EV=1B [SYN, KEY, ABS, MSC]) are not handled by this library.
//
// AFAIK an “LED” is a _status_ state for Caps Lock, Num Lock, etc. Some modern keyboards let
// you set the physical light independent of the lock keys, but IDK how that relates here.
//
// MSC:SCAN is for reporting raw “scan code” key/button values.
//
#include <iso646.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <fcntl.h>
#include <poll.h>
#include <termios.h>
#include <unistd.h>
#include <linux/input.h>
#include <linux/input-event-codes.h>
enum { KEYBOARD_DEVICE_TYPE, MOUSE_DEVICE_TYPE, NUM_DEVICE_TYPES };
static
struct
{
unsigned long capabilities;
int test_key;
}
desired_devices[NUM_DEVICE_TYPES] =
{
{ 0x120013, KEY_ESC }, // SYN, KEY, MSC:SCAN, LED, REP
{ 0x17, BTN_MOUSE }, // SYN, KEY, REL, MSC:SCAN
};
//-------------------------------------------------------------------------------------------------
// Functions to get information about a /dev/input device
//-------------------------------------------------------------------------------------------------
char * get_device_name(int fd, char * name, size_t n)
{
int count = ioctl(fd, EVIOCGNAME(n), name);
if (count < 0) *name = '\0';
return (count < 0) ? NULL : name;
}
char * get_device_path(int fd, char * path, size_t n)
{
char spath[50];
sprintf(spath, "/proc/self/fd/%d", fd);
ssize_t len = readlink(spath, path, n);
if (len < 0)
{
*path = '\0';
return NULL;
}
path[len] = '\0';
return path;
}
static
bool is_character_device(const char * filename)
{
struct stat sb;
return (stat(filename, &sb) == 0) and ((sb.st_mode & S_IFMT) == S_IFCHR);
}
static
unsigned long long ev_get_id(int fd)
{
#define LSHIFT(x,n) ((unsigned long long)(id[x]) << n*16)
unsigned short id[4];
return (ioctl(fd, EVIOCGID, id) == 0)
? LSHIFT(ID_BUS,3) | LSHIFT(ID_VENDOR,2) | LSHIFT(ID_PRODUCT, 1) | id[ID_VERSION]
: 0;
#undef LSHIFT
}
static
unsigned long ev_get_capabilities(int fd)
{
unsigned long bits = 0;
return (ioctl(fd, EVIOCGBIT(0, sizeof(bits)), &bits) >= 0) ? bits : 0;
}
static
bool ev_has_key(int fd, unsigned key)
{
unsigned char bits[KEY_MAX / 8 + 1];
memset(bits, 0, sizeof(bits));
return
(ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(bits)), bits) >= 0) and
(bits[key / 8] & (1 << (key % 8)));
}
//-------------------------------------------------------------------------------------------------
// Finalization for /dev/input/eventN devices
//-------------------------------------------------------------------------------------------------
static
void finalize_devices(void)
{
if (num_devices)
while (num_devices--)
{
tcflush(input_devices[num_devices].fd, TCIFLUSH);
close (input_devices[num_devices].fd);
if (num_mice) --num_mice;
else if (num_keyboards) --num_keyboards;
}
}
//-------------------------------------------------------------------------------------------------
// Initialization for /dev/input/eventN devices
//-------------------------------------------------------------------------------------------------
static
void initialize_devices_scan_(
bool want_mice,
unsigned long long * device_IDs,
char * is_nth_a_mouse,
int * nth_fd,
int NTH_SIZE)
{
// For each file in:
DIR * dir = opendir("/dev/input/");
if (!dir)
rage_quit(
"Failure to open any devices.\r\n"
"(Do you lack permissions for the /dev/input/ directory?)");
struct dirent * dirent;
while ((dirent = readdir(dir)))
{
// We’re not interested in stuff like "js0" and "mice", just "eventN"s.
if (strncmp(dirent->d_name, "event", 5) != 0) continue;
// But we DO want that event number... we’ll sort with it later.
int N = atoi(dirent->d_name + 5);
if (!(N < NTH_SIZE)) continue; // Too big! This is an error, but not fatal.
char filename[1024] = "";
strcats(filename, "/dev/input/", dirent->d_name);
if (!is_character_device(filename)) continue;
// Open device
int fd = open(filename, O_RDONLY);
if (fd < 0) continue;
input_devices[num_devices++].fd = fd;
input_devices[num_devices-1].events = POLLIN;
nth_fd[N] = fd;
// Determine whether it is a keyboard or a mouse
unsigned long device_capabilities = ev_get_capabilities(fd);
device_IDs[num_devices-1] = ev_get_id(fd);
for (int type = 0; type < (want_mice ? 1 : NUM_DEVICE_TYPES); type++)
if ( (device_capabilities == desired_devices[type].capabilities)
and ev_has_key(fd, desired_devices[type].test_key) )
{
// Yay, we found a device we may want! Is it a mouse?
is_nth_a_mouse[N] = (type == MOUSE_DEVICE_TYPE);
// If it is a mouse then leave it at the end of our list and increment our mouse count.
if (is_nth_a_mouse[N]) ++num_mice;
// Else exchange it with the first mouse and increment our keyboard count.
else
{
swap(input_devices[num_keyboards].fd, input_devices[num_devices-1].fd);
swap(device_IDs [num_keyboards], device_IDs [num_devices-1] );
++num_keyboards;
}
goto lcontinue;
}
// It was neither keyboard nor mouse, so close it and continue with the next device
close(input_devices[--num_devices].fd);
nth_fd[N] = INVALID_FD;
lcontinue: ;
}
closedir(dir);
}
static
void initialize_devices_cull_(bool want_mice, unsigned long long * device_IDs)
{
if (!want_mice) return;
// Cull all the misconfigured keyboards pretending they are mice.
// A simple O(km) pass will suffice here.
for (int ik = 0; ik < num_keyboards; ik++)
for (int im = 0; im < num_mice; im++)
{
// If the hardware bus, vendor, product numbers of a keyboard matches a mouse...
if ((device_IDs[ik] & ~0xFFFFULL) == (device_IDs[num_keyboards+im] & ~0xFFFFULL))
{
// Then close and remove the mouse (actually a keyboard) from our list of mice
// (Do that by swapping it to the end of our list and then shortening our list counts)
swap(input_devices[num_keyboards+im].fd, input_devices[num_devices-1].fd);
swap(device_IDs [num_keyboards+im], device_IDs [num_devices-1] );
close(input_devices[--num_mice, --num_devices].fd);
}
}
}
static
void initialize_devices_sort_(char * is_nth_a_mouse, int * nth_fd, size_t NTH_SIZE)
{
// Sort the beasts for consistency.
// A simple O(n) counting sort will suffice for so few elements.
// (The counting part of the sort was actually already done when we scanned the files.
// Now we just select the file descriptors in order back into their positions.)
keyboards = input_devices;
for (size_t n = 0; n < NTH_SIZE; n++)
if (IS_VALID_FD(nth_fd[n]) and !is_nth_a_mouse[n])
(keyboards++)->fd = nth_fd[n];
mice = keyboards;
for (size_t n = 0; n < NTH_SIZE; n++)
if (IS_VALID_FD(nth_fd[n]) and is_nth_a_mouse[n])
(mice++)->fd = nth_fd[n];
mice = keyboards;
keyboards = input_devices;
}
static
void initialize_devices_done_(void)
{
for (int n = 0; n < num_devices; n++)
{
// Make each device non-blocking
ioctl(input_devices[n].fd, F_SETFL, ioctl(input_devices[n].fd, F_GETFL) | O_NONBLOCK);
#if 0 // I don't think it is actually necessary to
// Grab and release
if (ioctl(input_devices[n].fd, EVIOCGRAB, 1) == 0)
ioctl(input_devices[n].fd, EVIOCGRAB, 0);
#endif
}
}
void initialize_devices(bool want_mice)
{
NO_REENTRY
// Add our cleanup proc
atexit(&finalize_devices);
// The ID (bus, vendor, product, version) of an open file, indexed parallel to input_devices[]
// Used to get rid of rogue keyboards pretending to be mice.
unsigned long long device_IDs[MAX_OPEN_INPUT_DEVICES];
// This is a lookup table for /dev/input/eventN
// Used to sort file descriptors by filename below.
#define NTH_SIZE 128 // We assume user has fewer than 128 "eventN" files
char is_nth_a_mouse[NTH_SIZE] = {0}; // Nth element --> is it a mouse?
int nth_fd[NTH_SIZE]; // Nth element --> file descriptor value
memset(nth_fd, -1, sizeof(nth_fd)); // (initialize to all an invalid FD)
initialize_devices_scan_(want_mice, device_IDs, is_nth_a_mouse, nth_fd, NTH_SIZE);
initialize_devices_cull_(want_mice, device_IDs);
// Did we find at least one keyboard and at least zero mice?
if (!num_keyboards)
rage_quit(
"Failure to find a suitable keyboard device.\r\n"
"(You must have a keyboard to use this program.)");
initialize_devices_sort_(is_nth_a_mouse, nth_fd, NTH_SIZE);
initialize_devices_done_();
// At this point we have some open devices that we can use, listed all together in
// input_devices[num_devices] and by type in keyboards[num_keyboards] and mice[num_mice].
//
// Make sure to set up some signal handlers to properly call exit(), etc.
//
// Also make sure to use the FocusIn/FocusOut terminal controls to decide whether or not
// to ignore device input. Print "\033[?1004h" to enable it when your program starts and
// "\033[?1004l" to disable it before you terminate. When enabled you will get "\033[I"
// when the terminal gets focus, and "\033[O" when you lose focus.
#undef NTH_SIZE
}
//-------------------------------------------------------------------------------------------------
// poll() helpers
//-------------------------------------------------------------------------------------------------
static
int poll_devices_(int begin, int end, int timeout_ms)
{
if (end < begin) swap(begin, end);
if (begin == end) return INVALID_FD;
for (int n = 0; n < num_devices; n++)
input_devices[n].revents = 0;
if (poll(input_devices+begin, end-begin, timeout_ms) > 0)
for (int n = 0; n < (end-begin); n++)
if (input_devices[begin+n].revents)
return input_devices[begin+n].fd;
return INVALID_FD;
}
int poll_devices(int timeout_ms)
{
return poll_devices_(0, num_devices, timeout_ms);
}
int poll_device(int n, int timeout_ms)
{
return ((0 <= n) and (n < num_devices))
? poll_devices_(n, n+1, timeout_ms)
: INVALID_FD;
}
int poll_keyboards(int timeout_ms)
{
return num_keyboards
? poll_devices_(0, num_keyboards, timeout_ms)
: INVALID_FD;
}
int poll_keyboard(int n, int timeout_ms)
{
return ((0 <= n) and (n < num_keyboards))
? poll_devices_(n, n+1, timeout_ms)
: INVALID_FD;
}
int poll_mice(int timeout_ms)
{
return num_mice
? poll_devices_(num_keyboards, num_keyboards+num_mice, timeout_ms)
: INVALID_FD;
}
int poll_mouse(int n, int timeout_ms)
{
return ((0 <= n) and (n < num_mice))
? poll_devices_(num_keyboards+n, num_keyboards+n+1, timeout_ms)
: INVALID_FD;
}
And here’s a little program to get it to tell you which file in /dev/input
is your keyboard. You can use it to start evtest without having to hunt around to figure out which device file is your keyboard.
example.c
#include <assert.h>
#include <stdio.h>
#include "initialize-devices.h"
int main( void )
{
initialize_devices(false);
assert(num_keyboards != 0);
char path[1024];
assert(get_device_path(keyboards->fd, path, sizeof(path)));
puts(path);
return 0;
}
Alas, evtest.c does not compile with Clang; you must use GCC. But you can use either compiler with my code. Compile it all thusly:
~ $ gcc -O3 evtest.c -o evtest
~ $ clang -Wall -Wextra -Werror -pedantic-errors -O3 -c initialize-devices.c
~ $ clang -Wall -Wextra -Werror -pedantic-errors -O3 -c example.c
~ $ clang *.o -o example
~ $ █
You can now run it:
~ $ ./example |xargs ./evtest
Input driver version is 1.0.1
Input device ID: bus 0x3 vendor 0x1038 product 0x161a version 0x111
Input device name: "SteelSeries SteelSeries Apex 3"
Supported events:
Event type 0 (Sync)
Event type 1 (Key)
Event code 1 (Esc)
Event code 2 (1)
...
...
Event code 2 (ScrollLock)
Event type 20 (Repeat)
Grab succeeded, ungrabbing.
Testing ... (interrupt to exit)
Event: time 1749035388.606120, type 4 (Misc), code 4 (ScanCode), value 7001c
Event: time 1749035388.606120, type 1 (Key), code 21 (Y), value 1
Event: time 1749035388.606120, -------------- Report Sync ------------
yEvent: time 1749035388.680113, type 4 (Misc), code 4 (ScanCode), value 7001c
Event: time 1749035388.680113, type 1 (Key), code 21 (Y), value 0
Event: time 1749035388.680113, -------------- Report Sync ------------
Event: time 1749035388.736115, type 4 (Misc), code 4 (ScanCode), value 70008
Event: time 1749035388.736115, type 1 (Key), code 18 (E), value 1
Event: time 1749035388.736115, -------------- Report Sync ------------
eEvent: time 1749035388.830115, type 4 (Misc), code 4 (ScanCode), value 70008
Event: time 1749035388.830115, type 1 (Key), code 18 (E), value 0
Event: time 1749035388.830115, -------------- Report Sync ------------
Event: time 1749035388.956118, type 4 (Misc), code 4 (ScanCode), value 70004
Event: time 1749035388.956118, type 1 (Key), code 30 (A), value 1
Event: time 1749035388.956118, -------------- Report Sync ------------
aEvent: time 1749035389.030132, type 4 (Misc), code 4 (ScanCode), value 70004
Event: time 1749035389.030132, type 1 (Key), code 30 (A), value 0
Event: time 1749035389.030132, -------------- Report Sync ------------
Event: time 1749035389.036122, type 4 (Misc), code 4 (ScanCode), value 7000b
Event: time 1749035389.036122, type 1 (Key), code 35 (H), value 1
Event: time 1749035389.036122, -------------- Report Sync ------------
hEvent: time 1749035389.106208, type 4 (Misc), code 4 (ScanCode), value 7000b
Event: time 1749035389.106208, type 1 (Key), code 35 (H), value 0
Event: time 1749035389.106208, -------------- Report Sync ------------
...
Press Ctrl
+ C
to terminate evtest.
04-06-2025 16:40 EST • Added polling helper functions.