Commit | Line | Data |
---|---|---|
0bd48b37 | 1 | ;;; org-timer.el --- The relative timer code for Org-mode |
bc23baaa | 2 | |
ba318903 | 3 | ;; Copyright (C) 2008-2014 Free Software Foundation, Inc. |
bc23baaa CD |
4 | |
5 | ;; Author: Carsten Dominik <carsten at orgmode dot org> | |
6 | ;; Keywords: outlines, hypermedia, calendar, wp | |
7 | ;; Homepage: http://orgmode.org | |
bc23baaa CD |
8 | ;; |
9 | ;; This file is part of GNU Emacs. | |
10 | ;; | |
11 | ;; GNU Emacs is free software: you can redistribute it and/or modify | |
12 | ;; it under the terms of the GNU General Public License as published by | |
13 | ;; the Free Software Foundation, either version 3 of the License, or | |
14 | ;; (at your option) any later version. | |
15 | ||
16 | ;; GNU Emacs is distributed in the hope that it will be useful, | |
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 | ;; GNU General Public License for more details. | |
20 | ||
21 | ;; You should have received a copy of the GNU General Public License | |
22 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | |
23 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
24 | ;; | |
25 | ;;; Commentary: | |
26 | ||
27 | ;; This file contains the relative timer code for Org-mode | |
28 | ||
86fbb8ca CD |
29 | ;;; Code: |
30 | ||
bc23baaa CD |
31 | (require 'org) |
32 | ||
afe98dfa | 33 | (declare-function org-notify "org-clock" (notification &optional play-sound)) |
c8d0cf5c CD |
34 | (declare-function org-agenda-error "org-agenda" ()) |
35 | ||
bc23baaa CD |
36 | (defvar org-timer-start-time nil |
37 | "t=0 for the running timer.") | |
38 | ||
0bd48b37 CD |
39 | (defvar org-timer-pause-time nil |
40 | "Time when the timer was paused.") | |
41 | ||
bc23baaa CD |
42 | (defconst org-timer-re "\\([-+]?[0-9]+\\):\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\)" |
43 | "Regular expression used to match timer stamps.") | |
44 | ||
45 | (defcustom org-timer-format "%s " | |
46 | "The format to insert the time of the timer. | |
47 | This format must contain one instance of \"%s\" which will be replaced by | |
48 | the value of the relative timer." | |
49 | :group 'org-time | |
50 | :type 'string) | |
51 | ||
86fbb8ca CD |
52 | (defcustom org-timer-default-timer 0 |
53 | "The default timer when a timer is set. | |
54 | When 0, the user is prompted for a value." | |
55 | :group 'org-time | |
372d7b21 | 56 | :version "24.1" |
86fbb8ca CD |
57 | :type 'number) |
58 | ||
8223b1d2 BG |
59 | (defcustom org-timer-display 'mode-line |
60 | "When a timer is running, org-mode can display it in the mode | |
61 | line and/or frame title. | |
62 | Allowed values are: | |
63 | ||
64 | both displays in both mode line and frame title | |
65 | mode-line displays only in mode line (default) | |
66 | frame-title displays only in frame title | |
67 | nil current timer is not displayed" | |
68 | :group 'org-time | |
69 | :type '(choice | |
70 | (const :tag "Mode line" mode-line) | |
71 | (const :tag "Frame title" frame-title) | |
72 | (const :tag "Both" both) | |
73 | (const :tag "None" nil))) | |
74 | ||
ed21c5c8 CD |
75 | (defvar org-timer-start-hook nil |
76 | "Hook run after relative timer is started.") | |
77 | ||
78 | (defvar org-timer-stop-hook nil | |
79 | "Hook run before relative timer is stopped.") | |
80 | ||
81 | (defvar org-timer-pause-hook nil | |
82 | "Hook run before relative timer is paused.") | |
83 | ||
3ab2c837 | 84 | (defvar org-timer-continue-hook nil |
8223b1d2 | 85 | "Hook run after relative timer is continued.") |
3ab2c837 | 86 | |
ed21c5c8 CD |
87 | (defvar org-timer-set-hook nil |
88 | "Hook run after countdown timer is set.") | |
89 | ||
90 | (defvar org-timer-done-hook nil | |
91 | "Hook run after countdown timer reaches zero.") | |
92 | ||
93 | (defvar org-timer-cancel-hook nil | |
94 | "Hook run before countdown timer is canceled.") | |
95 | ||
bc23baaa CD |
96 | ;;;###autoload |
97 | (defun org-timer-start (&optional offset) | |
98 | "Set the starting time for the relative timer to now. | |
99 | When called with prefix argument OFFSET, prompt the user for an offset time, | |
100 | with the default taken from a timer stamp at point, if any. | |
101 | If OFFSET is a string or an integer, it is directly taken to be the offset | |
102 | without user interaction. | |
103 | When called with a double prefix arg, all timer strings in the active | |
104 | region will be shifted by a specific amount. You will be prompted for | |
105 | the amount, with the default to make the first timer string in | |
106 | the region 0:00:00." | |
107 | (interactive "P") | |
108 | (if (equal offset '(16)) | |
109 | (call-interactively 'org-timer-change-times-in-region) | |
110 | (let (delta def s) | |
111 | (if (not offset) | |
112 | (setq org-timer-start-time (current-time)) | |
113 | (cond | |
114 | ((integerp offset) (setq delta offset)) | |
115 | ((stringp offset) (setq delta (org-timer-hms-to-secs offset))) | |
116 | (t | |
117 | (setq def (if (org-in-regexp org-timer-re) | |
118 | (match-string 0) | |
119 | "0:00:00") | |
120 | s (read-string | |
121 | (format "Restart timer with offset [%s]: " def))) | |
122 | (unless (string-match "\\S-" s) (setq s def)) | |
123 | (setq delta (org-timer-hms-to-secs (org-timer-fix-incomplete s))))) | |
124 | (setq org-timer-start-time | |
125 | (seconds-to-time | |
86fbb8ca | 126 | (- (org-float-time) delta)))) |
0bd48b37 | 127 | (org-timer-set-mode-line 'on) |
bc23baaa CD |
128 | (message "Timer start time set to %s, current value is %s" |
129 | (format-time-string "%T" org-timer-start-time) | |
ed21c5c8 CD |
130 | (org-timer-secs-to-hms (or delta 0))) |
131 | (run-hooks 'org-timer-start-hook)))) | |
bc23baaa | 132 | |
0bd48b37 | 133 | (defun org-timer-pause-or-continue (&optional stop) |
86fbb8ca CD |
134 | "Pause or continue the relative timer. |
135 | With prefix arg STOP, stop it entirely." | |
0bd48b37 CD |
136 | (interactive "P") |
137 | (cond | |
138 | (stop (org-timer-stop)) | |
139 | ((not org-timer-start-time) (error "No timer is running")) | |
140 | (org-timer-pause-time | |
141 | ;; timer is paused, continue | |
142 | (setq org-timer-start-time | |
143 | (seconds-to-time | |
144 | (- | |
54a0dee5 CD |
145 | (org-float-time) |
146 | (- (org-float-time org-timer-pause-time) | |
147 | (org-float-time org-timer-start-time)))) | |
0bd48b37 CD |
148 | org-timer-pause-time nil) |
149 | (org-timer-set-mode-line 'on) | |
3ab2c837 | 150 | (run-hooks 'org-timer-continue-hook) |
0bd48b37 CD |
151 | (message "Timer continues at %s" (org-timer-value-string))) |
152 | (t | |
153 | ;; pause timer | |
ed21c5c8 | 154 | (run-hooks 'org-timer-pause-hook) |
0bd48b37 CD |
155 | (setq org-timer-pause-time (current-time)) |
156 | (org-timer-set-mode-line 'pause) | |
157 | (message "Timer paused at %s" (org-timer-value-string))))) | |
158 | ||
30cb51f1 | 159 | (defvar org-timer-current-timer nil) |
0bd48b37 CD |
160 | (defun org-timer-stop () |
161 | "Stop the relative timer." | |
162 | (interactive) | |
ed21c5c8 | 163 | (run-hooks 'org-timer-stop-hook) |
0bd48b37 | 164 | (setq org-timer-start-time nil |
30cb51f1 BG |
165 | org-timer-pause-time nil |
166 | org-timer-current-timer nil) | |
63aa0982 BG |
167 | (org-timer-set-mode-line 'off) |
168 | (message "Timer stopped")) | |
0bd48b37 | 169 | |
bc23baaa | 170 | ;;;###autoload |
afe98dfa | 171 | (defun org-timer (&optional restart no-insert-p) |
bc23baaa CD |
172 | "Insert a H:MM:SS string from the timer into the buffer. |
173 | The first time this command is used, the timer is started. When used with | |
86fbb8ca | 174 | a \\[universal-argument] prefix, force restarting the timer. |
afe98dfa | 175 | When used with a double prefix argument \\[universal-argument], change all the timer string |
bc23baaa | 176 | in the region by a fixed amount. This can be used to recalibrate a timer |
afe98dfa CD |
177 | that was not started at the correct moment. |
178 | ||
179 | If NO-INSERT-P is non-nil, return the string instead of inserting | |
180 | it in the buffer." | |
bc23baaa | 181 | (interactive "P") |
afe98dfa CD |
182 | (when (or (equal restart '(4)) (not org-timer-start-time)) |
183 | (org-timer-start)) | |
184 | (if no-insert-p | |
185 | (org-timer-value-string) | |
186 | (insert (org-timer-value-string)))) | |
0bd48b37 CD |
187 | |
188 | (defun org-timer-value-string () | |
189 | (format org-timer-format (org-timer-secs-to-hms (floor (org-timer-seconds))))) | |
190 | ||
afe98dfa | 191 | (defvar org-timer-timer-is-countdown nil) |
0bd48b37 | 192 | (defun org-timer-seconds () |
afe98dfa CD |
193 | (if org-timer-timer-is-countdown |
194 | (- (org-float-time org-timer-start-time) | |
195 | (org-float-time (current-time))) | |
196 | (- (org-float-time (or org-timer-pause-time (current-time))) | |
197 | (org-float-time org-timer-start-time)))) | |
bc23baaa CD |
198 | |
199 | ;;;###autoload | |
200 | (defun org-timer-change-times-in-region (beg end delta) | |
201 | "Change all h:mm:ss time in region by a DELTA." | |
202 | (interactive | |
8223b1d2 | 203 | "r\nsEnter time difference like \"-1:08:26\". Default is first time to zero: ") |
bc23baaa CD |
204 | (let ((re "[-+]?[0-9]+:[0-9]\\{2\\}:[0-9]\\{2\\}") p) |
205 | (unless (string-match "\\S-" delta) | |
206 | (save-excursion | |
207 | (goto-char beg) | |
208 | (when (re-search-forward re end t) | |
209 | (setq delta (match-string 0)) | |
210 | (if (equal (string-to-char delta) ?-) | |
211 | (setq delta (substring delta 1)) | |
212 | (setq delta (concat "-" delta)))))) | |
213 | (setq delta (org-timer-hms-to-secs (org-timer-fix-incomplete delta))) | |
214 | (when (= delta 0) (error "No change")) | |
215 | (save-excursion | |
216 | (goto-char end) | |
217 | (while (re-search-backward re beg t) | |
218 | (setq p (point)) | |
219 | (replace-match | |
220 | (save-match-data | |
221 | (org-timer-secs-to-hms (+ (org-timer-hms-to-secs (match-string 0)) delta))) | |
222 | t t) | |
223 | (goto-char p))))) | |
224 | ||
225 | ;;;###autoload | |
226 | (defun org-timer-item (&optional arg) | |
33306645 | 227 | "Insert a description-type item with the current timer value." |
bc23baaa | 228 | (interactive "P") |
3ab2c837 BG |
229 | (let ((itemp (org-in-item-p)) (pos (point))) |
230 | (cond | |
231 | ;; In a timer list, insert with `org-list-insert-item', | |
232 | ;; then fix the list. | |
233 | ((and itemp (goto-char itemp) (org-at-item-timer-p)) | |
234 | (let* ((struct (org-list-struct)) | |
235 | (prevs (org-list-prevs-alist struct)) | |
236 | (s (concat (org-timer (when arg '(4)) t) ":: "))) | |
237 | (setq struct (org-list-insert-item pos struct prevs nil s)) | |
238 | (org-list-write-struct struct (org-list-parents-alist struct)) | |
239 | (looking-at org-list-full-item-re) | |
240 | (goto-char (match-end 0)))) | |
241 | ;; In a list of another type, don't break anything: throw an error. | |
242 | (itemp (goto-char pos) (error "This is not a timer list")) | |
243 | ;; Else, start a new list. | |
244 | (t | |
245 | (beginning-of-line) | |
8223b1d2 | 246 | (org-indent-line) |
3ab2c837 BG |
247 | (insert "- ") |
248 | (org-timer (when arg '(4))) | |
249 | (insert ":: "))))) | |
bc23baaa CD |
250 | |
251 | (defun org-timer-fix-incomplete (hms) | |
252 | "If hms is a H:MM:SS string with missing hour or hour and minute, fix it." | |
253 | (if (string-match "\\(?:\\([0-9]+:\\)?\\([0-9]+:\\)\\)?\\([0-9]+\\)" hms) | |
254 | (replace-match | |
255 | (format "%d:%02d:%02d" | |
256 | (if (match-end 1) (string-to-number (match-string 1 hms)) 0) | |
257 | (if (match-end 2) (string-to-number (match-string 2 hms)) 0) | |
258 | (string-to-number (match-string 3 hms))) | |
259 | t t hms) | |
33306645 | 260 | (error "Cannot parse HMS string \"%s\"" hms))) |
bc23baaa CD |
261 | |
262 | (defun org-timer-hms-to-secs (hms) | |
263 | "Convert h:mm:ss string to an integer time. | |
264 | If the string starts with a minus sign, the integer will be negative." | |
265 | (if (not (string-match | |
266 | "\\([-+]?[0-9]+\\):\\([0-9]\\{2\\}\\):\\([0-9]\\{2\\}\\)" | |
267 | hms)) | |
268 | 0 | |
269 | (let* ((h (string-to-number (match-string 1 hms))) | |
270 | (m (string-to-number (match-string 2 hms))) | |
271 | (s (string-to-number (match-string 3 hms))) | |
272 | (sign (equal (substring (match-string 1 hms) 0 1) "-"))) | |
273 | (setq h (abs h)) | |
274 | (* (if sign -1 1) (+ s (* 60 (+ m (* 60 h)))))))) | |
275 | ||
276 | (defun org-timer-secs-to-hms (s) | |
277 | "Convert integer S into h:mm:ss. | |
33306645 | 278 | If the integer is negative, the string will start with \"-\"." |
bc23baaa CD |
279 | (let (sign m h) |
280 | (setq sign (if (< s 0) "-" "") | |
281 | s (abs s) | |
282 | m (/ s 60) s (- s (* 60 m)) | |
283 | h (/ m 60) m (- m (* 60 h))) | |
284 | (format "%s%d:%02d:%02d" sign h m s))) | |
285 | ||
0bd48b37 CD |
286 | (defvar org-timer-mode-line-timer nil) |
287 | (defvar org-timer-mode-line-string nil) | |
288 | ||
289 | (defun org-timer-set-mode-line (value) | |
8bfe682a | 290 | "Set the mode-line display of the relative timer. |
0bd48b37 | 291 | VALUE can be `on', `off', or `pause'." |
8223b1d2 BG |
292 | (when (or (eq org-timer-display 'mode-line) |
293 | (eq org-timer-display 'both)) | |
294 | (or global-mode-string (setq global-mode-string '(""))) | |
295 | (or (memq 'org-timer-mode-line-string global-mode-string) | |
296 | (setq global-mode-string | |
297 | (append global-mode-string '(org-timer-mode-line-string))))) | |
298 | (when (or (eq org-timer-display 'frame-title) | |
299 | (eq org-timer-display 'both)) | |
300 | (or (memq 'org-timer-mode-line-string frame-title-format) | |
301 | (setq frame-title-format | |
302 | (append frame-title-format '(org-timer-mode-line-string))))) | |
0bd48b37 CD |
303 | (cond |
304 | ((equal value 'off) | |
305 | (when org-timer-mode-line-timer | |
306 | (cancel-timer org-timer-mode-line-timer) | |
307 | (setq org-timer-mode-line-timer nil)) | |
8223b1d2 BG |
308 | (when (or (eq org-timer-display 'mode-line) |
309 | (eq org-timer-display 'both)) | |
310 | (setq global-mode-string | |
311 | (delq 'org-timer-mode-line-string global-mode-string))) | |
312 | (when (or (eq org-timer-display 'frame-title) | |
313 | (eq org-timer-display 'both)) | |
314 | (setq frame-title-format | |
315 | (delq 'org-timer-mode-line-string frame-title-format))) | |
0bd48b37 CD |
316 | (force-mode-line-update)) |
317 | ((equal value 'pause) | |
318 | (when org-timer-mode-line-timer | |
319 | (cancel-timer org-timer-mode-line-timer) | |
320 | (setq org-timer-mode-line-timer nil))) | |
321 | ((equal value 'on) | |
8223b1d2 BG |
322 | (when (or (eq org-timer-display 'mode-line) |
323 | (eq org-timer-display 'both)) | |
324 | (or global-mode-string (setq global-mode-string '(""))) | |
325 | (or (memq 'org-timer-mode-line-string global-mode-string) | |
326 | (setq global-mode-string | |
327 | (append global-mode-string '(org-timer-mode-line-string))))) | |
328 | (when (or (eq org-timer-display 'frame-title) | |
329 | (eq org-timer-display 'both)) | |
330 | (or (memq 'org-timer-mode-line-string frame-title-format) | |
331 | (setq frame-title-format | |
332 | (append frame-title-format '(org-timer-mode-line-string))))) | |
0bd48b37 CD |
333 | (org-timer-update-mode-line) |
334 | (when org-timer-mode-line-timer | |
8223b1d2 BG |
335 | (cancel-timer org-timer-mode-line-timer) |
336 | (setq org-timer-mode-line-timer nil)) | |
337 | (when org-timer-display | |
338 | (setq org-timer-mode-line-timer | |
339 | (run-with-timer 1 1 'org-timer-update-mode-line)))))) | |
0bd48b37 CD |
340 | |
341 | (defun org-timer-update-mode-line () | |
342 | "Update the timer time in the mode line." | |
343 | (if org-timer-pause-time | |
344 | nil | |
345 | (setq org-timer-mode-line-string | |
346 | (concat " <" (substring (org-timer-value-string) 0 -1) ">")) | |
347 | (force-mode-line-update))) | |
348 | ||
ed21c5c8 CD |
349 | (defun org-timer-cancel-timer () |
350 | "Cancel the current timer." | |
c8d0cf5c | 351 | (interactive) |
ed21c5c8 CD |
352 | (when (eval org-timer-current-timer) |
353 | (run-hooks 'org-timer-cancel-hook) | |
354 | (cancel-timer org-timer-current-timer) | |
afe98dfa CD |
355 | (setq org-timer-current-timer nil) |
356 | (setq org-timer-timer-is-countdown nil) | |
357 | (org-timer-set-mode-line 'off)) | |
ed21c5c8 | 358 | (message "Last timer canceled")) |
c8d0cf5c CD |
359 | |
360 | (defun org-timer-show-remaining-time () | |
361 | "Display the remaining time before the timer ends." | |
362 | (interactive) | |
363 | (require 'time) | |
ed21c5c8 | 364 | (if (not org-timer-current-timer) |
c8d0cf5c CD |
365 | (message "No timer set") |
366 | (let* ((rtime (decode-time | |
ed21c5c8 | 367 | (time-subtract (timer--time org-timer-current-timer) |
c8d0cf5c CD |
368 | (current-time)))) |
369 | (rsecs (nth 0 rtime)) | |
370 | (rmins (nth 1 rtime))) | |
ed21c5c8 | 371 | (message "%d minute(s) %d seconds left before next time out" |
c8d0cf5c CD |
372 | rmins rsecs)))) |
373 | ||
271672fa BG |
374 | (defvar org-clock-sound) |
375 | ||
c8d0cf5c | 376 | ;;;###autoload |
86fbb8ca CD |
377 | (defun org-timer-set-timer (&optional opt) |
378 | "Prompt for a duration and set a timer. | |
379 | ||
380 | If `org-timer-default-timer' is not zero, suggest this value as | |
381 | the default duration for the timer. If a timer is already set, | |
afe98dfa | 382 | prompt the user if she wants to replace it. |
86fbb8ca CD |
383 | |
384 | Called with a numeric prefix argument, use this numeric value as | |
385 | the duration of the timer. | |
386 | ||
387 | Called with a `C-u' prefix arguments, use `org-timer-default-timer' | |
388 | without prompting the user for a duration. | |
389 | ||
390 | With two `C-u' prefix arguments, use `org-timer-default-timer' | |
391 | without prompting the user for a duration and automatically | |
392 | replace any running timer." | |
393 | (interactive "P") | |
394 | (let ((minutes (or (and (numberp opt) (number-to-string opt)) | |
395 | (and (listp opt) (not (null opt)) | |
396 | (number-to-string org-timer-default-timer)) | |
397 | (read-from-minibuffer | |
398 | "How many minutes left? " | |
399 | (if (not (eq org-timer-default-timer 0)) | |
400 | (number-to-string org-timer-default-timer)))))) | |
401 | (if (not (string-match "[0-9]+" minutes)) | |
402 | (org-timer-show-remaining-time) | |
8223b1d2 BG |
403 | (let* ((mins (string-to-number (match-string 0 minutes))) |
404 | (secs (* mins 60)) | |
405 | (hl (cond | |
406 | ((string-match "Org Agenda" (buffer-name)) | |
407 | (let* ((marker (or (get-text-property (point) 'org-marker) | |
408 | (org-agenda-error))) | |
409 | (hdmarker (or (get-text-property (point) 'org-hd-marker) | |
410 | marker)) | |
411 | (pos (marker-position marker))) | |
412 | (with-current-buffer (marker-buffer marker) | |
413 | (widen) | |
414 | (goto-char pos) | |
415 | (org-show-entry) | |
416 | (or (ignore-errors (org-get-heading)) | |
417 | (concat "File:" (file-name-nondirectory (buffer-file-name))))))) | |
418 | ((derived-mode-p 'org-mode) | |
419 | (or (ignore-errors (org-get-heading)) | |
420 | (concat "File:" (file-name-nondirectory (buffer-file-name))))) | |
421 | (t (error "Not in an Org buffer")))) | |
422 | timer-set) | |
423 | (if (or (and org-timer-current-timer | |
424 | (or (equal opt '(16)) | |
425 | (y-or-n-p "Replace current timer? "))) | |
426 | (not org-timer-current-timer)) | |
427 | (progn | |
428 | (require 'org-clock) | |
429 | (when org-timer-current-timer | |
430 | (cancel-timer org-timer-current-timer)) | |
431 | (setq org-timer-current-timer | |
432 | (run-with-timer | |
433 | secs nil `(lambda () | |
434 | (setq org-timer-current-timer nil) | |
271672fa | 435 | (org-notify ,(format "%s: time out" hl) ,org-clock-sound) |
8223b1d2 BG |
436 | (setq org-timer-timer-is-countdown nil) |
437 | (org-timer-set-mode-line 'off) | |
438 | (run-hooks 'org-timer-done-hook)))) | |
439 | (run-hooks 'org-timer-set-hook) | |
440 | (setq org-timer-timer-is-countdown t | |
441 | org-timer-start-time | |
442 | (time-add (current-time) (seconds-to-time (* mins 60)))) | |
443 | (org-timer-set-mode-line 'on)) | |
444 | (message "No timer set")))))) | |
c8d0cf5c | 445 | |
a2a2e7fb CD |
446 | (provide 'org-timer) |
447 | ||
bdebdb64 BG |
448 | ;; Local variables: |
449 | ;; generated-autoload-file: "org-loaddefs.el" | |
450 | ;; End: | |
451 | ||
bc23baaa | 452 | ;;; org-timer.el ends here |