Fix problem introduced by recent commit that broke using SPC as separator.
[planner-el.git] / planner-appt.el
blobb16538d014ae14e588bc0c76e02393d5925a432e
1 ;;; planner-appt.el --- appointment alerts from planner
2 ;;
3 ;;
4 ;; Copyright (C) 2005, 2006, 2007, 2008 Jim Ottaway <j.ottaway@lse.ac.uk>
5 ;; Copyright (C) 2005, 2008 Henrik S. Hansen <hsh@freecode.dk>
6 ;; Parts copyright (C) 2005, 2006, 2008 Free Software Foundation, Inc.
7 ;; Parts copyright (C) 2005, 2008 Seth Falcon <sethfalcon AT gmail.com>
8 ;;
9 ;; Author: Jim Ottaway
10 ;; Keywords: hypermedia
13 ;; This file is part of Planner. It is not part of GNU Emacs.
15 ;; Planner is free software; you can redistribute it and/or modify it
16 ;; under the terms of the GNU General Public License as published by
17 ;; the Free Software Foundation; either version 3, or (at your option)
18 ;; any later version.
20 ;; Planner is distributed in the hope that it will be useful, but
21 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23 ;; General Public License for more details.
25 ;; You should have received a copy of the GNU General Public License
26 ;; along with Planner; see the file COPYING. If not, write to the
27 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
28 ;; Boston, MA 02110-1301, USA.
31 ;; Please report any bugs that you come across to the authors at the
32 ;; addresses given above.
34 ;; Usage:
36 ;; Add "(planner-appt-insinuate)" to your configuration to make
37 ;; Planner work with appt.
39 ;; See the Appointments section of the Planner manual for further
40 ;; details.
42 ;; Contributors:
44 ;; * Seth Falcon supplied the idea and the code that is the basis of
45 ;; the forthcoming appointments display functionality.
47 ;;; TODO:
49 ;; * Correct sorting of task appointments
51 ;; * Consider changing "insinuate" into "install". I don't like the
52 ;; word "insinuate" very much! Or a minor mode perhaps:
53 ;; planner-appt-minor-mode
55 ;; * A lot of the code properly belongs elsewhere: schedule sorting,
56 ;; schedule cyclical entries, calendar marking...
59 ;;; Code:
62 (require 'planner)
63 (require 'appt)
64 (require 'calendar)
66 ;;; Customization
68 (defgroup planner-appt nil
69 "Appointment integration for planner.el."
70 :prefix "planner-appt-"
71 :group 'planner)
73 (defcustom planner-appt-schedule-section "Schedule"
74 "The name of the section where the schedule is to be found."
75 :group 'planner-appt
76 :type 'string)
78 (defcustom planner-appt-font-lock-appointments-flag t
79 "Non-nil means use font-locking for appointments."
80 :group 'planner-appt
81 :type '(choice (const :tag "Use font-locking" t)
82 (const :tag "Don't use font-locking" nil)))
84 (defcustom planner-appt-update-appts-on-save-flag nil
85 "Non-nil means update appointment alerts on saving today's plan."
86 :group 'planner-appt
87 :type '(choice (const :tag "Update on save" t)
88 (const :tag "Don't update on save" nil)))
90 (defcustom planner-appt-sort-schedule-on-update-flag nil
91 "Non-nil means sort the schedule when updating appointments."
92 :group 'planner-appt
93 :type '(choice (const :tag "Sort on update" t)
94 (const :tag "Don't sort on update" nil)))
97 (defcustom planner-appt-update-hook '()
98 "Hook run after appointments have been updated."
99 :group 'planner-appt
100 :type 'hook)
102 (defcustom planner-appt-schedule-cyclic-behaviour 'today
103 "Determines the behaviour of cyclical schedule insertion.
104 Used after `planner-appt-schedule-cyclic-insinuate' has been called.
105 'today means only add cylical schedule entries for today
106 'future means add cyclical entries for all future day pages visited."
107 :group 'planner-appt
108 :type '(choice (const :tag "For today only" today)
109 (const :tag "For all future pages." future)))
111 (defcustom planner-appt-alert-buffer "*Alerts*"
112 "Name of the buffer for displaying active alerts.
113 Used by `planner-appt-show-alerts'."
114 :group 'planner-appt
115 :type 'string)
118 (defcustom planner-appt-task-use-appointments-section-flag nil
119 "When non-nil, task appointments will be copied to an appoinments section.
120 The section name is supplied by
121 `planner-appt-task-appointments-section'."
122 :group 'planner-appt
123 :type 'boolean)
125 (defcustom planner-appt-task-appointments-section "Schedule"
126 "Name of the section where task appointments are copied.
127 The copying is contingent upon
128 `planner-appt-task-use-appointments-section-flag'."
129 :group 'planner-appt
130 :type 'string)
132 (defcustom planner-appt-format-appt-section-line-function
133 #'planner-appt-format-appt-section-line
134 "The function used when formatting an appointment section line.
136 This function should take one argument: an appointment description.
137 The description is in the form used when an appointment alert is
138 signalled: a string with the time of the appointment and some text
139 such as \"12:00 Do something\". Look at the default function
140 `planner-appt-format-appt-section-line' for inspiration if you want to
141 make a different format."
142 :group 'planner-appt
143 :type 'function)
145 (defcustom planner-appt-limit-highlighting-flag t
146 "When non-nil, only highlight appointment times in tasks and the schedule.
147 When nil, all appointment times are highlighted, wherever they may be
148 in the buffer."
149 :group 'planner-appt
150 :type 'boolean)
152 (defcustom planner-appt-forthcoming-days 7
153 "Number of days to look ahead for appointments."
154 :group 'planner-appt
155 :type 'integer)
157 (defcustom planner-appt-forthcoming-appt-section "Forthcoming Appointments"
158 "Title of the section for forthcoming appointments."
159 :group 'planner-appt
160 :type 'string)
162 (defcustom planner-appt-forthcoming-show-day-names-flag t
163 "When non nil, show day names in forthcoming appointments."
164 :group 'planner-appt
165 :type 'boolean)
167 (defcustom planner-appt-forthcoming-repeat-date-string " "
168 "String to insert for repeated dates.
169 When there are multiple appointments for a date, the date is inserted
170 in the first appointment and the others have this string in their date
171 cell.
173 If the string consists of anything other than whitespace, then a link
174 to the day page for the appoinment is created."
175 :group 'planner-appt
176 :type 'string)
178 (defcustom planner-appt-forthcoming-look-at-cyclic-flag nil
179 "When non nil, add cyclic entries to the forthcoming appointments section."
180 :group 'planner-appt
181 :type 'boolean)
184 ;; Regular Expressions
186 ;; TODO: Should these really be customizable anyway?
188 ;; TODO: Dynamically changing dependent customizations; i.e., if this
189 ;; is changed, all the other time-based regexps should change too [I
190 ;; don't understand customize well enough to do this].
192 (defcustom planner-appt-time-regexp
193 "[0-9]?[0-9]:[0-5][0-9]\\(?:am\\|pm\\)?"
194 "Regular expression matching times."
195 :group 'planner-appt
196 :type 'regexp)
198 (defcustom planner-appt-task-regexp
199 (concat "[@!][ \t]*\\(" planner-appt-time-regexp "\\)[ \t]*")
200 "If a task description matches this regexp, it's an appointment.
201 Match group 1 is the time of the appointment.
202 Used with the task-based method. If you use schedules, look at
203 `planner-appt-schedule-appt-regexp'."
204 :group 'planner-appt
205 :type 'regexp)
207 (defcustom planner-appt-task-nagging-regexp
208 (concat "![ \t]*\\(" planner-appt-time-regexp "\\)[ \t]*")
209 "If a task description matches this regexp, it's a nagging
210 appointment. Used with the task-based method. If you use schedules,
211 look at `planner-appt-schedule-appt-regexp'."
212 :group 'planner-appt
213 :type 'regexp)
215 (defcustom planner-appt-schedule-basic-regexp
216 (concat
217 "\\("
218 ;; the appointment time (match group 1)
219 planner-appt-time-regexp
220 "\\)"
221 ;; possibly some space, possibly a |, and any amount of space
222 "[ \t]*|?[ \t]*"
223 ;; perhaps another time [the end time] (match group 2)
224 "\\("
225 planner-appt-time-regexp
226 "\\)?"
227 ;; possibly some space or some ?' chars, possibly a |, and any
228 ;; amount of space
229 "[' \t]*|?[ \t]*"
230 ;; the appointment text (match group 3)
231 "\\(.+\\)")
232 "Basic regular expression to match a schedule.
233 Match group 1 should yield the start time, match group 2 the stop
234 time, and match group 3 the schedule text."
235 :group 'planner-appt)
237 ;; NB: The groups are shifted in this regexp.
238 (defcustom planner-appt-schedule-regexp
239 (concat
240 ;; any amount of whitespace possibly followed by @ and any amount
241 ;; of whitespace
242 "^[ \t]*\\(@?\\)[ \t]*"
243 ;; followed by the basic regexp
244 planner-appt-schedule-basic-regexp)
245 "Regexp matching schedule entries.
246 Match group 1 should match at most one leading instance of the
247 appointment marker, Match group 2 should yield the start time, match
248 group 3 the stop time, and match group 4 the schedule text."
249 :group 'planner-appt
250 :type 'regexp)
252 (defcustom planner-appt-schedule-appt-regexp
253 (concat
254 ;; any amount of whitespace followed by @ and any amount of
255 ;; whitespace
256 "^[ \t]*@[ \t]*"
257 ;; followed by the basic regexp
258 planner-appt-schedule-basic-regexp)
259 "Regexp matching appointments in the schedule requiring alerts.
260 Used with the schedule-based method. If you use tasks for appointments,
261 look at `planner-appt-task-regexp.'
262 Match group 1 should yield the start time, match group 2 the stop
263 time, and match group 3 the alert text."
264 :group 'planner-appt
265 :type 'regexp)
268 ;;; Planner Miscellany
270 ;; Could be useful elsewhere in planner?
272 (defun planner-appt-todays-page-p ()
273 "Return t if the current page is today's, otherwise nil."
274 (string= (planner-page-name) (planner-today)))
276 (defun planner-appt-seek-to-end-of-current-section ()
277 "Go to the end of the current section."
278 (goto-char
279 (or (and (re-search-forward "^\\*[^*\n]" nil t)
280 (1- (planner-line-beginning-position)))
281 (point-max))))
283 (defvar planner-appt-write-file-hook
284 (if (and (boundp 'write-file-functions)
285 (not (featurep 'xemacs)))
286 'write-file-functions
287 'write-file-hooks)
288 "The write file hook to use.")
290 ;;; Planner-Appt Miscellany
292 (defvar planner-appt-debug-buffer "*planner-appt debug messages*"
293 "The buffer to put debugging messages from planner-appt.")
295 (defvar planner-appt-debug-flag nil
296 "Non-nil means turn on planner-appt debugging.")
298 (defmacro planner-appt-debug (form &rest body)
299 "Evaluate FORM if `planner-appt-debug-flag' is non-nil.
300 Optional BODY is evaluated otherwise."
301 `(if planner-appt-debug-flag
302 ,form
303 ,@body))
305 (defun planner-appt-debug-message (&rest args)
306 "Insert ARGS into `planner-appt-debug-buffer'.
307 This code runs only if `planner-appt-debug-flag' is non-nil."
308 (planner-appt-debug
309 (with-current-buffer
310 (get-buffer-create planner-appt-debug-buffer)
311 (goto-char (point-max))
312 (apply #'insert args)
313 (insert ?\n))))
315 (defun planner-appt-earlier-than-now-p (time)
316 "Return t if TIME is earlier than the current time.
317 Time formats are those used by the appt appointment system."
318 ;; From appt-check
319 (let* ((now (decode-time))
320 (cur-hour (nth 2 now))
321 (cur-min (nth 1 now))
322 (cur-time (+ (* cur-hour 60) cur-min)))
323 (> cur-time (appt-convert-time time))))
325 ;; Not used in this file, but added for completeness.
326 (defun planner-appt-later-than-now-p (time)
327 "Return t if TIME is later than the current time.
328 Time formats are those used by the appt appointment system."
329 ;; From appt-check
330 (let* ((now (decode-time))
331 (cur-hour (nth 2 now))
332 (cur-min (nth 1 now))
333 (cur-time (+ (* cur-hour 60) cur-min)))
334 (< cur-time (appt-convert-time time))))
337 (defvar --planner-appt-tasks-added-appts '()
338 "Internal variable: Tracks added task-based appointment alerts.")
340 (defvar --planner-appt-tasks-earlier-appts '()
341 "Internal variable:
342 Tracks appointments ignored because they were too early.")
344 (defun planner-appt-clear-appts (appt-list)
345 (while appt-list
346 (setq appt-time-msg-list
347 (delete (pop appt-list) appt-time-msg-list))))
349 (defun planner-appt-format-time-and-description (time description)
350 "Format TIME [a string] and DESCRIPTION as an appointment."
351 (concat time " " description))
353 (eval-and-compile
354 (if (> emacs-major-version 21)
355 (defun planner-appt-make-appt-element (time text)
356 (list
357 (list (appt-convert-time time))
358 (planner-appt-format-time-and-description time text)
360 (defun planner-appt-make-appt-element (time text)
361 (list
362 (list (appt-convert-time time))
363 (planner-appt-format-time-and-description time text)))))
365 (defun planner-appt-remember-appt (time text list)
366 "Store details of an appointment with TIME and TEXT in LIST.
367 Return the new list."
368 (push (planner-appt-make-appt-element time text) list))
370 (defun planner-appt-forget-appt (appt appt-list)
371 "Remove APPT from APPT-LIST and return the new list.
372 APPT is in the appt format."
373 (delete (car (member appt appt-list)) appt-list))
375 (defun planner-appt-add-hook (hook function &optional append global)
376 "Add to the value of HOOK the function FUNCTION.
377 This is `add-hook' with local and global switched.
378 FUNCTION is not added if already present.
379 FUNCTION is added (if necessary) at the beginning of the hook list
380 unless the optional argument APPEND is non-nil, in which case
381 FUNCTION is added at the end.
382 The optional fourth argument, GLOBAL, if non-nil, says to modify
383 the hook's global value rather than its local value."
384 (add-hook hook function append (not global)))
387 (defun planner-appt-remove-task-id (description)
388 (if (string-match
389 (concat "\\s-*"
390 (if (boundp 'planner-id-regexp)
391 planner-id-regexp))
392 description)
393 (replace-match "" t t description)
394 description))
397 (defun planner-appt-format-description (description)
398 (planner-appt-remove-task-id
399 (planner-remove-links description)))
402 ;;; Advice
404 ;; for speedy enabling and disabling of advice:
406 (defvar --planner-appt-advice '()
407 "Internal variable: List of advices added by `planner-appt-defadvice'.
408 Each element is a list of args for `ad-enable-advice' and
409 `ad-disable-advice'.")
411 (eval-and-compile
412 (defvar planner-appt-advice-common-flags
413 '(preactivate disable)
414 "Advice flags common to all planner-appt advice."))
416 (defmacro planner-appt-defadvice (function args doc &rest body)
417 "Advise FUNCTION with ARGS, DOC and BODY.
418 Remembers the advice function and args in `--planner-appt-advice'."
419 `(prog1
420 (defadvice ,function
421 (,@args ,@planner-appt-advice-common-flags) ,doc ,@body)
422 (let ((info '(,function ,(car args) ,(cadr args))))
423 (unless (member info --planner-appt-advice)
424 (push info --planner-appt-advice)))))
426 (put 'planner-appt-defadvice
427 'edebug-form-spec
428 '(&define name
429 (name name &rest sexp)
430 stringp
431 [&optional
432 ("interactive" interactive)]
433 def-body))
435 (put 'planner-appt-defadvice 'lisp-indent-function 'defun)
437 ;; See what happened with the preactivation.
438 (planner-appt-debug
439 (progn
440 (require 'trace)
441 (trace-function-background
442 'ad-cache-id-verification-code
443 "*planner-appt advice trace*")))
445 (defun planner-appt-disable-all-advice ()
446 "Disable all advice added with `planner-appt-defadvice'."
447 (mapcar #'(lambda (args)
448 (apply #'ad-disable-advice args)
449 (ad-activate (car args)))
450 --planner-appt-advice))
452 (defun planner-appt-enable-all-advice ()
453 "Enable all advice added with `planner-appt-defadvice'."
454 (mapcar #'(lambda (args)
455 (apply #'ad-enable-advice args)
456 (ad-activate (car args)))
457 --planner-appt-advice))
460 (defmacro with-planner-appt-task-advice-disabled (&rest body)
461 "Evaluate BODY forms with all advice matching \"planner-appt-task\" disabled."
462 `(unwind-protect
463 (progn
464 (planner-appt-disable-all-advice)
465 (planner-appt-debug-message "all advice disabled")
466 ,@body)
467 (planner-appt-enable-all-advice)
468 (planner-appt-debug-message "all advice enabled")))
470 (put 'with-planner-appt-task-advice-disabled 'lisp-indent-function 0)
471 (put 'with-planner-appt-task-advice-disabled 'edebug-form-spec '(body))
473 ;;; Showing Appointments In Various Ways
475 (defvar planner-appt-methods '()
476 "Methods used for appointment alerts.
477 Internal variable: to set up appointment methods use one of:
478 `planner-appt-use-tasks'
479 `planner-appt-use-schedule'
480 `planner-appt-use-tasks-and-schedule'.")
482 ;; Copying task appts over to an "Appointments" section.
484 (defun planner-appt-format-appt-section-line (desc)
485 "Format DESC as a line for the appointments section."
486 (let* ((td (planner-appt-task-parse-task
487 ;; Trick the function into parsing:
488 (concat "@" desc)))
489 (text (car td))
490 (time (cadr td))
491 (end-time (if (string-match
492 (format "\\s-*\\(%s\\)\\s-*"
493 planner-appt-time-regexp)
494 text)
495 (prog1
496 (match-string 1 text)
497 (setq text (replace-match "" t t text)))
498 " ")))
499 ;; Format in the style of a tabular schedule.
500 (format "%6s | %5s | %s"
501 ;; Using an @ means the time gets fontified for free.
502 (concat "@" time)
503 end-time
504 (if (string= planner-appt-task-appointments-section
505 planner-appt-schedule-section)
506 ;; To avoid confusion, add an indication that this
507 ;; item came from a task.
508 (concat "# " text)
509 text))))
511 (defvar --planner-appt-lines-added-to-section '()
512 "Internal variable:
513 Remembers lines added by `planner-appt-update-appt-section' the last
514 time it was called.")
516 (defun planner-appt-task-schedule-item-p (string)
517 "Return t if STRING is a schedule item derived from a task."
518 (member string --planner-appt-lines-added-to-section))
519 ;; ;; Look for any property in the string since STRING will usually be
520 ;; ;; derived from a buffer substring which may have been edited.
521 ;; (text-property-any 0 (length string) 'appt-task t string))
523 (defun planner-appt-update-appt-section ()
524 (save-excursion
525 (planner-seek-to-first planner-appt-task-appointments-section)
526 (let ((bound (make-marker))
527 (lines-to-delete
528 (copy-sequence --planner-appt-lines-added-to-section))
529 line)
530 (save-excursion
531 (planner-appt-seek-to-end-of-current-section)
532 (set-marker bound (point)))
533 (dolist (appt (append --planner-appt-tasks-added-appts
534 --planner-appt-tasks-earlier-appts))
535 (setq line (funcall planner-appt-format-appt-section-line-function
536 (cadr appt)))
537 (setq lines-to-delete (delete line lines-to-delete))
538 (save-excursion
539 (unless (search-forward line bound t)
540 (insert line ?\n)))
541 ;; Remember the line even if it was already there
542 (push line --planner-appt-lines-added-to-section))
543 ;; Remove lines of deleted tasks
544 (dolist (del-line lines-to-delete)
545 (setq --planner-appt-lines-added-to-section
546 (delete del-line --planner-appt-lines-added-to-section))
547 (save-excursion
548 (when (search-forward del-line bound t)
549 (replace-match "")
550 (when (eq (char-after) ?\n)
551 (delete-char 1)))))
552 (set-marker bound nil))
553 ;; Use schedule sorting with some changes
554 (let ((planner-appt-schedule-section
555 planner-appt-task-appointments-section)
556 (planner-appt-schedule-regexp
557 (concat "\\(.*?\\)" ; to shift the match groups
558 planner-appt-schedule-basic-regexp)))
559 (planner-appt-schedule-sort))))
561 (defun planner-appt-update-appt-section-maybe ()
562 (when (and
563 ;; The appointment section is only relevant if the task
564 ;; method is used
565 (memq 'tasks planner-appt-methods)
566 planner-appt-task-use-appointments-section-flag)
567 (with-planner-update-setup
568 (save-excursion
569 (with-planner-appt-task-advice-disabled
570 (planner-goto-today))
571 (planner-appt-update-appt-section)))))
573 (defmacro with-planner-appt-update-section-disabled (&rest body)
574 `(let ((planner-appt-task-use-appointments-section-flag nil))
575 ,@body))
577 (put 'with-planner-appt-update-section-disabled 'lisp-indent-function 0)
578 (put 'with-planner-appt-update-section-disabled 'edebug-form-spec '(body))
580 ;; Compatibility fix for Xemacs [and for Emacs <21?]
581 (if (fboundp 'fit-window-to-buffer)
582 (defalias 'planner-fit-window-to-buffer 'fit-window-to-buffer)
583 (defalias 'planner-fit-window-to-buffer 'shrink-window-if-larger-than-buffer))
585 ;; Display Current Appointments
586 (defun planner-appt-show-alerts ()
587 "Display a list of currently active alerts in another window."
588 (interactive)
589 (let ((buf (get-buffer-create planner-appt-alert-buffer)))
590 (with-current-buffer buf
591 (erase-buffer)
592 (insert "Current alerts\n==============")
593 (if appt-time-msg-list
594 (dolist (appt appt-time-msg-list)
595 (insert "\n" (cadr appt)))
596 (insert "\nNone"))
597 (goto-char (point-min)))
598 (planner-fit-window-to-buffer (display-buffer buf))))
601 ;; Display/Insert Forthcoming Appointments
603 (defvar planner-appt-forthcoming-regexp
604 (concat "\\("
605 planner-appt-schedule-appt-regexp
606 "\\)\\|\\("
607 planner-live-task-regexp
608 planner-appt-task-regexp
609 "\\)"))
611 (defvar planner-appt-forthcoming-task-regexp
612 (concat planner-live-task-regexp
613 planner-appt-task-regexp))
615 (defun planner-appt-forthcoming-format-appt-description (time description)
616 (funcall
617 planner-appt-format-appt-section-line-function
618 (planner-appt-format-time-and-description
619 time
620 (planner-appt-format-description description))))
622 (defun planner-appt-forthcoming-task-data (info)
623 (let ((task-appt
624 (planner-appt-task-parse-task
625 (planner-task-description info))))
626 (when task-appt
627 (cons (appt-convert-time (nth 1 task-appt))
628 (planner-appt-forthcoming-format-appt-description
629 (nth 1 task-appt)
630 (nth 0 task-appt))))))
632 (defun planner-appt-forthcoming-get-appts (n &optional include-today)
633 (planner-save-buffers)
634 (let ((appts '())
635 (pages (planner-get-day-pages
636 (if include-today
637 (planner-today)
638 (planner-calculate-date-from-day-offset
639 (planner-today) 1))
640 (planner-calculate-date-from-day-offset
641 (planner-today) (if include-today n (1+ n)))))
642 cyclic-data cyclic-task-descriptions
643 line task-info task-data
644 date-absolute date time text)
645 ;; After scanning pages and [conditionally] cyclic entries, each
646 ;; element of appts has:
648 ;; (<absolute date>
649 ;; <time in appt format [minutes from midnight]>
650 ;; <date in planner format>
651 ;; description text)
653 ;; The first two elements are used for sorting/merging; they are
654 ;; removed from the returned list.
655 (when (and (featurep 'planner-cyclic)
656 planner-appt-forthcoming-look-at-cyclic-flag)
657 ;; Returns (<appts> . <list of planner-cyclic-ly formatted tasks>)
658 (setq cyclic-data (planner-appt-forthcoming-get-cyclic n))
659 (setq appts (car cyclic-data)
660 cyclic-task-descriptions (cdr cyclic-data)))
661 (with-temp-buffer
662 (with-planner
663 (dolist (page pages)
664 (when (file-exists-p (cdr page))
665 (setq date (car page))
666 (setq date-absolute (calendar-absolute-from-gregorian
667 (planner-filename-to-calendar-date
668 date)))
669 (insert-file-contents (cdr page))
670 (goto-char (point-min))
671 (while (re-search-forward planner-appt-forthcoming-regexp nil t)
672 (setq line (match-string 0))
673 (if (string-match planner-appt-schedule-appt-regexp line)
674 (unless (planner-appt-task-schedule-item-p line)
675 (setq time (save-match-data
676 (appt-convert-time (match-string 1 line)))
677 text (match-string 0 line)))
678 (setq task-info (planner-current-task-info))
679 (setq task-data (planner-appt-forthcoming-task-data task-info))
680 (when (and task-data
681 ;; Check for a cyclic task already added.
682 ;; This is a bit messy, since a task id
683 ;; won't have been added [and there might
684 ;; be other special case that I haven't
685 ;; anticipated].
686 (not (member
687 (if (string-match
688 "\\s-+{{Tasks:[0-9]+}}\\s-*"
689 (planner-task-description task-info))
690 (replace-match
691 "" nil t
692 (planner-task-description task-info))
693 (planner-task-description task-info))
694 cyclic-task-descriptions)))
695 (setq time (car task-data)
696 text (cdr task-data))))
697 (when (and time text)
698 ;; Add if it is not there already [there may be a
699 ;; duplicate if this is a schedule item derived from a
700 ;; task item]
701 (add-to-list 'appts (list date-absolute time date text))
702 (setq time nil text nil)))
703 (erase-buffer)))))
704 (when appts
705 (mapcar #'cddr
706 (sort appts
707 #'(lambda (a b)
708 (or (< (car a) (car b))
709 (and (= (car a) (car b))
710 (< (cadr a) (cadr b))))))))))
713 (defun planner-appt-forthcoming-get-cyclic (n)
714 (let ((appts '())
715 (cyclic-task-descriptions '())
716 date line time text task-info task-data)
717 (dolist (entry (planner-list-diary-entries
718 planner-cyclic-diary-file
719 (planner-filename-to-calendar-date
720 (planner-calculate-date-from-day-offset
721 (planner-today) 1))
722 (1- n)))
723 (setq date (planner-date-to-filename (car entry))
724 line (cadr entry))
725 (if (string-match planner-appt-schedule-appt-regexp line)
726 (setq time (save-match-data
727 (appt-convert-time (match-string 1 line)))
728 text (match-string 0 line))
729 (when (string-match planner-appt-forthcoming-task-regexp line)
730 (setq task-info (planner-task-info-from-string date line))
731 (setq task-data (planner-appt-forthcoming-task-data task-info))
732 (when task-data
733 ;; For duplicate checking: remember the description as
734 ;; it would be transformed by planner-cyclic.
735 (push (format planner-cyclic-task-description-format
736 (planner-task-description task-info) date)
737 cyclic-task-descriptions)
738 (setq time (car task-data)
739 text (cdr task-data)))))
740 (when (and time text)
741 (add-to-list
742 'appts
743 (list (calendar-absolute-from-gregorian (car entry))
744 time date text))
745 (setq time nil text nil)))
746 (cons appts cyclic-task-descriptions)))
748 ;; Format the data into a big string to make it easy either to put
749 ;; into a display buffer or into the day page.
750 (defun planner-appt-forthcoming-format (appt-data)
751 (let* ((last-date "")
752 (empty-cell-p (string-match
753 (format "\\`[%s]+\\'" muse-regexp-blank)
754 planner-appt-forthcoming-repeat-date-string))
755 (calendar-abbrev-length
756 ;; compatibility fix for Gnu Emacs < 22 and XEmacs
757 (if (boundp 'calendar-abbrev-length)
758 calendar-abbrev-length
760 (day-padding (make-string (1+ calendar-abbrev-length) ?\ )))
761 (mapconcat #'(lambda (a)
762 (prog1
763 (concat
764 (if (string= (car a) last-date)
765 (concat
766 (when planner-appt-forthcoming-show-day-names-flag
767 day-padding)
768 (if empty-cell-p
769 planner-appt-forthcoming-repeat-date-string
770 (planner-make-link
771 (car a)
772 planner-appt-forthcoming-repeat-date-string)))
773 (concat
774 (when planner-appt-forthcoming-show-day-names-flag
775 (concat
776 (calendar-day-name
777 (planner-filename-to-calendar-date (car a))
778 ;; compatibility:
779 calendar-abbrev-length)
780 " "))
781 (planner-make-link (car a))))
782 " | "
783 ;; Remove @s from times to avoid spurious
784 ;; highlighting.
785 (muse-replace-regexp-in-string
786 "@\\([ \t]*[0-9]\\)"
787 " \\1"
788 (cadr a)))
789 (setq last-date (car a))))
790 appt-data "\n")))
793 (defvar planner-appt-forthcoming-display-buffer
794 "*Forthcoming Appointments*"
795 "Buffer to display forthcoming appointments.")
797 (defun planner-appt-forthcoming-display (&optional days)
798 (interactive
799 ;; TODO: I wanted to use (interactive "p"), but that defaults to
800 ;; 1. Is this really the best way of getting nil as the default
801 ;; for a command that takes an optional integer prefix?:
802 (list (cond ((consp current-prefix-arg)
803 (car current-prefix-arg))
804 ((integerp current-prefix-arg)
805 current-prefix-arg)
806 (t nil))))
807 (unless days (setq days planner-appt-forthcoming-days))
808 (with-current-buffer
809 (get-buffer-create planner-appt-forthcoming-display-buffer)
810 (unless (planner-derived-mode-p 'planner-mode)
811 (setq muse-current-project (muse-project planner-project))
812 (planner-mode)
813 (cd (planner-directory)))
814 (delete-region (point-min) (point-max))
815 (insert "* Appointments in the next "
816 (number-to-string days)
817 (if (= days 1) " day" " days")
818 "\n\n"
819 (planner-appt-forthcoming-format
820 (planner-appt-forthcoming-get-appts
821 (or days planner-appt-forthcoming-days) t)))
822 (goto-char (point-min)))
823 (display-buffer planner-appt-forthcoming-display-buffer)
824 (planner-fit-window-to-buffer
825 (get-buffer-window planner-appt-forthcoming-display-buffer)))
828 (defun planner-appt-forthcoming-update-section (&optional days)
829 (interactive
830 (list (cond ((consp current-prefix-arg)
831 (car current-prefix-arg))
832 ((integerp current-prefix-arg)
833 current-prefix-arg)
834 (t nil))))
835 (with-planner-update-setup
836 (save-excursion
837 (planner-goto-today)
838 (planner-seek-to-first planner-appt-forthcoming-appt-section)
839 (delete-region (point)
840 (planner-appt-seek-to-end-of-current-section))
841 (insert (planner-appt-forthcoming-format
842 (planner-appt-forthcoming-get-appts
843 (or days planner-appt-forthcoming-days)))
844 ?\n))))
847 ;; A function suitable for planner-mode-hook
848 (defun planner-appt-forthcoming-update-section-maybe ()
849 (when (planner-appt-todays-page-p)
850 (planner-appt-forthcoming-update-section)))
853 ;;; Alerts From Task Descriptions
855 ;; Add a bit of [premature] optimization, mainly for updating
856 ;; functions where the same task gets examined often.
857 (defvar --planner-appt-task-cache (make-hash-table :test 'equal))
859 (defun planner-appt-task-parse-task (description)
860 "Extract appointment time and text from the DESCRIPTION.
861 Return a list (text time). If the task is not an
862 appointment, time defaults to nil."
863 (or (gethash description --planner-appt-task-cache)
864 (puthash
865 description
866 (if (string-match planner-appt-task-regexp description)
867 (list (substring description (match-end 0))
868 (substring description (match-beginning 1) (match-end 1)))
869 (list description nil))
870 --planner-appt-task-cache)))
872 (defun planner-appt-task-nagging-p (description)
873 "Return non-nil if task DESCRIPTION is a nagging appointment."
874 (string-match planner-appt-task-nagging-regexp description))
876 (defun planner-appt-task-member (description time appt-list)
877 "Return non-nil if DESCRIPTION at TIME in APPT-LIST has been scheduled."
878 (member (planner-appt-make-appt-element time description) appt-list))
881 ;; Integration with planner-schedule.el
883 (defvar planner-appt-schedule-task-estimate-regexp
884 (concat "[!@][ \t]*\\(?:" ; "shy" form of planner-appt-task-regexp
885 planner-appt-time-regexp
886 "\\)[ \t]*"
887 "\\s-*\\([0-9]+[smhdw]\\)")
888 "Regular expression matching a task time estimate.")
890 ;; NB: The following advice could be avoided if the regexp were not
891 ;; hard-coded into the original function.
893 ;; NNB: This is not well tested!
895 (defadvice planner-schedule-task-estimate (around planner-appt-task disable)
896 "Modify the regexp matched to take appointments into account."
897 (when (string-match planner-appt-schedule-task-estimate-regexp
898 (planner-task-description info))
899 (schedule-duration-to-seconds
900 (match-string 2 (planner-task-description info)))))
903 (defun planner-appt-task-add (&optional info)
904 "Create an appointment from the current task if this is today's plan.
905 Return t if an appointment was added.
906 If the task is an appointment, it is not cancelled, it is scheduled for
907 later today, and is not already added.
908 Optional argument: use INFO instead of the current task info."
909 (interactive)
910 (let* ((info (or info
911 ;; Unfortunately, in emacs-lisp there are no
912 ;; defaults for optional arguments, so one can't
913 ;; distinguish between null info in an arg and null
914 ;; info from planner-current-task-info; so the
915 ;; error message might be uninformative here.
916 (planner-current-task-info)
917 (error "There is no task on the current line")))
918 (appt (planner-appt-task-parse-task
919 (planner-task-description info)))
920 (time (nth 1 appt))
921 (desc (and time (planner-appt-format-description (nth 0 appt)))))
922 (when (and time
923 (not (string= (planner-task-status info) "C"))
924 (string= (planner-task-date info)
925 (planner-date-to-filename
926 (decode-time (current-time))))
927 (not (planner-appt-task-member desc time
928 appt-time-msg-list)))
929 (if (planner-appt-earlier-than-now-p time)
930 (progn
931 ;; Remember earlier appts separately [principally for
932 ;; their addition in an appointment section].
933 (unless (planner-appt-task-member
934 desc time --planner-appt-tasks-earlier-appts)
935 (setq --planner-appt-tasks-earlier-appts
936 (planner-appt-remember-appt
937 time desc
938 --planner-appt-tasks-earlier-appts)))
939 (planner-appt-update-appt-section-maybe)
940 ;; Make sure nil is returned.
941 nil)
942 (appt-add time desc)
943 ;; Keep track of tasks added by this function.
944 (setq --planner-appt-tasks-added-appts
945 (planner-appt-remember-appt
946 time desc
947 --planner-appt-tasks-added-appts))
948 (planner-appt-update-appt-section-maybe)
949 t))))
951 (defun planner-appt-task-delete (&optional info)
952 "Delete the appointment from the current task if this is today's plan.
953 Do not remove the time string.
954 Return any deleted appointments.
955 Optional argument: use INFO instead of the current task info."
956 (interactive)
957 (let* ((info (or info
958 ;; See planner-appt-task-add for comments about
959 ;; the possibly uninformative error message.
960 (planner-current-task-info)
961 (error "There is no task on the current line")))
962 (appt (planner-appt-task-parse-task
963 (planner-task-description info)))
964 (time (nth 1 appt))
965 (desc (and time (planner-appt-format-description (nth 0 appt))))
966 (tmp-msg-list appt-time-msg-list))
967 (when time
968 ;; Method from appt-delete
969 (let ((deleted-appts '())
970 (earlier-appt
971 (car (planner-appt-task-member
972 desc time --planner-appt-tasks-earlier-appts)))
973 element)
974 ;; NB: Mustn't concat time onto description until earlier-appt
975 ;; has been determined [since planner-appt-task-member does
976 ;; the concat itself [this could be improved somehow]]
977 (setq desc (concat time " " desc))
978 (while tmp-msg-list
979 (setq element (car tmp-msg-list))
980 (when (string= (car (cdr element)) desc)
981 (push element deleted-appts)
982 (setq appt-time-msg-list (delq element appt-time-msg-list))
983 (setq --planner-appt-tasks-added-appts
984 (planner-appt-forget-appt
985 element --planner-appt-tasks-added-appts)))
986 (setq tmp-msg-list (cdr tmp-msg-list)))
987 (when (or deleted-appts earlier-appt)
988 ;; Forget a deleted appt that was earlier than now.
989 (when earlier-appt
990 (setq --planner-appt-tasks-earlier-appts
991 (planner-appt-forget-appt
992 earlier-appt --planner-appt-tasks-earlier-appts)))
993 (planner-appt-update-appt-section-maybe))
994 deleted-appts))))
996 (defun planner-appt-add-appts-from-tasks ()
997 "Parse all of today's tasks and add appointments automatically."
998 (interactive)
999 (when (planner-appt-todays-page-p)
1000 ;; Clear old appts added by this function.
1001 (planner-appt-clear-appts --planner-appt-tasks-added-appts)
1002 (setq --planner-appt-tasks-added-appts '()
1003 --planner-appt-tasks-earlier-appts '())
1004 (let ((case-fold-search nil))
1005 (save-excursion
1006 (goto-char (point-min))
1007 (with-planner-appt-update-section-disabled
1008 (while (re-search-forward
1009 (concat planner-live-task-regexp
1010 planner-appt-task-regexp)
1011 nil t)
1012 (planner-appt-task-add))))
1013 (when (or --planner-appt-tasks-added-appts
1014 --planner-appt-tasks-earlier-appts)
1015 (planner-appt-update-appt-section-maybe)))))
1017 (planner-appt-defadvice planner-task-cancelled
1018 (before planner-appt-task)
1019 "Delete the appointment as well."
1020 (planner-appt-debug-message
1021 "*** called advice on planner-task-cancelled")
1022 (planner-appt-task-delete))
1024 (planner-appt-defadvice planner-task-done
1025 (before planner-appt-task)
1026 "Delete the appointment as well."
1027 (planner-appt-debug-message
1028 "*** called advice on planner-task-done")
1029 (planner-appt-task-delete))
1031 (planner-appt-defadvice planner-delete-task
1032 (before planner-appt-task)
1033 "Delete the appointment as well."
1034 (planner-appt-debug-message
1035 "*** called advice on planner-delete-task")
1036 (planner-appt-task-delete))
1038 ;; The advice for planner-update-task is quite tricky. A task will
1039 ;; need updating [for appointments] if the task is dated today and the
1040 ;; description differs from other task lines linked to by the current
1041 ;; task. If this is true, we have to examine all the other links,
1042 ;; delete any appointments, and then add the task after planner-update
1043 ;; has been called. Note that it is only possible for this to happen
1044 ;; if planner-id is loaded since otherwise the "same" task line can't
1045 ;; have different descriptions.
1047 (defun planner-appt-get-diff-links (info)
1048 "Given INFO, return a list of tasks linked to it whose info differs."
1049 (let ((diffs '())
1050 (linked-info))
1051 ;; Todo: with-planner-update-setup really ought to return the
1052 ;; value of the body.
1053 (with-planner-update-setup
1054 ;; Preserve point as well.
1055 (save-excursion
1056 (dolist (link
1057 (if (featurep 'planner-multi)
1058 (planner-multi-link-delete
1059 (planner-task-page info)
1060 (planner-multi-task-link-as-list info))
1061 (list (planner-task-link info))))
1062 (when (and (planner-find-file (planner-link-base link))
1063 (planner-find-task info)
1064 (setq linked-info (planner-current-task-info))
1065 (not (planner-tasks-equal-p info linked-info)))
1066 (push linked-info diffs)))))
1067 diffs))
1069 (planner-appt-defadvice planner-update-task
1070 (around planner-appt-task)
1071 "Update the appointment as well."
1072 (planner-appt-debug-message
1073 "*** called advice on planner-update-task")
1074 (let* ((info (planner-current-task-info))
1075 (diff-linked
1076 (and (featurep 'planner-id)
1077 (string= (planner-task-date info) (planner-today))
1078 (planner-appt-get-diff-links info))))
1079 (with-planner-appt-task-advice-disabled ad-do-it)
1080 (when diff-linked
1081 (dolist (i diff-linked)
1082 (planner-appt-task-delete i))
1083 (planner-appt-task-add))))
1085 ;; For planner-id-update-tasks-on-page, it is actually much faster to
1086 ;; update today's page after it has done its work rather than using
1087 ;; the update advice above.
1089 (planner-appt-defadvice planner-id-update-tasks-on-page
1090 (around planner-appt-task)
1091 "Update today's appointments as well."
1092 (planner-appt-debug-message
1093 "*** called advice on planner-id-update-tasks-on-page")
1094 (with-planner-appt-task-advice-disabled ad-do-it)
1095 (with-planner-update-setup
1096 (save-excursion
1097 (planner-goto-today)
1098 ;; Update the appointments section afterwards for efficiency.
1099 (with-planner-appt-update-section-disabled
1100 (planner-appt-update))
1101 (planner-appt-update-appt-section-maybe))))
1103 (defvar --planner-appt-planning nil
1104 "Internal flag:
1105 Lets planner-appt advice know that it has been called within a call to
1106 `plan'.")
1108 (planner-appt-defadvice plan (around planner-appt-task)
1109 "Note that plan is in progress."
1110 (planner-appt-debug-message
1111 "*** called advice on plan")
1112 (let ((--planner-appt-planning t))
1113 ad-do-it))
1115 (planner-appt-defadvice planner-copy-or-move-task
1116 (around planner-appt-task)
1117 "Update the appointment as well."
1118 (planner-appt-debug-message
1119 "*** called advice on planner-copy-or-move-task; "
1120 (if --planner-appt-planning
1121 "in plan"
1122 "not in plan"))
1123 (cond ((not --planner-appt-planning)
1124 (let
1125 ;; Save the appt information for error cleanup and
1126 ;; appointment adding.
1127 ((deleted-appts (planner-appt-task-delete))
1128 (old-info (planner-current-task-info)))
1129 (condition-case err
1130 (progn
1131 (with-planner-appt-task-advice-disabled ad-do-it)
1132 ;; If the task was moved to today, add it.
1133 (when (and date ; Bound in the advised function.
1134 (string= date (planner-today)))
1135 ;; Fiddle the old info so it looks like
1136 ;; today's. NB: would need changing should
1137 ;; the task-info format ever change.
1139 ;; planner-multi uses the date in the link for
1140 ;; planner-task-date [why?], so that has to be
1141 ;; modified too. This has to be done before the
1142 ;; date element is changed, of course.
1143 (when (and (featurep 'planner-multi)
1144 (consp (nth 5 old-info))) ; the link element
1145 (setcar (or
1146 (member (nth 8 old-info)
1147 (nth 5 old-info))
1148 ;; Silly way of avoiding an error if the
1149 ;; date is not in the list: can the date
1150 ;; not be in the list?
1151 '(nil))
1152 date))
1153 ;; Set the date element.
1154 (setcar (nthcdr 8 old-info) date)
1155 (planner-appt-task-add old-info)))
1156 ('error
1157 ;; Catch errors in planner-copy-or-move-task: restore
1158 ;; deleted tasks.
1159 (dolist (d deleted-appts)
1160 (push d appt-time-msg-list))
1161 (error (error-message-string err))))))
1163 ;; `plan' in progress: only move the task if it is not a
1164 ;; regular (non-nagging) appointment. If it's a nagging
1165 ;; appointment, remove the appointment and then move the
1166 ;; task.
1167 (let* ((info (planner-current-task-info))
1168 (appt (planner-appt-task-parse-task
1169 (planner-task-description info)))
1170 (time (nth 1 appt)))
1171 (with-planner-appt-task-advice-disabled
1172 (if (planner-appt-task-nagging-p (planner-task-description info))
1173 (progn (planner-edit-task-description (nth 0 appt))
1174 ad-do-it)
1175 (if (and info time)
1176 ;; Fri Nov 25 13:59:02 GMT 2005: Fix for new [?]
1177 ;; behaviour of planner-copy-or-move-region.
1178 (progn
1179 (forward-line -1)
1180 ;; Return nil, otherwise
1181 ;; planner-copy-or-move-region's count will be
1182 ;; wrong.
1183 (setq ad-return-value nil))
1184 ad-do-it)))))))
1186 ;; NB: this advice leads to two updates of the task appointment
1187 ;; section [when updating it is enabled, of course]: it is hard to see
1188 ;; how to avoid this unless there is yet another global variable
1189 ;; tracking deleted appts.
1190 (planner-appt-defadvice planner-edit-task-description
1191 (around planner-appt-task)
1192 "Update the appointment as well."
1193 (planner-appt-debug-message
1194 "*** called advice on planner-edit-task-description")
1195 (planner-appt-task-delete)
1196 (with-planner-appt-task-advice-disabled ad-do-it)
1197 (planner-appt-task-add))
1199 ;; planner-create-task-from-info: The appointment adding needs doing
1200 ;; after all hooks to planner-create-task-from-info have been run so
1201 ;; that planner-appt-task-add has the correct task line; planner-id,
1202 ;; for example, adds a task id so if the planner-appt hook is run
1203 ;; first it won't have the right task description.
1205 ;; Hence these shenanigans:
1207 (defvar --planner-appt-created-task-marker (make-marker))
1208 (defvar --planner-appt-close-the-buffer-flag nil)
1210 (eval-when-compile
1211 ;; Hide warning about live-buffers [q.v.].
1212 (defvar live-buffers))
1214 (defun planner-appt-create-task-hook-func ()
1215 "One half of planner-appt create task handling.
1216 Remembers the position of the added task. The other half of the
1217 handling is in advice to `planner-create-task-from-info'."
1218 (set-marker --planner-appt-created-task-marker (point))
1219 ;; If planner-tasks-file-behavior is 'close, it won't be possible to
1220 ;; recover the position from a marker, so temporarily defeat closing
1221 ;; this file, and close it if necessary in the
1222 ;; planner-create-task-from-info advice.
1223 (setq --planner-appt-close-the-buffer-flag
1224 (if (and (eq planner-tasks-file-behavior 'close)
1225 ;; `live-buffers' is defined in
1226 ;; planner-create-task-from-info, but it is not
1227 ;; defined in the planner-multi advice [hmm...].
1228 (boundp 'live-buffers)
1229 (not (memq (current-buffer) live-buffers)))
1230 ;; Adding the buffer to `live-buffers' means it won't be
1231 ;; closed automatically.
1232 (progn (push (current-buffer) live-buffers) t)
1233 nil)))
1236 (planner-appt-defadvice planner-create-task-from-info
1237 (around planner-appt-task)
1238 "Add an appointment alert for the new task if necessary."
1239 (planner-appt-debug-message
1240 "*** called advice on planner-create-task-from-info")
1241 (with-planner-appt-task-advice-disabled ad-do-it)
1242 (let ((buf (marker-buffer --planner-appt-created-task-marker))
1243 (pos (marker-position --planner-appt-created-task-marker)))
1244 (when buf
1245 (with-current-buffer buf
1246 (save-excursion
1247 (goto-char pos)
1248 (planner-appt-task-add)))
1249 (set-marker --planner-appt-created-task-marker nil)
1250 (when --planner-appt-close-the-buffer-flag
1251 ;; Use planner-save-buffers for consistency; remove the buffer
1252 ;; from buffer-list so that it gets closed.
1253 (planner-save-buffers (delq buf (buffer-list)))))))
1255 (defun planner-appt-task-insinuate ()
1256 "Do task-specific insinuation into `planner-mode'."
1257 ;; Nothing at the moment!
1260 ;; In case of breakage while developing
1261 ;; NB: remember to remove hooks locally where relevant
1262 (defun planner-appt-task-de-insinuate ()
1263 "Remove task-specific hooks."
1264 ;; Nothing at the moment!
1268 ;;; Alerts From The Schedule
1270 (defvar --planner-appt-schedule-added-appts '()
1271 "Internal variable: Tracks added schedule-based appointment alerts.")
1273 (defun planner-appt-add-appts-from-schedule ()
1274 "Add appointment reminders from the schedule if this is today's plan."
1275 (interactive)
1276 (when (planner-appt-todays-page-p)
1277 ;; Delete old appts created by this function.
1278 (planner-appt-clear-appts --planner-appt-schedule-added-appts)
1279 (setq --planner-appt-schedule-added-appts '())
1280 (save-excursion
1281 (planner-seek-to-first planner-appt-schedule-section)
1282 (let ((bound (save-excursion
1283 (planner-appt-seek-to-end-of-current-section)))
1284 line time text task-item-p)
1285 ;; There are no entries in the schedule unless bound is
1286 ;; greater than point.
1287 (when (> bound (point))
1288 (while (re-search-forward
1289 planner-appt-schedule-appt-regexp bound t)
1290 (setq line (planner-match-string-no-properties 0)
1291 time (planner-match-string-no-properties 1)
1292 text (save-match-data
1293 (planner-appt-format-description
1294 (planner-match-string-no-properties 3)))
1295 task-item-p
1296 (planner-appt-task-schedule-item-p (match-string 0)))
1297 (unless (or task-item-p
1298 (planner-appt-earlier-than-now-p time))
1299 (appt-add time text)
1300 ;; Remember tasks added here.
1301 (setq --planner-appt-schedule-added-appts
1302 (planner-appt-remember-appt
1303 time text --planner-appt-schedule-added-appts)))))))))
1305 (defun planner-appt-schedule-insinuate ()
1306 "Do schedule specific insinuation into `planner-mode'."
1307 ;; Nothing at the moment!
1310 (defun planner-appt-schedule-de-insinuate ()
1311 "Remove schedule-based hooks."
1312 ;; Nothing at the moment!
1315 ;; Make appt-make-list behave.
1317 (defadvice appt-make-list (after planner-appt activate)
1318 "Restore appointments added by planner-appt."
1319 (dolist (appt (append --planner-appt-tasks-added-appts
1320 --planner-appt-schedule-added-appts))
1321 (unless (member appt appt-time-msg-list)
1322 (push appt appt-time-msg-list)))
1323 (setq appt-time-msg-list (appt-sort-list appt-time-msg-list)))
1325 ;;; Sorting the Schedule
1327 (defun planner-appt-schedule-sort ()
1328 "Sort the schedule in the current page."
1329 (interactive)
1330 (save-excursion
1331 (save-restriction
1332 (planner-seek-to-first planner-appt-schedule-section)
1333 (narrow-to-region (point)
1334 (save-excursion
1335 (planner-appt-seek-to-end-of-current-section)
1336 (point)))
1337 (sort-subr nil 'forward-line 'end-of-line
1338 #'(lambda ()
1339 (goto-char (planner-line-beginning-position))
1340 (if (looking-at planner-appt-schedule-regexp)
1341 (appt-convert-time
1342 (match-string 2))
1343 ;; 1+ max number of minutes from midnight
1344 1441))
1345 nil))))
1347 ;;; Cyclical Schedule Entries
1349 (require 'diary-lib)
1351 (eval-when-compile
1352 (defvar planner-cyclic-diary-file))
1354 ;; Purloined from planner-cyclic.el.
1355 (defun planner-appt-schedule-get-cyclic-tasks (date &optional no-of-days)
1356 "For DATE, get the cyclic tasks.
1357 Optional argument get tasks for NO-OF-DAYS from DATE, the default is 1
1358 day [i.e., only for DATE]."
1359 (let ((date (if (stringp date)
1360 (planner-filename-to-calendar-date date)
1361 date)))
1362 (delq nil
1363 (mapcar #'(lambda (item)
1364 (when (string-match planner-appt-schedule-regexp
1365 (elt item 1))
1366 (match-string 0 (elt item 1))))
1367 (planner-list-diary-entries planner-cyclic-diary-file
1368 date no-of-days)))))
1370 (defun planner-appt-schedule-add-cyclic ()
1371 "Add cylical tasks to the schedule if the current buffer is a day page."
1372 (when (string-match planner-date-regexp (planner-page-name))
1373 (let ((entries
1374 (planner-appt-schedule-get-cyclic-tasks (planner-page-name))))
1375 (when entries
1376 (planner-seek-to-first planner-appt-schedule-section)
1377 (let ((start (point)))
1378 (dolist (entry entries)
1379 ;; Only insert if the entry is not already there.
1380 (unless (save-excursion
1381 (goto-char start)
1382 (search-forward entry nil t))
1383 (insert entry ?\n))))
1384 ;; Lazy way of putting them in the right place.
1385 (when planner-appt-sort-schedule-on-update-flag
1386 (planner-appt-schedule-sort))))))
1388 (defun planner-appt-schedule-add-cyclic-maybe ()
1389 "Add cylical tasks to the schedule.
1390 Behaviour depends upon `planner-appt-schedule-cyclic-behaviour'."
1391 (when (and (not (string< (planner-page-name) (planner-today)))
1392 (or (eq planner-appt-schedule-cyclic-behaviour 'future)
1393 (and
1394 (eq planner-appt-schedule-cyclic-behaviour 'today)
1395 (planner-appt-todays-page-p))))
1396 (planner-appt-schedule-add-cyclic)))
1398 (defun planner-appt-schedule-cyclic-insinuate ()
1399 "Insinuate the adding of cyclical schedule entries."
1400 ;; TODO: Add locally?
1401 (planner-appt-add-hook 'planner-mode-hook
1402 'planner-appt-schedule-add-cyclic-maybe nil t))
1404 (defun planner-appt-schedule-cyclic-de-insinuate ()
1405 "Remove cyclic schedule adding functionality."
1406 (remove-hook 'planner-mode-hook
1407 'planner-appt-schedule-add-cyclic-maybe))
1410 ;;; Common Functions
1413 ;; So that one doesn't have to use two separate commands when using
1414 ;; both methods:
1416 (defvar --planner-appt-updated nil)
1418 ;;;###autoload
1419 (defun planner-appt-update ()
1420 "Update the appointments on the current page."
1421 (interactive)
1422 ;; NB: Task adding has to be done before the schedule to avoid
1423 ;; duplicates if task appointments are copied to the schedule.
1424 (when (memq 'tasks planner-appt-methods)
1425 (planner-appt-add-appts-from-tasks))
1426 (when (memq 'schedule planner-appt-methods)
1427 (planner-appt-add-appts-from-schedule))
1428 (when (and planner-appt-sort-schedule-on-update-flag
1429 (planner-appt-todays-page-p))
1430 (planner-appt-schedule-sort))
1431 (run-hooks 'planner-appt-update-hook)
1432 ;; TODO: Use a belt and some braces: see comments in
1433 ;; `planner-appt-insinuate-if-today'.
1434 (setq --planner-appt-updated t))
1436 (defun planner-appt-update-for-write ()
1437 (when planner-appt-update-appts-on-save-flag
1438 (planner-appt-update)
1439 ;; Return nil for local-write-file-hooks.
1440 nil))
1442 ;; NB: Something like the above could be done for other hooks as
1443 ;; well...
1446 ;;; General Insinuation
1448 ;; This indirect method is used rather than some variable so that the
1449 ;; function advice can be avoided if it is not necessary [when using
1450 ;; schedule appointments exclusively].
1453 (defun planner-appt-use (source)
1454 "Use SOURCE to derive appointments from plan pages.
1455 Possible values for SOURCE are:
1456 'tasks [use today's task descriptions]
1457 'schedule [use today's schedule]
1458 '(tasks schedule) [use both tasks and schedule]."
1459 (dolist (s (if (listp source)
1460 source
1461 (list source)))
1462 (cond ((eq s 'tasks)
1463 ;; Add task-specific non-insinuating code here.
1464 ;; ;; Only activate advice when necessary.
1465 ;; (ad-enable-regexp "planner-appt-task")
1466 ;; (ad-activate-regexp "planner-appt-task")
1467 ;; An outstanding global hook [planner-appt-task-add tests
1468 ;; for today-ness anyway].
1469 (planner-appt-add-hook 'planner-create-task-hook
1470 'planner-appt-create-task-hook-func nil t)
1471 (add-to-list 'planner-appt-methods s))
1472 ((eq s 'schedule)
1473 ;; Add schedule task-specific non-insinuating code here.
1474 (add-to-list 'planner-appt-methods s))
1476 (error "Invalid appointment source %s" s))))
1477 ;; Add here any non method-specific code that should be executed
1478 ;; even if planner-appt-insinuate is not called.
1479 (if (memq 'tasks planner-appt-methods)
1480 (planner-appt-enable-all-advice)
1481 (planner-appt-disable-all-advice))
1482 (planner-appt-add-hook 'planner-mode-hook
1483 'planner-appt-font-setup nil t)
1484 ;; Might as well return something interesting.
1485 planner-appt-methods)
1487 ;;;###autoload
1488 (defun planner-appt-insinuate-if-today ()
1489 (when (planner-appt-todays-page-p)
1490 (planner-appt-add-hook 'planner-mode-hook
1491 'planner-appt-update t)
1492 ;; Add method specific things
1493 (when (memq 'tasks planner-appt-methods)
1494 (planner-appt-task-insinuate))
1495 (when (memq 'schedule planner-appt-methods)
1496 (planner-appt-schedule-insinuate))
1497 (planner-appt-add-hook planner-appt-write-file-hook
1498 'planner-appt-update-for-write t)
1499 ;; TODO: Under some conditions, as yet undetermined, an update is
1500 ;; not done when `plan' is called for the first time. So do an
1501 ;; update, but only if there hasn't been one already.
1503 ;; Fri 15 Apr 2005 16:26:04 BST: this has probably been sorted out
1504 ;; now, but it doesn't do any harm to leave this in for now, just
1505 ;; in case.
1506 (unless --planner-appt-updated
1507 (planner-appt-update))))
1509 ;;;###autoload
1510 (defun planner-appt-insinuate ()
1511 "Insinuate appointment alerting into planner mode.
1512 Appointment methods should be set up first using one of:
1513 `planner-appt-use-tasks'
1514 `planner-appt-use-schedule'
1515 `planner-appt-use-tasks-and-schedule'."
1516 (unless planner-appt-methods
1517 (error
1518 "No appointment source methods. Use one of:
1519 `planner-appt-use-tasks'
1520 `planner-appt-use-schedule'
1521 `planner-appt-use-tasks-and-schedule'
1522 before you call this function"))
1523 ;; Initialize the appt system according to emacs type and version.
1524 (cond ((fboundp 'appt-activate) ; Gnu Emacs >= 22
1525 (appt-activate 1))
1526 ((fboundp 'appt-initialize) ; Xemacs
1527 ;; appt-initialize needs a diary file, so create one if it
1528 ;; doesn't already exist
1529 (unless (file-exists-p diary-file)
1530 (with-temp-buffer
1531 (insert ?\n)
1532 (write-region (point-min) (point-max) diary-file)))
1533 (display-time)
1534 (appt-initialize))
1535 (t (display-time) ; Gnu Emacs < 22
1536 (add-hook 'diary-hook 'appt-make-list)))
1537 (planner-appt-add-hook 'planner-mode-hook
1538 'planner-appt-insinuate-if-today t t))
1540 ;; Remove hooks if there is something not working during testing.
1541 (defun planner-appt-de-insinuate ()
1542 "Remove all hooks associated with planner-appt.
1543 Use in an emergency if breakage in planner-appt interferes with your planning."
1544 (interactive)
1545 (remove-hook 'planner-mode-hook 'planner-appt-insinuate-if-today)
1546 (remove-hook 'mark-diary-entries-hook 'planner-appt-mark-calendar-maybe)
1547 (when (get-buffer (planner-today))
1548 (with-current-buffer (get-buffer (planner-today))
1549 ;; NB: Remember to remove locally where appropriate.
1550 (remove-hook 'planner-goto-hook
1551 'planner-appt-update t)
1552 (remove-hook 'planner-mode-hook
1553 'planner-appt-update t)
1554 (remove-hook planner-appt-write-file-hook
1555 'planner-appt-update-for-write t)
1556 (remove-hook 'planner-create-task-hook
1557 'planner-appt-create-task-hook-func)
1558 (planner-appt-task-de-insinuate)
1559 (planner-appt-schedule-de-insinuate)
1560 (planner-appt-schedule-cyclic-de-insinuate)))
1561 (planner-appt-disable-all-advice)
1562 (message "Planner-appt de-insinuated."))
1565 ;;; Convenient Functions For Users' Configuration.
1567 ;;;###autoload
1568 (defun planner-appt-use-tasks ()
1569 "Use tasks to derive appointment alerts."
1570 (planner-appt-use 'tasks))
1572 ;;;###autoload
1573 (defun planner-appt-use-schedule ()
1574 "Use the schedule to derive appointment alerts."
1575 (planner-appt-use 'schedule))
1577 ;;;###autoload
1578 (defun planner-appt-use-tasks-and-schedule ()
1579 "Use both tasks and the schedule to derive appointment alerts."
1580 (planner-appt-use '(tasks schedule)))
1582 ;;; Font Highlighting
1584 (defface planner-appt-face '((t (:foreground "green")))
1585 "Face for scheduled time."
1586 :group 'planner-appt)
1588 (defface planner-appt-overdue-face '((t (:foreground "red")))
1589 "Face for scheduled, but overdue time."
1590 :group 'planner-appt)
1592 (defun planner-appt-task-highlight-face (time)
1593 "Return appropriate face, depending on TIME.
1594 If TIME is earlier than now, return `planner-appt-face', else
1595 return `planner-appt-overdue-face'."
1596 (if (planner-appt-earlier-than-now-p time)
1597 'planner-appt-overdue-face
1598 'planner-appt-face))
1600 (defun planner-appt-task-highlight (beg end &optional verbose)
1601 "Highlight appointment times in tasks from BEG to END.
1602 VERBOSE is ignored."
1603 (when planner-appt-font-lock-appointments-flag
1604 (goto-char beg)
1605 (while (re-search-forward planner-appt-task-regexp end t)
1606 (when (or (not planner-appt-limit-highlighting-flag)
1607 (save-excursion
1608 (goto-char (planner-line-beginning-position))
1609 (save-match-data
1610 (or (and (looking-at planner-live-task-regexp)
1611 (string= (planner-task-date
1612 (planner-current-task-info))
1613 (planner-today)))
1614 (and
1615 (planner-appt-todays-page-p)
1616 (looking-at planner-appt-schedule-appt-regexp))))))
1617 (planner-highlight-region
1618 (match-beginning 1)
1619 (match-end 1)
1620 'planner-appt 60
1621 (list 'face
1622 (planner-appt-task-highlight-face
1623 (match-string 1))))))))
1625 (defun planner-appt-font-setup ()
1626 "Hook into `planner-mode'."
1627 (planner-appt-add-hook 'muse-colors-buffer-hook
1628 'planner-appt-task-highlight t))
1630 ;;; Calendar Marking
1632 ;; This is not strictly part of appointment handling, but if the
1633 ;; diary is to be by-passed for appointments, it makes sense to mark
1634 ;; the calendar using day pages.
1637 ;; Use a different face from the diary-entry-marker so we can
1638 ;; see where the different marks come from.
1639 (defface planner-appt-entry-marker
1640 '((t (:foreground "indianred")))
1641 "Face for planner day page mark in the calendar."
1642 :group 'planner-appt)
1645 (defun planner-appt-mark-calendar (&optional from to)
1646 "Mark dates in the calendar that have day pages.
1647 Optional args: mark dates from FROM to TO.
1648 FROM and to are lists: (month day year)."
1649 (with-current-buffer calendar-buffer ; Needed to get displayed date
1650 ; information.
1651 (let* ((displayed-month-last
1652 (1+ (cdr (assq 'displayed-month
1653 (buffer-local-variables)))))
1654 (displayed-year
1655 (cdr (assq 'displayed-year
1656 (buffer-local-variables))))
1657 (today-filename (planner-today))
1658 (today (planner-filename-to-calendar-date today-filename)))
1659 ;; Do nothing if the calendar is currently showing all months
1660 ;; earlier than today.
1661 (when (and (>= (elt today 2) ; year
1662 displayed-year)
1663 (>= displayed-month-last
1664 (elt today 0))) ; month
1665 (let ((diary-entry-marker 'planner-appt-entry-marker)
1666 (day-pages
1667 (planner-get-day-pages
1668 (if from
1669 (planner-date-to-filename from)
1670 today-filename)
1671 (if to
1672 (planner-date-to-filename to)
1673 ;; The default is last day visible
1674 ;; in the calendar.
1675 (planner-date-to-filename
1676 (list displayed-month-last 31 displayed-year))))))
1677 (dolist (day-page day-pages)
1678 (apply #'mark-calendar-date-pattern
1679 (planner-filename-to-calendar-date (car day-page)))))))))
1681 (defun planner-appt-calendar-insinuate ()
1682 (add-hook 'mark-diary-entries-hook 'planner-appt-mark-calendar))
1684 (provide 'planner-appt)
1685 ;;; planner-appt.el ends here
1687 ;; Local Variables:
1688 ;; indent-tabs-mode: t
1689 ;; tab-width: 8
1690 ;; End: