-- 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: BOOL _ TRUE, 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: ROPE _ NIL; {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.STREAM _ IO.CreateViewerStreams["Reminder Error"].out; eventList _ NIL; out.PutRope["Reminder: Format error in Reminders.txt\n"]}; timeRopeFormatError => {out: IO.STREAM _ IO.CreateViewerStreams["Reminder Error"].out; eventList _ NIL; out.PutRope["Reminder: DateAndTime.Unintelligible for "]; out.PutRope[timeRope]; out.PutRope["\n"]; }; noReminderFile => {out: IO.STREAM _ IO.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: BOOL _ FALSE] = { 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: ROPE _ NIL] = { 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: ROPE _ NIL] = { 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"