If your system supports termios as standardized in POSIX.1-2001, then you can manipulate the standard input terminal to not buffer your input. Consider the following example:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* SIGINT handler */
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
if (!done)
done = signum;
}
static int install_done(const int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
if (sigaction(signum, &act, NULL) == -1)
return errno;
return 0;
}
/* Reverting terminal back to original settings */
static struct termios terminal_config;
static void revert_terminal(void)
{
tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminal_config);
}
int main(void)
{
int c;
/* Set up INT (Ctrl+C), TERM, and HUP signal handlers. */
if (install_done(SIGINT) ||
install_done(SIGTERM) ||
install_done(SIGHUP)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
/* Make terminal input noncanonical; not line buffered. Also disable echo. */
if (isatty(STDIN_FILENO)) {
struct termios config;
if (tcgetattr(STDIN_FILENO, &terminal_config) == 0 &&
tcgetattr(STDIN_FILENO, &config) == 0) {
config.c_lflag &= ~(ICANON | ECHO);
config.c_cc[VMIN] = 1; /* Blocking input */
config.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &config);
atexit(revert_terminal);
}
}
/* Set standard input unbuffered. */
setvbuf(stdin, NULL, _IONBF, 0);
printf("Press Ctrl+C to exit.\n");
fflush(stdout);
while (!done) {
c = fgetc(stdin);
if (c == EOF)
printf("Read EOF%s\n", ferror(stdin) ? " as an error occurred" : "");
else
printf("Read %d = 0x%02x\n", c, (unsigned int)c);
fflush(stdout);
}
return EXIT_SUCCESS;
}
The #define
line tells your C library headers to expose POSIX.1 features for GNU-based systems.
The done
flag is set whenever an INT (Ctrl+C), TERM, or HUP signal is received. (HUP signal is sent if you disconnect from the terminal, for example by closing the terminal window.)
The terminal_config
structure will contain the original terminal settings, used by revert_terminal()
registered as an at-exit function, to revert the terminal settings back to the original ones read at program startup.
The function isatty(STDIN_FILENO)
returns 1 if standard input is a terminal. If so, we obtain the current terminal settings, and modify them for non-canonical mode, and ask that each read blocks until at least one character is read. (The I/O functions tend to get a bit confused if you set .c_cc[VMIN]=0
and .c_cc[VTIME]=0
, so that if no input is pending, fgetc()
returns 0. Typically it looks like an EOF to stdio.h I/O functions.)
Next, we tell the C library to not internally buffer standard input, using setvbuf()
. Normally, the C library uses an input buffer for standard input, for efficiency. However, for us, it would mean the C library would buffer characters typed, and our program might not see them immediately when typed.
Similarly, standard output is also buffered for efficiency. The C library should flush all complete lines to the actual standard output, but we can use the fflush(stdout)
call to ensure everything we've written to stdout is flushed to the actual standard output at that point.
In main()
, we then have a simple loop, that reads keypresses, and prints them in decimal and hexadecimal.
Note that when a signal is delivered, for example the INT signal because you typed Ctrl+C, the delivery of the signal to our handle_done()
signal handler interrupts the fgetc()
call if one is pending. This is why you see Read EOF
when you press Ctrl+C; if you check ferror(stdin)
afterwards, you'll see it returns nonzero (which indicates an error occurred). The "error" in this case is EINTR
, "interrupted by a signal".
Also note that when you press some certain keys, like cursor or function keys, you'll see multiple characters generated, usually beginning with 27 and 91 (decimal; 0x1B 0x5B in hexadecimal; "\033["
if expressed as a C string literal). These are usually, but not always, ANSI escape sequences. In general, they are terminal-specific codes that one can obtain via tigetstr()
, tigetnum()
, and tigetflag()
using the terminfo database.
A much more portable way to do this, is to use a Curses library; either ncurses on most systems, or PDCurses on Windows machines. Not only do they provide a much easier interface, but it does it in a terminal-specific way, for maximum compatibility across systems.
C programs using the Curses functions can be compiled against any Curses library, so the same C source file can be compiled and run on Linux, Mac, and Windows machines. However, ncurses does contain quite a few extensions, which may not be provided by other Curses libraries like PDCurses.