WalnutKeyboardImpl.mesa
Copyright Ó 1988, 1989, 1990 by Xerox Corporation. All rights reserved.
Swinehart, December 19, 1990 5:36 pm PST
Rick Beach, April 29, 1989 2:16:13 pm PDT
Notes:
1. Think about replacing some of the notorious global variables with something more reasonable.
2. Next requirement is for the most minimal form of editor. In some sense, the existing Rope commands with some aka's should do it.
3. Find out how to quote things in the command tool.
4. New syntax for commands:
Message set syntax:
<set> :=
<message set name> | <database nickname>.<message set name> | it | them
It and them are pronouns representing the search rule used in the last completed command.
Arguments:
source: from <set> (from is default if nothing specified)
destination: to <set>
messages:
message: <number>
range: [<number> .. <number>]
list: [<number>, <number>, ...., <number>]
filter: with <arbitrary text> | unread | read
Filter must be the last item in the command line. The remainder of the command line, after "with ", is matched in a case-insensitive way against each entire TOC entry in the indicated message set. Only TOC entries (or messages, depending on the command) matching both the message range and the filter will participate in the command. The read and unread options permit focus of attention on just new or just old mail.
Commands:
WKDBs -- lists the available Walnut databases
WKSets <db name> -- lists message sets for the named dbs, default default
WKNew -- pushes NewMail button for any open DB that has one.
WKSelect <source>|<destination> -- sets default value
WKListTOC <source> <messages> (parameters may be in any order)
WKSameTOC <source> <messages> (Does not refresh cache first! See below.)
WKDisplay <source> <message>
WKMove <source> <messages> <destination> (parameters may be in any order)
WKCopy <source> <messages> <destination> (parameters may be in any order)
WKTransfer <source> <messages> <destination> (databases may (should) differ)
WKDelete <source> <messages> (WalnutOps says "Deleted" is not a legal destination for Move or copy)
WKLogin (system will prompt for names, passwords; legal only when idle.)
WKFlush -- Dumps all message set caches.
Pragmata:
To save time, the program caches information about message sets. Only the WKListTOC command will refresh the caches, although all of the message-specific commands will generate one if it doesn't yet exist. In particular, commands that move messages from one set to another do not use the Walnut version features (so that many can happen sequentially), and do not change the user-visible numbering of messages within either message set.
WKListToc will refresh the cache of the named message set if the underlying message set has changed. WKSameToc will not. (Useful while performing a series of moves, copies, or deletes).
DIRECTORY
Commander USING [CommandProc, Handle, Register],
CommandTool USING [NextArgument],
Convert USING [ IntFromRope ],
Idle USING [ IsIdle ],
IO,
List USING [Length],
Process USING [ Detach, SecondsToTicks, SetTimeout ],
RuntimeError USING [ UNCAUGHT ],
Rope USING [Cat, Concat, Equal, Fetch, Find, Length, ROPE, Substr],
SymTab USING [ Create, EachPairAction, Fetch, Ref, Pairs, Store ],
UserProfile USING [ Token ],
ViewerClasses USING [ Menu, MenuEntry, Viewer ],
ViewerOps USING [ EnumerateViewers, EnumProc ],
ViewerTools USING [ TiogaContents ],
WalnutDefs USING [dontCareMsgSetVersion ],
WalnutOps USING [AddMsg, CreateMsg, dontCareDomainVersion, GetHandleForRootfile, GetMsg, MoveMsg, MsgExists, MsgSetExists, MsgSetNames, GetDisplayProps, GetMsgText, MsgSetVersion, RemoveMsg, SetHasBeenRead, SizeOfMsgSet, MsgsInSetEnumeration, WalnutOpsHandle ],
WalnutNewMail USING [ CheckMailBoxes ],
WalnutWindowPrivate USING [ WalnutHandle ]
;
WalnutKeyboardImpl: CEDAR MONITOR
IMPORTS Commander, CommandTool, Convert, Idle, IO, List, Process, Rope, RuntimeError, SymTab, UserProfile, ViewerOps, WalnutOps, WalnutNewMail
SHARES ViewerClasses ~ {
OPEN IO;
ROPE: TYPE = Rope.ROPE;
Handle: TYPE = WalnutOps.WalnutOpsHandle;
DB: TYPE = REF DBBody;
DBBody: TYPE = RECORD [
dbName: ROPE,
dbFileName: ROPE,
handle: Handle ← NIL,
retryCount: INT ← 0
];
Entries: TYPE ~ REF EntrySequence;
EntrySequence: TYPE ~ RECORD[e: SEQUENCE len: NAT OF EntryRecord];
EntryRecord: TYPE ~ RECORD [
msgID: ROPE, toc: ROPE, tocValid: BOOL, hasBeenRead: BOOL, deleted: BOOLFALSE];
MsgSet: TYPE = REF MsgSetBody;
MsgSetBody: TYPE = RECORD [
db: DB, -- belongs to this database.
msgSetName: ROPE,
msgSetVersion: WalnutOps.MsgSetVersion,
entries: Entries
];
NewFilter: TYPE = {none, read, unread};
dbs: SymTab.Ref; -- Indexed by db short name (e.g., "default", "voice", "old")
msgSets: SymTab.Ref; -- Cached indices, indexed by dbname.msgSetName
defaultSource: MsgSet;
defaultDest: MsgSet;
thisSource: MsgSet;
thisDest: MsgSet;
firstMessageNumber: INT𡤁
lastMessageNumber: INT𡤁
thisSourceFilter: ROPENIL;
thisSourceNewFilter: NewFilter←$none;
disableIdleReqt: BOOLFALSE;
checkMinutes: INT ← 10;
ListDBsCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
ListOneDB: SymTab.EachPairAction = {
[key: ROPE, val: SymTab.Val] RETURNS [quit: BOOL ← FALSE]
db: DB ← NARROW[val];
cmd.out.PutF[" %g: %g\n",
rope[db.dbName],
rope[db.dbFileName]];
};
[]←SymTab.Pairs[dbs, ListOneDB];
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
ListMsgSetsCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
msgSetNames: LIST OF ROPE;
[result, msg] ← ParseArguments[cmd: cmd, dbOnly: TRUE];
IF result = $Failed THEN RETURN;
msgSetNames ← WalnutOps.MsgSetNames[thisSource.db.handle].mL;
cmd.out.PutF["Message sets from database %g\n", rope[thisSource.db.dbName]];
FOR mN: LIST OF ROPE ← msgSetNames, mN.rest WHILE mN#NIL DO
cmd.out.PutF[" %g\n", rope[mN.first]]; ENDLOOP;
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
SelectMsgSetCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
[result, msg] ← ParseArguments[cmd];
IF result = $Failed THEN RETURN;
defaultSource ← thisSource;
defaultDest ← thisDest;
cmd.out.PutF["Selected message sets: Source = %g, Dest = %g\n", rope[FullName[defaultSource]], rope[FullName[defaultDest]]];
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
ListMsgsInMsgSetCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
handle: Handle;
refresh: BOOL ← cmd.procData.clientData#$Again;
[result, msg, handle] ← ParseRange[cmd: cmd, refresh: refresh, fullRange: TRUE];
cmd.out.PutF["Messages in message set %g:\n", rope[FullName[thisSource]]];
FOR i: INT IN [firstMessageNumber..MIN[lastMessageNumber, thisSource.entries.len]] DO
hasBeenRead, deleted: BOOL;
tocEntry: ROPE;
[hasBeenRead, deleted, tocEntry] ← GetTocEntry[thisSource, i];
IF FilterOut[i] THEN LOOP;
cmd.out.PutF["%g %3d %g%g\n",
char[IF hasBeenRead THEN '\040 ELSE '?], int[i],
rope[tocEntry], rope[IF deleted THEN " (deleted)" ELSE ""]];
ENDLOOP;
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
PrintMsgInMsgSetCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
contents: REF TEXT;
handle: Handle;
cr: ROPE;
[result, msg, handle] ← ParseRange[cmd: cmd, refresh: FALSE, fullRange: TRUE];
IF result = $Failed THEN RETURN;
FOR i: INT IN [firstMessageNumber..MIN[lastMessageNumber, thisSource.entries.len]] DO
hasBeenRead, deleted: BOOL;
tocEntry, msgID: ROPE;
[hasBeenRead, deleted, tocEntry] ← GetTocEntry[thisSource, i];
IF FilterOut[i] THEN LOOP;
msgID ← thisSource.entries[i-1].msgID;
contents ← WalnutOps.GetMsgText[handle, msgID, contents];
TRUSTED { cr ← LOOPHOLE[contents]; };
cmd.out.PutF["-- Message %g:%g\n%g\n\n",
int[i], rope[IF deleted THEN " (deleted)" ELSE ""], rope[cr]];
IF NOT thisSource.entries[i-1].hasBeenRead THEN {
WalnutOps.SetHasBeenRead[handle, msgID];
thisSource.entries[i-1].hasBeenRead ← TRUE;
};
ENDLOOP;
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
MoveMsgCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
handle: Handle;
op: ROPE ← "Moved ";
destExists: BOOL;
move: BOOL ← cmd.procData.clientData = $Move;
[result, msg, handle] ← ParseRange[cmd: cmd, refresh: FALSE, fullRange: FALSE];
IF result = $Failed THEN RETURN;
destExists ← WalnutOps.MsgSetExists[opsHandle: handle, name: thisDest.msgSetName, msDomainVersion: WalnutOps.dontCareDomainVersion].exists;
IF ~destExists THEN RETURN[$Failed, "No destination message set"];
FOR i: INT IN [firstMessageNumber..MIN[lastMessageNumber, thisSource.entries.len]] DO
IF FilterOut[i] THEN LOOP;
IF thisSource.entries[i-1].deleted THEN {
cmd.out.PutF["** Can't move message %g from set %g (deleted)\n",
int[i], rope[FullName[thisSource]]];
LOOP;
};
IF move THEN {
thisSource.entries[i-1].deleted ← TRUE;
[]←WalnutOps.MoveMsg[handle,
thisSource.entries[i-1].msgID, [thisSource.msgSetName], [thisDest.msgSetName]];
}
ELSE {
op ← "Copied ";
[]←WalnutOps.AddMsg[handle,
thisSource.entries[i-1].msgID, [thisSource.msgSetName], [thisDest.msgSetName]];
};
cmd.out.PutF["%gmessage %g from set %g to %g\n", rope[op],
int [i], rope[FullName[thisSource]], rope[FullName[thisDest]]];
ENDLOOP;
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
XferMsgCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
sourceHandle, destHandle: Handle;
destExists: BOOL;
[result, msg, sourceHandle] ← ParseRange[cmd: cmd, refresh: FALSE, fullRange: FALSE];
IF result = $Failed THEN RETURN;
destHandle ← thisDest.db.handle;
destExists ← WalnutOps.MsgSetExists[opsHandle: destHandle, name: thisDest.msgSetName, msDomainVersion: WalnutOps.dontCareDomainVersion].exists;
IF ~destExists THEN RETURN[$Failed, "No destination message set"];
FOR i: INT IN [firstMessageNumber..MIN[lastMessageNumber, thisSource.entries.len]] DO
pos: INT;
msgID, msgName: ROPE;
tc: ViewerTools.TiogaContents;
IF FilterOut[i] THEN LOOP;
IF thisSource.entries[i-1].deleted THEN {
cmd.out.PutF["** Can't transfer message %g from set %g (deleted)\n",
int[i], rope[FullName[thisSource]]];
LOOP;
};
msgID ← msgName ← thisSource.entries[i-1].msgID;
pos ← Rope.Find[msgID, "$"];
IF pos # 0 THEN msgName ← msgID.Substr[pos];
tc ← WalnutOps.GetMsg[sourceHandle, msgID].contents;
IF NOT WalnutOps.MsgExists[destHandle, msgName] THEN {
WalnutOps.CreateMsg[destHandle, msgID, tc];
WalnutOps.SetHasBeenRead[destHandle, msgName];
};
IF NOT thisDest.msgSetName.Equal["active", FALSE] THEN
[]←WalnutOps.MoveMsg[destHandle,
msgID, ["active"], [thisDest.msgSetName]];
cmd.out.PutF["Transferred message %g from set %g to %g\n",
int [i], rope[FullName[thisSource]], rope[FullName[thisDest]]];
ENDLOOP;
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
DeleteMsgCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
handle: Handle;
[result, msg, handle] ← ParseRange[cmd: cmd, refresh: FALSE, fullRange: FALSE];
IF result = $Failed THEN RETURN;
FOR i: INT IN [firstMessageNumber..MIN[lastMessageNumber, thisSource.entries.len]] DO
IF FilterOut[i] THEN LOOP;
IF thisSource.entries[i-1].deleted THEN {
cmd.out.PutF["** Can't delete message %g from set %g (already deleted)\n",
int[i], rope[FullName[thisSource]]];
LOOP;
};
thisSource.entries[i-1].deleted ← TRUE;
[]←WalnutOps.RemoveMsg[handle,
thisSource.entries[i-1].msgID, [thisSource.msgSetName], WalnutDefs.dontCareMsgSetVersion];
cmd.out.PutF["Deleted message %g from set %g\n",
int [i], rope[FullName[thisSource]]];
ENDLOOP;
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
GetNewMailCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
SELECT Check[] FROM
$running => NULL;
$retried => RETURN[$Failed, "** Walnut had to be restarted; wait a few seconds and try again **"];
$failed => RETURN[$Failed, "** Something broke during attempt to restart or retrieve mail. **"];
$quit => RETURN[$Failed, "** Walnut was demanding to be stopped, so it was. **"];
$timedout => RETURN[$Failed, "** Walnut cannot be restarted, apparently. **"];
$noNewMail => RETURN[$Failed, "** No Walnut instance can read mail. **"];
ENDCASE => ERROR;
};
Checker: PROC = TRUSTED {
DO IF disableIdleReqt OR Idle.IsIdle[] THEN [] ← Check[]; Wait[]; ENDLOOP;
};
Wait: ENTRY PROC = TRUSTED {
ENABLE UNWIND => NULL;
checker: CONDITION;
Process.SetTimeout[@checker, Process.SecondsToTicks[60*checkMinutes]];
WAIT checker;
};
Check: ENTRY PROC RETURNS[whatHappened: WhatHappened←$failed] = TRUSTED {
ENABLE { RuntimeError.UNCAUGHT => GOTO Failed; UNWIND => NULL; };
db: DB;
[whatHappened, db] ← CheckAndRetry[];
IF whatHappened = $running THEN WalnutNewMail.CheckMailBoxes[db.handle];
EXITS Failed => NULL;
};
WhatHappened: TYPE = { running, retried, timedout, failed, quit, noNewMail };
running: Walnut is not demanding a Quit or Retry.
retried: Walnut was demanding it, so we "pushed" the Retry button.
timedout: We have retried too many times.
failed: Something bad happened.
quit: Walnut was demanding we push "Quit", so we did.
maxRetryCount: INT ← 72; -- 12 hours without success and we quit; should we?
CheckAndRetry: INTERNAL PROC
RETURNS [whatHappened: WhatHappened←$noNewMail, db: DB←NIL] = {
CheckOne: ViewerOps.EnumProc ~ {
menu: ViewerClasses.Menu ← v.menu;
menuLine: ViewerClasses.MenuEntry;
quitEntry: ViewerClasses.MenuEntry;
retryEntry: ViewerClasses.MenuEntry;
db1: DB;
waHoppen: WhatHappened ← $running;
IF menu = NIL THEN RETURN;
menuLine ← menu.lines[0];
IF menuLine = NIL THEN RETURN;
db1 ← GetDBFromWalnutHandle[menuLine.clientData];
IF db1=NIL THEN RETURN;
quitEntry ← FindEntry[menuLine, "Quit"];
retryEntry ← FindEntry[menuLine, "Retry"];
IF quitEntry=NIL THEN db1.retryCount ← 0
ELSE IF retryEntry#NIL THEN waHoppen ← $retried;
IF db1.retryCount > maxRetryCount THEN {
retryEntry ← quitEntry; waHoppen ← $timedout; };
IF retryEntry#NIL THEN {
db1.retryCount ← db1.retryCount + 1;
retryEntry.proc[parent: v, clientData: retryEntry.clientData];
};
IF FindEntry[menuLine, "NewMail"]#NIL THEN {
db ← db1; whatHappened ← waHoppen; };
};
ViewerOps.EnumerateViewers[CheckOne];
};
FindEntry: PROC[menuLine: ViewerClasses.MenuEntry, menuLabel: ROPE]
RETURNS [entry: ViewerClasses.MenuEntry] ~ {
FOR entry ← menuLine, entry.link WHILE entry#NIL DO
IF entry.name.Equal[menuLabel, FALSE] THEN RETURN;
ENDLOOP;
};
GetDBFromWalnutHandle: PROC[wH: REF] RETURNS[ db: DB←NIL ] ~ {
handle: Handle;
IsItTheOne: SymTab.EachPairAction = {
[key: ROPE, val: SymTab.Val] RETURNS [quit: BOOL ← FALSE]
oneDB: DB ← NARROW[val];
IF handle.rootName.Equal[oneDB.dbFileName, FALSE] THEN {
[]←SetHandle[oneDB];
db ← oneDB;
RETURN[quit:TRUE];
};
};
WITH wH SELECT FROM
wwh: WalnutWindowPrivate.WalnutHandle => handle ← wwh.opsH;
ENDCASE => RETURN;
[]←SymTab.Pairs[dbs, IsItTheOne];
};
GetDBFromName: PROC[dbName: ROPE]
RETURNS[ db: DB←NIL] ~ {
db ← NARROW[dbs.Fetch[dbName].val];
};
CacheMsgSet: PROC[cmd: Commander.Handle, msgSet: MsgSet, refresh: BOOLFALSE]
RETURNS[handle: Handle] = {
ListLength: PROC [l: LIST OF ROPE] RETURNS [i: INT] ~ TRUSTED {
RETURN [List.Length[LOOPHOLE[l]]];
};
numMsgs: INT;
i: INT;
version1, version2: WalnutOps.MsgSetVersion;
msgs: LIST OF ROPE;
msgSetName: ROPE ← msgSet.msgSetName;
entries: Entries ← msgSet.entries;
handle ← SetHandle[msgSet.db];
IF (NOT refresh) AND entries#NIL THEN RETURN; -- have it already
[numMsgs, version1] ← WalnutOps.SizeOfMsgSet[handle, msgSetName];
IF entries#NIL AND version1 = msgSet.msgSetVersion THEN RETURN;
DO
[numMsgs, version1] ← WalnutOps.SizeOfMsgSet[handle, msgSetName];
[msgs, version2] ← WalnutOps.MsgsInSetEnumeration[handle, msgSetName];
IF version2=version1 THEN EXIT;
ENDLOOP;
IF numMsgs # (i ← ListLength[msgs]) THEN {
cmd.out.PutF["*** Curious that Walnut says %g messages but provided %g TOC entries\n", int[numMsgs], int[i]];
numMsgs ← i;
};
msgSet.msgSetVersion ← version2;
entries ← msgSet.entries ← NEW[EntrySequence[numMsgs]];
i←-1;
FOR ml: LIST OF ROPE ← msgs, ml.rest WHILE ml#NIL DO
entries[i←i+1].msgID ← ml.first; ENDLOOP;
cmd.out.PutF["Message set %g contains %g messages\n", rope[FullName[msgSet]], int[i+1]];
};
FilterOut: PROC[msgNo: INT] RETURNS[BOOL] = {-- TRUE IFF entry does NOT match filter
tocEntry: ROPE; read: BOOL;
[read,,tocEntry] ← GetTocEntry[thisSource, msgNo];
RETURN[
(SELECT thisSourceNewFilter FROM
$read => NOT read, $unread => read, ENDCASE => FALSE) OR
(thisSourceFilter#NIL AND Rope.Find[s1: tocEntry, s2: thisSourceFilter, case: FALSE]<0)];
};
ParseRange: PROC [
cmd: Commander.Handle, refresh: BOOLFALSE, fullRange: BOOLFALSE]
RETURNS [result: REF ANYNIL, msg: ROPENIL, handle: Handle←NIL] ~ {
[result, msg] ← ParseArguments[cmd: cmd];
IF result = $Failed THEN RETURN;
handle ← CacheMsgSet[cmd, thisSource, refresh];
IF thisSourceFilter#NIL OR thisSourceNewFilter # $none THEN fullRange ← TRUE;
IF fullRange THEN [result, msg] ← ParseArguments[cmd: cmd, rangeDefaults: $all];
IF result = $Failed THEN RETURN;
IF firstMessageNumber = lastMessageNumber AND lastMessageNumber = -1 THEN
RETURN[$Failed, "No message range was specified"];
IF firstMessageNumber > thisSource.entries.len THEN
RETURN[$Failed, "There aren't that many"];
IF firstMessageNumber <= 0 THEN
RETURN[$Failed, "There aren't that few"];
};
ParseArguments: PROC[
cmd: Commander.Handle, rangeDefaults: {none, all}←$none, dbOnly: BOOLFALSE, sameDB: BOOLFALSE]
dbOnly -- Ignore message set name quantities, dummy up index entry bearing corresponding dbDesc.
sameDB -- source and dest must come from same db.
RETURNS[result: REF ANYNIL, msg: ROPENIL] = {
Returns same as commandProc to clue in caller as to problems.
Problems include no such (registered) DB and nonexistent DB.
parsing: BOOLFALSE; {
ENABLE IO.EndOfStream => IF parsing THEN GOTO Failed ELSE CONTINUE;
s: IO.STREAMIO.RIS[cmd.commandLine];
arg: ROPENIL;
quoted: BOOL;
GetToken: PROC RETURNS[token: ROPE, quoted: BOOLFALSE] = {
ln: INT;
token ← IO.GetCedarTokenRope[s].token;
ln ← token.Length[];
IF ln<2 OR token.Fetch[0]#'" THEN RETURN;
quoted ← TRUE; token ← token.Substr[start: 1, len: ln-2];
};
ParseMsgSet: PROC[msgSetName: ROPENIL] RETURNS[msgSet: MsgSet] ~ {
dbName: ROPE ← "default";
parsing ← TRUE;
IF msgSetName=NIL THEN msgSetName ← GetToken[].token;
IF s.PeekChar[]='. OR dbOnly THEN {
dbName ← msgSetName;
msgSetName ← NIL;
IF dbOnly THEN msgSetName ← "Active" ELSE DO
[] ← s.GetChar[];
msgSetName ← msgSetName.Concat[GetToken[].token];
IF s.PeekChar[]#'. THEN EXIT;
msgSetName ← msgSetName.Concat["."];
ENDLOOP;
};
[result, msg, msgSet] ← GetMsgSet[dbName, msgSetName];
parsing ← FALSE;
};
Implementation of "it", "them"
lastSource, lastDest: MsgSet;
lastSourceFilter: ROPE;
lastSourceNewFilter: NewFilter;
lastFirstMessageNumber, lastLastMessageNumber: INT;
lastSource ← thisSource; thisSource ← defaultSource;
lastDest ← thisDest; thisDest ← defaultDest;
lastSourceFilter ← thisSourceFilter; thisSourceFilter ← NIL;
lastSourceNewFilter ← thisSourceNewFilter; thisSourceNewFilter ← $none;
lastFirstMessageNumber𡤏irstMessageNumber; lastLastMessageNumber←lastMessageNumber;
firstMessageNumber ← lastMessageNumber ← -1;
WHILE result=NIL AND ([arg, quoted]←GetToken[]).token # NIL DO
c: CHARIF arg.Length[]>=1 THEN arg.Fetch[0] ELSE '\000;
SELECT TRUE FROM
NOT quoted AND arg.Equal["unread", FALSE] => thisSourceNewFilter ← $unread;
NOT quoted AND arg.Equal["read", FALSE] => thisSourceNewFilter ← $read;
NOT quoted AND arg.Equal["from", FALSE] => thisSource ← ParseMsgSet[];
NOT quoted AND arg.Equal["to", FALSE] => thisDest ← ParseMsgSet[];
NOT quoted AND arg.Equal["with", FALSE] => {
parsing ← TRUE;
thisSourceFilter ← IO.GetRope[s];
IF thisSourceFilter.Length[]>1 THEN
thisSourceFilter ← thisSourceFilter.Substr[start: 1, len: thisSourceFilter.Length[]-2];
parsing ← FALSE;
};
NOT quoted AND (arg.Equal["it", FALSE] OR arg.Equal["them", FALSE]) => {
thisSource ← lastSource;
thisDest ← lastDest;
thisSourceFilter ← lastSourceFilter;
thisSourceNewFilter ← lastSourceNewFilter;
firstMessageNumber ← lastFirstMessageNumber; -- [sic]
lastMessageNumber ← lastLastMessageNumber; -- [mea culpa]
};
c = '[ => {
parsing ← TRUE;
arg ← GetToken[].token;
IF ~ (arg.Fetch[0] IN ['0..'9]) THEN RETURN[$Failed, "Non-number in message range"];
firstMessageNumber ← Convert.IntFromRope[arg];
arg ← GetToken[].token;
IF ~arg.Equal[".."] THEN RETURN[$Failed, "Missing ellipsis in message range"];
arg ← GetToken[].token;
IF ~ (arg.Fetch[0] IN ['0..'9]) THEN RETURN[$Failed, "Non-number in message range"];
lastMessageNumber ← Convert.IntFromRope[arg];
arg ← GetToken[].token;
IF ~arg.Equal["]"] THEN RETURN[$Failed, "Message range not properly terminated"];
parsing ← FALSE;
};
c IN ['0..'9] => {
firstMessageNumber ← lastMessageNumber ← Convert.IntFromRope[arg];
};
c IN ['a..'z], c IN ['A..'Z] => thisSource ← ParseMsgSet[arg];
ENDCASE => RETURN[$Failed, "Unrecognized input"];
ENDLOOP;
EXITS
Failed => RETURN[$Failed, "Error while reading command"];
};
IF rangeDefaults = $all THEN {
IF firstMessageNumber = -1 THEN firstMessageNumber ← 1;
IF lastMessageNumber = -1 THEN lastMessageNumber ← thisSource.entries.len;
};
IF sameDB AND thisSource.db#thisDest.db THEN RETURN[$Failed, "Source and destination message sets must be from same Walnut data base."];
};
GetMsgSet: PROC[dbName, msgSetName: ROPE]
RETURNS [result: REF ANY, msg: ROPE, msgSet: MsgSet←NIL] ~ {
key: ROPE ← Rope.Cat[dbName, ".", msgSetName];
db: DB;
msgSet ← NARROW[msgSets.Fetch[key].val];
IF msgSet#NIL THEN {
[]←SetHandle[msgSet.db];
RETURN;
};
db ← GetDBFromName[dbName];
IF db=NIL THEN {
result ← $Failed;
msg ← "DB Name not registered; use WalnutKeyboard command to register.";
RETURN;
};
[] ← SetHandle[db];
msgSet ← NEW[MsgSetBody ← [db, msgSetName]];
[] ← msgSets.Store[key, msgSet];
};
SetHandle: PROC[db: DB] RETURNS[Handle] = {
rootFileName: ROPE ← db.dbFileName;
RETURN[db.handle ← WalnutOps.GetHandleForRootfile[rootFileName]];
};
GetTocEntry: PROC[msgSet: MsgSet, i: INT]
RETURNS[hasBeenRead: BOOLFALSE, deleted: BOOLFALSE, toc: ROPENIL]~{
entries: Entries ← msgSet.entries;
i ← i-1;
IF NOT entries[i].tocValid THEN {
[hasBeenRead, toc] ←
WalnutOps.GetDisplayProps[msgSet.db.handle, entries[i].msgID];
entries[i].tocValid ← TRUE;
entries[i].toc ← toc;
entries[i].hasBeenRead ← hasBeenRead;
}
ELSE {
toc ← entries[i].toc;
hasBeenRead ← entries[i].hasBeenRead;
deleted ← entries[i].deleted;
};
};
FullName: PROC[msgSet: MsgSet] RETURNS [fullName: ROPE] ~ {
RETURN[
IO.PutFR["%g.%g", rope[msgSet.db.dbName], rope[msgSet.msgSetName]]];
};
FlushCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
msgSets ← SymTab.Create[case: FALSE];
[result, msg, defaultSource] ← GetMsgSet["default", "Active"];
IF result=$Failed THEN RETURN;
defaultDest ← defaultSource;
cmd.out.PutF["Flushed message sets, default source and dest are default.Active\n"];
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
WalnutKbdCmd: Commander.CommandProc = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
dbName: ROPE;
rootFile: ROPE;
dbName ← CommandTool.NextArgument[cmd];
rootFile ← CommandTool.NextArgument[cmd];
[result, msg, dbName, rootFile] ← WalnutKbdInit[dbName, rootFile];
IF result=$Failed THEN RETURN;
cmd.out.PutF["Registered %g: %g\n", rope[dbName], rope[rootFile]];
};
WalnutKbdInit: PROC[dbNameIn, rootFileIn: ROPENIL]
RETURNS[result: REF ANYNIL, msg: ROPENIL, dbName: ROPENIL, rootFile: ROPENIL] = {
[cmd: Commander.Handle] RETURNS [result: REF ANYNIL, msg: ROPENIL]
ENABLE RuntimeError.UNCAUGHT => GOTO Failed;
db: DB;
dbName ← dbNameIn;
rootFile ← rootFileIn;
IF dbName = NIL THEN {
dbName ← "default";
rootFile ← UserProfile.Token["Walnut.WalnutRootFile"];
}
ELSE IF rootFile=NIL THEN RETURN[$Failed, "No root file name provided"];
db ← GetDBFromName[dbName];
IF db=NIL THEN {
db ← NEW[DBBody←[dbName, NIL]];
[]�s.Store[dbName, db];
};
db.dbFileName ← rootFile;
db.handle ← NIL;
IF defaultSource=NIL THEN
defaultSource ← defaultDest ← GetMsgSet[dbName, "Active"].msgSet;
EXITS Failed => RETURN[$Failed, "*** Uncaught signal ***"]
};
Commander.Register["WKDBs", ListDBsCmd, "List available Walnut data bases"];
Commander.Register["WKSets", ListMsgSetsCmd, "List Walnut message sets"];
Commander.Register["WKSelect", SelectMsgSetCmd, "Select working Walnut message set\n Usage: WKSelect <source>|<dest>"];
Commander.Register["WKListTOC", ListMsgsInMsgSetCmd, "List messages in message set, as specified by filters and message range.\n Usage: WKList <source>", $List];
Commander.Register["WKSameTOC", ListMsgsInMsgSetCmd, "List messages in message set, without consulting Walnut again.\n Usage: WKSame <source>", $Again];
Commander.Register["WKDisplay", PrintMsgInMsgSetCmd, "Display a message designated by number from a selected message set\n Usage: WKDisplay <msgNumber> <source>"];
Commander.Register["WKPoll", GetNewMailCmd, "Poll Mail services for new mail"];
Commander.Register["WKMove", MoveMsgCmd, "Move messages from source message set to destination message set.\n Usage: WKMove <source> <msgNumber> <destination>", $Move];
Commander.Register["WKCopy", MoveMsgCmd, "Copy messages from source message set to destination message set.\n Usage: WKCopy <source> <msgNumber> <destination>", $Copy];
Commander.Register["WKTransfer", XferMsgCmd, "Transfer messages from source message set in one database to destination message set in another.\n Usage: WKTransfer <source> <msgNumber> <destination>"];
Commander.Register["WKDelete", DeleteMsgCmd, "Delete messages from source message set.\n Usage: WKDelete <source> <msgNumber>"];
Commander.Register["WKFlush", FlushCmd, "Flush all cached message sets. Restore default to default.Active."];
Commander.Register["WalnutKeyboard", WalnutKbdCmd, "Usage: Walnutkeyboard <dbName> <rootFile>\nAdds mapping from <dbName> to <rootFile> to list of available Walnut databases\nDefault <dbName>='default'; default <rootFile> from user profile"];
dbs ← SymTab.Create[case: FALSE];
msgSets ← SymTab.Create[case: FALSE];
TRUSTED { Process.Detach[FORK Checker[]]; };
[]←WalnutKbdInit[NIL, NIL];
}.