95

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?

alk
  • 69,737
  • 10
  • 105
  • 255
Zxaos
  • 7,791
  • 12
  • 47
  • 61
  • 1
    what about opening a thread? – Nathan B May 26 '20 at 14:10
  • I recommend a separate thread which blocks on the keyboard input and passes the input to the other, main thread each time a key press comes in. [Here is an example I wrote in Python](https://stackoverflow.com/a/53344690/4561887). For C, be sure to use thread-safe message-passing queues, or block on acquiring mutexes, as required. The Python queue library is already thread-safe, but no such safety for queues exists naturally in C nor C++. – Gabriel Staples Feb 03 '22 at 21:28
  • See also: [How to avoid pressing Enter with getchar() for reading a single character only?](https://stackoverflow.com/q/1798511/4561887) – Gabriel Staples Feb 03 '22 at 22:54

11 Answers11

74

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 */
}
Alnitak
  • 334,560
  • 70
  • 407
  • 495
  • 1
    Is getch() supposed to return which key was pressed, or is it simply supposed to consume the character? – Salepate Jul 19 '12 at 11:56
  • @Salepate it's supposed to return the character. It's the `(void)` bit that gets that character and then throws it away. – Alnitak Jul 19 '12 at 12:05
  • 1
    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. – Salepate Jul 19 '12 at 12:07
  • Slight edit, you need to #include for read() to work – jernkuan Apr 30 '13 at 00:42
  • Is there a simple way to read the whole line (for instance with 'getline') instead of a single character ? – azmeuk Feb 26 '14 at 17:02
  • 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()? – Nathan B May 26 '20 at 13:59
  • @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. – Alnitak May 26 '20 at 16:24
  • This seems to remove the ability to exit the program with ctrl c, is it possible to amend the above to avoid this? – Max888 Jul 27 '21 at 13:04
  • 1
    @Max888 not portably, but you could exit the program yourself if the next retrieved character is ^c (0x03) – Alnitak Jul 27 '21 at 22:05
  • 1
    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);` – BlueChip Oct 21 '21 at 19:41
  • @BlueChip good point - code updated accordingly! – Alnitak Oct 22 '21 at 09:41
16

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.

Norman Ramsey
  • 198,648
  • 61
  • 360
  • 533
  • 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. – Alnitak Jan 16 '09 at 08:09
  • 6
    Yeah, curses is pretty all or nothing. – Norman Ramsey Jan 17 '09 at 03:01
11

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.

Mehrdad Afshari
  • 414,610
  • 91
  • 852
  • 789
  • 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. – Zxaos Jan 15 '09 at 23:37
6

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;
   }
JustinB
  • 1,532
  • 15
  • 18
  • 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? – ThorSummoner Dec 26 '19 at 07:57
6

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();

Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
Jon Clegg
  • 3,870
  • 4
  • 25
  • 22
  • 4
    definitely not portable. kbhit() is a DOS/Windows console IO function. – Alnitak Jan 15 '09 at 23:38
  • 1
    It's still a valid answer many uses. The OP didn't specify portability or any particular platform. – AShelly Jan 15 '09 at 23:54
  • they didn't. however use of terms such as "non-blocking" suggests Unix. – Alnitak Jan 16 '09 at 00:05
  • 4
    Alnitak: I wasn't aware it implied a platform - what's the equivalent Windows term? – Zxaos Jan 16 '09 at 05:15
  • 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 Jan 16 '09 at 08:06
  • 3
    @Alnitak why would "non-blocking", as a programming concept, be more familiar in Unix environment? – MestreLion Feb 23 '15 at 05:45
  • @MestreLion Maybe now (six years later) that's no longer the case. It was always there in POSIX, though. – Alnitak Feb 23 '15 at 09:37
  • 4
    @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. – MestreLion Feb 23 '15 at 10:52
3

The curses library can be used for this purpose. Of course, select() and signal handlers can be used too to a certain extent.

frogatto
  • 28,539
  • 11
  • 83
  • 129
PolyThinker
  • 5,152
  • 21
  • 22
  • 1
    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. – Wayne Werner Jul 22 '10 at 15:46
2

If you are happy just catching Control-C, it's a done deal. If you really want non-blocking I/O but you don't want the curses library, another alternative is to move lock, stock, and barrel to the AT&T sfio library. It's nice library patterned on C stdio but more flexible, thread-safe, and performs better. (sfio stands for safe, fast I/O.)

indiv
  • 17,306
  • 6
  • 61
  • 82
Norman Ramsey
  • 198,648
  • 61
  • 360
  • 533
1

You can do that using select as follow:

  int nfds = 0;
  fd_set readfds;
  FD_ZERO(&readfds);
  FD_SET(0, &readfds); /* set the stdin in the set of file descriptors to be selected */
  while(1)
  {
     /* Do what you want */
     int count = select(nfds, &readfds, NULL, NULL, NULL);
     if (count > 0) {
      if (FD_ISSET(0, &readfds)) {
          /* If a character was pressed then we get it and exit */
          getchar();
          break;
      }
     }
  }

Not too much work :D

Rod
  • 39
  • 3
  • 3
    This answer is incomplete. You need to set the terminal to non-canonical mode. Also `nfds` should be set to 1. – luser droog Feb 18 '16 at 22:18
  • 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. – Mariano Vedovato Jul 09 '21 at 12:18
1

Here's a function to do this for you. You need termios.h which comes with POSIX systems.

#include <termios.h>
void stdin_set(int cmd)
{
    struct termios t;
    tcgetattr(1,&t);
    switch (cmd) {
    case 1:
            t.c_lflag &= ~ICANON;
            break;
    default:
            t.c_lflag |= ICANON;
            break;
    }
    tcsetattr(1,0,&t);
}

Breaking this down: tcgetattr gets the current terminal information and stores it in t. If cmd is 1, the local input flag in t is set to non-blocking input. Otherwise it is reset. Then tcsetattr changes standard input to t.

If you don't reset standard input at the end of your program you will have problems in your shell.

112
  • 75
  • 11
1

There is no portable way to do this, but select() might be a good way. See http://c-faq.com/osdep/readavail.html for more possible solutions.

Nate879
  • 383
  • 1
  • 3
  • 8
  • 1
    this answer is incomplete. without terminal manipulation with tcsetattr() [see my answer] you won't get anything on fd 0 until you press enter. – Alnitak Jan 15 '09 at 23:40
0

In C++, I did this:

#include <chrono>
#include <thread>

using namespace std::chrono_literals;

void OnEnter()
{
    while (true)
    {
        getchar();
        // do something when enter is pressed
    }
}

int main()
{
    std::thread keyBoardCommands(OnEnter);

    while(true)
    {
        // code for main loop
        std::this_thread::sleep_for(16ms);
    }
}

This code would be platform-independent.