|
@@ -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.
|
|
|
|
|
@@ -24,13 +24,20 @@
|
|
|
;;
|
|
|
;;; 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
|
|
@@ -39,13 +46,22 @@
|
|
|
(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,45 +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)))))
|
|
|
|
|
|
(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-pause-time nil
|
|
|
+ org-timer-countdown-timer nil)
|
|
|
(org-timer-set-mode-line 'off)
|
|
|
(message "Timer stopped"))
|
|
|
|
|
@@ -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,35 +386,20 @@ VALUE can be `on', `off', or `pause'."
|
|
|
(concat " <" (substring (org-timer-value-string) 0 -1) ">"))
|
|
|
(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 ()
|
|
|
"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.
|
|
@@ -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
|
|
|
using three `C-u' prefix arguments."
|
|
|
(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)))
|
|
|
effort-minutes
|
|
|
(number-to-string effort-minutes))
|
|
@@ -415,47 +440,57 @@ using three `C-u' prefix arguments."
|
|
|
(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:
|