Chat.mesa
Larry Stewart, January 6, 1984 1:28 pm
Warren Teitelman, November 3, 1982 5:53 pm
Last Edited by: Maxwell, January 25, 1983 3:09 pm
Last Edited by: Paul Rovner, November 29, 1983 10:56 am
Last Edited by: Pavel Curtis on February 26, 1985 2:59:48 pm PST
Last Edited by: Bill Jackson on November 1, 1984 5:46:27 pm PST
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;
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] =
BEGIN
c: CHAR;
mySST: MarkByte;
DO
ENABLE
{
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
};
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"];
};
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.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];
};
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 {
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"];
};
};
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;
}];
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 ←
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;
}];
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 ← 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 -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];
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 => {
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];
};
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[];
};
};
};
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 {
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];
};
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
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.