;;; planner-appt.el --- appointment alerts from planner ;; ;; ;; Copyright (C) 2005 Jim Ottaway ;; Copyright (C) 2005 Henrik S. Hansen ;; Parts copyright (C) 2005, 2006 Free Software Foundation, Inc. ;; Parts copyright (C) 2005 Seth Falcon ;; ;; Author: Jim Ottaway ;; Keywords: hypermedia ;; ;; This file is part of Planner. It is not part of GNU Emacs. ;; Planner is free software; you can redistribute it and/or modify it ;; under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3, or (at your option) ;; any later version. ;; Planner is distributed in the hope that it will be useful, but ;; WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with Planner; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;; ;; Please report any bugs that you come across to the authors at the ;; addresses given above. ;; ;; Usage: ;; ;; Add "(planner-appt-insinuate)" to your configuration to make ;; Planner work with appt. ;; ;; See the Appointments section of the Planner manual for further ;; details. ;; ;; Contributors: ;; ;; * Seth Falcon supplied the idea and the code that is the basis of ;; the forthcoming appointments display functionality. ;; ;;; TODO: ;; ;; * Correct sorting of task appointments ;; ;; * Consider changing "insinuate" into "install". I don't like the ;; word "insinuate" very much! Or a minor mode perhaps: ;; planner-appt-minor-mode ;; ;; * A lot of the code properly belongs elsewhere: schedule sorting, ;; schedule cyclical entries, calendar marking... ;; ;;; Code: (require 'planner) (require 'appt) (require 'calendar) ;;; Customization (defgroup planner-appt nil "Appointment integration for planner.el." :prefix "planner-appt-" :group 'planner) (defcustom planner-appt-schedule-section "Schedule" "The name of the section where the schedule is to be found." :group 'planner-appt :type 'string) (defcustom planner-appt-font-lock-appointments-flag t "Non-nil means use font-locking for appointments." :group 'planner-appt :type '(choice (const :tag "Use font-locking" t) (const :tag "Don't use font-locking" nil))) (defcustom planner-appt-update-appts-on-save-flag nil "Non-nil means update appointment alerts on saving today's plan." :group 'planner-appt :type '(choice (const :tag "Update on save" t) (const :tag "Don't update on save" nil))) (defcustom planner-appt-sort-schedule-on-update-flag nil "Non-nil means sort the schedule when updating appointments." :group 'planner-appt :type '(choice (const :tag "Sort on update" t) (const :tag "Don't sort on update" nil))) (defcustom planner-appt-update-hook '() "Hook run after appointments have been updated." :group 'planner-appt :type 'hook) (defcustom planner-appt-schedule-cyclic-behaviour 'today "Determines the behaviour of cyclical schedule insertion. Used after `planner-appt-schedule-cyclic-insinuate' has been called. 'today means only add cylical schedule entries for today 'future means add cyclical entries for all future day pages visited." :group 'planner-appt :type '(choice (const :tag "For today only" today) (const :tag "For all future pages." future))) (defcustom planner-appt-alert-buffer "*Alerts*" "Name of the buffer for displaying active alerts. Used by `planner-appt-show-alerts'." :group 'planner-appt :type 'string) (defcustom planner-appt-task-use-appointments-section-flag nil "When non-nil, task appointments will be copied to an appoinments section. The section name is supplied by `planner-appt-task-appointments-section'." :group 'planner-appt :type 'boolean) (defcustom planner-appt-task-appointments-section "Schedule" "Name of the section where task appointments are copied. The copying is contingent upon `planner-appt-task-use-appointments-section-flag'." :group 'planner-appt :type 'string) (defcustom planner-appt-format-appt-section-line-function #'planner-appt-format-appt-section-line "The function used when formatting an appointment section line. This function should take one argument: an appointment description. The description is in the form used when an appointment alert is signalled: a string with the time of the appointment and some text such as \"12:00 Do something\". Look at the default function `planner-appt-format-appt-section-line' for inspiration if you want to make a different format." :group 'planner-appt :type 'function) (defcustom planner-appt-limit-highlighting-flag t "When non-nil, only highlight appointment times in tasks and the schedule. When nil, all appointment times are highlighted, wherever they may be in the buffer." :group 'planner-appt :type 'boolean) (defcustom planner-appt-forthcoming-days 7 "Number of days to look ahead for appointments." :group 'planner-appt :type 'integer) (defcustom planner-appt-forthcoming-appt-section "Forthcoming Appointments" "Title of the section for forthcoming appointments." :group 'planner-appt :type 'string) (defcustom planner-appt-forthcoming-show-day-names-flag t "When non nil, show day names in forthcoming appointments." :group 'planner-appt :type 'boolean) (defcustom planner-appt-forthcoming-repeat-date-string " " "String to insert for repeated dates. When there are multiple appointments for a date, the date is inserted in the first appointment and the others have this string in their date cell. If the string consists of anything other than whitespace, then a link to the day page for the appoinment is created." :group 'planner-appt :type 'string) (defcustom planner-appt-forthcoming-look-at-cyclic-flag nil "When non nil, add cyclic entries to the forthcoming appointments section." :group 'planner-appt :type 'boolean) ;; Regular Expressions ;; TODO: Should these really be customizable anyway? ;; TODO: Dynamically changing dependent customizations; i.e., if this ;; is changed, all the other time-based regexps should change too [I ;; don't understand customize well enough to do this]. (defcustom planner-appt-time-regexp "[0-9]?[0-9]:[0-5][0-9]\\(?:am\\|pm\\)?" "Regular expression matching times." :group 'planner-appt :type 'regexp) (defcustom planner-appt-task-regexp (concat "[@!][ \t]*\\(" planner-appt-time-regexp "\\)[ \t]*") "If a task description matches this regexp, it's an appointment. Match group 1 is the time of the appointment. Used with the task-based method. If you use schedules, look at `planner-appt-schedule-appt-regexp'." :group 'planner-appt :type 'regexp) (defcustom planner-appt-task-nagging-regexp (concat "![ \t]*\\(" planner-appt-time-regexp "\\)[ \t]*") "If a task description matches this regexp, it's a nagging appointment. Used with the task-based method. If you use schedules, look at `planner-appt-schedule-appt-regexp'." :group 'planner-appt :type 'regexp) (defcustom planner-appt-schedule-basic-regexp (concat "\\(" ;; the appointment time (match group 1) planner-appt-time-regexp "\\)" ;; possibly some space, possibly a |, and any amount of space "[ \t]*|?[ \t]*" ;; perhaps another time [the end time] (match group 2) "\\(" planner-appt-time-regexp "\\)?" ;; possibly some space or some ?' chars, possibly a |, and any ;; amount of space "[' \t]*|?[ \t]*" ;; the appointment text (match group 3) "\\(.+\\)") "Basic regular expression to match a schedule. Match group 1 should yield the start time, match group 2 the stop time, and match group 3 the schedule text." :group 'planner-appt) ;; NB: The groups are shifted in this regexp. (defcustom planner-appt-schedule-regexp (concat ;; any amount of whitespace possibly followed by @ and any amount ;; of whitespace "^[ \t]*\\(@?\\)[ \t]*" ;; followed by the basic regexp planner-appt-schedule-basic-regexp) "Regexp matching schedule entries. Match group 1 should match at most one leading instance of the appointment marker, Match group 2 should yield the start time, match group 3 the stop time, and match group 4 the schedule text." :group 'planner-appt :type 'regexp) (defcustom planner-appt-schedule-appt-regexp (concat ;; any amount of whitespace followed by @ and any amount of ;; whitespace "^[ \t]*@[ \t]*" ;; followed by the basic regexp planner-appt-schedule-basic-regexp) "Regexp matching appointments in the schedule requiring alerts. Used with the schedule-based method. If you use tasks for appointments, look at `planner-appt-task-regexp.' Match group 1 should yield the start time, match group 2 the stop time, and match group 3 the alert text." :group 'planner-appt :type 'regexp) ;;; Planner Miscellany ;; Could be useful elsewhere in planner? (defun planner-appt-todays-page-p () "Return t if the current page is today's, otherwise nil." (string= (planner-page-name) (planner-today))) (defun planner-appt-seek-to-end-of-current-section () "Go to the end of the current section." (goto-char (or (and (re-search-forward "^\\*[^*\n]" nil t) (1- (planner-line-beginning-position))) (point-max)))) (defvar planner-appt-write-file-hook (if (and (boundp 'write-file-functions) (not (featurep 'xemacs))) 'write-file-functions 'write-file-hooks) "The write file hook to use.") ;;; Planner-Appt Miscellany (defvar planner-appt-debug-buffer "*planner-appt debug messages*" "The buffer to put debugging messages from planner-appt.") (defvar planner-appt-debug-flag nil "Non-nil means turn on planner-appt debugging.") (defmacro planner-appt-debug (form &rest body) "Evaluate FORM if `planner-appt-debug-flag' is non-nil. Optional BODY is evaluated otherwise." `(if planner-appt-debug-flag ,form ,@body)) (defun planner-appt-debug-message (&rest args) "Insert ARGS into `planner-appt-debug-buffer'. This code runs only if `planner-appt-debug-flag' is non-nil." (planner-appt-debug (with-current-buffer (get-buffer-create planner-appt-debug-buffer) (goto-char (point-max)) (apply #'insert args) (insert ?\n)))) (defun planner-appt-earlier-than-now-p (time) "Return t if TIME is earlier than the current time. Time formats are those used by the appt appointment system." ;; From appt-check (let* ((now (decode-time)) (cur-hour (nth 2 now)) (cur-min (nth 1 now)) (cur-time (+ (* cur-hour 60) cur-min))) (> cur-time (appt-convert-time time)))) ;; Not used in this file, but added for completeness. (defun planner-appt-later-than-now-p (time) "Return t if TIME is later than the current time. Time formats are those used by the appt appointment system." ;; From appt-check (let* ((now (decode-time)) (cur-hour (nth 2 now)) (cur-min (nth 1 now)) (cur-time (+ (* cur-hour 60) cur-min))) (< cur-time (appt-convert-time time)))) (defvar --planner-appt-tasks-added-appts '() "Internal variable: Tracks added task-based appointment alerts.") (defvar --planner-appt-tasks-earlier-appts '() "Internal variable: Tracks appointments ignored because they were too early.") (defun planner-appt-clear-appts (appt-list) (while appt-list (setq appt-time-msg-list (delete (pop appt-list) appt-time-msg-list)))) (defun planner-appt-format-time-and-description (time description) "Format TIME [a string] and DESCRIPTION as an appointment." (concat time " " description)) (eval-and-compile (if (> emacs-major-version 21) (defun planner-appt-make-appt-element (time text) (list (list (appt-convert-time time)) (planner-appt-format-time-and-description time text) t)) (defun planner-appt-make-appt-element (time text) (list (list (appt-convert-time time)) (planner-appt-format-time-and-description time text))))) (defun planner-appt-remember-appt (time text list) "Store details of an appointment with TIME and TEXT in LIST. Return the new list." (push (planner-appt-make-appt-element time text) list)) (defun planner-appt-forget-appt (appt appt-list) "Remove APPT from APPT-LIST and return the new list. APPT is in the appt format." (delete (car (member appt appt-list)) appt-list)) (defun planner-appt-add-hook (hook function &optional append global) "Add to the value of HOOK the function FUNCTION. This is `add-hook' with local and global switched. FUNCTION is not added if already present. FUNCTION is added (if necessary) at the beginning of the hook list unless the optional argument APPEND is non-nil, in which case FUNCTION is added at the end. The optional fourth argument, GLOBAL, if non-nil, says to modify the hook's global value rather than its local value." (add-hook hook function append (not global))) (defun planner-appt-remove-task-id (description) (if (string-match (concat "\\s-*" (if (boundp 'planner-id-regexp) planner-id-regexp)) description) (replace-match "" t t description) description)) (defun planner-appt-format-description (description) (planner-appt-remove-task-id (planner-remove-links description))) ;;; Advice ;; for speedy enabling and disabling of advice: (defvar --planner-appt-advice '() "Internal variable: List of advices added by `planner-appt-defadvice'. Each element is a list of args for `ad-enable-advice' and `ad-disable-advice'.") (eval-and-compile (defvar planner-appt-advice-common-flags '(preactivate disable) "Advice flags common to all planner-appt advice.")) (defmacro planner-appt-defadvice (function args doc &rest body) "Advise FUNCTION with ARGS, DOC and BODY. Remembers the advice function and args in `--planner-appt-advice'." `(prog1 (defadvice ,function (,@args ,@planner-appt-advice-common-flags) ,doc ,@body) (let ((info '(,function ,(car args) ,(cadr args)))) (unless (member info --planner-appt-advice) (push info --planner-appt-advice))))) (put 'planner-appt-defadvice 'edebug-form-spec '(&define name (name name &rest sexp) stringp [&optional ("interactive" interactive)] def-body)) (put 'planner-appt-defadvice 'lisp-indent-function 'defun) ;; See what happened with the preactivation. (planner-appt-debug (progn (require 'trace) (trace-function-background 'ad-cache-id-verification-code "*planner-appt advice trace*"))) (defun planner-appt-disable-all-advice () "Disable all advice added with `planner-appt-defadvice'." (mapcar #'(lambda (args) (apply #'ad-disable-advice args) (ad-activate (car args))) --planner-appt-advice)) (defun planner-appt-enable-all-advice () "Enable all advice added with `planner-appt-defadvice'." (mapcar #'(lambda (args) (apply #'ad-enable-advice args) (ad-activate (car args))) --planner-appt-advice)) (defmacro with-planner-appt-task-advice-disabled (&rest body) "Evaluate BODY forms with all advice matching \"planner-appt-task\" disabled." `(unwind-protect (progn (planner-appt-disable-all-advice) (planner-appt-debug-message "all advice disabled") ,@body) (planner-appt-enable-all-advice) (planner-appt-debug-message "all advice enabled"))) (put 'with-planner-appt-task-advice-disabled 'lisp-indent-function 0) (put 'with-planner-appt-task-advice-disabled 'edebug-form-spec '(body)) ;;; Showing Appointments In Various Ways (defvar planner-appt-methods '() "Methods used for appointment alerts. Internal variable: to set up appointment methods use one of: `planner-appt-use-tasks' `planner-appt-use-schedule' `planner-appt-use-tasks-and-schedule'.") ;; Copying task appts over to an "Appointments" section. (defun planner-appt-format-appt-section-line (desc) "Format DESC as a line for the appointments section." (let* ((td (planner-appt-task-parse-task ;; Trick the function into parsing: (concat "@" desc))) (text (car td)) (time (cadr td)) (end-time (if (string-match (format "\\s-*\\(%s\\)\\s-*" planner-appt-time-regexp) text) (prog1 (match-string 1 text) (setq text (replace-match "" t t text))) " "))) ;; Format in the style of a tabular schedule. (format "%6s | %5s | %s" ;; Using an @ means the time gets fontified for free. (concat "@" time) end-time (if (string= planner-appt-task-appointments-section planner-appt-schedule-section) ;; To avoid confusion, add an indication that this ;; item came from a task. (concat "# " text) text)))) (defvar --planner-appt-lines-added-to-section '() "Internal variable: Remembers lines added by `planner-appt-update-appt-section' the last time it was called.") (defun planner-appt-task-schedule-item-p (string) "Return t if STRING is a schedule item derived from a task." (member string --planner-appt-lines-added-to-section)) ;; ;; Look for any property in the string since STRING will usually be ;; ;; derived from a buffer substring which may have been edited. ;; (text-property-any 0 (length string) 'appt-task t string)) (defun planner-appt-update-appt-section () (save-excursion (planner-seek-to-first planner-appt-task-appointments-section) (let ((bound (make-marker)) (lines-to-delete (copy-sequence --planner-appt-lines-added-to-section)) line) (save-excursion (planner-appt-seek-to-end-of-current-section) (set-marker bound (point))) (dolist (appt (append --planner-appt-tasks-added-appts --planner-appt-tasks-earlier-appts)) (setq line (funcall planner-appt-format-appt-section-line-function (cadr appt))) (setq lines-to-delete (delete line lines-to-delete)) (save-excursion (unless (search-forward line bound t) (insert line ?\n))) ;; Remember the line even if it was already there (push line --planner-appt-lines-added-to-section)) ;; Remove lines of deleted tasks (dolist (del-line lines-to-delete) (setq --planner-appt-lines-added-to-section (delete del-line --planner-appt-lines-added-to-section)) (save-excursion (when (search-forward del-line bound t) (replace-match "") (when (eq (char-after) ?\n) (delete-char 1))))) (set-marker bound nil)) ;; Use schedule sorting with some changes (let ((planner-appt-schedule-section planner-appt-task-appointments-section) (planner-appt-schedule-regexp (concat "\\(.*?\\)" ; to shift the match groups planner-appt-schedule-basic-regexp))) (planner-appt-schedule-sort)))) (defun planner-appt-update-appt-section-maybe () (when (and ;; The appointment section is only relevant if the task ;; method is used (memq 'tasks planner-appt-methods) planner-appt-task-use-appointments-section-flag) (with-planner-update-setup (save-excursion (with-planner-appt-task-advice-disabled (planner-goto-today)) (planner-appt-update-appt-section))))) (defmacro with-planner-appt-update-section-disabled (&rest body) `(let ((planner-appt-task-use-appointments-section-flag nil)) ,@body)) (put 'with-planner-appt-update-section-disabled 'lisp-indent-function 0) (put 'with-planner-appt-update-section-disabled 'edebug-form-spec '(body)) ;; Compatibility fix for Xemacs [and for Emacs <21?] (if (fboundp 'fit-window-to-buffer) (defalias 'planner-fit-window-to-buffer 'fit-window-to-buffer) (defalias 'planner-fit-window-to-buffer 'shrink-window-if-larger-than-buffer)) ;; Display Current Appointments (defun planner-appt-show-alerts () "Display a list of currently active alerts in another window." (interactive) (let ((buf (get-buffer-create planner-appt-alert-buffer))) (with-current-buffer buf (erase-buffer) (insert "Current alerts\n==============") (if appt-time-msg-list (dolist (appt appt-time-msg-list) (insert "\n" (cadr appt))) (insert "\nNone")) (goto-char (point-min))) (planner-fit-window-to-buffer (display-buffer buf)))) ;; Display/Insert Forthcoming Appointments (defvar planner-appt-forthcoming-regexp (concat "\\(" planner-appt-schedule-appt-regexp "\\)\\|\\(" planner-live-task-regexp planner-appt-task-regexp "\\)")) (defvar planner-appt-forthcoming-task-regexp (concat planner-live-task-regexp planner-appt-task-regexp)) (defun planner-appt-forthcoming-format-appt-description (time description) (funcall planner-appt-format-appt-section-line-function (planner-appt-format-time-and-description time (planner-appt-format-description description)))) (defun planner-appt-forthcoming-task-data (info) (let ((task-appt (planner-appt-task-parse-task (planner-task-description info)))) (when task-appt (cons (appt-convert-time (nth 1 task-appt)) (planner-appt-forthcoming-format-appt-description (nth 1 task-appt) (nth 0 task-appt)))))) (defun planner-appt-forthcoming-get-appts (n &optional include-today) (planner-save-buffers) (let ((appts '()) (pages (planner-get-day-pages (if include-today (planner-today) (planner-calculate-date-from-day-offset (planner-today) 1)) (planner-calculate-date-from-day-offset (planner-today) (if include-today n (1+ n))))) cyclic-data cyclic-task-descriptions line task-info task-data date-absolute date time text) ;; After scanning pages and [conditionally] cyclic entries, each ;; element of appts has: ;; ;; ( ;;