<> <> <<>> 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] ; ReminderImpl: CEDAR MONITOR IMPORTS Atom, CedarSnapshot, CedarScanner, CIFS, Convert, FileIO, IconRegistry, IO, IOExtras, List, Menus, MessageWindow, Process, Rope, SafeStorage, Tempus, TiogaExtraOps, TiogaMenuOps, TiogaOps, UserProfile, ViewerEvents, ViewerOps, ViewerSpecs, ViewerTools, WalnutDisplayerOps EXPORTS ReminderDefs, ReminderDefsPrivate = BEGIN OPEN ReminderDefs, ReminderDefsPrivate; <> 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; <> 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; fileStream _ FileIO.Open["Reminders.txt", read ! ANY => CONTINUE]; IF fileStream = NIL THEN GOTO noReminderFile; <> DO parameters: ParameterList _ NIL; timeRope, text, repeat, duration, lead, until, iconLabel, icon, message: ROPE _ NIL; IO.SkipOver[fileStream]; timeRope _ ReadTimeRope[fileStream]; IF timeRope = NIL THEN 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; fileStream.Close[]; EXITS formatError => { out: IO.STREAM _ IO.CreateViewerStreams["Reminder Error"].out; eventList _ NIL; out.PutF["Reminder: Format error in Reminders.txt, at or near position: %d\n", IO.int[fileStream.GetIndex[]]]; }; timeRopeFormatError => { out: IO.STREAM _ IO.CreateViewerStreams["Reminder Error"].out; eventList _ NIL; out.PutF["Reminder: DateAndTime.Unintelligible in Reminders.txt, at or near position: %d\n", IO.int[fileStream.GetIndex[]]]; out.PutRope["\n"]; }; noReminderFile => { eventList _ NIL; MessageWindow.Append["Could not find Reminders.txt: no events logged.", TRUE]; MessageWindow.Blink[]; }; END; }; 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.SkipOver[stream, IO.WhiteSpace]; ENDLOOP; }; ReadParameter: PROC[stream: IO.STREAM] RETURNS[parameterClass: ParameterClass _ text, value: ROPE _ NIL] = { ENABLE { IO.EndOfStream => GOTO formatError; IO.SyntaxError => GOTO formatError; }; key: ROPE; kind: CedarScanner.TokenKind; fromTokenProc: IOExtras.FromTokenProc -- [closure: CedarScanner.GetClosure, token: CedarScanner.Token] -- = { kind _ token.kind; SELECT kind FROM tokenROPE => value _ CedarScanner.RopeFromToken[closure, token]; ENDCASE => value _ CedarScanner.ContentsFromToken[closure, token]; }; IOExtras.GetCedarScannerToken[stream, fromTokenProc]; SELECT kind FROM tokenROPE => RETURN[text, value]; tokenID => key _ value; tokenSINGLE => { -- e.g. , ; etc. left over on line. [parameterClass, value] _ ReadParameter[stream]; RETURN; }; ENDCASE => IO.SyntaxError[stream]; IO.SkipOver[stream, IO.WhiteSpace]; IF IO.PeekChar[stream] = ': THEN [] _ IO.GetChar[stream]; 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: "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]; IOExtras.GetCedarScannerToken[stream, fromTokenProc]; -- value will be the next object. If surrounded in "'s, the "'s will be removed. 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], 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; }; <> 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. 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; REPEAT Bogus => LOOP; ENDLOOP; }; IF now > startTime THEN { <> 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]; }; <<>> <> 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[]]}; }; <> 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; <> 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[]; EXITS Out => NULL; }; <> logEvents: BOOLEAN _ FALSE; eventLogStream: IO.STREAM _ NIL; 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 = { keyWords: Rope.ROPE; IF (logEvents _ UserProfile.Boolean["Reminder.CreateLog", FALSE]) THEN { eventLogStream _ FileIO.Open[fileName: "Reminder.log", accessOptions: append]; }; defaultLeadTime _ UserProfile.Number["Reminder.LeadTime", 0]; defaultDuration _ UserProfile.Number["Reminder.Duration", -1]; keyWords _ UserProfile.Token["Reminder.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 _ IOExtras.GetLine[stream]; scratchStream _ IO.RIS[line, scratchStream]; key _ IO.GetToken[scratchStream]; 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]; }; <> 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[]}; <> 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[]]}; [] _ Construct[]; END.