[comp.sys.atari.8bit] VT52B: Yet Another Terminal Emulator

ehs@jumbo.dec.com (Ed Satterthwaite) (09/28/87)

Hardly what the 8-bit Atari world needs most right now, but here's why I
often use this one when I just want a Unix terminal (no file transfers):

- It uses the character set I find most legible on my particular monitor
(Commodore 1702, composite color).  This is a very subjective call, and I
make no claims at all about results on a good monochrome monitor.

- It has relatively fast implementations of most escape sequences.  These
provide the terminal capabilities needed to make the use of a
screen-oriented editor (Emacs) bearable.

- The program is small and simple; it loads and initializes quickly.

- Source (in ACTION!) is available for customization and adaptation.

I'm posting VT52B in case anyone else has similar requirements.  It is an
upgrade of the ACTION! program VT52A posted by Michael Jenkin about a year
ago.  Michael's font looks bolder and more blocky than the ones used by
the other 80 column programs that I've tried.  In addition, the
proportions of some characters are exaggerated a bit to make the
corresponding shapes more distinctive.  This is a good trade-off for my
eyes and monitor; your mileage may differ.  I have modified VT52A by
reworking the display management, adding a few new escape sequences, and
generally trying to streamline what seemed to be the critical loops.  My
thanks to Michael for permission to use his character set as well as the
general framework of his program.

I am posting the sources for three reasons.  First, I don't have a copy of
the ACTION! runtime library to link with the object code.  Second, this is
an edit-and-compile project; there are no runtime menus, and some
customization of the source will probably be necessary.  Finally,
80-column screen management is done by an "A:" handler that is comparable
to the standard "S:" handler.  It might be of some interest for other
applications, even if you don't care about a terminal emulator.

Most of the revision of VT52A was done as a Sunday afternoon hack, with
some further additions and changes suggested by experience with the
program.  It now does what I need, but there is plenty of room for further
development.  All my testing has been done with an Atari 800, an Atari 850
interface, and Hayes-incompatible modems at 1200 and 2400 baud.
The mainframes have been VAXen running various versions of Ultrix
(essentially similar to BSD 4.2 and 4.3 Unix) with Unipress (Gosling)
Emacs as the primary editor.  I've had considerable trouble getting other
terminal emulators to work in this environment.  If you have similar
problems getting VT52B to work for you, I'd be interested in hearing about
it.

Some documentation and a suggested Unix termcap entry appear in the next
message.  The message following that contains the ACTION! source code for
VT52B.ACT.

Ed Satterthwaite
DEC Systems Research Center, Palo Alto, CA
Arpa: ehs@src.DEC.COM
Uucp: {...}!decwrl!ehs

ehs@jumbo.dec.com (Ed Satterthwaite) (09/28/87)

------------------------------------------------------------------------------

                        VT52B Documentation

Overview
--------

VT52B consists of two modules.  One is a fairly general handler for an
"A:" device that manages a 24 x 80 screen.  It provides a
character-oriented output device that recognizes embedded escape
sequences.  These escape sequences perform various display control
functions.  They support a subset of the VT52 terminal capabilities plus a
few extensions needed for incremental display updates.  The other module
is a very simple (perhaps too simple) terminal protocol manager that
attempts to coordinate the K:, R: and A: devices to emulate an
extended VT52 terminal.

The A: handler uses techniques of display list management that are by now
fairly standard and well-known.  I hope that most of the code will be
fairly scrutable to an experienced ACTION! programmer.  I will not attempt
to explain it here but I will try to answer any specific questions that
arise.


The A: Handler
--------------

The following VT52 escape sequences are supported:

	esc-A		moves cursor up one line
	esc-B		moves cursor down one line
	esc-C		moves cursor right one column
	esc-D		moves cursor left one column
	esc-H		homes cursor
	esc-I		reverse line feed
	esc-J		clears from cursor to end of screen
	esc-K		clears from cursor to end of line
	esc-Y		positions cursor (see VT52 specs for X,Y encoding)

The following extensions are also supported:

	esc-F		enters 'stand-out' (inverse video) mode
	esc-G		exits 'stand-out' mode
	esc-L		inserts blank character space at the cursor
	esc-M		deletes character at the cursor
	esc-N		inserts blank line at the cursor
	esc-O		deletes line containing the cursor

A suitable UNIX termcap entry appears at the end of this file.  The
extension codes have been chosen to be compatible with the "VT-52XL"
terminal supported by Chameleon, and the same termcap should be suitable
for both.

The support for 'stand-out' mode has not been extensively tested, but it
is good enough to do Emacs mode lines and the like.  Its use is a mixed
blessing, however, since the inverted video can be much less readable.  To
disable it, remove the next-to-last line from the suggested termcap.

The code could easily be modified to support 'insert' and 'delete' modes
directly, and the corresponding incremental updates would be considerably
faster.  I have not done this because such modality can be disastrous over
a noisy or unreliable line.

When the A: device is opened, it allocates space for its display bit maps
and custom display lists from the top of available memory as recorded in
MEMTOP (location $2E5) and updates MEMTOP.  These data structures require
approximately 8200 bytes.  If sufficient space is not available, the open
fails and MEMTOP is not changed.


Terminal Emulation
------------------

ASCII characters not on the Atari keyboard can be entered as follows:

	{	ctrl-< or shift-<
	}	ctrl-> or shift->
	~	ctrl-backS or shift-backS
	`	ctrl-.

The emulator does not provide flow control; buffer overruns drop
characters.  In my experience, this is never a problem at 1200 baud and is
not a problem at 2400 for screen-at-a-time output.  The program cannot
keep up with continuous output at 2400 baud.

The ASCII bell character (^G) produces no visible or audible output.


Installation
------------

You will need an ACTION! cartridge to compile and run this program.  You
might also have to edit the source to select the options you require,
since there is no provision for changing these at runtime.  The
configuration of the 850 interface (or similar R: interface) is controlled
by the following variables, which are declared and initialized at the
beginning of the main program module:
	speed, wsize, sbits, lf, iparity, oparity
See the 850 manual for tables of possible values and their meanings.  Note
that the value of 'speed' is 7 less than the tabulated value, e.g.,
	speed	baud rate
	  1	 300
	  2	 600
	  3	1200
	  5	2400
Alternatively, you can modify the calls of XIO_R in the procedure init_R;
these variables are not used elsewhere.  As it stands, VT52B supports a
1200 baud modem with 8-bit words, 2 stop bits and no parity.  

Many monitors will actually display 25 or 26 lines of text.  You can
take advantage of this by changing the following definitions at the
beginning of the module for the A: handler:
	NL	number of screen lines (normally 24)
	NB	number of skip lines in top border (normally 3)
	LL	*must* equal NL-1
	DLSize	*must* equal NB + 10*NL + 3
I have had good results with NL=25 and NB=2.  If you change NL, be sure to
change your termcap entry to agree.

I experimented with several background colors for the display.  I
eventually settled on a pale blue that seemed to be the best compromise
between contrast and flicker on my particular monitor.  See the procedure
init_A if you want to change this.

The emulator module contains a procedure load_R.  This is a machine-code
insert essentially similar to the code in the autorun file that downloads
the R: handlers from the 850.  I added it when I was having some trouble
with 850 (re)initialization.  I left it in because it makes the object
file somewhat smaller and easier to build.  If you have a different
interface unit or prefer a different handler, delete init_R and prepend
your handler to the object file in the usual way.

You must compile this program with lower case enabled.  You must also
compile it with space for the R: handlers reserved in low memory.  The
simplest way to do this is to load the handler you intend to use (by
executing the AUTORUN.SYS file, for example) before compiling VT52B.ACT.
You can then execute the compiled program directly or save it for later
execution using the DOS "L" command or equivalent.  If you deleted init_R,
you must prepend an appropriate R: handler.  Alternatively, you can set
the code origin to reserve space for your handler as described on page 144
of the ACTION!  manual.  You are in terminal emulation mode as soon as
execution begins.

Note: At least some versions of the 850 will not download the R: handlers
a second time until the power has been cycled on either the console
computer or the 850.  Thus if you load the handlers (e.g., with the
standard AUTORUN.SYS file), do something that destroys them (e.g., going
to DOS without a MEM.SAV file) and then attempt to reload them, you will
be left without warning with bad handlers.


Suggested Termcap Entry
-----------------------

Here's one that has worked for me:

dw|vt52|decvt52:\
  :cr=^M:do=^J:nl=^J:bl=^G:le=^H:bs:cd=\EJ:ce=\EK:\
  :cl=\EH\EJ:cm=\EY%+ %+ :co#80:li#24:nd=\EC:ta=^I:pt:sr=\EI:\
  :up=\EA:ku=\EA:kd=\EB:kr=\EC:kl=\ED:kb=^H:
a1|atari|atari vt52 emulator:\
  :am:pt:tc=vt52:
a2|atari+|vt52xl|atari vt52+ emulator:\
  :al=\EN:dl=\EO:im=:ei=:ic=\EL:dm=:ed=:dc=\EM:\
  :so=\EF:se=\EG:\
  :tc=atari:

Note that it can take some extra work to get your favorite program to
notice changes in the terminal type and capabilities.  If the program is
able to use the escape sequences, you should notice a definite difference
between vt52 and atari+ termcap entries.  Here's a script from our system
administrator that works for Emacs:

#! /bin/csh -f
#
#	vt52b-mode - setup terminal for vt52b mode.
#
stty dec new cr0
set noglob
setenv TERMCAP atari.termcap
setenv TERM atari+
eval `tset -s -I -Q`
set term = atari+
unset noglob

Replace "atari.termcap" with the name of the file containing a termcap
entry similar to the one above.
-----------------------------------------------------------------------------

Ed Satterthwaite
Arpa: ehs@src.DEC.COM
Uucp: {...}!decwrl!ehs

ehs@jumbo.UUCP (09/28/87)

--VT52B.ACT--cut here-------------------------
;*********************************
;*                               *
;* VT52B.ACT - a VT52+ emulator  *
;* written in ACTION(tm) by      *
;*                               *
;*     Ed Satterthwaite          *
;*     ehs@src.DEC.COM           *
;*     ...!decwrl!ehs            *
;*     Copyright 1987            *
;*                               *
;* derived with permission from  *
;*                               *
;*********************************
;*                               *
;* VT52A.ACT - a VT52+ emulator  *
;* written in ACTION(tm) by      *
;*                               *
;*     Michael R. M. Jenkin      *
;*     University of Toronto     *
;*     ...!utcsri!utai!jenkin    *
;*     copyright(c) 1985         *
;*                               *
;********************************* 
;*                               *
;* This program may be copied    *
;* and redistributed without     *
;* charge for noncommercial use. *
;* All commercial rights         *
;* reserved.                     *
;*                               *
;*********************************

MODULE
;A: handler, by Michael Jenkin
;   revised, by Ed Satterthwaite

DEFINE NB = "3",   ;# top blank lines
       NL = "24",  ;# lines on screen
       LL = "23",  ;indexed 0..LL
       DLSize = "246" ;NB + 10*NL + 3

DEFINE LDY = "$A0",
       RTS = "$60",
       JMP = "$4C"

CARD ARRAY ProgEnd(1)  ;compiler allocates this last

BYTE lmargin = $52, rmargin = $53,
     rowcrs  = $54, oldrow  = $5A,
     colcrs  = $55,
     oldchr  = $5D, inesc, need,
     needx, inv

CARD appmhi  = $0E,
     oldcol  = $5B,
     sdlst   = $230,
     memtop  = $2E5

BYTE ARRAY InvMask = [$00 $0F]

BYTE ARRAY chset = [
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0
  0 6 6 6 6 0 6 0
  0 10 10 10 0 0 0 0
  10 14 10 14 10 0 0 0
  4 14 8 14 2 14 4 0
  0 10 2 6 12 8 10 0
  0 14 2 6 6 2 14 0
  0 6 6 6 0 0 0 0
  0 6 12 8 8 12 6 0
  0 12 6 2 2 6 12 0
  0 10 4 14 4 10 0 0
  0 4 4 14 4 4 0 0
  0 0 0 0 0 6 6 12
  0 0 0 14 0 0 0 0
  0 0 0 0 0 6 6 0
  0 2 2 4 4 8 8 0
  0 14 10 10 10 10 14 0
  0 4 12 4 4 4 14 0
  0 14 2 2 14 8 14 0
  0 14 2 14 2 2 14 0
  0 10 10 10 14 2 2 0
  0 14 8 14 2 2 14 0
  0 14 8 14 10 10 14 0
  0 14 2 6 4 4 4 0
  0 14 10 14 10 10 14 0
  0 14 10 14 2 2 2 0
  0 0 6 6 0 6 6 0
  0 0 6 6 0 6 6 12
  0 2 6 12 12 6 2 0
  0 0 14 0 0 14 0 0
  0 8 12 6 6 12 8 0
  0 4 10 2 4 0 4 0
  14 10 10 14 8 8 14 0
  0 4 14 10 10 14 10 0
  0 12 10 12 10 10 12 0
  0 14 10 8 8 10 14 0
  0 12 10 10 10 10 12 0
  0 14 8 12 8 8 14 0
  0 14 8 12 8 8 8 0
  0 14 8 8 10 10 14 0
  0 10 10 14 10 10 10 0
  0 14 4 4 4 4 14 0
  0 2 2 2 2 10 14 0
  0 10 10 12 12 10 10 0
  0 8 8 8 8 8 14 0
  0 10 14 14 10 10 10 0
  0 12 10 10 10 10 10 0
  0 14 10 10 10 10 14 0
  0 14 10 14 8 8 8 0
  0 14 10 10 10 10 14 2
  0 14 10 14 12 10 10 0
  0 14 8 14 2 2 14 0
  0 14 4 4 4 4 4 0
  0 10 10 10 10 10 14 0
  0 10 10 10 10 10 4 0
  0 10 10 10 14 14 10 0
  0 10 10 4 4 10 10 0
  0 10 10 4 4 4 4 0
  0 14 2 4 4 8 14 0
  0 14 8 8 8 8 14 0
  0 8 8 4 4 2 2 0
  0 14 2 2 2 2 14 0
  0 4 4 10 0 0 0 0
  0 0 0 0 0 0 15 0
  0 4 6 2 0 0 0 0
  0 0 14 2 14 10 14 0
  0 8 8 14 10 10 14 0
  0 0 0 14 8 8 14 0
  0 2 2 14 10 10 14 0
  0 0 14 10 14 8 14 0
  0 0 14 8 12 8 8 0
  0 0 14 10 10 14 2 14
  0 8 8 14 10 10 10 0
  0 6 0 6 6 6 6 0
  0 6 0 6 6 6 6 12
  0 8 8 10 14 10 10 0
  0 12 4 4 4 4 14 0
  0 0 10 14 14 10 10 0
  0 0 12 10 10 10 10 0
  0 0 14 10 10 10 14 0
  0 0 14 10 10 14 8 8
  0 0 14 10 10 14 2 2
  0 0 14 10 8 8 8 0
  0 0 14 8 14 2 14 0
  0 4 14 4 4 4 4 0
  0 0 10 10 10 10 14 0
  0 0 10 10 10 10 4 0
  0 0 10 10 14 14 10 0
  0 0 10 14 4 14 10 0
  0 0 10 10 10 14 2 14
  0 0 14 2 4 8 14 0
  2 4 4 8 4 4 2 0
  6 6 6 0 0 6 6 6
  8 4 4 2 4 4 8 0
  0 10 5 0 0 0 0 0
  0 0 0 0 0 0 0 0 
]

CARD ARRAY
  DBase(NL), ;display line bases
  DLBase(2), ;display list bases
  LMaps(2)   ;line map bases

BYTE ARRAY   ;storage for line maps
  LMapA(NL),
  LMapB(NL)

BYTE ARRAY
  LMap      ;logical line -> DBase index

BYTE dl     ;current display list and line map selection

CARD FUNC DAlloc(CARD size, mask)
  CARD base
  base = memtop - size
  IF (base & mask) # ((memtop-1) & mask) THEN
    base = (memtop & mask) - size
  FI
  memtop = base
RETURN (base)

PROC InitBitMap()  ;build GR.8 line chunks
  BYTE i
  CARD base
  FOR i = 0 TO LL DO
    base = DAlloc(320, $F000)
    DBase(i) = base
    IF base >= appmhi THEN
      Zero(base, 320)
    FI
  OD
RETURN

PROC InitDL()     ;build display lists
  BYTE i, j
  CARD b, base
  BYTE ARRAY d
  LMaps(0) = LMapA
  LMaps(1) = LMapB
  LMap = LMaps(0)
  b = DAlloc(DLSize, $FC00)
  d = b
  IF b >= appmhi THEN
    FOR i = 1 TO NB DO
      d(0) = $70   ; blank skip
      d ==+ 1
    OD
    FOR i = 0 TO LL DO
      base = DBase(i)
      d(0) = $4F     ; mode line + LMS, Gr. 8
      d(1) = base & $FF
      d(2) = base RSH 8
      FOR j = 3 TO 9 DO
        d(j) = $0F   ; mode line, Gr. 8
      OD
      LMap(i) = i
      d ==+ 10
    OD
    d(0) = $41
    d(1) = b & $FF
    d(2) = b RSH 8
  FI
  DLBase(0) = b
  b = DAlloc(DLSize, $FC00)
  d = b
  IF b >= appmhi THEN
    MoveBlock(b, DLBase(0), DLSize)
    d(DLSize-2) = b & $FF
    d(DLSize-1) = b RSH 8
  FI
  DLBase(1) = b
  dl = 0
  sdlst = DLBase(0)
RETURN

PROC UpdateDL(BYTE del, ins)
  ; move line from row del to row ins
  ; and blank it
  BYTE i, j
  CARD old, new
  BYTE ARRAY oldLMap
  CARD POINTER s, d
  BYTE m
  CARD p
  old = DLBase(dl)
  oldLMap = LMap
  dl ==! 1
  new = DLBase(dl)
  LMap = LMaps(dl)
  s = old + NB + 1  ;skip blank lines
  d = new + NB + 1  ;and first mode byte
  m = oldLMap(del)
  j = 0
  FOR i = 0 TO LL DO
    IF j = del THEN
      s ==+ 10
      j ==+ 1
    FI
    IF i = ins THEN
      p = DBase(m)
      d^ = p
      LMap(i) = m
    ELSE
      d^ = s^      ;copy addr for LMS
      LMap(i) = oldLMap(j)
      s ==+ 10
      j ==+ 1
    FI
    d ==+ 10
  OD
  Zero(p, 320)
  sdlst = new    ;next VBI install dl
RETURN

PROC Achr(BYTE cx, cy, cc)
  BYTE POINTER sb, db
  BYTE i, mask
  sb = cc & $7F
  sb = (sb LSH 3) + chset
  db = (cx RSH 1) + DBase(LMap(cy))
  mask = InvMask(inv)
  IF cx & 1 THEN
    FOR i = 0 TO 7 DO
      db^ = ((db^ & $F0) % sb^)!mask
      sb ==+ 1
      db ==+ 40
    OD
  ELSE
    mask ==LSH 4
    FOR i = 0 TO 7 DO
      db^ = ((db^ & $0F) % (sb^ LSH 4))!mask
      sb ==+ 1
      db ==+ 40
    OD
  FI
RETURN

PROC Acurse(BYTE cx, cy); invert char
  BYTE POINTER db
  BYTE i, mask
  IF (cx & 1) THEN
    mask = $0F
  ELSE
    mask = $F0
  FI
  db = (cx RSH 1) + DBase(LMap(cy))
  FOR i = 0 TO 7 DO
    db^ ==! mask
    db ==+ 40
  OD
RETURN

PROC Ains(BYTE cx, cy)
  CARD lb
  BYTE POINTER db
  BYTE i, bx, b
  BYTE cout, t
  bx = cx RSH 1
  lb = DBase(LMap(cy))
  FOR i = 0 TO 7 DO
    b = bx
    db = lb + b
    IF cx & 1 THEN
      cout = db^ LSH 4
      db^ ==& $F0
      db ==+ 1
      b ==+ 1
    ELSE
      cout = 0
    FI
    WHILE b < 40 DO
      t = db^
      db^ = (t RSH 4) % cout
      cout = t LSH 4
      db ==+ 1
      b ==+ 1
    OD
    lb ==+ 40
  OD
RETURN
 
PROC Adel(BYTE cx, cy)
  CARD lb
  BYTE POINTER db
  BYTE i, bx, b
  BYTE cout, t
  bx = cx RSH 1
  lb = DBase(LMap(cy))
  FOR i = 0 TO 7 DO
    b = bx
    db = lb + 39
    cout = 0
    IF cx & 1 THEN
      b ==+ 1
    FI
    WHILE b < 40 DO
      t = db^
      db^ = (t LSH 4) % cout
      cout = t RSH 4
      db ==- 1
      b ==+ 1
    OD
    IF cx & 1 THEN
      db^ = (db^ & $F0) % cout
    FI
    lb ==+ 40
  OD
RETURN
 
PROC Ascroll() ; update cursor
  IF colcrs > rmargin THEN
    colcrs = lmargin
    rowcrs ==+ 1
  FI
  IF rowcrs > LL THEN
    UpdateDL(0, LL) 
    rowcrs = LL
  FI
  Acurse(colcrs,rowcrs)
RETURN

PROC Aesc(BYTE char) ; escape sequence
  BYTE ch
  BYTE i
  IF need = 2 THEN ; 1st ESC Y
    needx = char - $20
    need ==- 1
  ELSEIF need = 1 THEN ; 2nd ESC Y
    char ==- $20
    IF (needx <= LL) AND (char <= rmargin) THEN
      Acurse(colcrs,rowcrs)
      colcrs = char
      rowcrs = needx
      Acurse(colcrs,rowcrs)
    FI
    need = 0
  ELSEIF char = 'A THEN ; cursor up
    IF rowcrs > 0 THEN
      Acurse(colcrs,rowcrs)
      rowcrs ==- 1
      Acurse(colcrs,rowcrs)
    FI
  ELSEIF char = 'B THEN ; cursor down
    IF rowcrs < LL THEN
      Acurse(colcrs,rowcrs)
      rowcrs ==+ 1
      Acurse(colcrs,rowcrs)
    FI
  ELSEIF char = 'C THEN ; cursor right
    IF colcrs < rmargin THEN
      Acurse(colcrs,rowcrs)
      colcrs ==+ 1
      Acurse(colcrs,rowcrs)
    FI
  ELSEIF char = 'D THEN ; cursor left
    IF colcrs > 0 THEN
      Acurse(colcrs,rowcrs)
      colcrs ==- 1
      Acurse(colcrs,rowcrs)
    FI
  ELSEIF char = 'F THEN ; inverse on
    inv = 1
  ELSEIF char = 'G THEN ; inverse off
    inv = 0
  ELSEIF char = 'H THEN ; home
    Acurse(colcrs,rowcrs)
    colcrs = 0
    rowcrs = 0
    Acurse(colcrs,rowcrs)
  ELSEIF char = 'I THEN ; reverse lf
    Acurse(colcrs,rowcrs)
    IF rowcrs > 0 THEN
      rowcrs ==- 1
    ELSE
      UpdateDL(LL, 0)
    FI
    Acurse(colcrs,rowcrs)
  ELSEIF char = 'J THEN ; erase to EOS
    FOR ch = colcrs TO 79 DO
      Achr(ch,rowcrs,' )
    OD
    i = rowcrs + 1
    WHILE i <= LL DO
      Zero(DBase(LMap(i)),320)
      i ==+ 1
    OD
    Acurse(colcrs,rowcrs)
  ELSEIF char = 'K THEN ; erase to EOL
    FOR ch = colcrs TO 79 DO
      Achr(ch,rowcrs,' )
    OD
    Acurse(colcrs,rowcrs)
  ELSEIF char = 'L THEN ; insert space
    Acurse(colcrs, rowcrs)
    Ains(colcrs, rowcrs)
    Acurse(colcrs,rowcrs)
  ELSEIF char = 'M THEN ; delete char
    Adel(colcrs, rowcrs)
    Acurse(colcrs,rowcrs)
  ELSEIF char = 'N THEN ; insert line
    Acurse(colcrs, rowcrs)
    UpdateDL(LL, rowcrs)
    colcrs = 0
    Acurse(colcrs,rowcrs)
  ELSEIF char = 'O THEN ; delete line
    UpdateDL(rowcrs, LL)
    colcrs = 0
    Acurse(colcrs,rowcrs)
  ELSEIF char = 'Y THEN ; cursor addr
    need = 2
  FI
  IF need = 0 THEN
    inesc = 0
  FI
RETURN

PROC Aopen()
  CARD savetop
  savetop = memtop
  InitBitMap()
  InitDL()
  IF memtop < appmhi THEN
    memtop = savetop
    [LDY 147 RTS]    ; fail
  FI
  inesc = 0
  inv = 0
  need = 0
  lmargin = 0
  rmargin = 79
  rowcrs = 0
  colcrs = 0
  Acurse(colcrs,rowcrs)
  [LDY 1 RTS]

PROC Aclose()
  [LDY 1 RTS]

PROC Aput(BYTE areg)
  IF inesc =1 THEN; escape sequence
    Aesc(areg)
  ELSEIF areg = $1B THEN ; ESC
    inesc = 1
  ELSEIF areg = $9B THEN ; EOL
    Acurse(colcrs,rowcrs)
    colcrs = 0
    Ascroll()
  ELSEIF areg = $0A THEN ; lf
    Acurse(colcrs,rowcrs)
    rowcrs ==+ 1
    Ascroll()
  ELSEIF areg = $08 THEN ; BS
    IF colcrs > 0 THEN
      Acurse(colcrs,rowcrs)
      colcrs ==- 1
      Ascroll()
    FI
  ELSEIF areg = $07 THEN ; bell
    ; do nothing
  ELSEIF areg = $09 THEN ; TAB
    Acurse(colcrs,rowcrs)
    colcrs = (colcrs + 8) & $F8
    Ascroll()
  ELSE
    Achr(colcrs,rowcrs,areg)
    colcrs ==+ 1
    Ascroll()
  FI
  [LDY 1 RTS]

PROC Anofunc()
  [RTS]

PROC Adummy()
  [LDY 1 RTS]

PROC Ahandler()
  BYTE ARRAY hatabs = $031A
  BYTE pos, found
  ;do not change the following 3 lines
  CARD ARRAY atab(6)
  BYTE Jmp = [JMP]
  CARD init
  ; define device entry points
  atab(0) = Aopen - 1      ;OPEN
  atab(1) = Aclose - 1     ;CLOSE
  atab(2) = Anofunc - 1    ;READ
  atab(3) = Aput - 1       ;WRITE
  atab(4) = Adummy - 1     ;STATUS
  atab(5) = Anofunc - 1    ;SPECIAL
  init = Adummy            ;INIT
  ; find entry in hatabs
  found = 0
  pos = 0
  WHILE (pos < 34) AND (found = 0)
  DO
    IF hatabs(pos) = 0 THEN
      found = 1
    ELSE
      pos ==+ 3
    FI
  OD
  IF found = 0 THEN
    PrintE("*** A: too many devices")
  ELSE
    hatabs(pos) = 'A
    hatabs(pos + 1) = atab & 255
    hatabs(pos + 2) = atab RSH 8
  FI
RETURN

;****************************
;* MAIN PROGRAM
;****************************

MODULE

BYTE
  ch = $02FC,
  shflok = $02BE,
  speed = [3],
  wsize = [0],
  sbits = [0],
  lf = [0],
  iparity = [0],
  oparity = [0]

CARD
  bcount = $02EB

; iocb 3 definitions
BYTE iocb3cmd=$372 ; cmd byte
CARD iocb3buf=$374,  ; buffer address
     iocb3len=$378   ; buffer length
BYTE iocb3aux1=$37A, ; aux1 byte
     iocb3aux2=$37B  ; aux2 byte

DEFINE BUFLEN = "1024"
BYTE ARRAY BUFFER(BUFLEN)

PROC CIO=$E456(BYTE areg, xreg)

PROC XIO_R(BYTE cmd, aux1, aux2)
 ;because library XIO seemed flakey
  iocb3cmd = cmd
  iocb3buf = 0
  iocb3len = 0
  iocb3aux1 = aux1
  iocb3aux2 = aux2
  CIO(0,$30)
RETURN

PROC load_R(); load R: handlers
 [$A9 $50  $8D $00 $03
  $A9 $01  $8D $01 $03
  $A9 $3F  $8D $02 $03
  $A9 $40  $8D $03 $03
  $A9 $05  $8D $06 $03  $8D $05 $03
  $A9 $00  $8D $04 $03  $8D $09 $03
           $8D $0A $03  $8D $0B $03
  $A9 $0C  $8D $08 $03
  $20 $59 $E4
  $10 $01  $60
  $A2 $0B
  $BD $00 $05  $9D $00 $03
  $CA
  $10 $F7
  $20 $59 $E4
  $30 $06
  $20 $06 $05  $6C $0C $00
  $60
 ]

PROC init_R(); set options for R:
  Close(3)
  Open(3,"R1:",13,0)
  XIO_R(34,192+48,0)
  XIO_R(38,lf*64+oparity+4*iparity,0)
  XIO_R(36,speed+7+wsize*16+128*sbits,0)
  iocb3cmd=40 ; start concurrent I/O
  iocb3buf=BUFFER
  iocb3len=BUFLEN
  CIO(0,$30)
  bcount = 0
RETURN

PROC init_A(); set up A: device
  Ahandler() ; install A: handler
  Close(2)
  Open(2,"A:",8,0)
  SetColor(1,0,0)
; SetColor(2,12,15)  ;white field
; SetColor(2,3,6)    ;amber field
  SetColor(2,10,12)  ;lt. blue field
RETURN

PROC intro()
  Close(7)
  Open(7,"K:",4,0)
  shflok = 0
  init_R()
  init_A()
RETURN

BYTE FUNC remote(); remote char?
  iocb3cmd=13 ; get R: status       
  CIO(0,$30) ; *** call CIO ***
  IF bcount = 0 THEN
    RETURN(0)
  FI
RETURN(1)

BYTE FUNC local() ; local char?
RETURN($FF-ch)

PROC do_local(); process local
  BYTE char
  char = GetD(7)
  IF char = 127 THEN     ;tab 
    char = 9
  ELSEIF char = 125 THEN ;left curl
    char = 123
  ELSEIF char = 157 THEN ;right curl
    char = 125
  ELSEIF char = 255 THEN ;right curl
    char = 125
  ELSEIF char = 156 THEN  ;tilde
    char = 126
  ELSEIF char = 254 THEN  ;tilde
    char = 126
  ELSEIF char = 126 THEN ; delete
    char = 127
  FI
  PutD(3,char)
RETURN

PROC main()
  BYTE char
  load_R()
  appmhi = ProgEnd
  intro()
  DO
    IF remote() THEN
      char = GetD(3)
      PutD(2,char)
    ELSEIF local() THEN
      do_local()
    FI
  OD
RETURN