Changes
[emacs.git] / .emacs.d / elisp / org / org-timer.el
index 8161699..0593573 100644 (file)
@@ -1,4 +1,4 @@
-;;; org-timer.el --- The relative timer code for Org-mode
+;;; org-timer.el --- Timer code for Org mode
 
 ;; Copyright (C) 2008-2014 Free Software Foundation, Inc.
 
 ;;
 ;;; Commentary:
 
-;; This file contains the relative timer code for Org-mode
+;; This file implements two types of timers for Org buffers:
+;;
+;; - A relative timer that counts up (from 0 or a specified offset)
+;; - A countdown timer that counts down from a specified time
+;;
+;; The relative and countdown timers differ in their entry points.
+;; Use `org-timer' or `org-timer-start' to start the relative timer,
+;; and `org-timer-set-timer' to start the countdown timer.
 
 ;;; Code:
 
 (require 'org)
+(require 'org-clock)
 
-(declare-function org-notify "org-clock" (notification &optional play-sound))
 (declare-function org-agenda-error "org-agenda" ())
 
 (defvar org-timer-start-time nil
 (defvar org-timer-pause-time nil
   "Time when the timer was paused.")
 
+(defvar org-timer-countdown-timer nil
+  "Current countdown timer.
+This is a timer object if there is an active countdown timer,
+'paused' if there is a paused countdown timer, and nil
+otherwise.")
+
+(defvar org-timer-countdown-timer-title nil
+  "Title for notification displayed when a countdown finishes.")
+
 (defconst org-timer-re "\\([-+]?[0-9]+\\):\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\)"
   "Regular expression used to match timer stamps.")
 
 (defcustom org-timer-format "%s "
   "The format to insert the time of the timer.
 This format must contain one instance of \"%s\" which will be replaced by
-the value of the relative timer."
+the value of the timer."
   :group 'org-time
   :type 'string)
 
@@ -76,13 +92,13 @@ nil          current timer is not displayed"
   "Hook run after relative timer is started.")
 
 (defvar org-timer-stop-hook nil
-  "Hook run before relative timer is stopped.")
+  "Hook run before relative or countdown timer is stopped.")
 
 (defvar org-timer-pause-hook nil
-  "Hook run before relative timer is paused.")
+  "Hook run before relative or countdown timer is paused.")
 
 (defvar org-timer-continue-hook nil
-  "Hook run after relative timer is continued.")
+  "Hook run after relative or countdown timer is continued.")
 
 (defvar org-timer-set-hook nil
   "Hook run after countdown timer is set.")
@@ -90,9 +106,6 @@ nil          current timer is not displayed"
 (defvar org-timer-done-hook nil
   "Hook run after countdown timer reaches zero.")
 
-(defvar org-timer-cancel-hook nil
-  "Hook run before countdown timer is canceled.")
-
 ;;;###autoload
 (defun org-timer-start (&optional offset)
   "Set the starting time for the relative timer to now.
@@ -105,8 +118,12 @@ region will be shifted by a specific amount.  You will be prompted for
 the amount, with the default to make the first timer string in
 the region 0:00:00."
   (interactive "P")
-  (if (equal offset '(16))
-      (call-interactively 'org-timer-change-times-in-region)
+  (cond
+   ((equal offset '(16))
+    (call-interactively 'org-timer-change-times-in-region))
+   (org-timer-countdown-timer
+    (user-error "Countdown timer is running.  Cancel first"))
+   (t
     (let (delta def s)
       (if (not offset)
          (setq org-timer-start-time (current-time))
@@ -123,47 +140,66 @@ the region 0:00:00."
          (setq delta (org-timer-hms-to-secs (org-timer-fix-incomplete s)))))
        (setq org-timer-start-time
              (seconds-to-time
-              (- (org-float-time) delta))))
+              ;; Pass `current-time' result to `org-float-time'
+              ;; (instead of calling without arguments) so that only
+              ;; `current-time' has to be overriden in tests.
+              (- (org-float-time (current-time)) delta))))
+      (setq org-timer-pause-time nil)
       (org-timer-set-mode-line 'on)
       (message "Timer start time set to %s, current value is %s"
               (format-time-string "%T" org-timer-start-time)
               (org-timer-secs-to-hms (or delta 0)))
-      (run-hooks 'org-timer-start-hook))))
+      (run-hooks 'org-timer-start-hook)))))
 
 (defun org-timer-pause-or-continue (&optional stop)
-  "Pause or continue the relative timer.
+  "Pause or continue the relative or countdown timer.
 With prefix arg STOP, stop it entirely."
   (interactive "P")
   (cond
    (stop (org-timer-stop))
    ((not org-timer-start-time) (error "No timer is running"))
    (org-timer-pause-time
-    ;; timer is paused, continue
-    (setq org-timer-start-time
-         (seconds-to-time
-          (-
-           (org-float-time)
-           (- (org-float-time org-timer-pause-time)
-              (org-float-time org-timer-start-time))))
-         org-timer-pause-time nil)
-    (org-timer-set-mode-line 'on)
-    (run-hooks 'org-timer-continue-hook)
-    (message "Timer continues at %s" (org-timer-value-string)))
+    (let ((start-secs (org-float-time org-timer-start-time))
+         (pause-secs (org-float-time org-timer-pause-time)))
+      (if org-timer-countdown-timer
+         (progn
+           (let ((new-secs (- start-secs pause-secs)))
+             (setq org-timer-countdown-timer
+                   (org-timer--run-countdown-timer
+                    new-secs org-timer-countdown-timer-title))
+             (setq org-timer-start-time
+                   (time-add (current-time) (seconds-to-time new-secs)))))
+       (setq org-timer-start-time
+             ;; Pass `current-time' result to `org-float-time'
+             ;; (instead of calling without arguments) so that only
+             ;; `current-time' has to be overriden in tests.
+             (seconds-to-time (- (org-float-time (current-time))
+                                 (- pause-secs start-secs)))))
+      (setq org-timer-pause-time nil)
+      (org-timer-set-mode-line 'on)
+      (run-hooks 'org-timer-continue-hook)
+      (message "Timer continues at %s" (org-timer-value-string))))
    (t
     ;; pause timer
+    (when org-timer-countdown-timer
+      (cancel-timer org-timer-countdown-timer)
+      (setq org-timer-countdown-timer 'pause))
     (run-hooks 'org-timer-pause-hook)
     (setq org-timer-pause-time (current-time))
     (org-timer-set-mode-line 'pause)
     (message "Timer paused at %s" (org-timer-value-string)))))
 
-(defvar org-timer-current-timer nil)
 (defun org-timer-stop ()
-  "Stop the relative timer."
+  "Stop the relative or countdown timer."
   (interactive)
+  (unless org-timer-start-time
+    (user-error "No timer running"))
+  (when (timerp org-timer-countdown-timer)
+    (cancel-timer org-timer-countdown-timer))
   (run-hooks 'org-timer-stop-hook)
   (setq org-timer-start-time nil
        org-timer-pause-time nil
-       org-timer-current-timer nil)
+       org-timer-countdown-timer nil)
   (org-timer-set-mode-line 'off)
   (message "Timer stopped"))
 
@@ -179,11 +215,13 @@ that was not started at the correct moment.
 If NO-INSERT-P is non-nil, return the string instead of inserting
 it in the buffer."
   (interactive "P")
-  (when (or (equal restart '(4)) (not org-timer-start-time))
-    (org-timer-start))
-  (if no-insert-p
-      (org-timer-value-string)
-    (insert (org-timer-value-string))))
+  (if (equal restart '(16))
+      (org-timer-start restart)
+    (when (or (equal restart '(4)) (not org-timer-start-time))
+      (org-timer-start))
+    (if no-insert-p
+       (org-timer-value-string)
+      (insert (org-timer-value-string)))))
 
 (defun org-timer-value-string ()
   "Set the timer string."
@@ -191,11 +229,10 @@ it in the buffer."
          (org-timer-secs-to-hms
           (abs (floor (org-timer-seconds))))))
 
-(defvar org-timer-timer-is-countdown nil)
 (defun org-timer-seconds ()
-  (if org-timer-timer-is-countdown
+  (if org-timer-countdown-timer
       (- (org-float-time org-timer-start-time)
-        (org-float-time (current-time)))
+        (org-float-time (or org-timer-pause-time (current-time))))
     (- (org-float-time (or org-timer-pause-time (current-time)))
        (org-float-time org-timer-start-time))))
 
@@ -290,7 +327,7 @@ If the integer is negative, the string will start with \"-\"."
 (defvar org-timer-mode-line-string nil)
 
 (defun org-timer-set-mode-line (value)
-  "Set the mode-line display of the relative timer.
+  "Set the mode-line display for relative or countdown timer.
 VALUE can be `on', `off', or `pause'."
   (when (or (eq org-timer-display 'mode-line)
            (eq org-timer-display 'both))
@@ -349,33 +386,20 @@ VALUE can be `on', `off', or `pause'."
          (concat " <" (substring (org-timer-value-string) 0 -1) ">"))
     (force-mode-line-update)))
 
-(defun org-timer-cancel-timer ()
-  "Cancel the current timer."
-  (interactive)
-  (when (eval org-timer-current-timer)
-    (run-hooks 'org-timer-cancel-hook)
-    (cancel-timer org-timer-current-timer)
-    (setq org-timer-current-timer nil)
-    (setq org-timer-timer-is-countdown nil)
-    (org-timer-set-mode-line 'off))
-  (message "Last timer canceled"))
-
 (defun org-timer-show-remaining-time ()
   "Display the remaining time before the timer ends."
   (interactive)
   (require 'time)
-  (if (not org-timer-current-timer)
+  (if (not org-timer-countdown-timer)
       (message "No timer set")
     (let* ((rtime (decode-time
-                  (time-subtract (timer--time org-timer-current-timer)
+                  (time-subtract (timer--time org-timer-countdown-timer)
                                  (current-time))))
           (rsecs (nth 0 rtime))
           (rmins (nth 1 rtime)))
       (message "%d minute(s) %d seconds left before next time out"
               rmins rsecs))))
 
-(defvar org-clock-sound)
-
 ;;;###autoload
 (defun org-timer-set-timer (&optional opt)
   "Prompt for a duration and set a timer.
@@ -392,9 +416,20 @@ without prompting the user for a duration.
 
 With two `C-u' prefix arguments, use `org-timer-default-timer'
 without prompting the user for a duration and automatically
-replace any running timer."
+replace any running timer.
+
+By default, the timer duration will be set to the number of
+minutes in the Effort property, if any.  You can ignore this by
+using three `C-u' prefix arguments."
   (interactive "P")
-  (let ((minutes (or (and (numberp opt) (number-to-string opt))
+  (when (and org-timer-start-time
+            (not org-timer-countdown-timer))
+    (user-error "Relative timer is running.  Stop first"))
+  (let* ((effort-minutes (ignore-errors (org-get-at-eol 'effort-minutes 1)))
+        (minutes (or (and (not (equal opt '(64)))
+                          effort-minutes
+                          (number-to-string effort-minutes))
+                    (and (numberp opt) (number-to-string opt))
                     (and (listp opt) (not (null opt))
                          (number-to-string org-timer-default-timer))
                     (read-from-minibuffer
@@ -405,47 +440,57 @@ replace any running timer."
        (org-timer-show-remaining-time)
       (let* ((mins (string-to-number (match-string 0 minutes)))
             (secs (* mins 60))
-            (hl (cond
-                 ((string-match "Org Agenda" (buffer-name))
-                  (let* ((marker (or (get-text-property (point) 'org-marker)
-                                     (org-agenda-error)))
-                         (hdmarker (or (get-text-property (point) 'org-hd-marker)
-                                       marker))
-                         (pos (marker-position marker)))
-                    (with-current-buffer (marker-buffer marker)
-                      (widen)
-                      (goto-char pos)
-                      (org-show-entry)
-                      (or (ignore-errors (org-get-heading))
-                          (concat "File:" (file-name-nondirectory (buffer-file-name)))))))
-                 ((derived-mode-p 'org-mode)
-                  (or (ignore-errors (org-get-heading))
-                      (concat "File:" (file-name-nondirectory (buffer-file-name)))))
-                 (t (error "Not in an Org buffer"))))
-            timer-set)
-       (if (or (and org-timer-current-timer
-                    (or (equal opt '(16))
-                        (y-or-n-p "Replace current timer? ")))
-               (not org-timer-current-timer))
+            (hl (org-timer--get-timer-title)))
+       (if (or (not org-timer-countdown-timer)
+               (equal opt '(16))
+               (y-or-n-p "Replace current timer? "))
            (progn
-             (require 'org-clock)
-             (when org-timer-current-timer
-               (cancel-timer org-timer-current-timer))
-             (setq org-timer-current-timer
-                   (run-with-timer
-                    secs nil `(lambda ()
-                                (setq org-timer-current-timer nil)
-                                (org-notify ,(format "%s: time out" hl) ,org-clock-sound)
-                                (setq org-timer-timer-is-countdown nil)
-                                (org-timer-set-mode-line 'off)
-                                (run-hooks 'org-timer-done-hook))))
+             (when (timerp org-timer-countdown-timer)
+               (cancel-timer org-timer-countdown-timer))
+             (setq org-timer-countdown-timer-title
+                   (org-timer--get-timer-title))
+             (setq org-timer-countdown-timer
+                   (org-timer--run-countdown-timer
+                    secs org-timer-countdown-timer-title))
              (run-hooks 'org-timer-set-hook)
-             (setq org-timer-timer-is-countdown t
-                   org-timer-start-time
-                   (time-add (current-time) (seconds-to-time (* mins 60))))
+             (setq org-timer-start-time
+                   (time-add (current-time) (seconds-to-time secs)))
+             (setq org-timer-pause-time nil)
              (org-timer-set-mode-line 'on))
          (message "No timer set"))))))
 
+(defun org-timer--run-countdown-timer (secs title)
+  "Start countdown timer that will last SECS.
+TITLE will be appended to the notification message displayed when
+time is up."
+  (let ((msg (format "%s: time out" title)))
+    (run-with-timer
+     secs nil `(lambda ()
+                (setq org-timer-countdown-timer nil
+                      org-timer-start-time nil)
+                (org-notify ,msg ,org-clock-sound)
+                (org-timer-set-mode-line 'off)
+                (run-hooks 'org-timer-done-hook)))))
+
+(defun org-timer--get-timer-title ()
+  "Construct timer title from heading or file name of Org buffer."
+  (cond
+   ((derived-mode-p 'org-agenda-mode)
+    (let* ((marker (or (get-text-property (point) 'org-marker)
+                      (org-agenda-error)))
+          (hdmarker (or (get-text-property (point) 'org-hd-marker)
+                        marker)))
+      (with-current-buffer (marker-buffer marker)
+       (org-with-wide-buffer
+        (goto-char hdmarker)
+        (org-show-entry)
+        (or (ignore-errors (org-get-heading))
+            (buffer-name (buffer-base-buffer)))))))
+   ((derived-mode-p 'org-mode)
+    (or (ignore-errors (org-get-heading))
+       (buffer-name (buffer-base-buffer))))
+   (t (error "Not in an Org buffer"))))
+
 (provide 'org-timer)
 
 ;; Local variables: