[comp.lang.c] trivial "portable" terminal driver interface package

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