-- ChatImpl.mesa
-- Stolen from Laurel Chat.mesa
-- Larry Stewart, April 6, 1983 6:43 pm
-- Warren Teitelman, November 3, 1982 5:53 pm
-- Last Edited by: Maxwell, January 25, 1983 3:09 pm

DIRECTORY
 Ascii USING [LF],
 Commander USING [CommandProc, Register],
  ConvertUnsafe USING [AppendRope],
  FileIO USING [Open, OpenFailed],
  Inline USING [BITAND],
  IO,
  List USING [Length, Remove],
  Menus USING [AppendMenuEntry, CreateEntry, FindEntry, MenuEntry, MenuProc, ReplaceMenuEntry],
  PupDefs USING [PupAddress, PupPackageMake, PupPackageDestroy],
  PupStream USING [GetPupAddress, PupByteStreamCreate, PupNameTrouble, SecondsToTocks, StreamClosing],
  PupTypes USING [telnetSoc],
  Rope USING [Cat, Fetch, Find, Length, ROPE, Substr],
  Runtime USING [BoundsFault, GetBcdTime],
  Stream USING [CompletionCode, GetBlock, Handle, InputOptions, PutByte, PutChar, SendNow, SetInputOptions, SetSST, SubSequenceType, TimeOut],
  TiogaOps USING [GetCaret, GetRope, Location],
  TIPUser USING [InstantiateNewTIPTable, RegisterTIPPredicate, TIPPredicate, TIPTable],
  TypeScript USING [ChangeLooks, Create, InsertCharAtFrontOfBuffer, TS],
  UECP USING [Argv, Parse],
  UserCredentials USING [GetUserCredentials],
  ViewerClasses,
  ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc],
  ViewerIO USING [CreateViewerStreams],
  ViewerOps USING [AddProp, DestroyViewer, FetchProp, PaintViewer],
  ViewerTools USING [GetSelectedViewer, GetSelectionContents, SetSelection];

ChatImpl: CEDAR MONITOR LOCKS h.LOCK USING h: Handle
IMPORTS Commander, ConvertUnsafe, FileIO, Inline, IO, List, Menus, PupDefs, PupStream,
 Rope, Runtime, Stream, TiogaOps, TIPUser,
 TypeScript, UECP, UserCredentials,
 ViewerEvents, ViewerIO, ViewerOps, ViewerTools
SHARES Menus, ViewerClasses =
BEGIN

Handle: TYPE = REF ChatInstanceRecord;
State: TYPE = {idle, starting, running, closing, destroy};

DisconnectChar: CHAR = 220C;
AbortChar: CHAR = 221C;
ConnectChar: CHAR = 222C;
LoginChar: CHAR = 223C;
RemoteCloseChar: CHAR = 224C;
setLineWidth: Stream.SubSequenceType = 2;
setPageLength: Stream.SubSequenceType = 3;
timingMark: Stream.SubSequenceType = 5;
timingMarkReply: Stream.SubSequenceType = 6;

ChatInstanceRecord: TYPE = MONITORED RECORD [
 ts: TypeScript.TS, -- the primary typescript
 state: State ← idle,
 lorc: CHAR ← 'c,
 logFileName: Rope.ROPE,
 logStream: IO.STREAM,
 keyFile: Rope.ROPE,
 argv: UECP.Argv,
 pleaseStop: BOOL ← FALSE,
 uToSStopped: BOOL ← FALSE,
 sToUStopped: BOOL ← FALSE,
 inDestroy: BOOL ← FALSE,
 serverToUserProcess: PROCESS,
 userToServerProcess: PROCESS,
 serverName: Rope.ROPE ← "Ivy",
 useOldHost: BOOL ← FALSE,
 keyStream: IO.STREAM,
 destroyOnClose: BOOL ← FALSE,
 setSelection: BOOL ← FALSE,
 in: IO.STREAM,
 origOut: IO.STREAM,
 out: IO.STREAM,
 tipTable: TIPUser.TIPTable,
 serverStream: Stream.Handle ← NIL,
 oldSplit: Menus.MenuEntry ← NIL
 ];

logFileNumber: INT ← 0;
chatInstanceList: LIST OF REF ANY ← NIL;
destroyEvent: ViewerEvents.EventRegistration ← NIL;
closeEvent: ViewerEvents.EventRegistration ← NIL;

-- This procedure is FORKed by the UserToServer process at the time a
-- connection is opened. It is JOINED whenever the connection is closed.
-- It also goes away if the Chat viewer is destroyed.

ServerToUser: PROC [h: Handle] = TRUSTED {{
 buffer: STRING ← [400];
 why: Stream.CompletionCode;
 mySST: Stream.SubSequenceType;
 DO
  ENABLE {
   ABORTED => GOTO Cleanup;
   PupStream.StreamClosing => {
    IF NOT h.pleaseStop THEN TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: RemoteCloseChar];
    GOTO Cleanup;
    };
   IO.Error => GOTO Cleanup;
   };
  buffer.length ← 0;
  [buffer.length, why, mySST] ← Stream.GetBlock[h.serverStream, [@buffer.text, 0, buffer.maxlength] ! Stream.TimeOut => {
   IF h.pleaseStop OR h.state # running THEN GOTO Cleanup
   ELSE RESUME;
   }];
  IF h.pleaseStop OR h.state # running THEN GOTO Cleanup;
  IF h.out.UserAbort[] THEN ERROR;
  FOR i: NAT IN [0.. buffer.length) DO
   IF buffer[i]#Ascii.LF THEN h.out.PutChar[Inline.BITAND[buffer[i], 177B]];
   ENDLOOP;
  IF why = sstChange AND mySST = timingMark
   THEN Stream.SetSST[h.serverStream, timingMarkReply];
  ENDLOOP;
 EXITS
 Cleanup => h.sToUStopped ← TRUE;
 };
 };

InitialNegotiations: PROC [h: Handle] = TRUSTED {
 Stream.SetSST[h.serverStream, setLineWidth];
 Stream.PutByte[h.serverStream, 0];
 --Stream.SetSST[h.serverStream, setPageLength];
 --Stream.PutByte[h.serverStream, 255]; kludge to minimize BELLS from Juniper
 IF h.lorc='l OR h.lorc='x THEN {
  name, password: Rope.ROPE;
  [name: name, password: password] ← UserCredentials.GetUserCredentials[];
  SendStringToServer[h, "Login "];
  SendStringToServer[h, name];
  IF h.lorc='l AND Rope.Find[s1: name, s2: "."] = -1 THEN {
   SendStringToServer[h, ".PA"];
   };
  SendStringToServer[h, " "];
  SendStringToServer[h, password];
  SendStringToServer[h, " \n"];
  };
 };

 SendStringToServer: PROC[h: Handle, s: Rope.ROPE] = TRUSTED {
  FOR i: INT IN [0 .. s.Length[]) DO
   Stream.PutChar[h.serverStream, s.Fetch[i]];
   ENDLOOP;
  Stream.SendNow[h.serverStream];
  };

 FinishStringWithErrorMsg: PROC [h: Handle, errorMsg: STRING] = {
  IF errorMsg # NIL THEN h.out.PutF[": %s.\n", IO.string[errorMsg]]
  ELSE h.out.PutF[".\n"];
  };

-- UserAbort is active because Chat.TIP is not activated until h.state = running (due to
-- the TIP predicate), so control-DEL sets UserAbort.
-- However, while in this routine, UserAbort is used only to stop taking characters from
-- the keyStream, not to close the connection. If the user types control-DEL here, she
-- will have to type it again to really close the connection.

 FromKeys: PROC [h: Handle] = TRUSTED {
  count: NAT ← 0;
  {
  WHILE h.keyStream # NIL AND ~h.keyStream.EndOf[] DO
   IF h.in.UserAbort[] OR h.pleaseStop OR h.state # starting THEN GOTO CloseKeyStream;
   Stream.PutChar[h.serverStream, h.keyStream.GetChar[]];
   count ← count + 1;
   IF count >= 50 THEN {
    Stream.SendNow[h.serverStream];
    count ← 0;
    };
   ENDLOOP;
  GOTO CloseKeyStream;
  EXITS
  CloseKeyStream => {
   IF h.in.UserAbort[] THEN h.in.ResetUserAbort[];
   IF h.keyStream # NIL THEN{
    h.keyStream.Close[];
    h.keyStream ← NIL;
    };
   IF count # 0 THEN Stream.SendNow[h.serverStream];
   };
  };
  };

 StartUp: PROC [h: Handle] = {{
  ENABLE UNWIND => h.state ← idle;
  h.state ← starting;
  IF NOT h.useOldHost OR h.serverName.Length[] = 0 THEN h.serverName ← FindHostName[h];
  h.useOldHost ← FALSE;
  h.pleaseStop ← FALSE;
  h.sToUStopped ← FALSE;
  h.uToSStopped ← FALSE;
  IF h.setSelection THEN ViewerTools.SetSelection[h.ts, NIL];
  h.setSelection ← FALSE;
  h.in.ResetUserAbort[];
  h.out.PutF["\nViewers Chat of %t.\n", IO.time[Runtime.GetBcdTime[]]];
  IF h.logStream # NIL THEN h.out.PutF["Log file: %g\n", IO.rope[h.logFileName]]
  ELSE h.out.PutF["No log file.\n"];
  OpenConnection[h];
  IF h.serverStream = NIL THEN {
   h.state ← idle;
   RETURN;
   };
  SetName[h, Rope.Cat["Chat ", h.serverName]];
  InitialNegotiations[h];
  FromKeys[h];
  h.state ← running;
  h.serverToUserProcess ← FORK ServerToUser[h];
  };
  };

 FindHostName: PROC [h: Handle] RETURNS [host: Rope.ROPE] = {
  caret: TiogaOps.Location;
  r: Rope.ROPE;
  i: INT;
  r ← ViewerTools.GetSelectionContents[];
  IF r.Length[] > 1 THEN RETURN[r];
  caret ← TiogaOps.GetCaret[];
  r ← TiogaOps.GetRope[caret.node];
  i ← caret.where;
  WHILE i > 0 DO
   char: CHARACTER = Rope.Fetch[r, i - 1];
   IF -- char # '* AND -- NOT ChatTokenProc[char] = other THEN EXIT;
   i ← i -1;
   ENDLOOP;
  host ← Rope.Substr[base: r, start: i, len: caret.where - i];
  };

 -- This procedure includes '+ in order to handle Pup names of the form
 -- dls+100004
 ChatTokenProc: IO.BreakProc -- [char: CHAR] RETURNS [IO.CharClass] -- = TRUSTED {
  IF IO.TokenProc[char] = other THEN RETURN [other];
  IF char = '+ THEN RETURN [other];
  RETURN [sepr];
  };

 OpenConnection: PROC [h: Handle] = TRUSTED {
  connect: STRING ← [64];
  addr: PupDefs.PupAddress;
  PupDefs.PupPackageMake[];
  {
  connect.length ← 0;
  h.out.ResetUserAbort[];
  ConvertUnsafe.AppendRope[to: connect, from: h.serverName ! Runtime.BoundsFault => {
   h.out.PutF[" ... name too long!"];
   GOTO Return;
   } ];
  h.out.PutF["Opening connection to %g ... ", IO.rope[h.serverName]];
  
  addr.socket ← PupTypes.telnetSoc; -- default value
  
  PupStream.GetPupAddress
   [@addr, connect
    ! PupStream.PupNameTrouble => {
     h.out.ResetUserAbort[];
     h.out.PutF["PUP name error"];
     FinishStringWithErrorMsg[h, e];
     GOTO Return;
     }];
  h.serverStream ← PupStream.PupByteStreamCreate
   [addr, PupStream.SecondsToTocks[1]
    ! PupStream.StreamClosing => {
     h.out.ResetUserAbort[];
     h.out.PutF["Can't connect"];
     FinishStringWithErrorMsg[h, text];
     GOTO Return;
     }];
  Stream.SetInputOptions[h.serverStream, Stream.InputOptions[TRUE,FALSE,FALSE,FALSE,FALSE]];
  h.out.PutF["open.\n"];
  EXITS
  Return => NULL;
  };
  };

 CloseConnection: PROC [h: Handle, print: BOOL] = TRUSTED {
  h.pleaseStop ← TRUE;
  IF h.serverStream # NIL THEN {
   IF print THEN h.out.PutF["\nClosing connection to %s", IO.rope[h.serverName] ! IO.Error => CONTINUE];
   h.serverStream.delete[h.serverStream];
   h.serverStream ← NIL; -- could cause pointer faults!
   IF print THEN h.out.PutF[" ... Closed\n" ! IO.Error => CONTINUE];
   PupDefs.PupPackageDestroy[];
   };
  IF h.keyStream # NIL THEN {
   h.keyStream.Close[];
   h.keyStream ← NIL;
   };
  IF h.state = running THEN JOIN h.serverToUserProcess;
  IF h.logStream # NIL THEN h.logStream.Flush[];
  h.state ← idle;
  SetName[h, "Chat"];
  };

SetName: PROC [h: Handle, r: Rope.ROPE] = {
 InternalSetName: PROC [v: ViewerClasses.Viewer] = {
  v.name ← r;
  ViewerOps.PaintViewer[viewer: v, hint: caption];
  };
 EnumerateSplits[h.ts, InternalSetName ! ANY => CONTINUE];
 };

ChatMain: Commander.CommandProc = TRUSTED {
 h: Handle ← NEW[ChatInstanceRecord ← []];
 chatTipTable: TIPUser.TIPTable ← TIPUser.InstantiateNewTIPTable["Chat.TIP"];
 execOut: IO.STREAM ← cmd.out;
 switchChar: CHAR;
 i: NAT ← 2;

 h.argv ← UECP.Parse[cmd.commandLine];
 h.lorc ← 'l; -- default GV login
 WHILE i < h.argv.argc DO
  IF h.argv[i].Length[] > 1 THEN
   SELECT h.argv[i].Fetch[0] FROM
    '- => {
     switchChar ← h.argv[i].Fetch[1];
     SELECT switchChar FROM
      'd => h.destroyOnClose ← TRUE;
      'k => {
       IF i+1 < h.argv.argc THEN {
        h.keyStream ← IO.RIS[h.argv[i+1]];
        i ← i + 1;
        };
       };
      ENDCASE => h.lorc ← switchChar;
     };
    '> => h.logFileName ← Rope.Substr[base: h.argv[i], start: 1];
    '< => h.keyFile ← Rope.Substr[base: h.argv[i], start: 1];
    ENDCASE => execOut.PutF["chat: unknown command: %s.\n", IO.rope[h.argv[i]]];
  i ← i + 1;
  ENDLOOP;
 IF h.logFileName.Length[] = 0 THEN {
  h.logFileName ← IO.PutFR["Chat%d.log", IO.int[logFileNumber]];
  logFileNumber ← logFileNumber + 1;
  };

 IF h.keyFile.Length[] > 0 THEN h.keyStream ← FileIO.Open[fileName: h.keyFile ! FileIO.OpenFailed => {
  execOut.PutF["Chat:"];
  IO.PutSignal[execOut];
  execOut.PutF[", %s\n", IO.rope[h.keyFile]];
  CONTINUE;
  }];
 -- iconic unless command line not empty
 h.ts ← TypeScript.Create[info: [name: "Chat", iconic: h.argv.argc <= 1 OR h.lorc = 'i], paint: TRUE];
 TypeScript.ChangeLooks[h.ts, 'f];
 h.ts.file ← h.logFileName;
 h.ts.icon ← typescript;
 -- create log file
 h.logStream ← FileIO.Open[fileName: h.logFileName, accessOptions: overwrite, raw: TRUE ! FileIO.OpenFailed => {
  execOut.PutF["Chat:"];
  IO.PutSignal[execOut];
  execOut.PutF[", %s\n", IO.rope[h.logFileName]];
  CONTINUE;
  }];

 -- plug in Chat TIP table.
 chatTipTable.link ← h.ts.tipTable;
 chatTipTable.opaque ← FALSE;
 h.tipTable ← h.ts.tipTable ← chatTipTable;
 [in: h.in, out: h.origOut] ← ViewerIO.CreateViewerStreams[name: "Chat.log", viewer: h.ts, editedStream: FALSE];
 h.out ← h.origOut;
 IF h.logStream # NIL THEN h.out ← IO.CreateDribbleStream[stream: h.origOut, dribbleTo: h.logStream, flushEveryNChars: 256];
 [] ← IO.SetEcho[h.in, NIL];
 IF h.argv.argc > 1 THEN {
  h.serverName ← h.argv[1];
  h.useOldHost ← TRUE;
  -- IF -i then we are just initializing the host name, else really connect
  IF h.lorc = 'i THEN h.lorc ← 'c
  ELSE TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: IF h.lorc = 'c THEN ConnectChar ELSE LoginChar];
  };
 IF List.Length[chatInstanceList] = 0 THEN {
  -- closeEvent ← ViewerEvents.RegisterEventProc[proc: MyClose, event: close];
  destroyEvent ← ViewerEvents.RegisterEventProc[proc: MyDestroy, event: destroy]
  };
 chatInstanceList ← CONS[h, chatInstanceList];
 Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "Disconnect", proc: MyDisconnect, clientData: h, fork: TRUE, documentation: "Close Ethernet connection"]];
 Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "Login", proc: MyLogin, clientData: h, documentation: "Open Ethernet Connection to selected host and send Login sequence"]];
 Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "Connect", proc: MyConnect, clientData: h, documentation: "Open Ethernet Connection to selected host"]];
 Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "BreakKey", proc: MyBreakKey, clientData: h, documentation: "Transmit Ascii.NULL (DLS interprets as RS-232 Line Break)"]];
 Menus.AppendMenuEntry[menu: h.ts.menu, entry: Menus.CreateEntry[name: "FlushLog", proc: MyFlushLog, clientData: h, documentation: "Flush log file to disk."]];

 h.oldSplit ← Menus.FindEntry[menu: h.ts.menu, entryName: "Split"];
 Menus.ReplaceMenuEntry[menu: h.ts.menu, oldEntry: h.oldSplit, newEntry: Menus.CreateEntry[name: "Split", proc: MySplit, fork: TRUE, clientData: h, documentation: "Split window"]];
 ViewerOps.AddProp[h.ts, $ChatToolData, h];
 ViewerOps.PaintViewer[viewer: h.ts, hint: all];
 h.userToServerProcess ← FORK UserToServer[h];
 };

 UserToServer: PROC [h: Handle] = TRUSTED {{
  char: CHAR;
  count: NAT ← 0;
  DO ENABLE {
    ABORTED => ERROR; -- ever happen?
    IO.UserAborted => {
     h.out.ResetUserAbort[];
     h.out.PutF["\nUserAbort!\n"];
     CONTINUE;
     };
    IO.Error => {
     -- last viewer destroyed !
     -- Close connection and exit
     CloseConnection[h, FALSE];
     GOTO Die;
     };
    PupStream.StreamClosing => {
     IF h.serverStream # NIL THEN {
      h.out.PutF["\nConnection being closed by %s", IO.rope[h.serverName]];
      FinishStringWithErrorMsg[h, text];
      CloseConnection[h, FALSE];
      IF h.destroyOnClose THEN {
       ViewerOps.DestroyViewer[h.ts];
       GOTO Die;
       };
      };
     CONTINUE;
     };
    };
   char ← h.in.GetChar[];
   IF h.inDestroy THEN {
    CloseConnection[h, FALSE];
    GOTO Die;
    };
   SELECT char FROM
    AbortChar => {
     IF h.state = running THEN CloseConnection[h, TRUE];
     h.out.ResetUserAbort[];
     };
    DisconnectChar => {
     IF h.state = running THEN CloseConnection[h, TRUE];
     };
    RemoteCloseChar => {
     ERROR PupStream.StreamClosing[why: remoteClose, text: NIL];
     };
    ConnectChar => {
     IF h.state = idle THEN {
      h.lorc ← 'c;
      StartUp[h];
      count ← 0;
      };
     };
    LoginChar => {
     IF h.state = idle THEN {
      h.lorc ← 'l;
      StartUp[h];
      count ← 0;
      };
     };
    ENDCASE => {
     SELECT h.state FROM
      running => {
       Stream.PutChar[h.serverStream, char];
       IF count >= 50 OR NOT h.in.CharsAvail[] THEN {
        Stream.SendNow[h.serverStream];
        count ← 0;
        }
       ELSE count ← count + 1;
       };
      ENDCASE => h.out.PutChar[char];
     };
   ENDLOOP;
  EXITS
  Die => {
   IF h.logStream # NIL THEN h.logStream.Close[];
   };
  };
  };

 -- This EventProc exits only to keep h.ts pointing to a valid copy of the typescript
 -- viewer. It is only needed for the use of TypeScript.InsertCharAtFrontOfBuffer.

 MyDestroy: ViewerEvents.EventProc = {
  h: Handle ← NARROW[ViewerOps.FetchProp[viewer, $ChatToolData]];
  IF h = NIL THEN RETURN;
  IF NumSplit[viewer] = 1 THEN { -- last one
   h.inDestroy ← TRUE;
   -- next line is a crock to avoid signal in TypeScripts, see McGregor
   -- Process.Pause[Process.SecondsToTicks[2]];
   -- h.out.Close[]; breaks viewers destroy!!
   chatInstanceList ← List.Remove[h, chatInstanceList];
   IF List.Length[chatInstanceList] = 0 THEN {
    ViewerEvents.UnRegisterEventProc[proc: destroyEvent, event: destroy];
    -- ViewerEvents.UnRegisterEventProc[proc: closeEvent, event: close];
    };
   }
  ELSE IF NumSplit[viewer] > 1 THEN {
   IF viewer = h.ts THEN {
     Another: PROC [v: ViewerClasses.Viewer] = {
      IF v # viewer THEN h.ts ← v;
      };
     EnumerateSplits[viewer, Another];
     };
   };
  };
  
 MyClose: ViewerEvents.EventProc = {
  h: Handle ← NARROW[ViewerOps.FetchProp[viewer, $ChatToolData]];
  IF h = NIL THEN RETURN;
  TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: DisconnectChar];
  };
  
 MyConnect: Menus.MenuProc = {
  viewer: TypeScript.TS ← NARROW[parent];
  h: Handle ← NARROW[clientData];
  h.ts ← viewer; -- "primary" copy
  h.useOldHost ← mouseButton # red;
  TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: ConnectChar];
  };

 MyLogin: Menus.MenuProc = {
  viewer: TypeScript.TS ← NARROW[parent];
  h: Handle ← NARROW[clientData];
  h.ts ← viewer; -- "primary" copy
  h.useOldHost ← mouseButton # red;
  TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: LoginChar];
  };
  
 MyDisconnect: Menus.MenuProc = {
  h: Handle ← NARROW[clientData];
  TypeScript.InsertCharAtFrontOfBuffer[ts: h.ts, char: DisconnectChar];
  };

 MyBreakKey: Menus.MenuProc = TRUSTED {
  h: Handle ← NARROW[clientData];
  IF h.state # running THEN RETURN;
  IF h.serverStream # NIL THEN {
   Stream.PutChar[h.serverStream, '\000];
   Stream.SendNow[h.serverStream];
   };
  };

 MyFlushLog: Menus.MenuProc = {
  h: Handle ← NARROW[clientData];
  IF h.logStream # NIL THEN h.logStream.Flush[];
  };

MySplit: Menus.MenuProc = {
  h: Handle ← NARROW[clientData];
  CheckChatProperties: PROC [v: ViewerClasses.Viewer] = {
   IF ViewerOps.FetchProp[v, $ChatToolData] = NIL THEN ViewerOps.AddProp[v, $ChatToolData, h];
   v.tipTable ← h.tipTable;
   };
  h.oldSplit.proc[parent: parent, clientData: h.oldSplit.clientData, mouseButton: mouseButton, shift: shift, control: control];
  EnumerateSplits[NARROW[parent, ViewerClasses.Viewer], CheckChatProperties];
  };

ConnectionOpen: TIPUser.TIPPredicate --PROC RETURNS [BOOLEAN]-- = {
 h: Handle;
 viewer: ViewerClasses.Viewer ← ViewerTools.GetSelectedViewer[];
 IF viewer=NIL THEN RETURN [FALSE]; -- no primary selection
 h ← NARROW[ViewerOps.FetchProp[viewer, $ChatToolData]];
 IF h=NIL THEN RETURN [FALSE]; -- not a chat tool
 RETURN [h.state = running]; -- connection open?
 };

EnumerateSplits: PROC [v: ViewerClasses.Viewer, p: PROC [v: ViewerClasses.Viewer]] = {
 v2: ViewerClasses.Viewer ← v;
 IF v = NIL THEN RETURN;
 DO
  p[v2];
  IF v2.link = NIL OR v2.link = v THEN RETURN;
  v2 ← v2.link;
  ENDLOOP;
 };

NumSplit: PROC [v: ViewerClasses.Viewer] RETURNS [count: INT ← 0] = {
 Counter: PROC [v2: ViewerClasses.Viewer] = {
  count ← count + 1;
  };
 EnumerateSplits[v, Counter];
 };

Init: PROC = {
 Commander.Register[key: "Chat", proc: ChatMain, doc: "Pup User Telnet, see Chat.doc"];
 TIPUser.RegisterTIPPredicate[$ConnectionOpen, ConnectionOpen];
 };

-- main program for chat

Init[];

END.

March 31, 1982 9:27 pm, Stewart, copied from Laurel Chat
April 1, 1982 4:06 pm, Stewart, own viewer class & TIP table
April 4, 1982 5:18 pm, Stewart, Menu
April 6, 1982 10:27 pm, Stewart, command line stuff
18-Apr-82 17:42:24, Use TypeScript again
June 6, 1982 4:44 pm, Stewart, fix Menu instantiation to add Split
June 6, 1982 8:08 pm, Stewart, using own Split code until viewers copies PropList
September 21, 1982 4:26 pm, Stewart, Cedar 3.4
January 23, 1983 10:18 pm, Stewart, Cedar 3.5, major rework
January 24, 1983 5:49 pm, Stewart, Cedar 3.6