Browse Source

Added support for habit consistency tracking

org-habit.el: New file, which implements code to build a "habit
consistency graph".

org-agenda.el (org-agenda-get-deadlines)
(org-agenda-get-scheduled): Display consistency graphs when outputting
habits into the agenda.  The graphs are always relative to the current
time.

(org-format-agenda-item): Added new parameter `habitp', which indicates
whether we are formatting a habit or not.  Do not display "extra"
leading information if habitp is true.
John Wiegley 16 years ago
parent
commit
063cb58fab
7 changed files with 548 additions and 62 deletions
  1. 1 0
      Makefile
  2. 5 0
      doc/ChangeLog
  3. 100 1
      doc/org.texi
  4. 16 0
      lisp/ChangeLog
  5. 92 61
      lisp/org-agenda.el
  6. 333 0
      lisp/org-habit.el
  7. 1 0
      lisp/org.el

+ 1 - 0
Makefile

@@ -77,6 +77,7 @@ LISPF      = 	org.el			\
 		org-feed.el		\
 		org-feed.el		\
 		org-footnote.el		\
 		org-footnote.el		\
 		org-gnus.el		\
 		org-gnus.el		\
+		org-habit.el		\
 		org-html.el		\
 		org-html.el		\
 		org-icalendar.el	\
 		org-icalendar.el	\
 		org-id.el		\
 		org-id.el		\

+ 5 - 0
doc/ChangeLog

@@ -1,3 +1,8 @@
+2009-10-19  John Wiegley  <johnw@newartisans.com>
+
+	* org.texi (Tracking your habits): Added a new section in the
+	manual about how to track habits.
+
 2009-10-18  Carsten Dominik  <carsten.dominik@gmail.com>
 2009-10-18  Carsten Dominik  <carsten.dominik@gmail.com>
 
 
 	* org.texi (Pushing to MobileOrg): Mention that `org-directory'
 	* org.texi (Pushing to MobileOrg): Mention that `org-directory'

+ 100 - 1
doc/org.texi

@@ -198,6 +198,7 @@ Progress logging
 
 
 * Closing items::               When was this entry marked DONE?
 * Closing items::               When was this entry marked DONE?
 * Tracking TODO state changes::  When did the status change?
 * Tracking TODO state changes::  When did the status change?
+* Tracking your habits::        How consistent have you been?
 
 
 Tags
 Tags
 
 
@@ -3524,6 +3525,7 @@ work time}.
 @menu
 @menu
 * Closing items::               When was this entry marked DONE?
 * Closing items::               When was this entry marked DONE?
 * Tracking TODO state changes::  When did the status change?
 * Tracking TODO state changes::  When did the status change?
+* Tracking your habits::        How consistent have you been?
 @end menu
 @end menu
 
 
 @node Closing items, Tracking TODO state changes, Progress logging, Progress logging
 @node Closing items, Tracking TODO state changes, Progress logging, Progress logging
@@ -3558,7 +3560,7 @@ In the timeline (@pxref{Timeline}) and in the agenda
 display the TODO items with a @samp{CLOSED} timestamp on each day,
 display the TODO items with a @samp{CLOSED} timestamp on each day,
 giving you an overview of what has been done.
 giving you an overview of what has been done.
 
 
-@node Tracking TODO state changes,  , Closing items, Progress logging
+@node Tracking TODO state changes, Tracking your habits, Closing items, Progress logging
 @subsection Tracking TODO state changes
 @subsection Tracking TODO state changes
 @cindex drawer, for state change recording
 @cindex drawer, for state change recording
 
 
@@ -3635,6 +3637,103 @@ settings like @code{TODO(!)}.  For example
   :END:
   :END:
 @end example
 @end example
 
 
+@node Tracking your habits,  , Tracking TODO state changes, Progress logging
+@subsection Tracking your habits
+@cindex habits
+
+Org has the ability to track the consistency of a special category of TODOs,
+called ``habits''.  A habit has the following properties:
+
+@enumerate
+@item
+The habit is a TODO, with a TODO keyword that represents an open state.
+@item
+The property @code{STYLE} is set to the value @code{habit}.
+@item
+The TODO has a scheduled date, with a @code{.+} style repeat interval.
+@item
+The TODO may also have a deadline, as long as it also has a @code{.+} style
+repeat interval and it starts a number of days after the scheduled date equal
+to the difference between the repeat interval lengths@footnote{Note that if
+you don't set this right, Org will alert you as to what's incorrect about the
+habit definition.}.
+@item
+You must also have state logging for the @code{DONE} state enabled, in order
+for historical data to be represented in the consistency graph.  If it's not
+enabled it's not an error, but the consistency graphs will be largely
+meaningless.
+@end enumerate
+
+To give you an idea of what the above rules look like in action, here's an
+actual habit with some history:
+
+@example
+** TODO Shave
+   SCHEDULED: <2009-10-17 Sat .+2d> DEADLINE: <2009-10-19 Mon .+4d>
+   - State "DONE"       from "TODO"       [2009-10-15 Thu]
+   - State "DONE"       from "TODO"       [2009-10-12 Mon]
+   - State "DONE"       from "TODO"       [2009-10-10 Sat]
+   - State "DONE"       from "TODO"       [2009-10-04 Sun]
+   - State "DONE"       from "TODO"       [2009-10-02 Fri]
+   - State "DONE"       from "TODO"       [2009-09-29 Tue]
+   - State "DONE"       from "TODO"       [2009-09-25 Fri]
+   - State "DONE"       from "TODO"       [2009-09-19 Sat]
+   - State "DONE"       from "TODO"       [2009-09-16 Wed]
+   - State "DONE"       from "TODO"       [2009-09-12 Sat]
+   :PROPERTIES:
+   :STYLE:    habit
+   :LAST_REPEAT: [2009-10-19 Mon 00:36]
+   :END:
+@end example
+
+What this habit says is: I want to shave at most every 2 days (given by the
+@code{SCHEDULED} date and repeat interval) at least every 4 days (given by
+the @code{DEADLINE} date and repeat interval).  If today is the 15th, then
+the habit first appears in the agenda on Oct 17, after the minimum of 2 days
+has elapsed, and will appear overdue on Oct 19, after four days have elapsed.
+
+What's really useful about habits is that they are displayed along with a
+conistency graph, to show how consistent you've been at getting that task
+done in the past.  This graph shows every day that the task was done over the
+past three weeks, with colors for each day.  The colors used are:
+
+@table @code
+@item Blue
+If the task wasn't to be done yet on that day.
+@item Green
+If the task could have been done on that day.
+@item Yellow
+If the task was going to be overdue the next day.
+@item Red
+If the task was overdue on that day.
+@end table
+
+In addition to coloring each day, the day is also marked with an asterix if
+the task was actually done that day, and an exclamation mark to show where
+the current day falls in the graph.
+
+There are several configuration variables that can be used to change the way
+habits are displayed in the agenda.
+
+@table @code
+@item org-habit-graph-column
+The buffer column at which the consistency graph should be drawn.  This will
+overwite any text in that column, so it's a good idea to keep your habits'
+titles brief and to the point.
+@item org-habit-preceding-days
+The amount of history, in days before today, to appear in consistency graphs.
+@item org-habit-following-days
+The number of days after today that will appear in consistency graphs.
+@item org-habit-show-habits-only-for-today
+If non-nil, only show habits in today's agenda view.  This is set to true by
+default.
+@end table
+
+Lastly, pressing @kbd{K} in the agenda buffer will cause habits to
+temporarily be disabled and they won't appear at all.  Press @kbd{K} again to
+bring them back.  They are also subject to tag filtering, if you have habits
+which should only be done in certain contexts, for example.
+
 @node Priorities, Breaking down tasks, Progress logging, TODO Items
 @node Priorities, Breaking down tasks, Progress logging, TODO Items
 @section Priorities
 @section Priorities
 @cindex priorities
 @cindex priorities

+ 16 - 0
lisp/ChangeLog

@@ -1,3 +1,8 @@
+2009-10-20  John Wiegley  <johnw@newartisans.com>
+
+	* org-agenda.el (org-finalize-agenda): Draw habit consistency
+	graphs after everything else in the buffer has been setup.
+
 2009-10-19  Carsten Dominik  <carsten.dominik@gmail.com>
 2009-10-19  Carsten Dominik  <carsten.dominik@gmail.com>
 
 
 	* org-mobile.el (org-mobile-apply): Count success and failure.
 	* org-mobile.el (org-mobile-apply): Count success and failure.
@@ -15,6 +20,17 @@
 
 
 2009-10-19  John Wiegley  <johnw@newartisans.com>
 2009-10-19  John Wiegley  <johnw@newartisans.com>
 
 
+	* org-habit.el: New file, which implements code to build a "habit
+	consistency graph".
+
+	* org-agenda.el (org-agenda-get-deadlines)
+	(org-agenda-get-scheduled): Display consistency graphs when
+	outputting habits into the agenda.  The graphs are always relative
+	to the current time.
+	(org-format-agenda-item): Added new parameter `habitp', which
+	indicates whether we are formatting a habit or not.  Do not
+	display "extra" leading information if habitp is true.
+
 	* org.el (org-repeat-re): Improved regexp to include .+ and ++
 	* org.el (org-repeat-re): Improved regexp to include .+ and ++
 	leaders for repeat strings.
 	leaders for repeat strings.
 	(org-get-repeat): Now takes a string parameter `tagline', so the
 	(org-get-repeat): Now takes a string parameter `tagline', so the

+ 92 - 61
lisp/org-agenda.el

@@ -201,6 +201,7 @@ you can \"misuse\" it to also add other text to the header.  However,
     (const priority-up) (const priority-down)
     (const priority-up) (const priority-down)
     (const todo-state-up) (const todo-state-down)
     (const todo-state-up) (const todo-state-down)
     (const effort-up) (const effort-down)
     (const effort-up) (const effort-down)
+    (const habit-up) (const habit-down)
     (const user-defined-up) (const user-defined-down))
     (const user-defined-up) (const user-defined-down))
   "Sorting choices.")
   "Sorting choices.")
 
 
@@ -950,9 +951,9 @@ a grid line."
   :group 'org-agenda)
   :group 'org-agenda)
 
 
 (defcustom org-agenda-sorting-strategy
 (defcustom org-agenda-sorting-strategy
-  '((agenda time-up        priority-down category-keep)
-    (todo   priority-down  category-keep)
-    (tags   priority-down  category-keep)
+  '((agenda habit-down time-up priority-down category-keep)
+    (todo   priority-down category-keep)
+    (tags   priority-down category-keep)
     (search category-keep))
     (search category-keep))
   "Sorting structure for the agenda items of a single day.
   "Sorting structure for the agenda items of a single day.
 This is a list of symbols which will be used in sequence to determine
 This is a list of symbols which will be used in sequence to determine
@@ -975,6 +976,8 @@ effort-up          Sort numerically by estimated effort, high effort last.
 effort-down        Sort numerically by estimated effort, high effort first.
 effort-down        Sort numerically by estimated effort, high effort first.
 user-defined-up    Sort according to `org-agenda-cmp-user-defined', high last.
 user-defined-up    Sort according to `org-agenda-cmp-user-defined', high last.
 user-defined-down  Sort according to `org-agenda-cmp-user-defined', high first.
 user-defined-down  Sort according to `org-agenda-cmp-user-defined', high first.
+habit-up           Put entries that are habits first
+habit-down         Put entries that are habits last
 
 
 The different possibilities will be tried in sequence, and testing stops
 The different possibilities will be tried in sequence, and testing stops
 if one comparison returns a \"not-equal\".  For example, the default
 if one comparison returns a \"not-equal\".  For example, the default
@@ -2600,6 +2603,8 @@ bind it in the options section.")
       (when org-agenda-entry-text-mode
       (when org-agenda-entry-text-mode
 	(org-agenda-entry-text-hide)
 	(org-agenda-entry-text-hide)
 	(org-agenda-entry-text-show))
 	(org-agenda-entry-text-show))
+      (if (functionp 'org-habit-insert-consistency-graphs)
+	  (org-habit-insert-consistency-graphs))
       (run-hooks 'org-finalize-agenda-hook)
       (run-hooks 'org-finalize-agenda-hook)
       (setq org-agenda-type (org-get-at-bol 'org-agenda-type))
       (setq org-agenda-type (org-get-at-bol 'org-agenda-type))
       (when (get 'org-agenda-filter :preset-filter)
       (when (get 'org-agenda-filter :preset-filter)
@@ -4214,7 +4219,7 @@ the documentation of `org-diary'."
 	 (regexp org-deadline-time-regexp)
 	 (regexp org-deadline-time-regexp)
 	 (todayp (org-agenda-todayp date)) ; DATE bound by calendar
 	 (todayp (org-agenda-todayp date)) ; DATE bound by calendar
 	 (d1 (calendar-absolute-from-gregorian date))  ; DATE bound by calendar
 	 (d1 (calendar-absolute-from-gregorian date))  ; DATE bound by calendar
-	 d2 diff dfrac wdays pos pos1 category tags
+	 d2 diff dfrac wdays pos pos1 category tags habitp
 	 ee txt head face s todo-state upcomingp donep timestr)
 	 ee txt head face s todo-state upcomingp donep timestr)
     (goto-char (point-min))
     (goto-char (point-min))
     (while (re-search-forward regexp nil t)
     (while (re-search-forward regexp nil t)
@@ -4227,15 +4232,22 @@ the documentation of `org-diary'."
 		  (match-string 1) d1 'past
 		  (match-string 1) d1 'past
 		  org-agenda-repeating-timestamp-show-all)
 		  org-agenda-repeating-timestamp-show-all)
 	      diff (- d2 d1)
 	      diff (- d2 d1)
+	      ;; Never show habits as deadline entries, only as scheduled
+	      ;; entries.  The habit code already requires that every habit
+	      ;; have a scheduled date if it has a deadline, and that the
+	      ;; scheduled date is prior to the deadline.
+	      habitp (and (functionp 'org-is-habit-p)
+			  (org-is-habit-p))
 	      wdays (org-get-wdays s)
 	      wdays (org-get-wdays s)
 	      dfrac (/ (* 1.0 (- wdays diff)) (max wdays 1))
 	      dfrac (/ (* 1.0 (- wdays diff)) (max wdays 1))
 	      upcomingp (and todayp (> diff 0)))
 	      upcomingp (and todayp (> diff 0)))
 	;; When to show a deadline in the calendar:
 	;; When to show a deadline in the calendar:
 	;; If the expiration is within wdays warning time.
 	;; If the expiration is within wdays warning time.
 	;; Past-due deadlines are only shown on the current date
 	;; Past-due deadlines are only shown on the current date
-	(if (or (and (<= diff wdays)
-		     (and todayp (not org-agenda-only-exact-dates)))
-		(= diff 0))
+	(if (and (or (and (<= diff wdays)
+			  (and todayp (not org-agenda-only-exact-dates)))
+		     (= diff 0))
+		 (not habitp))
 	    (save-excursion
 	    (save-excursion
 	      (setq todo-state (org-get-todo-state))
 	      (setq todo-state (org-get-todo-state))
 	      (setq donep (member todo-state org-done-keywords))
 	      (setq donep (member todo-state org-done-keywords))
@@ -4311,11 +4323,11 @@ FRACTION is what fraction of the head-warning time has passed."
 	 mm
 	 mm
 	 (deadline-position-alist
 	 (deadline-position-alist
 	  (mapcar (lambda (a) (and (setq mm (get-text-property
 	  (mapcar (lambda (a) (and (setq mm (get-text-property
-					     0 'org-hd-marker a))
-				   (cons (marker-position mm) a)))
+					0 'org-hd-marker a))
+			      (cons (marker-position mm) a)))
 		  deadline-results))
 		  deadline-results))
 	 d2 diff pos pos1 category tags donep
 	 d2 diff pos pos1 category tags donep
-	 ee txt head pastschedp todo-state face timestr s)
+	 ee txt head pastschedp todo-state face timestr s habitp)
     (goto-char (point-min))
     (goto-char (point-min))
     (while (re-search-forward regexp nil t)
     (while (re-search-forward regexp nil t)
       (catch :skip
       (catch :skip
@@ -4330,60 +4342,69 @@ FRACTION is what fraction of the head-warning time has passed."
 	(setq pastschedp (and todayp (< diff 0)))
 	(setq pastschedp (and todayp (< diff 0)))
 	;; When to show a scheduled item in the calendar:
 	;; When to show a scheduled item in the calendar:
 	;; If it is on or past the date.
 	;; If it is on or past the date.
-	(if (or (and (< diff 0)
-		     (< (abs diff) org-scheduled-past-days)
-		     (and todayp (not org-agenda-only-exact-dates)))
-		(= diff 0))
-	    (save-excursion
-	      (setq todo-state (org-get-todo-state))
-	      (setq donep (member todo-state org-done-keywords))
-	      (if (and donep
-		       (or org-agenda-skip-scheduled-if-done
-			   (not (= diff 0))))
-		  (setq txt nil)
-		(setq category (org-get-category))
-		(if (not (re-search-backward "^\\*+[ \t]+" nil t))
-		    (setq txt org-agenda-no-heading-message)
-		  (goto-char (match-end 0))
-		  (setq pos1 (match-beginning 0))
+	(when (or (and (< diff 0)
+		       (< (abs diff) org-scheduled-past-days)
+		       (and todayp (not org-agenda-only-exact-dates)))
+		  (= diff 0))
+	  (save-excursion
+	    (setq todo-state (org-get-todo-state))
+	    (setq donep (member todo-state org-done-keywords))
+	    (setq habitp (and (functionp 'org-is-habit-p)
+			      (org-is-habit-p)))
+	    (if (and donep
+		     (or habitp org-agenda-skip-scheduled-if-done
+			 (not (= diff 0))))
+		(setq txt nil)
+	      (setq category (org-get-category))
+	      (if (not (re-search-backward "^\\*+[ \t]+" nil t))
+		  (setq txt org-agenda-no-heading-message)
+		(goto-char (match-end 0))
+		(setq pos1 (match-beginning 0))
+		(if habitp
+		    (if (or (not org-habit-show-habits)
+			    (and (not todayp)
+				 org-habit-show-habits-only-for-today))
+			(throw :skip nil))
 		  (if (and
 		  (if (and
 		       (or (eq t org-agenda-skip-scheduled-if-deadline-is-shown)
 		       (or (eq t org-agenda-skip-scheduled-if-deadline-is-shown)
 			   (and org-agenda-skip-scheduled-if-deadline-is-shown
 			   (and org-agenda-skip-scheduled-if-deadline-is-shown
 				pastschedp))
 				pastschedp))
 		       (setq mm (assoc pos1 deadline-position-alist)))
 		       (setq mm (assoc pos1 deadline-position-alist)))
-		      (throw :skip nil))
-		  (setq tags (org-get-tags-at))
-		  (setq head (buffer-substring-no-properties
-			      (point)
-			      (progn (skip-chars-forward "^\r\n") (point))))
-		  (if (string-match " \\([012]?[0-9]:[0-9][0-9]\\)" s)
-		      (setq timestr
-			    (concat (substring s (match-beginning 1)) " "))
-		    (setq timestr 'time))
-		  (setq txt (org-format-agenda-item
-			     (if (= diff 0)
-				 (car org-agenda-scheduled-leaders)
-			       (format (nth 1 org-agenda-scheduled-leaders)
-				       (- 1 diff)))
-			     head category tags
-			     (if (not (= diff 0)) nil timestr)))))
-	      (when txt
-		(setq face
-		      (cond
-		       (pastschedp 'org-scheduled-previously)
-		       (todayp 'org-scheduled-today)
-		       (t 'org-scheduled)))
-		(org-add-props txt props
-		  'undone-face face
-		  'face (if donep 'org-agenda-done face)
-		  'org-marker (org-agenda-new-marker pos)
-		  'org-hd-marker (org-agenda-new-marker pos1)
-		  'type (if pastschedp "past-scheduled" "scheduled")
-		  'date (if pastschedp d2 date)
-		  'priority (+ 94 (- 5 diff) (org-get-priority txt))
-		  'org-category category
-		  'todo-state todo-state)
-		(push txt ee))))))
+		      (throw :skip nil)))
+		(setq tags (org-get-tags-at))
+		(setq head (buffer-substring-no-properties
+			    (point)
+			    (progn (skip-chars-forward "^\r\n") (point))))
+		(if (string-match " \\([012]?[0-9]:[0-9][0-9]\\)" s)
+		    (setq timestr
+			  (concat (substring s (match-beginning 1)) " "))
+		  (setq timestr 'time))
+		(setq txt (org-format-agenda-item
+			   (if (= diff 0)
+			       (car org-agenda-scheduled-leaders)
+			     (format (nth 1 org-agenda-scheduled-leaders)
+				     (- 1 diff)))
+			   head category tags
+			   (if (not (= diff 0)) nil timestr)
+			   nil nil habitp))))
+	    (when txt
+	      (setq face
+		    (cond
+		     (pastschedp 'org-scheduled-previously)
+		     (todayp 'org-scheduled-today)
+		     (t 'org-scheduled)))
+	      (org-add-props txt props
+		'undone-face face
+		'face (if donep 'org-agenda-done face)
+		'org-marker (org-agenda-new-marker pos)
+		'org-hd-marker (org-agenda-new-marker pos1)
+		'type (if pastschedp "past-scheduled" "scheduled")
+		'date (if pastschedp d2 date)
+		'priority (+ 94 (- 5 diff) (org-get-priority txt))
+		'org-category category
+		'org-habit-p (and habitp (org-habit-parse-todo))
+		'todo-state todo-state)
+	      (push txt ee))))))
     (nreverse ee)))
     (nreverse ee)))
 
 
 (defun org-agenda-get-blocks ()
 (defun org-agenda-get-blocks ()
@@ -4462,7 +4483,7 @@ The flag is set if the currently compiled format contains a `%e'.")
   "Used by `org-compile-prefix-format' to remember the category field widh.")
   "Used by `org-compile-prefix-format' to remember the category field widh.")
 
 
 (defun org-format-agenda-item (extra txt &optional category tags dotime
 (defun org-format-agenda-item (extra txt &optional category tags dotime
-				     noprefix remove-re)
+				     noprefix remove-re habitp)
   "Format TXT to be inserted into the agenda buffer.
   "Format TXT to be inserted into the agenda buffer.
 In particular, it adds the prefix and corresponding text properties.  EXTRA
 In particular, it adds the prefix and corresponding text properties.  EXTRA
 must be a string and replaces the `%s' specifier in the prefix format.
 must be a string and replaces the `%s' specifier in the prefix format.
@@ -4575,7 +4596,7 @@ Any match of REMOVE-RE will be removed from TXT."
 	(setq time (cond (s2 (concat s1 "-" s2))
 	(setq time (cond (s2 (concat s1 "-" s2))
 			 (s1 (concat s1 "......"))
 			 (s1 (concat s1 "......"))
 			 (t ""))
 			 (t ""))
-	      extra (or extra "")
+	      extra (or (and (not habitp) extra) "")
 	      category (if (symbolp category) (symbol-name category) category)
 	      category (if (symbolp category) (symbol-name category) category)
 	      thecategory (copy-sequence category))
 	      thecategory (copy-sequence category))
 	(if (string-match org-bracket-link-regexp category)
 	(if (string-match org-bracket-link-regexp category)
@@ -4858,6 +4879,14 @@ HH:MM."
 	  ((< tb ta) +1)
 	  ((< tb ta) +1)
 	  (t nil))))
 	  (t nil))))
 
 
+(defsubst org-cmp-habit-p (a b)
+  "Compare the todo states of strings A and B."
+  (let ((ha (get-text-property 1 'org-habit-p a))
+	(hb (get-text-property 1 'org-habit-p b)))
+    (cond ((and ha (not hb)) -1)
+	  ((and (not ha) hb) +1)
+	  (t nil))))
+
 (defun org-entries-lessp (a b)
 (defun org-entries-lessp (a b)
   "Predicate for sorting agenda entries."
   "Predicate for sorting agenda entries."
   ;; The following variables will be used when the form is evaluated.
   ;; The following variables will be used when the form is evaluated.
@@ -4875,6 +4904,8 @@ HH:MM."
 	 (tag-down (if tag-up (- tag-up) nil))
 	 (tag-down (if tag-up (- tag-up) nil))
 	 (todo-state-up (org-cmp-todo-state a b))
 	 (todo-state-up (org-cmp-todo-state a b))
 	 (todo-state-down (if todo-state-up (- todo-state-up) nil))
 	 (todo-state-down (if todo-state-up (- todo-state-up) nil))
+	 (habit-up (org-cmp-habit-p a b))
+	 (habit-down (if habit-up (- habit-up) nil))
 	 user-defined-up user-defined-down)
 	 user-defined-up user-defined-down)
     (if (and org-agenda-cmp-user-defined
     (if (and org-agenda-cmp-user-defined
 	     (functionp org-agenda-cmp-user-defined))
 	     (functionp org-agenda-cmp-user-defined))

+ 333 - 0
lisp/org-habit.el

@@ -0,0 +1,333 @@
+;;; org-habit.el --- The habit tracking code for Org-mode
+
+;; Copyright (C) 2009
+;;   Free Software Foundation, Inc.
+
+;; Author: John Wiegley <johnw at gnu dot org>
+;; Keywords: outlines, hypermedia, calendar, wp
+;; Homepage: http://orgmode.org
+;; Version: 6.31trans
+;;
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;;; Commentary:
+
+;; This file contains the habit tracking code for Org-mode
+
+(require 'org)
+(require 'org-agenda)
+(eval-when-compile
+  (require 'cl)
+  (require 'calendar))
+
+(defgroup org-habit nil
+  "Options concerning habit tracking in Org-mode."
+  :tag "Org Habit"
+  :group 'org-progress)
+
+(defcustom org-habit-graph-column 40
+  "The absolute column at which to insert habit consistency graphs.
+Note that consistency graphs will overwrite anything else in the buffer."
+  :group 'org-habit
+  :type 'integer)
+
+(defcustom org-habit-preceding-days 21
+  "Number of days before today to appear in consistency graphs."
+  :group 'org-habit
+  :type 'integer)
+
+(defcustom org-habit-following-days 7
+  "Number of days after today to appear in consistency graphs."
+  :group 'org-habit
+  :type 'integer)
+
+(defcustom org-habit-show-habits t
+  "If non-nil, show habits in agenda buffers."
+  :group 'org-habit
+  :type 'boolean)
+
+(defcustom org-habit-show-habits-only-for-today t
+  "If non-nil, only show habits on today's agenda, and not for future days.
+Note that even when shown for future days, the graph is always
+relative to the current effective time."
+  :group 'org-habit
+  :type 'boolean)
+
+(defcustom org-habit-clear-color "slateblue"
+  "Color for days on which a task shouldn't be done yet."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+(defcustom org-habit-clear-future-color "powderblue"
+  "Color for future days on which a task shouldn't be done yet."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+
+(defcustom org-habit-ready-color "green"
+  "Color for days on which a task should start to be done."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+(defcustom org-habit-ready-future-color "palegreen"
+  "Color for days on which a task should start to be done."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+
+(defcustom org-habit-warning-color "yellow"
+  "Color for days on which a task ought to be done."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+(defcustom org-habit-warning-future-color "palegoldenrod"
+  "Color for days on which a task ought be done."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+
+(defcustom org-habit-alert-color "yellow"
+  "Color for days on which a task is due."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+(defcustom org-habit-alert-future-color "palegoldenrod"
+  "Color for days on which a task is due."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+
+(defcustom org-habit-overdue-color "red"
+  "Color for days on which a task is overdue."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+(defcustom org-habit-overdue-future-color "mistyrose"
+  "Color for days on which a task is overdue."
+  :group 'org-habit
+  :group 'org-faces
+  :type 'color)
+
+(defun org-habit-duration-to-days (ts)
+  (if (string-match "\\([0-9]+\\)\\([dwmy]\\)\\'" ts)
+      ;; lead time is specified.
+      (floor (* (string-to-number (match-string 1 ts))
+		(cdr (assoc (match-string 2 ts)
+			    '(("d" . 1)    ("w" . 7)
+			      ("m" . 30.4) ("y" . 365.25))))))
+    (error "Invalid duration string: %s" ts)))
+
+(defun org-is-habit-p (&optional pom)
+  (string= "habit" (org-entry-get (or pom (point)) "STYLE")))
+
+(defun org-habit-parse-todo (&optional pom)
+  "Parse the TODO surrounding point for its habit-related data.
+Returns a list with the following elements:
+
+  0: Scheduled date for the habit (may be in the past)
+  1: \".+\"-style repeater for the schedule, in days
+  2: Optional deadline (nil if not present)
+  3: If deadline, the repeater for the deadline, otherwise nil
+  4: A list of all the past dates this todo was mark closed
+
+This list represents a \"habit\" for the rest of this module."
+  (save-excursion
+    (if pom (goto-char pom))
+    (assert (org-is-habit-p (point)))
+    (let ((scheduled (org-get-scheduled-time (point)))
+	  (scheduled-repeat (org-get-repeat "SCHEDULED"))
+	  (deadline (org-get-deadline-time (point)))
+	  (deadline-repeat (org-get-repeat "DEADLINE")))
+      (unless scheduled
+	(error "Habit has no scheduled date"))
+      (unless scheduled-repeat
+	(error "Habit has no scheduled repeat period"))
+      (unless (string-match "\\`\\.\\+[0-9]+" scheduled-repeat)
+	(error "Habit's scheduled repeat period does not match `.+[0-9]*'"))
+      (if (and deadline (not deadline-repeat))
+	  (error "Habit has a deadline, but no deadline repeat period"))
+      (if (and deadline
+	       (not (string-match "\\`\\.\\+[0-9]+" scheduled-repeat))) 
+	  (error "Habit's deadline repeat period does not match `.+[0-9]*'"))
+      (let ((sr-days (org-habit-duration-to-days scheduled-repeat))
+	    (dr-days (and deadline-repeat
+			  (org-habit-duration-to-days deadline-repeat))))
+	(when (and scheduled deadline)
+	  (cond
+	   ((time-less-p deadline scheduled)
+	    (error "Habit's deadline date is before the scheduled date"))
+	   ((< dr-days sr-days)
+	    (error "Habit's deadline repeat period is less than scheduled"))
+	   ((/= (- (time-to-days deadline)
+		   (time-to-days scheduled))
+		(- dr-days sr-days))
+	    (error "Habit's deadline and scheduled period lengths are off"))))
+	(let ((end (org-entry-end-position))
+	      closed-dates)
+	  (org-back-to-heading t)
+	  (while (re-search-forward "- State \"DONE\".*\\[\\([^]]+\\)\\]" end t)
+	    (push (org-time-string-to-time (match-string-no-properties 1))
+		  closed-dates))
+	  (list scheduled sr-days deadline dr-days closed-dates))))))
+
+(defsubst org-habit-scheduled (habit)
+  (nth 0 habit))
+(defsubst org-habit-scheduled-repeat (habit)
+  (nth 1 habit))
+(defsubst org-habit-deadline (habit)
+  (nth 2 habit))
+(defsubst org-habit-deadline-repeat (habit)
+  (nth 3 habit))
+(defsubst org-habit-done-dates (habit)
+  (nth 4 habit))
+
+(defun org-habit-get-colors (habit &optional moment scheduled-time donep)
+  "Return faces for HABIT relative to MOMENT and SCHEDULED-TIME.
+MOMENT defaults to the current time if it is nil.
+SCHEDULED-TIME defaults to the habit's actual scheduled time if nil.
+
+Habits are assigned colors on the following basis:
+  Blue      Task is before the scheduled date.
+  Green     Task is on or after scheduled date, but before the
+            end of the schedule's repeat period.
+  Yellow    If the task has a deadline, then it is after schedule's
+            repeat period, but before the deadline.
+  Orange    The task has reached the deadline day, or if there is
+            no deadline, the end of the schedule's repeat period.
+  Red       The task has gone beyond the deadline day or the
+            schedule's repeat period."
+  (unless moment (setq moment (current-time)))
+  (let* ((scheduled (or scheduled-time (org-habit-scheduled habit)))
+	 (s-repeat (org-habit-scheduled-repeat habit))
+	 (scheduled-end (time-add scheduled (days-to-time s-repeat)))
+	 (d-repeat (org-habit-deadline-repeat habit))
+	 (deadline (if scheduled-time
+		       (time-add scheduled-time
+				 (days-to-time (- d-repeat s-repeat)))
+		     (org-habit-deadline habit))))
+    (cond
+     ((time-less-p moment scheduled)
+      (cons org-habit-clear-color org-habit-clear-future-color))
+     ((time-less-p moment scheduled-end)
+      (cons org-habit-ready-color org-habit-ready-future-color))
+     ((and deadline
+	   (time-less-p moment deadline))
+      (if donep
+	  (cons org-habit-ready-color org-habit-ready-future-color)
+	(cons org-habit-warning-color org-habit-warning-future-color)))
+     ((= (time-to-days moment)
+	 (if deadline
+	     (time-to-days deadline)
+	   (time-to-days scheduled-end)))
+      (if donep
+	  (cons org-habit-ready-color org-habit-ready-future-color)
+	(cons org-habit-alert-color org-habit-alert-future-color)))
+     (t
+      (cons org-habit-overdue-color org-habit-overdue-future-color)))))
+
+(defun org-habit-build-graph (habit &optional starting current ending)
+  "Build a color graph for the given HABIT, from STARTING to ENDING."
+  (let ((done-dates (sort (org-habit-done-dates habit) 'time-less-p))
+	(s-repeat (org-habit-scheduled-repeat habit))
+	(day starting)
+	(current-days (time-to-days current))
+	last-done-date
+	(graph (make-string (1+ (- (time-to-days ending)
+				   (time-to-days starting))) ?\ ))
+	(index 0))
+    (if done-dates
+	(while (time-less-p (car done-dates) starting)
+	  (setq last-done-date (car done-dates)
+		done-dates (cdr done-dates))))
+    (while (time-less-p day ending)
+      (let* ((now-days (time-to-days day))
+	     (in-the-past-p (< now-days current-days))
+	     (todayp (= now-days current-days))
+	     (donep (and done-dates
+			 (= now-days (time-to-days (car done-dates)))))
+	     (colors (if (and in-the-past-p (not last-done-date))
+			 (cons org-habit-clear-color
+			       org-habit-clear-future-color)
+		       (org-habit-get-colors
+			habit day
+			(and in-the-past-p
+			     (time-add last-done-date
+				       (days-to-time s-repeat)))
+			donep)))
+	     markedp color)
+	(if donep
+	    (progn
+	      (aset graph index ?*)
+	      (setq last-done-date (car done-dates)
+		    done-dates (cdr done-dates)
+		    markedp t))
+	  (if todayp
+	      (aset graph index ?!)))
+	(setq color (if (or in-the-past-p
+			    todayp)
+			(car colors)
+		      (cdr colors)))
+	(if (and in-the-past-p
+		 (not (string= color org-habit-overdue-color))
+		 (not markedp))
+	    (setq color (cdr colors)))
+	(put-text-property index (1+ index)
+			   'face (list :background color) graph))
+      (setq day (time-add day (days-to-time 1))
+	    index (1+ index)))
+    graph))
+
+(defun org-habit-insert-consistency-graphs (&optional line)
+  "Insert consistency graph for any habitual tasks."
+  (let ((inhibit-read-only t) l c
+	(moment (time-subtract (current-time)
+			       (list 0 (* 3600 org-extend-today-until) 0))))
+    (save-excursion
+      (goto-char (if line (point-at-bol) (point-min)))
+      (while (not (eobp))
+	(let ((habit (get-text-property (point) 'org-habit-p)))
+	  (when habit
+	    (move-to-column org-habit-graph-column t)
+	    (delete-char (min (+ 1 org-habit-preceding-days
+				 org-habit-following-days)
+			      (- (line-end-position) (point))))
+	    (insert (org-habit-build-graph
+		     habit
+		     (time-subtract moment
+				    (days-to-time org-habit-preceding-days))
+		     moment
+		     (time-add moment
+			       (days-to-time org-habit-following-days))))))
+	(forward-line)))))
+
+(defun org-habit-toggle-habits ()
+  "Toggle display of habits in an agenda buffer."
+  (interactive)
+  (org-agenda-check-type t 'agenda)
+  (setq org-habit-show-habits (not org-habit-show-habits))
+  (org-agenda-redo)
+  (org-agenda-set-mode-name)
+  (message "Habits turned %s"
+	   (if org-habit-show-habits "on" "off")))
+
+(org-defkey org-agenda-mode-map "K" 'org-habit-toggle-habits)
+
+(provide 'org-habit)
+
+;; arch-tag: 
+
+;;; org-habit.el ends here

+ 1 - 0
lisp/org.el

@@ -188,6 +188,7 @@ to add the symbol `xyz', and the package must have a call to
 	(const :tag "   id:                Global IDs for identifying entries" org-id)
 	(const :tag "   id:                Global IDs for identifying entries" org-id)
 	(const :tag "   info:              Links to Info nodes" org-info)
 	(const :tag "   info:              Links to Info nodes" org-info)
 	(const :tag "   jsinfo:            Set up Sebastian Rose's JavaScript org-info.js" org-jsinfo)
 	(const :tag "   jsinfo:            Set up Sebastian Rose's JavaScript org-info.js" org-jsinfo)
+	(const :tag "   habit:             Track your consistency with habits" org-habit)
 	(const :tag "   inlinetask:        Tasks independent of outline hierarchy" org-inlinetask)
 	(const :tag "   inlinetask:        Tasks independent of outline hierarchy" org-inlinetask)
 	(const :tag "   irc:               Links to IRC/ERC chat sessions" org-irc)
 	(const :tag "   irc:               Links to IRC/ERC chat sessions" org-irc)
 	(const :tag "   mac-message:       Links to messages in Apple Mail" org-mac-message)
 	(const :tag "   mac-message:       Links to messages in Apple Mail" org-mac-message)