| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 | ;;; org-notify.el --- Notifications for Org-mode;; Copyright (C) 2012, 2013  Free Software Foundation, Inc.;; Author: Peter Münster <pmrb@free.fr>;; Keywords: notification, todo-list, alarm, reminder, pop-up;; This file is not part of GNU Emacs.;; 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-convert-deadline (orig)  "Convert original deadline from `org-element-parse-buffer' tosimple timestamp string."  (if orig      (replace-regexp-in-string "^<\\|>$" ""				(plist-get (plist-get orig 'timestamp)					   :raw-value))))(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 (org-notify-convert-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 byPERIOD."  (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 orforgotten 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 withoutthe `notify' property.  This file predefines such a defaultnotification type.Each element of PARAMS is a list with parameters for a given timedistance to the deadline.  This distance must increase from oneelement 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 predefinedones, 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 processingthe notifications of one org-agenda file, and if negative,notifications will be checked only when emacs is idle for -SECSseconds.  The default value for SECS is 20."  (interactive)  (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 anorg-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 textterminal 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
 |