-- Reminder.mesa
-- Last Modified On February 9, 1983 12:24 pm 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],
CedarSnapshot USING[After, Register],
Convert USING[Parse, Value, DefaultUnsigned],
DateAndTime USING[Parse],
FileIO USING[Open],
IO USING[STREAM, GetChar, Close, PutRope, EndOfStream, CreateViewerStreams],
Process USING[Pause, SecondsToTicks, Detach],
Rope USING[ROPE, Concat, FromChar, Equal],
System USING[SecondsSinceEpoch, GetGreenwichMeanTime],
ViewerClasses USING[Viewer],
ViewerEvents USING[RegisterEventProc, ViewerEvent],
ViewerTools USING[MakeNewTextViewer],
ViewerOps USING[BlinkIcon, DestroyViewer];

Reminder: CEDAR MONITOR

IMPORTS CedarSnapshot, Convert, DateAndTime, FileIO, IO, Process, Rope, System,
ViewerEvents, ViewerOps, ViewerTools

= BEGIN OPEN Ascii, Rope, ViewerClasses, ViewerEvents, ViewerOps;

Event: TYPE = REF EventObj;
EventObj: TYPE = RECORD[text: ROPE,
timeRope: ROPE,
timeToStartNotification: LONG CARDINAL ← 0,
newStartTime: BOOLTRUE,
repetitionInterval: LONG CARDINAL ← 0,
duration: LONG CARDINAL ← 0,
viewer: Viewer ← NIL];
ParameterClass: TYPE = {message, repeat, duration--minutes--, until, time};

eventList: LIST OF Event ← NIL;

FormatError: ERROR = CODE;

Construct: ENTRY PROC =
{ ENABLE UNWIND => NULL;
fileStream: IO.STREAM;
timeRope: ROPENIL;

{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 ← FileIO.Open["Reminders.txt", read ! ANY => CONTINUE];
IF fileStream = NIL THEN GOTO noReminderFile;

DO --ENABLE ANY => EXIT;
time: LONG CARDINAL;
text: ROPE;
class: ParameterClass;
repetitionInterval: LONG CARDINAL ← 0;
duration: LONG CARDINAL ← 0;

timeRope ← ReadTimeRope[fileStream];
IF timeRope = NIL THEN EXIT;
[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 ← LongCardFromRope[text]*60;
until => duration ← DateAndTime.Parse[text ! ANY => GOTO formatError].dt
- DateAndTime.Parse[timeRope
! ANY => GOTO timeRopeFormatError].dt;
ENDCASE;
[class, text] ← ReadParameter[fileStream ! FormatError => GOTO formatError];
ENDLOOP;
time ← System.SecondsSinceEpoch[DateAndTime.Parse[timeRope
! ANY => GOTO timeRopeFormatError].dt];
{now: LONG CARDINAL = System.SecondsSinceEpoch[System.GetGreenwichMeanTime[]];
IF repetitionInterval > 0
THEN UNTIL time + repetitionInterval > now
DO time ← time + repetitionInterval;
ENDLOOP;
eventList ← CONS[NEW[EventObj ← [timeToStartNotification: time,
timeRope: timeRope,
text: text,
repetitionInterval: repetitionInterval,
duration: duration]],
eventList]};
ENDLOOP;
fileStream.Close[];
EXITS
formatError =>
{out: IO.STREAMIO.CreateViewerStreams["Reminder Error"].out;
eventList ← NIL;
out.PutRope["Reminder: Format error in Reminders.txt\n"]};
timeRopeFormatError =>
{out: IO.STREAMIO.CreateViewerStreams["Reminder Error"].out;
eventList ← NIL;
out.PutRope["Reminder: DateAndTime.Unintelligible for "];
out.PutRope[timeRope];
out.PutRope["\n"];
};
noReminderFile =>
{out: IO.STREAMIO.CreateViewerStreams["Reminder Error"].out;
eventList ← NIL;
out.PutRope["Reminder: Can't find the file named Reminders.txt\n"]};
};
};

LongCardFromRope: PROC [rope: ROPE] RETURNS [LONG CARDINAL] = INLINE {
v: Convert.Value = Convert.Parse[text: [rope[rope]], template: Convert.DefaultUnsigned].value;
RETURN [NARROW[v, Convert.Value[unsigned]].unsigned]};

CheckSavedFile: PROC[viewer: Viewer, event: ViewerEvent, before: BOOL]
RETURNS[abort: BOOLFALSE] =
{ IF Equal[s1: viewer.name, s2: "Reminders.txt", case: FALSE]
THEN [] ← Construct[];
};

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: LONG CARDINAL = System.SecondsSinceEpoch[System.GetGreenwichMeanTime[]];
-- secs
FOR el: LIST OF Event ← eventList, el.rest UNTIL el = NIL
DO -- for each event.
startTime: LONG CARDINAL;
IF el.first.repetitionInterval > 0 -- compute the most recent startTime
THEN UNTIL el.first.timeToStartNotification + el.first.repetitionInterval > now
DO el.first.timeToStartNotification
← el.first.timeToStartNotification + el.first.repetitionInterval;
el.first.newStartTime ← TRUE;
ENDLOOP;
startTime ← el.first.timeToStartNotification;
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
-- and there isn't already an active viewer for this event
IF el.first.newStartTime
AND (el.first.duration = 0 -- means forever
OR now-startTime < 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 now-startTime > 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;
};

RollbackProc: PROC[situation: CedarSnapshot.After] =
{IF situation = rollback THEN [] ← Construct[]};

-- MODULE INITIALIZATION

[] ← RegisterEventProc[CheckSavedFile, save, $Text, FALSE];
TRUSTED {CedarSnapshot.Register[r: RollbackProc]};
TRUSTED {Process.Detach[FORK EventMinder[]]};
[] ← Construct[];

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"