Skip to content

Commit

Permalink
added support for completion of recurring tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
avillafiorita committed Aug 10, 2012
1 parent 544a3cc commit 22cf1c8
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 3 deletions.
32 changes: 30 additions & 2 deletions README.textile
Expand Up @@ -6,10 +6,25 @@ The mode provides commands for:

* toggling the done status of a tag
* setting, changing or deleting a task's priority
* opening the default @todo.txt@ file or add todos to the file

The mode supports an extension to the todo.txt syntax that allows to
specify due dates, start dates, and repetitions. In particular,
anywhere in the text of a todo you can use:

* @DUE:2012-03-05@ to specify that the todo is due on March 5, 2012.
* @START:2012-02-16@ to specify that the todo can be started no earlier than February 16, 2012.
* @RECUR:3.months@ to specify that the todo is to be repeated three months after its @DUE@ date (note: if @RECUR@ is present, then also a @DUE@ date has to be specified in the todo).

Possible repetition patterns:

* @daily@, @weekly@, @monthly@, @yearly@
* @N.days@, @N.weeks@, @N.months@, @N.years@ for more flexibly repetition periods (also the singular forms can be used, e.g. @1.day@).


h1. Installation

Load (or require) @todotxt-mode@ in your @.emacs@ file.
Load (or add to your path and require) @todotxt-mode@ in your @.emacs@ file.

For instance, put in your @.emacs@ file:

Expand All @@ -28,11 +43,24 @@ Open a @todo.txt@ file in Emacs and then

When in @todo.txt@ mode, use @M-x describe-mode@ for more information about available functions and keybindings.

h1. Configuration

You might want to put the following code in your @.emacs@ file:

<pre>
(setq todotxt-default-file (expand-file-name "<<WHERE YOUR TODO FILE LIVES>>"))
(define-key global-map "\C-co" 'todotxt-open-file)
(define-key global-map "\C-ct" 'todotxt-add-todo)
</pre>

to quickly access your @todo.txt@ file and quickly add todos to it.

h1. Quick Links

* "Todo.txt":http://www.todotxt.com. The official todo.txt website
* "todotxt.el":https://github.com/rpdillon/todotxt.el an(other) Emacs mode for managing todo.txt files

h1. License

Distributed under the conditions of the MIT license.
Distributed under the conditions of the "MIT license":http://opensource.org/licenses/mit-license.php/.

170 changes: 169 additions & 1 deletion todotxt-mode.el
Expand Up @@ -120,7 +120,155 @@ c. the function can be called from any buffer (remember to set the variable todo
(beginning-of-line)
(if (looking-at "x \\([0-9]+-[0-9]+-[0-9]+ \\)*")
(delete-region (match-beginning 0) (match-end 0))
(insert (concat "x " (format-time-string "%Y-%m-%d "))))))
(complete-and-instantiate))))

(defun complete-and-instantiate ()
"Take todo at point. Mark it as done. If it contains a REPEAT
directive, instantiate a new instance of the todo and add it at
the end of the current buffer.
Not meant to be used directly: call todotxt-toggle-done instead,
which ensures save-excursion and pointer at beginning-of-line."
(let ( (todo-as-string (todotxt-get-current-todo)) )
;; complete the current todo (this has to be done in any case
(insert (concat "x " (format-time-string "%Y-%m-%d ")))
;; instantiate a new one if necessary
(let ( (repetition (todotxt-get-repetition todo-as-string)) )
(if repetition
;; notice that repetition is a list (and not a time)
(let ( (new-todo (todotxt-move-dates todo-as-string repetition)) )
(goto-char (point-max))
(if (not (bolp)) (insert "\n"))
(insert new-todo)
(message (concat "Inserted "
new-todo
" at the end of the buffer")))))))

;;;
;;; lower level functions to manage todos
;;;

(defun todotxt-get-current-todo ()
"Get the current todo (= the todo at the line where the cursor is) as a string.
(the function copies the current line; in the context of a
todo.txt file this is equivalent to copying a todo)"
(interactive)
(buffer-substring (line-beginning-position)
(line-end-position)))

;; (defun todotxt-replace-current-todo (new-todo-as-string)
;; "Replace the todo at the current line with the todo passed as argument.
;; (the function replaces the line at point with the string passed
;; as argument. If applied to a todo.txt file this is equivalent to
;; replacing the current todo with a new todo)"
;; (save-excursion
;; (delete-region (line-beginning-position) (line-end-position))
;; (insert new-todo-as-string)))

(defun todotxt-get-time (type todo-as-string)
"Get the date of a field in the current string.
First argument type is either 'DUE' or 'START'. The function returns the encoded time
after 'DUE' or 'START' appearing in the todo."
(let ((match (string-match
(concat type ":\\([0-9]+\\)-\\([0-9]+\\)-\\([0-9]+\\)")
todo-as-string)))
(if match
(let ((year (string-to-number (match-string 1 todo-as-string)))
(month (string-to-number (match-string 2 todo-as-string)))
(day (string-to-number (match-string 3 todo-as-string))))
(encode-time 0 0 0 day month year))
nil)))

(defun todotxt-set-time (type new-date todo-as-string)
"Set the date of a field in the current string.
* First argument type is either 'DUE' or 'START'.
* Second argument 'new-date' is the time to insert; it can be nil, in
which case nothing is done and todo-as-string is returned untouched.
* Third argument todo-as-string is the string where new-time is inserted."
(if new-date
(let ((new-date-as-string (format-time-string "%Y-%m-%d" new-date)))
(replace-regexp-in-string (concat type ":[0-9]+-[0-9]+-[0-9]+")
(concat type ":" new-date-as-string)
todo-as-string))
todo-as-string))


(defun todotxt-move-dates (todo-as-string interval)
"Given a todo as a string (first argument), create a new todo in which
START and DUE date are moved according to interval (second argument).
The second argument is typically the output of a todotxt-get-repetition call."
;; this function exploits the fact that todotxt-move-time and todotxt-set-time manage
;; nil values in input (and return what is expected: nil in the first case, the input string in
;; the second case)
(let ( (new-start-time (todotxt-add-interval interval (todotxt-get-time "START" todo-as-string)))
(new-due-time (todotxt-add-interval interval (todotxt-get-time "DUE" todo-as-string))) )
(todotxt-set-time "START"
new-start-time
(todotxt-set-time "DUE" new-due-time todo-as-string))))


(defun todotxt-add-interval (interval time)
"Add interval to time.
First argument interval is in the format required by encode-time.
Second argument time is a time (the output of a encode-time).
The function returns a time."
(if (or (eq time nil) (eq interval nil))
nil
(apply 'encode-time (todotxt-recursive-sum interval (decode-time time)))))

(defun todotxt-recursive-sum (a b)
"Sum the elements of two lists, element by element.
Do so only for the N-th elements, where N is the length of the shortest list.
(todotxt-recursive-sum '(1 2 3) '(3 4 5)) -> (4 6 8)"
(if (and a b)
(cons (+ (car a) (car b)) (rec-sum (cdr a) (cdr b)))
nil))


(defvar todotxt-repetitions-assoc nil
"Association list of repetitions and functions that take as input a string and return the time interval specified by the repetition, using the 'time' conventions.")
(setq todotxt-repetitions-assoc
'(("daily" . (lambda (x) '(0 0 0 1 0 0)))
("weekly" . (lambda (x) '(0 0 0 7 0 0)))
("monthly" . (lambda (x) '(0 0 0 0 1 0)))
("yearly" . (lambda (x) '(0 0 0 0 0 1)))
("\\([0-9]+\\)\\.day" . (lambda (x) (progn
(string-match "\\([0-9]+\\)\\.day" x)
(list 0 0 0 (string-to-int (match-string 1 x)) 0 0))))
("\\([0-9]+\\)\\.week" . (lambda (x) (progn
(string-match "\\([0-9]+\\)\\.week" x)
(list 0 0 0 (* 7 (string-to-int (match-string 1 x))) 0 0))))
("\\([0-9]+\\)\\.month" . (lambda (x) (progn
(string-match "\\([0-9]+\\)\\.month" x)
(list 0 0 0 0 (string-to-int (match-string 1 x)) 0))))
("\\([0-9]+\\)\\.year" . (lambda (x) (progn
(string-match "\\([0-9]+\\)\\.year" x)
(list 0 0 0 0 0 (string-to-int (match-string 1 x))))))))

(defun todotxt-get-repetition (todo-as-string)
"Extract a repetition string from todo-as-string (a string
representing a todo) and return a tuple (seconds minutes hours days months years)
encoding the repetition and good for encode-time.
The value of todotxt-repetitions-assoc encodes the specification
of the repetition strings."
;; the following code looks for the car's of repetitions-assoc (regular
;; expressions with repetitions) and applies the cdr of the matching pair if
;; the string matches. The output is a list of the type (nil nil nil
;; ... nil) or (nil nil (0 1 0) nil ...) -- with at most one element which
;; is not nil (and if not nil it is the repetition interval)
(let ( (result (mapcar '(lambda (x)
(if (string-match (concat "RECUR:" (car x)) todo-as-string)
(funcall (cdr x) todo-as-string)
nil))
todotxt-repetitions-assoc)) )
;; find the first non nil occurrence (if any), return nil otherwise
(find-if '(lambda (x) (not (eq x nil))) result)))


(defun todotxt-pri (char)
"Set (or change) priority of task at cursor.
Expand Down Expand Up @@ -177,6 +325,26 @@ The mode does not depend upon the todo.txt app, thanks also to
the extremely simple and effective format defined for todo.txt
files. For more information about todo.txt, http://www.todotxt.com.
The mode also implements a syntax extension to support task
repetition, due and start dates. In particular the following
strings have a special meaning in a todo:
* DUE:YYYY-MM-DD (e.g. DUE:2012-12-15), to mark the due date
* START:YYYY-MM-DD (e.g. START:2012-11-15), to mark the first date when
a todo can actually be started.
* RECUR:repetition, where repetition is any of:
- 'daily', 'weekly', 'monthly', 'yearly' or
- N.period where period is 'year', 'month', 'week', 'day'
(e.g. RECUR:yearly, RECUR:2.year), to mark that a taks repeats once
completed.
The following actions are taken by todotxt-mode:
* if a task containing a 'RECUR' directive is marked as complete
using the todotxt-mark-done command, a new instance of the task
is created with the correct DUE and START directives, if present.
\\{todotxt-mode-map}"
(interactive)
(kill-all-local-variables)
Expand Down

0 comments on commit 22cf1c8

Please sign in to comment.