<> <> <> <> <> <> <> DIRECTORY Ascii, Basics USING [BITAND], Commander USING [CommandProc, Lookup, Register], CommandExtras USING [MakeUninterpreted], CommandTool USING [ArgumentVector, Failed, Parse], EditedStream USING [SetEcho], FS USING [Error, StreamOpen], Graphics USING [Context, DrawBox, SetPaintMode], GraphicsBasic, IO, IOClasses USING [CreateDribbleOutputStream], List USING [Length, Remove], Loader USING [BCDBuildTime], Menus USING [AppendMenuEntry, CreateEntry, FindEntry, MenuEntry, MenuProc, ReplaceMenuEntry], Process USING [Detach, MsecToTicks, Pause], PupDefs USING [PupAddress, PupPackageMake, PupPackageDestroy], PupStream USING [ConsumeMark, GetPupAddress, PupByteStreamCreate, PupNameTrouble, SecondsToTocks, SendMark, StreamClosing, TimeOut], PupTypes USING [telnetSoc], Rope USING [Cat, Fetch, Find, Length, ROPE, Substr], TiogaOps USING [GetCaret, GetRope, Location], TIPUser USING [InstantiateNewTIPTable, RegisterTIPPredicate, TIPPredicate, TIPTable], TypeScript USING [BackSpace, ChangeLooks, Create, InsertCharAtFrontOfBuffer, TS], UserCredentials USING [Get], ViewerClasses, ViewerEvents USING [EventProc, EventRegistration, RegisterEventProc, UnRegisterEventProc], ViewerIO USING [CreateViewerStreams], ViewerOps USING [AcquireContext, AddProp, DestroyViewer, FetchProp, PaintViewer, ReleaseContext, XYWH2Box], ViewerTools USING [GetSelectedViewer, GetSelectionContents, SetSelection]; Chat: CEDAR MONITOR LOCKS h.LOCK USING h: Handle IMPORTS Basics, Commander, CommandExtras, CommandTool, EditedStream, FS, Graphics, IO, IOClasses, List, Loader, Menus, Process, PupDefs, PupStream, Rope, TiogaOps, TIPUser, TypeScript, UserCredentials, ViewerEvents, ViewerIO, ViewerOps, ViewerTools SHARES Menus, ViewerClasses, ViewerOps = 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; MarkByte: TYPE = [0..256); setLineWidth: MarkByte = 2; setPageLength: MarkByte = 3; timingMark: MarkByte = 5; timingMarkReply: MarkByte = 6; ChatInstanceRecord: TYPE = MONITORED RECORD [ ts: TypeScript.TS, -- the primary typescript state: State _ idle, lorc: CHAR _ 'c, logFileName: Rope.ROPE, logStream: IO.STREAM, debugging: BOOL _ FALSE, rawLogFileName: Rope.ROPE, rawLogStream: IO.STREAM, keyFile: Rope.ROPE, argv: CommandTool.ArgumentVector, 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, synchronous: BOOL _ FALSE, in: IO.STREAM, origOut: IO.STREAM, out: IO.STREAM, tipTable: TIPUser.TIPTable, serverStream: IO.STREAM _ NIL, oldSplit: Menus.MenuEntry _ NIL ]; logFileNumber: INT _ 0; chatInstanceList: LIST OF REF ANY _ NIL; destroyEvent: ViewerEvents.EventRegistration _ NIL; closeEvent: ViewerEvents.EventRegistration _ NIL; chatTipTable: TIPUser.TIPTable; <> <> <> ServerToUser: PROC [h: Handle] = BEGIN c: CHAR; mySST: MarkByte; 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; }; IF h.serverStream.EndOf[] THEN { mySST _ PupStream.ConsumeMark[h.serverStream]; IF mySST = timingMark THEN PupStream.SendMark[h.serverStream, timingMarkReply]; }; c _ h.serverStream.GetChar[! PupStream.TimeOut => { IF h.pleaseStop OR h.state # running THEN GOTO Cleanup ELSE RESUME; }; IO.EndOfStream => { LOOP; } ]; IF h.pleaseStop OR h.state # running THEN GOTO Cleanup; IF h.debugging THEN { h.rawLogStream.PutChar[c]; h.rawLogStream.Flush[]; }; c _ LOOPHOLE[Basics.BITAND[LOOPHOLE[c, CARDINAL], 177B], CHAR]; SELECT c FROM Ascii.BEL => Flash[h]; Ascii.ControlA, Ascii.BS => TypeScript.BackSpace[h.ts]; Ascii.TAB, IN[Ascii.SP..0176C] => h.out.PutChar[c]; Ascii.LF => h.out.PutChar[Ascii.CR]; ENDCASE => NULL; ENDLOOP; EXITS Cleanup => h.sToUStopped _ TRUE; END; Flash: PROC [h: Handle] = { context: Graphics.Context _ ViewerOps.AcquireContext[h.ts, IF h.ts.iconic THEN FALSE ELSE h.ts.column = color]; [] _ Graphics.SetPaintMode[context, invert]; Graphics.DrawBox[context, ViewerOps.XYWH2Box[0, 0, h.ts.ww, h.ts.wh]]; Process.Pause[Process.MsecToTicks[100]]; Graphics.DrawBox[context, ViewerOps.XYWH2Box[0, 0, h.ts.ww, h.ts.wh]]; ViewerOps.ReleaseContext[context]; }; InitialNegotiations: PROC [h: Handle] = TRUSTED { PupStream.SendMark[h.serverStream, setLineWidth]; h.serverStream.PutChar[0C]; h.serverStream.Flush[]; IF h.lorc='l OR h.lorc='x THEN { name, password: Rope.ROPE; [name: name, password: password] _ UserCredentials.Get[]; h.serverStream.PutRope["Login "]; h.serverStream.PutRope[name]; IF h.lorc='l AND Rope.Find[s1: name, s2: "."] = -1 THEN h.serverStream.PutRope[".PA"]; h.serverStream.PutRope[" "]; h.serverStream.PutRope[password]; h.serverStream.PutRope[" \n"]; h.serverStream.Flush[]; }; }; FinishStringWithErrorMsg: PROC [h: Handle, errorMsg: Rope.ROPE] = { IF errorMsg # NIL THEN h.out.PutF[": %s.\n", IO.rope[errorMsg]] ELSE h.out.PutF[".\n"]; }; <> <> <> <> <> FromKeys: PROC [h: Handle] = TRUSTED { count: NAT _ 0; { WHILE h.keyStream # NIL AND ~h.keyStream.EndOf[] DO IF h.pleaseStop OR h.state # starting THEN GOTO CloseKeyStream; h.serverStream.PutChar[h.keyStream.GetChar[]]; count _ count + 1; IF count >= 50 THEN { h.serverStream.Flush[]; count _ 0; }; ENDLOOP; GOTO CloseKeyStream; EXITS CloseKeyStream => { IF h.keyStream # NIL THEN{ h.keyStream.Close[]; h.keyStream _ NIL; }; IF count # 0 THEN h.serverStream.Flush[]; }; }; }; StartUp: PROC [h: Handle] = BEGIN 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; ViewerTools.SetSelection[h.ts, NIL]; TRUSTED { h.out.PutF["\nViewers Chat of %t.\n", IO.time[Loader.BCDBuildTime[FindHostName]]]; }; 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]; END; 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]; }; <> <> 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 { addr: PupDefs.PupAddress; PupDefs.PupPackageMake[]; { h.out.PutF["Opening connection to %g ... ", IO.rope[h.serverName]]; addr _ PupStream.GetPupAddress[PupTypes.telnetSoc, h.serverName ! PupStream.PupNameTrouble => { h.out.PutF["PUP name error"]; FinishStringWithErrorMsg[h, e]; GOTO Return; } ]; h.serverStream _ PupStream.PupByteStreamCreate[addr, PupStream.SecondsToTocks[1] ! PupStream.StreamClosing => { h.out.PutF["Can't connect"]; FinishStringWithErrorMsg[h, text]; GOTO Return; } ]; 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.Close[]; 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 _ []]; execOut: IO.STREAM _ cmd.out; switchChar: CHAR; i: NAT _ 2; h.argv _ CommandTool.Parse[cmd ! CommandTool.Failed => {msg _ errorMsg; CONTINUE; }]; IF h.argv = NIL THEN RETURN; 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; }; }; 's => h.synchronous _ TRUE; 'D => { h.debugging _ TRUE; h.rawLogFileName _ IO.PutFR["Chat%d.rawLog", IO.int[logFileNumber]]; h.rawLogStream _ FS.StreamOpen[fileName: h.rawLogFileName, accessOptions: $create ! FS.Error => IF error.group = user THEN { execOut.PutF["Chat: Cannot open raw log %s\n", IO.rope[h.rawLogFileName]]; CONTINUE; } ]; }; 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 _ FS.StreamOpen[fileName: h.keyFile ! FS.Error => IF error.group = user THEN { execOut.PutF["Chat: Cannot open %s\n", IO.rope[h.keyFile]]; CONTINUE; }]; <> 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; <> h.logStream _ FS.StreamOpen[fileName: h.logFileName, accessOptions: $create ! FS.Error => IF error.group = user THEN { execOut.PutF["Chat: Cannot open %s\n", IO.rope[h.logFileName]]; CONTINUE; }]; <> 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 _ IOClasses.CreateDribbleOutputStream[output1: h.origOut, output2: h.logStream]; IF h.debugging THEN h.out.PutF["Debugging Mode On ... raw log on %g\n", IO.rope[h.rawLogFileName]]; EditedStream.SetEcho[h.in, NIL]; IF h.argv.argc > 1 THEN { h.serverName _ h.argv[1]; h.useOldHost _ TRUE; <> 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 { <> 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]; IF h.synchronous THEN UserToServer[h] ELSE { h.userToServerProcess _ FORK UserToServer[h]; Process.Detach[h.userToServerProcess]; }; }; UserToServer: PROC [h: Handle] = TRUSTED {{ char: CHAR; count: NAT _ 0; DO ENABLE { ABORTED => ERROR; -- ever happen? IO.Error => { <> <> 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]; }; 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 => { h.serverStream.PutChar[char]; IF count >= 50 OR h.in.CharsAvail[] = 0 THEN { h.serverStream.Flush[]; count _ 0; } ELSE count _ count + 1; }; ENDCASE => h.out.PutChar[char]; }; ENDLOOP; EXITS Die => { IF h.logStream # NIL THEN h.logStream.Close[]; IF h.debugging THEN h.rawLogStream.Close[]; }; }; }; <> <> 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; <> <> <> chatInstanceList _ List.Remove[h, chatInstanceList]; IF List.Length[chatInstanceList] = 0 THEN { ViewerEvents.UnRegisterEventProc[proc: destroyEvent, event: destroy]; <> }; } 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 { h.serverStream.PutChar['\000]; h.serverStream.Flush[]; }; }; 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 ChatDoc.tioga"]; CommandExtras.MakeUninterpreted[Commander.Lookup["Chat"]]; chatTipTable _ TIPUser.InstantiateNewTIPTable["Chat.TIP"]; TIPUser.RegisterTIPPredicate[$ConnectionOpen, ConnectionOpen]; }; <
> 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 November 3, 1982 5:53 pm, Warren Teitelman January 23, 1983 10:18 pm, Stewart, Cedar 3.5, major rework January 24, 1983 5:49 pm, Stewart, Cedar 3.6 January 25, 1983 3:09 pm, Maxwell April 6, 1983 6:43 pm, Larry Stewart June 13, 1983 1:41 pm, Stewart, Cedar 4.2, added synchronous feature September 7, 1983 6:19 pm, Stewart, Cedar 5 January 6, 1984 1:13 pm, Stewart, TypeScript.BackSpace on Control-A and BS March 4, 1984 4:15:26 pm PST, Pavel, Cleanup, flash on Control-G, grab input focus on start up and connection. March 11, 1984 5:59:56 pm PST, Pavel, Map received back-quotes (140C) into normal single quotes. March 12, 1984 1:14:30 pm PST, Pavel, Gacha font changed to add a back-quote, so previous hack removed. February 26, 1985 2:50:10 pm PST, Pavel, Added -D switch for making a raw log of characters received from the server. Also changed it to make a newline on receipt of LF instead of CR.