From 215cec3786be27b5752a55dfd21c6024ecda4f20 Mon Sep 17 00:00:00 2001
From: John Wiegley <johnw@newartisans.com>
Date: Tue, 13 Oct 2009 03:38:16 -0400
Subject: [PATCH] Added "clock resolution" logic

Allows the user to find and resolve dangling clock entries, as well as
notify them if they have been idle while clocked in.

There are several ways to resolve an open clock (whether dangling or
idle):

  k - Keep X minutes of the time, default being all of the away time
      (for dangling entries, the away time is computed from the last
      time the org file was saved)
  K - Keep X minutes, then immediately clock out
  s - Subtract all of the idle/away minutes and resume clock from now
  S - Subtract all of the idle/away minutes, and clock out
  C - Cancel the clock

Note that some of these keys work without affecting the current clock,
unless the entry being operated on happens to be the current clock.
This means if you clocked into a different task, ran M-x
org-resolve-clocks and found a dangling entry, you could resolve it
using S without interrupting the current clock.  If you resolved with k
or s, however, it would clock you out of the active clock, and back into
the one being resolved, since those keys assume a desire to resume the
task that was orphaned.
---
 doc/org.texi      |   69 +++++++++++-
 lisp/org-clock.el |  334 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 lisp/org.el       |    4 +-
 3 files changed, 399 insertions(+), 8 deletions(-)

diff --git a/doc/org.texi b/doc/org.texi
index 538aba8..e2aaa4e 100644
--- a/doc/org.texi
+++ b/doc/org.texi
@@ -231,6 +231,7 @@ Dates and Times
 * Creating timestamps::         Commands which insert timestamps
 * Deadlines and scheduling::    Planning your work
 * Clocking work time::          Tracking how long you spend on a task
+* Resolving idle time::         
 * Effort estimates::            Planning work effort in advance
 * Relative timer::              Notes with a running timer
 
@@ -4703,6 +4704,7 @@ is used in a much wider sense.
 * Creating timestamps::         Commands which insert timestamps
 * Deadlines and scheduling::    Planning your work
 * Clocking work time::          Tracking how long you spend on a task
+* Resolving idle time::         
 * Effort estimates::            Planning work effort in advance
 * Relative timer::              Notes with a running timer
 @end menu
@@ -5220,7 +5222,7 @@ subtree, with dates shifted in each copy.  The command @kbd{C-c C-x c} was
 created for this purpose, it is described in @ref{Structure editing}.
 
 
-@node Clocking work time, Effort estimates, Deadlines and scheduling, Dates and Times
+@node Clocking work time, Resolving idle time, Deadlines and scheduling, Dates and Times
 @section Clocking work time
 
 Org mode allows you to clock the time you spend on specific tasks in a
@@ -5405,7 +5407,70 @@ The @kbd{l} key may be used in the timeline (@pxref{Timeline}) and in
 the agenda (@pxref{Weekly/daily agenda}) to show which tasks have been
 worked on or closed during a day.
 
-@node Effort estimates, Relative timer, Clocking work time, Dates and Times
+@node Resolving idle time, Effort estimates, Clocking work time, Dates and Times
+@section Resolving idle time
+@cindex resolve idle time
+
+@cindex idle, resolve, dangling
+If you clock in on a work item, and then walk away from your
+computer---perhaps to take a phone call---you often need to ``resolve'' the
+time you were away by either subtracting it from the current clock, or
+applying it to another one.
+
+@vindex org-clock-idle-time
+By customizing the variable @code{org-clock-idle-time} to some integer, such
+as 10 or 15, Emacs can alert you when you get back to your computer after
+being idle for that many minutes, and ask what you want to do with the idle
+time.  There will be a question waiting for you when you get back, indicating
+how much idle time has passed (constantly updated with the current amount),
+as well as a set of choices to correct the discrepancy:
+
+@table @kbd
+@item k
+To keep some or all of the minutes and stay clocked in, press @key{k}.  Org
+will ask how many of the minutes to keep.  Press @key{RET} to keep them all,
+effectively changing nothing, or enter a number to keep that many minutes.
+@item K
+If you use the shift key and press @key{K}, it will keep however many minutes
+you request and then immediately clock out of that task.  If you keep all of
+the minutes, this is the same as just clocking out of the current task.
+@item s
+To keep none of the minutes, use @key{s} to subtract all the away time from
+the clock, and then check back in from the moment you returned.
+@item S
+To keep none of the minutes and just clock out at the start of the away time,
+use the shift key and press @key{S}.  Remember that using shift will always
+leave you clocked out, no matter which option you choose.
+@item C
+To cancel the clock altogether, use @key{C}.  Note that if instead of
+cancelling you subtract the away time, and the resulting clock amount is less
+than a minute, the clock will still be cancelled rather than clutter up the
+log with an empty entry.
+@end table
+
+What if you subtracted those away minutes from the current clock, and now
+want to apply them to a new clock?  Simply clock in to any task immediately
+after the subtraction.  Org will notice that you have subtracted time ``on
+the books'', so to speak, and will ask if you want to apply those minutes to
+the next task you clock in on.
+
+There is one other instance when this clock resolution magic occurs.  Say you
+were clocked in and hacking away, and suddenly your cat chased a mouse who
+scared a hamster that crashed into your UPS's power button!  You suddenly
+lose all your buffers, but thanks to auto-save you still have your recent Org
+mode changes, including your last clock in.
+
+If you restart Emacs and clock into any task, Org will notice that you have a
+dangling clock which was never clocked out from your last session.  Using the
+Org file's last modified time as the beginning of the ``away'' period, Org
+will ask how you want to resolve that unaccounted-for time.  The logic and
+behavior is identical to dealing with away time due to idleness, it's just
+happening due to a recovery event rather than a set amount of idle time.
+
+You can also check all the files visited by your Org agenda for dangling
+clocks at any time using @kbd{M-x org-resolve-clocks}.
+
+@node Effort estimates, Relative timer, Resolving idle time, Dates and Times
 @section Effort estimates
 @cindex effort estimates
 
diff --git a/lisp/org-clock.el b/lisp/org-clock.el
index 97e5552..e00ecf0 100644
--- a/lisp/org-clock.el
+++ b/lisp/org-clock.el
@@ -228,10 +228,14 @@ to add an effort property.")
 (put 'org-mode-line-string 'risky-local-variable t)
 
 (defvar org-clock-mode-line-timer nil)
+(defvar org-clock-idle-timer nil)
 (defvar org-clock-heading "")
 (defvar org-clock-heading-for-remember "")
 (defvar org-clock-start-time "")
 
+(defvar org-clock-left-over-time ""
+  "If non-nil, user cancelled a clock; this is when leftover time started.")
+
 (defvar org-clock-effort ""
   "Effort estimate of the currently clocking task")
 
@@ -495,6 +499,294 @@ Use alsa's aplay tool if available."
 (defvar org-clock-mode-line-entry nil
   "Information for the modeline about the running clock.")
 
+(defun org-find-open-clocks (file)
+  "Search through the given file and find all open clocks."
+  (let ((buf (or (get-file-buffer file)
+		 (find-file-noselect file)))
+	clocks)
+    (with-current-buffer buf
+      (save-excursion
+	(goto-char (point-min))
+	(while (re-search-forward "CLOCK: \\(\\[.*?\\]\\)$" nil t)
+	  (push (cons (copy-marker (1- (match-end 1)) t)
+		      (org-time-string-to-time (match-string 1))) clocks))))
+    clocks))
+
+(defsubst org-is-active-clock (clock)
+  "Return t if CLOCK is the currently active clock."
+  (and (org-clock-is-active)
+       (= org-clock-marker (car clock))))
+
+(defmacro org-with-clock-position (clock &rest forms)
+  "Evaluate FORMS with CLOCK as the current active clock."
+  `(with-current-buffer (marker-buffer (car ,clock))
+     (save-excursion
+       (save-restriction
+	 (widen)
+	 (goto-char (car ,clock))
+	 (beginning-of-line)
+	 ,@forms))))
+
+(put 'org-with-clock-position 'lisp-indent-function 1)
+
+(defmacro org-with-clock (clock &rest forms)
+  "Evaluate FORMS with CLOCK as the current active clock.
+This macro also protects the current active clock from being altered."
+  `(org-with-clock-position ,clock
+     (let ((org-clock-start-time (cdr ,clock))
+	   (org-clock-total-time)
+	   (org-clock-history)
+	   (org-clock-effort)
+	   (org-clock-marker (car ,clock))
+	   (org-clock-hd-marker (save-excursion
+				  (outline-back-to-heading t)
+				  (point-marker))))
+       ,@forms)))
+
+(put 'org-with-clock 'lisp-indent-function 1)
+
+(defsubst org-clock-clock-in (clock &optional resume)
+  "Clock in to the clock located by CLOCK.
+If necessary, clock-out of the currently active clock."
+  (org-with-clock-position clock
+    (let ((org-clock-in-resume (or resume org-clock-in-resume)))
+      (org-clock-in))))
+
+(defsubst org-clock-clock-out (clock &optional fail-quietly at-time)
+  "Clock out of the clock located by CLOCK."
+  (let ((temp (copy-marker (car clock)
+			   (marker-insertion-type (car clock)))))
+    (if (org-is-active-clock clock)
+	(org-clock-out fail-quietly at-time)
+      (org-with-clock clock
+	(org-clock-out fail-quietly at-time)))
+    (setcar clock temp)))
+
+(defsubst org-clock-clock-cancel (clock)
+  "Cancel the clock located by CLOCK."
+  (let ((temp (copy-marker (car clock)
+			   (marker-insertion-type (car clock)))))
+    (if (org-is-active-clock clock)
+	(org-clock-cancel)
+      (org-with-clock clock
+	(org-clock-cancel)))
+    (setcar clock temp)))
+
+(defun org-clock-resolve-clock (clock resolve-to last-valid &optional
+				      close-p restart-p fail-quietly)
+  "Resolve `CLOCK' given the time `RESOLVE-TO', and the present.
+`CLOCK' is a cons cell of the form (MARKER START-TIME).
+This routine can do one of many things:
+
+  if `RESOLVE-TO' is nil
+    if `CLOSE-P' is non-nil, give an error
+    if this clock is the active clock, cancel it
+    else delete the clock line (as if it never happened)
+    if `RESTART-P' is non-nil, start a new clock
+
+  else if `RESOLVE-TO' is the symbol `now'
+    if `RESTART-P' is non-nil, give an error
+    if `CLOSE-P' is non-nil, clock out the entry and
+       if this clock is the active clock, stop it
+    else if this clock is the active clock, do nothing
+    else if there is no active clock, resume this clock
+    else ask to cancel the active clock, and if so,
+         resume this clock after cancelling it
+
+  else if `RESOLVE-TO' is some date in the future
+    give an error about `RESOLVE-TO' being invalid
+
+  else if `RESOLVE-TO' is some date in the past
+    if `RESTART-P' is non-nil, give an error
+    if `CLOSE-P' is non-nil, enter a closing time and
+       if this clock is the active clock, stop it
+    else if this clock is the active clock, enter a
+       closing time, stop the current clock, then
+       start a new clock for the same item
+    else just enter a closing time for this clock
+       and then start a new clock for the same item"
+  (cond
+   ((null resolve-to)
+    (org-clock-clock-cancel clock)
+    (if restart-p
+	(org-clock-clock-in clock)))
+
+   ((eq resolve-to 'now)
+    (if restart-p
+	(error "RESTART-P is not valid here"))
+    (if close-p
+	(org-clock-clock-out clock fail-quietly)
+      (unless (org-is-active-clock clock)
+	(org-clock-clock-in clock t))))
+
+   ((not (time-less-p resolve-to (current-time)))
+    (error "RESOLVE-TO must refer to a time in the past"))
+
+   (t
+    (if restart-p
+	(error "RESTART-P is not valid here"))
+    (if close-p
+	(progn
+	  (org-clock-clock-out clock fail-quietly resolve-to)
+	  (setq org-clock-left-over-time last-valid))
+      (org-clock-clock-out clock fail-quietly resolve-to)
+      (org-clock-clock-in clock)))))
+
+(defun org-clock-resolve (clock &optional prompt-fn last-valid fail-quietly)
+  "Resolve an open org-mode clock.
+An open clock was found, with `dangling' possibly being non-nil.
+If this function was invoked with a prefix argument, non-dangling
+open clocks are ignored.  The given clock requires some sort of
+user intervention to resolve it, either because a clock was left
+dangling or due to an idle timeout.  The clock resolution can
+either be:
+
+  (a) deleted, the user doesn't care about the clock
+  (b) restarted from the current time (if no other clock is open)
+  (c) closed, giving the clock X minutes
+  (d) closed and then restarted
+  (e) resumed, as if the user had never left
+
+The format of clock is (CONS MARKER START-TIME), where MARKER
+identifies the buffer and position the clock is open at (and
+thus, the heading it's under), and START-TIME is when the clock
+was started."
+  (assert clock)
+  (let* ((ch (progn
+	       (unless org-clock-resolving-clocks-due-to-idleness
+		 (org-with-clock clock
+		   (org-clock-goto))
+		 (with-current-buffer (marker-buffer (car clock))
+		   (goto-char (car clock))))
+	       (let (char-pressed)
+		 (while (null char-pressed)
+		   (setq char-pressed
+			 (read-char (concat (funcall prompt-fn clock)
+					    " [(kK)eep (sS)ubtract (C)ancel]? ")
+				    nil 45)))
+		 char-pressed)))
+	 (last-modified (unless last-valid
+			  (with-current-buffer (marker-buffer (car clock))
+			    (nth 5 (file-attributes (buffer-file-name))))))
+	 (default (floor (/ (time-to-seconds
+			     (time-subtract (current-time)
+					    (or last-valid
+						last-modified))) 60)))
+	 (keep (and (memq ch '(?k ?K))
+		    (read-number (format "Keep how many minutes (since %s)? "
+					 (if last-valid
+					     "idle" "org last saved"))
+				 default)))
+	 (subtractp (memq ch '(?s ?S)))
+	 (barely-started-p (< (- (time-to-seconds (or last-valid last-modified))
+				 (time-to-seconds (cdr clock))) 45))
+	 (start-over (and subtractp barely-started-p)))
+    (if (or (null ch)
+	    (not (memq ch '(?k ?K ?s ?S ?C))))
+	(message "")
+      (org-clock-resolve-clock
+       clock (cond
+	      ((or (eq ch ?C)
+		   ;; If the time on the clock was less than a minute before
+		   ;; the user went idle, and they've ask to subtract all the
+		   ;; time...
+		   start-over)
+	       nil)
+	      (subtractp
+	       (or last-valid last-modified))
+	      ((= keep default)
+	       'now)
+	      (t
+	       (time-add (or last-valid last-modified)
+			 (seconds-to-time (* 60 keep)))))
+       (or last-valid last-modified)
+       (memq ch '(?K ?S))
+       (and start-over (not (memq ch '(?K ?S))))
+       fail-quietly))))
+
+(defvar org-clock-resolving-clocks nil)
+(defvar org-clock-resolving-clocks-due-to-idleness nil)
+
+(defun org-resolve-clocks (&optional also-non-dangling-p
+				     prompt-fn last-valid)
+  "Resolve all currently open org-mode clocks.
+If `also-non-dangling-p' is non-nil, also ask to resolve
+non-dangling (i.e., currently open and valid) clocks."
+  (interactive "P")
+  (unless org-clock-resolving-clocks
+    (let ((org-clock-resolving-clocks t))
+      (dolist (file org-agenda-files)
+	(let ((clocks (org-find-open-clocks file)))
+	  (dolist (clock clocks)
+	    (let ((dangling (or (not (org-clock-is-active))
+				(/= (car clock) org-clock-marker))))
+	      (unless (and (not dangling) (not also-non-dangling-p))
+		(org-clock-resolve
+		 clock
+		 (or prompt-fn
+		     (function
+		      (lambda (clock)
+			(format
+			 "Dangling clock from %d mins ago"
+			 (floor
+			  (/ (- (time-to-seconds (current-time))
+				(time-to-seconds
+				 (with-current-buffer (marker-buffer (car clock))
+				   (nth 5 (file-attributes (buffer-file-name))))))
+			     60))))))
+		 last-valid)))))))))
+
+(defcustom org-clock-idle-time nil
+  "When non-nil, resolve open clocks if the user is idle more than X minutes."
+  :group 'org-clock
+  :type '(choice
+	  (const :tag "Never" nil)
+	  (integer :tag "After N minutes")))
+
+(defun org-emacs-idle-seconds ()
+  "Return the current Emacs idle time in seconds, or nil if not idle."
+  (let ((idle-time (current-idle-time)))
+    (if idle-time
+	(time-to-seconds idle-time)
+      0)))
+
+(defun org-mac-idle-seconds ()
+  "Return the current Mac idle time in seconds"
+  (string-to-number (shell-command-to-string "ioreg -c IOHIDSystem | perl -ane 'if (/Idle/) {$idle=(pop @F)/1000000000; print $idle; last}'")))
+
+(defun org-user-idle-seconds ()
+  "Return the number of seconds the user has been idle for.
+This routine returns a floating point number."
+  (if (eq system-type 'darwin)
+      (let ((emacs-idle (org-emacs-idle-seconds)))
+	;; If Emacs has been idle for longer than the user's
+	;; `org-clock-idle-time' value, check whether the whole system has
+	;; really been idle for that long.
+	(if (> emacs-idle (* 60 org-clock-idle-time))
+	    (min emacs-idle (org-mac-idle-seconds))
+	  emacs-idle))
+    (org-emacs-idle-seconds)))
+
+(defun org-resolve-clocks-if-idle ()
+  "Resolve all currently open org-mode clocks.
+This is performed after `org-clock-idle-time' minutes, to check
+if the user really wants to stay clocked in after being idle for
+so long."
+  (when (and org-clock-idle-time (not org-clock-resolving-clocks))
+    (let ((idle (org-user-idle-seconds))
+	  (org-clock-resolving-clocks-due-to-idleness t))
+      (if (> idle (* 60 org-clock-idle-time))
+	  (org-resolve-clocks
+	   t
+	   (function
+	    (lambda (clock)
+	      (format "Clocked in & idle for %d mins"
+		      (/ (org-user-idle-seconds) 60))))
+	   (time-subtract (current-time)
+			  (seconds-to-time (org-user-idle-seconds))))))))
+
+(defvar org-clock-clocking-in nil)
+
 (defun org-clock-in (&optional select)
   "Start the clock on the current item.
 If necessary, clock-out of the currently active clock.
@@ -505,8 +797,20 @@ the clocking selection, associated with the letter `d'."
   (interactive "P")
   (setq org-clock-notification-was-shown nil)
   (catch 'abort
-    (let ((interrupting (marker-buffer org-clock-marker))
-	  ts selected-task target-pos (msg-extra ""))
+    (let ((interrupting (and (not org-clock-resolving-clocks-due-to-idleness)
+			     (marker-buffer org-clock-marker)))
+	  ts selected-task target-pos (msg-extra "")
+	  (left-over (and (not org-clock-resolving-clocks)
+			  org-clock-left-over-time)))
+      (setq org-clock-left-over-time nil)
+      (unless org-clock-clocking-in
+	(let ((org-clock-clocking-in t))
+	  (org-resolve-clocks)	      ; first check if any clocks are dangling
+	  ;; If the resolution resulted in a new clock being started, then
+	  ;; abort this clock in.
+	  (if (and (not interrupting)
+		   (marker-buffer org-clock-marker))
+	      (throw 'abort nil))))
       (when (equal select '(4))
 	(setq selected-task (org-clock-select-task "Clock-in on task: "))
 	(if selected-task
@@ -604,7 +908,15 @@ the clocking selection, associated with the letter `d'."
 	      (setq org-clock-effort (org-get-effort))
 	      (setq org-clock-total-time (org-clock-sum-current-item
 					  (org-clock-get-sum-start)))
-	      (setq org-clock-start-time (current-time))
+	      (setq org-clock-start-time
+		    (or (and left-over
+			     (y-or-n-p
+			      (format
+			       "You stopped another clock %d mins ago; start this one from then? "
+			       (/ (- (time-to-seconds (current-time))
+				     (time-to-seconds left-over)) 60)))
+			     left-over)
+			(current-time)))
 	      (setq ts (org-insert-time-stamp org-clock-start-time
 					      'with-hm 'inactive))))
 	    (move-marker org-clock-marker (point) (buffer-base-buffer))
@@ -616,8 +928,16 @@ the clocking selection, associated with the letter `d'."
 		(setq global-mode-string
 		      (append global-mode-string '(org-mode-line-string))))
 	    (org-clock-update-mode-line)
+	    (when org-clock-mode-line-timer
+	      (cancel-timer org-clock-mode-line-timer)
+	      (setq org-clock-mode-line-timer nil))
 	    (setq org-clock-mode-line-timer
 		  (run-with-timer 60 60 'org-clock-update-mode-line))
+	    (when org-clock-idle-timer
+	      (cancel-timer org-clock-idle-timer)
+	      (setq org-clock-idle-timer nil))
+	    (setq org-clock-idle-timer
+		  (run-with-timer 60 60 'org-resolve-clocks-if-idle))
 	    (message "Clock starts at %s - %s" ts msg-extra)
 	    (run-hooks 'org-clock-in-hook)))))))
 
@@ -748,7 +1068,7 @@ line and position cursor in that line."
 	    (and (re-search-forward org-property-end-re nil t)
 		 (goto-char (match-beginning 0))))))))
 
-(defun org-clock-out (&optional fail-quietly)
+(defun org-clock-out (&optional fail-quietly at-time)
   "Stop the currently running clock.
 If there is no running clock, throw an error, unless FAIL-QUIETLY is set."
   (interactive)
@@ -769,7 +1089,8 @@ If there is no running clock, throw an error, unless FAIL-QUIETLY is set."
 	  (goto-char (match-end 0))
 	  (delete-region (point) (point-at-eol))
 	  (insert "--")
-	  (setq te (org-insert-time-stamp (current-time) 'with-hm 'inactive))
+	  (setq te (org-insert-time-stamp (or at-time (current-time))
+					  'with-hm 'inactive))
 	  (setq s (- (org-float-time (apply 'encode-time (org-parse-time-string te)))
 		     (org-float-time (apply 'encode-time (org-parse-time-string ts))))
 		h (floor (/ s 3600))
@@ -791,6 +1112,9 @@ If there is no running clock, throw an error, unless FAIL-QUIETLY is set."
 	  (when org-clock-mode-line-timer
 	    (cancel-timer org-clock-mode-line-timer)
 	    (setq org-clock-mode-line-timer nil))
+	  (when org-clock-idle-timer
+	    (cancel-timer org-clock-idle-timer)
+	    (setq org-clock-idle-timer nil))
 	  (setq global-mode-string
 		(delq 'org-mode-line-string global-mode-string))
 	  (when org-clock-out-switch-to-state
diff --git a/lisp/org.el b/lisp/org.el
index d3d886f..d895e2f 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -3141,6 +3141,8 @@ If TABLE-TYPE is non-nil, also check for table.el-type tables."
 (declare-function org-clock-save-markers-for-cut-and-paste "org-clock"
 		  (beg end))
 (declare-function org-clock-update-mode-line "org-clock" ())
+(declare-function org-resolve-clocks "org-clock"
+		  (&optional also-non-dangling-p prompt last-valid))
 (defvar org-clock-start-time)
 (defvar org-clock-marker (make-marker)
   "Marker recording the last clock-in.")
@@ -3158,7 +3160,7 @@ The return value is actually the clock marker."
 		  org-clock-goto org-clock-sum org-clock-display
 		  org-clock-remove-overlays org-clock-report
 		  org-clocktable-shift org-dblock-write:clocktable
-		  org-get-clocktable)))
+		  org-get-clocktable org-resolve-clocks)))
 
 (defun org-clock-update-time-maybe ()
   "If this is a CLOCK line, update it and return t.
-- 
1.6.5

