|
|
DataMuseum.dkPresents historical artifacts from the history of: DKUUG/EUUG Conference tapes |
This is an automatic "excavation" of a thematic subset of
See our Wiki for more about DKUUG/EUUG Conference tapes Excavated with: AutoArchaeologist - Free & Open Source Software. |
top - metrics - downloadIndex: T p
Length: 21268 (0x5314)
Types: TextFile
Names: »p1trm.c«
└─⟦a0efdde77⟧ Bits:30001252 EUUGD11 Tape, 1987 Spring Conference Helsinki
└─⟦this⟧ »EUUGD11/euug-87hel/sec1/vtrm/p1trm.c«
/* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */
/*
* ibm Pc virtual TeRMinal package.
*
* An implementation of the VTRM interface for MS-DOS machines.
*
* This code supports two MODE's of accessing the screen.
* The first one (BIOS) will be used, unless the user overwrites this
* by setting the SCREEN environment variable.
* This variable can also be used to convey a screen size that differs
* from the default 25 lines and 80 columns. See below.
*
* The two modes are:
*
* 1) IBM BIOS interrupt 10 hex, video io.
* (See IBM PC XT Technical Reference 6936833, May 1983,
* Appendix A, pages A46-A47).
* This is what you really want to use, since it's the only one that
* can decently scroll. It cannot insert or delete characters, so
* most optimisations from vtrm.c are useless and taken out.
* Unfortunately, not every PC-compatible machine supports this BIOS
* interrupt, so for these unlucky souls there is the following escape:
*
* 2) The ANSI.SYS driver.
* (See IBM MS-DOS 6936839, Jan 1983, Version 2.00, Chapter 13.)
* (Some compatibles don't have a separate ANSI.SYS driver but do the
* same escape interpretation by default.)
* This works reasonably, apart from scrolling downward, or part of
* the screen, which is clumsy.
* (The ANSI standard provides an escape sequence for scrolling
* but ANSI.SYS does not support it, nor any other way of scrolling.)
*
* The rest of the interface is the same as described in vtrm.c,
* with the following exceptions:
* - to ease coding for ansi scrolls, the terminal is supposed to
* contain blanks at positions that were not written yet;
* the unknown rubbish that is initially on the screen can
* only be cleared by the caller by scrolling the whole screen up
* by one or more lines;
* - the number of lines on the terminal is assumed to be 25;
* the number of columns is (1) determined by a BIOS function, or
* (2) assumed to be 80 for ANSI;
* the user can overwrite this by setting the environment variable:
*
* SET SCREEN=BIOS x y
* or
* SET SCREEN=ANSI x y
*
* where x and y are the number of lines and columns respectively.
*
* The lines and columns of our virtual terminal are numbered
* y = {0...lines-1} from top to bottom, and
* x = {0...cols-1} from left to right,
* respectively.
*
* The Visible Procedures in this package are as described in vtrm.c.
*
*/
/*
* Includes and data definitions.
*/
#include <stdio.h>
#include <signal.h>
#include <ctype.h>
#include <dos.h>
char *malloc();
#include "trm.h"
#ifdef lint
#define VOID (void)
#else
#define VOID
#endif
#define Forward
#define Visible
#define Hidden static
#define Procedure
typedef short intlet;
typedef char *string;
typedef char bool;
#define Yes '\1'
#define No '\0'
#define Undefined (-1)
#define Min(a,b) ((a) <= (b) ? (a) : (b))
#define MESS(number, text) text
#ifdef GFX
#include "gfx.h"
#endif
/* terminal status */
Hidden int started = No;
Hidden int scr_mode = 0;
#define ANSI 'A'
#define BIOS 'B'
#define Nlines 25
#define Ncols 80
Hidden int lines = Nlines;
Hidden int cols = Ncols;
Hidden int flags = 0;
/* current standout mode */
#define Off 0
#define On 0200
Hidden int so_mode = Off;
/* masks for char's and intlet's */
#define NULCHAR '\000'
#define CHAR 0177
#define SOBIT On
#define SOCHAR 0377
/* current cursor position */
Hidden intlet cur_y = Undefined, cur_x = Undefined;
/* "line[y][x]" holds the char on the terminal, with the SOBIT.
* the SOBIT tells whether the character is standing out.
* "lenline[y]" holds the length of the line.
* (Partially) empty lines are distinghuished by "lenline[y] < cols".
* Unknown chars will be ' ', so the scrolling routines for ANSI
* can use "unwritten" chars (with indent > 0 in trmputdata).
* To make the optimising compare in putline fail, lenline[y] is initially 0.
* The latter implies that if a line is first addressed with trmputdata,
* any rubbish that is on the screen beyond the data that gets put, will
* remain there.
*/
Hidden char **line = 0;
Hidden intlet *lenline = 0;
/* Make the cursor invisible when trmsync() tries to move outside the screen */
Hidden bool no_cursor = No;
/*
* Starting, Ending and (fatal) Error.
*/
/*
* Initialization call.
* Determine terminal mode.
* Start up terminal and internal administration.
* Return Yes if succeeded, No if trouble (which doesn't apply here).
*/
Visible int
trmstart(plines, pcols, pflags)
int *plines;
int *pcols;
int *pflags;
{
static char setup = No;
int err;
#ifdef TRACE
if (!setup) freopen("TRACE.DAT", "a", stderr);
fprintf(stderr, "\ttrmstart(&li, &co, &fl);\n");
#endif
if (started)
return TE_TWICE;
#ifdef GFX
if (gfx_mode != TEXT_MODE)
gfx_mode= SPLIT_MODE;
#endif
if (!setup) {
err= set_screen_up();
if (err != TE_OK)
return err;
setup = Yes;
}
err= start_trm(); /* internal administration */
if (err != TE_OK)
return err;
*plines = lines;
*pcols = cols;
*pflags = flags;
set_handler();
started = Yes;
return TE_OK;
}
Hidden int
set_screen_up()
{
int height;
int width;
int get_screen_env();
int get_cols();
height = width = 0;
scr_mode = get_screen_env(&height, &width);
switch (scr_mode) {
case BIOS:
case TE_OK:
cols = get_cols();
flags = HAS_STANDOUT|CAN_SCROLL;
break;
case ANSI:
flags = HAS_STANDOUT;
break;
default:
return scr_mode; /* Error flag */
}
/* allow x and y in environment variable SCREEN to override */
if (height > 0)
lines = height;
if (width > 0)
cols = width;
return TE_OK;
}
Hidden int
get_screen_env(pheight, pwidth)
int *pheight, *pwidth;
{
string s;
int mode;
char screrr;
string getenv();
string strip();
string skip();
screrr = No;
s = getenv("SCREEN");
if (s == NULL)
return BIOS;
s = strip(s);
switch (*s) {
case '\0':
return BIOS;
case 'a':
mode = ANSI;
s = skip(s, "ansi");
break;
case 'A':
mode = ANSI;
s = skip(s, "ANSI");
break;
case 'b':
mode = BIOS;
s = skip(s, "bios");
break;
case 'B':
mode = BIOS;
s = skip(s, "BIOS");
break;
default:
mode = BIOS;
screrr = Yes;
}
/* *pheight and *pwidth were set to 0 above */
s = strip(s);
while (isdigit(*s)) {
*pheight = *pheight * 10 + (*s++ - '0');
}
s = strip(s);
while (isdigit(*s)) {
*pwidth = *pwidth * 10 + (*s++ -'0');
}
s = strip(s);
if (screrr || *s != '\0')
return TE_BADTERM;
return mode;
}
Hidden string strip(s)
string s;
{
while (*s == ' ' || *s == '\t')
++s;
return s;
}
Hidden string skip(s, pat)
string s, pat;
{
while (*s == *pat)
++s, ++pat;
return s;
}
Hidden int /* initialise internal administration */
start_trm()
{
register int y;
if (line == 0) {
if ((line = (char**) malloc(lines * sizeof(char*))) == NULL)
return TE_NOMEM;
for (y = 0; y < lines; y++) {
if ((line[y] = malloc(cols * sizeof(char))) == NULL)
return TE_NOMEM;
}
}
if (lenline == 0) {
if ((lenline = (intlet *)
malloc(lines * sizeof(intlet))) == NULL)
return TE_NOMEM;
}
trmundefined();
return TE_OK;
}
/*
* Termination call.
* Beware that it might be called by a catched interrupt even in the middle
* of trmstart()!
*/
Visible Procedure
trmend()
{
#ifdef TRACE
fprintf(stderr, "\ttrmend();\n");
#endif
if (started && so_mode != Off)
standend();
if (scr_mode == ANSI) {
VOID fflush(stdout);
}
started = No;
}
/*
* Set all internal statuses to undefined, especially the contents of
* the screen, so a hard redraw will not be optimised to heaven.
*/
Visible Procedure
trmundefined()
{
register int y, x;
#ifdef TRACE
fprintf(stderr, "\ttrmundefined();\n");
#endif
cur_y = cur_x = Undefined;
so_mode = Undefined;
for (y = 0; y < lines; y++) {
for (x = 0; x < cols; x++)
line[y][x] = ' ';
/* they may get printed in scrolling */
lenline[y] = 0;
}
}
#ifdef DEBUG
Hidden Procedure
check_started(m)
char *m;
{
if (!started) {
printf("Not started: %s\n", m);
exit(TE_TWICE);
}
}
#else
#define check_started(m) /*empty*/
#endif
/*
* Sensing the cursor.
* (NOT IMPLEMENTED, since there is no way to locally move the cursor.)
*/
/*
* Sense the current (y, x) cursor position, after a possible manual
* change by the user with local cursor motions.
* If the terminal cannot be asked for the current cursor position,
* or if the string returned by the terminal is garbled,
* the position is made Undefined.
*/
Visible Procedure
trmsense(py, px)
int *py;
int *px;
{
/* bool getpos(); */
#ifdef TRACE
fprintf(stderr, "\ttrmsense(&yy, &xx);\n");
#endif
check_started(MESS(7904, "trmsense called outside trmstart/trmend"));
*py = *px = Undefined;
/*
* if (flags&CAN_SENSE && getpos(py, px)) {
* if (*py < 0 || lines <= *py || *px < 0 || cols <= *px)
* *py = *px = Undefined;
* }
*/
cur_y = *py;
cur_x = *px;
}
/*
* Putting data on the screen.
*/
/*
* Fill screen area with given data.
* Characters with the SO-bit (0200) set are put in standout mode.
* (Unfortunately this makes it impossible to display accented characters.
* The interface should change.)
*/
Visible Procedure
trmputdata(yfirst, ylast, indent, data)
int yfirst;
int ylast;
register int indent;
register string data;
{
register int y;
int x, len, lendata, space;
#ifdef TRACE
fprintf(stderr, "\ttrmputdata(%d, %d, %d, \"%s\");\n", yfirst, ylast, indent, data);
#endif
check_started(MESS(7905, "trmputdata called outside trmstart/trmend"));
if (yfirst < 0)
yfirst = 0;
if (ylast >= lines)
ylast = lines-1;
space = cols*(ylast-yfirst+1) - indent;
if (space <= 0)
return;
yfirst += indent/cols;
indent %= cols;
y = yfirst;
if (data) {
x = indent;
lendata = strlen(data);
if (ylast == lines-1 && lendata >= space)
lendata = space - 1;
len = Min(lendata, cols-x);
while (y <= ylast) {
put_line(y, x, data, len);
y++;
lendata -= len;
if (lendata > 0) {
x = 0;
data += len;
len = Min(lendata, cols);
}
else
break;
}
}
if (y <= ylast)
clear_lines(y, ylast);
}
/*
* We will try to get the picture:
*
* op>>>>>>>>>>>op oq
* ^ ^ ^
* <xskip><-----m1----><---------------od-------------------->
* OLD: "You're in a maze of twisty little pieces of code, all alike"
* NEW: "in a maze of little twisting pieces of code, all alike"
* <-----m1----><----------------nd--------------------->
* ^ ^ ^
* np>>>>>>>>>>>np nq
* where
* op, oq, np, nq are pointers to start and end of Old and New data,
* and
* xskip = length of indent to be skipped,
* m1 = length of Matching part at start,
* od = length of Differing end on screen,
* nd = length of Differing end in data to be put.
*/
Hidden int
put_line(y, xskip, data, len)
int y, xskip;
string data;
int len;
{
register char *op, *oq, *np, *nq;
int m1, od, nd, delta;
/* calculate the magic parameters */
op = &line[y][xskip];
oq = &line[y][lenline[y]-1];
np = data;
nq = data + len - 1;
m1 = 0;
while ((*op&SOCHAR) == (*np&SOCHAR) && op <= oq && np <= nq)
op++, np++, m1++;
od = oq - op + 1;
nd = nq - np + 1;
/* now we have the picture above */
if (od==0 && nd==0)
return;
delta = nd - od;
move(y, xskip + m1);
if (nd > 0) {
put_str(np, nd);
}
if (delta < 0) {
clr_to_eol();
return;
}
lenline[y] = xskip + len;
if (cur_x == cols) {
cur_y++;
cur_x = 0;
}
}
/*
* Scrolling (part of) the screen up (or down, dy<0).
*/
Visible Procedure
trmscrollup(yfirst, ylast, by)
register int yfirst;
register int ylast;
register int by;
{
#ifdef TRACE
fprintf(stderr, "\ttrmscrollup(%d, %d, %d);\n", yfirst, ylast, by);
#endif
check_started(MESS(7906, "trmscrollup called outside trmstart/trmend"));
if (by == 0)
return;
if (yfirst < 0)
yfirst = 0;
if (ylast >= lines)
ylast = lines-1;
if (yfirst > ylast)
return;
if (so_mode != Off)
standend();
if (by > 0 && yfirst + by > ylast
||
by < 0 && yfirst - by > ylast)
{
clear_lines(yfirst, ylast);
return;
}
switch (scr_mode) {
case BIOS:
biosscrollup(yfirst, ylast, by);
break;
case ANSI:
if (by > 0 && yfirst == 0) {
lf_scroll(ylast, by);
}
else if (by > 0) {
move_lines(yfirst+by, yfirst, ylast-yfirst+1-by, 1);
clear_lines(ylast-by+1, ylast);
}
else {
move_lines(ylast+by, ylast, ylast-yfirst+1+by, -1);
clear_lines(yfirst, yfirst-by-1);
}
break;
}
}
/*
* Synchronization, move cursor to given position (or previous if < 0).
*/
Visible Procedure
trmsync(y, x)
int y;
int x;
{
#ifdef TRACE
fprintf(stderr, "\ttrmsync(%d, %d);\n", y, x);
#endif
check_started(MESS(7907, "trmsync called outside trmstart/trmend"));
if (0 <= y && y < lines && 0 <= x && x < cols) {
move(y, x);
}
VOID fflush(stdout);
}
/*
* Send a bell, visible if possible.
*/
Visible Procedure
trmbell()
{
#ifdef TRACE
fprintf(stderr, "\ttrmbell();\n");
#endif
check_started(MESS(7908, "trmbell called outside trmstart/trmend"));
ring_bell();
}
/*
* Now for the real work: here are all low level routines that really
* differ for BIOS or ANSI mode.
*/
/*
* BIOS video io is called by generating an 8086 software interrupt,
* using lattice's int86() function.
* To ease coding, all routines fill in the apropriate parameters in regs,
* and than call bios10(code), where code is to be placed in ah.
*/
Hidden union REGS regs, outregs;
/* A macro for speed */
#define bios10(code) (regs.h.ah = (code), int86(0x10, ®s, ®s))
#define nbios10(code) (regs.h.ah = (code), int86(0x10, ®s, &outregs))
/* Video attributes: (see the BASIC manual) (used for standout mode) */
Hidden int video_attr;
#ifndef GFX
#define V_NORMAL 7
#else
#define V_NORMAL (gfx_mode == TEXT_MODE ? 7 : 0)
#endif
#define V_STANDOUT (7<<4)
/* Some BIOS only routines */
Hidden get_cols()
{
bios10(15);
return regs.h.ah;
}
/*
* ANSI escape sequences
*/
#define A_CUP "\033[%d;%dH" /* cursor position */
#define A_SGR0 "\033[0m" /* set graphics rendition to normal */
#define A_SGR7 "\033[7m" /* set graphics rendition to standout */
#define A_ED "\033[2J" /* erase display (and cursor home) */
#define A_EL "\033[K" /* erase (to end of) line */
/*
* The following routine is the time bottleneck, I believe!
*/
Hidden Procedure
put_str(data, n)
char *data;
int n;
{
register char c, so;
so = so_mode;
if (scr_mode == BIOS) {
regs.x.cx = 1; /* repition count */
regs.h.bh = 0; /* page number */
regs.h.bl = video_attr;
while (--n >= 0) {
c = (*data++)&SOCHAR;
if ((c&SOBIT) != so) {
so = c&SOBIT;
so ? standout() : standend();
regs.h.bl = video_attr;
}
regs.h.al = c&CHAR;
nbios10(9);
if (cur_x >= cols-1) {
line[cur_y][cols-1] = c;
continue;
}
regs.h.dh = cur_y;
regs.h.dl = cur_x + 1;
nbios10(2);
line[cur_y][cur_x] = c;
cur_x++;
}
}
else {
while (--n >= 0) {
c = (*data++)&SOCHAR;
if ((c&SOBIT) != so) {
so = c&SOBIT;
so ? standout() : standend();
}
putch(c&CHAR);
line[cur_y][cur_x] = c;
cur_x++;
}
}
}
/*
* Move to position y,x on the screen
*/
Hidden Procedure
move(y, x)
int y, x;
{
if (scr_mode != BIOS && cur_y == y && cur_x == x)
return;
switch (scr_mode) {
case BIOS:
regs.h.dh = y;
regs.h.dl = x;
regs.h.bh = 0; /* Page; must be 0 for graphics */
bios10(2);
break;
case ANSI:
cprintf(A_CUP, y+1, x+1);
break;
}
cur_y = y;
cur_x = x;
}
Hidden Procedure
standout()
{
so_mode = On;
switch (scr_mode) {
case BIOS:
video_attr = V_STANDOUT;
break;
case ANSI:
cputs(A_SGR7);
break;
}
}
Hidden Procedure
standend()
{
so_mode = Off;
switch (scr_mode) {
case BIOS:
video_attr = V_NORMAL;
break;
case ANSI:
cputs(A_SGR0);
break;
}
}
#ifdef UNUSED
Hidden Procedure
put_c(c)
int c;
{
int ch;
ch = c&CHAR;
#ifndef NDEBUG
if (!isprint(ch)) {
ch = '?';
c = (c&SOBIT)|'?';
}
#endif
switch (scr_mode) {
case BIOS:
regs.h.al = ch;
regs.h.bl = video_attr;
regs.x.cx = 1; /* repition count */
regs.h.bh = 0; /* page number */
bios10(9);
if (cur_x >= cols-1) {
line[cur_y][cols-1] = c;
return;
}
regs.h.dh = cur_y;
regs.h.dl = cur_x + 1;
bios10(2);
break;
case ANSI:
putch(ch);
break;
}
line[cur_y][cur_x] = c;
cur_x++;
}
#endif UNUSED
Hidden Procedure
clear_lines(yfirst, ylast)
int yfirst, ylast ;
{
register int y;
if (scr_mode == BIOS) {
regs.h.al = 0; /* scroll with al = 0 means blank window */
regs.h.ch = yfirst;
regs.h.cl = 0;
regs.h.dh = ylast;
regs.h.dl = cols-1;
regs.h.bh = V_NORMAL;
bios10(6);
for (y = yfirst; y <= ylast; y++)
lenline[y] = 0;
return;
}
/* scr_mode == ANSI */
if (yfirst == 0 && ylast == lines-1) {
if (so_mode == On)
standend();
move(0, 0); /* since some ANSI'd don't move */
cputs(A_ED);
cur_y = cur_x = 0;
for (y = yfirst; y < ylast; y++)
lenline[y] = 0;
return;
}
for (y = yfirst; y <= ylast; y++) {
if (lenline[y] > 0) {
move(y, 0);
clr_to_eol();
}
}
}
Hidden Procedure
clr_to_eol()
{
if (so_mode == On)
standend();
switch (scr_mode) {
case BIOS:
regs.h.bh = 0; /* page */
regs.x.cx = lenline[cur_y] - cur_x;
regs.h.al = ' ';
regs.h.bl = V_NORMAL;
bios10(9);
break;
case ANSI:
cputs(A_EL);
break;
}
lenline[cur_y] = cur_x;
}
Hidden Procedure /* scrolling for BIOS */
biosscrollup(yfirst, ylast, by)
int yfirst;
int ylast;
int by;
{
regs.h.al = (by < 0 ? -by : by);
regs.h.ch = yfirst;
regs.h.cl = 0;
regs.h.dh = ylast;
regs.h.dl = cols-1;
regs.h.bh= V_NORMAL;
bios10(by < 0 ? 7 : 6);
cur_y = cur_x = Undefined;
if (by > 0)
scr_lines(yfirst, ylast, by, 1);
else
scr_lines(ylast, yfirst, -by, -1);
}
Hidden Procedure /* Reset internal administration accordingly */
scr_lines(yfrom, yto, n, dy)
int yfrom, yto, n, dy;
{
register int y, x;
char *saveln;
while (n-- > 0) {
saveln = line[yfrom];
for (y = yfrom; y != yto; y += dy) {
line[y] = line[y+dy];
lenline[y] = lenline[y+dy];
}
line[yto] = saveln;
for (x = 0; x < cols; x++ )
line[yto][x] = ' ';
lenline[yto] = 0;
}
}
Hidden Procedure
lf_scroll(yto, by)
int yto;
int by;
{
register int n = by;
move(lines-1, 0);
while (n-- > 0) {
putch('\n');
}
scr_lines(0, lines-1, by, 1);
move_lines(lines-1-by, lines-1, lines-1-yto, -1);
clear_lines(yto-by+1, yto);
}
Hidden Procedure /* for dumb scrolling, uses and updates */
move_lines(yfrom, yto, n, dy) /* internal administration */
int yfrom;
int yto;
int n;
int dy;
{
while (n-- > 0) {
put_line(yto, 0, line[yfrom], lenline[yfrom]);
yfrom += dy;
yto += dy;
}
}
Hidden Procedure ring_bell()
{
switch (scr_mode) {
case BIOS:
regs.h.al = '\007';
regs.h.bl = V_NORMAL;
bios10(14);
break;
case ANSI:
putch('\007');
break;
}
}
/*
* Show the current internal statuses of the screen on stderr.
* For debugging only.
*/
#ifdef SHOW
Visible Procedure
trmshow(s)
char *s;
{
int y, x;
fprintf(stderr, "<<< %s >>>\n", s);
for (y = 0; y < lines; y++) {
for (x = 0; x <= lenline[y] /*** && x < cols ***/ ; x++) {
fputc(line[y][x]&CHAR, stderr);
}
fputc('\n', stderr);
for (x = 0; x <= lenline[y] && x < cols-1; x++) {
if (line[y][x]&SOBIT)
fputc('-', stderr);
else
fputc(' ', stderr);
}
fputc('\n', stderr);
}
fprintf(stderr, "CUR_Y = %d, CUR_X = %d.\n", cur_y, cur_x);
VOID fflush(stderr);
}
#endif
/*
* Interrupt handling.
*
* (This has not properly been tested, nor is it clear that
* this interface is what we want. Anyway, it's here for you
* to experiment with. What does it do, you may ask?
* Assume an interactive program which reads its characters
* through trminput. Assume ^C is the interrupt character.
* Normally, ^C is treated just like any other character: when
* typed, it turns up in the input. The program may understand
* input ^C as "quit from the current mode".
* Occasionally, the program goes into a long period of computation.
* Now it would be uninterruptible, except if it calls trminterrupt
* at times in its computational loop. Trminterrupt magically looks
* ahead in the input queue, and if it sees a ^C, discards all input
* before that point and returns Yes. It also sets a flag, so that
* the interupt "sticks around" until either trminput or trmavail
* is called. It is undefined whether typing ^C several times in
* a row is seen as one interrupt, or an interrupt followed by input
* of ^C's. A program should be prepared for either.)
*/
static bool intrflag= No;
static
handler(sig)
int sig;
{
signal(sig, handler);
intrflag= Yes;
}
static
set_handler()
{
signal(SIGINT, handler);
}
bool
trminterrupt()
{
/* Force a check for Control-break which will call handler. */
kbhit();
return intrflag;
}
/*
* Terminal input without echo.
* (This is a recent addition to this module, but will soon be standard).
*/
trminput()
{
intrflag= No;
return bdos(0x7, 0, 0) & 0377; /* Input, no echo, no ^C checks */
}
/*
* Check for character available.
*
* To do this properly, should call DOS function 6,
* but the relevant bit (the Z flag) can't be checked from C.
* For now, say we don't know.
*/
trmavail()
{
intrflag= No;
return -1;
}
trmsuspend()
{
/* Not implementable on MS-DOS */
}