X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/d607b96bc2824116a8fe0e5840ce49da7ce4514f..bdfa5dd206b05feed0b9312470bda434e8c21974:/lisp/calendar/appt.el diff --git a/lisp/calendar/appt.el b/lisp/calendar/appt.el index d6f4f9862d..c44eb6e1b5 100644 --- a/lisp/calendar/appt.el +++ b/lisp/calendar/appt.el @@ -1,7 +1,7 @@ ;;; appt.el --- appointment notification functions -;; Copyright (C) 1989, 1990, 1994, 1998, 2001, 2002, 2003, 2004, 2005, -;; 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc. +;; Copyright (C) 1989-1990, 1994, 1998, 2001-2011 +;; Free Software Foundation, Inc. ;; Author: Neil Mager ;; Maintainer: Glenn Morris @@ -62,15 +62,10 @@ ;; `appt-check' reads. ;; ;; You can change the way the appointment window is created/deleted by -;; setting the variables -;; -;; appt-disp-window-function -;; and -;; appt-delete-window-function -;; -;; For instance, these variables could be set to functions that display -;; appointments in pop-up frames, which are lowered or iconified after -;; `appt-display-interval' minutes. +;; setting the variables `appt-disp-window-function' and +;; `appt-delete-window-function'. For instance, you could be set them +;; to functions that display appointments in pop-up frames, which are +;; lowered or iconified after `appt-display-interval' minutes. ;; ;;; Code: @@ -84,10 +79,23 @@ :group 'calendar) (defcustom appt-message-warning-time 12 - "Time in minutes before an appointment that the warning begins." + "Default time in minutes before an appointment that the warning begins. +You probably want to make `appt-display-interval' a factor of this." :type 'integer :group 'appt) +(defcustom appt-warning-time-regexp "warntime \\([0-9]+\\)" + "Regexp matching a string giving the warning time for an appointment. +The first subexpression matches the time in minutes (an integer). +This overrides the default `appt-message-warning-time'. +You may want to put this inside a diary comment (see `diary-comment-start'). +For example, to be warned 30 minutes in advance of an appointment: + 2011/06/01 12:00 Do something ## warntime 30 +" + :version "24.1" + :type 'regexp + :group 'appt) + (defcustom appt-audible t "Non-nil means beep to indicate appointment." :type 'boolean @@ -110,7 +118,9 @@ See also `appt-audible' and `appt-display-mode-line'." (defcustom appt-display-mode-line t "Non-nil means display minutes to appointment and time on the mode line. -This is in addition to any other display of appointment messages." +This is in addition to any other display of appointment messages. +The mode line updates every minute, independent of the value of +`appt-display-interval'." :type 'boolean :group 'appt) @@ -122,12 +132,21 @@ Only relevant if reminders are to be displayed in their own window." (defcustom appt-display-diary t "Non-nil displays the diary when the appointment list is first initialized. -This will occur at midnight when the appointment list is updated." +This occurs when this package is first activated, and then at +midnight when the appointment list updates." :type 'boolean :group 'appt) (defcustom appt-display-interval 3 - "Number of minutes to wait between checking the appointment list." + "Interval in minutes at which to display appointment reminders. +Once an appointment becomes due, Emacs displays reminders every +`appt-display-interval' minutes. You probably want to make +`appt-message-warning-time' be a multiple of this, so that you get +a final message displayed precisely when the appointment is due. + +Note that this variable controls the interval at which +`appt-display-message' is called. The mode line display (if active) +always updates every minute." :type 'integer :group 'appt) @@ -135,16 +154,16 @@ This will occur at midnight when the appointment list is updated." "Function called to display appointment window. Only relevant if reminders are being displayed in a window. It should take three string arguments: the number of minutes till -the appointment, the current time, and the text of the appointment." - :type '(choice (const appt-disp-window) - function) +the appointment, the current time, and the text of the appointment. +Each argument may also be a list, if multiple appointments are +relevant at any one time." + :type 'function :group 'appt) (defcustom appt-delete-window-function 'appt-delete-window "Function called to remove appointment window and buffer. Only relevant if reminders are being displayed in a window." - :type '(choice (const appt-delete-window) - function) + :type 'function :group 'appt) @@ -155,23 +174,22 @@ Only relevant if reminders are being displayed in a window." ;; TODO Turn this into an alist? It would be easier to add more ;; optional elements. -;; TODO There should be a way to set WARNTIME (and other properties) -;; from the diary-file. Implementing that would be a good reason -;; to change this to an alist. +;; Why is the first element (MINUTES) rather than just MINUTES? +;; It may just inherit from diary-entries-list, where we have +;; ((MONTH DAY YEAR) ENTRY) (defvar appt-time-msg-list nil "The list of appointments for today. Use `appt-add' and `appt-delete' to add and delete appointments. The original list is generated from today's `diary-entries-list', and can be regenerated using the function `appt-check'. Each element of the generated list has the form -\(MINUTES STRING [FLAG] [WARNTIME]) +\((MINUTES) STRING [FLAG] [WARNTIME]) where MINUTES is the time in minutes of the appointment after midnight, and STRING is the description of the appointment. -FLAG and WARNTIME can only be present if the element was made -with `appt-add'. A non-nil FLAG indicates that the element was made -with `appt-add', so calling `appt-make-list' again should preserve it. -If WARNTIME is non-nil, it is an integer to use in place -of `appt-message-warning-time'.") +FLAG and WARNTIME are not always present. A non-nil FLAG +indicates that the element was made with `appt-add', so calling +`appt-make-list' again should preserve it. If WARNTIME is non-nil, +it is an integer to use in place of `appt-message-warning-time'.") (defconst appt-max-time (1- (* 24 60)) "11:59pm in minutes - number of minutes in a day minus 1.") @@ -183,13 +201,9 @@ Only used if `appt-display-mode-line' is non-nil.") (put 'appt-mode-string 'risky-local-variable t) ; for 'face property (defvar appt-prev-comp-time nil - "Time of day (mins since midnight) at which we last checked appointments. -A nil value forces the diary file to be (re-)checked for appointments.") - -(defvar appt-now-displayed nil - "Non-nil when we have started notifying about a appointment that is near.") + "Time of day (mins since midnight) at which we last checked appointments.") -(defvar appt-display-count nil +(defvar appt-display-count 0 "Internal variable used to count number of consecutive reminders.") (defvar appt-timer nil @@ -202,21 +216,60 @@ If this is non-nil, appointment checking is active.") (defun appt-display-message (string mins) "Display a reminder about an appointment. The string STRING describes the appointment, due in integer MINS minutes. -The format of the visible reminder is controlled by `appt-display-format'. -The variable `appt-audible' controls the audible reminder." +The arguments may also be lists, where each element relates to a +separate appointment. The variable `appt-display-format' controls +the format of the visible reminder. If `appt-audible' is non-nil, +also calls `beep' for an audible reminder." (if appt-audible (beep 1)) + ;; Backwards compatibility: avoid passing lists to a-d-w-f if not necessary. + (and (listp mins) + (= (length mins) 1) + (setq mins (car mins) + string (car string))) (cond ((eq appt-display-format 'window) - (funcall appt-disp-window-function - (number-to-string mins) - ;; TODO - use calendar-month-abbrev-array rather than %b? - (format-time-string "%a %b %e " (current-time)) - string) + ;; TODO use calendar-month-abbrev-array rather than %b? + (let ((time (format-time-string "%a %b %e " (current-time))) + err) + (condition-case err + (funcall appt-disp-window-function + (if (listp mins) + (mapcar 'number-to-string mins) + (number-to-string mins)) + time string) + (wrong-type-argument + (if (not (listp mins)) + (signal (car err) (cdr err)) + (message "Argtype error in `appt-disp-window-function' - \ +update it for multiple appts?") + ;; Fallback to just displaying the first appt, as we used to. + (funcall appt-disp-window-function + (number-to-string (car mins)) time + (car string)))))) (run-at-time (format "%d sec" appt-display-duration) nil appt-delete-window-function)) ((eq appt-display-format 'echo) - (message "%s" string)))) - + (message "%s" (if (listp string) + (mapconcat 'identity string "\n") + string))))) + +(defun appt-mode-line (min-to-app &optional abbrev) + "Return an appointment string suitable for use in the mode-line. +MIN-TO-APP is a list of minutes, as strings. +If ABBREV is non-nil, abbreviates some text." + ;; All this silliness is just to make the formatting slightly nicer. + (let* ((multiple (> (length min-to-app) 1)) + (imin (if (or (not multiple) + (not (delete (car min-to-app) min-to-app))) + (car min-to-app)))) + (format "%s%s %s" + (if abbrev "App't" "Appointment") + (if multiple "s" "") + (if (equal imin "0") "now" + (format "in %s %s" + (or imin (mapconcat 'identity min-to-app ",")) + (if abbrev "min." + (format "minute%s" (if (equal imin "1") "" "s")))))))) (defun appt-check (&optional force) "Check for an appointment and update any reminder display. @@ -241,29 +294,28 @@ The following variables control appointment notification: Controls the format in which reminders are displayed. `appt-audible' - Variable used to determine if reminder is audible. - Default is t. + Non-nil means there is an audible component to reminders. `appt-message-warning-time' - Variable used to determine when appointment message - should first be displayed. + The default number of minutes in advance at which reminders + should start. `appt-display-mode-line' - If non-nil, a generic message giving the time remaining - is shown in the mode-line when an appointment is due. + Non-nil means show in the mode line a countdown to the + time of each appointment, once reminders start. `appt-display-interval' - Interval in minutes at which to check for pending appointments. + Interval in minutes at which to display appointment messages. `appt-display-diary' - Display the diary buffer when the appointment list is - initialized for the first time in a day. + Non-nil means display the diary whenever the appointment list is + initialized (e.g. the first time we check for appointments each day). The following variables are only relevant if reminders are being displayed in a window: `appt-display-duration' - The number of seconds an appointment message is displayed. + Number of seconds for which an appointment message is displayed. `appt-disp-window-function' Function called to display appointment window. @@ -271,54 +323,48 @@ displayed in a window: `appt-delete-window-function' Function called to remove appointment window and buffer." (interactive "P") ; so people can force updates - (let* ((min-to-app -1) - (prev-appt-mode-string appt-mode-string) - (prev-appt-display-count (or appt-display-count 0)) - ;; Non-nil means do a full check for pending appointments and - ;; display in whatever ways the user has selected. When no - ;; appointment is being displayed, we always do a full check. - (full-check - (or (not appt-now-displayed) - ;; This is true every appt-display-interval minutes. - (zerop (mod prev-appt-display-count appt-display-interval)))) - ;; Non-nil means only update the interval displayed in the mode line. - (mode-line-only (unless full-check appt-now-displayed)) - now cur-comp-time appt-comp-time appt-warn-time) - (when (or full-check mode-line-only) - (save-excursion ; FIXME ? - ;; Convert current time to minutes after midnight (12.01am = 1). - (setq now (decode-time) - cur-comp-time (+ (* 60 (nth 2 now)) (nth 1 now))) - ;; At first check in any day, update appointments to today's list. - (if (or force ; eg initialize, diary save - (null appt-prev-comp-time) ; first check - (< cur-comp-time appt-prev-comp-time)) ; new day - (ignore-errors - (let ((diary-hook (if (assoc 'appt-make-list diary-hook) - diary-hook - (cons 'appt-make-list diary-hook)))) - (if appt-display-diary - (diary) - ;; Not displaying the diary, so we can ignore - ;; diary-number-of-entries. Since appt.el only - ;; works on a daily basis, no need for more entries. - (diary-list-entries (calendar-current-date) 1 t))))) - (setq appt-prev-comp-time cur-comp-time - appt-mode-string nil - appt-display-count nil) - ;; If there are entries in the list, and the user wants a - ;; message issued, get the first time off of the list and - ;; calculate the number of minutes until the appointment. - (when appt-time-msg-list - (setq appt-comp-time (caar (car appt-time-msg-list)) - appt-warn-time (or (nth 3 (car appt-time-msg-list)) - appt-message-warning-time) - min-to-app (- appt-comp-time cur-comp-time)) - (while (and appt-time-msg-list - (< appt-comp-time cur-comp-time)) + (let* ((prev-appt-mode-string appt-mode-string) + (prev-appt-display-count appt-display-count) + ;; Convert current time to minutes after midnight (12.01am = 1). + (now (decode-time)) + (now-mins (+ (* 60 (nth 2 now)) (nth 1 now))) + appt-mins appt-warn-time min-to-app min-list string-list) + (save-excursion ; FIXME ? + ;; At first check in any day, update appointments to today's list. + (if (or force ; eg initialize, diary save + (null appt-prev-comp-time) ; first check + (< now-mins appt-prev-comp-time)) ; new day + (ignore-errors + (let ((diary-hook (if (assoc 'appt-make-list diary-hook) + diary-hook + (cons 'appt-make-list diary-hook)))) + (if appt-display-diary + (diary) + ;; Not displaying the diary, so we can ignore + ;; diary-number-of-entries. Since appt.el only + ;; works on a daily basis, no need for more entries. + (diary-list-entries (calendar-current-date) 1 t))))) + ;; Reset everything now in case we somehow missed a minute, + ;; or (more likely) an appt was deleted. (This is the only + ;; reason we need prev-appt-display-count.) + (setq appt-prev-comp-time now-mins + appt-mode-string nil + appt-display-count 0) + ;; If there are entries in the list get each time off of the + ;; list and calculate the number of minutes until the appointment. + ;; TODO we are looping over all the appointments each time. + ;; We could instead sort them by the time at which we need to + ;; start warning. But then removing entries in the past becomes + ;; less straightforward. + (dolist (appt appt-time-msg-list) + ;; Remove any entries that are in the past. + ;; FIXME how can there be any such entries, given that this + ;; function removes entries when they hit zero minutes, + ;; and appt-make-list doesn't add any in the past in the first place? + (if (< (setq appt-mins (caar appt)) now-mins) (setq appt-time-msg-list (cdr appt-time-msg-list)) - (if appt-time-msg-list - (setq appt-comp-time (caar (car appt-time-msg-list))))) + (setq appt-warn-time (or (nth 3 appt) appt-message-warning-time) + min-to-app (- appt-mins now-mins)) ;; If we have an appointment between midnight and ;; `appt-warn-time' minutes after midnight, we ;; must begin to issue a message before midnight. Midnight @@ -327,45 +373,59 @@ displayed in a window: ;; appointment variable. It is equal to the number of ;; minutes before midnight plus the number of minutes after ;; midnight our appointment is. - (if (and (< appt-comp-time appt-warn-time) - (> (+ cur-comp-time appt-warn-time) - appt-max-time)) - (setq min-to-app (+ (- (1+ appt-max-time) cur-comp-time) - appt-comp-time))) - ;; Issue warning if the appointment time is within - ;; appt-message-warning time. + ;; FIXME but appt-make-list constructs appt-time-msg-list to only + ;; contain entries with today's date, so this cannot work? + ;; Also above we just removed anything with appt-mins < now-mins. + (if (and (< appt-mins appt-warn-time) + (> (+ now-mins appt-warn-time) appt-max-time)) + (setq min-to-app (+ (- (1+ appt-max-time) now-mins) + appt-mins))) + ;; Issue warning if the appointment time is within the warning time. (when (and (<= min-to-app appt-warn-time) (>= min-to-app 0)) - (setq appt-now-displayed t - appt-display-count (1+ prev-appt-display-count)) - (unless mode-line-only - (appt-display-message (cadr (car appt-time-msg-list)) - min-to-app)) - (when appt-display-mode-line - (setq appt-mode-string - (concat " " (propertize - (format "App't in %s min." min-to-app) - 'face 'mode-line-emphasis)))) - ;; When an appointment is reached, delete it from the - ;; list. Reset the count to 0 in case we display another - ;; appointment on the next cycle. + (push min-to-app min-list) + (push (cadr appt) string-list) + ;; When an appointment is reached, delete it from the list. (if (zerop min-to-app) - (setq appt-time-msg-list (cdr appt-time-msg-list) - appt-display-count nil)))) - ;; If we have changed the mode line string, redisplay all mode lines. - (and appt-display-mode-line - (not (string-equal appt-mode-string - prev-appt-mode-string)) - (progn - (force-mode-line-update t) - ;; If the string now has a notification, redisplay right now. - (if appt-mode-string - (sit-for 0)))))))) + (setq appt-time-msg-list (delete appt appt-time-msg-list)))))) + (when min-list + (setq min-list (nreverse min-list) + string-list (nreverse string-list)) + ;; This is true every appt-display-interval minutes from the + ;; time at which we first started reminding. + ;; TODO in the case of multiple appointments, whose interval + ;; should we respect? The first one that we started warning about? + ;; That's what we do now, and this makes sense if you interpret + ;; a-d-i as "don't remind me any more frequently than this". + ;; But should we always show a message when a new appt becomes due? + ;; When one appt gets removed, should we switch to the interval + ;; of the next? + (and (zerop (mod prev-appt-display-count appt-display-interval)) + (appt-display-message string-list min-list)) + (when appt-display-mode-line + (setq appt-mode-string + (concat " " (propertize + (appt-mode-line (mapcar 'number-to-string + min-list) t) + 'face 'mode-line-emphasis)))) + ;; Reset count to 0 in case we display another appt on the next cycle. + (setq appt-display-count (if (eq '(0) min-list) 0 + (1+ prev-appt-display-count)))) + ;; If we have changed the mode line string, redisplay all mode lines. + (and appt-display-mode-line + (not (string-equal appt-mode-string prev-appt-mode-string)) + (progn + (force-mode-line-update t) + ;; If the string now has a notification, redisplay right now. + (if appt-mode-string + (sit-for 0))))))) (defun appt-disp-window (min-to-app new-time appt-msg) "Display appointment due in MIN-TO-APP (a string) minutes. -NEW-TIME is a string giving the date. Displays the appointment -message APPT-MSG in a separate buffer." +NEW-TIME is a string giving the current date. +Displays the appointment message APPT-MSG in a separate buffer. +The arguments may also be lists, where each element relates to a +separate appointment." (let ((this-window (selected-window)) (appt-disp-buf (get-buffer-create appt-buffer-name))) ;; Make sure we're not in the minibuffer before splitting the window. @@ -386,17 +446,29 @@ message APPT-MSG in a separate buffer." (when (>= (window-height) (* 2 window-min-height)) (select-window (split-window)))) (switch-to-buffer appt-disp-buf)) + (or (listp min-to-app) + (setq min-to-app (list min-to-app) + appt-msg (list appt-msg))) + ;; I don't really see the point of the new-time argument. + ;; It repeatedly reminds you of the date? + ;; It would make more sense if it was eg the time of the appointment. + ;; Let's allow it to be a list or not independent of the other elements. + (or (listp new-time) + (setq new-time (list new-time))) ;; FIXME Link to diary entry? (calendar-set-mode-line - (format " Appointment %s. %s " - (if (string-equal "0" min-to-app) "now" - (format "in %s minute%s" min-to-app - (if (string-equal "1" min-to-app) "" "s"))) - new-time)) + (format " %s. %s" (appt-mode-line min-to-app) + (mapconcat 'identity new-time ", "))) (setq buffer-read-only nil buffer-undo-list t) (erase-buffer) - (insert appt-msg) + ;; If we have appointments at different times, prepend the times. + (if (or (= 1 (length min-to-app)) + (not (delete (car min-to-app) min-to-app))) + (insert (mapconcat 'identity appt-msg "\n")) + (dotimes (i (length appt-msg)) + (insert (format "%s%sm: %s" (if (> i 0) "\n" "") + (nth i min-to-app) (nth i appt-msg))))) (shrink-window-if-larger-than-buffer (get-buffer-window appt-disp-buf t)) (set-buffer-modified-p nil) (setq buffer-read-only t) @@ -509,8 +581,7 @@ Any appointments made with `appt-add' are not affected by this function." ;; entry begins with a time, add it to the ;; appt-time-msg-list. Then sort the list. (let ((entry-list diary-entries-list) - (new-time-string "") - time-string) + time-string literal) ;; Below, we assume diary-entries-list was in date ;; order. It is, unless something on ;; diary-list-entries-hook has changed it, eg @@ -531,7 +602,10 @@ Any appointments made with `appt-add' are not affected by this function." (while (and entry-list (calendar-date-equal (calendar-current-date) (caar entry-list))) - (setq time-string (cadr (car entry-list))) + (setq time-string (cadr (car entry-list)) + ;; Including any comments. + literal (or (nth 2 (nth 3 (car entry-list))) + time-string)) (while (string-match appt-time-regexp time-string) (let* ((beg (match-beginning 0)) ;; Get just the time for this appointment. @@ -542,29 +616,41 @@ Any appointments made with `appt-add' are not affected by this function." (concat "\n[ \t]*" appt-time-regexp) time-string (match-end 0))) + (warntime + (if (string-match appt-warning-time-regexp literal) + (string-to-number (match-string 1 literal)))) ;; Get the whole string for this appointment. (appt-time-string (substring time-string beg end)) + ;; FIXME why the list? It makes the first + ;; element (MINUTES) rather than MINUTES. (appt-time (list (appt-convert-time only-time))) - (time-msg (list appt-time appt-time-string))) + (time-msg (append + (list appt-time appt-time-string) + (if warntime (list nil warntime))))) ;; Add this appointment to appt-time-msg-list. (setq appt-time-msg-list (nconc appt-time-msg-list (list time-msg)) ;; Discard this appointment from the string. + ;; (This allows for multiple appts per entry.) time-string - (if end (substring time-string end) "")))) + (if end (substring time-string end) "")) + ;; Similarly, discard the start of literal. + (and (> (length time-string) 0) + (string-match appt-time-regexp literal) + (setq end (string-match + (concat "\n[ \t]*" appt-time-regexp) + literal (match-end 0))) + (setq literal (substring literal end))))) (setq entry-list (cdr entry-list))))) (setq appt-time-msg-list (appt-sort-list appt-time-msg-list)) ;; Convert current time to minutes after midnight (12:01am = 1), - ;; so that elements in the list that are earlier than the - ;; present time can be removed. + ;; and remove elements in the list that are in the past. (let* ((now (decode-time)) - (cur-comp-time (+ (* 60 (nth 2 now)) (nth 1 now))) - (appt-comp-time (caar (car appt-time-msg-list)))) - (while (and appt-time-msg-list (< appt-comp-time cur-comp-time)) - (setq appt-time-msg-list (cdr appt-time-msg-list)) - (if appt-time-msg-list - (setq appt-comp-time (caar (car appt-time-msg-list))))))))) + (now-mins (+ (* 60 (nth 2 now)) (nth 1 now)))) + (while (and appt-time-msg-list + (< (caar (car appt-time-msg-list)) now-mins)) + (setq appt-time-msg-list (cdr appt-time-msg-list))))))) (defun appt-sort-list (appt-list)