C non-blocking keyboard input

120,597

Solution 1

As already stated, you can use sigaction to trap ctrl-c, or select to trap any standard input.

Note however that with the latter method you also need to set the TTY so that it's in character-at-a-time rather than line-at-a-time mode. The latter is the default - if you type in a line of text it doesn't get sent to the running program's stdin until you press enter.

You'd need to use the tcsetattr() function to turn off ICANON mode, and probably also disable ECHO too. From memory, you also have to set the terminal back into ICANON mode when the program exits!

Just for completeness, here's some code I've just knocked up (nb: no error checking!) which sets up a Unix TTY and emulates the DOS <conio.h> functions kbhit() and getch():

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <termios.h>

struct termios orig_termios;

void reset_terminal_mode()
{
    tcsetattr(0, TCSANOW, &orig_termios);
}

void set_conio_terminal_mode()
{
    struct termios new_termios;

    /* take two copies - one for now, one for later */
    tcgetattr(0, &orig_termios);
    memcpy(&new_termios, &orig_termios, sizeof(new_termios));

    /* register cleanup handler, and set the new terminal mode */
    atexit(reset_terminal_mode);
    cfmakeraw(&new_termios);
    tcsetattr(0, TCSANOW, &new_termios);
}

int kbhit()
{
    struct timeval tv = { 0L, 0L };
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(0, &fds);
    return select(1, &fds, NULL, NULL, &tv) > 0;
}

int getch()
{
    int r;
    unsigned char c;
    if ((r = read(0, &c, sizeof(c))) < 0) {
        return r;
    } else {
        return c;
    }
}

int main(int argc, char *argv[])
{
    set_conio_terminal_mode();

    while (!kbhit()) {
        /* do some work */
    }
    (void)getch(); /* consume the character */
}

Solution 2

select() is a bit too low-level for convenience. I suggest you use the ncurses library to put the terminal in cbreak mode and delay mode, then call getch(), which will return ERR if no character is ready:

WINDOW *w = initscr();
cbreak();
nodelay(w, TRUE);

At that point you can call getch without blocking.

Solution 3

On UNIX systems, you can use sigaction call to register a signal handler for SIGINT signal which represents the Control+C key sequence. The signal handler can set a flag which will be checked in the loop making it to break appropriately.

Solution 4

Another way to get non-blocking keyboard input is to open the device file and read it!

You have to know the device file you are looking for, one of /dev/input/event*. You can run cat /proc/bus/input/devices to find the device you want.

This code works for me (run as an administrator).

  #include <stdlib.h>
  #include <stdio.h>
  #include <unistd.h>
  #include <fcntl.h>
  #include <errno.h>
  #include <linux/input.h>

  int main(int argc, char** argv)
  {
      int fd, bytes;
      struct input_event data;

      const char *pDevice = "/dev/input/event2";

      // Open Keyboard
      fd = open(pDevice, O_RDONLY | O_NONBLOCK);
      if(fd == -1)
      {
          printf("ERROR Opening %s\n", pDevice);
          return -1;
      }

      while(1)
      {
          // Read Keyboard Data
          bytes = read(fd, &data, sizeof(data));
          if(bytes > 0)
          {
              printf("Keypress value=%x, type=%x, code=%x\n", data.value, data.type, data.code);
          }
          else
          {
              // Nothing read
              sleep(1);
          }
      }

      return 0;
   }

Solution 5

You probably want kbhit();

//Example will loop until a key is pressed
#include <conio.h>
#include <iostream>

using namespace std;

int main()
{
    while(1)
    {
        if(kbhit())
        {
            break;
        }
    }
}

this may not work on all environments. A portable way would be to create a monitoring thread and set some flag on getch();

Share:
120,597
mfonda
Author by

mfonda

Updated on December 02, 2021

Comments

  • mfonda
    mfonda over 2 years

    I'm trying to write a program in C (on Linux) that loops until the user presses a key, but shouldn't require a keypress to continue each loop.

    Is there a simple way to do this? I figure I could possibly do it with select() but that seems like a lot of work.

    Alternatively, is there a way to catch a ctrl-c keypress to do cleanup before the program closes instead of non-blocking io?

  • mfonda
    mfonda over 15 years
    I think I'll probably end up doing this for ease, but I'm going to accept the other answer since it is closer to what the title asks.
  • Alnitak
    Alnitak over 15 years
    definitely not portable. kbhit() is a DOS/Windows console IO function.
  • Alnitak
    Alnitak over 15 years
    this answer is incomplete. without terminal manipulation with tcsetattr() [see my answer] you won't get anything on fd 0 until you press enter.
  • Sunilsingh
    Sunilsingh over 15 years
    It's still a valid answer many uses. The OP didn't specify portability or any particular platform.
  • Alnitak
    Alnitak over 15 years
    they didn't. however use of terms such as "non-blocking" suggests Unix.
  • mfonda
    mfonda over 15 years
    Alnitak: I wasn't aware it implied a platform - what's the equivalent Windows term?
  • Alnitak
    Alnitak over 15 years
    you can do non-blocking IO on windows too, but the concept (along with use of select) is more familiar to Unix programmers (IMHO)
  • Alnitak
    Alnitak over 15 years
    I thought about using curses too, but the potential problem with that is that the initscr() call clears the screen, and it also maybe gets in the way of using normal stdio for screen output.
  • Norman Ramsey
    Norman Ramsey over 15 years
    Yeah, curses is pretty all or nothing.
  • Wayne Werner
    Wayne Werner almost 14 years
    curses is both the name of the library and what you'll mutter when you use it ;) However, since I was going to mention it +1 to you.
  • Salepate
    Salepate almost 12 years
    Is getch() supposed to return which key was pressed, or is it simply supposed to consume the character?
  • Alnitak
    Alnitak almost 12 years
    @Salepate it's supposed to return the character. It's the (void) bit that gets that character and then throws it away.
  • Salepate
    Salepate almost 12 years
    oh I see, maybe i'm doing it wrong but when i use getch() on a printf("%c"); it displays weird characters on the screen.
  • jernkuan
    jernkuan about 11 years
    Slight edit, you need to #include <unistd.h> for read() to work
  • azmeuk
    azmeuk about 10 years
    Is there a simple way to read the whole line (for instance with 'getline') instead of a single character ?
  • MestreLion
    MestreLion about 9 years
    @Alnitak why would "non-blocking", as a programming concept, be more familiar in Unix environment?
  • Alnitak
    Alnitak about 9 years
    @MestreLion Maybe now (six years later) that's no longer the case. It was always there in POSIX, though.
  • MestreLion
    MestreLion about 9 years
    @Alnitak: I mean I was familiar with the term "non-blocking call" long before touching any *nix.. so just like Zxaos I would never realize it would imply a platform.
  • luser droog
    luser droog about 8 years
    This answer is incomplete. You need to set the terminal to non-canonical mode. Also nfds should be set to 1.
  • étale-cohomology
    étale-cohomology over 7 years
    The switch statement is missing one bracket :)
  • ThorSummoner
    ThorSummoner over 4 years
    fantastic example, but I ran into an issue where Xorg would keep receiving key events, when the event device stopped emitting when more than six keys were held :\ weird right?
  • Nathan B
    Nathan B almost 4 years
    what's the purpose of the non blocking kbhit()? just to know if there are characters left to consume by the blocking getch()? How long does the character pressed 'stay alive' before we should check its existence with kbhit()?
  • Alnitak
    Alnitak almost 4 years
    @NadavB yes, kbhit() just tells you if there are any characters waiting to be read, and it'll remain valid forever, so long as that's true.
  • Admin
    Admin almost 3 years
    The first reading is fine, but the following reads bring up two characters. The first I supposse is the enter of the previous one and the second is the key itself.
  • Max888
    Max888 almost 3 years
    This seems to remove the ability to exit the program with ctrl c, is it possible to amend the above to avoid this?
  • Alnitak
    Alnitak almost 3 years
    @Max888 not portably, but you could exit the program yourself if the next retrieved character is ^c (0x03)
  • BlueChip
    BlueChip over 2 years
    Thanks for an elegant solution ...I have an interrupt timer running which causes kbhit() to occasionally return -1 == EINTR ...This means kbhit() returns true when it should (arguably) return false ...I have fixed this by changing the return statement in kbhit() to return (select(1, &fds, NULL, NULL, &tv) > 0);
  • Alnitak
    Alnitak over 2 years
    @BlueChip good point - code updated accordingly!