-- 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