unit Unit1;

////////////////////////////////////////////////////////////////////////////////
// (c) 2017  Robert Rozee
////////////////////////////////////////////////////////////////////////////////

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls, Buttons, Math, ComCtrls, ClipBrd, Menus, Registry;

type
  TForm1 = class(TForm)
    Timer1: TTimer;
    Timer2: TTimer;
    Timer3: TTimer;
    Timer4: TTimer;

    Cursor: TLabel;

    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    Label7: TLabel;
    Label8: TLabel;
    Image1: TImage;
    Image2: TImage;
    Memo1: TMemo;

    OpenDialog1: TOpenDialog;
    SaveDialog1: TSaveDialog;
    PopupMenu1: TPopupMenu;
    Item1A: TMenuItem;                   // connect
    item1B: TMenuItem;                   // disconnect
    Item2A: TMenuItem;                   // LOG to file
    Item2B: TMenuItem;                   // STOP logging
    Item3A: TMenuItem;                   // paste
    Item3A1: TMenuItem;                  //     from clipboard
    Item3A2: TMenuItem;                  //     from text file
    Item3B: TMenuItem;                   // CANCEL paste
    N1: TMenuItem;
    Item4: TMenuItem;                    // font size
    Item4A: TMenuItem;                   //     9pt
    Item4B: TMenuItem;                   //     12pt
    Item4C: TMenuItem;                   //     14pt
    Item4D: TMenuItem;                   //     manual
    Item5: TMenuItem;                    // font colour
    Item5A: TMenuItem;                   //     red
    Item5B: TMenuItem;                   //     green
    Item5C: TMenuItem;                   //     yellow
    Item5D: TMenuItem;                   //     blue
    Item5E: TMenuItem;                   //     magenta
    Item5F: TMenuItem;                   //     cyan
    Item5G: TMenuItem;                   //     WHITE
    Item6: TMenuItem;                    // dimmable text
    Item6A: TMenuItem;                   //     enabled
    Item6B: TMenuItem;                   //     bright #1
    Item6C: TMenuItem;                   //     bright #2
    Item7: TMenuItem;                    // clear
    Item7A: TMenuItem;                   //     GFX layer
    Item7B: TMenuItem;                   //     text layer
    Item7C: TMenuItem;                   //     ring buffer
    N2: TMenuItem;
    Item8: TMenuItem;                    // select and copy
    Item9: TMenuItem;                    // command window (show)
    N3: TMenuItem;
    Item10: TMenuItem;
    FontDialog1: TFontDialog;            // (c) Robert Rozee  2017

    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);

    procedure Timer1Timer(Sender: TObject);
    procedure Timer2Timer(Sender: TObject);
    procedure Timer3Timer(Sender: TObject);
    procedure Timer4Timer(Sender: TObject);

    procedure FormKeyPress(Sender: TObject; var Key: Char);
    procedure FormKeyUp(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure FormMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure FormMouseWheel(Sender: TObject; Shift: TShiftState;
      WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);

    procedure PopupMenu1Popup(Sender: TObject);
    procedure Item1Click(Sender: TObject);
    procedure Item2Click(Sender: TObject);
    procedure Item3Click(Sender: TObject);
    procedure Item4Click(Sender: TObject);
    procedure Item5Click(Sender: TObject);
    procedure Item6Click(Sender: TObject);
    procedure Item7Click(Sender: TObject);
    procedure Item8Click(Sender: TObject);
    procedure Item9Click(Sender: TObject);

  private
    { Private declarations }
    procedure CMDialogKey(var Msg: TCMDialogKey); message CM_DIALOGKEY;
    procedure WMSysCommand(var Msg: TWMSysCommand); message WM_SYSCOMMAND;
    procedure WMDeviceChange(var Msg: TMessage); message WM_DEVICECHANGE;
//  procedure WMEnterSizeMove(var Msg: TMessage); message WM_ENTERSIZEMOVE;
  public
    { Public declarations }
    procedure WMKeyDown(var Msg: TWMKeyDown); message WM_KEYDOWN;
  end;

var
  Form1: TForm1;

implementation

uses Unit2, Unit3;

{$R *.DFM}

procedure TForm1.CMDialogKey(var Msg: TCMDialogKey);   // allows tab key to be used
begin
  if Msg.CharCode<>VK_TAB then inherited
end;

procedure TForm1.WMSysCommand(var Msg: TWMSysCommand); // disable hotkey passthrough
begin                                                  // (alt-keys producing error beep)
  if Msg.CmdType<>SC_KEYMENU then inherited
end;

procedure TForm1.WMDeviceChange(var Msg: TMessage);    // USB device plug and unplug events
begin
//if Msg.Msg=537 then begin
                        Timer4.Enabled:=false;
                        Timer4.Enabled:=true;
                        inherited
//                    end
end;                             // 537 = 0x0219


(*
var  hRead_local:THandle;
    hWrite_local:THandle;
     hRead_remote:THandle;
    hWrite_remote:THandle;
*)

const SoftwareKey='\Software\NZ made\GFXterm';
        BuildName='Graphical VT Terminal';
        BuildDate='(build: 5-dec-2017)';   // +'      *** TEST VERSION ***';

const ROWS=24;      // 24 or 36 or 43
      COLS=80;      // limit:1036 pixels max

var LogFile:text;

var CommFile:THandle;
    CommName:string;
    CommRate:integer;

var CSR:TLabel;                          // used as a shortcut to cursor object
    SCR:TCanvas;                         // used as a shortcut to text screen
    GFX:TCanvas;                         // used as a shortcut to graphics screen
    DefFont:record
              Name:string;               // default font settings
              CharSet:integer;           // (saved from Cursor at startup)
              Size:integer
            end;

const FGdefault:integer=7;               // default text FG colour
      BGdefault:integer=0;               // default text BG colour
       FGcolour:integer=7;               // text foreground colour
       BGcolour:integer=0;               // text background colour
       TxtStyle:TFontStyles=[];          // text style (underline, bold, etc)
        DimText:boolean=false;           // low intensity flag
        InvText:boolean=false;           // inverse video flag
         DimOpt:integer=0;               // enabled / bright 1 / bright 2

           Xpos:integer=1;               // initial cursor column
           Ypos:integer=1;               // initial cursor row
        Tmargin:integer=1;               // VT100 scroll area top margin
        Bmargin:integer=ROWS;            // VT100 scroll area bottom margin

          lastK:char=#$00;               // last ascii key pressed
          lastC:char=#$00;               // last ascii character printed

         MouseX:integer=-1;              // last mouse X position
         MouseY:integer=-1;              // last mouse Y position

            CLC:integer=0;               // counter of lines added to command log

            TC1:DWORD=0;                 // timestamp of last serial port data read
            TC2:DWORD=0;                 // timestamp of last keyboard event
            TC3:DWORD=0;                 // timestamp of last VT command decoded
       ErrorMsg:string='';               // description of error causing a disconnect

             PB:record
                  str:string;            // paste buffer (characters to paste)
                  len:integer;           // length of buffer
                  idx:integer            // index into buffer
                end=(str:''; len:0; idx:0);

             RB:record                   // serial input ring buffer
                  data:array [0..65535] of char;
                  head:integer;
                  tail:integer
                end=(head:0; tail:0);

const BreakCounter:integer=-1;           // used to time sending a break
         CONNECTED:integer=0;            // 0=disconnected, 1=USBcheck, 2=connected
         LOGTOFILE:boolean=false;        // true if logging text to a file
//            KILL:boolean=false;        // kill signal to application loop
//            DEAD:boolean=false;        // response from application loop

     MouseReportXY:boolean=false;        // true if X10 mouse support turned on
     MouseProtocol:integer=0;            // mouse reporting protocol to use


var TextStore:array [1..ROWS] of array [1..COLS] of char;

type str255=string[255];

const CPC:array [0..15] of TColor=(clBlack, clNavy, clGreen, clTeal,
                                   clMaroon, clPurple, clOlive, clSilver,
                                   clGray, clBlue, clLime, clAqua,
                                   clRed, clFuchsia, clYellow, clWhite);
{   (dim)           (bright)
0 = Black 	8 = Gray
1 = Blue 	9 = Light Blue
2 = Green 	A = Light Green
3 = Aqua 	B = Light Aqua     (Cyan)
4 = Red 	C = Light Red
5 = Purple 	D = Light Purple   (Magenta)
6 = Yellow 	E = Light Yellow
7 = White 	F = Bright White
}
const CVT:array [0..15] of TColor=(clBlack, clMaroon, clGreen, clOlive,
                                   clNavy, clPurple, clTeal, clSilver,
                                   clGray, clRed, clLime, clYellow,
                                   clBlue, clFuchsia, clAqua, clWhite);

{       (0-7 = dim, 8-15 = bright)
30	Black
31	Red
32	Green
33	Yellow
34	Blue                  TColor($FF8080) = bright blue, as per teraterm
35	Magenta   (Purple)
36	Cyan      (Aqua)
37	White
}

type
  TUSBcheckThread = class(TThread)
  private
  protected
    procedure Execute; override;
  end;

// separate thread used to check if comm port is still alive
procedure TUSBcheckThread.Execute;
var DCB:TDCB;
     ok:boolean;
begin
  DCB.DCBlength:=sizeof(DCB);
  repeat
    sleep(100);

    if CONNECTED=1 then
    begin
      ok:=false;
      try
        if GetCommState(CommFile, DCB) then
        if SetCommState(CommFile, DCB) then ok:=true
      except
      end;

      if ok then CONNECTED:=2
            else begin
                   try CloseHandle(CommFile); except end;
                   ErrorMsg:='Serial I/O error: USB port unplugged';
                   CONNECTED:=0
                 end;

    end
  until false
end;

// returns how much time has elapsed since GetTickCount was assigned to counter
function timesince(counter:DWORD):DWORD;
var I:int64;
begin
{
  QueryPerformanceCounter(I);
  I:=I div $400;
  dec(I,counter);
}
  I:=GetTickCount-counter;
  if I<0 then I:=I+$100000000;
  result:=I
end;

function StrParity(I:integer):string;
begin
  case I of 0:result:='NONE';
            1:result:='ODD';
            2:result:='EVEN';
            3:result:='MARK';
            4:result:='SPACE'
         else result:=IntToStr(I)
  end  { of case }
end;

function StrStopBits(I:integer):string;
begin
  case I of 0:result:='1 bit';
            1:result:='1.5 bits';
            2:result:='2 bits'
         else result:=IntToStr(I)
  end  { of case }
end;

(*
// search registry for serial port matching name
function validCP(name:string):boolean;
var CPL:TStringList;                           // comm port list
    Reg:TRegistry;
      I:integer;
      S:string;
begin
  CPL:=TStringList.Create;
  CPL.Clear;
  result:=false;

  Reg:=TRegistry.Create;
  with Reg do
  begin
    RootKey:=HKEY_LOCAL_MACHINE;
    try
      if OpenKeyReadOnly('hardware\devicemap\serialcomm') then
      begin
        GetValueNames(CPL);
        for I:=0 to CPL.Count-1 do
        begin
          S:=ReadString(CPL.Strings[I]);       // get port name
          if uppercase(S)=uppercase(name) then result:=true
        end
      end
    except
      result:=true                             // can't check against registry
    end;
    try CloseKey except end
  end;
  Reg.Free;
  CPL.Free
end;
*)


////////////////////////////////////////////////////////////////////////////////
// the following routines are the text plane primatives. this plane sits
// behind the graphics plane. the following commands are provided:
// putch (x, y, character)              - puts a single character at (x,y)
// gotoxy (x, y)                        - moves cursor to (x,y)
// clear (x1, y1, x2, y2)               - clears a rectangular area
// scroll (x1, y1, x2, y2, direction)   - direction can be -1 (down) or +1 (up)
// emit (ch)                            - print (ch) at cursor, update cursor
////////////////////////////////////////////////////////////////////////////////

procedure putch(X, Y:integer; ch:char);
var FGmask, BGmask:byte;
begin
  case DimOpt of 0:begin
                     if DimText then FGmask:=$0
                                else FGmask:=$8;
                     BGmask:=$0
                   end;
                 1:begin
                     FGmask:=$8;
                     BGmask:=$0
                   end
              else begin
                     FGmask:=$8;
                     if BGcolour<>0 then BGmask:=$8
                                    else BGmask:=$0
                   end;
  end;  { of case }
{
  if DimText then mask:=$0
             else mask:=$8;
}
  if InvText then begin
                    SCR.Font.Color:=CVT[BGcolour or BGmask];
                    SCR.Brush.Color:=CVT[FGColour or FGmask]
                  end
             else begin
                    SCR.Font.Color:=CVT[FGcolour or FGmask];
                    SCR.Brush.Color:=CVT[BGColour or BGmask]
                  end;
  SCR.Font.Style:=TxtStyle;

  if CSR.AutoSize then SCR.TextOut(CSR.Width*(X-1), CSR.Height*(Y-1), ch)  // "Terminal", monospaced
                  else begin                                               // manual font override
                         SCR.FillRect(Rect(CSR.Width*(X-1), CSR.Height*(Y-1),
                                           CSR.Width*(X),   CSR.Height*(Y)  ));
                         SCR.TextOut(CSR.Width*(X-1)
                                   +(CSR.Width-SCR.TextWidth(ch)) div 2,
                                     CSR.Height*(Y-1), ch)
                       end;

  if (X in [1..COLS]) and (Y in [1..ROWS]) then TextStore[Y, X]:=ch
end;


procedure gotoxy(X, Y:integer);        // -1 indicate no change
begin
  if X<>-1 then
  begin
    Xpos:=X;               // Xpos = 1 .. ROWS
    if Xpos<1 then Xpos:=1;
    if Xpos>COLS then Xpos:=COLS
  end;

  if Y<>-1 then
  begin
    Ypos:=Y;               // Ypos = 1 .. COLS
    if Ypos<1 then Ypos:=1;
    if Ypos>ROWS then Ypos:=ROWS
  end;

  CSR.Tag:=1                           // flag cursor position as moved
end;


procedure clear(X1, Y1, X2, Y2:integer);
var X, Y:integer;
begin
  if X2<X1 then begin X:=X1; X1:=X2; X2:=X end;
  if Y2<Y1 then begin Y:=Y1; Y1:=Y2; Y2:=Y end;

  SCR.Brush.Color:=CVT[BGColour];
  SCR.FillRect(Rect(CSR.Width*(X1-1), CSR.Height*(Y1-1),
                    CSR.Width*(X2),   CSR.Height*(Y2)  ));

  for Y:=Y1 to Y2 do FillChar(TextStore[Y, X1], X2-X1+1, ' ')
end;


procedure scroll(X1, Y1, X2, Y2, direction:integer);   // +1 = scroll up, -1 = scroll down
var R1, R2:TRect;
      X, Y:integer;
begin
  if X2<X1 then begin X:=X1; X1:=X2; X2:=X end;
  if Y2<Y1 then begin Y:=Y1; Y1:=Y2; Y2:=Y end;

  R1:=Rect(CSR.Width*(X1-1), CSR.Height*(Y1-1),        // upper rectangle
           CSR.Width*(X2)  , CSR.Height*(Y2-1));
  R2:=Rect(CSR.Width*(X1-1), CSR.Height*(Y1  ),        // lower rectangle
           CSR.Width*(X2)  , CSR.Height*(Y2)  );

  SCR.Brush.Color:=CVT[BGColour];

  case direction of +1:begin                           // scroll screen upwards by 1 line
                         SCR.CopyRect(R1, SCR, R2);    // destination=R1, canvas, source=R2
                         R2.Top:=R1.Bottom;
               //        inc(R2.Bottom);
               //        inc(R2.Right);
                         SCR.FillRect(R2);
                         for Y:=Y1 to Y2-1 do move(TextStore[Y+1, X1], TextStore[Y, X1], X2-X1+1);
                                              //   source,             destination,      count
                         FillChar(TextStore[Y2, X1], X2-X1+1, ' ')     // blank line at bottom
                       end;
                    -1:begin;                          // scroll screen downwards by 1 line
                         SCR.CopyRect(R2, SCR, R1);    // destination=R2, canvas, source=R1
                         R1.Bottom:=R2.Top;    // -1;
               //        inc(R1.Right);
                         SCR.FillRect(R1);
                         for Y:=Y2 downto Y1+1 do move(TextStore[Y-1, X1], TextStore[Y, X1], X2-X1+1);
                                                  //   source,             destination,      count
                         FillChar(TextStore[Y1, X1], X2-X1+1, ' ')     // blank line at top
                       end
                 else ShowMessage('invalid scroll value (+1,-1 required)')
  end  { of case }
end;


procedure emit(ch:char);
begin
  lastC:=ch;
  if ch<#32 then case ch of #07:windows.beep(440,250);               // bell
                            #08:if Xpos<>1 then dec(Xpos);           // backspace
                            #09:begin
                                  if Xpos>COLS then
                                  begin
                                    Xpos:=1;
                                    inc(Ypos)
                                  end;
                                  repeat                             // tab
                                    putch(Xpos, Ypos, ' ');
                                    inc(Xpos)
                                  until (Xpos-1) mod 8=0
                                end;
                            #10:begin                                // linefeed
                                  inc(Ypos);
                                  if Ypos=Bmargin+1 then
                                  begin
                                    scroll(1, Tmargin, COLS, Bmargin, +1);
                                    Ypos:=Bmargin
                                  end;
                                  if Ypos>ROWS then Ypos:=ROWS
(*
                                  inc(Ypos);
                                  if Ypos>ROWS then
                                  begin
                                    scroll(1, 1, COLS, ROWS, +1);
                                    Ypos:=ROWS
                                  end
*)
                                end;
                            #13:Xpos:=1;                             // carriage return
                            #17:;                                    // DC 1
                            #18:;                                    // DC 2
                            #19:;                                    // DC 3
                            #20:;                                    // DC 4
                            #27:;                                    // escape
                 end  { of case }
            else begin
                   if Xpos>COLS then
                   begin
                     Xpos:=1;
                     inc(Ypos);
                     if Ypos=Bmargin+1 then
                     begin
                       scroll(1, Tmargin, COLS, Bmargin, +1);
                       Ypos:=Bmargin
                     end;
                     if Ypos>ROWS then Ypos:=ROWS
                   end;
(*
                   if Xpos>COLS then
                   begin
                     Xpos:=1;
                     inc(Ypos)
                   end;

                   if Ypos>ROWS then
                   begin
                     scroll(1, 1, COLS, ROWS, +1);
                     Ypos:=ROWS
                   end;
*)
                   putch(Xpos, Ypos, ch);
                   inc(Xpos)
                 end;

  if ch in [#8,#9,#10,#13,#32..#255] then
  begin
    CSR.Tag:=1;                                // flag cursor position as moved

    if LOGTOFILE then
    try
      Write(Logfile, ch)                       // write to log file
    except
      try closefile(LogFile) except end;
      LOGTOFILE:=false
    end
  end
end;


////////////////////////////////////////////////////////////////////////////////
// the following routines are the graphic plane primatives. this plane sits
// in front of the text plane. the following commands are provided:
// Gw                              - returns width of graphic area
// Gh                              - returns height of graphic area
// GFXclear ((X1, Y1, X2, Y2)      - erase a rectular area
// GFXlineAB (X1, Y1, X2, Y2)      - draw a line from (x1,y1) to (x2,y2)
// GFXarc (X1, Y1, X2, Y2, A1, A2) - draw arc within a rectangle, between A1 and A2 degrees
// GFXplot (X, Y)                  - plot a single pixel
// GFXink (R, G, B, width)         - set ink colour and pen width
// GFXfill (X, Y)                  - fill an area we have just enclosed
// GFXmoveto (X, Y)                - set starting location
// DFXdrawto (X, Y)                - draw from previous location to (x,y)
// GFXscroll(X1, Y1, X2, Y2,
//           deltaX, deltaY)       - scroll a graphics area
////////////////////////////////////////////////////////////////////////////////

function Gw:integer;
begin
  result:=Form1.Image2.Picture.Bitmap.Width
end;


function Gh:integer;
begin
  result:=Form1.Image2.Picture.Bitmap.Height
end;


procedure GFXclear(X1, Y1, X2, Y2:integer);
begin
  inc(X2);                             // +1 as fillrect normally excludes RHS edge
  inc(Y2);                             // +1 as fillrect normally excludes bottom edge
  GFX.Brush.Color:=clBlack;
  GFX.FillRect(Rect(X1, Y1, X2, Y2))
end;


procedure GFXlineAB(X1, Y1, X2, Y2:integer);
begin
  GFX.MoveTo(X1, Y1);
  GFX.LineTo(X2, Y2)                   // draw line, excluding the last point
//GFX.LineTo(X2, Y2)                   // fill in last point ????????????????
end;


procedure GFXarc(X1, Y1, X2, Y2:integer; A1, A2:single);
var X0, Y0, X3, Y3, X4, Y4:integer;
begin
  A1:=(A1*pi/180.0)-(pi/2.0);            // convert A1 to radians, shift origin ccw 1/4 turn
  A2:=(A2*pi/180.0)-(pi/2.0);            // convert A2 to radians, shift origin ccw 1/4 turn

  X0:=(X1 + X2) div 2;                   // locate centre of elipse: X0
  Y0:=(Y1 + Y2) div 2;                   // locate centre of elipse: Y0

  X3:=X0 + trunc(1000.0*Cos(A2));
  Y3:=Y0 + trunc(1000.0*Sin(A2));
  X4:=X0 + trunc(1000.0*Cos(A1));
  Y4:=Y0 + trunc(1000.0*Sin(A1));

  GFX.Arc(X1, Y1, X2, Y2, X3, Y3, X4, Y4)
end;


procedure GFXplot(X, Y:integer);
begin
  GFX.Pixels[X, Y]:=GFX.Pen.Color
end;


procedure GFXink(R, G, B, width:integer);
begin
  R:=min(255, max(R, 0));
  G:=min(255, max(G, 0));
  B:=min(255, max(B, 0));
  GFX.Pen.Color:=(B*$10000) + (G*$100) + R;
  GFX.Pen.Width:=width
end;


procedure GFXfill(X, Y:integer);
begin
  GFX.Brush.Color:=GFX.Pen.Color;                  // select pen colour as fill
  GFX.FloodFill(X, Y, GFX.Pen.Color, fsBorder);    // fill everything enclosed by pen colour
  GFX.Brush.Color:=clBlack                         // go back to a black brush
(*
  GFX.Brush.Color:=clBlack;                        // select black fill colour
  GFX.FloodFill(X, Y, GFX.Pen.Color, fsBorder);    // fill area bounded by pen colour
  GFX.Brush.Color:=GFX.Pen.Color;                  // select pen colour as fill
  GFX.FloodFill(X, Y, clBlack, fsSurface);         // fill everything we previously set to black
  GFX.Brush.Color:=clBlack                         // go back to a black brush
*)
end;


procedure GFXmoveto(X, Y:integer);
begin
  GFX.MoveTo(X, Y)                                 // set starting point of a multi-line
end;


procedure GFXdrawto(X, Y:integer);
begin
  GFX.LineTo(X, Y)                                 // draw line to (x,y)
end;                                               // excluding the last point


procedure GFXscroll_OLD(X1, Y1, X2, Y2, deltaX, deltaY:integer);
var R1, R2, Fh, Fv:TRect;
begin
  if (deltaX=0) and (deltaY=0) then exit;      // nothing to do!

// R1 is the source rectangle, R2 is the destingation rectangle
  if deltaX<0 then begin                       // moving *** left ***
                     R1.Left:=X1-deltaX;       // - source left side
                     R1.Right:=X2;             // - source right side
                     R2.Left:=X1;              // - destination left side
                     R2.Right:=X2+deltaX;      // - destination right side

                     Fv.Left:=R2.Right+1;
                     Fv.Right:=X2+1            // +1 as fillrect skips right side
                   end
              else begin                       // moving *** right ***
                     R1.Left:=X1;              // - source left side
                     R1.Right:=X2-deltaX;      // - source right side
                     R2.Left:=X1+deltaX;       // - destination left side
                     R2.Right:=X2;             // - destination right side

                     Fv.Left:=X1;
                     Fv.Right:=R2.Left         // fillrect skips right side
                   end;
  Fv.Top:=Y1;
  Fv.Bottom:=Y2+1;                             // +1 as fillrect skips bottom edge

  if deltaY<0 then begin                       // moving *** up ***
                     R1.Top:=Y1-deltaY;        // - source top side
                     R1.Bottom:=Y2;            // - source bottom side
                     R2.Top:=Y1;               // - destination top side
                     R2.Bottom:=Y2+deltaY;     // - destination bottom side

                     Fh.Top:=R2.Bottom+1;
                     Fh.Bottom:=Y2+1           // +1 as fillrect skips bottom edge
                   end
              else begin                       // moving *** down  ***
                     R1.Top:=Y1;               // - source top side
                     R1.Bottom:=Y2-deltaY;     // - source bottom side
                     R2.Top:=Y1+deltaY;        // - destination top side
                     R2.Bottom:=Y2;            // - destination bottom side

                     Fh.Top:=Y1;
                     Fh.Bottom:=R2.Top         // fillrect skips bottom edge
                   end;
  Fh.Left:=X1;
  Fh.Right:=Y2+1;                              // +1 as fillrect skips right side

  GFX.CopyRect(R2, GFX, R1);    // destination=R2, canvas, source=R1

  GFX.Brush.Color:=clBlack;
  if deltaX<>0 then GFX.FillRect(Fv);
  if deltaY<>0 then GFX.FillRect(Fh)
end;

////////////////////////////////////////////////////////////////////////////////
// assumes CopyRect and FillRect both skip right column and bottom row        //
////////////////////////////////////////////////////////////////////////////////

procedure GFXscroll(X1, Y1, X2, Y2, deltaX, deltaY:integer);
var R1, R2, Fh, Fv:TRect;
begin
  if (deltaX=0) and (deltaY=0) then exit;      // nothing to do!
  inc(X2);
  inc(Y2);

// R1 is the source rectangle, R2 is the destingation rectangle
  if deltaX<0 then begin                       // moving *** left ***
                     R1.Left:=X1-deltaX;       // - source left side
                     R1.Right:=X2;             // - source right side
                     R2.Left:=X1;              // - destination left side
                     R2.Right:=X2+deltaX;      // - destination right side

                     Fv.Left:=R2.Right;
                     Fv.Right:=X2
                   end
              else begin                       // moving *** right ***
                     R1.Left:=X1;              // - source left side
                     R1.Right:=X2-deltaX;      // - source right side
                     R2.Left:=X1+deltaX;       // - destination left side
                     R2.Right:=X2;             // - destination right side

                     Fv.Left:=X1;
                     Fv.Right:=R2.Left         // fillrect skips right side
                   end;
  Fv.Top:=Y1;
  Fv.Bottom:=Y2;                               // +1 as fillrect skips bottom edge

  if deltaY<0 then begin                       // moving *** up ***
                     R1.Top:=Y1-deltaY;        // - source top side
                     R1.Bottom:=Y2;            // - source bottom side
                     R2.Top:=Y1;               // - destination top side
                     R2.Bottom:=Y2+deltaY;     // - destination bottom side

                     Fh.Top:=R2.Bottom;
                     Fh.Bottom:=Y2             // +1 as fillrect skips bottom edge
                   end
              else begin                       // moving *** down  ***
                     R1.Top:=Y1;               // - source top side
                     R1.Bottom:=Y2-deltaY;     // - source bottom side
                     R2.Top:=Y1+deltaY;        // - destination top side
                     R2.Bottom:=Y2;            // - destination bottom side

                     Fh.Top:=Y1;
                     Fh.Bottom:=R2.Top         // fillrect skips bottom edge
                   end;
  Fh.Left:=X1;
  Fh.Right:=Y2;                                // +1 as fillrect skips right side

  GFX.CopyRect(R2, GFX, R1);    // destination=R2, canvas, source=R1

  GFX.Brush.Color:=clBlack;
  if deltaX<>0 then GFX.FillRect(Fv);
  if deltaY<>0 then GFX.FillRect(Fh)
end;


////////////////////////////////////////////////////////////////////////////////
// the follow are the communications (serial) port routines:
// SetupCommPort (CommPortName, Config:string)
// ReadComm (var S:string[255]):boolean
// WriteComm (S:string[255]);
// CommBytesWaiting:cardinal;
////////////////////////////////////////////////////////////////////////////////

(* the following flags apply to the DCB.Flags field:
const dcb_Binary =              $00000001;     // always 1
      dcb_ParityCheck =         $00000002;
      dcb_OutxCtsFlow =         $00000004;
      dcb_OutxDsrFlow =         $00000008;

      dcb_DtrControlMask =      $00000030;
      dcb_DtrControlDisable =   $00000000;
      dcb_DtrControlEnable =    $00000010;     // TT turns DTR on
      dcb_DtrControlHandshake = $00000020;

      dcb_DsrSensivity =        $00000040;
      dcb_TXContinueOnXoff =    $00000080;
      dcb_OutX =                $00000100;
      dcb_InX =                 $00000200;
      dcb_ErrorChar =           $00000400;
      dcb_NullStrip =           $00000800;

      dcb_RtsControlMask =      $00003000;
      dcb_RtsControlDisable =   $00000000;
      dcb_RtsControlEnable =    $00001000;     // #
      dcb_RtsControlHandshake = $00002000;
      dcb_RtsControlToggle =    $00003000;

      dcb_AbortOnError =        $00004000;
      dcb_Reserveds =           $FFFF8000;
*)

procedure SetupCommPort(CommPortName, Config:string);
var DeviceName:array [0..80] of char;
           DCB:TDCB;
       CommTOs:TCommTimeouts;
         error:DWORD;
          proc:string;
        action:word;
begin
  CONNECTED:=0;
  try CloseHandle(CommFile) except end;
  try
    StrPCopy(DeviceName, copy('\\.\'+CommPortName,1,80));      // 80 characters max
    proc:='CreateFile';
    CommFile:=CreateFile(DeviceName,
                         GENERIC_READ or GENERIC_WRITE,
                         0, Nil,
                         OPEN_EXISTING,
                         FILE_ATTRIBUTE_NORMAL, 0);
    if (CommFile=INVALID_HANDLE_VALUE) then
    begin
      error:=GetLastError;
      MessageDlg('Serial I/O error: '+proc+' failed'+#13+
                 '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"',
                 mtError, [mbOk], 0);
      exit
    end;

    proc:='SetupComm';
    while not SetupComm(CommFile, 32768, 1024) do
    begin
      error:=GetLastError;
      action:=MessageDlg('Serial I/O error: '+proc+' failed'+#13+
                         '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"',
                         mtWarning, mbAbortRetryIgnore, 0);
      case action of mrIgnore:break;
                      mrRetry:begin end
                  else        begin
                                try CloseHandle(CommFile) except end;
                                exit
                              end
      end  { of case }
    end;

    proc:='GetCommState';
    DCB.DCBlength:=sizeof(DCB);
    while not GetCommState(CommFile, DCB) do
    begin
      error:=GetLastError;
      action:=MessageDlg('Serial I/O error: '+proc+' failed'+#13+
                         '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"',
                         mtWarning, mbAbortRetryIgnore, 0);
      case action of mrIgnore:break;
                      mrRetry:begin end
                  else        begin
                                try CloseHandle(CommFile) except end;
                                exit
                              end
      end  { of case }
    end;
(*
    with DCB do
    ShowMessage('DCB retrieved'+#13+
    'length = '+IntToStr(DCBlength)+#13+
    'baudrate = '+IntToStr(BaudRate)+#13+
    'flags = 0x'+IntToHex(Flags, 4)+#13+
    'reserved = '+IntToStr(wReserved)+#13+
    'XON limit = '+IntToStr(XonLim)+#13+
    'XOFF limit = '+IntToStr(XoffLim)+#13+
    'data bits = '+IntToStr(ByteSize)+#13+
    'parity = '+StrParity(Parity)+#13+
    'stop bits = '+StrStopBits(StopBits)+#13+
    'XON char = '+IntToStr(ord(XonChar))+#13+
    'XOFF char = '+IntToStr(ord(XoffChar))+#13+
    'error char = '+IntToStr(ord(ErrorChar))+#13+
    'EOF char = '+IntToStr(ord(EofChar))+#13+
    'event char = '+IntToStr(ord(EvtChar))+#13+
    'reserved = '+IntToStr(wReserved1)+#13+
    StringOfChar(' ', 48));
*)
//  Config:='baud='+IntToStr(38400)+' parity=n data=8 stop=1 xon=off'#0;     // port parameters
    Config:=Config+#0;                                                       // xon={on|off}
    proc:='BuildCommDCB';
    while not BuildCommDCB(@Config[1], DCB) do
    begin
      error:=GetLastError;
      action:=MessageDlg('Serial I/O error: '+proc+' failed'+#13+
                         '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"',
                         mtWarning, mbAbortRetryIgnore, 0);
      case action of mrIgnore:break;
                      mrRetry:begin end
                  else        begin
                                try CloseHandle(CommFile) except end;
                                exit
                              end
      end  { of case }
    end;
(*
    with DCB do
    ShowMessage('updated DCB'+#13+
    'length = '+IntToStr(DCBlength)+#13+
    'baudrate = '+IntToStr(BaudRate)+#13+
    'flags = 0x'+IntToHex(Flags, 4)+#13+
    'reserved = '+IntToStr(wReserved)+#13+
    'XON limit = '+IntToStr(XonLim)+#13+
    'XOFF limit = '+IntToStr(XoffLim)+#13+
    'data bits = '+IntToStr(ByteSize)+#13+
    'parity = '+StrParity(Parity)+#13+
    'stop bits = '+StrStopBits(StopBits)+#13+
    'XON char = '+IntToStr(ord(XonChar))+#13+
    'XOFF char = '+IntToStr(ord(XoffChar))+#13+
    'error char = '+IntToStr(ord(ErrorChar))+#13+
    'EOF char = '+IntToStr(ord(EofChar))+#13+
    'event char = '+IntToStr(ord(EvtChar))+#13+
    'reserved = '+IntToStr(wReserved1)+#13+
    StringOfChar(' ', 48));
*)
    proc:='SetCommState';
    while not SetCommState(CommFile, DCB) do
    begin
      error:=GetLastError;
      action:=MessageDlg('Serial I/O error: '+proc+' failed'+#13+
                         '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"',
                         mtWarning, mbAbortRetryIgnore, 0);
      case action of mrIgnore:break;
                      mrRetry:begin end
                  else        begin
                                try CloseHandle(CommFile) except end;
                                exit
                              end
      end  { of case }
    end;

    with CommTOs do
    begin
      ReadIntervalTimeout:=1;                  // 10;  // 0
      ReadTotalTimeoutMultiplier:=0;           // 0
      ReadTotalTimeoutConstant:=1;             // 10;  // 300
      WriteTotalTimeoutMultiplier:=0;          // 0
      WriteTotalTimeoutConstant:=0;            // 10   // 300
    end;
    proc:='SetCommTimeouts';
    while not SetCommTimeouts(CommFile, CommTOs) do
    begin
      error:=GetLastError;
      action:=MessageDlg('Serial I/O error: '+proc+' failed'+#13+
                         '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"',
                         mtWarning, mbAbortRetryIgnore, 0);
      case action of mrIgnore:break;
                      mrRetry:begin end
                  else        begin
                                try CloseHandle(CommFile) except end;
                                exit
                              end
      end  { of case }
    end

  except
    try CloseHandle(CommFile); except end;
    MessageDlg('Serial I/O error: '+proc+' exception',
               mtError, [mbOk], 0);
    exit
  end;
//SetCommMask(CommFile, EV_RXFLAG);

  TC1:=GetTickCount;
  TC2:=TC1;
  TC3:=TC2;
  CommName:=CommPortName;
  CommRate:=DCB.BaudRate;
  CONNECTED:=2
end;


function ReadComm(var S:str255):boolean;
const lock:boolean=false;
var get, got, error, reason:DWORD;
                   CommStat:TComStat;
                       proc:string;
begin
  result:=false;
  if lock or (CONNECTED<2) then exit;
  lock:=true;
  try
    proc:='ClearCommError';
    if not ClearCommError(CommFile, reason, @CommStat) then
    begin
      CONNECTED:=0;
      error:=GetLastError;
      try CloseHandle(CommFile); except end;

      ErrorMsg:='Serial I/O error: '+proc+' failed'+#13+
                '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"';
      lock:=false;
      exit
    end;

    get:=min(CommStat.cbInQue, sizeof(S)-1 );          // number of characters waiting
//  get:=sizeof(S)-1;

    proc:='ReadFile';
    if get=0 then got:=0 else                          // skip ReadFile if nothing waiting
    if not ReadFile(CommFile, S[1], get, got, nil) then
    begin
      CONNECTED:=0;
      error:=GetLastError;
      try CloseHandle(CommFile); except end;

      ErrorMsg:='Serial I/O error: '+proc+' failed'+#13+
                '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"';
      lock:=false;
      exit
    end
  except
    CONNECTED:=0;
    try CloseHandle(CommFile); except end;

    ErrorMsg:='Serial I/O error: '+proc+' exception';
    lock:=false;
    exit
  end;

  if got<>0 then
  begin
(*
    S:=StringOfChar(#0, got);
    move(buffer[1], S[1], got);
//  S:=copy(buffer, 0, got);
*)
    S[0]:=char(got);
    TC1:=GetTickCount;
    result:=true
  end;
  lock:=false
end;


function WriteComm(S:str255):boolean;
const lock:boolean=false;
var put, error:DWORD;
begin
  result:=false;
  if lock or (CONNECTED<2) then exit;
  lock:=true;
  try
    if not WriteFile(CommFile, S[1], length(S), put, nil) then
    begin
      CONNECTED:=0;
      error:=GetLastError;
      try CloseHandle(CommFile); except end;

      ErrorMsg:='Serial I/O error: WriteFile failed'+#13+
                '('+IntToStr(error)+')  "'+SysErrorMessage(error)+'"';
      lock:=false;
      exit
    end
  except
    CONNECTED:=0;
    try CloseHandle(CommFile); except end;
    ErrorMsg:='Serial I/O error: WriteFile exception';
    lock:=false;
    exit
  end;

  TC2:=GetTickCount;
  result:=true;
  lock:=false
end;


function CommBytesWaiting:cardinal;
var CommStat:TComStat;
      Errors:cardinal;
begin
  try
    if ClearCommError(CommFile, Errors, @CommStat) then result:=CommStat.cbInQue
                                                   else result:=0
  except
    result:=0
  end
end;


////////////////////////////////////////////////////////////////////////////////
// the following is a VT100 command processing engine. it takes characters one
// at a time via the parameter 'ch'. normally these are simply passed out
// unaltered as a one character long return string to then be passed onto
// emit(ch).
//
// however, when a VT100 command sequence is detected characters are accumulated
// in the internal string 'VTline'. once matched to a valid VT100 command, the
// command is enacted. while if the string accumulated is detected as invalid,
// the whole string is returned.
////////////////////////////////////////////////////////////////////////////////

function VT100engine(ch:char):str255;
const VTflag:boolean=false;         // internally held flag
      VTline:str255='';             // internally held string
       Xsave:integer=-1;            //  )
       Ysave:integer=-1;            //  )
      FGsave:integer=-1;            //  ) saved values for
      BGsave:integer=-1;            //  ) cursor location
      TSsave:TFontStyles=[];        //  ) and attributes
      DTsave:boolean=false;         //  )
      ITsave:boolean=false;         //  )
var n, v, h, I, J:integer;
                S:str255;

  function PC(S:str255):integer;       // returns count of semicolon delimited parameters
  var I, dc, sc:integer;               // or -1 if any invalid characters are found
  begin
    if length(S)=0 then PC:=0 else     // empty string -> no parameters
    begin
      dc:=0;                           // number of digits
      sc:=0;                           // number of semicolon delimiters
      for I:=1 to length(S) do
      begin
        if S[I] in ['0'..'9'] then inc(dc);
        if S[I]=';' then inc(sc);
      end;
      if (dc+sc)<>length(S) then PC:=-1        // invalid characters found
                            else PC:=sc+1      // number of parameters in S
    end
  end;      // ( 1;2 = 2 parameters, 1;;2 = 3 parameters, ;;; = 4 parameters

  function Pn(S:str255; n:integer):integer;    // returns parameter n as an integer
  var I:integer;
  begin
    while (n>1) and (length(S)<>0) do
    begin
      if pos(';', S)=1 then dec(n);
      delete(S, 1, 1)
    end;
    if (length(S)=0) or (S[1]=';') then Pn:=0 else
    begin
      I:=pos(';', S);
      if I<>0 then S:=copy(S, 1, I-1);
      if length(S)=0 then Pn:=0
                     else try
                            Pn:=StrToInt(S)
                          except
                            Pn:=0              // should never arrive here
                          end
    end
  end;

  function OK(S:str255; var n:integer):boolean;        // returns true if a valid number
  begin                                                // (with number placed in n)
    OK:=true;
    try
      if length(S)=0 then n:=0
                     else n:=StrToInt(S)
    except
      OK:=false
    end
  end;

  procedure fail;              // failed to decode string
  begin
    if Form2.Visible then with Form2.RichEdit1 do
    begin
      inc(CLC);
      Lines.BeginUpdate;
//    if CLC>999 then Lines.Delete(0);
      while Lines.Count>1000 do Lines.Delete(0);
      Lines.Add(Format('%.6d  ',[CLC mod 1000000])+
                'FAIL: <ESC>'+copy(VTline,2,length(VTline)-1));
      SendMessage(Handle, WM_VSCROLL, SB_BOTTOM, 0);
      Lines.EndUpdate
    end;

    result:=VTline;            // return complete string
    VTline:='';
    VTflag:=false              // drop out of VT100 mode
  end;

  procedure pass;              // sucessfully processed string
  begin
    if Form2.Visible then with Form2.RichEdit1 do
    begin
      inc(CLC);
      Lines.BeginUpdate;
//    if CLC>999 then Lines.Delete(0);
      while Lines.Count>1000 do Lines.Delete(0);
      Lines.Add(Format('%.6d  ',[CLC mod 1000000])+
                'PASS: <ESC>'+copy(VTline,2,length(VTline)-1));
      SendMessage(Handle, WM_VSCROLL, SB_BOTTOM, 0);
      Lines.EndUpdate
    end;

    VTline:='';
    VTflag:=false;             // drop out of VT100 mode
    TC3:=GetTickCount
  end;

begin
  if ch=#27 then VTflag:=true;
  if not VTflag then result:=ch else
  begin
    result:='';
    VTline:=VTline+ch;
    if length(VTline)=1 then exit;

    if length(VTline)>250 then fail            // way too long an escape sequence!
    else
    if pos(#27+'[', VTline)=1 then             // try to process a complete <esc>[ string
    begin
      if length(VTline)=2 then exit;           // still building command string
      if ch in ['0'..'9',';','?'] then exit;   // still building command string

      S:=copy(VTline, 3, length(VTline)-3);    // extract parameter segment

      case ch of 'A':begin                     // move cursor up n lines
                       if not OK(S, n) then fail else
                       begin
//                       ShowMessage('UP '+IntToStr(n));
                         if n=0 then n:=1;
                         if Ypos<Tmargin then gotoxy(-1, Ypos-n)
                                         else gotoxy(-1, max(Ypos-n, Tmargin));
                         pass
                       end
                     end;
                 'B':begin                     // move cursor down n lines
                       if not OK(S, n) then fail else
                       begin
//                       ShowMessage('DOWN '+IntToStr(n));
                         if n=0 then n:=1;
                         if Ypos>Bmargin then gotoxy(-1, Ypos+n)
                                         else gotoxy(-1, min(Ypos+n, Bmargin));
                         pass
                       end
                     end;
                 'C':begin                     // move cursor right n columns
                       if not OK(S, n) then fail else
                       begin
//                       ShowMessage('RIGHT '+IntToStr(n));
                         if n=0 then n:=1;
                         gotoxy(Xpos+n, -1);
                         pass
                       end
                     end;
                 'D':begin                     // move cursor left n columns
                       if not OK(S, n) then fail else
                       begin
//                       ShowMessage('LEFT '+IntToStr(n));
                         if n=0 then n:=1;
                         gotoxy(Xpos-n, -1);
                         pass
                       end
                     end;
             'H','f':begin                     // position cursor (ignores Tmargin and Bmargin)
                       n:=PC(S);
//                     showmessage('position cursor |'+S+'|'+VTline+'| '+IntToStr(n));
                       case n of 0:begin
                                     gotoxy(1, 1);
//                                   ShowMessage('top left');
                                     pass
                                   end;
                                 1:begin
                                     v:=Pn(S,1);
                                     if v=0 then v:=1;
                                     gotoxy(1, v);
                                     pass
                                   end;
                                 2:begin
                                     v:=Pn(S,1);
                                     h:=Pn(S,2);
                                     if v=0 then v:=1;
                                     if h=0 then h:=1;
                                     gotoxy(h, v);
//                                   showmessage('row = '+IntToStr(v)+'  col = '+IntToStr(h));
                                     pass
                                   end
                              else fail
                       end  { of case }
                     end;
                 'J':if not OK(S, n) then fail else    // clear screen above/below cursor
                     case n of 0:begin                 // clear cursor to end of screen
                                   clear(Xpos, Ypos, COLS, Ypos);
                                   if Ypos<>ROWS then clear(1, Ypos, COLS, ROWS);
                                   pass
                                 end;
                               1:begin                 // clear start of screen to cursor
                                   if Ypos<>1 then clear(1, 1, COLS, Ypos-1);
                                   clear(1, Ypos, Xpos, Ypos);
                                   pass
                                 end;
                               2:begin                 // clear whole screen
                                   clear(1, 1, COLS, ROWS);
//                                 gotoxy(1, 1);       // ANSI std *may* specify home cursor
                                   pass                // VT100 manual says cursor DOES NOT MOVE
                                 end
                            else fail
                     end;  { of case }
                 'K':if not OK(S, n) then fail else    // clear line to left/right of cursor
                     case n of 0:begin                 // clear cursor to EOL
                                   clear(Xpos, Ypos, COLS, Ypos);
                                   pass
                                 end;
                               1:begin                 // clear SOL to cursor
                                   clear(1, Ypos, Xpos, Ypos);
                                   pass
                                 end;
                               2:begin                 // clear whole line
                                   clear(1, Ypos, COLS, Ypos);
                                   pass
                                 end
                                else fail
                     end;  { of case }
                 'm':begin                             // set up character colours and attributes
                       if S='' then S:='0';
                       n:=PC(S);
//                     ShowMessage(IntToStr(n)+'  |'+S+'|');
                       if n=-1 then fail else
                       begin
                         for I:=1 to n do
                         begin
                           J:= Pn(S,I);
                           case J of 0:begin
//                                       ShowMessage('reset colours and attributes');
                                         DimText:=false;
                                         InvText:=false;
                                         TxtStyle:=[];
                                         FGcolour:=FGdefault;
                                         BGcolour:=BGdefault
                                       end;
                                     1:TxtStyle:= TxtStyle+[fsBold];       // bold
                                     2:DimText:=true;                      // dim FGcolour
                                     4:TxtStyle:=TxtStyle+[fsUnderline];   // underline
                                     5:begin end;                          // (not supported)
                                     7:InvText:=true;                      // reverse video
                                30..37:FGcolour:=J-30;
                                    39:FGcolour:=FGdefault;
                                40..47:BGcolour:=J-40;
                                    49:BGcolour:=BGdefault
                           end  { of case }
                         end;
                         pass
                       end
                     end;
                 'n':if not OK(S, n) then fail else
                     if n<>6 then fail else
                     begin                             // return cursor row and column
//                     ShowMessage('cursor position request');
                       WriteComm(#27+'['+IntToStr(Ypos)+';'+IntToStr(Xpos)+'R');
                       pass
                     end;
                 'r':begin                             // set up scrolling region
                       n:=PC(S);
//                     ShowMessage('set scroll window |'+S+'|'+VTline+'| '+IntToStr(n));
                       case n of 0:begin
                                     Tmargin:=1;
                                     Bmargin:=ROWS;
                                     gotoxy(1, 1);     // VT100 manual specifies home cursor
                                     pass
                                   end;
                                 2:begin
                                     I:=max(Pn(S,1), 1);
                                     J:=min(Pn(S,2), ROWS);
                                     if (I<J)then begin
                                                    Tmargin:=I;
                                                    Bmargin:=J
                                                  end;
                                     gotoxy(1, 1);     // VT100 manual specifies home cursor
                                     pass
                                   end
                              else fail
                       end  { of case }
                     end;
                 'h':begin                             // enable feature - X11 mouse reporting
                       if S='?9'    then MouseReportXY:=true else
                       if S='?1006' then MouseProtocol:=1 else
                       if S='?1015' then MouseProtocol:=2 else fail;
                       if VTflag then pass
                     end;
                 'l':begin                             // disable feature - X11 mouse reporting
                       if S='?9'    then MouseReportXY:=false else
                       if S='?1006' then MouseProtocol:=0 else
                       if S='?1015' then MouseProtocol:=0 else fail;
                       if VTflag then pass
                     end
              else   fail
      end  { of case }
    end
    else

    if pos(#27, VTline)=1 then                 // process a complete <esc> string
    begin
      if length(VTline)=1 then exit;           // still building command string
      if ch='[' then exit;                     // still building command string

//    S:=copy(VTline, 2, length(VTline)-2);    // extract parameter segment

      case ch of '7':begin                     // save cursor position and attributes
                       Xsave:=Xpos;
                       Ysave:=Ypos;
                       FGsave:=FGcolour;
                       BGsave:=BGcolour;
                       TSsave:=TxtStyle;
                       DTsave:=DimText;
                       ITsave:=InvText;
                       pass
                     end;
                 '8':begin                     // restore cursor position and attributes
                       if (Xsave>0) and (Ysave>0) then
                       begin
                         gotoxy(Xsave,Ysave);
                         FGcolour:=FGsave;
                         BGcolour:=BGsave;
                         TxtStyle:=TSsave;
                         DimText:=DTsave;
                         InvText:=ITsave;
                         Xsave:=-1;
                         Ysave:=-1
                       end;
                       pass
                     end;
                 'E':begin                     // same as ESC D (below) followed by CR
                       if Ypos<Bmargin then gotoxy(1,Ypos+1)
                                            //  inc(Ypos)      (old version)
                                       else begin
                                              scroll(1, Tmargin, COLS, Bmargin, +1);
                                              gotoxy(1,-1)
                                            end;
                       //  Xpos:=1;            // move cursor to first column of current line
                       pass
                     end;
                 'D':begin                     // scroll region up if cursor is at bottom of window
                       if Ypos<Bmargin then gotoxy(-1,Ypos+1)
                                            //  inc(Ypos)      (old version)
                                       else scroll(1, Tmargin, COLS, Bmargin, +1);
                       pass
                     end;
                 'M':begin                     // scroll region down if cursor is at top of window
                       if Ypos>Tmargin then gotoxy(-1,Ypos-1)
                                            // dec(Ypos)       (old version)
                                       else scroll(1, Tmargin, COLS, Bmargin, -1);
                       pass
                     end
              else   fail
      end;  { of case }
    end else fail
  end
end;


////////////////////////////////////////////////////////////////////////////////
// the following is a GFX command processing engine. it takes characters one
// at a time via the parameter 'ch'. normally these are simply passed out
// unaltered as a one character long return string to then be passed onto
// emit(ch).
//
// however, when a GFX command sequence is detected characters are accumulated
// in the internal string 'GFXline'. once matched to a valid GFX command, the
// command is enacted. while if the string accumulated is detected as invalid,
// the whole string is returned.
////////////////////////////////////////////////////////////////////////////////

function GFXengine(ch:char):str255;
const GFXflag:boolean=false;        // internally held flag
      GFXline:str255='';            // internally held string
      GFXpass:boolean=false;        // true if we've just succeeded in decoding
var params:array[1..16] of integer;
    CMD, S, PSn:str255;
        I, J, n:integer;

  procedure fail;             // failed to decode string
  begin
    if Form2.Visible then with Form2.RichEdit1 do
    begin
      inc(CLC);
      Lines.BeginUpdate;
//    if CLC>999 then Lines.Delete(0);
      while Lines.Count>1000 do Lines.Delete(0);
      Lines.Add(Format('%.6d  ',[CLC mod 1000000])+
                'FAIL: <DLE>'+copy(GFXline,2,length(GFXline)-2));
      SendMessage(Handle, WM_VSCROLL, SB_BOTTOM, 0);
      Lines.EndUpdate
    end;

    result:=GFXline;          // return complete string
    GFXline:='';
    GFXflag:=false            // drop out of GFX mode
  end;

  procedure pass;             // sucessfully processed string
  begin
    if Form2.Visible then with Form2.RichEdit1 do
    begin
      inc(CLC);
      Lines.BeginUpdate;
//    if CLC>999 then Lines.Delete(0);
      while Lines.Count>1000 do Lines.Delete(0);
      Lines.Add(Format('%.6d  ',[CLC mod 1000000])+
                'PASS: <DLE>'+copy(GFXline,2,length(GFXline)-2));
      SendMessage(Handle, WM_VSCROLL, SB_BOTTOM, 0);
      Lines.EndUpdate
    end;

    if not Form1.Image2.Visible then Form1.Image2.Show;        // turn on graphics layer
                                                               // if not already visible
    GFXline:='';
    GFXflag:=false;                  // drop out of GFX mode
    GFXpass:=true
  end;

begin
  if GFXpass and (ch=#10) then       // supress trailing LF for BASIC compatibility
  begin
    GFXpass:=false;
    result:='';
    exit
  end;

  if ch=#16 then GFXflag:=true;
  if not GFXflag then result:=ch else
  begin
    result:='';
    GFXline:=GFXline+ch;

    if length(GFXline)>250 then begin    // way too long a GFX sequence!
                                  fail;
                                  exit
                                end;

    if (GFXline[1]=#16) and (GFXline[length(GFXline)]=#13) then
    begin                          // process a complete <dle> string
//  format is:  <DLE> Command [,|<TAB>|<SPC>]  Param1 [,|<TAB><SPC>]  Param2... <cr><lf>

      S:=GFXline;

// the below FOR and WHILE loops perform the following:
// - convert every control character, space, comma and semicolon into a TAB
// - convert all consecutive groups of TABs into a single TAB
// - remove any leading TABs from the beginning
// - remove any trailing TABs from the end
      for I:=1 to length(S) do if S[I] in [#00..#32,',',';'] then S[I]:=#09;

      I:=pos(#09#09, S);
      while I<>0 do                // convert pairs of TABs into single TABs
      begin
        delete(S, I, 1);
        I:=pos(#09#09, S)
      end;

      while (length(S)<>0) and (S[1]=#09) do
          delete(S, 1, 1);                     // remove leading TABs
      while (length(S)<>0) and (S[length(S)]=#09) do
          delete(S, length(S), 1);             // remove trailing TABs

      if length(S)=0 then fail;
      if not GFXflag then exit;

// next the command is peeled off the start of the string, and converted to upper case
      I:=pos(#09, S);
      J:=pos('-', S);
      if J<>0 then I:=min(I, J);
      J:=pos('+', S);
      if J<>0 then I:=min(I, J);   // new version, allows '+' or '-' delimiting command

      if I<>0 then begin
                     CMD:=UpperCase(copy(S, 1, I-1));
                     if S[I]<>#09 then dec(I);
                     delete(S, 1, I)
                   end
              else begin
                     CMD:=S;
                     S:=''
                   end;

// now we (1) abbreviate/shorten the command to a single letter, and,
//        (2) decide how many parameters we are expecting to see
      if length(CMD)<>1 then
      if CMD='CLEAR'  then CMD:='C' else
      if CMD='INK'    then CMD:='I' else
      if CMD='LINE'   then CMD:='L' else
      if CMD='PLOT'   then CMD:='P' else
      if CMD='ARC'    then CMD:='A' else
      if CMD='FILL'   then CMD:='F' else
      if CMD='MOVETO' then CMD:='M' else
      if CMD='DRAWTO' then CMD:='D' else
      if CMD='SCROLL' then CMD:='S'
                      else fail;               // failed to identify a long command
      if not GFXflag then exit;

      n:=0;                                    // suppress compiler warning
      case CMD[1] of 'P', 'F', 'M', 'D':n:=2;
                          'C', 'I', 'L':n:=4;
                               'A', 'S':n:=6;
                                    '?':n:=0
                  else                  fail   // failed to identify a short command
      end;  { of case }
      if not GFXflag then exit;

// the next thing to do is to separate out the parameters
      for I:=1 to n do
      begin
        J:=pos(#09, S);
        if J<>0 then begin
                       PSn:=copy(S, 1, J-1);
                       delete(S, 1, J)
                     end
                else begin
                       PSn:=S;
                       S:=''
                     end;

        try
          params[I]:=trunc(StrToFloat(PSn))
        except
          fail;
          exit
        end
      end;

//    if false then
      case CMD[1] of 'C':GFXclear(params[1], params[2], params[3], params[4]);
                     'I':GFXink(params[1], params[2], params[3], params[4]);
                     'L':GFXlineAB(params[1], params[2], params[3], params[4]);
                     'P':GFXplot(params[1], params[2]);
                     'A':GFXarc(params[1], params[2], params[3], params[4], params[5], params[6]);
                     'F':GFXfill(params[1], params[2]);
                     'M':GFXmoveto(params[1], params[2]);
                     'D':GFXdrawto(params[1], params[2]);
                     'S':GFXscroll(params[1], params[2], params[3], params[4], params[5], params[6]);
                     '?':WriteComm(IntToStr(Gw)+', '+IntToStr(Gh)+#13)
                  else   fail
      end;  { of case }
      if GFXflag then pass
    end  { end of processing a valid GFX command string }
  end
end;


////////////////////////////////////////////////////////////////////////////////
// Timer event handlers (x4)
// Timer1: 15mS nominal tick rate
//         flash cursor: 500mS on, 500mS off
//         every 15mS: update status bar and cursor location
//         every 15mS: process incoming serial data into ring buffer
//         handle break signalling to 1455 firmware
// Timer2: 45mS, process outgoing serial data (max 30mS runtime)
// Timer3: 15mS or 100ms, paste ticker, interval changes while pasting
// Timer4: 1000ms one-shot, part of USB connection health test
//
// note: Timer2 may stamp all over the other timers if
//       it needs to process large quantities of data.
////////////////////////////////////////////////////////////////////////////////

procedure TForm1.Timer1Timer(Sender: TObject);
const flag1:boolean=false;
      flag2:boolean=false;
      flag3:integer=-1;
var buffer:str255;                     // (local) serial input buffer
       I64:int64;
       I,J:integer;
         S:string;
begin
// ******** FLASH CURSOR: 500ms on, 500ms off **********************************
  flag1:=((GetTickCount mod 1000)<500);
  if flag1<>flag2 then                             // change state
  begin
    CSR.Visible:=flag1 or (CSR.Tag=1);             // cursor always visible if it has moved
    CSR.Tag:=0;                                    // clear cursor movement flag
    flag2:=flag1
  end;

// ******** UPDATE TASKBAR TEXT ************************************************
  if CONNECTED<>flag3 then
  begin
    case CONNECTED of 0:begin
                          Form1.Caption:=BuildName+'   '+BuildDate;
                          Application.Title:='offline';
                          CommName:='';
                          CommRate:=0
                        end;
                      2:begin
                          Form1.Caption:=CommName+':'+
                                      IntToStr(CommRate)+
                                      '   '+BuildName+
                                      '   '+BuildDate;
                          Application.Title:=CommName+':'+IntToStr(CommRate)
                        end
    end;  { of case }
    flag3:=CONNECTED
  end;

// ******** UPDATE STATUS BAR and CURSOR POSITION ******************************
  Label1.Caption:=Format('%.2x  row=%.2d  col=%.2d  key=%.2x',
                         [ord(lastC), Ypos, Xpos, ord(lastK)]);

  I64:=timesince(TC1);
  S:=Format('%.10d', [I64]);
  if I64<1000  then Label2.Caption:=copy(S,8,3)+'ms' else
  if I64<60999 then Label2.Caption:=copy(S,6,2)+'.'+S[8]+'s' else
                    Label2.Caption:='>1min';
  Label2.Visible:=(CONNECTED>0);               // hide Rx timer if not connected

  I64:=timesince(TC2);
  S:=Format('%.10d', [I64]);
  if I64<1000  then Label3.Caption:=copy(S,8,3)+'ms' else
  if I64<60999 then Label3.Caption:=copy(S,6,2)+'.'+S[8]+'s' else
                    Label3.Caption:='>1min';
  Label3.Visible:=(CONNECTED>0);               // hide Tx timer if not connected

  I:=RB.head-RB.tail;
  if I<0 then I:=I+sizeof(RB.data);
  I:=min(99, (I*100) div sizeof(RB.data));
  Label4.Caption:=Format('%.2d%%', [I]);       // percentage of (64k) ring buffer used
  if I<10 then Label4.Color:=clSilver else
  if I<40 then Label4.Color:=clAqua else
  if I<70 then Label4.Color:=clYellow
          else Label4.Color:=clRed;

  Label5.Visible:=(CONNECTED>0);               // hide 'online' indicator if not connected
  Label6.Visible:=LOGTOFILE;                   // hide 'logging' indicator if not logging

  Label7.Caption:=Format('[%.6d]', [min(999999, max(PB.len-PB.idx, 0))]);
  if timesince(TC3)>500 then Label7.Color:=clLime
                        else Label7.Color:=clAqua;
  Label7.Visible:=(PB.idx<>0);

//Label8.Caption:='('+IntToStr(MouseX)+','+IntToStr(MouseY)+')';
  Label8.Caption:=Format('(%.2d,%.2d)', [min(max(MouseX, 1), COLS),
                                         min(max(MouseY, 1), ROWS)]);
//Label8.Visible:=(MouseX>=1) and (MouseX<=COLS) and (MouseY>=1) and (MouseY<=ROWS);

  CSR.Top :=Image1.Top+ (CSR.Height*(min(max(Ypos, 1), ROWS)-1));
  CSR.Left:=Image1.Left+(CSR.Width* (min(max(Xpos, 1), COLS)-1));

  if Form2.Visible then
  begin
    if not MouseReportXY then S:='' else
    case MouseProtocol of 0:S:='  X10';
                          1:S:='  SGR';
                          2:S:='  URXVT'
                       else S:='  ???'
    end;  { of case }
    Form2.Label1.Caption:=Format('FG=%.1d  BG=%.1d  TM=%.2d  BM=%.2d',
                                 [FGcolour,BGcolour,Tmargin, Bmargin])+S
  end;

// ******** READ FROM COMM PORT, placing characters into ring buffer ***********
  while (CONNECTED=2) and ReadComm(buffer) do
  begin
    for I:=1 to length(buffer) do
    begin
      J:=(RB.head+1) mod sizeof(RB.data);
      if J<>RB.tail then
      begin
        RB.data[RB.head]:=buffer[I];
        RB.head:=J
      end
    end
  end;

// ******** HANDLE BREAK (alt-B) SIGNALLING :SetCommBreak / ClearCommBreak *****
  if BreakCounter<>-1 then                         // BreakCounter is active
  begin
    if BreakCounter=0 then
      try SetCommBreak(CommFile) except end;       // set break
    Inc(BreakCounter, Timer1.Interval);

    if BreakCounter>200 then                       // then after 200mS
    begin
      try ClearCommBreak(CommFile) except end;     // reset break
      BreakCounter:=-1
    end
  end
end;


procedure TForm1.Timer2Timer(Sender: TObject);     // screen ticker
var I1, I2:integer;                                // 45ms = approx 1/20th sec.
    S1, S2:str255;                                 // fallthrough after 30ms
      mark:int64;
        ch:char;
         S:string;
begin
  if ErrorMsg<>'' then             // if there is an error message pending...
  begin
    S:=ErrorMsg;                   // save error message locally
    ErrorMsg:='';                  // clear message (to stop recursive calls)
//  ShowMessage(S);                // display local copy of error message
    MessageDlg(S, mtError, [mbOk], 0);
    exit
  end;

  mark:=GetTickCount;

// *** until a maximum elapsed of 30ms, process characters to screen ***
  while (timesince(mark)<30) and (RB.head<>RB.tail) do
  begin
    ch:=RB.data[RB.tail];
    RB.tail:=(RB.tail+1) mod sizeof(RB.data);

    S1:=VT100engine(ch);                           // first process throught the VT100 engine
    for I1:=1 to length(S1) do
    begin
      S2:=GFXengine(S1[I1]);                       // then process through the GFX engine
      for I2:=1 to length(S2) do
      if S2[I2]=#05 then WriteComm(#06)            // ENQ -> ACK
                    else emit(S2[I2])              // lastly print out the remainder
    end
  end
end;


// *** process paste operations, one character per loop through if in editor ***
procedure TForm1.Timer3Timer(Sender: TObject);     // paste ticker (variable)
const ch:char=#00;
var S:string;
    I:integer;
begin
  if (PB.idx=0) and (Timer3.Interval<>100) then Timer3.Interval:=100;
                                                   // idles at 100ms
  if (PB.idx<>0) and (CONNECTED=2) then
  begin
    if PB.idx=1 then TC3:=GetTickCount;            // force slow paste initially

    if timesince(TC3)>500 then                     // *** fast paste ***
    begin                                          // ==================
      if Timer3.Interval<>35 then Timer3.Interval:=35;

      if timesince(TC1)>30 then                    // 30ms silence between lines
      begin
        S:='';
        I:=PB.idx;
        repeat
          ch:=PB.str[I];
          S:=S+ch;
          inc(I)
        until (I>PB.len) or (ch=#13);              // grab a line up to and inc CR
        if WriteComm(S) then PB.idx:=I
      end
    end
    else                                           // *** slow paste ***
    begin                                          // ==================
      if (Timer3.Interval<>15) then Timer3.Interval:=15;

      if not((ch=#13) and (timesince(TC1)<300)) then
      begin                                        // 300ms since last char returned
        ch:=PB.str[PB.idx];
        if WriteComm(ch) then
        begin
          inc(PB.idx);
          if ch=#13 then Timer3.Interval:=330      // 330ms minimum delay after CR
        end
      end
    end;

    if (PB.idx>PB.len) then                        // have finished pasting
    begin
      PB.idx:=0;
      PB.len:=0;
      PB.str:='';
      ch:=#00
    end
  end
end;


procedure TForm1.Timer4Timer(Sender: TObject);
begin
  Timer4.Enabled:=false;
  if (CONNECTED=2) then CONNECTED:=1     // temporarily disables comms and signal for
end;                                     // other thread to check if still functional


////////////////////////////////////////////////////////////////////////////////
// Keyboard and Mouse Events
// =========================
////////////////////////////////////////////////////////////////////////////////

// *** handle normal ascii keys ***
procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
  if Memo1.Visible then exit;                  // ignore keys if text buffer visible

  lastK:=Key;
  if PB.idx=0 then case CONNECTED of 0:emit(Key);
                                     2:WriteComm(Key)
                   end  { of case }
end;


// *** handle special keys that need to be auto-repeating ***
procedure TForm1.WMKeyDown(var Msg:TWMKeyDown);
var S:string;
begin
  if Memo1.Visible then exit;                  // ignore keys if text buffer visible

  S:='';
  case Msg.CharCode of VK_LEFT:S:=#27+'[D';
                         VK_UP:S:=#27+'[A';
                      VK_RIGHT:S:=#27+'[C';
                       VK_DOWN:S:=#27+'[B';
                      VK_PRIOR:S:=#27+'[5~';
                       VK_NEXT:S:=#27+'[6~';
                       VK_HOME:S:=#27+'[1~';
                        VK_END:S:=#27+'[4~';
                     VK_INSERT:S:=#27+'[2~';
                     VK_DELETE:S:=#127         // convert delete key to chr(127)
  end;  { of case }

  if (CONNECTED=2) and (PB.idx=0) and (length(S)>0) then WriteComm(S)
end;


// *** handle non-repeating keys upon release ***
procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
var S:string;
begin
  if Memo1.Visible then                        // ignore keys if text buffer visible
  begin
    if (Key=13) or (Key=27) then               // except hide Memo1 on ESC or CR
    begin
      Form1.PopupMenu:=PopupMenu1;             // re-enable our popup menu

      Memo1.Lines.Clear;
      Memo1.Enabled:=false;                    // probably not needed
      Memo1.Visible:=false
    end;
    exit
  end;

// ssShift	The Shift key is held down.
// ssAlt	The Alt key is held down.
// ssCtrl	The Ctrl key is held down.

  S:='';                                       // default action: do nothing

// *** shifted and unshifted function keys ***
  if ssShift in Shift
  then case Key of VK_F3:S:=#27+'[25~';        // shifted function keys
                   VK_F4:S:=#27+'[26~';
                   VK_F5:S:=#27+'[28~';
                   VK_F6:S:=#27+'[29~';
                   VK_F7:S:=#27+'[31~';
                   VK_F8:S:=#27+'[32~';
                   VK_F9:S:=#27+'[33~';
                  VK_F10:S:=#27+'[34~'
       end  { of case }
  else case Key of VK_F1:S:=#27+'[11~';        // unshifted function keys
                   VK_F2:S:=#27+'[12~';
                   VK_F3:S:=#27+'[13~';
                   VK_F4:S:=#27+'[14~';
                   VK_F5:S:=#27+'[15~';
                   VK_F6:S:=#27+'[17~';
                   VK_F7:S:=#27+'[18~';
                   VK_F8:S:=#27+'[19~';
                   VK_F9:S:=#27+'[20~';
                  VK_F10:S:=#27+'[21~';
                  VK_F11:S:=#27+'[23~';
                  VK_F12:S:=#27+'[24~'
       end;  { of case }

// *** alt keys (letters and numbers), these are all 'special' commands ***
  if ssAlt in Shift then
  begin
    case Key of ord('B'):BreakCounter:=0;                      // try to reset micromite
                ord('P'):Item3Click(Item3A1);                  // (paste from clipboard)
                ord('C'):Item7Click(Item7A);
//                       begin
//                         GFXclear(0, 0, Gw, Gh);             // clear graphics layer
//                         Image2.Hide                         // hide graphics layer
//                       end;
                ord('D'):Item7Click(Item7B);
//                       clear(1, 1, COLS, ROWS);              // clear text layer
                ord('Z'):Item3Click(Item3B);
//                       begin                                 // cancel paste
//                         PB.idx:=0;
//                         PB.len:=0;
//                         PB.str:=''
//                       end;
                ord('A'):Item7Click(Item7C);
//                       RB.head:=RB.tail;                     // clear ring buffer
                ord(#13):begin
                           Form2.RichEdit1.Lines.Add('________________________');
                           Form2.RichEdit1.Lines.Add('')
                         end;
                ord('R'):begin
                           if LOGTOFILE then                   // close log file if open
                           begin
                             try CloseFile(LogFile) except end;
                             LOGTOFILE:=false
                           end;

                           RB.head:=RB.tail;                   // empty ring buffer

                           PB.idx:=0;
                           PB.len:=0;
                           PB.str:='';                         // empty paste buffer

                           DimText:=false;                     // reset text attributes
                           InvText:=false;
                           TxtStyle:=[];
                           FGcolour:=FGdefault;                // reset text colours
                           BGcolour:=BGdefault;

                           GFX.Brush.Color:=clBlack;           // default brush: clBlack
                           GFX.Pen.Color:=clRed;               // default pen: clRed
                           GFX.Pen.Width:=1;                   // default line width: 1

                           Tmargin:=1;                         // reset scroll margins
                           Bmargin:=ROWS;

                           MouseReportXY:=false;               // turn off mouse reporting
                           MouseProtocol:=0;

                           clear(1, 1, COLS, ROWS);            // clear text layer
                           GFXclear(0, 0, Gw, Gh);             // clear graphics layer
                           Image2.Hide;                        // hide graphics layer
                           gotoxy(1,1)                         // home cursor
                         end;
{
                ord('1'):begin                                 // alt-1
                           Cursor.Font.Name:='Terminal';
                           Item4Click(nil)
                         end;
                ord('2'):begin                                 // alt-2
                           Cursor.Font.Name:='Terminus';
                           Item4Click(nil)
                         end;
                ord('3'):begin                                 // alt-3
                           Cursor.Font.size:=Cursor.Font.size-1;
                           Cursor.AutoSize:=true;
                           Item4Click(nil)
                         end;
                ord('4'):begin                                 // alt-4
                           Cursor.Font.size:=Cursor.Font.size+1;
                           Cursor.AutoSize:=true;
                           Item4Click(nil)
                         end;
                ord('0'):begin
                           FontDialog1.Font:=Cursor.Font;
                           FontDialog1.Execute
//                         ShowMessage(Cursor.Font.Name+#13+
//                                     IntToStr(Cursor.Font.Size)+'pt')
                         end
}
    end  { of case }
  end;

//if Key=VK_Delete then S:=#127;               // convert delete key to chr$(127)
  if Key=VK_F10 then Key:=0;                   // stop f10 triggering menu

  if (CONNECTED=2) and (PB.idx=0) and (length(S)>0) then WriteComm(S)
end;


procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
// var row, col:integer;
begin
  if (CONNECTED=2) and MouseReportXY and (Button=mbLeft) and (PB.idx=0) then
  begin
    if sender=CSR then begin
                         MouseX:=min(Xpos, COLS);      // fix: can be =81 if at end of line
                         MouseY:=Ypos
                       end
                  else begin
                         MouseX:=trunc(((X-Image1.Left)/CSR.Width)+1);
                         MouseY:=trunc(((Y-Image1.Top)/CSR.Height)+1)
                       end;

//  ShowMessage(IntToStr(MouseX)+','+IntToStr(MouseY))
    if (MouseX>=1) and (MouseX<=COLS) and (MouseY>=1) and (MouseY<=ROWS) then
    case MouseProtocol of 0:WriteComm(#27+'[M '+chr(32+MouseX)+chr(32+MouseY));
                          1:WriteComm(#27+'[<0;'+IntToStr(MouseX)+';'+IntToStr(MouseY)+'M');
                          2:WriteComm(#27+'[32;'+IntToStr(MouseX)+';'+IntToStr(MouseY)+'M')
    end  { of case }
  end
end;


procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin
  if Sender=CSR   then begin
                         MouseX:=min(Xpos, COLS);      // fix: can be =81 if at end of line
                         MouseY:=Ypos
                       end
                  else begin
                         MouseX:=trunc(((X-Image1.Left)/CSR.Width)+1);
                         MouseY:=trunc(((Y-Image1.Top)/CSR.Height)+1)
                       end
end;


procedure TForm1.FormMouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
begin
  if (CONNECTED=2) { and mouse_reportXY } then         // always active, even if not reporting
  begin
    if WheelDelta>0 then WriteComm(#27+'[A');
    if WheelDelta<0 then WriteComm(#27+'[B')
  end
end;


////////////////////////////////////////////////////////////////////////////////
// startup (FormCreate) and shutdown (FormClose) code
// ==================================================
////////////////////////////////////////////////////////////////////////////////

procedure TForm1.FormCreate(Sender: TObject);
var Reg:TRegistry;
     SL:TstringList;
      S:string;
      I,n1,n2,n3:integer;
begin
//Application.HintHidePause:=5000;
  Form1.Caption:=BuildName;            // updated by Timer1
  Application.Title:='GFXterm';        // updated by Timer1

  Form1.AutoSize:=true;
//Form1.AutoScroll:=false;             // set in form view
//Form1.KeyPreview:=true;              // set in form view
  Form1.DoubleBuffered:=true;

  DefFont.Name:=Cursor.Font.Name;
  DefFont.CharSet:=Cursor.Font.CharSet;
  DefFont.Size:=Cursor.Font.Size;

  Reg:=TRegistry.Create;
  with Reg do
  begin
    RootKey:=HKEY_CURRENT_USER;
    try
      if OpenKey(SoftwareKey, false) then
      begin
        SL:=TStringList.Create;
        try                            // try to read font size from registry
          I:=ReadInteger('FontSize');
          if I in [9, 12, 14] then Cursor.Font.Size:=I else
          if I<0 then
          begin                        // manual font override (for Linux)
            SL.CommaText:=ReadString('FontInfo');
            if SL.Count=4 then         // exactly 4 parameters required
            begin
//            windows.beep(880,250);
              S:=SL[0];
              n1:=StrToInt(SL[1]);
              n2:=StrToInt(SL[2]);
              n3:=StrToInt(SL[3]);
              Cursor.Font.Name:=S;
              Cursor.Font.Charset:=n1;
              Cursor.Font.Size:=n2;
              Cursor.AutoSize:=false;
              Cursor.Width:=n3
            end
          end
        except end;
        SL.Free;

        try                            // try to read foreground colour from registry
          I:=ReadInteger('DefaultFG');
          if I in [1..7] then begin FGdefault:=I; FGcolour:=I end
        except end;

        try                            // try to read dim text option from registry
          I:=ReadInteger('DimOption');
          if I in [0..2] then DimOpt:=I
        except end
      end
    except end;
    try CloseKey except end
  end;
  Reg.Free;

// create text area
  Image1.Left:=0;                                      // horizontal position of screen grid
  Image1.Top:=Label1.Height+2;                         // vertical position of screen grid
  Image1.Picture.Bitmap:=TBitmap.Create;               // create a bitmap object for text
  Image1.Picture.Bitmap.Width:=Cursor.Width*COLS;      // set width
  Image1.Picture.Bitmap.Height:=Cursor.Height*ROWS;    // set height

  Image1.Picture.Bitmap.Canvas.TextFlags:=ETO_OPAQUE;  // opaque writing of text, improves speed
  Image1.Picture.Bitmap.Canvas.Font:=Cursor.Font;      // copy font details from cursor object
  Image1.Picture.Bitmap.Canvas.Font.Style:=[];         // no underline, etc.
  Image1.Picture.Bitmap.Canvas.Font.Color:=clWhite;    // WHITE text by default
  Image1.Picture.Bitmap.Canvas.Brush.Color:=clBlack;   // default brush: clBlack (for clearing screen)

// create graphics area
  Image2.Left:=Image1.Left;                            // graphics plane overlays text plane
  Image2.Top:=Image1.Top;
//Image2.Transparent:=true;                            // allow transparence (set in form view)
  Image2.Picture.Bitmap:=TBitmap.Create;               // create a bitmap object for graphics
  Image2.Picture.Bitmap.Width:=Cursor.Width*COLS;      // set width
  Image2.Picture.Bitmap.Height:=Cursor.Height*ROWS;    // set height

  Image2.Picture.Bitmap.Canvas.Brush.Color:=clBlack;   // default brush: clBlack (for clearing screen)
  Image2.Picture.Bitmap.Canvas.Pen.Color:=clRed;       // default pen: clRed (for drawing lines)
  Image2.Picture.Bitmap.Canvas.Pen.Width:=1;

  Image2.Picture.Bitmap.TransparentColor:=clBlack;     // transparent colour is black
  Image2.Picture.Bitmap.TransparentMode:=tmFixed;      // use above setting for TC

// configure cursor
  Cursor.Top:=Image1.Top;                              // initial cursor row
  Cursor.Left:=Image1.Left;                            // initial cursor column
  Cursor.Font.Color:=clRed;                            // cursor colour (FG)
  Cursor.Color:=clRed;                                 // cursor colour (BG)

// configure text copy object
  Memo1.Left:=Image1.Left;
  Memo1.Top:=Image1.Top;
(*                                                     // the following are handled
  Memo1.Width:=Cursor.Width*COLS;                      // within right-click menu's
  Memo1.Height:=Cursor.Height*ROWS;                    /  Item8

  Memo1.Font:=Cursor.Font;
  Memo1.Font.Style:=[];
  Memo1.Font.Color:=clBlack;           // black text on
  Memo1.Color:=clSilver;               // a silver background
*)
  Memo1.Hint:='Select the text you want to copy using the mouse,'+#13+
              'then press control-C to copy it to the clipboard.'+#13+
              'When finished, press ENTER to exit this view.';

  Label5.Color:=clLime;                // green 'running' annunciator by default
  Label6.Color:=clYellow;              // yellow 'logging' annunciator by default
  Label7.Color:=clAqua;                // blue paste annunciator by default

  OpenDialog1.InitialDir:=ExtractFilePath(ExpandFileName(paramstr(0)));
  SaveDialog1.InitialDir:=ExtractFilePath(ExpandFileName(paramstr(0)));

// align graphics and text layers along the Z-axis
  Memo1.Enabled:=false;                // probably not needed
  Memo1.Visible:=false;
  Memo1.SendToBack;
  Image1.Enabled:=false;               // ignore mouse and keyboard events
  Image2.Enabled:=false;               // ignore mouse and keyboard events
  Image1.BringToFront;                 // text layer ends in background
  Image2.BringToFront;                 // graphics layer overlays text
  Cursor.BringToFront;                 // cursor always on top of text and graphics

// create shortcuts
  CSR:=Cursor;                         // CSR is shorthand for the cursor object
  SCR:=Image1.Picture.Bitmap.Canvas;   // shorthand for canvas object (text screen)
  GFX:=Image2.Picture.Bitmap.Canvas;   // shorthand for canvas object (graphics screen)

  clear(1, 1, COLS, ROWS);             // clear text layer
  GFXclear(0, 0, Gw, Gh);              // clear graphics layer
  gotoxy(1,1);                         // home cursor
  Image2.Hide;                         // graphics layer is turned off initially

  TC1:=GetTickCount;
  TC2:=TC1;
  TC3:=TC2;

  TUSBcheckThread.Create(false)
end;


procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if (CONNECTED>0) then try
                          CONNECTED:=0;
                          CloseHandle(CommFile)
                        except end;
  if LOGTOFILE then try
                      LOGTOFILE:=false;
                      CloseFile(Logfile)
                    except end;
  sleep(200)
end;


//------------------------------------------------------------------------------
// start: unused launch application code
//------------------------------------------------------------------------------
(*
procedure CaptureConsoleOutput(const ACommand, AParameters: string;
                               var ExitCode: integer);
var saSecurity:TSecurityAttributes;
    suiStartup:TStartupInfo;
     piProcess:TProcessInformation;
    ReadBuffer:array [0..127] of char;
     BytesRead,
    BytesAvail:DWORD;
       Running:DWORD;
           I,J:integer;
             S:string;
            ch:char;
            LB:LongBool;
 begin
   ExitCode:=-($DEAD);         // default exit code if unable to complete

   FillChar(saSecurity, SizeOf(TSecurityAttributes), #0);
   saSecurity.nLength:=SizeOf(TSecurityAttributes);
   saSecurity.bInheritHandle:=true;            // seems to be essential for pipes
   saSecurity.lpSecurityDescriptor:=nil;

   if CreatePipe(hRead_local, hWrite_remote, @saSecurity, 0) then
   if CreatePipe(hRead_remote, hWrite_local, @saSecurity, 0) then     { try 1 byte write }
   begin
     FillChar(suiStartup, SizeOf(TStartupInfo), #0);
     suiStartup.cb:=SizeOf(TStartupInfo);
     suiStartup.hStdInput:=hRead_remote;
     suiStartup.hStdOutput:=hWrite_remote;
     suiStartup.hStdError:=hWrite_remote;
     suiStartup.dwFlags:=STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
     suiStartup.wShowWindow:=SW_HIDE;
{
   dwXCountChars, dwYCountChars
dwFlags specifies STARTF_USECOUNTCHARS.
For console processes,
if a new console window is created,
dwXCountChars specifies the screen buffer
width in character columns, and dwYCountChars
specifies the screen buffer height in character rows.
These values are ignored in GUI processes.  }

     saSecurity.bInheritHandle:=false;         // standard handles are still inherited

     if CreateProcess(nil, PChar(ACommand + ' ' + AParameters),
                      @saSecurity, @saSecurity,
//                    nil, nil,
                      True, NORMAL_PRIORITY_CLASS,
                      nil, nil, suiStartup, piProcess) then
     begin
       CONNECTED:=true;
// ***************************************
//     try CloseHandle(hWrite) except ShowMessage('unable to close hWrite') end;
// ***************************************

       repeat
         Running:=WaitForSingleObject(piProcess.hProcess, 20);
//       Application.ProcessMessages();
         repeat
           Application.ProcessMessages();
           try
             if PeekNamedPipe(hRead_local, nil, 0, nil, @BytesAvail, nil) and (BytesAvail>0)
                then LB:=ReadFile(hRead_local, ReadBuffer[0],
                                  min(sizeof(ReadBuffer), BytesAvail), BytesRead, nil)
                else LB:=false
           except
             LB:=false
           end;       // may exit with ERROR_BROKEN_PIPE after child process closes

           if not LB then BytesRead:=0 else
           begin
             for I:=1 to BytesRead do
             begin
               ch:=ReadBuffer[I-1];
               S:=VT100engine(ch);
               for J:=1 to length(S) do emit(S[J])
             end
           end;
         until (BytesRead<sizeof(ReadBuffer)) or KILL
       until (Running<>WAIT_TIMEOUT) or KILL;

       CONNECTED:=false;

       GetExitCodeProcess(piProcess.hProcess, DWORD(ExitCode));  // returns STILL_ACTIVE if still running
       if KILL then TerminateProcess(piProcess.hProcess, 0);

       try CloseHandle(piProcess.hProcess) except end;
       try CloseHandle(piProcess.hThread) except end
     end;

     try CloseHandle(hRead_local) except end;
     try CloseHandle(hWrite_local) except end;
     try CloseHandle(hRead_remote) except end;
     try CloseHandle(hWrite_remote) except end

   end;
   DEAD:=true
end;


procedure RUNprogram;
var I:integer;
    S:string;
begin
  if not CONNECTED then
  if inputquery('RUN', 'enter program to launch', S) then
  begin
    DEAD:=false;
    KILL:=false;
    emit(#13);
    emit(#10);
    CaptureConsoleOutput(S,'', I);
    emit(#13);
    emit(#10)
  end
end;


procedure STOPprogram;
begin
  KILL:=true
end;
*)
//------------------------------------------------------------------------------
// end: unused launch application code
//------------------------------------------------------------------------------


////////////////////////////////////////////////////////////////////////////////
// popup menu item handlers
// ========================
////////////////////////////////////////////////////////////////////////////////

// *** popup menu - configure menu before making visible ***********************
procedure TForm1.PopupMenu1Popup(Sender: TObject);
begin
  Item1A.Visible:=(CONNECTED=0);
  Item1B.Visible:=(CONNECTED>0);
  Item1B.Enabled:=(CONNECTED<>1);
  Item2A.Visible:=not LOGTOFILE;
  Item2B.Visible:=LOGTOFILE;
  Item3A.Visible:=(PB.idx=0);
  Item3B.Visible:=(PB.idx<>0);
//if (CSR.Font.Name<>'Terminal') or
//   (CSR.Font.Charset<>OEM_CHARSET) then Item4D.Checked:=true else
  if CSR.AutoSize then case CSR.Font.Size of  9:Item4A.Checked:=true;
                                             12:Item4B.Checked:=true;
                                             14:Item4C.Checked:=true
                       end  { of case }
                  else begin
                         Item4A.Checked:=false;
                         Item4B.Checked:=false;
                         Item4C.Checked:=false
                       end;
//Item4D.Visible:=(GetAsyncKeyState(VK_SHIFT)<0) or not CSR.AutoSize;

  case FGdefault of 1:Item5A.Checked:=true;
                    2:Item5B.Checked:=true;
                    3:Item5C.Checked:=true;
                    4:Item5D.Checked:=true;
                    5:Item5E.Checked:=true;
                    6:Item5F.Checked:=true;
                    7:Item5G.Checked:=true
  end;  { of case }
  case DimOpt of 0:Item6A.Checked:=true;
                 1:Item6B.Checked:=true;
                 2:Item6C.Checked:=true
  end  { of case }
end;


// popup menu item - CONNECT / DISCONNECT
procedure TForm1.Item1Click(Sender: TObject);
var name,config:string;
          OS,ok:boolean;
              I:integer;
              S:string;
            Reg:TRegistry;
begin
  OS:=(GetAsyncKeyState(VK_SHIFT)<0);

  S:='';
  Reg:=TRegistry.Create;       // try to read S from registry
  with Reg do
  begin
    RootKey:=HKEY_CURRENT_USER;
    try
      if OpenKey(SoftwareKey, false) then S:=ReadString('CommPort');
    except end;
    try CloseKey; except end
  end;
  Reg.Free;

  if (CONNECTED=2) then begin
                          CONNECTED:=0;
                          try CloseHandle(CommFile) except end
                        end
                   else repeat
                          if OS then ok:=InputQuery('CONNECT', 'enter <port>:<baud>', S)   // old style
                                else ok:=GetPortString(S);                                 // new style

                          if ok then
                          begin
                            I:=pos(':',S);

                            if I<>0 then
                            begin
                              name:=copy(S,1,I-1);
                              config:='baud='+copy(S,I+1,length(S)-I)+' parity=n data=8 stop=1';
                              SetupCommPort(name, config)
                            end
                          end;

                          if (CONNECTED=2) then        // save S into registry
                          begin
                            Reg:=TRegistry.Create;
                            with Reg do
                            begin
                              RootKey:=HKEY_CURRENT_USER;
                              try
                                if OpenKey(SoftwareKey,  true) then WriteString('CommPort', S)
                              except end;
                              try CloseKey except end
                            end;
                            Reg.Free
                          end
                        until (CONNECTED=2) or not ok
end;


// popup menu item - LOG to file / STOP logging
procedure TForm1.Item2Click(Sender: TObject);
begin
  if LOGTOFILE then begin
                      try CloseFile(LogFile) except end;
                      LOGTOFILE:=false
                    end
               else begin
                      if SaveDialog1.Execute then
                      try
                        AssignFile(LogFile, SaveDialog1.Filename);
                        ReWrite(LogFile);
                        LOGTOFILE:=true
                      except
                        try CloseFile(Logfile) except end;
                        LOGTOFILE:=false
                      end
                    end
end;


// popup menu item - paste from clipboard / paste from text file / CANCEL paste
procedure TForm1.Item3Click(Sender: TObject);
var SL:TStringList;
     S:string;
     I:integer;
begin
// setup for paste from clipboard
  if Sender=Item3A1 then begin
                           if ClipBoard.HasFormat(CF_TEXT) then
                           try
                             S:=ClipBoard.AsText;
                           except
                             S:=''
                           end
                         end;

// setup for paste from text file
  if Sender=Item3A2 then begin
                           if OpenDialog1.Execute then
                           begin
                             SL:=TStringlist.Create;
                             try
                               SL.LoadFromFile(OpenDialog1.Filename);
                               S:=SL.Text
                             except
                               S:=''
                             end;
                             SL.Free
                           end
                         end;

// common code for both types of paste (from clipboard and from text file)
  if (Sender=Item3A1) or (Sender=Item3A2) then
  begin
    I:=pos(#13#10, S);
    while I<>0 do
    begin                                // translate all CR-LF pairs into single CR
      delete(S, I+1, 1);
      I:=pos(#13#10, S)
    end;

    for I:=1 to length(S) do             // translate any remaining LFs into CRs
        if S[I]=#10 then S[I]:=#13;

    for I:=length(S) downto 1 do         // remove any other control characters
        if S[I] in [#0..#12,#14..#31] then delete(S, I, 1);

    I:=pos(#32#13, S);
    while I<>0 do
    begin                                // remove all trailing spaces at ends of lines
      delete(S, I, 1);
      I:=pos(#32#13, S)
    end;

    while (length(S)<>0) and (S[length(S)]=#32) do delete(S, length(S), 1);
                                         // remove final trailing spaces at end of S
    if length(S)<>0 then
    begin
      PB.str:=S;                         // load data into string ready to be streamed
      PB.len:=length(S);                 // out of the comm port by a timer interrupt
      PB.idx:=1
    end
  end;

// code for CANCEL paste
  if Sender=Item3B then begin            // cancel paste
                          PB.idx:=0;
                          PB.len:=0;
                          PB.str:=''
                        end
end;


// popup menu item - font size 9pt / 12pt / 14pt
procedure TForm1.Item4Click(Sender: TObject);
var Reg:TRegistry;
    I,J:integer;
    OEM:boolean;
      S:string;
begin
  if Sender=Item4A then begin         // select 9pt Terminal
                          CSR.Font.Name:=DefFont.Name;
                          CSR.Font.Charset:=DefFont.CharSet;
                          CSR.Font.Size:=9;
                          CSR.AutoSize:=true
                        end else
  if Sender=Item4B then begin        // select 12pt Terminal
                          CSR.Font.Name:=DefFont.Name;
                          CSR.Font.Charset:=DefFont.CharSet;
                          CSR.Font.Size:=12;
                          CSR.AutoSize:=true
                        end else
  if Sender=Item4C then begin        // select 14pt Terminal
                          CSR.Font.Name:=DefFont.Name;
                          CSR.Font.Charset:=DefFont.CharSet;
                          CSR.Font.Size:=14;
                          CSR.AutoSize:=true
                        end else
  if Sender=Item4D then begin
                          FontDialog1.Font:=CSR.Font;
                          S:=IntToStr(CSR.Width);
                          if not FontDialog1.Execute then exit;
                          if not InputQuery('CELL WIDTH', 'enter cell width (0=auto, or 8..32)', S) then exit;

                          case MessageDlg('force codepage 437 (OEM/DOS) ?',
                                          mtConfirmation, [mbYes, mbNo], 0)
                              of mrYes:OEM:=true;
                                  mrNo:OEM:=false
                              else     exit
                          end;  { of case }

                          S:=trim(S);
                          try
                            I:=abs(StrToInt(S));
//                          OEM:=(S[1]='-');
                            if I in [0, 8..32] then
                            begin
                              CSR.AutoSize:=true;
                              CSR.Font.Name:=FontDialog1.Font.Name;
                              if OEM then CSR.Font.Charset:=OEM_CHARSET
                                     else CSR.Font.Charset:=FontDialog1.Font.Charset;
                              CSR.Font.Size:=FontDialog1.Font.Size;
                              if I=0 then begin
                                            S:=Cursor.Caption;         // save cursor character
                                            for J:=32 to 126 do        // (space to tilde)
                                            begin
                                              Cursor.Caption:=chr(J);
                                              if J<>ord('@') then I:=max(I, Cursor.Width)
                                            end;                       // exclude @ symbol
                                            Cursor.Caption:=S          // restore cursor character
                                          end;
                              CSR.AutoSize:=false;
                              CSR.Width:=max(I, 8)     // must be at least 8 pixels wide
                            end
                          except exit end
                        end else
  if sender<>nil then exit;

  Image1.Picture.Bitmap.Width:=CSR.Width*COLS;    // set width
  Image1.Picture.Bitmap.Height:=CSR.Height*ROWS;  // set height

  Image2.Picture.Bitmap.Width:=CSR.Width*COLS;    // set width
  Image2.Picture.Bitmap.Height:=CSR.Height*ROWS;  // set height

  SCR.Font:=CSR.Font;                              // copy font details from cursor

  clear(1, 1, COLS, ROWS);                         // clear text layer
  GFXclear(0, 0, Gw, Gh);                          // clear graphics layer
  gotoxy(1,1);                                     // home cursor

  Reg:=TRegistry.Create;
  if Sender<>nil then
  with Reg do                          // save font size into registry
  begin
    RootKey:=HKEY_CURRENT_USER;
    try
      if OpenKey(SoftwareKey, true) then
      begin
//      WriteString('FontName', CSR.Font.Name);
//      WriteInteger('FontCSet', CSR.Font.Charset);
        if Sender=Item4D then begin
                                WriteInteger('FontSize', -1);
                                WriteString('FontInfo', '"'+CSR.Font.Name+'", '+
                                                        IntToStr(CSR.Font.Charset)+', '+
                                                        IntToStr(CSR.Font.Size)+', '+
                                                        IntToStr(CSR.Width))
                              end
                         else begin
                                WriteInteger('FontSize', CSR.Font.Size);
                                WriteString('FontInfo', 'auto')
                              end
      end
    except end;
    try CloseKey except end
  end;
  Reg.Free
end;


// popup menu item - select default text colour
procedure TForm1.Item5Click(Sender: TObject);
var Reg:TRegistry;
     TC:integer;
begin
  if Sender=Item5A then TC:=1 else
  if Sender=Item5B then TC:=2 else
  if Sender=Item5C then TC:=3 else
  if Sender=Item5D then TC:=4 else
  if Sender=Item5E then TC:=5 else
  if Sender=Item5F then TC:=6 else
  if Sender=Item5G then TC:=7
                   else TC:=-1;
  if TC<>-1 then
  begin
    FGdefault:=TC;
    FGcolour:=TC;

    Reg:=TRegistry.Create;
    with Reg do                        // save default FG colour into registry
    begin
      RootKey:=HKEY_CURRENT_USER;
      try
        if OpenKey(SoftwareKey, true) then WriteInteger('DefaultFG', FGdefault)
      except end;
      try CloseKey except end
    end;
    Reg.Free
  end
end;


// popup menu item - select how dim attribute is handled (8 vs 16 colour mode)
procedure TForm1.Item6Click(Sender: TObject);
var Reg:TRegistry;
begin
  if Sender=Item6A then DimOpt:=0 else                 // dim attribute enabled
  if Sender=Item6B then DimOpt:=1 else                 // force bright 1
  if Sender=Item6C then DimOpt:=2;                     // force bright 2

  Reg:=TRegistry.Create;
  with Reg do                          // save font size into registry
  begin
    RootKey:=HKEY_CURRENT_USER;
    try
      if OpenKey(SoftwareKey, true) then WriteInteger('DimOption', DimOpt)
    except end;
    try CloseKey except end
  end;
  Reg.Free
end;


// popup menu item - clear GFX plane / text plane / ring buffer
procedure TForm1.Item7Click(Sender: TObject);
begin
  if Sender=Item7A then begin
                          GFXclear(0, 0, Gw, Gh);      // clear graphics layer
                          Image2.Hide                  // hide graphics layer
                        end else
  if Sender=Item7B then clear(1, 1, COLS, ROWS) else   // clear text layer
  if Sender=Item7C then RB.head:=RB.tail               // clear ring buffer
end;


// popup menu item - show copy memo
procedure TForm1.Item8Click(Sender: TObject);
var X, Y:integer;
       S:string;
      TR:TRect;
begin
{
  if FGcolour=0 then Memo1.Font.Color:=CVT[15]
                else Memo1.Font.Color:=CVT[FGcolour or 8];
  Memo1.Color:=CVT[BGcolour];
}
  Memo1.Width:=Cursor.Width*COLS;
  Memo1.Height:=Cursor.Height*ROWS;

  Memo1.Font:=Cursor.Font;
  Memo1.Font.Style:=[];
  Memo1.Font.Color:=clBlack;                   // black text on a
  Memo1.Color:=clSilver;                       // silver background

  Memo1.Lines.Clear;                           // ensure memo is completely blank

  for Y:=1 to ROWS do                          // fill in memo text
  begin
    S:=StringOfChar(' ', COLS);                // blank line of 80 chars
    for X:=1 to COLS do
    begin
      S[X]:=TextStore[Y, X];                   // copy text from buffer (TextStore)
      if S[X]<' ' then S[X]:=' '               // turn control chars into whitespace
    end;
    S:=TrimRight(S);
    Memo1.Lines.Add(S)                         // transfer completed line into memo
  end;

  S:=Memo1.Text;
  S:=copy(S, 1, length(S)-2);                  // remove final CR-LF from memo
  Memo1.Text:=S;

  TR:=Memo1.ClientRect;                                        // get rid of
  SendMessage(Memo1.Handle, EM_SETRECT, 0, integer(@TR));      // those pesky
  Memo1.Invalidate;                                            // margins!!!

  Form1.PopupMenu:=nil;                        // disable our popup menu
                                               // (cut/copy menu takes over)
  Memo1.Enabled:=true;
  Memo1.Visible:=true
end;


// popup menu item - make command window visible
procedure TForm1.Item9Click(Sender: TObject);
begin
  Form2.Visible:=true;
  Form2.BringToFront
end;



end.


