Reminder.mesa
Last Modified On December 15, 1983 10:14 am By Paul Rovner
Reminder reads a text file named Reminders.txt and FORKs a process to post notices as flashing, iconic viewers at or after the specified time.
Reminder is an interim (!) hack until Hickory is ready for general use.
The format of the file is a sequence of entries separated by CR, each entry being a time specification (e.g. July 29, 1982 3:25 pm) ending with CR, followed by zero or more (optional) parameter specifications (see list below), followed by a ROPE constant that is the text of the notice. The text following the END. below is an example Reminders.txt.
Until a better date and time parser is available, an "optional parameter" facility (keyword including ': followed by parameter value) is provided as a stopgap. Case is ignored.
REPEAT: {one of: HOURLY, DAILY, WEEKLY, YEARLY}
this causes the notice to be posted at the specified interval.
MONTHLY isn't implemented yet
DURATION: {number of minutes}
automatically kill the notice after this duration
UNTIL: {timestamp}
automatically kill the notice after this time
You can open a reminder icon. It will resume blinking when you close it. You can delete a Reminder viewer. After editing your Reminders.txt file and saving it, Reminder will automatically discard the old list of reminders and re-read the file to construct a new one. This also happens automatically after a Rollback.
DIRECTORY
Ascii USING[SP, TAB, CR],
BasicTime USING[Period, Now, Update, GMT],
Booting USING[RollbackProc, RegisterProcs],
Convert USING[CardFromRope, TimeFromRope, Error],
FS USING[Error, StreamOpen],
IO USING[STREAM, GetChar, Close, PutRope, EndOfStream],
Process USING[Pause, SecondsToTicks, Detach],
Rope USING[ROPE, Concat, FromChar, Equal, Match],
ViewerClasses USING[Viewer],
ViewerEvents USING[RegisterEventProc, ViewerEvent],
ViewerIO USING[CreateViewerStreams],
ViewerOps USING[BlinkIcon, DestroyViewer],
ViewerTools USING[MakeNewTextViewer];
Reminder: CEDAR MONITOR
IMPORTS BasicTime, Booting, Convert, FS, IO, Process, Rope,
ViewerEvents, ViewerIO, ViewerOps, ViewerTools
= BEGIN OPEN Ascii, Rope, ViewerClasses, ViewerEvents, ViewerOps;
Event: TYPE = REF EventObj;
EventObj: TYPE = RECORD[text: ROPE,
timeRope: ROPE,
timeToStartNotification: BasicTime.GMT,
newStartTime: BOOLTRUE,
repetitionInterval: INT ← 0,
duration: INT ← 0,
viewer: Viewer ← NIL];
ParameterClass: TYPE = {message, repeat, duration--minutes--, until, time};
eventList: LIST OF Event ← NIL;
FormatError: ERROR = CODE;
Construct: ENTRY Booting.RollbackProc =
{ ENABLE UNWIND => NULL;
fileStream: IO.STREAM ← NIL;
timeRope: ROPENIL;
{ -- for EXITS clause
IF eventList # NIL
THEN FOR el: LIST OF Event ← eventList, el.rest UNTIL el = NIL
DO IF el.first.viewer # NIL AND NOT el.first.viewer.destroyed
THEN DestroyViewer[el.first.viewer];
ENDLOOP;
eventList ← NIL;
fileStream ← FS.StreamOpen["Reminders.txt" ! FS.Error => GOTO noReminderFile];
DO
time: BasicTime.GMT;
text: ROPE;
class: ParameterClass;
repetitionInterval: INT ← 0;
duration: INT ← 0;
timeRope ← ReadTimeRope[fileStream];
IF timeRope = NIL THEN EXIT;
time ← Convert.TimeFromRope[timeRope
   ! Convert.Error => GOTO timeRopeFormatError];
[class, text] ← ReadParameter[fileStream ! FormatError => GOTO formatError];
UNTIL class = message
DO SELECT class FROM
repeat => IF Equal[s1: text, s2: "HOURLY", case: FALSE]
THEN {repetitionInterval ← LONG[60*60]}
ELSE IF Equal[s1: text, s2: "DAILY", case: FALSE]
THEN {repetitionInterval ← LONG[60*60]*24}
ELSE IF Equal[s1: text, s2: "WEEKLY", case: FALSE]
THEN {repetitionInterval ← LONG[60*60]*24*7}
ELSE IF Equal[s1: text, s2: "YEARLY", case: FALSE]
THEN {repetitionInterval ← LONG[60*60]*24*7*52}
ELSE GOTO formatError;
duration => duration ← Convert.CardFromRope[text]*60;
until =>
duration
← BasicTime.Period[
 to: Convert.TimeFromRope[text
 ! Convert.Error => GOTO formatError],
 from: time];
ENDCASE;
[class, text] ← ReadParameter[fileStream ! FormatError => GOTO formatError];
ENDLOOP;
{
latest: BasicTime.GMT ← time;
IF repetitionInterval > 0 THEN
WHILE BasicTime.Period[from: latest, to: BasicTime.Now[]] >= repetitionInterval
DO latest ← BasicTime.Update[latest, repetitionInterval] ENDLOOP;
eventList ← CONS[NEW[EventObj ← [timeToStartNotification: latest,
timeRope: timeRope,
text: text,
repetitionInterval: repetitionInterval,
duration: duration]],
eventList];
};
ENDLOOP;
fileStream.Close[];
EXITS
formatError => {
out: IO.STREAM ← ViewerIO.CreateViewerStreams["Reminder Error"].out;
eventList ← NIL;
out.PutRope["Reminder: Format error in Reminders.txt\n"];
};
timeRopeFormatError => {
out: IO.STREAM ← ViewerIO.CreateViewerStreams["Reminder Error"].out;
eventList ← NIL;
out.PutRope["Reminder: Unintelligible time: "];
out.PutRope[timeRope];
out.PutRope["\n"];
};
noReminderFile => {
out: IO.STREAM ← ViewerIO.CreateViewerStreams["Reminder Error"].out;
eventList ← NIL;
out.PutRope["Reminder: Can't find the file named Reminders.txt\n"];
};
};
};
CheckSavedFile: PROC[viewer: Viewer, event: ViewerEvent, before: BOOL]
RETURNS[abort: BOOLFALSE] =
{IF Match[object: viewer.name, pattern: "*Reminders.txt!*", case: FALSE] THEN Construct[NIL]};
IsBreak: PROC[ch: CHAR] RETURNS[BOOL] =
{RETURN[ch = SP OR ch = TAB OR ch = CR OR ch = '[ OR ch = ']]};
ReadParameter: PROC[fileStream: IO.STREAM]
RETURNS[parameterClass: ParameterClass ← message, text: ROPENIL] =
{ ch: CHAR ← fileStream.GetChar[! IO.EndOfStream => GOTO formatError];
WHILE IsBreak[ch]
DO ch ← fileStream.GetChar[! IO.EndOfStream => GOTO formatError];
ENDLOOP;
IF ch # '"
THEN {text ← NIL;
UNTIL IsBreak[ch]
DO text ← Concat[base: text, rest: FromChar[ch]];
ch ← fileStream.GetChar[! IO.EndOfStream => EXIT];
ENDLOOP;
here with keyword
SELECT TRUE FROM
Equal[s1: text, s2: "REPEAT:", case: FALSE]
=> {parameterClass ← repeat;
WHILE IsBreak[ch]
DO ch ← fileStream.GetChar[! IO.EndOfStream => GOTO formatError];
ENDLOOP;
text ← NIL;
UNTIL IsBreak[ch]
DO text ← Concat[base: text, rest: FromChar[ch]];
ch ← fileStream.GetChar[! IO.EndOfStream => EXIT];
ENDLOOP;
};
Equal[s1: text, s2: "DURATION:", case: FALSE]
=> {parameterClass ← duration;
WHILE IsBreak[ch]
DO ch ← fileStream.GetChar[! IO.EndOfStream => GOTO formatError];
ENDLOOP;
text ← NIL;
UNTIL IsBreak[ch]
DO text ← Concat[base: text, rest: FromChar[ch]];
ch ← fileStream.GetChar[! IO.EndOfStream => EXIT];
ENDLOOP;
};
Equal[s1: text, s2: "UNTIL:", case: FALSE]
=> {parameterClass ← until;
text ← ReadTimeRope[fileStream]};
Equal[s1: text, s2: "TIME:", case: FALSE]
=> {parameterClass ← time;
text ← ReadTimeRope[fileStream]};
Equal[s1: text, s2: "MESSAGE:", case: FALSE]
=> {parameterClass ← message;
UNTIL ch = '"
DO ch ← fileStream.GetChar[! IO.EndOfStream => GOTO formatError];
ENDLOOP;
text ← NIL;
UNTIL ch = '"
DO text ← Concat[base: text, rest: FromChar[ch]];
ch ← fileStream.GetChar[! IO.EndOfStream => GOTO formatError];
ENDLOOP;
};
ENDCASE => ERROR FormatError;
RETURN;
};
parameterClass ← message;
ch ← fileStream.GetChar[! IO.EndOfStream => GOTO formatError];
UNTIL ch = '"
DO text ← Concat[base: text, rest: FromChar[ch]];
ch ← fileStream.GetChar[! IO.EndOfStream => GOTO formatError];
ENDLOOP;
EXITS formatError => ERROR FormatError;
};
ReadTimeRope: PROC[fileStream: IO.STREAM] RETURNS[ans: ROPENIL] = {
ch: CHAR ← fileStream.GetChar[! IO.EndOfStream => GOTO exit];
WHILE ch = SP OR ch = TAB OR ch = CR
DO ch ← fileStream.GetChar[! IO.EndOfStream => GOTO exit];
ENDLOOP;
UNTIL ch = CR OR ch = ']
DO ans ← Concat[base: ans, rest: FromChar[ch]];
ch ← fileStream.GetChar[! IO.EndOfStream => GOTO exit];
ENDLOOP;
EXITS exit => NULL;
};
EventMinder: PROC = {
DO EnterEventMinder[]; Process.Pause[Process.SecondsToTicks[3]]; ENDLOOP;
};
EnterEventMinder: ENTRY PROC =
{ ENABLE UNWIND => NULL;
now: BasicTime.GMT = BasicTime.Now[];
FOR el: LIST OF Event ← eventList, el.rest UNTIL el = NIL
DO -- for each event.
startTime: BasicTime.GMT;
IF el.first.repetitionInterval > 0 THEN { -- compute the most recent startTime
latest: BasicTime.GMT ← el.first.timeToStartNotification;
WHILE BasicTime.Period[from: latest, to: BasicTime.Now[]] >= el.first.repetitionInterval
DO
latest ← BasicTime.Update[latest, el.first.repetitionInterval];
el.first.newStartTime ← TRUE;
ENDLOOP;
el.first.timeToStartNotificationlatest;
};
startTime ← el.first.timeToStartNotification;
IF BasicTime.Period[from: startTime, to: now] > 0
THEN {
create a new viewer if there just occurred a new start time (the first qualifies) and its duration has not expired and there isn't already an active viewer for this event
IF el.first.newStartTime
AND (el.first.duration = 0 -- means forever
  OR BasicTime.Period[from: startTime, to: now] < el.first.duration)
AND (el.first.viewer = NIL OR el.first.viewer.destroyed)
THEN {
el.first.newStartTime ← FALSE;
el.first.viewer ← ViewerTools.MakeNewTextViewer
[[ name: el.first.text,
data: Concat[base: el.first.timeRope, rest: Concat[": ", el.first.text]] ]];
};
destroy an existing viewer if a duration is specified and it has lapsed
IF el.first.duration # 0 -- 0 means forever
AND BasicTime.Period[from: startTime, to: now] > el.first.duration
AND el.first.viewer # NIL
AND NOT el.first.viewer.destroyed
THEN DestroyViewer[el.first.viewer];
blink the viewer if there is an active one and it is iconic
IF el.first.viewer # NIL
AND (NOT el.first.viewer.destroyed)
AND el.first.viewer.iconic
THEN BlinkIcon[el.first.viewer];
};
ENDLOOP;
};
MODULE INITIALIZATION
[] ← RegisterEventProc[CheckSavedFile, save, $Text, FALSE];
TRUSTED {Booting.RegisterProcs[r: Construct]};
TRUSTED {Process.Detach[FORK EventMinder[]]};
Construct[NIL];
END.
July 29, 1982 5:00 pm
REPEAT: DAILY
DURATION: 60
"Time to go home."
July 28, 1982 1:00 pm
REPEAT: WEEKLY
DURATION: 30
"Dealer at 1:15"
July 30, 1982 1:10 pm
DURATION: 30
"Mike Feuer arriving at 1:30"