1

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.

20
  • As kindly as I can: you are out of your mind. You should be using any convenient graphic rendering library to do this stuff — the vast majority of them handle window keyboard input for you. What you want can be done, but it is a whole lot of worms in a surprisingly large can. For starters, you should give up on key up events. If undaunted, what OS and terminal requirements? Commented May 31 at 5:18
  • (JSYK, I am not trying to be a jerk. I just know a whole lot about deep terminal magic for both Windows and *nixen, and I wouldn’t try what I understand you to be trying — for the simple reason that it is more trouble than it is worth. Update your question to (significantly) narrow down your requirements and I (and others here) will see what we can offer...) Commented May 31 at 5:23
  • Welcome to Stack Overflow. Please read the About page soon and also visit the links describing How to Ask a Question and How to create a Minimal Complete Reproducible Example. Providing the necessary details, including your MCRE, compiler warnings and associated errors, and sample data if any, will allow everyone here to help you with your question. Commented May 31 at 6:30
  • 1
    Sorry to respond so late. A canonical example of how to use the linux evdev input is here: people.freedesktop.org/~whot/evtest.c . Some extra magic is necessary to poke around and find the correct driver files in /dev/input/*, for which you indeed need to be a member of the “input” group to access (or just run with sudo). And you need X and some terminal control sequence magic (on a reasonable terminal emulator) to get proper mouse position events... Commented Jun 2 at 5:29
  • 1
    Give me a day or so and I’ll post some C code that finds the correct /dev/input/eventN device. In the meantime, I think there are a couple blogs lying around, uh... Explain EV in /proc/bus/input/devices... and Decoding input device capabilities on Linux... all those are for parsing the textual devices file though... Commented Jun 2 at 17:34

3 Answers 3

3

Terminals and Key Release Events

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.

The Linux Input Subsystem

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.

Reading key events properly

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.

Finding the correct event device

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;
}

Test Program

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.

Revision History

04-06-2025 16:40 EST • Added polling helper functions.

Sign up to request clarification or add additional context in comments.

4 Comments

Amazing answer! On small thing to note with this: while I was running the code, I had "keyd" installed, which seems to silence all the other keyboard input devices and only project keystrokes with the "keyd virtual keyboard" input device, so I initially wasn't getting any keystrokes. Just detecting inputs from every keyboard available should theoretically fix this though.
Also, how could I run this on the side? Since read waits until it gets an answer to continue the program, could I somehow check if there even is a new input before reading?
The results are returned in an array of struct pollfd containing num_keyboards elements for the keyboards it finds. Just run through it to make sure all the input_devices[n].revents members are zeroed, then pass it to poll(). A non-zero .revents will indicate which input_devices[n].fd has input to read(). • If, however, you are only getting one keyboard then it is worth checking to see what keyd is doing with it. • Alas, working with /dev/input is fairly new to me, so IDK if I have made some obscure error that will affect users with odd configurations.
@user29944582 Added polling functions. Use your chosen poll function in a loop. A timeout of 100–250 ms is reasonable.
0

I’m also working on a renderer like you, but honestly, I’m afraid you can't do much about this at a low level. If you want to detect multiple simultaneous key presses properly, you generally need to use a higher-level API around keyboard input, which requires a GUI environment. But there is some approaches it I don't guarantee it works everywhere. Most of users use desktop environment (they don't use Linux kernel).
In Linux:

  • X11: You can use something like a dummy invisible decorated window, follow the terminal window, use input (keyboard/mouse) forwarding input to the terminal. In the middle layer, you will processed it to your program and print to the terminal.
  • Wayland: Forget about it, there is no way to do that. Wayland was specifically designed to have higher security measure than X11.

In Windows:

  • Console only: You can use ReadConsoleInput / GetAsyncKeyState through polling (you can put in the main loop or separated thread)
  • GUI: You can recieve keyboard events in your window message handler (WM_KEYUP/WM_KEYDOWN)

Comments

0

EDIT: This answer is specific to Microsoft Windows. It was posted at a time before the question was clarified with an edit, after which the question now states that it applies to Linux.

If you are using Windows then use the Win32 API function GetAsyncKeyState(); or if you know Win32API well then use WM_KEYDOWN message. Though assuming you would not want to do multithreading and all, your best bet is GetAsyncKeyState();

5 Comments

I don't recommend using GetAyncKeyState, because that function will only reliably tell you the current state of the keyboard at the time the function is called. It won't reliably tell you whether a keyboard event has occurred between function calls. I believe the correct API to use for this is ReadConsoleInput, as it allows you to read from a queue of input events.
I don't think that WM_KEYDOWN can be used for console windows, because you have no control over that Windows's message loop.
The question refers to "canonical terminal input". I believe that this term usualy applies to UNIX-like terminals, not Windows consoles/terminals. Therefore, I don't believe that the question applies to Microsoft Windows. However, this is not your fault, because OP did not state in their question to which operating system their question applies (although they should have done so).
@AndreasWenzel You can, in fact, read the console input for both key down and key up (and key repeat) messages on Windows. I, uh, had assumed OP wanted an answer for *nixen, though, because of his use of ioctl() and read()... I wonder if he was meaning something that would also work on Windows...?
@Dúthomhas: The original question did not specify to which platform the question applies, and also did not contain any code. That is why this answer is Windows-specific. However, afterwards, the question was edited to specify that it applies to Linux, and the question now also contains corresponding code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.