Explorar el Código

Add org-notify.el by Peter Münster to contrib/lisp/.

It's useful for getting notifications for todo items and more flexible
than org-agenda-to-appt.

Here a summary:
- different warning periods for different todo-types
- fine grained warning periods (smallest unit is second)
- continue notifications, when deadline is overdue
- easy modification of timestamps (just one click, to say
  "let's do it tomorrow")
- switch from "todo" to "done" by clicking on the notification window
- configurable notification types (email, notifications-notify, beep,
  etc.)
- configurable notification period
- configurable notification duration
- crescendo notifications (be more aggressive, when time gets closer to
  deadline)

There was a little thread about this subject:
http://thread.gmane.org/gmane.emacs.orgmode/48832

Example usage:
  (org-notify-add 'appt
                  '(:time "-1s" :period "20s" :duration 10
                    :actions (-message -ding))
                  '(:time "15m" :period "2m" :duration 100
                    :actions -notify/window)
                  '(:time "2h" :period "5m" :actions -message)
                  '(:time "1d" :actions -email))

This means for todo-items with `notify' property set to `appt': 1 day before
deadline, send a reminder-email, 2 hours before deadline, start to send
messages every 5 minutes, then, 15 minutes before deadline, start to pop up
notification windows every 2 minutes. The timeout of the window is set to
100 seconds. Finally, when deadline is overdue, send messages and make
noise.
Bastien Guerry hace 13 años
padre
commit
973695beb3
Se han modificado 1 ficheros con 377 adiciones y 0 borrados
  1. 377 0
      contrib/lisp/org-notify.el

+ 377 - 0
contrib/lisp/org-notify.el

@@ -0,0 +1,377 @@
+;;; org-notify.el --- Notifications for Org-mode
+
+;; Copyright (C) 2012  Free Software Foundation, Inc.
+
+;; Author: Peter Münster <pmrb@free.fr>
+;; Keywords: notification, todo-list, alarm, reminder, pop-up
+
+;; This program 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 of the License, or
+;; (at your option) any later version.
+
+;; This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Get notifications, when there is something to do.
+;; Sometimes, you need a reminder a few days before a deadline, e.g. to buy a
+;; present for a birthday, and then another notification one hour before to
+;; have enough time to choose the right clothes.
+;; For other events, e.g. rolling the dustbin to the roadside once per week,
+;; you probably need another kind of notification strategy.
+;; This package tries to satisfy the various needs.
+
+;; In order to activate this package, you must add the following code
+;; into your .emacs:
+;;
+;;   (require 'org-notify)
+;;   (org-notify-start)
+
+;; Example setup:
+;; (org-notify-add 'appt
+;;                 '(:time "-1s" :period "20s" :duration 10
+;;                   :actions (-message -ding))
+;;                 '(:time "15m" :period "2m" :duration 100
+;;                   :actions -notify)
+;;                 '(:time "2h" :period "5m" :actions -message)
+;;                 '(:time "3d" :actions -email))
+;; This means for todo-items with `notify' property set to `appt': 3 days
+;; before deadline, send a reminder-email, 2 hours before deadline, start to
+;; send messages every 5 minutes, then 15 minutes before deadline, start to
+;; pop up notification windows every 2 minutes. The timeout of the window is
+;; set to 100 seconds. Finally, when deadline is overdue, send messages and
+;; make noise."
+
+;; Take also a look at the function `org-notify-add'.
+
+;;; Code:
+
+(eval-when-compile (require 'cl))
+(require 'org-element)
+
+(declare-function appt-delete-window    "appt"          ())
+(declare-function notifications-notify  "notifications" (&rest prms))
+(declare-function article-lapsed-string "gnus-art"      (t &optional ms))
+
+(defgroup org-notify nil
+  "Options for Org-mode notifications."
+  :tag "Org Notify"
+  :group 'org)
+
+(defcustom org-notify-audible t
+  "Non-nil means beep to indicate notification."
+  :type 'boolean
+  :group 'org-notify)
+
+(defconst org-notify-actions
+  '("show" "show" "done" "done" "hour" "one hour later" "day" "one day later"
+    "week" "one week later")
+  "Possible actions for call-back functions.")
+
+(defconst org-notify-window-buffer-name "*org-notify-%s*"
+  "Buffer-name for the `org-notify-action-window' function.")
+
+(defvar org-notify-map nil
+  "Mapping between names and parameter lists.")
+
+(defvar org-notify-timer nil
+  "Timer of the notification daemon.")
+
+(defvar org-notify-parse-file nil
+  "Index of current file, that `org-element-parse-buffer' is parsing.")
+
+(defvar org-notify-on-action-map nil
+  "Mapping between on-action identifiers and parameter lists.")
+
+(defun org-notify-string->seconds (str)
+  "Convert time string STR to number of seconds."
+  (when str
+    (let* ((conv `(("s" . 1) ("m" . 60) ("h" . ,(* 60 60))
+                   ("d" . ,(* 24 60 60)) ("w" . ,(* 7 24 60 60))
+                   ("M" . ,(* 30 24 60 60))))
+           (letters (concat
+                     (mapcar (lambda (x) (string-to-char (car x))) conv)))
+           (case-fold-search nil))
+      (string-match (concat "\\(-?\\)\\([0-9]+\\)\\([" letters "]\\)") str)
+      (* (string-to-number (match-string 2 str))
+         (cdr (assoc (match-string 3 str) conv))
+         (if (= (length (match-string 1 str)) 1) -1 1)))))
+
+(defun org-notify-make-todo (heading &rest ignored)
+  "Create one todo item."
+  (macrolet ((get (k) `(plist-get list ,k))
+             (pr (k v) `(setq result (plist-put result ,k ,v))))
+    (let* ((list (nth 1 heading))      (notify (or (get :notify) "default"))
+           (deadline (get :deadline))  (heading (get :raw-value))
+           result)
+      (when (and (eq (get :todo-type) 'todo) heading deadline)
+        (pr :heading heading)     (pr :notify (intern notify))
+        (pr :begin (get :begin))
+        (pr :file (nth org-notify-parse-file (org-agenda-files 'unrestricted)))
+        (pr :timestamp deadline)  (pr :uid (md5 (concat heading deadline)))
+        (pr :deadline (- (org-time-string-to-seconds deadline)
+                         (org-float-time))))
+      result)))
+
+(defun org-notify-todo-list ()
+  "Create the todo-list for one org-agenda file."
+  (let* ((files (org-agenda-files 'unrestricted))
+         (max (1- (length files))))
+    (setq org-notify-parse-file
+          (if (or (not org-notify-parse-file) (>= org-notify-parse-file max))
+              0
+            (1+ org-notify-parse-file)))
+    (save-excursion
+      (with-current-buffer (find-file-noselect
+                            (nth org-notify-parse-file files))
+        (org-element-map (org-element-parse-buffer 'headline)
+                         'headline 'org-notify-make-todo)))))
+
+(defun org-notify-maybe-too-late (diff period heading)
+  "Print waring message, when notified significantly later than defined by
+PERIOD."
+  (if (> (/ diff period) 1.5)
+      (message "Warning: notification for \"%s\" behind schedule!" heading))
+  t)
+
+(defun org-notify-process ()
+  "Process the todo-list, and possibly notify user about upcoming or
+forgotten tasks."
+  (macrolet ((prm (k) `(plist-get prms ,k))  (td (k) `(plist-get todo ,k)))
+    (dolist (todo (org-notify-todo-list))
+      (let* ((deadline (td :deadline))  (heading (td :heading))
+             (uid (td :uid))            (last-run-sym
+                                         (intern (concat ":last-run-" uid))))
+        (dolist (prms (plist-get org-notify-map (td :notify)))
+          (when (< deadline (org-notify-string->seconds (prm :time)))
+            (let ((period (org-notify-string->seconds (prm :period)))
+                  (last-run (prm last-run-sym))  (now (org-float-time))
+                  (actions (prm :actions))       diff  plist)
+              (when (or (not last-run)
+                        (and period (< period (setq diff (- now last-run)))
+                             (org-notify-maybe-too-late diff period heading)))
+                (setq prms (plist-put prms last-run-sym now)
+                      plist (append todo prms))
+                (if (if (plist-member prms :audible)
+                        (prm :audible)
+                      org-notify-audible)
+                    (ding))
+                (unless (listp actions)
+                  (setq actions (list actions)))
+                (dolist (action actions)
+                  (funcall (if (fboundp action) action
+                             (intern (concat "org-notify-action"
+                                             (symbol-name action))))
+                           plist))))
+            (return)))))))
+
+(defun org-notify-add (name &rest params)
+  "Add a new notification type. The NAME can be used in Org-mode property
+`notify'. If NAME is `default', the notification type applies for todo items
+without the `notify' property. This file predefines such a default
+notification type.
+
+Each element of PARAMS is a list with parameters for a given time
+distance to the deadline. This distance must increase from one element to
+the next.
+List of possible parameters:
+  :time      Time distance to deadline, when this type of notification shall
+             start. It's a string: an integral value (positive or negative)
+             followed by a unit (s, m, h, d, w, M).
+  :actions   A function or a list of functions to be called to notify the
+             user. Instead of a function name, you can also supply a suffix
+             of one of the various predefined `org-notify-action-xxx'
+             functions.
+  :period    Optional: can be used to repeat the actions periodically. Same
+             format as :time.
+  :duration  Some actions use this parameter to specify the duration of the
+             notification. It's an integral number in seconds.
+  :audible   Overwrite the value of `org-notify-audible' for this action.
+
+For the actions, you can use your own functions or some of the predefined
+ones, whose names are prefixed with `org-notify-action-'."
+  (setq org-notify-map (plist-put org-notify-map name params)))
+
+(defun org-notify-start (&optional secs)
+  "Start the notification daemon. If SECS is positive, it's the
+period in seconds for processing the notifications of one
+org-agenda file, and if negative, notifications will be checked
+only when emacs is idle for -SECS seconds. The default value for
+SECS is 20."
+  (if org-notify-timer
+      (org-notify-stop))
+  (setq secs (or secs 20)
+        org-notify-timer (if (< secs 0)
+                             (run-with-idle-timer (* -1 secs) t
+                                                  'org-notify-process)
+                           (run-with-timer secs secs 'org-notify-process))))
+
+(defun org-notify-stop ()
+  "Stop the notification daemon."
+  (when org-notify-timer
+      (cancel-timer org-notify-timer)
+      (setq org-notify-timer nil)))
+
+(defun org-notify-on-action (plist key)
+  "User wants to see action."
+  (let ((file (plist-get plist :file))
+        (begin (plist-get plist :begin)))
+    (if (string-equal key "show")
+        (progn
+          (switch-to-buffer (find-file-noselect file))
+          (org-with-wide-buffer
+           (goto-char begin)
+           (show-entry))
+          (goto-char begin)
+          (search-forward "DEADLINE: <")
+          (if (display-graphic-p)
+              (x-focus-frame nil)))
+      (save-excursion
+        (with-current-buffer (find-file-noselect file)
+          (org-with-wide-buffer
+           (goto-char begin)
+           (search-forward "DEADLINE: <")
+           (cond
+            ((string-equal key "done")  (org-todo))
+            ((string-equal key "hour")  (org-timestamp-change 60 'minute))
+            ((string-equal key "day")   (org-timestamp-up-day))
+            ((string-equal key "week")  (org-timestamp-change 7 'day)))))))))
+
+(defun org-notify-on-action-notify (id key)
+  "User wants to see action after mouse-click in notify window."
+  (org-notify-on-action (plist-get org-notify-on-action-map id) key)
+  (org-notify-on-close id nil))
+
+(defun org-notify-on-action-button (button)
+  "User wants to see action after button activation."
+  (macrolet ((get (k) `(button-get button ,k)))
+    (org-notify-on-action (get 'plist) (get 'key))
+    (org-notify-delete-window (get 'buffer))
+    (cancel-timer (get 'timer))))
+
+(defun org-notify-delete-window (buffer)
+  "Delete the notification window."
+  (require 'appt)
+  (let ((appt-buffer-name buffer)
+        (appt-audible nil))
+    (appt-delete-window)))
+
+(defun org-notify-on-close (id reason)
+  "Notification window has been closed."
+  (setq org-notify-on-action-map (plist-put org-notify-on-action-map id nil)))
+
+(defun org-notify-action-message (plist)
+  "Print a message."
+  (message "TODO: \"%s\" at %s!" (plist-get plist :heading)
+           (plist-get plist :timestamp)))
+
+(defun org-notify-action-ding (plist)
+  "Make noise."
+  (let ((timer (run-with-timer 0 1 'ding)))
+    (run-with-timer (or (plist-get plist :duration) 3) nil
+                    'cancel-timer timer)))
+
+(defun org-notify-body-text (plist)
+  "Make human readable string for remaining time to deadline."
+  (require 'gnus-art)
+  (format "%s\n(%s)"
+          (replace-regexp-in-string
+           " in the future" ""
+           (article-lapsed-string
+            (time-add (current-time)
+                      (seconds-to-time (plist-get plist :deadline))) 2))
+          (plist-get plist :timestamp)))
+
+(defun org-notify-action-email (plist)
+  "Send email to user."
+  (compose-mail user-mail-address (concat "TODO: " (plist-get plist :heading)))
+  (insert (org-notify-body-text plist))
+  (funcall send-mail-function)
+  (flet ((yes-or-no-p (prompt) t))
+    (kill-buffer)))
+
+(defun org-notify-select-highest-window ()
+  "Select the highest window on the frame, that is not is not an
+org-notify window. Mostly copied from `appt-select-lowest-window'."
+  (let ((highest-window (selected-window))
+        (bottom-edge (nth 3 (window-edges)))
+        next-bottom-edge)
+    (walk-windows (lambda (w)
+                    (when (and
+                           (not (string-match "^\\*org-notify-.*\\*$"
+                                              (buffer-name
+                                               (window-buffer w))))
+                           (> bottom-edge (setq next-bottom-edge
+                                                (nth 3 (window-edges w)))))
+                      (setq bottom-edge next-bottom-edge
+                            highest-window w))) 'nomini)
+    (select-window highest-window)))
+
+(defun org-notify-action-window (plist)
+  "Pop up a window, mostly copied from `appt-disp-window'."
+  (save-excursion
+    (macrolet ((get (k) `(plist-get plist ,k)))
+      (let ((this-window (selected-window))
+            (buf (get-buffer-create
+                  (format org-notify-window-buffer-name (get :uid)))))
+        (when (minibufferp)
+          (other-window 1)
+          (and (minibufferp) (display-multi-frame-p) (other-frame 1)))
+        (if (cdr (assq 'unsplittable (frame-parameters)))
+            (progn (set-buffer buf) (display-buffer buf))
+          (unless (or (special-display-p (buffer-name buf))
+                      (same-window-p (buffer-name buf)))
+            (org-notify-select-highest-window)
+            (when (>= (window-height) (* 2 window-min-height))
+              (select-window (split-window nil nil 'above))))
+          (switch-to-buffer buf))
+        (setq buffer-read-only nil  buffer-undo-list t)
+        (erase-buffer)
+        (insert (format "TODO: %s, %s.\n" (get :heading)
+                        (org-notify-body-text plist)))
+        (let ((timer (run-with-timer (or (get :duration) 10) nil
+                                     'org-notify-delete-window buf)))
+          (dotimes (i (/ (length org-notify-actions) 2))
+            (let ((key (nth (* i 2) org-notify-actions))
+                  (text (nth (1+ (* i 2)) org-notify-actions)))
+              (insert-button text 'action 'org-notify-on-action-button
+                             'key key 'buffer buf 'plist plist 'timer timer)
+              (insert "    "))))
+        (shrink-window-if-larger-than-buffer (get-buffer-window buf t))
+        (set-buffer-modified-p nil)       (setq buffer-read-only t)
+        (raise-frame (selected-frame))    (select-window this-window)))))
+
+(defun org-notify-action-notify (plist)
+  "Pop up a notification window."
+  (require 'notifications)
+  (let* ((duration (plist-get plist :duration))
+         (id (notifications-notify
+              :title     (plist-get plist :heading)
+              :body      (org-notify-body-text plist)
+              :timeout   (if duration (* duration 1000))
+              :actions   org-notify-actions
+              :on-action 'org-notify-on-action-notify)))
+    (setq org-notify-on-action-map
+          (plist-put org-notify-on-action-map id plist))))
+
+(defun org-notify-action-notify/window (plist)
+  "For a graphics display, pop up a notification window, for a text
+terminal an emacs window."
+  (if (display-graphic-p)
+      (org-notify-action-notify plist)
+    (org-notify-action-window plist)))
+
+;;; Provide a minimal default setup.
+(org-notify-add 'default '(:time "1h" :actions -notify/window
+                           :period "2m" :duration 60))
+
+(provide 'org-notify)
+
+;;; org-notify.el ends here