RememberImpl.mesa.
Copyright © 1985 by Xerox Corporation. All rights reserved.
Teitelman, July 21, 1983 10:50 am
Maxwell, January 6, 1984 1:36 pm
Pier, May 2, 1984 3:59:07 pm PDT
Beach, August 15, 1984 10:33:03 am PDT
Pavel on June 7, 1984 9:49:28 pm PDT
Rick Beach, May 28, 1985 9:08:25 pm PDT
DIRECTORY
Atom USING [MakeAtom, GetProp, PutProp, RemProp],
CommandTool USING[DoCommand],
Convert USING[CardFromRope, RopeFromLiteral],
BasicTime USING[GMT, nullGMT, Unpack],
Booting USING[CheckpointProc, RegisterProcs, RollbackProc],
FileNames USING [IsRemote],
FS,
FSBackdoor,
Icons USING [IconFlavor],
IconRegistry USING [GetIcon],
IO,
List USING [Sort, CompareProc],
Menus USING [SetLine, GetNumberOfLines, MenuEntry],
MessageWindow USING [Append, Blink],
Process USING[Pause, SecondsToTicks, Detach],
Remember,
RememberPrivate,
Rope,
Tempus USING [Adjust, defaultTime, Error, MakeRope, Packed, PackedSeconds, Seconds, PackedToSeconds, SecondsToPacked, Parse, Precision, Unintelligible],
TiogaExtraOps USING [GetFile],
TiogaOps USING [SetStyle, Break, SetNodeFormat, Ref, FirstChild, Jump, GetRope, CaretAfter, SaveSpanForPaste, LastLocWithin, SelectDocument, Paste, Next, LockSel, UnlockSel, SaveSelB, RestoreSelB, ViewerDoc],
UserProfile USING [Boolean, Number, Token, ProfileChangedProc, CallWhenProfileChanges],
ViewerClasses USING[Viewer],
ViewerEvents USING[RegisterEventProc, ViewerEvent, EventProc],
ViewerIO USING [CreateViewerStreams],
ViewerSpecs USING [openBottomY],
ViewerTools USING[MakeNewTextViewer, SetContents, SetTiogaContents],
ViewerOps USING[AddProp, FetchProp, PaintViewer, BlinkIcon, DestroyViewer, FindViewer],
WalnutOps USING [MsgExists, GetMsgText];
RememberImpl:
CEDAR
MONITOR
IMPORTS Atom, BasicTime, Booting, CommandTool, Convert, FileNames, FS, FSBackdoor, IconRegistry, IO, List, Menus, MessageWindow, Process, RememberPrivate, Rope, Tempus, TiogaExtraOps, TiogaOps, UserProfile, ViewerEvents, ViewerIO, ViewerOps, ViewerSpecs, ViewerTools, WalnutOps
EXPORTS Remember, RememberPrivate
= BEGIN OPEN Remember, RememberPrivate;
Types
Error: PUBLIC ERROR [ec: ErrorCode, msg: Rope.ROPE ← NIL] = CODE;
Global Variables
eventList: PUBLIC LIST OF Event ← NIL;
itIsNow: PUBLIC Tempus.Packed ← Tempus.defaultTime; -- defaultTime used in Register, EnterEventTime. Is a variable so that can be reset for debugging.
walnutUser: PUBLIC BOOL ← FALSE;
peanutUser: PUBLIC BOOL ← FALSE;
debugging: BOOLEAN ← TRUE;
eventFile: Rope.ROPE;
eventFileCreateTime: BasicTime.GMT ← BasicTime.nullGMT;
Parsing
Construct:
ENTRY
PROC = {
ENABLE UNWIND => NULL;
fileStream: IO.STREAM;
BEGIN
-- establish scope for exits
IF eventList #
NIL
THEN
FOR el:
LIST
OF Event ← eventList, el.rest
UNTIL el =
NIL
DO
event: Event = el.first;
IF event.viewer # NIL AND NOT event.viewer.destroyed THEN ViewerOps.DestroyViewer[event.viewer];
ENDLOOP;
eventList ← NIL;
eventFileCreateTime ← BasicTime.nullGMT;
fileStream ← FS.StreamOpen[eventFile, $read ! FS.Error => CONTINUE];
IF fileStream = NIL THEN GOTO noRememberFile;
DO
parameters: ParameterList ← NIL;
timeRope, text, repeat, duration, lead, until, iconLabel, icon, message: ROPE ← NIL;
[] ← IO.SkipWhitespace[fileStream];
timeRope ← ReadTimeRope[fileStream ! IO.EndOfStream => EXIT];
parameters ← LIST[NEW[Parameter ← [startTime, timeRope]]];
DO
r: ROPE;
class: ParameterClass;
[class, r] ← ReadParameter[fileStream ! Error => GOTO formatError];
parameters ← CONS[NEW[Parameter ← [class, r]], parameters];
IF class = text THEN EXIT;
ENDLOOP;
[] ← Register[
parameters: parameters
! Error =>
IF ec = formatError THEN GOTO formatError
ELSE IF ec = timeRopeFormatError THEN GOTO timeRopeFormatError
];
ENDLOOP;
RememberMostRecentEventFile[fileStream];
fileStream.Close[];
EXITS
formatError => {
out: IO.STREAM ← ViewerIO.CreateViewerStreams["Remember Error Log"].out;
eventList ← NIL;
out.PutF["Format error in %g, at or near position: %d\n", IO.rope[eventFile], IO.int[fileStream.GetIndex[]]]; fileStream.Close[];
};
timeRopeFormatError => {
out: IO.STREAM ← ViewerIO.CreateViewerStreams["Remember Error Log"].out;
eventList ← NIL;
out.PutF["DateAndTime.Unintelligible in %g, at or near position: %d\n", IO.rope[eventFile], IO.int[fileStream.GetIndex[]]];
out.PutRope["\n"]; fileStream.Close[];
};
noRememberFile => {
eventList ← NIL;
MessageWindow.Append[
Rope.Cat["Could not find ", eventFile, " : no events logged."], TRUE];
MessageWindow.Blink[];
};
END;
IF fileStream#
NIL
THEN
FS.SetKeep[name: eventFile, keep: 2 !
FS.Error =>
CONTINUE];
--help out FS
ReadParameters:
PUBLIC
PROC [stream:
IO.
STREAM]
RETURNS[parameters: ParameterList] = {
UNTIL
IO.EndOf[stream]
DO
r: ROPE;
class: ParameterClass;
[class, r] ← ReadParameter[stream];
parameters ← CONS[NEW[Parameter ← [class, r]], parameters];
[] ← IO.SkipWhitespace[stream];
ENDLOOP;
};
ReadParameter:
PROC[stream:
IO.
STREAM]
RETURNS[parameterClass: ParameterClass ← text, value:
ROPE ←
NIL] = {
ENABLE {
IO.EndOfStream => GOTO formatError;
IO.Error => GOTO formatError;
};
key: ROPE;
kind: IO.TokenKind;
[kind, value, ] ← IO.GetCedarTokenRope[stream];
SELECT kind
FROM
tokenROPE => RETURN[text, Convert.RopeFromLiteral[value]];
tokenID => key ← value;
tokenSINGLE => {
-- e.g. , ; etc. left over on line.
[parameterClass, value] ← ReadParameter[stream];
RETURN;
};
ENDCASE => IO.Error[SyntaxError, stream];
[] ← IO.SkipWhitespace[stream];
IF NOT stream.EndOf[] AND stream.PeekChar[] = ': THEN [] ← stream.GetChar[];
SELECT
TRUE
FROM
Rope.Equal[s1: key, s2: "REPEAT", case: FALSE] => parameterClass ← repeat;
Rope.Equal[s1: key, s2: "DURATION", case: FALSE] => parameterClass ← duration;
Rope.Equal[s1: key, s2: "GETSERIOUSAFTER", case: FALSE], Rope.Equal[s1: key, s2: "NAGTIME", case: FALSE] => parameterClass ← getSeriousAfter;
Rope.Equal[s1: key, s2: "UNTIL", case: FALSE] => parameterClass ← until;
Rope.Equal[s1: key, s2: "LEADTIME", case: FALSE] => parameterClass ← leadTime;
Rope.Equal[s1: key, s2: "TIME", case: FALSE] => parameterClass ← startTime;
Rope.Equal[s1: key, s2: "TEXT", case: FALSE] => parameterClass ← text;
Rope.Equal[s1: key, s2: "ICONFLAVOR", case: FALSE] => parameterClass ← iconFlavor;
Rope.Equal[s1: key, s2: "ICONLABEL", case: FALSE] => parameterClass ← iconLabel;
Rope.Equal[s1: key, s2: "ICONLABELTYPE", case: FALSE] => parameterClass ← iconLabelType;
Rope.Equal[s1: key, s2: "MESSAGE", case: FALSE] => parameterClass ← message;
ENDCASE => Error [formatError, key];
[] ← IO.SkipWhitespace[stream];
IF stream.EndOf[] THEN value ← NIL
ELSE IF stream.PeekChar[] = '" THEN value ← IO.GetRopeLiteral[stream]
ELSE value ← IO.GetTokenRope[stream].token;
RETURN;
EXITS
formatError => Error[formatError];
};
Registering Events
RegisterEvent:
PUBLIC
ENTRY
PROC [parameters: ParameterList,
scanThis: Rope.
ROPE ←
NIL, out:
IO.
STREAM] =
TRUSTED {
ENABLE UNWIND => NULL;
event: Event ← Register[parameters, scanThis];
IF out #
NIL
THEN {
out.PutF["Reminder will be posted at %g", IO.rope[Tempus.MakeRope[time: Tempus.SecondsToPacked[[event.timeToStartNotification - event.leadTime]], includeDayOfWeek: TRUE]]];
IF event.durationTime > 0 THEN out.PutF[", for %d minutes", IO.int[event.durationTime/60]];
IF event.repeat # NIL THEN out.PutF[", repeated %g", IO.rope[event.repeat]];
IF event.getSeriousAfter # 0 THEN out.PutF[", start nagging after %d minutes", IO.int[event.getSeriousAfter/60]];
out.PutRope["\n"];
};
Process.Detach[FORK Save[]];
};
Register:
INTERNAL
PROC [parameters: ParameterList, scanThis: Rope.
ROPE ←
NIL]
RETURNS[event: Event] = {
ENABLE Tempus.Unintelligible => Error[timeRopeFormatError, Rope.Cat[
SELECT ec
FROM
invalid => "Invalid: ",
overConstrained => "Over Constrained: ",
tooVague => "Too Vague: ",
nothingSpecified => "Not a time: ",
notImplemented => "Not Implemented: ",
ENDCASE => ERROR,
Rope.Substr[base: rope, len: vicinity],
"<>",
Rope.Substr[base: rope, start: vicinity]
]];
startAt, text, lead, until, duration, getSerious, repeat, iconFlavor, iconLabel: Rope.ROPE;
timeToStartNotification, nextNotification: PackedSeconds ← [0];
durationTime, leadTime, getSeriousTime: Seconds ← 0;
precision: Tempus.Precision;
Lookup:
PROC [class: ParameterClass]
RETURNS[value: Rope.
ROPE ←
NIL] =
INLINE {
FOR l: ParameterList ← parameters, l.rest
UNTIL l =
NIL
DO
IF l.first.class = class THEN RETURN[l.first.value];
ENDLOOP;
};
LookupKeyWord:
PROC [word: Rope.
ROPE]
RETURNS[ParameterList] =
INLINE {
FOR l: KeyWordList ← keyWordList, l.rest
UNTIL l =
NIL
DO
IF Rope.Equal[l.first.key, word, FALSE] THEN RETURN[l.first.defaults];
ENDLOOP;
RETURN[NIL];
};
text ← Lookup[text];
startAt ← Lookup[startTime]; -- i.e. specified in parameters
iconFlavor ← Lookup[iconFlavor];
iconLabel ← Lookup[iconLabel];
IF scanThis #
NIL
THEN {
h: IO.STREAM = IO.RIS[scanThis];
token: ROPE ← NIL;
tokens: LIST OF ROPE;
UNTIL h.EndOf[]
DO
breakProc:
IO.BreakProc = {
RETURN[
SELECT char
FROM
', , '. , ';, ': => sepr,
' , '\n, '\t, '\l => sepr, -- white space
ENDCASE => other
];
};
defaults: ParameterList;
iconLabelType: Rope.ROPE;
token ← h.GetTokenRope[breakProc ! IO.EndOfStream => EXIT].token;
tokens ← CONS[token, tokens];
IF (defaults ← LookupKeyWord[token]) #
NIL
THEN {
-- add defaults to end of parameters, i.e. if parameters are specified by / .., they take precedence over those specified via keywords.
FOR l: ParameterList ← parameters, l.rest
UNTIL l =
NIL
DO
defaults ← CONS[l.first, defaults];
ENDLOOP;
parameters ← defaults;
IF startAt = NIL AND timeToStartNotification = 0 THEN startAt ← Lookup[startTime]; -- defaults may specify a startAt
IF iconLabel =
NIL
AND (iconLabel ← Lookup[iconLabel]) =
NIL
AND (iconLabelType ← Lookup[iconLabelType]) #
NIL
THEN
-- this check must be done here because iconLabelTypes may require knowing exactly where in the rope the corresponding key word appeared.
SELECT
TRUE
FROM
Rope.Equal[iconLabelType, "this", FALSE] => iconLabel ← token;
Rope.Equal[iconLabelType, "next",
FALSE] => {
iconLabel ← h.GetTokenRope[breakProc ! IO.EndOfStream => EXIT].token;
IF Rope.Fetch[iconLabel, 0]
IN ['A..'Z]
THEN {
pos: INT = h.GetIndex[];
r: ROPE;
WHILE Rope.Fetch[r ← h.GetTokenRope[breakProc !
IO.EndOfStream =>
EXIT].token, 0]
IN ['A..'Z]
DO
iconLabel ← Rope.Cat[iconLabel, " ", r];
ENDLOOP;
h.SetIndex[pos];
};
};
Rope.Equal[iconLabelType, "prev",
FALSE] =>
IF tokens.rest #
NIL
THEN {
iconLabel ← tokens.rest.first;
IF Rope.Fetch[iconLabel, 0]
IN ['A..'Z]
THEN {
FOR l:
LIST
OF
ROPE ← tokens.rest.rest, l.rest
UNTIL l =
NIL
DO
IF NOT (Rope.Fetch[l.first, 0] IN ['A..'Z]) THEN EXIT;
iconLabel ← Rope.Cat[l.first, " ", iconLabel];
ENDLOOP;
};
};
ENDCASE => Error[formatError];
};
ENDLOOP; -- end of loop processing scan this
IF startAt =
NIL
AND timeToStartNotification = 0
THEN {
-- look for a time in scanThis
t: Packed ← BasicTime.nullGMT;
start, len: INT;
startAt ← Lookup[startTime];
[t, precision, start, len] ← Tempus.Parse[rope: scanThis, baseTime: itIsNow ! Tempus.Unintelligible => IF ec # overConstrained THEN CONTINUE]; -- time may still be specified via keywords. Used to just check for nothingSpecified, but invalid can be raised in case that the word 'week' or 'day' appears in the text.
IF t # BasicTime.nullGMT
THEN {
IF startAt #
NIL
THEN
-- time specified both in text and keyword, e.g. keyword says Wednesday 1:15PM, user says Today, or the actual date.
[t, precision] ← Tempus.Parse[rope: Rope.Cat[startAt, " ", Rope.Substr[base: scanThis, start: start, len: len]], baseTime: itIsNow ! Tempus.Unintelligible => CONTINUE];
timeToStartNotification ← Tempus.PackedToSeconds[t];
};
};
IF text = NIL THEN text ← scanThis;
}; -- end of IF scanThis # NIL
IF timeToStartNotification = 0
AND startAt #
NIL
THEN {
-- startAt specified either in parameters, or via keyword.
t: Packed;
[t, precision] ← Tempus.Parse[startAt, itIsNow];
timeToStartNotification ← Tempus.PackedToSeconds[t];
};
IF timeToStartNotification = 0 THEN Error[timeNotSpecified];
IF (repeat ← Lookup[repeat]) # NIL THEN nextNotification ← ComputeRepetitionInterval[repeat, timeToStartNotification]; -- call ComputeRepetitionInterval here just so any errors will get noticed at registration time, rather than in event minder
IF (duration ← Lookup[duration]) #
NIL
THEN {
IF
NOT Rope.Equal[duration, "NIL"]
THEN
-- so user can override defaultDuration by saying Duration: NIL
durationTime ← Convert.CardFromRope[duration]*60
}
ELSE IF defaultDuration # -1 AND precision > days THEN durationTime ← defaultDuration * 60; -- if user says Thursday, then don't default duration, but if he says a time do so
IF (lead ← Lookup[leadTime]) #
NIL
THEN {
leadTime ← Convert.CardFromRope[lead]*60;
timeToStartNotification ← [timeToStartNotification - leadTime];
}
ELSE IF precision <= days THEN NULL -- similarly if user says "See somebody Thursday", don't use leadtime.
ELSE IF defaultLeadTime > 0 THEN leadTime ← (defaultLeadTime * 60);
IF durationTime > 0 THEN durationTime ← MAX[durationTime, leadTime]; --show the reminder up to the event time
IF (until ← Lookup[until]) # NIL THEN durationTime ← Tempus.PackedToSeconds[Tempus.Parse[until, Tempus.SecondsToPacked[timeToStartNotification]].time] - timeToStartNotification; -- e.g. user can say until Tuesday, meaning Tuesday after event
IF (getSerious ← Lookup[getSeriousAfter]) # NIL THEN getSeriousTime ← Convert.CardFromRope[getSerious]*60;
event ←
NEW[EventRecord ← [
timeToStartNotification: timeToStartNotification,
text: text,
repeat: repeat,
nextNotification: nextNotification,
durationTime: durationTime,
leadTime: leadTime,
getSeriousAfter: getSeriousTime,
message: Lookup[message],
iconLabel: iconLabel,
iconFlavor: Lookup[iconFlavor]
]];
FOR l:
LIST
OF Event ← eventList, l.rest
UNTIL l =
NIL
DO
e: Event = l.first;
IF Rope.Equal[e.message, event.message,
FALSE]
AND Rope.Equal[e.text, event.text,
FALSE]
THEN {
-- i.e. if an event already exists which differs only in start time, duration, icon label, etc. then this event is considered to replace the original
e^ ← event^;
EXIT;
};
REPEAT
FINISHED => eventList ← CONS[event, eventList];
ENDLOOP;
};
AddEvent:
PUBLIC
ENTRY
PROC [event: Event] = {
-- for use by buttons
eventList ← CONS[event, eventList];
};
Event Minder
eventMinderDisabled: BOOL ← FALSE;
EventMinder:
PROC = {
DO
EnterEventMinder[itIsNow];
Process.Pause[Process.SecondsToTicks[3]];
ENDLOOP;
};
EnterEventMinder:
PUBLIC
ENTRY
PROC [itIsNow: Packed,
itIsLater: Packed ← BasicTime.nullGMT] =
{
ENABLE
UNWIND =>
NULL;
now: PackedSeconds ← Tempus.PackedToSeconds[itIsNow];
later: PackedSeconds ← IF itIsLater#BasicTime.nullGMT THEN Tempus.PackedToSeconds[itIsLater] ELSE now;
IF eventMinderDisabled OR now > later THEN RETURN;
FOR el:
LIST
OF Event ← eventList, el.rest
UNTIL el =
NIL
DO
-- for each event.
event: Event = el.first;
startTime: PackedSeconds ← [event.timeToStartNotification - event.leadTime];
IF event.justPretending AND itIsNow = Tempus.defaultTime THEN { -- event posted as a result of just pretend
event.justPretending ← FALSE;
event.newStartTime ← TRUE;
};
IF now > startTime
AND event.repeat #
NIL
THEN {
-- repeating event. compute most recent startTime, otherwise, if duration specified, may think this event has lapsed and there may be a later time which would satisfy.
t1: PackedSeconds ← event.timeToStartNotification;
IF itIsNow # Tempus.defaultTime
THEN {
--pretending with repeated event
event.trueTimeToStartNotification ← event.timeToStartNotification;
event.trueNextNotification ← event.nextNotification;
event.restoreTrueTimes ← TRUE;
};
DO
ENABLE {
Tempus.Error, Tempus.Unintelligible => GOTO Bogus;
};
t1 ← IF event.nextNotification # 0 THEN event.nextNotification ELSE ComputeRepetitionInterval[event.repeat, t1];
nextNotification is the first time past the now when the event will occur. It is kept (cached) in the event structure to avoid recomputation. It is only recomputed when now becomes > nextNotification. Note that the latest time of notification before now may or may not cause the event to be posted, depending on whether or not there is a duration, and whether it has lapsed.
IF t1 > now THEN {event.nextNotification ← t1; EXIT};
event.newStartTime ← TRUE;
event.nextNotification ← [0];
event.timeToStartNotification ← startTime ← t1;
ENDLOOP;
};
IF later > startTime
THEN {
create a new viewer if there just occurred a new start time (the first qualifies) and its duration has not expired
event.justPretending ← (itIsNow # Tempus.defaultTime);
IF event.newStartTime
AND (event.durationTime = 0
OR now < startTime + event.durationTime
OR (event.repeat #
NIL
AND later > event.nextNotification))
THEN {
viewer: Viewer ← event.viewer;
text: ROPE ← Rope.Cat["Time: ",
Tempus.MakeRope[time: Tempus.defaultTime, includeDayOfWeek: TRUE], "\nEventTime: ",
Tempus.MakeRope[time: Tempus.SecondsToPacked[startTime], includeDayOfWeek: TRUE],
"\n"];
text ← Rope.Concat[text, event.text];
event.newStartTime ← FALSE;
event.blink ← TRUE;
IF viewer =
NIL
OR viewer.destroyed
THEN {
viewer ← ViewerTools.MakeNewTextViewer[
info: [
name: event.text,
data: text,
icon: IconRegistry.GetIcon[IF el.first.iconFlavor # NIL THEN el.first.iconFlavor ELSE "Remember.defaultIcon", IconRegistry.GetIcon["Remember.defaultIcon", document]]], -- uses el.first.iconFlavor if it can be found, otherwise Remember.defaultIcon otherwise document. The reason for the NIL check instead of just looking up NIL and letting it fail is that somebody actually managed to register an icon for NIL.
paint: FALSE];
Menus.SetLine[menu: viewer.menu, line: Menus.GetNumberOfLines[viewer.menu], entryList: reminderButtons];
};
IF event.message #
NIL
THEN {
IF walnutUser
THEN {
IF WalnutOps.MsgExists[event.message]
THEN
ViewerTools.SetTiogaContents[viewer: viewer,
contents: WalnutOps.GetMsgText[event.message].contents,
paint: FALSE]
ELSE
ViewerTools.SetContents[viewer: viewer, contents: Rope.Cat[text, "\n(Could not find message: \"", event.message, "\")\n"], paint: FALSE];
}
ELSE
IF peanutUser
THEN {
fileAtom: ATOM;
ropeIndex: INT;
message: ROPE;
mailViewer: ViewerClasses.Viewer;
mailDoc: TiogaOps.Ref;
stream: IO.STREAM = IO.RIS[event.message];
fileName: ROPE ← stream.GetTokenRope[IO.IDProc].token;
--strip off the version number because the "current" version won't last
IF (ropeIndex ← Rope.Find[s1: fileName, s2: "!", case:
FALSE])#-1
THEN
fileName ← Rope.Substr[base: fileName, start: 0, len: ropeIndex];
fileAtom ← Atom.MakeAtom[fileName];
[] ← IO.GetChar[stream]; -- the extra space
message ← IO.GetTokenRope[stream, EveryThing ! IO.EndOfStream => CONTINUE].token;
TRUSTED {mailDoc ← LOOPHOLE[Atom.GetProp[atom: fileAtom, prop: $Root]]};
IF mailDoc # NIL THEN NULL
ELSE
IF (mailViewer ← ViewerOps.FindViewer[fileName]) #
NIL
THEN {
mailDoc ← TiogaOps.ViewerDoc[mailViewer];
Atom.RemProp[atom: fileAtom, prop: $Root]; -- if a reminder is posted from a mail file which does not have a viewer, thereby caching the reminder, and then user opens a viewer, invalidate the cache because user might edit the viewer. In other words, cache is used only during those periods where no viewer is around, and hence it is known that user is not editing the mail file
}
ELSE Atom.PutProp[atom: fileAtom, prop: $Root, val: mailDoc ← TiogaExtraOps.GetFile[fileName ! FS.Error => CONTINUE]];
IF mailDoc = NIL THEN ViewerTools.SetContents[viewer: viewer, contents: Rope.Cat[text, "\n(Could not find mail file: ", fileName, ") ", message], paint: FALSE]
ELSE {
node: TiogaOps.Ref ← TiogaOps.FirstChild[mailDoc];
DO
IF Rope.Equal[TiogaOps.GetRope[node], message]
THEN {
ENABLE UNWIND => TiogaOps.UnlockSel[];
TiogaOps.LockSel[];
TiogaOps.SaveSelB[];
TiogaOps.SelectDocument[viewer: viewer, pendingDelete: FALSE];
TiogaOps.CaretAfter[];
TiogaOps.Break[]; --root node
TiogaOps.SaveSpanForPaste[startLoc: [node, 0], endLoc: [node, Rope.Size[message]]];
TiogaOps.Paste[];
TiogaOps.Break[]; --first sibling of root
TiogaOps.SetStyle[style: "Mail", which: root];
TiogaOps.SetNodeFormat[format: "header", node: TiogaOps.Next[TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]]];
TiogaOps.SaveSpanForPaste[startLoc: [node, Rope.Size[message]], endLoc: TiogaOps.LastLocWithin[node]];
TiogaOps.Paste[];
TiogaOps.Jump[viewer: viewer, loc: [TiogaOps.ViewerDoc[viewer], 0]];
viewer.newVersion ← FALSE;
viewer.icon ← IconRegistry.GetIcon["Remember.defaultIcon", document];
TiogaOps.RestoreSelB[];
TiogaOps.UnlockSel[];
EXIT;
}
ELSE
IF (node ← TiogaOps.Next[node]) =
NIL
THEN {
text ← Rope.Cat[text, "\n(Could not find message: \"", message, "\"\n"];
text ← Rope.Cat[text, "in mail file: ", fileName, ")\n"];
ViewerTools.SetContents[viewer: viewer, contents: text, paint: FALSE];
EXIT;
};
ENDLOOP;
};
}
ELSE ViewerTools.SetContents[viewer: viewer, contents: Rope.Cat[text, "\n(Walnut/Peanut not loaded, could not include message: ", event.message, ")\n"], paint: FALSE] ;
};
IF event.iconLabel # NIL THEN ViewerOps.AddProp[viewer, $IconLabel, event.iconLabel];
ViewerOps.AddProp[viewer, $Remember, el.first]; -- for menus
event.viewer ← viewer;
ViewerOps.PaintViewer[viewer, all];
IF
NOT logFileName.IsEmpty
AND
NOT event.justPretending
THEN
LogEvent[event, "Following Reminder posted"];
};
IF event.durationTime # 0
-- 0 means forever
AND now > startTime + event.durationTime
THEN {
IF event.justPretending THEN NULL
ELSE
IF event.repeat #
NIL
THEN
TRUSTED {
IF event.nextNotification = 0 THEN ERROR;
event.timeToStartNotification ← event.nextNotification;
event.newStartTime ← TRUE;
Process.Detach[FORK Save[]];
}
ELSE
IF
NOT event.destroyed
THEN
TRUSTED {
event.destroyed ← TRUE;
Process.Detach[FORK Save[]];
};
IF
NOT event.justPretending
AND event.viewer #
NIL
AND
NOT event.viewer.destroyed
THEN
-- destroy an existing viewer if a duration is specified and it has lapsed
ViewerOps.DestroyViewer[event.viewer];
};
IF event.restoreTrueTimes THEN TRUSTED {Process.Detach[FORK Save[]]};
IF event.getSeriousAfter # 0
AND now > startTime + event.getSeriousAfter
AND
NOT event.destroyed
AND
NOT event.newStartTime
THEN
TRUSTED {
IF event.viewer #
NIL
AND
NOT event.viewer.destroyed
AND event.viewer.iconic
THEN
-- waste the sucker
[] ← CommandTool.DoCommand[commandLine: Rope.Cat["← SpecialCache.NameIt[\"", event.viewer.name,"\"]"], parent: NIL];
MessageWindow.Append[Rope.Concat["Important! ", event.text], TRUE];
MessageWindow.Blink[];
};
blink the viewer if there is an active one and it is iconic and the blinker is still active.
IF event.viewer #
NIL
AND
NOT event.viewer.destroyed
AND event.viewer.iconic
AND event.blink
THEN {
IF event.viewer.wy >= ViewerSpecs.openBottomY
THEN {
-- icon is not visible
MessageWindow.Append[Rope.Concat["Reminder: ", event.text], TRUE];
MessageWindow.Blink[];
}
ELSE ViewerOps.BlinkIcon[event.viewer];
};
};
ENDLOOP;
};
EveryThing: IO.BreakProc = {RETURN[other]};
Saving Events
Save:
PUBLIC
ENTRY
PROC = {
OPEN IO;
ENABLE UNWIND => NULL;
Compare: List.CompareProc = {
e1, e2: Event;
e1 ← NARROW[ref1];
e2 ← NARROW[ref2];
RETURN[IF e1.timeToStartNotification < e2.timeToStartNotification THEN less ELSE greater];
};
newEventList: LIST OF Event ← NIL;
stream: IO.STREAM ← NIL;
now: PackedSeconds = Tempus.PackedToSeconds[];
eventFileCreateTime ← BasicTime.nullGMT;
stream ←
FS.StreamOpen[fileName: eventFile, accessOptions: $create, keep: 2 !
FS.Error => {
MessageWindow.Append[Rope.Cat["FS Error trying to open ", eventFile], TRUE];
MessageWindow.Blink[];
CONTINUE;
}
];
IF stream=NIL THEN RETURN;
FOR l:
LIST
OF Event ← eventList, l.rest
UNTIL l =
NIL
DO
event: Event ← l.first;
IF event.restoreTrueTimes
THEN {
--fix up pretended repeated events
event.timeToStartNotification ← event.trueTimeToStartNotification;
event.nextNotification ← event.trueNextNotification;
event.restoreTrueTimes ← FALSE;
};
purge those events no longer interesting
IF event.repeat # NIL THEN NULL -- if this is a repeated event, can't purge it
ELSE IF event.justPretending THEN NULL -- this event posted as a result of a justpretend
ELSE
IF event.destroyed
-- reminder was posted and user has destroyed the viewer. the check for destroyed, rather than viewer.destroyed, is because save may be called as a result of the destroy operation, i.e. from WasAReminderDestroyed, in which case the viewer.destroyed has not yet been set to TRUE.
OR (event.durationTime # 0 AND event.timeToStartNotification + event.durationTime < now) -- this is an old event and no longer interesting
THEN {
IF
NOT logFileName.IsEmpty
THEN
LogEvent[event, "Following Event deleted"];
LOOP;
};
newEventList ← CONS[event, newEventList];
ENDLOOP;
TRUSTED {eventList ← LOOPHOLE[List.Sort[list: LOOPHOLE[newEventList], compareProc: Compare]]};
FOR l:
LIST
OF Event ← eventList, l.rest
UNTIL l =
NIL
DO
SaveEvent[l.first, stream];
ENDLOOP;
RememberMostRecentEventFile[stream];
stream.Close[];
IF (viewer ← ViewerOps.FindViewer[eventFile]) # NIL THEN ViewerOps.RestoreViewer[viewer];
};
SaveEvent:
PROC [event: Event, stream:
IO.
STREAM] = {
OPEN IO;
timeToStartNotification: PackedSeconds ← event.timeToStartNotification;
IF event.leadTime # 0 THEN timeToStartNotification ← [timeToStartNotification + event.leadTime];
stream.PutF["%g\n", rope[Tempus.MakeRope[time: Tempus.SecondsToPacked[timeToStartNotification], includeDayOfWeek: TRUE]]];
IF event.repeat #
NIL
THEN
stream.PutF["Repeat: %g\n", rope[event.repeat]];
IF event.durationTime # 0 THEN stream.PutF["Duration: %d\n", int[event.durationTime/60]];
IF event.leadTime # 0 THEN stream.PutF["LeadTime: %d\n", int[event.leadTime/60]];
IF event.getSeriousAfter # 0 THEN stream.PutF["NagTime: %d\n", int[event.getSeriousAfter/60]];
IF event.iconFlavor # NIL THEN stream.PutF["IconFlavor: \"%g\"\n", rope[event.iconFlavor]];
IF event.iconLabel # NIL THEN stream.PutF["IconLabel: \"%q\"\n", rope[event.iconLabel]];
IF event.message # NIL THEN stream.PutF["Message: \"%q\"\n", rope[event.message]];
stream.PutF["\"%q\"\n\n", rope[event.text]];
};
LogEvent:
PROC [event: Event, logMessage:
ROPE] = {
IF logFileName.IsEmpty THEN RETURN
ELSE {
eventLogStream: IO.STREAM ← NIL;
eventLogStream ← FS.StreamOpen[fileName: logFileName, accessOptions: append ! FS.Error => CONTINUE];
IF eventLogStream =
NIL
THEN {
Could not append to log file, try copying the log file to a new version
newLogStream: IO.STREAM ← NIL;
eventLogStream ← FS.StreamOpen[fileName: logFileName, accessOptions: read ! FS.Error => CONTINUE];
IF eventLogStream =
NIL THEN {
Could not read the existing log, simply create a new log stream
eventLogStream ← FS.StreamOpen[fileName: logFileName, accessOptions: create ! FS.Error => CONTINUE];
}
ELSE {
Copy the old log contents to a new log
newLogStream ← FS.StreamOpen[fileName: logFileName, accessOptions: create ! FS.Error => CONTINUE];
IF newLogStream #
NIL
THEN {
WHILE
NOT eventLogStream.EndOf[]
DO
newLogStream.PutChar[eventLogStream.GetChar[]];
ENDLOOP;
eventLogStream.Close[];
eventLogStream ← newLogStream;
};
};
};
IF eventLogStream =
NIL
THEN
{
MessageWindow.Append["Remember.LogFile could not be opened. Abandoning logging.", TRUE];
MessageWindow.Blink[];
}
ELSE {
eventLogStream.PutF["(%g at %t.)\n", IO.rope[logMessage], IO.time[]];
SaveEvent[event, eventLogStream];
eventLogStream.Close[];
};
};
};
User Profile Options
logFileName: ROPE;
defaultDuration: INT ← -1;
defaultLeadTime: INT ← 0;
KeyWordList: TYPE = LIST OF REF KeyWord;
KeyWord: TYPE = RECORD [key: ROPE, defaults: ParameterList];
keyWordList: KeyWordList ← NIL;
Options: UserProfile.ProfileChangedProc = {
--PROC [reason: ProfileChangeReason]--
keyWords: Rope.ROPE;
ch: CHAR;
logEvents: BOOLEAN ← UserProfile.Boolean["Remember.CreateLog", FALSE];
logFileName ← UserProfile.Token["Remember.LogFile", "///RememberEvents.log"];
IF logEvents
AND NOT logFileName.IsEmpty
AND reason = firstTime
THEN {
IO.Close[
FS.StreamOpen[fileName: logFileName, accessOptions: create !
FS.Error =>
{
MessageWindow.Append["Remember.LogFile could not be opened. Abandoning logging.", TRUE];
MessageWindow.Blink[];
eventFile ← NIL;
CONTINUE;
}]];
};
defaultLeadTime ← UserProfile.Number["Remember.LeadTime", 0];
defaultDuration ← UserProfile.Number["Remember.Duration", -1];
eventFile ← UserProfile.Token["Remember.EventFile", "///RememberEvents.txt"];
ch ← eventFile.Fetch[1];
IF FileNames.IsRemote[eventFile]
OR (ch # '[
AND ch # '/)
THEN {
MessageWindow.Append["Remember.EventFile must be a local full path name.", TRUE];
MessageWindow.Blink[];
eventFile ← "///RememberEvents.txt";
};
keyWords ← UserProfile.Token["Remember.KeyWords"];
keyWordList ← NIL;
IF keyWords #
NIL
THEN {
stream: IO.STREAM = IO.RIS[keyWords];
scratchStream: IO.STREAM;
UNTIL
IO.EndOf[stream]
DO
key, line: Rope.ROPE;
line ← IO.GetLineRope[stream];
scratchStream ← IO.RIS[line, scratchStream];
key ← IO.GetTokenRope[scratchStream ! IO.EndOfStream => CONTINUE].token;
IF key #
NIL
THEN
keyWordList ← CONS[NEW[KeyWord ← [key, ReadParameters[scratchStream]]], keyWordList];
ENDLOOP;
};
};
RegisterKeyWord:
PUBLIC
PROC [keyWord:
ROPE, parameters: ParameterList] = {
keyWordList ← CONS[NEW[KeyWord ← [keyWord, parameters]], keyWordList];
};
Miscellaneous
ComputeRepetitionInterval:
PROCEDURE [r: Rope.
ROPE, time: Tempus.PackedSeconds]
RETURNS[Tempus.PackedSeconds] = {
ENABLE Tempus.Error => ERROR Tempus.Unintelligible[rope: r, vicinity: -1, ec: ec];
t: Tempus.Packed = Tempus.SecondsToPacked[time];
RETURN[Tempus.PackedToSeconds[
IF Rope.Equal[r, "HOURLY", FALSE] THEN Tempus.Adjust[baseTime: t, hours: 1, precisionOfResult: minutes].time
ELSE IF Rope.Equal[r, "DAILY", FALSE] THEN Tempus.Adjust[baseTime: t, days: 1, precisionOfResult: minutes].time
ELSE
IF Rope.Equal[r, "WEEKDAYS",
FALSE]
THEN
Tempus.Adjust[baseTime: t, days: IF BasicTime.Unpack[t].weekday = Friday THEN 3 ELSE 1, precisionOfResult: minutes].time
ELSE IF Rope.Equal[r, "WEEKLY", FALSE] THEN Tempus.Adjust[baseTime: t, days: 7, precisionOfResult: minutes].time
ELSE IF Rope.Equal[r, "BIWEEKLY", FALSE] THEN Tempus.Adjust[baseTime: t, days: 14, precisionOfResult: minutes].time
ELSE IF Rope.Equal[r, "FORTNIGHT", FALSE] THEN Tempus.Adjust[baseTime: t, days: 14, precisionOfResult: minutes].time
ELSE IF Rope.Equal[r, "MONTHLY", FALSE] THEN Tempus.Adjust[baseTime: t, months: 1, precisionOfResult: minutes].time
ELSE IF Rope.Equal[r, "YEARLY", FALSE] THEN Tempus.Adjust[baseTime: t, years: 1, precisionOfResult: minutes].time
ELSE Tempus.Parse[baseTime: t, rope: r].time]];
};
ReadTimeRope:
PROC [stream:
IO.
STREAM]
RETURNS[
ROPE] = {
RETURN[IO.GetLineRope[stream]];
};
WasAReminderDestroyed: ViewerEvents.EventProc =
TRUSTED {
PROC [viewer: ViewerClasses.Viewer, event: ViewerEvent]
prop: REF ANY ← ViewerOps.FetchProp[viewer, $Remember];
IF prop #
NIL
THEN {
event: Event = NARROW[prop];
IF event.destroyed THEN RETURN
ELSE
IF event.justPretending
THEN {
event.justPretending ← FALSE;
event.newStartTime ← TRUE;
event.viewer ← NIL;
RETURN;
}
ELSE IF event.newStartTime THEN RETURN; -- e.g. reset via snooze alarm
event.destroyed ← TRUE;
IF event.repeat #
NIL
THEN {
IF event.nextNotification = 0 THEN ERROR;
event.timeToStartNotification ← event.nextNotification;
};
Process.Detach[FORK Save[]]; -- rewrite remember.txt
};
};
stopWatching: BOOLEAN ← FALSE;
FileWatcher:
PROC = {
fsEvent: REF READONLY FSBackdoor.CreateEvent ← NIL;
MatchFileNames:
PROC [a, b:
ROPE]
RETURNS [
BOOLEAN] ~ {
aFName, bFName: ROPE;
aCP, bCP: FS.ComponentPositions;
[fullFName: aFName, cp: aCP] ← FS.ExpandName[a ! FS.Error => GOTO Fail];
[fullFName: bFName, cp: bCP] ← FS.ExpandName[b ! FS.Error => GOTO Fail];
RETURN [(aCP.ext.start+aCP.ext.length) = (bCP.ext.start+bCP.ext.length) AND
Rope.Run[s1: aFName, s2: bFName, case: FALSE] >= (aCP.ext.start+aCP.ext.length)];
EXITS Fail => RETURN[FALSE];
};
DO
fsEvent ← FSBackdoor.NextCreateEvent[fsEvent];
IF stopWatching THEN EXIT;
IF MatchFileNames[eventFile, fsEvent.fName]
THEN {
fsEventTime: BasicTime.GMT ← BasicTime.nullGMT;
[created: fsEventTime] ← FS.FileInfo[name: fsEvent.fName, remoteCheck: FALSE ! FS.Error => CONTINUE];
IF fsEventTime # eventFileCreateTime THEN [] ← Construct[];
};
ENDLOOP;
};
RememberMostRecentEventFile:
PROC [s:
IO.
STREAM] ~ {
[created: eventFileCreateTime] ← FS.GetInfo[FS.OpenFileFromStream[s]];
};
CheckpointProc: Booting.CheckpointProc = {
DisableEventMinder: ENTRY PROC = {eventMinderDisabled ← TRUE};
DisableEventMinder[];
};
RollbackProc: Booting.RollbackProc = {
EnableEventMinder: ENTRY PROC = {eventMinderDisabled ← FALSE};
[] ← Construct[];
EnableEventMinder[];
};
Initialization
UserProfile.CallWhenProfileChanges[Options];
[] ← ViewerEvents.RegisterEventProc[proc: WasAReminderDestroyed, filter: $Text, event: destroy];
[] ← Construct[];
TRUSTED {Booting.RegisterProcs[c: CheckpointProc, r: RollbackProc]};
TRUSTED {Process.Detach[FORK EventMinder[]]};
TRUSTED {Process.Detach[
FORK FileWatcher[]]};
END.