scs@adam.mit.edu (Steve Summit) (03/02/91)
This posting contains sources and documentation for a small package which implements a quasi-portable interface to the notoriously variegated terminal driver facilities provided by various operating systems. This code springs from a widespread desire, voiced frequently on comp.lang.c (though not, I hasten to add, by me) for a defined, standard way to ask for certain services of the terminal driver, so that "simple" programs having "simple" needs (i.e. character-at-a-time input) would not need to have #ifdefs in their low-level I/O code for 37 different operating systems. This is not, by any stretch of the imagination, production- quality code. It is intended mainly as a pedagogical example. To use this simple package, a program must #include "ttyio.h", and call tty_init. It can then call tty_setmode to control tty operating modes (only two are defined: character-at-a-time-ness and echo), and tty_getchar to read single characters. tty_navail reports the number of characters immediately available, if known. tty_reset cleans up the terminal driver before exiting. This posting contains implementations for V7/bsd Unix and MS-DOS, an untested implementation based on curses, and a dubious, completely untested implementation for Posix. (The MS-DOS version has been tested under Microsoft C, but I believe most if not all PC C RTL's supply getch and kbhit.) A System V version would probably be very close to Posix, but I don't have access to a System V system for testing. It has been several years since I worked with VMS, and I have forgotten the details of its terminal driver, so I am not able to provide a VMS implementation at the moment. The shar file following my signature contains these seven files: ttyio.3 man page ttyio.h header file ttytest.c test program ttyio.c BSD/V7 implementation ttyio.curses.c curses implementation (untested) ttyio.msdos.c MS-DOS implementation ttyio.posix.c Posix/SysVr4 implementation (completely untested) After writing ttyio.curses.c, I discovered (because this system does not have them!) that the cbreak(), nocbreak(), echo(), noecho(), and getch() curses functions upon which it is based do not appear in all versions of curses. Caveat emptor. Anyone who knows anything about Posix/SysV termios is going to look at ttyio.posix.c and laugh. This is the first time I've actually read the termios documentation and tried to write any code against it, and since I don't have a system to test it on, it is guaranteed to be wrong. I confess that I don't understand the distinction between c_cc[MIN] and c_cc[VMIN] (similarly for TIME and VTIME), and I know that there is some ineffable nonsense having to do with the possibility of VMIN and VTIME overlapping VEOF and VEOL, requiring circumlocutions when playing with ICANON, which I have certainly gotten wrong. (Since this is the first time I've looked at termios, I'll withhold judgement on it; perhaps it has attractions I'm overlooking. Presumably it was defined and adopted in its present form because of its inherent advantages and superiority over other alternatives.) This package is in the public domain; use it in good health. I couldn't put copyright notices on it if I wanted to; the code (with the exception of the termios stuff, which is wrong, anyway) is obvious, trivial, and appears as examples in countless other published works. I'm not sure what to suggest be done with the bugfixes and improvements to this package that some people are going to insist upon providing. Definitely don't post them to comp.lang.c . Post them to alt.sources if you must. You can mail them to me, and I'll mail them back out to anyone who asks, but I'm not going to spend much, if any, time integrating them or maintaining this "package." Steve Summit scs@adam.mit.edu echo extracting ttyio.3 cat > ttyio.3 <<\% .TH TTYIO 3 .SH NAME ttyio \- trivial terminal driver interface .SH SYNOPSIS .nf #include "ttyio.h" tty_init() tty_setmode(mode) int mode; tty_getchar() tty_navail() tty_reset() .fi .SH DESCRIPTION .PP These routines implement a trivial interface to a few popular features of the terminal drivers usually supplied with interactive operating systems. All facilities are defined in terms of the "controlling terminal" of the calling process (standard input, or file descriptor 0, for Unix systems). .PP The header file "ttyio.h" must be #included by any source file making use of these facilities. It contains the symbolic constants used by the tty_setmode routine, and may also implement some of these routines as function-like macros. .PP tty_init must be called first, before any of the other routines. .PP tty_setmode is used to set operating modes. The mode argument is formed by or-ing together symbolic constants, which are #defined in "ttyio.h". Separate values are used to turn on and off each mode. The available values are: .sp .nf .ta 1i +\w'TTY_NOCANON'u+3m TTY_ECHO TTY_NOECHO TTY_CANON "canonical" erase/newline processing TTY_NOCANON character-at-a-time processing .fi .sp TTY_ECHO and TTY_NOECHO have the obvious meanings. TTY_NOCANON turns off the "canonical" processing of the various line editing characters (backspace, delete/rubout, and any word or line kill characters) and makes input characters available immediately, without waiting for a newline (carriage return) character. (Interrupt characters, however, remain active.) TTY_CANON returns to "canonical" processing. .PP tty_setmode returns 0 if it is successful and -1 if the requested mode(s) could not be set. After a failure, errno may or may not be useful; the most likely cause for failure is that the "controlling terminal" is not really a terminal (ENOTTY) but is rather a file or a pipe. .PP tty_getchar gets and returns one character, with processing performed according to the modes set by previous calls to tty_setmode. tty_getchar blocks until character(s) are available. If TTY_NOCANON mode is in effect, the operating system's end-of-file character (usually control-D for Unix, control-Z for VMS and MS-DOS) may or may not be translated to the <stdio.h> value EOF. .PP (tty_getchar must be used for input while this package is being used; any other input routines -- getchar(3), read(2), etc. -- may behave unpredictably.) .PP tty_navail returns the number of characters which are immediately available for reading via tty_getchar, if this number can be determined. tty_navail is not implementable under all systems, and may return an approximation on systems where it does work. It returns -1 if it cannot determine the number of characters available; the calling program will then have to make its own conservative approximation. (Whether it is better to assume that characters are or aren't available when the answer is not definitively determinable is a decision which is best made by the calling program.) Some operating systems can report that there are characters available, but not how many; tty_navail returns 1 in this case. .PP tty_reset should be called before the calling program exits, to restore the terminal driver to its default or initial state. tty_reset can also be called before the calling program suspends operation or otherwise gives up control. It is permissible to call tty_setmode again, to re-establish non-default modes, after calling tty_reset, as long as tty_reset is called a final time before exiting. .SH BUGS .PP Not general enough to satisfy anybody (let alone "Tenex fans"). .PP Under some systems, calling setmode with only the value TTY_NOECHO may implicitly turn on TTY_NOCANON. .PP There is no way to specify the file descriptor of the tty to be controlled; standard input (fd 0) is assumed. .PP No provision for output or output modes. % chmod 664 ttyio.3 if test `wc -c < ttyio.3` -ne 3878; then echo "error extracting ttyio.3" 1>&2 fi echo extracting ttyio.h cat > ttyio.h <<\% #ifndef TTYIO_H #define TTYIO_H /* values for tty_setmode(): */ #define TTY_ECHO 0x01 #define TTY_NOECHO 0x02 #define TTY_CANON 0x04 /* "canonical" erase/newline processing */ #define TTY_NOCANON 0x08 /* character-at-a-time processing */ #endif % chmod 664 ttyio.h if test `wc -c < ttyio.h` -ne 248; then echo "error extracting ttyio.h" 1>&2 fi echo extracting ttytest.c cat > ttytest.c <<\% #include <stdio.h> #include <ctype.h> #include "ttyio.h" #define Streq(s1, s2) (strcmp(s1, s2) == 0) main(argc, argv) int argc; char *argv[]; { int flags = 0; int i; int c; int r; for(i = 1; i < argc; i++) { if(Streq(argv[i], "echo")) flags |= TTY_ECHO; else if(Streq(argv[i], "noecho")) flags |= TTY_NOECHO; else if(Streq(argv[i], "canon")) flags |= TTY_CANON; else if(Streq(argv[i], "nocanon")) flags |= TTY_NOCANON; else fprintf(stderr, "ttytest: unknown mode \"%s\"\n", argv[i]); } tty_init(); if(tty_setmode(flags) < 0) { fprintf(stderr, "ttytest: can't set requested mode(s)\n"); exit(1); } while(1) { while((r = tty_navail()) == 0) ; printf("tty_navail returned %d\n", r); if(r <= 0) r = 1; for(i = 0; i < r; i++) { c = tty_getchar(); if(isprint(c)) printf("you typed %c\n", c); else printf("you typed %d\n", c); if(c == EOF) break; } if(c == EOF) break; } tty_reset(); } % chmod 664 ttytest.c if test `wc -c < ttytest.c` -ne 942; then echo "error extracting ttytest.c" 1>&2 fi echo extracting ttyio.c cat > ttyio.c <<\% #include <stdio.h> #include <sgtty.h> #include "ttyio.h" #define TRUE 1 #define FALSE 0 static struct sgttyb savetty; static struct sgttyb tty; static int havetty = FALSE; tty_init() { } tty_setmode(flags) int flags; { if(!havetty) { if(ioctl(0, TIOCGETP, &savetty) < 0) return -1; tty = savetty; havetty = TRUE; } if(flags & TTY_ECHO) tty.sg_flags |= ECHO; if(flags & TTY_NOECHO) tty.sg_flags &= ~ECHO; if(flags & TTY_CANON) { tty.sg_flags &= ~CBREAK; tty.sg_flags |= CRMOD; } if(flags & TTY_NOCANON) { tty.sg_flags |= CBREAK; tty.sg_flags &= ~CRMOD; } if(ioctl(0, TIOCSETN, &tty) < 0) return -1; return 0; } tty_getchar() { return getchar(); } tty_navail() { #ifdef FIONREAD int nchars; if(ioctl(0, FIONREAD, &nchars) < 0) return -1; return nchars; #else return -1; #endif } tty_reset() { if(havetty) ioctl(0, TIOCSETN, &savetty); } % chmod 664 ttyio.c if test `wc -c < ttyio.c` -ne 875; then echo "error extracting ttyio.c" 1>&2 fi echo extracting ttyio.curses.c cat > ttyio.curses.c <<\% #include <stdio.h> #include "ttyio.h" tty_init() { initscr(); } tty_setmode(flags) int flags; { if(flags & TTY_ECHO) echo(); if(flags & TTY_NOECHO) noecho(); if(flags & TTY_CANON) { nocbreak(); nl(); } if(flags & TTY_NOCANON) { cbreak(); nonl(); } return 0; } tty_getchar() { return getch(); } tty_navail() { /* I don't know how to do this in curses */ return -1; } tty_reset() { endwin(); } % chmod 664 ttyio.curses.c if test `wc -c < ttyio.curses.c` -ne 411; then echo "error extracting ttyio.curses.c" 1>&2 fi echo extracting ttyio.msdos.c cat > ttyio.msdos.c <<\% #include <stdio.h> #include "ttyio.h" static int mode = 0; /* values for internal mode word: */ #define NOCANON 1 #define NOECHO 2 tty_init() { } tty_setmode(flags) int flags; { if(!isatty(0)) /* if you don't have isatty(), just delete this test */ return -1; if(flags & TTY_ECHO) mode &= ~NOECHO; if(flags & TTY_NOECHO) mode |= NOECHO; if(flags & TTY_CANON) mode &= ~NOCANON; if(flags & TTY_NOCANON) mode |= NOCANON; return 0; } tty_getchar() { switch(mode) { case 0: return getchar(); case NOCANON: return getche(); case NOCANON | NOECHO: case NOECHO: /* oh, well, noecho gets you nocanon */ return getch(); } } tty_navail() { if(!(mode & NOCANON)) return -1; return kbhit() ? 1 : 0; } tty_reset() { mode = 0; } % chmod 664 ttyio.msdos.c if test `wc -c < ttyio.msdos.c` -ne 753; then echo "error extracting ttyio.msdos.c" 1>&2 fi echo extracting ttyio.posix.c cat > ttyio.posix.c <<\% #include <stdio.h> #include <termios.h> #include "ttyio.h" #define TRUE 1 #define FALSE 0 static struct termios savetty; static struct termios tty; static int havetty = FALSE; static int savevmin, savevtime; static int havevminvtime = FALSE; tty_init() { } tty_setmode(flags) int flags; { if(!havetty) { if(tcgetattr(0, &savetty) < 0) return -1; tty = savetty; havetty = TRUE; } if(flags & TTY_ECHO) tty.c_lflag |= ECHO; if(flags & TTY_NOECHO) tty.c_lflag &= ~ECHO; if(flags & TTY_CANON) { tty.c_lflag |= ICANON; tty.c_iflag |= ICRNL; if(havevminvtime) { tty.c_cc[VMIN] = savevmin; tty.c_cc[VTIME] = savevtime; } } if(flags & TTY_NOCANON) { tty.c_lflag &= ~ICANON; tty.c_iflag &= ~ICRNL; savevmin = tty.c_cc[VMIN]; savevtime = tty.c_cc[VTIME]; havevminvtime = TRUE; tty.c_cc[VMIN] = 1; tty.c_cc[VTIME] = 0; } if(tcsetattr(0, TCSANOW, &tty) < 0) return -1; return 0; } tty_getchar() { return getchar(); } tty_navail() { #ifdef FIONREAD int nchars; if(ioctl(0, FIONREAD, &nchars) < 0) return -1; return nchars; #else return -1; #endif } tty_reset() { if(havetty) { if(!(tty.c_lflag & ICANON) && havevminvtime) { savetty.c_cc[VMIN] = savevmin; savetty.c_cc[VTIME] = savevtime; } tcsetattr(0, TCSANOW, &savetty); } } % chmod 664 ttyio.posix.c if test `wc -c < ttyio.posix.c` -ne 1280; then echo "error extracting ttyio.posix.c" 1>&2 fi