-- SpecialTerminalImpl.mesa
-- Derived from SimpleTTY.mesa last edited by Forrest January 6, 1981  7:28 PM 
-- last edited by Levin on November 12, 1982 5:26 pm
-- last edited by McGregor on April 22, 1982 4:23 pm


DIRECTORY
  Ascii USING [
    BS, ControlA, ControlR, ControlQ, ControlV, ControlW, ControlX, CR, DEL,
    ESC, FF, SP, TAB],
  BitBlt USING [AlignedBBTable, BBptr, BBTableSpace, BITBLT],
  Environment USING [BitAddress],
  Format USING [
    Date, Decimal, LongDecimal, LongNumber, LongOctal, LongSubStringItem,
    Number, Octal, SubString],
  Inline USING [BITAND],
  Process USING [DisableTimeout, Pause, SecondsToTicks, SetPriority, SetTimeout],
  Runtime USING [GetTableBase],
  SpecialSpace USING [
    MakeGlobalFrameResident, --MakeGlobalFrameSwappable,--
    MakeProcedureResident, MakeProcedureSwappable],
  SpecialTerminal USING [GetProc, PutProc],
  Streams USING [Destroy, Handle, NewStream, PutChar, Write],
  String USING [
    AppendChar, AppendLongNumber, AppendNumber,
    StringToLongNumber, StringToNumber, SubString],
  System USING [GetGreenwichMeanTime, gmtEpoch, GreenwichMeanTime],
  TerminalMultiplex USING [CurrentTerminal, PermitSwaps, PreventSwaps, SelectTerminal, Terminal],
  Time USING [Append, Packed, Unpack],
  TTY USING [DateFormat, Handle, LongSubString, NumberFormat],
  UserTerminal USING [
    Coordinate, cursor, GetBitBltTable, keyboard, mouse, screenHeight,
    screenWidth, SetMousePosition, SetState, SetBackground,
    WaitForScanLine],
  Volume USING [GetLabelString, maxNameLength, systemID];

SpecialTerminalImpl: MONITOR
  LOCKS m USING m: POINTER TO MONITORLOCK
  IMPORTS
    BitBlt, Format, Inline, Process, Runtime, SpecialSpace,
    Streams, String, System, TerminalMultiplex, Time, UserTerminal, Volume
  EXPORTS TTY, SpecialTerminal =

BEGIN OPEN Ascii, BitBlt;

screenLock: MONITORLOCK;

-- These variables are protected by "screenLock"
typescript: Streams.Handle;
typescriptEnabled: BOOLEAN ← FALSE;

noTrack: CARDINAL ← 0;

keyboardLock: MONITORLOCK;

-- These variables are protected by "keyboardLock"

state: {off, turningOn, on, turningOff} ← off;
stateChange: CONDITION ← [timeout: 0];


terminalLock: MONITORLOCK;

-- These variables are protected by "terminalLock"

originalTerminal: TerminalMultiplex.Terminal;
originalLockCount: CARDINAL;



keyboardWatcher, timePainter: PROCESS;

-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Exports to SpecialTerminal
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

InputTimeout: PUBLIC SIGNAL = CODE;

SetInputTimeout: PUBLIC PROC [ticks: CARDINAL] = {
  SetInputTimeoutInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    IF ticks = 0 THEN Process.DisableTimeout[@charactersAvailable]
    ELSE Process.SetTimeout[@charactersAvailable, ticks]};
  SetInputTimeoutInternal[@keyboardLock]};

TurnOn: PUBLIC PROC RETURNS [SpecialTerminal.GetProc, SpecialTerminal.PutProc] = {
  EnsureSpecialVirtualTerminalInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    OPEN TerminalMultiplex;
    terminal: TerminalMultiplex.Terminal;
    lockCount: CARDINAL;
    [terminal, lockCount] ← CurrentTerminal[];
    IF terminal ~= special THEN {
      originalTerminal ← terminal; originalLockCount ← lockCount;
      THROUGH [0..lockCount) DO PermitSwaps[] ENDLOOP;
      IF ~SelectTerminal[terminal: special, lock: TRUE] THEN ERROR}
    ELSE PreventSwaps[]
    };
  EnsureSpecialVirtualTerminalInternal[@terminalLock];
  RETURN [getChar, putChar]
  };

getChar: PROC RETURNS [CHAR] = {RETURN[GetChar[tty]]};
putChar: PROC [c: CHAR] = {PutChar[tty, c]};


TurnOff: PUBLIC PROC = {
  EnsureOriginalVirtualTerminalInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    OPEN TerminalMultiplex;
    PermitSwaps[];
    IF CurrentTerminal[].lockCount = 0 THEN {
      IF ~SelectTerminal[terminal: originalTerminal, lock: originalLockCount > 0] THEN ERROR;
      THROUGH [1..originalLockCount) DO PreventSwaps[] ENDLOOP}
      };
  EnsureOriginalVirtualTerminalInternal[@terminalLock];
  };

EnableCursorTracking: PUBLIC PROC = {
  EnableInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    IF (noTrack ← noTrack - 1) = 0 THEN doBlink ← TRUE};
  EnableInternal[@screenLock]};

DisableCursorTracking: PUBLIC PROC = {
  DisableInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    IF (noTrack ← noTrack + 1) ~= 0 THEN doBlink ← FALSE};
  DisableInternal[@screenLock]};

EnableTypescriptFile: PUBLIC PROC = {
  EnableTypescriptInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    IF typescriptEnabled THEN RETURN;
    typescript ← Streams.NewStream["Init.log"L, Streams.Write];
    typescriptEnabled ← TRUE};
  EnableTypescriptInternal[@screenLock]};

DisableTypescriptFile: PUBLIC PROC = {
  DisableTypescriptInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    IF ~typescriptEnabled THEN RETURN;
    Streams.Destroy[typescript];
    typescriptEnabled ← FALSE};
  DisableTypescriptInternal[@screenLock]};


-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Stuff for special clients
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

tty: PUBLIC TTY.Handle ← LOOPHOLE[123456B];  -- value is never used

TerminalOn: PUBLIC PROC RETURNS [BOOLEAN] = {
  TerminalOnInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] RETURNS [BOOLEAN] = INLINE {
    ENABLE UNWIND => NULL;
    WHILE state ~= off AND state ~= on DO WAIT stateChange ENDLOOP;
    RETURN [state = on]};
  RETURN[TerminalOnInternal[@keyboardLock]]
  };

TurnOnInternal: PUBLIC PROC [nameStripe: LONG STRING] = {
  TurnOnInternalA: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    ENABLE UNWIND => NULL;
    WHILE state ~= off DO WAIT stateChange ENDLOOP;
    state ← turningOn; BROADCAST stateChange};
  TurnOnInternalB: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    state ← on; BROADCAST stateChange};
  IF ~font.newStyle OR font.indexed OR ~(font.min IN [0C..177C])
      OR ~(font.max+1 IN [0C..177C]) THEN ERROR;
  TurnOnInternalA[@keyboardLock];
  SpecialSpace.MakeProcedureResident[ProcessKeyboard];
  SpecialSpace.MakeGlobalFrameResident[SpecialTerminalImpl];
  CDT ← FALSE;
  echo ← TRUE;
  in ← out ← 0;
  InitScreen[nameStripe];
  keyboardWatcher ← FORK ProcessKeyboard[];
  TurnOnInternalB[@keyboardLock];
  };

TurnOffInternal: PUBLIC PROC = {
  TurnOffInternalA: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    ENABLE UNWIND => NULL;
    WHILE state ~= on DO WAIT stateChange ENDLOOP;
    state ← turningOff; BROADCAST stateChange};
  TurnOffInternalB: ENTRY PROC [m: POINTER TO MONITORLOCK] = {
    state ← off; BROADCAST stateChange};
  TurnOffInternalA[@keyboardLock];
  SpecialSpace.MakeProcedureSwappable[ProcessKeyboard];
  -- Pilot doesn't do this right: SpecialSpace.MakeGlobalFrameSwappable[SpecialTTY];
  [] ← UserTerminal.SetState[disconnected];
  JOIN keyboardWatcher;
  JOIN timePainter;
  TurnOffInternalB[@keyboardLock];};


-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- FONT Definitions and variables
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
font: LONG POINTER TO MACHINE DEPENDENT RECORD [
  newStyle(0:0..0): BOOLEAN,
  indexed(0:1..1): BOOLEAN,
  fixed(0:2..2): BOOLEAN,
  kerned(0:3..3): BOOLEAN,
  pad(0:4..15): [0..7777B],
  min(1): CHARACTER,   -- limits of chars in font
  max(2): CHARACTER,   -- limits of chars in font
  maxwidth(3): CARDINAL,
  length(4): CARDINAL,
  ascent(5): CARDINAL,
  descent(6): CARDINAL,
  xoffset(7): CARDINAL,
  raster(8): CARDINAL,
  chars(9:0..63): SELECT OVERLAID * FROM
    hasBoundingBox => [
      boundingBox(9:0..63): RECORD [FontBBox, FontBBoy, FontBBdx, FontBBDy: INTEGER],
      BBBitmap(13): ARRAY [0..0) OF WORD],
    noBoundingBox => [bitmap(9): ARRAY [0..0) OF WORD],
    ENDCASE] = GetFont[];

bitmap: LONG POINTER = IF font.kerned THEN @font.BBBitmap ELSE @font.bitmap;
xInSegment: LONG POINTER TO ARRAY CHARACTER [0C..0C) OF CARDINAL =
  bitmap + font.raster*FontHeight[] - (font.min-0C);
height: INTEGER[0..LAST[INTEGER]] = FontHeight[];

CharWidth: PROC [char: CHARACTER] RETURNS [[0..LAST[INTEGER]]] =
  INLINE BEGIN
  IF ~(char IN [font.min..font.max]) THEN char ← font.max+1;
  RETURN[xInSegment[char+1] - xInSegment[char]]
  END;
  
FontHeight: PROC RETURNS [[0..LAST[INTEGER]]] = INLINE {RETURN[font.ascent+font.descent]};
  
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Keyboard Definitions and Constants
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
downUp: TYPE = {down, up};
keycount: CARDINAL = 80; -- must be 0 mod 16
Keyarray: TYPE = MACHINE DEPENDENT RECORD [SELECT OVERLAID * FROM
  b => [bits: PACKED ARRAY [0..keycount) OF downUp],
  wds => [wds: ARRAY [0..keycount/16) OF WORD],
  ENDCASE];

KeyItem: TYPE = RECORD [
  Letter: BOOLEAN, ShiftCode: CHARACTER [0C..177C], NormalCode: CHARACTER [0C..377C]];

-- Keyboard Info
ctrl:       CARDINAL = 52;
leftShift:  CARDINAL = 57;
shiftLock:  CARDINAL = 72;
rightShift: CARDINAL = 76;
spare3:     CARDINAL = 77;

KeyTable: ARRAY [16..keycount) OF KeyItem = [
  -- 	Index [0..15] mouse, etc
  -- 	Index [16..31]
  [FALSE,  45C,  65C],  -- %,5
  [FALSE,  44C,  64C],  -- $,4
  [FALSE, 176C,  66C],  -- ~,6
  [TRUE,  105C, 145C],  -- E
  [FALSE,  46C,  67C],  -- &,7
  [TRUE,  104C, 144C],  -- D
  [TRUE,  125C, 165C],  -- U
  [TRUE,  126C, 166C],  -- V
  [FALSE,  51C,  60C],  -- ),0
  [TRUE,  113C, 153C],  -- K
  [FALSE,  30C,  55C],  -- `,-
  [TRUE,  120C, 160C],  -- P
  [FALSE,  77C,  57C],  -- ?,/
  [FALSE, 174C, 134C],  -- |,\
  [FALSE,  12C,  12C],  -- LF
  [FALSE,  10C,  10C],  -- BS
  -- 	Index [32..47]
  [FALSE,  43C,  63C],  -- #,3
  [FALSE, 100C,  62C],  -- @,2
  [TRUE,  127C, 167C],  -- W
  [TRUE,  121C, 161C],  -- Q
  [TRUE,  123C, 163C],  -- S
  [TRUE,  101C, 141C],  -- A
  [FALSE,  50C,  71C],  -- (,9
  [TRUE,  111C, 151C],  -- I
  [TRUE,  130C, 170C],  -- X
  [TRUE,  117C, 157C],  -- O
  [TRUE,  114C, 154C],  -- L
  [FALSE,  74C,  54C],  -- <,,
  [FALSE,  42C,  47C],  -- ",'
  [FALSE, 175C, 135C],  --},]
  [FALSE,   0C,   0C],  -- SPARE2
  [FALSE,   0C,   0C],  -- SPARE1
  -- 	Index [48..63]
  [FALSE,  41C,  61C],  -- !,1
  [FALSE,  33C,  33C],  -- ESCAPE
  [FALSE,  11C,  11C],  -- TAB
  [TRUE,  106C, 146C],  -- F
  [FALSE,    0C,  0C],  -- CONTROL
  [TRUE,  103C, 143C],  -- C
  [TRUE,  112C, 152C],  -- J
  [TRUE,  102C, 142C],  -- B
  [TRUE,  132C, 172C],  -- Z
  [FALSE,    0C,  0C],  -- SHIFT
  [FALSE,  76C,  56C],  -- >,.
  [FALSE,  72C,  73C],  -- :,;
  [FALSE,  15C,  15C],  -- CR
  [FALSE, 136C, 137C],  -- ↑,←
  [FALSE, 177C, 177C],  -- DEL
  [FALSE,    0C,  0C],  -- NOT USED (FL3)
  -- 	Index [64..79]
  [TRUE,  122C, 162C],  -- R
  [TRUE,  124C, 164C],  -- T
  [TRUE,  107C, 147C],  -- G
  [TRUE,  131C, 171C],  -- Y
  [TRUE,  110C, 150C],  -- H
  [FALSE,  52C,  70C],  -- *,8
  [TRUE,  116C, 156C],  -- N
  [TRUE,  115C, 155C],  -- M
  [FALSE,   0C,   0C],  -- LOCK
  [FALSE,  40C,  40C],  -- SPACE
  [FALSE, 173C, 133C],  -- {,[
  [FALSE,  53C,  75C],  -- +,=
  [FALSE,   0C,   0C],  -- Shift
  [FALSE,   0C,   0C],  -- Spare3
  [FALSE,   0C,   0C],  -- not user (FR4)
  [FALSE,   0C,   0C]];  -- not user (FR5)

-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Keyboard Implementation
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

-- These variables are protected by "keyboardLock"
CDT: BOOLEAN;
charactersAvailable: CONDITION;
echo: BOOLEAN;

in, out: CARDINAL;
buffer: PACKED ARRAY [0..50) OF CHARACTER;

-- EXTERNAL PROCS
CtrlChar: PROC [c: CHARACTER] RETURNS [CHARACTER] = INLINE {RETURN[LOOPHOLE[Inline.BITAND[c, 37B]]]};

-- ENTRY PROCS
ProcessKeyboard: PROC =
  BEGIN
  old, new: Keyarray;
  kp: LONG POINTER TO Keyarray = LOOPHOLE[UserTerminal.keyboard];
  blinkCount: CARDINAL ← 33;
  GoAway: ENTRY PROC [m: POINTER TO MONITORLOCK] RETURNS [BOOLEAN] = INLINE {
    RETURN[state = turningOff]};
  Process.SetPriority[6];
  new ← kp↑;
  UNTIL GoAway[@keyboardLock] DO
    Brdcst: ENTRY PROC [m: POINTER TO MONITORLOCK] =
      INLINE {BROADCAST charactersAvailable};
    charsSeen: BOOLEAN ← FALSE;
    old ← new;
    UserTerminal.WaitForScanLine[0];
    new ← kp↑;
    TrackCursor[@screenLock];
    IF (blinkCount←blinkCount-1) = 0 THEN {BlinkCursor[]; blinkCount ← 34};
    FOR i: CARDINAL IN [1..keycount/16) DO
      IF old.wds[i]#new.wds[i] THEN
        FOR j: CARDINAL IN [i*16..(i+1)*16) DO
          char: CHARACTER;
          entry: KeyItem;
          IF new.bits[j]=up OR old.bits[j]=down THEN LOOP;
          IF (char ← (entry←KeyTable[j]).NormalCode)#0C THEN
            BEGIN
            SELECT TRUE FROM
              new.bits[ctrl]=down =>
                IF char=177C THEN {CDT ← TRUE; LOOP} ELSE char ← CtrlChar[char];
              new.bits[leftShift]=down, new.bits[rightShift]=down =>
                char ← entry.ShiftCode;
              new.bits[shiftLock]=down AND entry.Letter =>
                char ← entry.ShiftCode;
              ENDCASE;
            StuffBuffer[char, @keyboardLock];
            charsSeen ← TRUE
            END;
          ENDLOOP
      ENDLOOP;
    IF charsSeen THEN Brdcst[@keyboardLock];
    ENDLOOP
  END;

TrackCursor:  ENTRY PROC [m: POINTER TO MONITORLOCK] =
  INLINE BEGIN
  mouse: UserTerminal.Coordinate ← UserTerminal.mouse↑;
  IF noTrack > 0 THEN RETURN;
  mouse.x ← MIN[MAX[0, mouse.x], UserTerminal.screenWidth];
  mouse.y ← MIN[MAX[0, mouse.y], UserTerminal.screenHeight];
  UserTerminal.cursor↑ ← mouse;
  UserTerminal.SetMousePosition[mouse]
  END;

StuffBuffer: ENTRY PROC [c: CHARACTER, m: POINTER TO MONITORLOCK] =
  INLINE BEGIN
  newin: CARDINAL;
  IF (newin←in+1) = LENGTH[buffer] THEN newin ← 0;
  IF newin#out THEN {buffer[in] ← c; in ← newin};
  END;

-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- DISPLAY
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

-- These variables are protected by "screenLock"

BBTable: BBTableSpace;
bbPtr: BBptr = AlignedBBTable[@BBTable];
charPos, line, nCharPos, nLines: CARDINAL;
usableHeight, usableWidth: CARDINAL;
firstLine, thisLine: Environment.BitAddress;
bitsPerTextLine: CARDINAL;  -- = screenWidth*font.height
doBlink: BOOLEAN ← FALSE;
nameStripeOrg: Environment.BitAddress;
nameStripeWidth: CARDINAL;
lastTime: System.GreenwichMeanTime;

GetBitAddress: PROC [p: LONG POINTER, o: LONG CARDINAL]
  RETURNS [Environment.BitAddress] =
  {RETURN[[p+o/16, 0, o MOD 16]]};

BlinkCursor: PROC =
  BEGIN
  BlinkCursorEntry: ENTRY PROC [m: POINTER TO MONITORLOCK] =
    BEGIN
    blinker: CARDINAL ← 60000B;
    IF doBlink THEN
      BEGIN
      bbPtr.src ← [@blinker, 0, 0];
      bbPtr.srcDesc ← [gray[[0, 0, 0, 0]]];
      bbPtr.flags ← [gray: TRUE, dstFunc: xor];
      BITBLT[bbPtr];
      bbPtr.srcDesc ← [srcBpl[font.raster*16]];
      bbPtr.flags ← [];
      END;
    END;
  IF doBlink THEN BlinkCursorEntry[@screenLock];
  END;

Backup: INTERNAL PROC =
  BEGIN
  t: CARDINAL = bbPtr.dst.bit+16-font.maxwidth;
  IF charPos=0 THEN RETURN;
  charPos ← charPos - 1;
  bbPtr.dst.word ← bbPtr.dst.word + t/16 - 1;
  bbPtr.dst.bit ← t MOD 16;
  END;

InitBitmap: PROC =
  BEGIN
  zero: CARDINAL ← 0;
  bbPtr↑ ← UserTerminal.GetBitBltTable[];
  -- bbPtr.dst, bbPtr.height, and bbPtr.width are set up
  bbPtr.src ← [@zero, 0, 0];
  bbPtr.srcDesc ← [gray[[0, 0, 0, 0]]];
  bbPtr.flags ← [gray: TRUE];
  BITBLT[bbPtr];
  END;

InitScreen: PROC [nameStripe: LONG STRING] =
  BEGIN
  borderWidth: CARDINAL = 16;
  boxWidth: CARDINAL = 1;
  marginWidth: CARDINAL = 8;
  org: Environment.BitAddress;
  InitBitmap: PROC = {
    zero: CARDINAL ← 0;
    -- bbPtr.dst, bbPtr.height, and bbPtr.width are set up
    bbPtr.src ← [@zero, 0, 0];
    bbPtr.srcDesc ← [gray[[0, 0, 0, 0]]];
    bbPtr.flags ← [gray: TRUE];
    BITBLT[bbPtr]};
  MakeBorder: PROC [w: CARDINAL] = {
    stipple: ARRAY [0..3] OF CARDINAL ← [104210B,104210B,021042B,021042B];
    -- bbPtr.dstBpl never changes
    bbPtr.src ← [@stipple, 0, 0];
    bbPtr.srcDesc ← [gray[[yOffset: 0, widthMinusOne: 0, heightMinusOne: 3]]];
    bbPtr.flags ← [gray: TRUE];
    -- top stripe
    bbPtr.dst ← org;
    bbPtr.width ← usableWidth;
    bbPtr.height ← w;
    BITBLT[bbPtr];
    -- bottom stripe
    bbPtr.dst ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*(usableHeight-w)];
    BITBLT[bbPtr];
    -- left side
    bbPtr.dst ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*w];
    bbPtr.width ← w;
    bbPtr.height ← usableHeight-w-w;
    BITBLT[bbPtr];
    -- right side
    bbPtr.dst ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*w+usableWidth-w];
    BITBLT[bbPtr];
    org ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*w+w];
    usableHeight ← usableHeight - w - w;
    usableWidth ← usableWidth - w - w;
    };
  MakeBox: PROC [w: CARDINAL] = {
    black: CARDINAL ← 177777B;
    bbPtr.src ← [@black, 0, 0];
    bbPtr.srcDesc ← [gray[[yOffset: 0, widthMinusOne: 0, heightMinusOne: w-1]]];
    bbPtr.flags ← [gray: TRUE];
    -- top stripe
    bbPtr.dst ← org;
    bbPtr.width ← usableWidth;
    bbPtr.height ← w;
    BITBLT[bbPtr];
    -- bottom stripe
    bbPtr.dst ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*(usableHeight-w)];
    BITBLT[bbPtr];
    -- left side
    bbPtr.dst ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*w];
    bbPtr.width ← w;
    bbPtr.height ← usableHeight-w-w;
    BITBLT[bbPtr];
    -- right side
    bbPtr.dst ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*w+usableWidth-w];
    BITBLT[bbPtr];
    org ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*w+w];
    usableHeight ← usableHeight - w - w;
    usableWidth ← usableWidth - w - w;
    };
  MakeNameStripe: PROC [nameStripe: LONG STRING] = {
    black: CARDINAL ← 177777B;
    volumeLabel: STRING ← [Volume.maxNameLength];
    Volume.GetLabelString[Volume.systemID, volumeLabel];
    nameStripeOrg ← org;
    nameStripeWidth ← usableWidth;
    -- paint version text
    bbPtr.dst ← GetBitAddress[org.word, org.bit+marginWidth];
    bbPtr.srcDesc ← [srcBpl[font.raster*16]];
    bbPtr.height ← height;
    bbPtr.width ← font.maxwidth;
    bbPtr.flags ← [];
    FOR i: CARDINAL IN [0..nameStripe.length) DO
      bbPtr.src ← GetBitAddress[bitmap, xInSegment[nameStripe[i]]];
      BITBLT[bbPtr];
      bbPtr.dst ← GetBitAddress[bbPtr.dst.word, bbPtr.dst.bit+font.maxwidth];
      ENDLOOP;
    -- paint volume name
    bbPtr.dst ← GetBitAddress[org.word,
                    org.bit+usableWidth-marginWidth-volumeLabel.length*font.maxwidth];
    FOR i: CARDINAL IN [0..volumeLabel.length) DO
      bbPtr.src ← GetBitAddress[bitmap, xInSegment[volumeLabel[i]]];
      BITBLT[bbPtr];
      bbPtr.dst ← GetBitAddress[bbPtr.dst.word, bbPtr.dst.bit+font.maxwidth];
      ENDLOOP;
    -- overlay with stripe
    bbPtr.src ← [@black, 0, 0];
    bbPtr.srcDesc ← [gray[[yOffset: 0, widthMinusOne: 0, heightMinusOne: 0]]];
    bbPtr.flags ← [gray: TRUE, dstFunc: xor];
    bbPtr.dst ← org;
    bbPtr.width ← usableWidth;
    bbPtr.height ← height;
    BITBLT[bbPtr];
    org ← GetBitAddress[org.word, org.bit+LONG[bbPtr.dstBpl]*height];
    usableHeight ← usableHeight - height;
    lastTime ← System.gmtEpoch;
    timePainter ← FORK MaintainTime[];
    };
  [] ← UserTerminal.SetBackground[white];
  [] ← UserTerminal.SetState[off];  -- allocate the bitmap, but don't display it yet.
  bbPtr↑ ← UserTerminal.GetBitBltTable[];
  org ← bbPtr.dst;
  usableHeight ← bbPtr.height;
  usableWidth ← bbPtr.width;
  InitBitmap[];
  MakeBorder[borderWidth];
  MakeBox[boxWidth];
  MakeNameStripe[nameStripe];
  -- now display the bitmap
  [] ← UserTerminal.SetState[on];
  usableHeight ← usableHeight - 2*marginWidth;
  usableWidth ← usableWidth - 2*marginWidth;
  bitsPerTextLine ← bbPtr.dstBpl*height;
  firstLine ← 
    GetBitAddress[org.word, org.bit+LONG[marginWidth]*bbPtr.dstBpl+marginWidth];
  nCharPos ← usableWidth/font.maxwidth;
  nLines ← usableHeight/height;
  InitPainting[];
  END;

MaintainTime: PROC = {
  GoAway: ENTRY PROC [m: POINTER TO MONITORLOCK] RETURNS [BOOLEAN] = INLINE {
    RETURN[state = turningOff]};
  BBTable: BBTableSpace;
  bbPtr: BBptr = AlignedBBTable[@BBTable];
  secondsOrg: Environment.BitAddress;
  date: STRING ← [25];
  black: CARDINAL ← 177777B;
  SetToBlacken: PROC = {
    bbPtr.src ← [@black, 0, 0];
    bbPtr.srcDesc ← [gray[[yOffset: 0, widthMinusOne: 0, heightMinusOne: 0]]];
    bbPtr.flags ← [gray: TRUE]};
  PaintTime: PROC = {
    current: System.GreenwichMeanTime ← System.GetGreenwichMeanTime[];
    IF lastTime/60 = current/60 THEN {
      seconds: [0..59] ← current MOD 60;
      SetToBlacken[];
      bbPtr.dst ← secondsOrg;
      bbPtr.width ← 2*font.maxwidth;
      bbPtr.height ← height;
      BITBLT[bbPtr];
      -- bbPtr.dst & bbPtr.height still OK
      bbPtr.srcDesc ← [srcBpl[font.raster*16]];
      bbPtr.width ← font.maxwidth;
      bbPtr.flags ← [srcFunc: complement];
      bbPtr.src ← GetBitAddress[bitmap, xInSegment['0+seconds/10]];
      BITBLT[bbPtr];
      bbPtr.dst ← GetBitAddress[bbPtr.dst.word, bbPtr.dst.bit+font.maxwidth];
      bbPtr.src ← GetBitAddress[bitmap, xInSegment['0+seconds MOD 10]];
      BITBLT[bbPtr]}
    ELSE {
      date.length ← 0;
      Time.Append[date, Time.Unpack[current]];
      SetToBlacken[];
      bbPtr.dst ← GetBitAddress[nameStripeOrg.word,
                                     nameStripeOrg.bit+(nameStripeWidth-date.length*font.maxwidth)/2];
      secondsOrg ← GetBitAddress[bbPtr.dst.word, bbPtr.dst.bit+(date.length-2)*font.maxwidth];
      bbPtr.width ← date.length*font.maxwidth;
      bbPtr.height ← height;
      BITBLT[bbPtr];
      -- bbPtr.dst & bbPtr.height still OK
      bbPtr.srcDesc ← [srcBpl[font.raster*16]];
      bbPtr.width ← font.maxwidth;
      bbPtr.flags ← [srcFunc: complement];
      FOR i: CARDINAL IN [0..date.length) DO
        bbPtr.src ← GetBitAddress[bitmap, xInSegment[date[i]]];
        BITBLT[bbPtr];
        bbPtr.dst ← GetBitAddress[bbPtr.dst.word, bbPtr.dst.bit+font.maxwidth];
        ENDLOOP};
    lastTime ← current};
  bbPtr↑ ← UserTerminal.GetBitBltTable[];
  UNTIL GoAway[@keyboardLock] DO
    PaintTime[];
    Process.Pause[Process.SecondsToTicks[1]];
    ENDLOOP};

ClearScreen: PROC =
  BEGIN
  zero: CARDINAL ← 0;
  bbPtr.dst ← firstLine;
  bbPtr.src ← [@zero, 0, 0];
  bbPtr.srcDesc ← [gray[[0, 0, 0, 0]]];
  bbPtr.flags ← [gray: TRUE];
  bbPtr.height ← usableHeight;
  bbPtr.width ← usableWidth;
  BITBLT[bbPtr];
  InitPainting[];
  END;

InitPainting: PROC =
  BEGIN
  -- set up standard arguments for character painting
  charPos ← 0; line ← 1;
  --bbPtr.dstBpl already set up
  --bbPtr.src set when proc called
  bbPtr.dst ← thisLine ← firstLine;
  bbPtr.srcDesc ← [srcBpl[font.raster*16]];
  bbPtr.height ← height;
  bbPtr.width ← font.maxwidth;
  bbPtr.flags ← [];
  END;

ClearThisChar: INTERNAL PROC =
  BEGIN
  zero: CARDINAL ← 0;
  bbPtr.src ← [@zero, 0, 0];
  bbPtr.srcDesc ← [gray[[0, 0, 0, 0]]];
  bbPtr.flags ← [gray: TRUE];
  BITBLT[bbPtr];
  bbPtr.srcDesc ← [srcBpl[font.raster*16]];
  bbPtr.flags ← [];
  END;

DisplayChar: INTERNAL PROC [c: CHARACTER] =
  BEGIN
  Newline: INTERNAL PROC =
    BEGIN
    IF line<nLines THEN
      {thisLine ← GetBitAddress[thisLine.word, thisLine.bit+bitsPerTextLine]; line ← line+1}
    ELSE
      BEGIN
      zero: CARDINAL ← 0;
      sBBTable: BBTableSpace;
      sbbPtr: BBptr = AlignedBBTable[@sBBTable];
      sbbPtr↑ ← [
        dst: firstLine, dstBpl: bbPtr.dstBpl,
        src: GetBitAddress[firstLine.word, firstLine.bit+bitsPerTextLine],
        srcDesc: [srcBpl[bbPtr.dstBpl]], flags: [direction: forward],
        width: nCharPos*font.maxwidth, height: height*(nLines-1)];
      BITBLT[sbbPtr];
      sbbPtr↑ ← [
        dst: thisLine, src: [@zero, 0, 0],
        dstBpl: bbPtr.dstBpl, srcDesc: [gray[[0, 0, 0, 0]]],
        width: nCharPos*font.maxwidth, height: height,
        flags: [gray: TRUE]];
      BITBLT[sbbPtr];
      END;
    bbPtr.dst ← thisLine;
    charPos ← 0;
    END;
  SELECT c FROM
    IN (SP..'~] =>
      BEGIN
      IF ~(c IN [font.min..font.max]) THEN c ← font.max+1;
      bbPtr.src ← GetBitAddress[bitmap, xInSegment[c]];
      BITBLT[bbPtr];
      END;
    SP => NULL;
    CR => {Newline[]; RETURN};
    BS => {Backup[]; ClearThisChar[]; RETURN};
    TAB => {UNTIL (charPos MOD 8)=0 DO DisplayChar[SP] ENDLOOP; RETURN};
    FF => {ClearScreen[]; RETURN};
    IN [0C..SP) => {DisplayChar['↑]; DisplayChar[c+('A-1C)]; RETURN};
    ENDCASE => RETURN;
  IF (charPos←charPos+1)>=nCharPos THEN Newline[]
  ELSE bbPtr.dst ← GetBitAddress[bbPtr.dst.word, bbPtr.dst.bit+font.maxwidth];
  END;
    
GetFont: PROC RETURNS [LONG POINTER] =
  BEGIN
  -- Note:  Even though it doesn't say so in its strike header, this is a fixed-width
  -- font.  Bitter experience has shown that not all Gacha10.strike font files are, in
  -- fact, fixed width.  You need to painstakingly examine the xInSegment table for
  -- the truth.
  Gacha10Strike: ARRAY [0..1142B) OF WORD = [
  --   0--  100000B, 000040B, 000176B, 000007B, 001136B, 000010B, 000004B, 000000B,
  --  10--  000052B, 000000B, 000001B, 000000B, 000000B, 000000B, 000000B, 000000B,
  --  20--  000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  --  30--  000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  --  40--  000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  --  50--  000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  --  60--  000000B, 000000B, 017000B, 000040B, 120243B, 106204B, 004010B, 040100B,
  --  70--  000000B, 000002B, 034040B, 160701B, 107616B, 037070B, 070000B, 000200B,
  -- 100--  010034B, 034041B, 140707B, 107637B, 016104B, 070162B, 022010B, 110434B,
  -- 110--  074161B, 160707B, 144221B, 021104B, 104761B, 141007B, 000000B, 000200B,
  -- 120--  000040B, 001600B, 020010B, 010401B, 140000B, 000000B, 000000B, 000000B,
  -- 130--  000000B, 000000B, 000060B, 103000B, 017000B, 000040B, 120245B, 052412B,
  -- 140--  004020B, 020520B, 000000B, 000002B, 042141B, 011042B, 104021B, 001104B,
  -- 150--  104000B, 000400B, 004042B, 042121B, 021044B, 044020B, 021104B, 020022B,
  -- 160--  042015B, 114442B, 042211B, 011041B, 004221B, 021104B, 104021B, 001001B,
  -- 170--  000000B, 000200B, 000040B, 002000B, 020000B, 000400B, 040000B, 000000B,
  -- 200--  000000B, 000400B, 000000B, 000000B, 000100B, 100400B, 017000B, 000040B,
  -- 210--  121765B, 054412B, 004020B, 020340B, 100000B, 000004B, 046241B, 010042B,
  -- 220--  104020B, 002104B, 104100B, 101000B, 002042B, 056121B, 021044B, 044020B,
  -- 230--  021104B, 020022B, 102015B, 114442B, 042211B, 011041B, 004221B, 025050B,
  -- 240--  050041B, 000401B, 002010B, 034260B, 160643B, 107415B, 026070B, 070440B,
  -- 250--  043313B, 007054B, 032260B, 161704B, 044221B, 021104B, 174100B, 100400B,
  -- 260--  017000B, 000040B, 000503B, 001004B, 000040B, 010520B, 100000B, 000004B,
  -- 270--  052040B, 020704B, 107436B, 002070B, 104100B, 102017B, 101004B, 052121B,
  -- 300--  161004B, 047436B, 020174B, 020023B, 102012B, 112442B, 042211B, 160601B,
  -- 310--  004212B, 025020B, 050101B, 000401B, 007020B, 002311B, 011144B, 042023B,
  -- 320--  031010B, 010500B, 042514B, 110462B, 046311B, 010404B, 044221B, 012104B,
  -- 330--  010100B, 100406B, 057000B, 000040B, 000501B, 101012B, 100040B, 010103B,
  -- 340--  160017B, 100010B, 052040B, 040044B, 104221B, 004104B, 074000B, 002000B,
  -- 350--  001010B, 052211B, 011004B, 044020B, 027104B, 020022B, 042012B, 112442B,
  -- 360--  074211B, 040101B, 004212B, 025020B, 020101B, 000201B, 012476B, 036211B,
  -- 370--  001047B, 142021B, 021010B, 010600B, 042510B, 110442B, 042200B, 140404B,
  -- 400--  042425B, 004104B, 020100B, 100411B, 117000B, 000000B, 003745B, 042321B,
  -- 410--  000040B, 010000B, 100000B, 000010B, 062040B, 100047B, 140221B, 004104B,
  -- 420--  004000B, 001017B, 102000B, 056371B, 011044B, 044020B, 021104B, 020422B,
  -- 430--  042012B, 111442B, 040211B, 021041B, 004212B, 012050B, 020201B, 000201B,
  -- 440--  002020B, 042211B, 001044B, 002021B, 021010B, 010500B, 042510B, 110442B,
  -- 450--  042200B, 020404B, 042425B, 004050B, 020600B, 100300B, 017000B, 000040B,
  -- 460--  001205B, 042521B, 000040B, 010000B, 101400B, 002020B, 042041B, 001040B,
  -- 470--  104221B, 010104B, 104101B, 100400B, 004010B, 040211B, 011044B, 044020B,
  -- 500--  021104B, 020422B, 022012B, 111442B, 040211B, 011041B, 004204B, 012104B,
  -- 510--  020401B, 000101B, 002010B, 042311B, 011144B, 042023B, 021010B, 010440B,
  -- 520--  042510B, 110462B, 046201B, 010444B, 141012B, 012050B, 040100B, 100400B,
  -- 530--  017000B, 000040B, 001203B, 104616B, 100040B, 010000B, 000400B, 002020B,
  -- 540--  034371B, 170700B, 103416B, 010070B, 070100B, 100200B, 010010B, 036211B,
  -- 550--  160707B, 107620B, 016104B, 070342B, 023710B, 110434B, 040161B, 010701B,
  -- 560--  003404B, 012104B, 020761B, 000101B, 002000B, 036260B, 160643B, 102015B,
  -- 570--  021010B, 010420B, 042510B, 107054B, 032200B, 160303B, 041012B, 021020B,
  -- 600--  174100B, 100400B, 000000B, 000000B, 000001B, 000000B, 000020B, 020000B,
  -- 610--  000400B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 100000B,
  -- 620--  000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  -- 630--  000040B, 000000B, 000000B, 000000B, 000001B, 000001B, 000000B, 000000B,
  -- 640--  000000B, 000001B, 000000B, 110000B, 000000B, 000040B, 002000B, 000000B,
  -- 650--  000000B, 000020B, 000100B, 100400B, 000000B, 000000B, 000000B, 000000B,
  -- 660--  000020B, 020000B, 001000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  -- 670--  000001B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  -- 700--  000000B, 000000B, 000030B, 000000B, 000000B, 000000B, 000001B, 000001B,
  -- 710--  000000B, 000000B, 000000B, 000016B, 000000B, 060000B, 000000B, 000040B,
  -- 720--  002000B, 000000B, 000000B, 000140B, 000100B, 100400B, 000000B, 000000B,
  -- 730--  000000B, 000000B, 000010B, 040000B, 000000B, 000000B, 000000B, 000000B,
  -- 740--  000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  -- 750--  000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  -- 760--  000001B, 140007B, 000000B, 000000B, 000000B, 000000B, 000000B, 000000B,
  -- 770--  000000B, 000000B, 000000B, 000000B, 000000B, 000000B, 000060B, 103000B,
  --1000--  012602B, 000000B, 000007B, 000016B, 000025B, 000034B, 000043B, 000052B,
  --1010--  000061B, 000070B, 000077B, 000106B, 000115B, 000124B, 000133B, 000142B,
  --1020--  000151B, 000160B, 000167B, 000176B, 000205B, 000214B, 000223B, 000232B,
  --1030--  000241B, 000250B, 000257B, 000266B, 000275B, 000304B, 000313B, 000322B,
  --1040--  000331B, 000340B, 000347B, 000356B, 000365B, 000374B, 000403B, 000412B,
  --1050--  000421B, 000430B, 000437B, 000446B, 000455B, 000464B, 000473B, 000502B,
  --1060--  000511B, 000520B, 000527B, 000536B, 000545B, 000554B, 000563B, 000572B,
  --1070--  000601B, 000610B, 000617B, 000626B, 000635B, 000644B, 000653B, 000662B,
  --1100--  000671B, 000700B, 000700B, 000707B, 000716B, 000725B, 000734B, 000743B,
  --1110--  000752B, 000761B, 000770B, 000777B, 001006B, 001015B, 001024B, 001033B,
  --1120--  001042B, 001051B, 001060B, 001067B, 001076B, 001105B, 001114B, 001123B,
  --1130--  001132B, 001141B, 001150B, 001157B, 001166B, 001175B, 001204B, 001213B,
  --1140--  001222B, 001230B];
  p: LONG POINTER TO ARRAY [0..1142B) OF WORD ← Runtime.GetTableBase[LOOPHOLE[SpecialTerminalImpl]];
  DO
    IF p[0] = Gacha10Strike[0] AND p↑ = Gacha10Strike THEN RETURN[p];
    p ← p+1;
    ENDLOOP;
  END;

  --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  -- TTY Implementation
  --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Create: PUBLIC PROC [STRING] RETURNS [TTY.Handle] = {ERROR};

Destroy: PUBLIC PROC [TTY.Handle] = {ERROR};

BackingStream: PUBLIC PROC [TTY.Handle] RETURNS [never: UNSPECIFIED] = {ERROR};

UserAbort: PUBLIC PROC RETURNS [BOOLEAN] = {RETURN[CDT]};

ResetUserAbort: PUBLIC PROC =
  BEGIN
  CDResetEntry: ENTRY PROC [m: POINTER TO MONITORLOCK] = INLINE {CDT ← FALSE};
  CDResetEntry[@keyboardLock]
  END;

-- input procedures

CharsAvailable: PUBLIC PROC [h: TTY.Handle] RETURNS [CARDINAL] =
  BEGIN
  P: ENTRY PROC [m: POINTER TO MONITORLOCK] RETURNS [CARDINAL] =
    INLINE {RETURN[IF in>=out THEN in-out ELSE in+LENGTH[buffer]-out]};
  RETURN[P[@keyboardLock]]
  END;

GetChar: PUBLIC PROC [h: TTY.Handle] RETURNS [c: CHARACTER] =
  BEGIN
  GetInternal: ENTRY PROC [m: POINTER TO MONITORLOCK] RETURNS [BOOL] = INLINE {
    ENABLE UNWIND => NULL;
    WHILE in=out DO
      WAIT charactersAvailable;
      IF in=out THEN RETURN[FALSE];
      ENDLOOP;
    c ← buffer[out];
    IF (out←out+1) = LENGTH[buffer] THEN out ← 0;
    RETURN[TRUE]
    };
  UNTIL GetInternal[@keyboardLock] DO SIGNAL InputTimeout; ENDLOOP;
  END;

GetDecimal: PUBLIC PROC [h: TTY.Handle] RETURNS [INTEGER] =
  BEGIN
  s: STRING ← [10];
  [] ← GetEditedString[h, s, IsAtom, TRUE];
  RETURN[String.StringToNumber[s, 10]]
  END;

GetID: PUBLIC PROC [h: TTY.Handle, s: STRING] =
  {[] ← GetEditedString[h, s, IsAtom, TRUE]};

GetLine: PUBLIC PROC [h: TTY.Handle, s: STRING] =
  {[] ← GetEditedString[h, s, IsCR, TRUE]; PutChar[h, CR]};

GetLongDecimal: PUBLIC PROC [h: TTY.Handle] RETURNS [n: LONG INTEGER] =
  BEGIN
  s: STRING ← [32];
  [] ← GetEditedString[h, s, IsAtom, TRUE];
  RETURN[String.StringToLongNumber[s, 10]]
  END;

GetLongNumber: PUBLIC PROC [
  h: TTY.Handle, default: LONG UNSPECIFIED, radix: CARDINAL]
  RETURNS [n: LONG UNSPECIFIED] =
  BEGIN
  s: STRING ← [32];
  IF radix = 10 AND LOOPHOLE[default, LONG INTEGER] < 0 THEN
    {s[0] ← '-; s.length ← 1; default ← -default};
  String.AppendLongNumber[s, default, radix];
  IF radix = 8 THEN String.AppendChar[s, 'B];
  [] ← GetEditedString[h, s, IsAtom, TRUE];
  RETURN[String.StringToLongNumber[s, radix]];
  END;

GetLongOctal: PUBLIC PROC [h: TTY.Handle] RETURNS [n: LONG UNSPECIFIED] =
  BEGIN
  s: STRING ← [32];
  [] ← GetEditedString[h, s, IsAtom, TRUE];
  RETURN[String.StringToLongNumber[s, 8]]
  END;

GetNumber: PUBLIC PROC [h: TTY.Handle, default: UNSPECIFIED, radix: CARDINAL]
  RETURNS [n: UNSPECIFIED] =
  BEGIN
  s: STRING ← [10];
  IF radix = 10 AND LOOPHOLE[default, INTEGER] < 0 THEN
    {default ← -default; s[0] ← '-; s.length ← 1};
  String.AppendNumber[s, default, radix];
  IF radix = 8 THEN String.AppendChar[s, 'B];
  [] ← GetEditedString[h, s, IsAtom, TRUE];
  RETURN[String.StringToNumber[s, radix]];
  END;

GetOctal: PUBLIC PROC [h: TTY.Handle] RETURNS [n: UNSPECIFIED] =
  BEGIN
  s: STRING ← [10];
  [] ← GetEditedString[h, s, IsAtom, TRUE];
  RETURN[String.StringToNumber[s, 8]]
  END;

GetString: PUBLIC PROC [
  h: TTY.Handle, s: STRING, t: PROC [c: CHARACTER] RETURNS [yes: BOOLEAN]] =
  {PutChar[h, GetEditedString[h, s, t, TRUE]]};

LineOverflow: PUBLIC SIGNAL [s: STRING] RETURNS [ns: STRING] = CODE;
Rubout: PUBLIC SIGNAL = CODE;

GetEditedString: PUBLIC PROC [
  h: TTY.Handle, s: STRING,
  t: PROC [c: CHARACTER] RETURNS [yes: BOOLEAN],
  newstring: BOOLEAN]
  RETURNS [c: CHARACTER] =
  BEGIN
  c ← GetChar[h];
  IF newstring THEN
    IF c = ESC THEN {PutString[h, s]; c ← GetChar[h]} ELSE s.length ← 0;
  UNTIL t[c] DO
    SELECT c FROM
      DEL => SIGNAL Rubout;
      ControlA, BS => -- backspace
        IF s.length > 0 THEN {IF echo THEN PutChar[h, BS]; s.length ← s.length - 1};
      ControlW, ControlQ => -- backword
        BEGIN
        -- text to be backed up is of the form ...<li><v><ti>, the <v> and <ti> are to
        -- be removed.
        state: {ti, v, li} ← ti;
        FOR i: CARDINAL DECREASING IN [0..s.length) DO
          SELECT s[i] FROM
            IN ['A..'Z], IN ['a..'z], IN ['0..'9] => IF state = ti THEN state ← v;
            ENDCASE => IF state = v THEN state ← li;
          IF state = li THEN GO TO Done;
          IF echo THEN PutChar[h, BS];
          REPEAT
            Done => s.length ← i + 1;
            FINISHED => s.length ← 0;
          ENDLOOP;
        END;
      ControlX => -- back everything
        BEGIN
        IF echo THEN FOR i: CARDINAL IN [0..s.length) DO PutChar[h, BS] ENDLOOP;
        s.length ← 0;
        END;
      ControlR => -- refresh-- IF echo THEN {PutChar[h, CR]; PutString[h, s]};
      ControlV => -- dont parse next char
        BEGIN
        WHILE s.length >= s.maxlength DO s ← SIGNAL LineOverflow[s] ENDLOOP;
        s[s.length] ← c ← GetChar[h];
        s.length ← s.length + 1;
        IF echo THEN PutChar[h, c]
        END;
      ENDCASE =>
        BEGIN
        WHILE s.length >= s.maxlength DO s ← SIGNAL LineOverflow[s] ENDLOOP;
        s[s.length] ← c;
        s.length ← s.length + 1;
        IF echo THEN PutChar[h, c];
        END;
    c ← GetChar[h];
    ENDLOOP;
  END;

IsAtom: PROC [c: CHARACTER] RETURNS [BOOLEAN] = {RETURN[c = SP OR c = CR]};

IsCR: PROC [c: CHARACTER] RETURNS [BOOLEAN] = {RETURN[c = CR]};

PutBackChar: PUBLIC PROC [h: TTY.Handle, c: CHARACTER] =
  BEGIN
  P: ENTRY PROC [m: POINTER TO MONITORLOCK] =
    INLINE BEGIN
    newout: CARDINAL = IF out=0 THEN LENGTH[buffer]-1 ELSE out-1;
    IF newout#in THEN {buffer[out ← newout] ← c; BROADCAST charactersAvailable};
    END;
  P[@keyboardLock]
  END;

SetEcho: PUBLIC PROC [h: TTY.Handle, new: BOOLEAN] RETURNS [old: BOOLEAN] =
  BEGIN
  SetEntry: ENTRY PROC [m: POINTER TO MONITORLOCK] = INLINE {old ← echo; echo ← new};
  SetEntry[@keyboardLock]
  END;

-- output procedures
NewLine: PUBLIC PROC [TTY.Handle] RETURNS [BOOLEAN] = {RETURN[charPos=0]};

PutChar: PUBLIC PROC [h: TTY.Handle, c: CHARACTER] =
  BEGIN
  PutCharEntry: ENTRY PROC [m: POINTER TO MONITORLOCK] = INLINE {
    doBlink ← FALSE; ClearThisChar[]; DisplayChar[c]; doBlink ← TRUE;
    IF typescriptEnabled THEN Streams.PutChar[typescript, c]};
  PutCharEntry[@screenLock];
  END;

PutLongString: PUBLIC PROC [h: TTY.Handle, s: LONG STRING] =
  {IF s#NIL THEN FOR i: CARDINAL IN [0..s.length) DO PutChar[h, s[i]] ENDLOOP};

PutString: PUBLIC PROC [h: TTY.Handle, s: STRING] = {PutLongString[h, s]};

OutString: PROC [s: STRING] = {PutLongString[LOOPHOLE[0], s]};

PutBlanks: PUBLIC PROC [h: TTY.Handle, n: CARDINAL] =
  {THROUGH [0..n) DO PutChar[h, ' ] ENDLOOP};

PutDate: PUBLIC PROC [h: TTY.Handle, gmt: Time.Packed, format: TTY.DateFormat] =
  {Format.Date[gmt, format, OutString]};

PutDecimal: PUBLIC PROC [h: TTY.Handle, n: INTEGER] = {Format.Decimal[n, OutString]};

PutLongDecimal: PUBLIC PROC [h: TTY.Handle, n: LONG INTEGER] =
  {Format.LongDecimal[n, OutString]};

PutLongNumber: PUBLIC PROC [h: TTY.Handle, n: LONG UNSPECIFIED, format: TTY.NumberFormat] =
  {Format.LongNumber[n, format, OutString]};

PutLongOctal: PUBLIC PROC [h: TTY.Handle, n: LONG UNSPECIFIED] =
  {Format.LongOctal[n, OutString]};

PutNumber: PUBLIC PROC [h: TTY.Handle, n: UNSPECIFIED, format: TTY.NumberFormat] =
  {Format.Number[n, format, OutString]};

PutOctal: PUBLIC PROC [h: TTY.Handle, n: UNSPECIFIED] = {Format.Octal[n, OutString]};

PutLongSubString: PUBLIC PROC [h: TTY.Handle, ss: TTY.LongSubString] =
  {Format.LongSubStringItem[ss, OutString]};

PutSubString: PUBLIC PROC [h: TTY.Handle, ss: String.SubString] =
  {Format.SubString[ss, OutString]};

END.