Bladeren bron

org-timer.el: Merge API for the two timers

* lisp/org-timer.el (org-timer-stop): Support countdown timers in addition
to relative timers.

* lisp/org-timer.el (org-timer-cancel-timer): Remove function.

* lisp/org-timer.el (org-timer-pause-or-continue): Support countdown
timers in addition to relative timers.

* testing/lisp/test-org-timer.el: New file.

* doc/org.texi: Merge relative and countdown timer nodes.

Several previous issues are fixed with these changes.

- org-timer-set-timer and org-timer-cancel-timer did not reset
  org-timer-start-time after countdown completed.

- Because org-timer-start did not return org-timer-pause-time to nil,
  the modeline remained stuck at the paused time.

- When org-timer-start was called with a countdown timer, the modeline
  was updated for the new relative timer, but the countdown timer
  remained scheduled.

- When org-timer-pause-or-continue was called with a countdown timer
  running, the modeline was put in a paused state, but the countdown
  timer remained scheduled.

- When org-timer-stop was called with a countdown timer running, the
  timer was removed from the modeline, but the countdown timer remained
  scheduled.

- When org-timer-set-timer was called with a paused relative timer, the
  relative timer was not reset properly (org-timer-pause-time was still
  non-nil) and the modeline remained in the paused state of the relative
  timer, even though the countdown timer was scheduled with
  run-with-timer.

- Running org-timer-set-timer at the beginning of an empty buffer
  resulted in an args-out-of-range error (due to the org-get-at-eol
  call).
Kyle Meyer 11 jaren geleden
bovenliggende
commit
173b0cb6d6
4 gewijzigde bestanden met toevoegingen van 438 en 131 verwijderingen
  1. 39 45
      doc/org.texi
  2. 5 0
      etc/ORG-NEWS
  3. 121 86
      lisp/org-timer.el
  4. 273 0
      testing/lisp/test-org-timer.el

+ 39 - 45
doc/org.texi

@@ -462,8 +462,7 @@ Dates and times
 * Deadlines and scheduling::    Planning your work
 * Deadlines and scheduling::    Planning your work
 * Clocking work time::          Tracking how long you spend on a task
 * Clocking work time::          Tracking how long you spend on a task
 * Effort estimates::            Planning work effort in advance
 * Effort estimates::            Planning work effort in advance
-* Relative timer::              Notes with a running timer
-* Countdown timer::             Starting a countdown timer for a task
+* Timers::                      Notes with a running timer
 
 
 Creating timestamps
 Creating timestamps
 
 
@@ -5790,8 +5789,7 @@ is used in a much wider sense.
 * Deadlines and scheduling::    Planning your work
 * Deadlines and scheduling::    Planning your work
 * Clocking work time::          Tracking how long you spend on a task
 * Clocking work time::          Tracking how long you spend on a task
 * Effort estimates::            Planning work effort in advance
 * Effort estimates::            Planning work effort in advance
-* Relative timer::              Notes with a running timer
-* Countdown timer::             Starting a countdown timer for a task
+* Timers::                      Notes with a running timer
 @end menu
 @end menu
 
 
 
 
@@ -6793,60 +6791,56 @@ with the @kbd{/} key in the agenda (@pxref{Agenda commands}).  If you have
 these estimates defined consistently, two or three key presses will narrow
 these estimates defined consistently, two or three key presses will narrow
 down the list to stuff that fits into an available time slot.
 down the list to stuff that fits into an available time slot.
 
 
-@node Relative timer
-@section Taking notes with a relative timer
+@node Timers
+@section Taking notes with a timer
 @cindex relative timer
 @cindex relative timer
+@cindex countdown timer
+@kindex ;
+
+Org provides provides two types of timers.  There is a relative timer that
+counts up, which can be useful when taking notes during, for example, a
+meeting or a video viewing.  There is also a countdown timer.
+
+The relative and countdown are started with separate commands.
+
+@table @kbd
+@orgcmd{C-c C-x 0,org-timer-start}
+Start or reset the relative timer.  By default, the timer is set to 0.  When
+called with a @kbd{C-u} prefix, prompt the user for a starting offset.  If
+there is a timer string at point, this is taken as the default, providing a
+convenient way to restart taking notes after a break in the process.  When
+called with a double prefix argument @kbd{C-u C-u}, change all timer strings
+in the active region by a certain amount.  This can be used to fix timer
+strings if the timer was not started at exactly the right moment.
+@orgcmd{C-c C-x ;,org-timer-set-timer}
+Start a countdown timer.  The user is prompted for a duration.
+@code{org-timer-default-timer} sets the default countdown value.  Giving a
+prefix numeric argument overrides this default value.  This command is
+available as @kbd{;} in agenda buffers.
+@end table
 
 
-When taking notes during, for example, a meeting or a video viewing, it can
-be useful to have access to times relative to a starting time.  Org provides
-such a relative timer and make it easy to create timed notes.
+Once started, relative and countdown timers are controlled with the same
+commands.
 
 
 @table @kbd
 @table @kbd
 @orgcmd{C-c C-x .,org-timer}
 @orgcmd{C-c C-x .,org-timer}
-Insert a relative time into the buffer.  The first time you use this, the
-timer will be started.  When called with a prefix argument, the timer is
-restarted.
+Insert the value of the current relative or countdown timer into the buffer.
+If no timer is running, the relative timer will be started.  When called with
+a prefix argument, the relative timer is restarted.
 @orgcmd{C-c C-x -,org-timer-item}
 @orgcmd{C-c C-x -,org-timer-item}
-Insert a description list item with the current relative time.  With a prefix
-argument, first reset the timer to 0.
+Insert a description list item with the value of the current relative or
+countdown timer.  With a prefix argument, first reset the relative timer to
+0.
 @orgcmd{M-@key{RET},org-insert-heading}
 @orgcmd{M-@key{RET},org-insert-heading}
 Once the timer list is started, you can also use @kbd{M-@key{RET}} to insert
 Once the timer list is started, you can also use @kbd{M-@key{RET}} to insert
 new timer items.
 new timer items.
-@c for key sequences with a comma, command name macros fail :(
-@kindex C-c C-x ,
-@item C-c C-x ,
-Pause the timer, or continue it if it is already paused
-(@command{org-timer-pause-or-continue}).
-@c removed the sentence because it is redundant to the following item
-@kindex C-u C-c C-x ,
-@item C-u C-c C-x ,
+@orgcmd{C-c C-x \,,org-timer-pause-or-continue}
+Pause the timer, or continue it if it is already paused.
+@orgcmd{C-c C-x _,org-timer-stop}
 Stop the timer.  After this, you can only start a new timer, not continue the
 Stop the timer.  After this, you can only start a new timer, not continue the
 old one.  This command also removes the timer from the mode line.
 old one.  This command also removes the timer from the mode line.
-@orgcmd{C-c C-x 0,org-timer-start}
-Reset the timer without inserting anything into the buffer.  By default, the
-timer is reset to 0.  When called with a @kbd{C-u} prefix, reset the timer to
-specific starting offset.  The user is prompted for the offset, with a
-default taken from a timer string at point, if any, So this can be used to
-restart taking notes after a break in the process.  When called with a double
-prefix argument @kbd{C-u C-u}, change all timer strings in the active region
-by a certain amount.  This can be used to fix timer strings if the timer was
-not started at exactly the right moment.
 @end table
 @end table
 
 
-@node Countdown timer
-@section Countdown timer
-@cindex Countdown timer
-@kindex C-c C-x ;
-@kindex ;
-
-Calling @code{org-timer-set-timer} from an Org mode buffer runs a countdown
-timer.  Use @kbd{;} from agenda buffers, @key{C-c C-x ;} everywhere else.
-
-@code{org-timer-set-timer} prompts the user for a duration and displays a
-countdown timer in the modeline.  @code{org-timer-default-timer} sets the
-default countdown value.  Giving a prefix numeric argument overrides this
-default value.
-
 @node Capture - Refile - Archive
 @node Capture - Refile - Archive
 @chapter Capture - Refile - Archive
 @chapter Capture - Refile - Archive
 @cindex capture
 @cindex capture

+ 5 - 0
etc/ORG-NEWS

@@ -72,6 +72,8 @@ This function inserted a Beamer specific template at point or in
 current subtree.  Use ~org-export-insert-default-template~ instead, as
 current subtree.  Use ~org-export-insert-default-template~ instead, as
 it provides more features and covers all export back-ends.  It is also
 it provides more features and covers all export back-ends.  It is also
 accessible from the export dispatcher.
 accessible from the export dispatcher.
+*** Removed function ~org-timer-cancel-timer~
+~org-timer-stop~ now stops both relative and countdown timers.
 ** Removed options
 ** Removed options
 *** ~org-list-empty-line-terminates-plain-lists~ is deprecated
 *** ~org-list-empty-line-terminates-plain-lists~ is deprecated
 It will be kept in code base until next release, for backward
 It will be kept in code base until next release, for backward
@@ -174,6 +176,9 @@ special blocks and images.  See docstring for more information.
 Headlines, for which the property ~UNNUMBERED~ is non-nil, are now
 Headlines, for which the property ~UNNUMBERED~ is non-nil, are now
 exported without section numbers irrespective of their levels.  The
 exported without section numbers irrespective of their levels.  The
 property is inherited by children.
 property is inherited by children.
+*** Countdown timers can now be paused.
+~org-timer-pause-time~ wil now pause and restart both relative and
+countdown timers.
 ** Miscellaneous
 ** Miscellaneous
 *** Strip all meta data from ITEM special property
 *** Strip all meta data from ITEM special property
 ITEM special property does not contain TODO, priority or tags anymore.
 ITEM special property does not contain TODO, priority or tags anymore.

+ 121 - 86
lisp/org-timer.el

@@ -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.
 ;; Copyright (C) 2008-2014 Free Software Foundation, Inc.
 
 
@@ -24,13 +24,20 @@
 ;;
 ;;
 ;;; Commentary:
 ;;; 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:
 ;;; Code:
 
 
 (require 'org)
 (require 'org)
+(require 'org-clock)
 
 
-(declare-function org-notify "org-clock" (notification &optional play-sound))
 (declare-function org-agenda-error "org-agenda" ())
 (declare-function org-agenda-error "org-agenda" ())
 
 
 (defvar org-timer-start-time nil
 (defvar org-timer-start-time nil
@@ -39,13 +46,22 @@
 (defvar org-timer-pause-time nil
 (defvar org-timer-pause-time nil
   "Time when the timer was paused.")
   "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\\}\\)"
 (defconst org-timer-re "\\([-+]?[0-9]+\\):\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\)"
   "Regular expression used to match timer stamps.")
   "Regular expression used to match timer stamps.")
 
 
 (defcustom org-timer-format "%s "
 (defcustom org-timer-format "%s "
   "The format to insert the time of the timer.
   "The format to insert the time of the timer.
 This format must contain one instance of \"%s\" which will be replaced by
 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
   :group 'org-time
   :type 'string)
   :type 'string)
 
 
@@ -76,13 +92,13 @@ nil          current timer is not displayed"
   "Hook run after relative timer is started.")
   "Hook run after relative timer is started.")
 
 
 (defvar org-timer-stop-hook nil
 (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
 (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
 (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
 (defvar org-timer-set-hook nil
   "Hook run after countdown timer is set.")
   "Hook run after countdown timer is set.")
@@ -90,9 +106,6 @@ nil          current timer is not displayed"
 (defvar org-timer-done-hook nil
 (defvar org-timer-done-hook nil
   "Hook run after countdown timer reaches zero.")
   "Hook run after countdown timer reaches zero.")
 
 
-(defvar org-timer-cancel-hook nil
-  "Hook run before countdown timer is canceled.")
-
 ;;;###autoload
 ;;;###autoload
 (defun org-timer-start (&optional offset)
 (defun org-timer-start (&optional offset)
   "Set the starting time for the relative timer to now.
   "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 amount, with the default to make the first timer string in
 the region 0:00:00."
 the region 0:00:00."
   (interactive "P")
   (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)
     (let (delta def s)
       (if (not offset)
       (if (not offset)
 	  (setq org-timer-start-time (current-time))
 	  (setq org-timer-start-time (current-time))
@@ -123,45 +140,66 @@ the region 0:00:00."
 	  (setq delta (org-timer-hms-to-secs (org-timer-fix-incomplete s)))))
 	  (setq delta (org-timer-hms-to-secs (org-timer-fix-incomplete s)))))
 	(setq org-timer-start-time
 	(setq org-timer-start-time
 	      (seconds-to-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)
       (org-timer-set-mode-line 'on)
       (message "Timer start time set to %s, current value is %s"
       (message "Timer start time set to %s, current value is %s"
 	       (format-time-string "%T" org-timer-start-time)
 	       (format-time-string "%T" org-timer-start-time)
 	       (org-timer-secs-to-hms (or delta 0)))
 	       (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)
 (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."
 With prefix arg STOP, stop it entirely."
   (interactive "P")
   (interactive "P")
   (cond
   (cond
    (stop (org-timer-stop))
    (stop (org-timer-stop))
    ((not org-timer-start-time) (error "No timer is running"))
    ((not org-timer-start-time) (error "No timer is running"))
    (org-timer-pause-time
    (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
    (t
     ;; pause timer
     ;; 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)
     (run-hooks 'org-timer-pause-hook)
     (setq org-timer-pause-time (current-time))
     (setq org-timer-pause-time (current-time))
     (org-timer-set-mode-line 'pause)
     (org-timer-set-mode-line 'pause)
     (message "Timer paused at %s" (org-timer-value-string)))))
     (message "Timer paused at %s" (org-timer-value-string)))))
 
 
 (defun org-timer-stop ()
 (defun org-timer-stop ()
-  "Stop the relative timer."
+  "Stop the relative or countdown timer."
   (interactive)
   (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)
   (run-hooks 'org-timer-stop-hook)
   (setq org-timer-start-time nil
   (setq org-timer-start-time nil
-	org-timer-pause-time nil)
+	org-timer-pause-time nil
+	org-timer-countdown-timer nil)
   (org-timer-set-mode-line 'off)
   (org-timer-set-mode-line 'off)
   (message "Timer stopped"))
   (message "Timer stopped"))
 
 
@@ -191,11 +229,10 @@ it in the buffer."
 	  (org-timer-secs-to-hms
 	  (org-timer-secs-to-hms
 	   (abs (floor (org-timer-seconds))))))
 	   (abs (floor (org-timer-seconds))))))
 
 
-(defvar org-timer-timer-is-countdown nil)
 (defun org-timer-seconds ()
 (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 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 (or org-timer-pause-time (current-time)))
        (org-float-time org-timer-start-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)
 (defvar org-timer-mode-line-string nil)
 
 
 (defun org-timer-set-mode-line (value)
 (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'."
 VALUE can be `on', `off', or `pause'."
   (when (or (eq org-timer-display 'mode-line)
   (when (or (eq org-timer-display 'mode-line)
 	    (eq org-timer-display 'both))
 	    (eq org-timer-display 'both))
@@ -349,35 +386,20 @@ VALUE can be `on', `off', or `pause'."
 	  (concat " <" (substring (org-timer-value-string) 0 -1) ">"))
 	  (concat " <" (substring (org-timer-value-string) 0 -1) ">"))
     (force-mode-line-update)))
     (force-mode-line-update)))
 
 
-(defvar org-timer-current-timer nil)
-(defun org-timer-cancel-timer ()
-  "Cancel the current timer."
-  (interactive)
-  (if (not org-timer-current-timer)
-      (message "No timer to cancel")
-    (run-hooks 'org-timer-cancel-hook)
-    (cancel-timer org-timer-current-timer)
-    (setq org-timer-current-timer nil
-	  org-timer-timer-is-countdown nil)
-    (org-timer-set-mode-line 'off)
-    (message "Last timer canceled")))
-
 (defun org-timer-show-remaining-time ()
 (defun org-timer-show-remaining-time ()
   "Display the remaining time before the timer ends."
   "Display the remaining time before the timer ends."
   (interactive)
   (interactive)
   (require 'time)
   (require 'time)
-  (if (not org-timer-current-timer)
+  (if (not org-timer-countdown-timer)
       (message "No timer set")
       (message "No timer set")
     (let* ((rtime (decode-time
     (let* ((rtime (decode-time
-		   (time-subtract (timer--time org-timer-current-timer)
+		   (time-subtract (timer--time org-timer-countdown-timer)
 				  (current-time))))
 				  (current-time))))
 	   (rsecs (nth 0 rtime))
 	   (rsecs (nth 0 rtime))
 	   (rmins (nth 1 rtime)))
 	   (rmins (nth 1 rtime)))
       (message "%d minute(s) %d seconds left before next time out"
       (message "%d minute(s) %d seconds left before next time out"
 	       rmins rsecs))))
 	       rmins rsecs))))
 
 
-(defvar org-clock-sound)
-
 ;;;###autoload
 ;;;###autoload
 (defun org-timer-set-timer (&optional opt)
 (defun org-timer-set-timer (&optional opt)
   "Prompt for a duration and set a timer.
   "Prompt for a duration and set a timer.
@@ -400,7 +422,10 @@ By default, the timer duration will be set to the number of
 minutes in the Effort property, if any.  You can ignore this by
 minutes in the Effort property, if any.  You can ignore this by
 using three `C-u' prefix arguments."
 using three `C-u' prefix arguments."
   (interactive "P")
   (interactive "P")
-  (let* ((effort-minutes (org-get-at-eol 'effort-minutes 1))
+  (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)))
 	 (minutes (or (and (not (equal opt '(64)))
 			   effort-minutes
 			   effort-minutes
 			   (number-to-string effort-minutes))
 			   (number-to-string effort-minutes))
@@ -415,47 +440,57 @@ using three `C-u' prefix arguments."
 	(org-timer-show-remaining-time)
 	(org-timer-show-remaining-time)
       (let* ((mins (string-to-number (match-string 0 minutes)))
       (let* ((mins (string-to-number (match-string 0 minutes)))
 	     (secs (* mins 60))
 	     (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
 	    (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)
 	      (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))
 	      (org-timer-set-mode-line 'on))
 	  (message "No timer set"))))))
 	  (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)
 (provide 'org-timer)
 
 
 ;; Local variables:
 ;; Local variables:

+ 273 - 0
testing/lisp/test-org-timer.el

@@ -0,0 +1,273 @@
+;;; test-org-timer.el --- Tests for org-timer.el
+
+;; Copyright (C) 2014  Kyle Meyer
+
+;; Author: Kyle Meyer <kyle@kyleam.com>
+
+;; 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/>.
+
+;;; Code:
+
+(defmacro test-org-timer/with-temp-text (text &rest body)
+  "Like `org-test-with-temp-text', but set timer-specific variables.
+Also, mute output from `message'."
+  (declare (indent 1))
+  `(cl-letf (((symbol-function 'message) (lambda (&rest args) nil)))
+     (org-test-with-temp-text ,text
+       (let (org-timer-start-time
+	     org-timer-pause-time
+	     org-timer-countdown-timer
+	     org-timer-display)
+	 (unwind-protect (progn ,@body)
+	   (when (timerp org-timer-countdown-timer)
+	     (cancel-timer org-timer-countdown-timer)))))))
+
+(defmacro test-org-timer/with-current-time (time &rest body)
+  "Run BODY, setting `current-time' output to TIME."
+  (declare (indent 1))
+  `(cl-letf (((symbol-function 'current-time) (lambda () ,time)))
+     ,@body))
+
+
+;;; Time conversion and formatting
+
+(ert-deftest test-org-timer/secs-to-hms ()
+  "Test conversion between HMS format and seconds."
+  ;; Seconds to HMS, and back again
+  (should
+   (equal "0:00:30"
+	  (org-timer-secs-to-hms 30)))
+  (should
+   (equal 30
+	  (org-timer-hms-to-secs (org-timer-secs-to-hms 30))))
+  ;; Minutes to HMS, and back again
+  (should
+   (equal "0:02:10"
+	  (org-timer-secs-to-hms 130)))
+  (should
+   (equal 130
+	  (org-timer-hms-to-secs (org-timer-secs-to-hms 130))))
+  ;; Hours to HMS, and back again
+  (should
+   (equal "1:01:30"
+	  (org-timer-secs-to-hms 3690)))
+  (should
+   (equal 3690
+	  (org-timer-hms-to-secs (org-timer-secs-to-hms 3690))))
+  ;; Negative seconds to HMS, and back again
+  (should
+   (equal "-1:01:30"
+	  (org-timer-secs-to-hms -3690)))
+  (should
+   (equal -3690
+	  (org-timer-hms-to-secs (org-timer-secs-to-hms -3690)))))
+
+(ert-deftest test-org-timer/fix-incomplete ()
+  "Test conversion to complete HMS format."
+  ;; No fix is needed.
+  (should
+   (equal "1:02:03"
+	  (org-timer-fix-incomplete "1:02:03")))
+  ;; Hour is missing.
+  (should
+   (equal "0:02:03"
+	  (org-timer-fix-incomplete "02:03")))
+  ;; Minute is missing.
+  (should
+   (equal "0:00:03"
+	  (org-timer-fix-incomplete "03"))))
+
+(ert-deftest test-org-timer/change-times ()
+  "Test changing HMS format by offset."
+  ;; Add time.
+  (should
+   (equal "
+1:31:15
+4:00:55"
+	  (org-test-with-temp-text "
+0:00:25
+2:30:05"
+	    (org-timer-change-times-in-region (point-min) (point-max)
+					      "1:30:50")
+	    (buffer-string))))
+  ;; Subtract time.
+  (should
+   (equal "
+-1:30:25
+0:59:15"
+	  (org-test-with-temp-text "
+0:00:25
+2:30:05"
+	    (org-timer-change-times-in-region (point-min) (point-max)
+					      "-1:30:50")
+	    (buffer-string)))))
+
+
+;;; Timers
+
+;; Dummy times for overriding `current-time'
+(defvar test-org-timer/time0 '(21635 62793 797149 675000))
+;; Add 3 minutes and 26 seconds.
+(defvar test-org-timer/time1
+  (time-add test-org-timer/time0 (seconds-to-time 206)))
+;; Add 2 minutes and 41 seconds (6 minutes and 7 seconds total).
+(defvar test-org-timer/time2
+  (time-add test-org-timer/time1 (seconds-to-time 161)))
+;; Add 4 minutes and 55 seconds (11 minutes and 2 seconds total).
+(defvar test-org-timer/time3
+  (time-add test-org-timer/time2 (seconds-to-time 295)))
+
+(ert-deftest test-org-timer/start-relative ()
+  "Test starting relative timer."
+  ;; Insert plain timer string, starting with `org-timer-start'.
+  (should
+   (equal "0:03:26"
+	  (test-org-timer/with-temp-text ""
+	    (test-org-timer/with-current-time test-org-timer/time0
+	      (org-timer-start))
+	    (test-org-timer/with-current-time test-org-timer/time1
+	      (org-timer))
+	    (org-trim (buffer-string)))))
+  ;; Insert item timer string.
+  (should
+   (equal "- 0:03:26 ::"
+	  (test-org-timer/with-temp-text ""
+	    (test-org-timer/with-current-time test-org-timer/time0
+	      (org-timer-start))
+	    (test-org-timer/with-current-time test-org-timer/time1
+	      (org-timer-item))
+	    (org-trim (buffer-string)))))
+  ;; Start with `org-timer'.
+  (should
+   (equal "0:00:00 0:03:26"
+	  (test-org-timer/with-temp-text ""
+	    (test-org-timer/with-current-time test-org-timer/time0
+	      (org-timer))
+	    (test-org-timer/with-current-time test-org-timer/time1
+	      (org-timer))
+	    (org-trim (buffer-string)))))
+  ;; Restart with `org-timer'.
+  (should
+   (equal "0:00:00"
+	  (test-org-timer/with-temp-text ""
+	    (test-org-timer/with-current-time test-org-timer/time0
+	      (org-timer-start))
+	    (test-org-timer/with-current-time test-org-timer/time1
+	      (org-timer '(4)))
+	    (org-trim (buffer-string))))))
+
+(ert-deftest test-org-timer/set-timer ()
+  "Test setting countdown timer."
+  (should
+   (equal "0:06:34"
+	  (test-org-timer/with-temp-text ""
+	    (test-org-timer/with-current-time test-org-timer/time0
+	      (org-timer-set-timer 10))
+	    (test-org-timer/with-current-time test-org-timer/time1
+	      (org-timer))
+	    (org-trim (buffer-string))))))
+
+(ert-deftest test-org-timer/pause-timer ()
+  "Test pausing relative and countdown timers."
+  ;; Pause relative timer.
+  (should
+   (equal "0:03:26"
+	  (test-org-timer/with-temp-text ""
+	    (test-org-timer/with-current-time test-org-timer/time0
+	      (org-timer-start))
+	    (test-org-timer/with-current-time test-org-timer/time1
+	      (org-timer-pause-or-continue))
+	    (org-timer)
+	    (org-trim (buffer-string)))))
+  ;; Pause then continue relative timer.
+  (should
+   (equal "0:08:21"
+	  (test-org-timer/with-temp-text ""
+	    (test-org-timer/with-current-time test-org-timer/time0
+	      (org-timer-start))
+	    (test-org-timer/with-current-time test-org-timer/time1
+	      (org-timer-pause-or-continue))
+	    (test-org-timer/with-current-time test-org-timer/time2
+	      (org-timer-pause-or-continue))
+	    (test-org-timer/with-current-time test-org-timer/time3
+	      (org-timer))
+	    (org-trim (buffer-string)))))
+  ;; Pause then continue countdown timer.
+  (should
+   (equal "0:01:39"
+	  (test-org-timer/with-temp-text ""
+	    (test-org-timer/with-current-time test-org-timer/time0
+	      (org-timer-set-timer 10))
+	    (test-org-timer/with-current-time test-org-timer/time1
+	      (org-timer-pause-or-continue))
+	    (test-org-timer/with-current-time test-org-timer/time2
+	      (org-timer-pause-or-continue))
+	    (test-org-timer/with-current-time test-org-timer/time3
+	      (org-timer))
+	    (org-trim (buffer-string))))))
+
+(ert-deftest test-org-timer/stop ()
+  "Test stopping relative and countdown timers."
+  ;; Stop running relative timer.
+  (test-org-timer/with-temp-text ""
+    (test-org-timer/with-current-time test-org-timer/time0
+      (org-timer-start))
+    (test-org-timer/with-current-time test-org-timer/time1
+      (org-timer-stop))
+    (should-not org-timer-start-time))
+  ;; Stop paused relative timer.
+  (test-org-timer/with-temp-text ""
+    (test-org-timer/with-current-time test-org-timer/time0
+      (org-timer-start))
+    (test-org-timer/with-current-time test-org-timer/time1
+      (org-timer-pause-or-continue)
+      (org-timer-stop))
+    (should-not org-timer-start-time)
+    (should-not org-timer-pause-time))
+  ;; Stop running countdown timer.
+  (test-org-timer/with-temp-text ""
+    (test-org-timer/with-current-time test-org-timer/time0
+      (org-timer-set-timer 10))
+    (test-org-timer/with-current-time test-org-timer/time1
+      (org-timer-stop))
+    (should-not org-timer-start-time)
+    (should-not org-timer-countdown-timer))
+  ;; Stop paused countdown timer.
+  (test-org-timer/with-temp-text ""
+    (test-org-timer/with-current-time test-org-timer/time0
+      (org-timer-set-timer 10))
+    (test-org-timer/with-current-time test-org-timer/time1
+      (org-timer-pause-or-continue)
+      (org-timer-stop))
+    (should-not org-timer-start-time)
+    (should-not org-timer-pause-time)
+    (should-not org-timer-countdown-timer)))
+
+(ert-deftest test-org-timer/other-timer-error ()
+  "Test for error when other timer running."
+  ;; Relative timer is running.
+  (should-error
+   (test-org-timer/with-temp-text ""
+     (org-timer-start)
+     (org-timer-set-timer 10)))
+  ;; Countdown timer is running.
+  (should-error
+   (test-org-timer/with-temp-text ""
+     (org-timer-set-timer 10)
+     (org-timer-start))))
+
+(provide 'test-org-timer)
+;;; test-org-timer.el end here