3

I have a infinite loop like the following one, and within this loop, I want to continuously check the keyboard to see if the escape key (ESC) has been pressed or not. If it is pressed, then the loop should be broken. How I can do this in C? (I am using gcc, and do access to pthreads as well in case this must be done via threads)

 while(1){
        //do something
        //check for the ESC key
 }
Blue Sky
  • 293
  • 1
  • 5
  • 14

4 Answers4

7

This is heavily system dependent. In Unix/Linux systems, the default terminal handler gathers lines and only notifies the program when a full line is available (after Enter is hit.) If you instead want keystrokes immediately, you need to put the terminal into non-canonical mode:

#include <termios.h>

struct termios info;
tcgetattr(0, &info);          /* get current terminal attirbutes; 0 is the file descriptor for stdin */
info.c_lflag &= ~ICANON;      /* disable canonical mode */
info.c_cc[VMIN] = 1;          /* wait until at least one keystroke available */
info.c_cc[VTIME] = 0;         /* no timeout */
tcsetattr(0, TCSANOW, &info); /* set immediately */

Once you've done that, you can use any calls that read from stdin and they will return keys without waiting for the end of the line. You can in addition set c_cc[VMIN] = 0 to cause it to not wait for keystrokes at all when you read from stdin.

If, however, you're reading stdin with stdio FILE related calls (getchar, etc), setting VMIN = 0 will make it think you've reached EOF whenever there are no keys available, so you'll have to call clearerr after that happens to try to read more characters. You can use a loop like:

int ch;
while((ch = getchar()) != 27 /* ascii ESC */) {
    if (ch < 0) {
        if (ferror(stdin)) { /* there was an error... */ }
        clearerr(stdin);
        /* do other stuff */
    } else {
        /* some key OTHER than ESC was hit, do something about it? */
    }
}

After you're done, you probably want to be sure to set the terminal back into canonical mode, lest other programs (such as your shell) get confused:

tcgetattr(0, &info);
info.c_lflag |= ICANON;
tcsetattr(0, TCSANOW, &info);

There are also other things you can do with tcsetattr -- see then manual page for details. One thing that might suffice for your purposes is setting an alternative EOL character.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
1

To continuously monitoring the keyboard while executing other commands requires changing terminal mode with termios.h and multithreading with pthread.h.

The terminal starts with canonical mode, meaning the standard input is only sent to the program after Enter is pressed. For instant response after keypress, we need raw mode.

When C input function getchar() is invoked, the thread stops and waits until the input is received. Continuously executing an action while listening for keyboard input thus requires multithreading.

Global variables help different threads to communicate.

Here is the code:

#include <pthread.h> // Multithreading
#include <stdio.h>
#include <stdlib.h>  // for atexit()
#include <termios.h> // For changing terminal mode
#include <unistd.h>  // For changing terminal mode

struct termios original; // A struct to save the original state of terminal
int ESCPressed = 0;      // For thread communication

void disableRAWMode();
void enableRAWMode();
void *asciRead();
void *print();

int main() {
  // Start Multithreading
  pthread_t id_print, id_read;

  pthread_create(&id_print, NULL, print, NULL);
  pthread_create(&id_read, NULL, asciRead, NULL);

  pthread_join(id_print, NULL);
  pthread_join(id_read, NULL);

  return 0;
}

/// Reads keyboard input
void *asciRead() {
  enableRAWMode(); // local function: Enable Raw Mode
  char ch;
  while ((ch = getchar()) != 27)
    ; // ASCI code for ESC is 27
  ESCPressed = 1;
  printf("ESC Pressed!\n");
}

/// Doing Stuff while listening to keyboard
void *print() {
  while (!ESCPressed) { // When ESC is not pressed
    sleep(1);
    printf("I am Printing!\n");
  }
  printf("Printing Thread Finished!\n");
}

/// This function enables RAW mode for terminal.
void enableRAWMode() {
  struct termios raw;
  tcgetattr(STDIN_FILENO, &raw); // Save the state of the terminal to struct raw
                                 // STDIN_FILENO is from <stdlib.h>
                                 // tcgetattr() from <termios.h>
  tcgetattr(STDIN_FILENO, &original);
  atexit(&disableRAWMode); // Revert to canonical mode when exiting the program
                           // atext() from <stdlib.h>
  raw.c_lflag &=
      ~(ECHO | ICANON); // Turn off canonical mode
                        // Turn off ECHO mode so that keyboard is not
                        // printing to terminal
                        // ICANON and ECHO is bitflag. ~ is binary NOT operator

  tcsetattr(STDIN_FILENO, TCSAFLUSH,
            &raw); // Set the terminal to be in raw mode
                   // tcsetattr() from <termios.h>
}

void disableRAWMode() {
  tcsetattr(STDIN_FILENO, TCSAFLUSH,
            &original); // Set terminal to original state
}

Helpful resources:

Terminal Raw Mode

Multithreading

YuhaoHanHarry
  • 61
  • 1
  • 6
0

If the main job you're doing can be placed within this main loop, you could go for using STDIN in non-blocking mode. You still have a problem with the terminal which does line-buffering normally. You shall put the terminal to raw mode as well.

What about using Ctrl-C (interrupt)?

Non-blocking means that the read() system call always returns immediately even if there are no new bytes in the file. On Linux/Unix you can make STDIN nonblocking this way:

#include <unistd.h>
#include <fcntl.h>
fcntl(0, F_SETFL, O_NONBLOCK); /* 0 is the stdin file decriptor */
SzG
  • 12,333
  • 4
  • 28
  • 41
  • Thanks! In fact, I want to use something like Ctrl-C but with another key like ESC. Could you explain more about STDIN in non-blocking mode? – Blue Sky Jan 13 '14 at 13:18
0

This is what you want:

#include <stdio.h>
#include <conio.h>

void main() {

   int c;

   while((c = getch()) != EOF )
      if(c == 27)   break;
/* 27 is the ASCII code for Esc */

}