Detecting keyboard key press and release on Linux

Viewed 1346

I'm trying to write a simple program that does a specific task when no key is pressed and does another task when any key is pressed. This is what I have so far:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>

int kbhit (void);

int main()
{
    char c;

    while(1)
    {
        while (!kbhit())
        {
            printf("%d", 0);
        }
        c = getchar();
        printf("%c", c);

        if(c == 'x' || c == 'X')
            break;

        sleep(0);
    }

    return 0;
}

int kbhit (void)
{
    struct timeval tv;
    fd_set rdfs;

    tv.tv_sec = 0;
    tv.tv_usec = 1000;

    FD_ZERO(&rdfs);
    FD_SET (STDIN_FILENO, &rdfs);

    select(STDIN_FILENO+1, &rdfs, NULL, NULL, &tv);
    return FD_ISSET(STDIN_FILENO, &rdfs);
}

This implementation is good for detecting a single key press over and over, but if you keep the keyboard key pressed, there are significant gaps between detecting the key press. Even though the key is pressed all the time, 0 is printed to console anyway. For example, output looks something like this if I keep 'W' pressed: 000000000000000WWWWWWWW00000000000000000WWWWWWWW00000000...
How can I fix this so that when a keyboard key is kept pressed, kbhit() always returns 0?

1 Answers

Your kbhit (and, apparently, even Windows' original kbhit) doesn't detect whether a key is pressed, but only whether there is something new to read on stdin. This will only be the case 25 times a second or so, depending on your autorepeat setting. Making stdout unbuffered in your example code will make that more obvious (000000W00000000W000000000W)

How can I fix this so that when a keyboard key is kept pressed, kbhit() always returns 0?

This is impossible to do portably. In Linux, it can be done using the device files under /dev/input See the example program below. It will register all keys, even a lone SHIFT Note that this is effectively a keylogger, so you will have to run as root (or make the program setuid). It will then register all keystrokes, even when it doesn't have keyboard focus. Yikes!

Note: the example below is based on keystate.c by Kevin Cox

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sysexits.h>
#include <glob.h>
#include <linux/input.h>
#include <sys/stat.h>
#include <fcntl.h>

/* Return -1 if no key is being pressed, or else the lowest keycode 
   (c.f. linux/input-event-codes.h) of all the keys that are being pressed */
int keycode_of_key_being_pressed() { 
  FILE *kbd;
  glob_t kbddev;                                   // Glob structure for keyboard devices
  glob("/dev/input/by-path/*-kbd", 0, 0, &kbddev); // Glob select all keyboards
  int keycode = -1;                                // keycode of key being pressed
  for (int i = 0; i < kbddev.gl_pathc ; i++ ) {    // Loop through all the keyboard devices ...
    if (!(kbd = fopen(kbddev.gl_pathv[i], "r"))) { // ... and open them in turn (slow!)
      perror("Run as root to read keyboard devices"); 
      exit(1);      
    }

    char key_map[KEY_MAX/8 + 1];          // Create a bit array the size of the number of keys
    memset(key_map, 0, sizeof(key_map));  // Fill keymap[] with zero's
    ioctl(fileno(kbd), EVIOCGKEY(sizeof(key_map)), key_map); // Read keyboard state into keymap[]
    for (int k = 0; k < KEY_MAX/8 + 1 && keycode < 0; k++) { // scan bytes in key_map[] from left to right
      for (int j = 0; j <8 ; j++) {       // scan each byte from lsb to msb
        if (key_map[k] & (1 << j)) {      // if this bit is set: key was being pressed
          keycode = 8*k + j ;             // calculate corresponding keycode 
          break;                          // don't scan for any other keys
        }
      }   
    }

    fclose(kbd);
    if (keycode)
      break;                              // don't scan for any other keyboards
  }
  return keycode;
}

void main()
{
  setvbuf(stdout, NULL, _IONBF, 0); // Set stdout unbuffered
  while (1)    {
    int key = keycode_of_key_being_pressed();
    printf((key < 0 ?  "no key\n" : "keycode: %d\n"), key);
    if (key == KEY_X)
      exit(0);
  }
}       
Related