Saturday, June 25, 2011

Character by Character Input on *nix

A frequently asked question is how to read a single key on *nix. Many people expect read(2) of a single byte to return immediately, but by default it doesn't.

On Unix you don't deal with keyboards, you deal with a terminal. A terminal can be the physical console, a terminal (or terminal emulator) on a serial port, an xterm, ...

By default, input lines are not made available to programs until the terminal (or terminal emulator) sees a line delimiter.

On POSIX, terminals are controlled by the termios(3) functions. These include tcgetattr(3) and tcsetattr(3), which can be used to modify terminal attributes. These include input modes, output modes, and local modes.

One of the local modes is canonical mode (enabled by default), in which input is made available line by line, and certain line editing characters are enabled.

So, to read input character by character, you need to do something like the following:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <termios.h>
#include <sys/ioctl.h>

static volatile sig_atomic_t end = 0;

static void sighandler(int signo)
{
        end = 1;
}

int main()
{
        struct termios oldtio, curtio;
        struct sigaction sa;

        /* Save stdin terminal attributes */
        if (tcgetattr(0, &oldtio) < 0) {
                perror("tcgetattr");
                exit(1);
        }

        /* Make sure we exit cleanly */
        memset(&sa, 0, sizeof(struct sigaction));
        sa.sa_handler = sighandler;
        if (sigaction(SIGINT, &sa, NULL) < 0) {
                perror("sigaction");
                exit(1);
        }
        if (sigaction(SIGQUIT, &sa, NULL) < 0) {
                perror("sigaction");
                exit(1);
        }
        if (sigaction(SIGTERM, &sa, NULL) < 0) {
                perror("sigaction");
                exit(1);
        }

        /* This is needed to be able to tcsetattr() after a hangup (Ctrl-C)
         * see tcsetattr() on POSIX
         */
        memset(&sa, 0, sizeof(struct sigaction));
        sa.sa_handler = SIG_IGN;
        if (sigaction(SIGTTOU, &sa, NULL) < 0) {
                perror("sigaction");
                exit(1);
        }

        /* Set non-canonical no-echo for stdin */
        if (tcgetattr(0, &curtio) < 0) {
                perror("tcgetattr");
                exit(1);
        }
        curtio.c_lflag &= ~(ICANON | ECHO);
        /* This could be interrupted by a signal if it used
         * TCSADRAIN or TCSAFLUSH, but it wouldn't matter, since
         * we would have not changed terminal attributes yet
         */
        if (tcsetattr(0, TCSANOW, &curtio) < 0) {
                perror("tcsetattr");
                exit(1);
        }
        if (tcgetattr(0, &curtio) < 0) {
                perror("tcgetattr");
                exit(1);
        }
        if (curtio.c_lflag & (ICANON | ECHO)) {
                fprintf(stderr, "couldn't set non-canonical no-echo mode\n");
                exit(1);
        }

        /* main loop */
        while (!end) {
                struct pollfd pfds[1];
                int ret;
                char c;

                /* See if there is data available */
                pfds[0].fd = 0;
                pfds[0].events = POLLIN;
                ret = poll(pfds, 1, 0);
                if (ret < 0 && errno != EINTR) {
                        perror("poll");
                        exit(1);
                }

                /* Consume data */
                if (ret > 0) {
                        printf("Data available\n");
                        if (read(0, &c, 1) < 0 && errno != EINTR) {
                                perror("read");
                                exit(1);
                        }
                }
        }

        /* restore terminal attributes */
        /* This could be interrupted by a signal if it used TCSADRAIN
         * or TCSAFLUSH
         */
        if (tcsetattr(0, TCSANOW, &oldtio) < 0) {
                perror("tcsetattr");
                exit(1);
        }

        return 0;
}