DIRECTORY Atom USING [MakeAtom, GetProp, PutProp, RemProp], CommandTool USING[DoCommand], Convert USING[CardFromRope, RopeFromLiteral], BasicTime USING[GMT, nullGMT, Unpack], Booting USING[CheckpointProc, RegisterProcs, RollbackProc], FileNames USING [IsRemote], FS, FSBackdoor, Icons USING [IconFlavor], IconRegistry USING [GetIcon], IO, List USING [Sort, CompareProc], Menus USING [SetLine, GetNumberOfLines, MenuEntry], MessageWindow USING [Append, Blink], Process USING[Pause, SecondsToTicks, Detach], Remember, RememberPrivate, Rope, Tempus USING [Adjust, defaultTime, Error, MakeRope, Packed, PackedSeconds, Seconds, PackedToSeconds, SecondsToPacked, Parse, Precision, Unintelligible], TiogaExtraOps USING [GetFile], TiogaOps USING [SetStyle, Break, SetNodeFormat, Ref, FirstChild, Jump, GetRope, CaretAfter, 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, SetTiogaContents], ViewerOps USING[AddProp, FetchProp, PaintViewer, BlinkIcon, DestroyViewer, FindViewer], WalnutOps USING [MsgExists, GetMsgText]; RememberImpl: CEDAR MONITOR IMPORTS Atom, BasicTime, Booting, CommandTool, Convert, FileNames, FS, FSBackdoor, IconRegistry, IO, List, Menus, MessageWindow, Process, RememberPrivate, Rope, Tempus, TiogaExtraOps, TiogaOps, UserProfile, ViewerEvents, ViewerIO, ViewerOps, ViewerSpecs, ViewerTools, WalnutOps EXPORTS Remember, RememberPrivate = BEGIN OPEN Remember, RememberPrivate; Error: PUBLIC ERROR [ec: ErrorCode, msg: Rope.ROPE _ NIL] = CODE; eventList: PUBLIC LIST OF Event _ NIL; itIsNow: PUBLIC Tempus.Packed _ Tempus.defaultTime; -- defaultTime used in Register, EnterEventTime. Is a variable so that can be reset for debugging. walnutUser: PUBLIC BOOL _ FALSE; peanutUser: PUBLIC BOOL _ FALSE; debugging: BOOLEAN _ TRUE; eventFile: Rope.ROPE; eventFileCreateTime: BasicTime.GMT _ BasicTime.nullGMT; Construct: ENTRY PROC = { ENABLE UNWIND => NULL; fileStream: IO.STREAM; BEGIN -- establish scope for exits IF eventList # NIL THEN FOR el: LIST OF Event _ eventList, el.rest UNTIL el = NIL DO event: Event = el.first; IF event.viewer # NIL AND NOT event.viewer.destroyed THEN ViewerOps.DestroyViewer[event.viewer]; ENDLOOP; eventList _ NIL; eventFileCreateTime _ BasicTime.nullGMT; fileStream _ FS.StreamOpen[eventFile, $read ! FS.Error => CONTINUE]; IF fileStream = NIL THEN GOTO noRememberFile; DO parameters: ParameterList _ NIL; timeRope, text, repeat, duration, lead, until, iconLabel, icon, message: ROPE _ NIL; [] _ IO.SkipWhitespace[fileStream]; timeRope _ ReadTimeRope[fileStream ! IO.EndOfStream => EXIT]; parameters _ LIST[NEW[Parameter _ [startTime, timeRope]]]; DO r: ROPE; class: ParameterClass; [class, r] _ ReadParameter[fileStream ! Error => GOTO formatError]; parameters _ CONS[NEW[Parameter _ [class, r]], parameters]; IF class = text THEN EXIT; ENDLOOP; [] _ Register[ parameters: parameters ! Error => IF ec = formatError THEN GOTO formatError ELSE IF ec = timeRopeFormatError THEN GOTO timeRopeFormatError ]; ENDLOOP; RememberMostRecentEventFile[fileStream]; fileStream.Close[]; EXITS formatError => { out: IO.STREAM _ ViewerIO.CreateViewerStreams["Remember Error Log"].out; eventList _ NIL; out.PutF["Format error in %g, at or near position: %d\n", IO.rope[eventFile], IO.int[fileStream.GetIndex[]]]; fileStream.Close[]; }; timeRopeFormatError => { out: IO.STREAM _ ViewerIO.CreateViewerStreams["Remember Error Log"].out; eventList _ NIL; out.PutF["DateAndTime.Unintelligible in %g, at or near position: %d\n", IO.rope[eventFile], IO.int[fileStream.GetIndex[]]]; out.PutRope["\n"]; fileStream.Close[]; }; noRememberFile => { eventList _ NIL; MessageWindow.Append[ Rope.Cat["Could not find ", eventFile, " : no events logged."], TRUE]; MessageWindow.Blink[]; }; END; IF fileStream#NIL THEN FS.SetKeep[name: eventFile, keep: 2 ! FS.Error => CONTINUE]; --help out FS }; ReadParameters: PUBLIC PROC [stream: IO.STREAM] RETURNS[parameters: ParameterList] = { UNTIL IO.EndOf[stream] DO r: ROPE; class: ParameterClass; [class, r] _ ReadParameter[stream]; parameters _ CONS[NEW[Parameter _ [class, r]], parameters]; [] _ IO.SkipWhitespace[stream]; ENDLOOP; }; ReadParameter: PROC[stream: IO.STREAM] RETURNS[parameterClass: ParameterClass _ text, value: ROPE _ NIL] = { ENABLE { IO.EndOfStream => GOTO formatError; IO.Error => GOTO formatError; }; key: ROPE; kind: IO.TokenKind; [kind, value, ] _ IO.GetCedarTokenRope[stream]; SELECT kind FROM tokenROPE => RETURN[text, Convert.RopeFromLiteral[value]]; tokenID => key _ value; tokenSINGLE => { -- e.g. , ; etc. left over on line. [parameterClass, value] _ ReadParameter[stream]; RETURN; }; ENDCASE => IO.Error[SyntaxError, stream]; [] _ IO.SkipWhitespace[stream]; IF NOT stream.EndOf[] AND stream.PeekChar[] = ': THEN [] _ stream.GetChar[]; SELECT TRUE FROM Rope.Equal[s1: key, s2: "REPEAT", case: FALSE] => parameterClass _ repeat; Rope.Equal[s1: key, s2: "DURATION", case: FALSE] => parameterClass _ duration; Rope.Equal[s1: key, s2: "GETSERIOUSAFTER", case: FALSE], Rope.Equal[s1: key, s2: "NAGTIME", case: FALSE] => parameterClass _ getSeriousAfter; Rope.Equal[s1: key, s2: "UNTIL", case: FALSE] => parameterClass _ until; Rope.Equal[s1: key, s2: "LEADTIME", case: FALSE] => parameterClass _ leadTime; Rope.Equal[s1: key, s2: "TIME", case: FALSE] => parameterClass _ startTime; Rope.Equal[s1: key, s2: "TEXT", case: FALSE] => parameterClass _ text; Rope.Equal[s1: key, s2: "ICONFLAVOR", case: FALSE] => parameterClass _ iconFlavor; Rope.Equal[s1: key, s2: "ICONLABEL", case: FALSE] => parameterClass _ iconLabel; Rope.Equal[s1: key, s2: "ICONLABELTYPE", case: FALSE] => parameterClass _ iconLabelType; Rope.Equal[s1: key, s2: "MESSAGE", case: FALSE] => parameterClass _ message; ENDCASE => Error [formatError, key]; [] _ IO.SkipWhitespace[stream]; IF stream.EndOf[] THEN value _ NIL ELSE IF stream.PeekChar[] = '" THEN value _ IO.GetRopeLiteral[stream] ELSE value _ IO.GetTokenRope[stream].token; RETURN; EXITS formatError => Error[formatError]; }; 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]]; out.PutRope["\n"]; }; 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 _ NIL; 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 ! IO.EndOfStream => EXIT].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 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 ! IO.EndOfStream => EXIT].token; IF Rope.Fetch[iconLabel, 0] IN ['A..'Z] THEN { pos: INT = h.GetIndex[]; r: ROPE; WHILE Rope.Fetch[r _ h.GetTokenRope[breakProc ! IO.EndOfStream => EXIT].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; } 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 durationTime > 0 THEN durationTime _ MAX[durationTime, leadTime]; --show the reminder up to the event time 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]; }; eventMinderDisabled: BOOL _ FALSE; EventMinder: PROC = { DO EnterEventMinder[itIsNow]; Process.Pause[Process.SecondsToTicks[3]]; ENDLOOP; }; EnterEventMinder: PUBLIC ENTRY PROC [itIsNow: Packed, itIsLater: Packed _ BasicTime.nullGMT] = { ENABLE UNWIND => NULL; now: PackedSeconds _ Tempus.PackedToSeconds[itIsNow]; later: PackedSeconds _ IF itIsLater#BasicTime.nullGMT THEN Tempus.PackedToSeconds[itIsLater] ELSE now; IF eventMinderDisabled OR now > later THEN RETURN; FOR el: LIST OF Event _ eventList, el.rest UNTIL el = NIL DO -- for each event. event: Event = el.first; startTime: PackedSeconds _ [event.timeToStartNotification - event.leadTime]; 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; IF itIsNow # Tempus.defaultTime THEN { --pretending with repeated event event.trueTimeToStartNotification _ event.timeToStartNotification; event.trueNextNotification _ event.nextNotification; event.restoreTrueTimes _ TRUE; }; DO ENABLE { Tempus.Error, Tempus.Unintelligible => GOTO Bogus; }; t1 _ IF event.nextNotification # 0 THEN event.nextNotification ELSE ComputeRepetitionInterval[event.repeat, t1]; IF t1 > now THEN {event.nextNotification _ t1; EXIT}; event.newStartTime _ TRUE; event.nextNotification _ [0]; event.timeToStartNotification _ startTime _ t1; REPEAT Bogus => LOOP; ENDLOOP; }; IF later > startTime THEN { event.justPretending _ (itIsNow # Tempus.defaultTime); IF event.newStartTime AND (event.durationTime = 0 OR now < startTime + event.durationTime OR (event.repeat # NIL AND later > event.nextNotification)) THEN { viewer: Viewer _ event.viewer; text: ROPE _ Rope.Cat["Time: ", Tempus.MakeRope[time: Tempus.defaultTime, includeDayOfWeek: TRUE], "\nEventTime: ", Tempus.MakeRope[time: Tempus.SecondsToPacked[startTime], includeDayOfWeek: TRUE], "\n"]; text _ Rope.Concat[text, event.text]; event.newStartTime _ FALSE; event.blink _ TRUE; IF viewer = NIL OR viewer.destroyed THEN { viewer _ ViewerTools.MakeNewTextViewer[ info: [ name: event.text, data: text, icon: IconRegistry.GetIcon[IF el.first.iconFlavor # NIL THEN el.first.iconFlavor ELSE "Remember.defaultIcon", IconRegistry.GetIcon["Remember.defaultIcon", document]]], -- uses el.first.iconFlavor if it can be found, otherwise Remember.defaultIcon otherwise document. The reason for the NIL check instead of just looking up NIL and letting it fail is that somebody actually managed to register an icon for NIL. paint: FALSE]; Menus.SetLine[menu: viewer.menu, line: Menus.GetNumberOfLines[viewer.menu], entryList: reminderButtons]; }; IF event.message # NIL THEN { IF walnutUser THEN { IF WalnutOps.MsgExists[event.message] THEN ViewerTools.SetTiogaContents[viewer: viewer, contents: WalnutOps.GetMsgText[event.message].contents, paint: FALSE] ELSE ViewerTools.SetContents[viewer: viewer, contents: Rope.Cat[text, "\n(Could not find message: \"", event.message, "\")\n"], paint: FALSE]; } ELSE IF peanutUser THEN { fileAtom: ATOM; ropeIndex: INT; message: ROPE; mailViewer: ViewerClasses.Viewer; mailDoc: TiogaOps.Ref; stream: IO.STREAM = IO.RIS[event.message]; fileName: ROPE _ stream.GetTokenRope[IO.IDProc].token; IF (ropeIndex _ Rope.Find[s1: fileName, s2: "!", case: FALSE])#-1 THEN fileName _ Rope.Substr[base: fileName, start: 0, len: ropeIndex]; fileAtom _ Atom.MakeAtom[fileName]; [] _ IO.GetChar[stream]; -- the extra space message _ IO.GetTokenRope[stream, EveryThing ! IO.EndOfStream => CONTINUE].token; 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 ! FS.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.SelectDocument[viewer: viewer, pendingDelete: FALSE]; TiogaOps.CaretAfter[]; TiogaOps.Break[]; --root node TiogaOps.SaveSpanForPaste[startLoc: [node, 0], endLoc: [node, Rope.Size[message]]]; TiogaOps.Paste[]; TiogaOps.Break[]; --first sibling of root TiogaOps.SetStyle[style: "Mail", which: root]; TiogaOps.SetNodeFormat[format: "header", node: TiogaOps.Next[TiogaOps.FirstChild[TiogaOps.ViewerDoc[viewer]]]]; TiogaOps.SaveSpanForPaste[startLoc: [node, Rope.Size[message]], endLoc: TiogaOps.LastLocWithin[node]]; TiogaOps.Paste[]; TiogaOps.Jump[viewer: viewer, loc: [TiogaOps.ViewerDoc[viewer], 0]]; viewer.newVersion _ FALSE; viewer.icon _ IconRegistry.GetIcon["Remember.defaultIcon", document]; TiogaOps.RestoreSelB[]; TiogaOps.UnlockSel[]; EXIT; } ELSE IF (node _ TiogaOps.Next[node]) = NIL THEN { text _ Rope.Cat[text, "\n(Could not find message: \"", message, "\"\n"]; 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, $Remember, el.first]; -- for menus event.viewer _ viewer; ViewerOps.PaintViewer[viewer, all]; IF NOT logFileName.IsEmpty AND NOT event.justPretending THEN LogEvent[event, "Following Reminder posted"]; }; IF event.durationTime # 0 -- 0 means forever AND now > startTime + event.durationTime THEN { IF event.justPretending THEN NULL ELSE IF event.repeat # NIL THEN TRUSTED { IF event.nextNotification = 0 THEN ERROR; event.timeToStartNotification _ event.nextNotification; event.newStartTime _ TRUE; Process.Detach[FORK Save[]]; } ELSE IF NOT event.destroyed THEN TRUSTED { event.destroyed _ TRUE; Process.Detach[FORK Save[]]; }; IF NOT event.justPretending AND 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]; }; IF event.restoreTrueTimes THEN TRUSTED {Process.Detach[FORK Save[]]}; IF event.getSeriousAfter # 0 AND now > startTime + event.getSeriousAfter AND NOT event.destroyed AND NOT event.newStartTime THEN TRUSTED { IF event.viewer # NIL AND NOT event.viewer.destroyed AND event.viewer.iconic THEN -- waste the sucker [] _ CommandTool.DoCommand[commandLine: Rope.Cat["_ SpecialCache.NameIt[\"", event.viewer.name,"\"]"], parent: NIL]; MessageWindow.Append[Rope.Concat["Important! ", event.text], TRUE]; MessageWindow.Blink[]; }; 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; }; EveryThing: IO.BreakProc = {RETURN[other]}; Save: PUBLIC ENTRY PROC = { OPEN IO; ENABLE UNWIND => NULL; Compare: List.CompareProc = { e1, e2: Event; e1 _ NARROW[ref1]; e2 _ NARROW[ref2]; RETURN[IF e1.timeToStartNotification < e2.timeToStartNotification THEN less ELSE greater]; }; newEventList: LIST OF Event _ NIL; stream: IO.STREAM _ NIL; now: PackedSeconds = Tempus.PackedToSeconds[]; eventFileCreateTime _ BasicTime.nullGMT; stream _ FS.StreamOpen[fileName: eventFile, accessOptions: $create, keep: 2 ! FS.Error => { MessageWindow.Append[Rope.Cat["FS Error trying to open ", eventFile], TRUE]; MessageWindow.Blink[]; CONTINUE; } ]; IF stream=NIL THEN RETURN; FOR l: LIST OF Event _ eventList, l.rest UNTIL l = NIL DO event: Event _ l.first; IF event.restoreTrueTimes THEN { --fix up pretended repeated events event.timeToStartNotification _ event.trueTimeToStartNotification; event.nextNotification _ event.trueNextNotification; event.restoreTrueTimes _ FALSE; }; 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 NOT logFileName.IsEmpty THEN LogEvent[event, "Following Event deleted"]; 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; RememberMostRecentEventFile[stream]; stream.Close[]; }; SaveEvent: PROC [event: Event, stream: IO.STREAM] = { OPEN IO; timeToStartNotification: PackedSeconds _ event.timeToStartNotification; 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]]; }; LogEvent: PROC [event: Event, logMessage: ROPE] = { IF logFileName.IsEmpty THEN RETURN ELSE { eventLogStream: IO.STREAM _ NIL; eventLogStream _ FS.StreamOpen[fileName: logFileName, accessOptions: append ! FS.Error => CONTINUE]; IF eventLogStream = NIL THEN { newLogStream: IO.STREAM _ NIL; eventLogStream _ FS.StreamOpen[fileName: logFileName, accessOptions: read ! FS.Error => CONTINUE]; IF eventLogStream = NIL THEN { eventLogStream _ FS.StreamOpen[fileName: logFileName, accessOptions: create ! FS.Error => CONTINUE]; } ELSE { newLogStream _ FS.StreamOpen[fileName: logFileName, accessOptions: create ! FS.Error => CONTINUE]; IF newLogStream # NIL THEN { WHILE NOT eventLogStream.EndOf[] DO newLogStream.PutChar[eventLogStream.GetChar[]]; ENDLOOP; eventLogStream.Close[]; eventLogStream _ newLogStream; }; }; }; IF eventLogStream = NIL THEN { MessageWindow.Append["Remember.LogFile could not be opened. Abandoning logging.", TRUE]; MessageWindow.Blink[]; } ELSE { eventLogStream.PutF["(%g at %t.)\n", IO.rope[logMessage], IO.time[]]; SaveEvent[event, eventLogStream]; eventLogStream.Close[]; }; }; }; logFileName: ROPE; defaultDuration: INT _ -1; defaultLeadTime: INT _ 0; KeyWordList: TYPE = LIST OF REF KeyWord; KeyWord: TYPE = RECORD [key: ROPE, defaults: ParameterList]; keyWordList: KeyWordList _ NIL; Options: UserProfile.ProfileChangedProc = { --PROC [reason: ProfileChangeReason]-- keyWords: Rope.ROPE; ch: CHAR; logEvents: BOOLEAN _ UserProfile.Boolean["Remember.CreateLog", FALSE]; logFileName _ UserProfile.Token["Remember.LogFile", "///RememberEvents.log"]; IF logEvents AND NOT logFileName.IsEmpty AND reason = firstTime THEN { IO.Close[FS.StreamOpen[fileName: logFileName, accessOptions: create ! FS.Error => { MessageWindow.Append["Remember.LogFile could not be opened. Abandoning logging.", TRUE]; MessageWindow.Blink[]; eventFile _ NIL; CONTINUE; }]]; }; defaultLeadTime _ UserProfile.Number["Remember.LeadTime", 0]; defaultDuration _ UserProfile.Number["Remember.Duration", -1]; eventFile _ UserProfile.Token["Remember.EventFile", "///RememberEvents.txt"]; ch _ eventFile.Fetch[1]; IF FileNames.IsRemote[eventFile] OR (ch # '[ AND ch # '/) THEN { MessageWindow.Append["Remember.EventFile must be a local full path name.", TRUE]; MessageWindow.Blink[]; eventFile _ "///RememberEvents.txt"; }; keyWords _ UserProfile.Token["Remember.KeyWords"]; keyWordList _ NIL; IF keyWords # NIL THEN { stream: IO.STREAM = IO.RIS[keyWords]; scratchStream: IO.STREAM; UNTIL IO.EndOf[stream] DO key, line: Rope.ROPE; line _ IO.GetLineRope[stream]; scratchStream _ IO.RIS[line, scratchStream]; key _ IO.GetTokenRope[scratchStream ! IO.EndOfStream => CONTINUE].token; IF key # NIL THEN keyWordList _ CONS[NEW[KeyWord _ [key, ReadParameters[scratchStream]]], keyWordList]; ENDLOOP; }; }; RegisterKeyWord: PUBLIC PROC [keyWord: ROPE, parameters: ParameterList] = { keyWordList _ CONS[NEW[KeyWord _ [keyWord, parameters]], keyWordList]; }; 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, "BIWEEKLY", FALSE] THEN Tempus.Adjust[baseTime: t, days: 14, precisionOfResult: minutes].time ELSE IF Rope.Equal[r, "FORTNIGHT", FALSE] THEN Tempus.Adjust[baseTime: t, days: 14, 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 = 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 remember.txt }; }; stopWatching: BOOLEAN _ FALSE; FileWatcher: PROC = { fsEvent: REF READONLY FSBackdoor.CreateEvent _ NIL; MatchFileNames: PROC [a, b: ROPE] RETURNS [BOOLEAN] ~ { aFName, bFName: ROPE; aCP, bCP: FS.ComponentPositions; [fullFName: aFName, cp: aCP] _ FS.ExpandName[a ! FS.Error => GOTO Fail]; [fullFName: bFName, cp: bCP] _ FS.ExpandName[b ! FS.Error => GOTO Fail]; RETURN [(aCP.ext.start+aCP.ext.length) = (bCP.ext.start+bCP.ext.length) AND Rope.Run[s1: aFName, s2: bFName, case: FALSE] >= (aCP.ext.start+aCP.ext.length)]; EXITS Fail => RETURN[FALSE]; }; DO fsEvent _ FSBackdoor.NextCreateEvent[fsEvent]; IF stopWatching THEN EXIT; IF MatchFileNames[eventFile, fsEvent.fName] THEN { fsEventTime: BasicTime.GMT _ BasicTime.nullGMT; [created: fsEventTime] _ FS.FileInfo[name: fsEvent.fName, remoteCheck: FALSE ! FS.Error => CONTINUE]; IF fsEventTime # eventFileCreateTime THEN [] _ Construct[]; }; ENDLOOP; }; RememberMostRecentEventFile: PROC [s: IO.STREAM] ~ { [created: eventFileCreateTime] _ FS.GetInfo[FS.OpenFileFromStream[s]]; }; CheckpointProc: Booting.CheckpointProc = { DisableEventMinder: ENTRY PROC = {eventMinderDisabled _ TRUE}; DisableEventMinder[]; }; RollbackProc: Booting.RollbackProc = { EnableEventMinder: ENTRY PROC = {eventMinderDisabled _ FALSE}; [] _ Construct[]; EnableEventMinder[]; }; UserProfile.CallWhenProfileChanges[Options]; [] _ ViewerEvents.RegisterEventProc[proc: WasAReminderDestroyed, filter: $Text, event: destroy]; [] _ Construct[]; TRUSTED {Booting.RegisterProcs[c: CheckpointProc, r: RollbackProc]}; TRUSTED {Process.Detach[FORK EventMinder[]]}; TRUSTED {Process.Detach[FORK FileWatcher[]]}; END. pRememberImpl.mesa. Copyright c 1985 by Xerox Corporation. All rights reserved. Teitelman, July 21, 1983 10:50 am Maxwell, January 6, 1984 1:36 pm Pier, May 2, 1984 3:59:07 pm PDT Beach, August 15, 1984 10:33:03 am PDT Pavel on June 7, 1984 9:49:28 pm PDT Rick Beach, May 28, 1985 9:08:25 pm PDT Types Global Variables Parsing Registering Events IF startAt = NIL AND timeToStartNotification = 0 THEN startAt _ Lookup[startTime]; -- defaults may specify a startAt timeToStartNotification _ [timeToStartNotification - leadTime]; Event Minder IF event.justPretending AND itIsNow = Tempus.defaultTime THEN { -- event posted as a result of just pretend event.justPretending _ FALSE; event.newStartTime _ TRUE; }; 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. create a new viewer if there just occurred a new start time (the first qualifies) and its duration has not expired --strip off the version number because the "current" version won't last blink the viewer if there is an active one and it is iconic and the blinker is still active. Saving Events purge those events no longer interesting IF (viewer _ ViewerOps.FindViewer[eventFile]) # NIL THEN ViewerOps.RestoreViewer[viewer]; IF event.leadTime # 0 THEN timeToStartNotification _ [timeToStartNotification + event.leadTime]; Could not append to log file, try copying the log file to a new version Could not read the existing log, simply create a new log stream Copy the old log contents to a new log User Profile Options Miscellaneous PROC [viewer: ViewerClasses.Viewer, event: ViewerEvent] Initialization Κ ά˜code™Kšœ Οmœ1™Kšœ žœ˜%Kšœ žœ˜ Kšœ žœ3˜DKšœ žœH˜WKšœ žœ˜(K˜—K˜šΠbl œžœž˜K˜—K˜Kšžœ<žœžœ²˜•Kšžœ˜!Kšœžœžœ˜(head™Kš œžœžœžœžœžœ˜A—™Kš œ žœžœžœ žœ˜&Kšœ žœ&Οcb˜—Kšœ žœžœžœ˜ Kšœ žœžœžœ˜ Kšœ žœžœ˜Kšœžœ˜Kšœžœ˜7—™ šΟn œžœžœ˜Kšžœžœžœ˜Kšœ žœžœ˜šžœ ˜#šžœ žœž˜š žœžœžœžœžœž˜——Kšœ˜—Kšžœ˜—K˜(K˜šžœ˜˜Kšœžœžœ:˜HKšœ žœ˜Kšœ:žœžœ1˜K˜—˜Kšœžœžœ:˜HKšœ žœ˜KšœHžœžœ˜{Kšœ&˜&K˜—˜Kšœ žœ˜šœ˜Kšœ@žœ˜F—K˜—K˜—Kšžœ˜—•StartOfExpansion=[name: ROPE, keep: CARDINAL _ 1B (1), wDir: ROPE _ NIL]š žœ žœžœžœ$žœ žœ˜SKš  ˜ —˜K˜——K˜š ‘œžœžœ žœžœžœ˜Všžœžœž˜Kšœžœ˜K˜Kšœ#˜#Kšœ žœžœ&˜;Kšœžœ˜Kšžœ˜—K˜—K˜š‘ œžœ žœžœžœ/žœžœ˜lšžœ˜Kšžœžœ ˜#Kšžœ žœ ˜K˜—Kšœžœ˜ Kšœžœ ˜Kšœžœ˜/šžœž˜Kšœ žœ'˜:Kšœ˜šœ #˜5Kšœ0˜0Kšžœ˜Kšœ˜—Kšžœžœ˜)—Kšœžœ˜Kšžœžœžœžœ˜Lšžœžœž˜K˜Kšœ(žœ˜JKšœ*žœ˜NKšœ1žœ,žœ&˜Kšœ'žœ˜HKšœ*žœ˜NKšœ&žœ ˜KKšœ&žœ˜FKšœ,žœ!˜RKšœ+žœ ˜PKšœ/žœ$˜XKšœ)žœ˜LKšžœ˜$K˜—Kšœžœ˜Kšžœžœ ž˜"Kšžœžœžœ žœ˜EKšžœ žœ˜+Kšžœ˜šž˜K˜"—K˜K˜——™K˜š‘ œžœžœžœ,žœžœžœžœžœ˜tKšžœžœžœ˜Kšœ.˜.šžœžœžœ˜Kšœ*žœxžœ˜¬Kšžœžœžœ˜[Kšžœžœžœžœ˜LKšžœžœ.žœ ˜qKšœ˜K˜—Kšœžœ ˜Kšœ˜—K˜š ‘œžœžœ,žœžœžœ˜hšžœ>˜Dšžœžœ˜Kšœ˜Kšœ(˜(Kšœ˜K˜#Kšœ&˜&Kšžœžœ˜—K–9[base: ROPE, start: INT _ 0, len: INT _ 2147483647]˜'K–9[base: ROPE, start: INT _ 0, len: INT _ 2147483647]˜K˜(Kšœ˜—KšœVžœ˜[Kšœ?˜?Kšœ4˜4K˜K˜š ‘œžœžœ žœžœžœ˜Ošžœ'žœžœž˜:Kšžœžœžœ˜4Kšžœ˜—K˜—š ‘ œžœ žœžœžœ˜Gšžœ&žœžœž˜9Kšžœžœžœžœ˜FKšžœ˜—Kšžœžœ˜ K˜—K˜Kšœ˜Kšœ ˜=Kšœ ˜ Kšœ˜šžœ žœžœ˜Kš œžœžœžœžœ ˜ Kšœžœžœ˜Kšœžœžœžœ˜šžœ ž˜šœ žœ˜šžœžœž˜Kšœ˜Kšœ ˜)Kšžœ ˜Kšœ˜Kšœ˜——Kšœ˜Kšœžœ˜Kšœ#žœžœ˜AKšœ žœ˜šžœ%žœžœ ˆ˜Ίšžœ'žœžœž˜:Kšœ žœ˜#Kšžœ˜—Kšœ˜Kš žœ žœžœžœ !™ušžœ žœžœ#žœžœ,žœžœ ‹˜šžœžœž˜Kšœ"žœ˜>šœ"žœ˜-Kšœ'žœžœ˜Ešžœžœ žœ˜.Kšœžœ˜Kšœžœ˜š žœ+žœžœ žœ ž˜`K˜(Kšžœ˜—K˜K˜—K˜—š œ"žœžœžœžœ˜GKšœ˜šžœžœ žœ˜.š žœžœžœžœžœžœž˜?Kš žœžœžœ žœžœ˜6Kšœ.˜.Kšžœ˜—K˜—K˜—Kšžœ˜——Kšœ˜—Kšžœ $˜/—š žœ žœžœžœ ˜VK˜Kšœ žœ˜Kšœ˜Kšœgžœžœžœ ͺ˜Ίšžœžœ˜šžœ žœžœ t˜‹Kšœžžœ˜¨—Kšœ4˜4Kšœ˜—Kšœ˜—Kšžœžœžœ˜#Kšœ ˜—š žœžœ žœžœ :˜sK˜ Kšœ0˜0Kšœ4˜4K˜—Kšžœžœ˜K˜Kšœ  ˜K˜TK˜Kšœ ˜)Kšœ.˜.Kšœo˜oK˜fK˜K˜EKšœžœ˜KšœE˜EK˜Kšœ˜Kšžœ˜Kšœ˜—šžœžœ žœžœ˜2KšœH˜HKšœ9˜9Kšœ?žœ˜FKšžœ˜K˜—Kšžœ˜—Kšœ˜—K˜—Kšžœœžœ˜¨K˜—Kšžœžœžœ9˜VKšœ1  ˜>Kšœ˜K˜#š žœžœžœžœž˜˜>KšœM˜MK˜šžœ žœ žœ žœ˜DKšœKžœ˜QK˜Kšœ$˜$K˜—Kšœ2˜2Kšœžœ˜šžœ žœžœ˜Kš œžœžœžœžœ ˜%Kšœžœžœ˜šžœžœž˜Kšœžœ˜Kšœžœ˜Kšœžœžœ˜,Kšœžœžœžœ˜Hšžœžœž˜Kšœžœžœ?˜U—Kšžœ˜—Kšœ˜—Kšœ˜—K˜š‘œžœžœ žœ ˜KKšœžœžœ0˜FK˜——™š‘œž œ žœžœ˜rKšžœžœ6˜RK˜0šžœ˜KšžœžœžœF˜l—KšžœžœžœžœE˜ošžœžœžœžœ˜.Kšœ!žœ&žœžœ$˜x—KšžœžœžœžœE˜pKšžœžœžœžœF˜sKšžœžœžœžœF˜tKšžœžœžœžœG˜sKšžœžœžœžœF˜qKšžœ-˜1Kšœ˜K˜—K˜š ‘ œžœ žœžœžœžœ˜8Kšžœžœ˜Kšœ˜—K˜š‘œžœ˜9Kšžœ3™7Kšœžœžœ*˜7šžœžœžœ˜Kšœžœ˜Kšžœžœž˜šžœžœžœ˜#Kšœžœ˜Kšœžœ˜Kšœžœ˜Kšžœ˜K˜—Kš žœžœžœžœ ˜GKšœžœ˜šžœžœžœ˜Kšžœžœžœ˜)Kšœ7˜7K˜—Kšœžœ  ˜6K˜—K˜—K˜Kšœžœžœ˜K˜š‘ œžœ˜Kšœ žœžœžœ˜3š ‘œžœžœžœžœ˜7Kšœžœ˜Kšœ žœ˜ Kšœžœžœ žœ˜HKšœžœžœ žœ˜HKšžœBžœ(žœ%˜Kšžœ žœžœ˜K˜—šž˜Kšœ.˜.Kšžœžœžœ˜šžœ*ž˜2Kšœžœ˜/Kš œžœ,žœžœ žœ˜eKšžœ"žœ˜;Kšœ˜—Kšž˜—K˜—K˜š‘œžœžœžœ˜4Kšœ!žœ žœ˜FK˜—K˜š’œ˜*Kš‘œžœžœžœ˜>K˜Kšœ˜—K˜š’ œ˜&Kš‘œžœžœžœ˜>Kšœ˜Kšœ˜Kšœ˜——™K˜K˜,Kšœ`˜`K˜K˜Kšžœ=˜DKšžœžœ˜-šžœžœ˜-K˜——K˜Kšžœ˜—…—x. z