DIRECTORY
Atom USING [MakeAtom, GetProp, PutProp, RemProp],
CedarScanner USING [ContentsFromToken, RopeFromToken, TokenKind],
CedarSnapshot USING[After, Register],
CIFS USING [Error],
Convert USING[Parse, Value, DefaultUnsigned],
FileIO USING[Open],
Icons USING [IconFlavor],
IconRegistry USING [GetIcon],
IO USING[BreakProc, Close, CreateViewerStreams, EndOf, EndOfStream, EveryThing, Flush, GetChar, PeekChar, GetBlock, GetIndex, GetLength, GetSequence, GetToken, int, PutBlock, PutF, PutRope, RIS, rope, SetIndex, SetLength, SkipOver, STREAM, SyntaxError, time, WhiteSpace],
IOExtras USING [GetLine, GetCedarScannerToken, FromTokenProc],
List USING [Sort, CompareProc],
Menus USING [Menu, CreateMenu, GetLine, SetLine, GetNumberOfLines, MenuEntry, ReplaceMenuEntry, ClickProc, FindEntry, CreateEntry, AppendMenuEntry],
MessageWindow USING [Append, Blink],
Process USING[Pause, SecondsToTicks, Detach],
ReminderDefs,
ReminderDefsPrivate,
Rope USING[ROPE, Cat, Concat, Fetch, Equal, Substr],
SafeStorage USING [NarrowFault],
Tempus USING [Adjust, defaultTime, Error, MakeRope, Packed, PackedSeconds, Current, Seconds, PackedToSeconds, SecondsToPacked, Parse, Precision, Unpack, Unintelligible],
TiogaExtraOps USING [GetFile],
TiogaMenuOps USING [Normalize],
TiogaOps USING [Ref, FirstChild, 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],
ViewerSpecs USING [openBottomY],
ViewerTools USING[MakeNewTextViewer, GetSelectionContents, 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], 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]];
};
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, repeat, iconFlavor, iconLabel: Rope.ROPE;
timeToStartNotification, nextNotification: PackedSeconds ← [0];
durationTime, leadTime: 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,
ENDCASE => IO.WhiteSpace[char]
];
};
GetRopeOrToken:
PROC
RETURNS [value:
ROPE] = {
fromTokenProc: IOExtras.FromTokenProc
-- [closure: CedarScanner.GetClosure, token: CedarScanner.Token] -- = {
SELECT token.kind
FROM
tokenROPE => value ← CedarScanner.RopeFromToken[closure, token];
ENDCASE => value ← CedarScanner.ContentsFromToken[closure, token];
};
IOExtras.GetCedarScannerToken[h, fromTokenProc];
};
defaults: ParameterList;
iconLabelType: Rope.ROPE;
token ← h.GetToken[breakProc];
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.GetToken[breakProc];
IF Rope.Fetch[iconLabel, 0]
IN ['A..'Z]
THEN {
pos: INT = h.GetIndex[];
r: ROPE;
WHILE Rope.Fetch[r ← h.GetToken[breakProc], 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
t: Packed ← [0];
[t, precision] ← Tempus.Parse[rope: scanThis, baseTime: itIsNow ! Tempus.Unintelligible => IF ec = nothingSpecified THEN CONTINUE]; -- time may be specified via keywords
IF t # 0 THEN 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 ← LongCardFromRope[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 {
IF
NOT Rope.Equal[duration, "NIL"]
THEN {
leadTime ← LongCardFromRope[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 timeToStartNotification ← [timeToStartNotification - leadTime * 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
event ←
NEW[EventRecord ← [
timeToStartNotification: timeToStartNotification,
text: text,
repeat: repeat,
nextNotification: nextNotification,
durationTime: durationTime,
leadTime: leadTime,
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;
};
Event Minder
EventMinder:
PROC = {
DO
EnterEventMinder[itIsNow];
Process.Pause[Process.SecondsToTicks[3]];
ENDLOOP;
};
EnterEventMinder:
PUBLIC ENTRY
PROC [itIsNow: Packed] =
{
ENABLE
UNWIND =>
NULL;
now: PackedSeconds ← Tempus.PackedToSeconds[itIsNow];
FOR el:
LIST
OF Event ← eventList, el.rest
UNTIL el =
NIL DO
-- for each event.
event: Event = el.first;
startTime: PackedSeconds ← event.timeToStartNotification;
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;
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 now > startTime
THEN {
create a new viewer if there just occurred a new start time (the first qualifies) and its duration has not expired
IF event.newStartTime
AND (event.durationTime = 0
OR now < startTime + event.durationTime)
THEN {
viewer: Viewer ← event.viewer;
text: ROPE ← Rope.Cat[Tempus.MakeRope[], ".\n", event.text];
event.newStartTime ← FALSE;
event.blink ← TRUE;
event.justPretending ← (itIsNow # Tempus.defaultTime);
IF viewer =
NIL
OR viewer.destroyed
THEN {
viewer ← ViewerTools.MakeNewTextViewer[
info: [name: event.text, data: text, icon: IconRegistry.GetIcon[el.first.iconFlavor, document]],
paint: FALSE];
Menus.SetLine[menu: viewer.menu, line: Menus.GetNumberOfLines[viewer.menu], entryList: reminderButtons];
};
IF event.message #
NIL
THEN {
IF walnutUser
THEN {
IF NOT WalnutDisplayerOps.StuffMsgContents[viewer, event.message] THEN ViewerTools.SetContents[viewer: viewer, contents: Rope.Cat[text, "\n(Could not find message: ", event.message, ")\n"], paint: FALSE]
}
ELSE
IF peanutUser
THEN {
stream: IO.STREAM = IO.RIS[event.message];
fileName: ROPE = IO.GetToken[stream];
message: ROPE;
fileAtom: ATOM = Atom.MakeAtom[fileName];
mailViewer: ViewerClasses.Viewer;
mailDoc: TiogaOps.Ref;
[] ← IO.GetChar[stream]; -- the extra space
message ← IO.GetSequence[stream, IO.EveryThing];
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 ! CIFS.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.SaveSpanForPaste[startLoc: [node, 0], endLoc: TiogaOps.LastLocWithin[node]];
TiogaOps.SelectDocument[viewer: viewer, pendingDelete: TRUE];
TiogaOps.Paste[];
TiogaMenuOps.Normalize[viewer: viewer];
viewer.newVersion ← FALSE;
viewer.icon ← document;
TiogaOps.RestoreSelB[];
TiogaOps.UnlockSel[];
EXIT;
}
ELSE
IF (node ← TiogaOps.Next[node]) =
NIL
THEN {
text ← Rope.Cat[text, "\n(Could not find message: ", message];
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, $Reminder, el.first]; -- for menus
event.viewer ← viewer;
ViewerOps.PaintViewer[viewer, all];
IF logEvents
THEN {
eventLogStream.PutF["(Following Reminder posted at %t.)\n", IO.time[itIsNow]];
SaveEvent[event, eventLogStream];
eventLogStream.Flush[];
};
};
IF event.durationTime # 0
-- 0 means forever
AND now > startTime + event.durationTime
THEN {
IF event.justPretending THEN NULL
ELSE IF event.repeat #
NIL
THEN {
IF event.nextNotification = 0 THEN ERROR;
event.timeToStartNotification ← event.nextNotification;
}
ELSE
IF
NOT event.destroyed
THEN TRUSTED {
event.destroyed ← TRUE;
Process.Detach[FORK Save[]];
};
IF 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];
};
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;
};
Blinker: Menus.ClickProc
-- [parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE] -- = {
viewer: ViewerClasses.Viewer = NARROW[parent];
event: Event = NARROW[ViewerOps.FetchProp[viewer, $Reminder]];
event.blink ← NOT event.blink;
MessageWindow.Append[Rope.Concat["Blinker turned ", IF event.blink THEN "on." ELSE "off."], TRUE];
};
Relabel: Menus.ClickProc
-- [parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE] -- = {
viewer: ViewerClasses.Viewer = NARROW[parent];
event: Event = NARROW[ViewerOps.FetchProp[viewer, $Reminder]];
sel: ROPE = ViewerTools.GetSelectionContents[];
event.iconLabel ← sel;
ViewerOps.AddProp[viewer, $IconLabel, event.iconLabel];
viewer.name ← sel;
ViewerOps.PaintViewer[viewer, caption];
};
SnoozeAlarm: Menus.ClickProc
-- [parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE] -- = {
viewer: ViewerClasses.Viewer = NARROW[parent];
event: Event ← NARROW[ViewerOps.FetchProp[viewer, $Reminder]];
delta: Seconds =
(IF mouseButton = red THEN 15*60
ELSE IF mouseButton = yellow THEN 60*60
ELSE LONG[60]*60*24); -- this should actually call AdjustTime, i.e. tomorrow.
IF event.repeat #
NIL
THEN {
-- must create a new event because there is no way to write out a single event consisting of a repeating event, and a specific instance with a different time, and we have to write this out so that if user rollsback or boots, the event that has been reset will still be remembered.
event ← NEW[EventRecord ← event^];
event.repeat ← NIL;
event.nextNotification ← [0]; -- paranoid
eventList ← CONS[event, eventList];
};
IF NOT shift THEN event.timeToStartNotification ← [MAX[Tempus.PackedToSeconds[itIsNow], event.timeToStartNotification] + delta] -- first snooze means from now, subsequent snooze means from that point.
ELSE event.timeToStartNotification ← [event.timeToStartNotification - delta]; -- this is so you can reset for several hours, then back up 15 minutes. etc.
event.newStartTime ← TRUE;
TRUSTED {Process.Detach[FORK Save[]]};
event.blink ← FALSE;
MessageWindow.Append[Rope.Concat["Reminder reset for ", Tempus.MakeRope[time: Tempus.SecondsToPacked[event.timeToStartNotification], includeDayOfWeek: TRUE]], TRUE];
};
ResetTime: Menus.ClickProc
-- [parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE] -- = {
viewer: ViewerClasses.Viewer = NARROW[parent];
event: Event = NARROW[ViewerOps.FetchProp[viewer, $Reminder]];
sel: ROPE = ViewerTools.GetSelectionContents[];
t: Tempus.Packed;
{t ← Tempus.Parse[rope: sel, baseTime: Tempus.SecondsToPacked[[MAX[Tempus.PackedToSeconds[itIsNow], event.timeToStartNotification]]] ! Tempus.Unintelligible => GOTO Bogus].time;
event.timeToStartNotification ← Tempus.PackedToSeconds[t];
event.newStartTime ← TRUE;
TRUSTED {Process.Detach[FORK Save[]]};
MessageWindow.Append[Rope.Concat["Reminder reset for ", Tempus.MakeRope[t]], TRUE];
EXITS
Bogus => {MessageWindow.Append[Rope.Concat["Unintelligible time: ", sel], TRUE]; MessageWindow.Blink[]};
};
};
Cancel: Menus.ClickProc
-- [parent: REF ANY, clientData: REF ANY ← NIL, mouseButton: Menus.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE] -- = {
viewer: ViewerClasses.Viewer = NARROW[parent];
event: Event = NARROW[ViewerOps.FetchProp[viewer, $Reminder]];
event.repeat ← NIL;
event.justPretending ← FALSE;
event.destroyed ← TRUE;
MessageWindow.Append["Event now forgotten.", TRUE];
TRUSTED {Process.Detach[FORK Save[]]};
};
Saving Events
Save:
ENTRY PROC = {
OPEN IO;
ENABLE UNWIND => NULL;
stream: IO.STREAM = FileIO.Open[fileName: "Reminders.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["Reminders.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.timeToStartNotification < now
AND --
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. before the bit is set.
OR (event.durationTime # 0 AND event.timeToStartNotification + event.durationTime < now) -- this is an old event and no longer interesting
THEN 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["Reminders.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.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 = FileIO.Open[fileName: dollarName, accessOptions: overwrite, createLength: 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
MenuEntry:
PROC [menu: Menus.Menu, name:
ROPE, proc: Menus.ClickProc] = {
entry: Menus.MenuEntry = Menus.CreateEntry[name: name, proc: proc, fork: FALSE];
oldEntry: Menus.MenuEntry = Menus.FindEntry[menu, name];
IF oldEntry = NIL THEN Menus.AppendMenuEntry[menu: menu, entry: entry]
ELSE Menus.ReplaceMenuEntry[menu: menu, oldEntry: oldEntry, newEntry: entry];
};
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 Tempus.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]];
};
LongCardFromRope:
PROC [rope:
ROPE]
RETURNS [
LONG
CARDINAL] = {
v: Convert.Value = Convert.Parse[text: [rope[rope]], template: Convert.DefaultUnsigned].value;
RETURN [NARROW[v, Convert.Value[unsigned] ! SafeStorage.NarrowFault => Error[formatError, rope]].unsigned]};
ReadTimeRope:
PROC [stream:
IO.
STREAM]
RETURNS[
ROPE] = {
RETURN[IOExtras.GetLine[stream]];
};
WasAReminderDestroyed: ViewerEvents.EventProc
-- [viewer: ViewerClasses.Viewer, event: ViewerEvent] -- =
TRUSTED {
prop: REF ANY ← ViewerOps.FetchProp[viewer, $Reminder];
IF prop #
NIL
THEN {
event: Event = NARROW[prop];
IF event.destroyed THEN RETURN;
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: "Reminders.txt", case:
FALSE]
THEN [] ← Construct[];
};
RollbackProc:
PROC[situation: CedarSnapshot.After] = {
IF situation = rollback THEN [] ← Construct[]};
Initialization
remindMenu: Menus.Menu ← Menus.CreateMenu[];
reminderButtons: Menus.MenuEntry;
Menus.AppendMenuEntry[menu: remindMenu, entry: Menus.CreateEntry["Blinker", Blinker]];
Menus.AppendMenuEntry[menu: remindMenu, entry: Menus.CreateEntry["Relabel", Relabel]];
Menus.AppendMenuEntry[menu: remindMenu, entry: Menus.CreateEntry["15 min/hour/day", SnoozeAlarm]];
Menus.AppendMenuEntry[menu: remindMenu, entry: Menus.CreateEntry["NewTime", ResetTime]];
Menus.AppendMenuEntry[menu: remindMenu, entry: Menus.CreateEntry["Forget", Cancel]];
reminderButtons ← Menus.GetLine[remindMenu, 0];
UserProfile.CallWhenProfileChanges[Options];
[] ← ViewerEvents.RegisterEventProc[proc: WasAReminderDestroyed, filter: $Text, event: destroy];
[] ← ViewerEvents.RegisterEventProc[CheckSavedFile, save, $Text, FALSE];
TRUSTED {CedarSnapshot.Register[r: RollbackProc]};
TRUSTED {Process.Detach[FORK EventMinder[]]};