Here is a minimal example, that should be compatible with standard C I/O.
The compatibility is based on the fact that this does not change terminal settings in a way that would change how the low-level read()/write() functions behave.
While it is possible that a C library will get confused by these changed, such a C library will not be able to handle all possible input from normal terminals either; only from a small subset of terminals specifically in canonical mode. In particular, the writers of such a C library must have made a bunch of stupid assumptions, which are likely to bite an unwary programmer in fascinatingly weird and annoying ways.
In other words, you can expect any normal C library implementation to work with this. (It has worked on all C libraries I have access to, that do support the termios interface.)
minimal.h:
Code:
#include <unistd.h>
#include <termios.h>
#include <signal.h>
#include <stdio.h>
static int terminal_descriptor = -1;
static struct termios terminal_original;
static struct termios terminal_settings;
/* Restore terminal to original settings
*/
static void terminal_done(void)
{
if (terminal_descriptor != -1)
tcsetattr(terminal_descriptor, TCSANOW, &terminal_original);
}
/* "Default" signal handler: restore terminal, then exit.
*/
static void terminal_signal(int signum)
{
if (terminal_descriptor != -1)
tcsetattr(terminal_descriptor, TCSANOW, &terminal_original);
/* exit() is not async-signal safe, but _exit() is.
* Use the common idiom of 128 + signal number for signal exits.
* Alternative approach is to reset the signal to default handler,
* and immediately raise() it. */
_exit(128 + signum);
}
/* Initialize terminal for non-canonical, non-echo mode,
* that should be compatible with standard C I/O.
* Returns 0 if success, nonzero errno otherwise.
*/
static int terminal_init(void)
{
struct sigaction act;
/* Already initialized? */
if (terminal_descriptor != -1)
return errno = 0;
/* Which standard stream is connected to our TTY? */
if (isatty(STDERR_FILENO))
terminal_descriptor = STDERR_FILENO;
else
if (isatty(STDIN_FILENO))
terminal_descriptor = STDIN_FILENO;
else
if (isatty(STDOUT_FILENO))
terminal_descriptor = STDOUT_FILENO;
else
return errno = ENOTTY;
/* Obtain terminal settings. */
if (tcgetattr(terminal_descriptor, &terminal_original) ||
tcgetattr(terminal_descriptor, &terminal_settings))
return errno = ENOTSUP;
/* Disable buffering for terminal streams. */
if (isatty(STDIN_FILENO))
setvbuf(stdin, NULL, _IONBF, 0);
if (isatty(STDOUT_FILENO))
setvbuf(stdout, NULL, _IONBF, 0);
if (isatty(STDERR_FILENO))
setvbuf(stderr, NULL, _IONBF, 0);
/* At exit() or return from main(),
* restore the original settings. */
if (atexit(terminal_done))
return errno = ENOTSUP;
/* Set new "default" handlers for typical signals,
* so that if this process is killed by a signal,
* the terminal settings will still be restored first. */
sigemptyset(&act.sa_mask);
act.sa_handler = terminal_signal;
act.sa_flags = 0;
if (sigaction(SIGHUP, &act, NULL) ||
sigaction(SIGINT, &act, NULL) ||
sigaction(SIGQUIT, &act, NULL) ||
sigaction(SIGTERM, &act, NULL) ||
#ifdef SIGXCPU
sigaction(SIGXCPU, &act, NULL) ||
#endif
#ifdef SIGXFSZ
sigaction(SIGXFSZ, &act, NULL) ||
#endif
#ifdef SIGIO
sigaction(SIGIO, &act, NULL) ||
#endif
sigaction(SIGPIPE, &act, NULL) ||
sigaction(SIGALRM, &act, NULL))
return errno = ENOTSUP;
/* Let BREAK cause a SIGINT in input. */
terminal_settings.c_iflag &= ~IGNBRK;
terminal_settings.c_iflag |= BRKINT;
/* Ignore framing and parity errors in input. */
terminal_settings.c_iflag |= IGNPAR;
terminal_settings.c_iflag &= ~PARMRK;
/* Do not strip eighth bit on input. */
terminal_settings.c_iflag &= ~ISTRIP;
/* Do not do newline translation on input. */
terminal_settings.c_iflag &= ~(INLCR | IGNCR | ICRNL);
#ifdef IUCLC
/* Do not do uppercase-to-lowercase mapping on input. */
terminal_settings.c_iflag &= ~IUCLC;
#endif
/* Use 8-bit characters. This too may affect standard streams,
* but any sane C library can deal with 8-bit characters. */
terminal_settings.c_cflag &= ~CSIZE;
terminal_settings.c_cflag |= CS8;
/* Enable receiver. */
terminal_settings.c_cflag |= CREAD;
/* Let INTR/QUIT/SUSP/DSUSP generate the corresponding signals. */
terminal_settings.c_lflag |= ISIG;
/* Enable noncanonical mode.
* This is the most important bit, as it disables line buffering etc. */
terminal_settings.c_lflag &= ~ICANON;
/* Disable echoing input characters. */
terminal_settings.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
/* Disable implementation-defined input processing. */
terminal_settings.c_lflag &= ~IEXTEN;
/* To maintain best compatibility with normal behaviour of terminals,
* we set TIME=0 and MAX=1 in noncanonical mode. This means that
* read() will block until at least one byte is available. */
terminal_settings.c_cc[VTIME] = 0;
terminal_settings.c_cc[VMIN] = 1;
/* Set the new terminal settings.
* Note that we don't actually check which ones were successfully
* set and which not, because there isn't much we can do about it. */
tcsetattr(terminal_descriptor, TCSANOW, &terminal_settings);
/* Done. */
return errno = 0;
}
When terminal_init() is called, the terminal will be set into noncanonical mode (no line buffering etc.), echo (displaying characters the user types) will be disabled, and related standard C streams set to non-buffered mode.
Normal letters will stay unchanged in the input, but special keys generate either an ASCII code (Ctrl+A = 1, Ctrl+B = 1, and so on), or an ANSI escape sequence. For example, the up cursor key will generate three characters: '\033', '[', 'A'. It is of course the ANSI escape sequence for "one row up", "\033[A".
Here is a simple example program, which shows you the codes keypresses generate. It #include's the above "minimal.h".
Code:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include "minimal.h"
int main(void)
{
int c;
if (terminal_init()) {
if (errno == ENOTTY)
fprintf(stderr, "This program requires a terminal.\n");
else
fprintf(stderr, "Cannot initialize terminal: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
printf("Press CTRL+C or Q to quit.\n");
while ((c = getc(stdin)) != EOF) {
if (c >= 33 && c <= 126)
printf("0x%02x = 0%03o = %3d = '%c'\n", c, c, c, c);
else
printf("0x%02x = 0%03o = %3d\n", c, c, c);
if (c == 3 || c == 'Q' || c == 'q')
break;
}
printf("Done.\n");
return EXIT_SUCCESS;
}
Now, if you do not need compatibility with standard C I/O, and intend to use the entire terminal screen/window anyway, you should use ncurses or some other Curses library.
However, if you wanted to write a command-line application that is used to e.g. read the user password, something like the above is a good choice. (In particular, the first refresh operation after a Curses initscr() call will clear the screen, and that is usually unwanted in a straightforward command-line application.)
Truly portable code can avoid any issues the standard C I/O implementation may have, by instead using custom low-level I/O functions instead. In particular, a subset of functions provided by curses libraries would definitely be doable. (This is what GNU readline library does, for example.)
If there is interest, I could show some example code for that too. In particular, it might be useful to have non-blocking input (that parses ANSI escape sequences into a single int on input), and some limited curses-like functions; plus of course a normal printf() function to output to standard output and/or error (regardless of whether they are the terminal or something else).