DIRECTORY
Atom USING [MakeAtom, GetProp, PutProp, RemProp],
CommandTool USING[DoCommand],
Convert USING[CardFromRope],
BasicTime USING[nullGMT, Unpack],
Booting USING[CheckpointProc, RegisterProcs, RollbackProc],
FS USING [Error, StreamOpen],
Icons USING [IconFlavor],
IconRegistry USING [GetIcon],
IO USING[GetCedarTokenRope, TokenKind, BreakProc, Close, EndOf, EndOfStream, Error, Flush, GetChar, PeekChar, GetBlock, GetIndex, GetLength, GetTokenRope, int, PutBlock, PutF, PutRope, RIS, rope, SetIndex, SetLength, SkipWhitespace, STREAM, time, GetLineRope],
List USING [Sort, CompareProc],
Menus USING [SetLine, GetNumberOfLines, MenuEntry],
MessageWindow USING [Append, Blink],
Process USING[Pause, SecondsToTicks, Detach],
RememberDefs,
RememberDefsPrivate,
Rope USING[ROPE, Cat, Concat, Fetch, Equal, Substr],
Tempus USING [Adjust, defaultTime, Error, MakeRope, Packed, PackedSeconds, Seconds, PackedToSeconds, SecondsToPacked, Parse, Precision, Unintelligible],
TiogaExtraOps USING [GetFile],
TiogaOps USING [Ref, FirstChild, Jump, GetRope, 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],
ViewerOps USING[AddProp, FetchProp, PaintViewer, BlinkIcon, DestroyViewer, FindViewer, RestoreViewer],
WalnutDisplayerOps USING [StuffMsgContents]
;
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]];
};
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;
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].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].token;
IF Rope.Fetch[iconLabel, 0]
IN ['A..'Z]
THEN {
pos: INT = h.GetIndex[];
r: ROPE;
WHILE Rope.Fetch[r ← h.GetTokenRope[breakProc].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 (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];
};
Saving Events
Save:
PUBLIC ENTRY PROC = {
OPEN IO;
ENABLE UNWIND => NULL;
stream: IO.STREAM = FS.StreamOpen[fileName: "RememberEvents.txt", accessOptions: write];
viewer: ViewerClasses.Viewer;
now: PackedSeconds = Tempus.PackedToSeconds[];
newEventList: LIST OF Event ← NIL;
compare: List.CompareProc = {
e1, e2: Event;
e1 ← NARROW[ref1];
e2 ← NARROW[ref2];
RETURN[IF e1.timeToStartNotification < e2.timeToStartNotification THEN less ELSE greater];
};
SaveOld["RememberEvents.txt", stream];
IO.SetLength[stream, 0];
FOR l:
LIST
OF Event ← eventList, l.rest
UNTIL l =
NIL
DO
event: Event ← l.first;
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 logEvents
THEN {
eventLogStream.PutF["(Following Event deleted at %t.)\n", IO.time[]];
SaveEvent[event, eventLogStream];
eventLogStream.Flush[];
};
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;
stream.Close[];
IF (viewer ← ViewerOps.FindViewer["RememberEvents.txt"]) # 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]];
};
SaveOld:
PROC [name:
Rope.ROPE, stream:
IO.
STREAM] = {
len: INT = IO.GetLength[stream];
dollarName: Rope.ROPE = Rope.Concat[name, "$"];
dollarStream: IO.STREAM = FS.StreamOpen[fileName: dollarName, accessOptions: create, createByteCount: len ! ANY => GOTO Out];
bufferSize: INT = 512;
buffer: REF TEXT = NEW[TEXT[bufferSize]];
bytes: INT;
IO.SetIndex[stream, 0];
WHILE (bytes ← stream.GetBlock[buffer]) > 0
DO
dollarStream.PutBlock[buffer];
ENDLOOP;
dollarStream.Close[];
};
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, "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
-- [viewer: ViewerClasses.Viewer, event: ViewerEvent] -- =
TRUSTED {
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 reminder.txt
};
CheckSavedFile: ViewerEvents.EventProc = {
IF Rope.Equal[s1: viewer.name, s2: "RememberEvents.txt", case:
FALSE]
THEN [] ← Construct[];
};
CheckpointProc: Booting.CheckpointProc = {
DisableEventMinder: ENTRY PROC = {eventMinderDisabled ← TRUE};
IF eventLogStream #
NIL
THEN {
IO.Close[eventLogStream];
eventLogStream ← NIL;
};
DisableEventMinder[];
};
RollbackProc: Booting.RollbackProc = {
EnableEventMinder: ENTRY PROC = {eventMinderDisabled ← FALSE};
IF logEvents THEN eventLogStream ← FS.StreamOpen[fileName: "Remember.log", accessOptions: append];
[] ← Construct[];
EnableEventMinder[];
};