aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Berman2012-05-17 22:20:44 +0100
committerStephen Berman2012-05-17 22:20:44 +0100
commit0e89c3fc75c7de33bcb625c325600af227d2b1d1 (patch)
treeb9c1cfb8342a7208b874d5101ae3064c1be1e61f
parent697bd4a3585bd7d9ef66d670d21b72a3e9a36e6a (diff)
downloademacs-0e89c3fc75c7de33bcb625c325600af227d2b1d1.tar.gz
emacs-0e89c3fc75c7de33bcb625c325600af227d2b1d1.zip
* calendar/todos.el Add and revise further doc strings and
comments; major code rearrangement. (todos-merged-files, todos-prompt-merged-files) (todos-print-priorities, todos-tmp-buffer-name) (todos-top-priorities-widgets, todos-update-merged-files) (todos-merged-top-priorities, todos-merged-diary-items) (todos-merged-regexp-items, todos-merged-custom-items) (todos-raw-mode, todos-change-default-file) (todos-toggle-diary-inclusion, todos-toggle-item-diary-nonmarking) (todos-toggle-diary-nonmarking, todos-validate-category-name): Remove. (todos-category-string-matcher): Comment out. (todos-categories): New defgroup. (todos-initial-file, todos-filter-buffer) (todos-top-priorities-buffer, todos-categories-category-label) (todos-diary-items-buffer, todos-regexp-items-buffer) (todos-custom-items-buffer, todos-filter-files) (todos-highlight-item, todos-todo-mode-date-time-regexp): New defcustoms. (todos-diary-expired): New face. (todos-print-buffer, todos-multiple-files) (todos-multiple-files-widget, todos-key-bindings): New variables. (todos-short-file-name, todos-reevaluate-default-file-defcustom) (todos-special-buffer-name) (todos-reevaluate-filter-files-defcustom) (todos-reset-highlight-item, todos-reevaluate-defcustoms) (todos-nondiary-marker-matcher, todos-diary-nonmarking-matcher) (todos-diary-expired-matcher, todos-category-string-matcher-1) (todos-category-string-matcher-2, todos-repair-categories-sexp) (todos-validate-name, todos-multiple-files) (todos-display-categories-1, todos-update-categories-display) (todos-modes-set-3, todos-mode-external-set): New functions. (todos-set-top-priorities-in-file) (todos-set-top-priorities-in-category) (todos-top-priorities-multifile, todos-diary-items-multifile) (todos-regexp-items-multifile, todos-custom-items-multifile) (todos-convert-legacy-files, todos-jump-to-item) (todos-edit-multiline-item, todos-edit-item-date-from-calendar) (todos-edit-item-diary-inclusion) (todos-edit-category-diary-inclusion) (todos-edit-item-diary-nonmarking) (todos-edit-category-diary-nonmarking): New commands. (todos, todos-faces): Update :version. (todos-done-separator, todos-completion-ignore-case): Change default value. (todos-done-separator): Change :set function. (todos-indent-to-here): Add :validate function to :type. (todos-prefix-string, todos-mark, todos-button) (todos-sorted-column, todos-archived-only, todos-search) (todos-done, todos-done-sep): Provide full face definitions instead of inheriting. (todos-edit-buffer, todos-categories-buffer): Change from defcustom to defvar. (todos-category-beg, todos-category-done): Change from defvar to defconst. (todos-files): Check if todos-files-directory exists. (todos-default-todos-file, todos-mode-line-control, todos-print): Use todos-short-file-name. (todos-font-lock-keywords): Use todos-nondiary-marker-matcher, todos-diary-nonmarking-matcher, todos-category-string-matcher-1, todos-category-string-matcher-2, todos-diary-expired-matcher. (todos-category-select): Use todos-done-string-start, and condition search on todos-show-with-done; don't make display overlay for done items separator string if there already is one; use todos-highlight-item, require hl-line and activate hl-line-mode here in order to avoid a hang if done in todos-mode or the mode hook. (todos-update-categories-sexp): Use todos-categories-full if set, otherwise todos-categories. (todos-make-categories-list): Don't test for archive file when processing a legacy todo-mode file. (todos-check-format): Add check for todos-categories sexp. (todos-diary-item-p): Use todos-nondiary-start instead of todos-date-pattern. (todos-marked-item-p): Rename from todos-item-marked-p. (todos-read-file-name): Don't accept empty name; validate. (todos-read-category): Validate new name before prompting whether to add new category; force quit if user answers no. (todos-filter-items): Improve implementation. (todos-set-top-priorities): Rewrite as a noninteractive function using minibuffer input instead of widgets. (todos-insert-sort-button): Call todos-display-sorted with argument nil to display categories in numerical order, instead of calling todos-display-categories. (powerset-recursive): Borrow and slightly reformulate the (GDFL'd) Common Lisp powerset function at http://rosettacode.org/wiki/Power_set#Common_Lisp. (powerset-bitwise): Implement in Emacs Lisp the (GDFL'd) C powerset function at http://rosettacode.org/wiki/Power_set#C. (todos-powerset): Defalias to powerset-bitwise. (todos-mode-map): Generate from todos-key-bindings instead of listing each key definition. (todos-categories-mode-map): Add two bindings. (todos-filter-items-mode-map): Add some bindings, remove others. (todos-mode): Derive from special-mode; use todos-modes-set-3; add function setting todos-done-separator to window-configuration-change-hook. (todos-unload-hook): Remove function setting todos-done-separator from window-configuration-change-hook. (todos-archive-mode): Derive from todos-mode; use todos-modes-set-3. (todos-edit-mode): Derive from text-mode; use todos-mode-external-set. (todos-categories-mode): Derive from special-mode; use todos-mode-external-set. (todos-filter-items-mode): Derive from special-mode. (todos-quit): Save archive file if it hasn't yet been saved. (todos-display-categories): Delegate all functionality to todos-display-categories-1 and todos-update-categories-display. (todos-toggle-view-done-items): Improve implementation. (todos-highlight-item): Require hl-line. (todos-toggle-display-date-time): Remove argument and make it apply only to whole file. (todos-top-priorities, todos-diary-items, todos-regexp-items) (todos-custom-items): Use todos-special-buffer-name. (todos-add-file): Use todos-short-file-name and todos-reevaluate-defcustoms; remove validation, since it's now done in todos-read-file-name. (todos-add-category): Also update todos-categories-full if non-nil. (todos-delete-category): Delete file after confirmation if only category is deleted. (todos-move-category): Use todos-short-file-name and todos-reevaluate-defcustoms. (todos-insert-item): Fix getting date from the calendar and insertion of time string. (todos-set-date-from-calendar): Enter calendar buffer, suppressing display of diary entries. (todos-edit-multiline): Add optional argument to restrict editing buffer to current item, otherwise make entire buffer (i.e. whole file) editable. (todos-edit-quit): When whole file is editable, check file format validity before killing buffer, and if valid, recalculate categories sexp to be safe. (todos-edit-item-header): Allow choosing date from calendar. (todos-item-done): Handle marked items.
-rw-r--r--lisp/ChangeLog136
-rw-r--r--lisp/calendar/todos.el4813
2 files changed, 2912 insertions, 2037 deletions
diff --git a/lisp/ChangeLog b/lisp/ChangeLog
index 9fe801d3f06..1933b7cea01 100644
--- a/lisp/ChangeLog
+++ b/lisp/ChangeLog
@@ -1,3 +1,139 @@
12012-09-20 Stephen Berman <stephen.berman@gmx.net>
2
3 * calendar/todos.el Add and revise further doc strings and
4 comments; major code rearrangement.
5 (todos-merged-files, todos-prompt-merged-files)
6 (todos-print-priorities, todos-tmp-buffer-name)
7 (todos-top-priorities-widgets, todos-update-merged-files)
8 (todos-merged-top-priorities, todos-merged-diary-items)
9 (todos-merged-regexp-items, todos-merged-custom-items)
10 (todos-raw-mode, todos-change-default-file)
11 (todos-toggle-diary-inclusion, todos-toggle-item-diary-nonmarking)
12 (todos-toggle-diary-nonmarking, todos-validate-category-name):
13 Remove.
14 (todos-category-string-matcher): Comment out.
15 (todos-categories): New defgroup.
16 (todos-initial-file, todos-filter-buffer)
17 (todos-top-priorities-buffer, todos-categories-category-label)
18 (todos-diary-items-buffer, todos-regexp-items-buffer)
19 (todos-custom-items-buffer, todos-filter-files)
20 (todos-highlight-item, todos-todo-mode-date-time-regexp):
21 New defcustoms.
22 (todos-diary-expired): New face.
23 (todos-print-buffer, todos-multiple-files)
24 (todos-multiple-files-widget, todos-key-bindings): New variables.
25 (todos-short-file-name, todos-reevaluate-default-file-defcustom)
26 (todos-special-buffer-name)
27 (todos-reevaluate-filter-files-defcustom)
28 (todos-reset-highlight-item, todos-reevaluate-defcustoms)
29 (todos-nondiary-marker-matcher, todos-diary-nonmarking-matcher)
30 (todos-diary-expired-matcher, todos-category-string-matcher-1)
31 (todos-category-string-matcher-2, todos-repair-categories-sexp)
32 (todos-validate-name, todos-multiple-files)
33 (todos-display-categories-1, todos-update-categories-display)
34 (todos-modes-set-3, todos-mode-external-set): New functions.
35 (todos-set-top-priorities-in-file)
36 (todos-set-top-priorities-in-category)
37 (todos-top-priorities-multifile, todos-diary-items-multifile)
38 (todos-regexp-items-multifile, todos-custom-items-multifile)
39 (todos-convert-legacy-files, todos-jump-to-item)
40 (todos-edit-multiline-item, todos-edit-item-date-from-calendar)
41 (todos-edit-item-diary-inclusion)
42 (todos-edit-category-diary-inclusion)
43 (todos-edit-item-diary-nonmarking)
44 (todos-edit-category-diary-nonmarking): New commands.
45 (todos, todos-faces): Update :version.
46 (todos-done-separator, todos-completion-ignore-case):
47 Change default value.
48 (todos-done-separator): Change :set function.
49 (todos-indent-to-here): Add :validate function to :type.
50 (todos-prefix-string, todos-mark, todos-button)
51 (todos-sorted-column, todos-archived-only, todos-search)
52 (todos-done, todos-done-sep): Provide full face definitions
53 instead of inheriting.
54 (todos-edit-buffer, todos-categories-buffer): Change from
55 defcustom to defvar.
56 (todos-category-beg, todos-category-done): Change from defvar to
57 defconst.
58 (todos-files): Check if todos-files-directory exists.
59 (todos-default-todos-file, todos-mode-line-control, todos-print):
60 Use todos-short-file-name.
61 (todos-font-lock-keywords): Use todos-nondiary-marker-matcher,
62 todos-diary-nonmarking-matcher, todos-category-string-matcher-1,
63 todos-category-string-matcher-2, todos-diary-expired-matcher.
64 (todos-category-select): Use todos-done-string-start, and
65 condition search on todos-show-with-done; don't make display
66 overlay for done items separator string if there already is one;
67 use todos-highlight-item, require hl-line and activate
68 hl-line-mode here in order to avoid a hang if done in todos-mode
69 or the mode hook.
70 (todos-update-categories-sexp): Use todos-categories-full if set,
71 otherwise todos-categories.
72 (todos-make-categories-list): Don't test for archive file when
73 processing a legacy todo-mode file.
74 (todos-check-format): Add check for todos-categories sexp.
75 (todos-diary-item-p): Use todos-nondiary-start instead of
76 todos-date-pattern.
77 (todos-marked-item-p): Rename from todos-item-marked-p.
78 (todos-read-file-name): Don't accept empty name; validate.
79 (todos-read-category): Validate new name before prompting whether
80 to add new category; force quit if user answers no.
81 (todos-filter-items): Improve implementation.
82 (todos-set-top-priorities): Rewrite as a noninteractive function
83 using minibuffer input instead of widgets.
84 (todos-insert-sort-button): Call todos-display-sorted with
85 argument nil to display categories in numerical order, instead of
86 calling todos-display-categories.
87 (powerset-recursive): Borrow and slightly reformulate the (GDFL'd)
88 Common Lisp powerset function at
89 http://rosettacode.org/wiki/Power_set#Common_Lisp.
90 (powerset-bitwise): Implement in Emacs Lisp the (GDFL'd) C
91 powerset function at http://rosettacode.org/wiki/Power_set#C.
92 (todos-powerset): Defalias to powerset-bitwise.
93 (todos-mode-map): Generate from todos-key-bindings instead of
94 listing each key definition.
95 (todos-categories-mode-map): Add two bindings.
96 (todos-filter-items-mode-map): Add some bindings, remove others.
97 (todos-mode): Derive from special-mode; use todos-modes-set-3; add
98 function setting todos-done-separator to
99 window-configuration-change-hook.
100 (todos-unload-hook): Remove function setting todos-done-separator
101 from window-configuration-change-hook.
102 (todos-archive-mode): Derive from todos-mode; use todos-modes-set-3.
103 (todos-edit-mode): Derive from text-mode; use todos-mode-external-set.
104 (todos-categories-mode): Derive from special-mode; use
105 todos-mode-external-set.
106 (todos-filter-items-mode): Derive from special-mode.
107 (todos-quit): Save archive file if it hasn't yet been saved.
108 (todos-display-categories): Delegate all functionality to
109 todos-display-categories-1 and todos-update-categories-display.
110 (todos-toggle-view-done-items): Improve implementation.
111 (todos-highlight-item): Require hl-line.
112 (todos-toggle-display-date-time): Remove argument and make it
113 apply only to whole file.
114 (todos-top-priorities, todos-diary-items, todos-regexp-items)
115 (todos-custom-items): Use todos-special-buffer-name.
116 (todos-add-file): Use todos-short-file-name and
117 todos-reevaluate-defcustoms; remove validation, since it's now
118 done in todos-read-file-name.
119 (todos-add-category): Also update todos-categories-full if non-nil.
120 (todos-delete-category): Delete file after confirmation if only
121 category is deleted.
122 (todos-move-category): Use todos-short-file-name and
123 todos-reevaluate-defcustoms.
124 (todos-insert-item): Fix getting date from the calendar and
125 insertion of time string.
126 (todos-set-date-from-calendar): Enter calendar buffer, suppressing
127 display of diary entries.
128 (todos-edit-multiline): Add optional argument to restrict editing
129 buffer to current item, otherwise make entire buffer (i.e. whole
130 file) editable.
131 (todos-edit-quit): When whole file is editable, check file format
132 validity before killing buffer, and if valid, recalculate
133 categories sexp to be safe.
134 (todos-edit-item-header): Allow choosing date from calendar.
135 (todos-item-done): Handle marked items.
136
12012-09-19 Stephen Berman <stephen.berman@gmx.net> 1372012-09-19 Stephen Berman <stephen.berman@gmx.net>
2 138
3 * calendar/todos.el (todos-item-start): Restore commented out code 139 * calendar/todos.el (todos-item-start): Restore commented out code
diff --git a/lisp/calendar/todos.el b/lisp/calendar/todos.el
index 457e49c267e..b6b62808613 100644
--- a/lisp/calendar/todos.el
+++ b/lisp/calendar/todos.el
@@ -1,6 +1,6 @@
1;;; Todos.el --- facilities for making and maintaining Todo lists 1;;; Todos.el --- facilities for making and maintaining Todo lists
2 2
3;; Copyright (C) 1997, 1999, 2001-2011 Free Software Foundation, Inc. 3;; Copyright (C) 1997, 1999, 2001-2012 Free Software Foundation, Inc.
4 4
5;; Author: Oliver Seidel <privat@os10000.net> 5;; Author: Oliver Seidel <privat@os10000.net>
6;; Stephen Berman <stephen.berman@gmx.net> 6;; Stephen Berman <stephen.berman@gmx.net>
@@ -8,7 +8,7 @@
8;; Created: 2 Aug 1997 8;; Created: 2 Aug 1997
9;; Keywords: calendar, todo 9;; Keywords: calendar, todo
10 10
11;; This file is part of GNU Emacs. 11;; This file is [not yet] part of GNU Emacs.
12 12
13;; GNU Emacs is free software: you can redistribute it and/or modify 13;; GNU Emacs is free software: you can redistribute it and/or modify
14;; it under the terms of the GNU General Public License as published by 14;; it under the terms of the GNU General Public License as published by
@@ -25,30 +25,11 @@
25 25
26;;; Commentary: 26;;; Commentary:
27 27
28;; UI
29;; - display
30;; - show todos in cat
31;; - show done in cat
32;; - show catlist
33;; - show top priorities in all cats
34;; - show archived
35;; - navigation
36;; -
37;; - editing
38;;
39;; Internals
40;; - cat props: name, number, todos, done, archived
41;; - item props: priority, date-time, status?
42;; - file format
43;; - cat begin
44;; - todo items 0...n
45;; - empty line
46;; - done-separator
47;; - done item 0...n
48
49;;; Code: 28;;; Code:
50 29
51(require 'diary-lib) 30(require 'diary-lib)
31;; For remove-duplicates in todos-insertion-commands-args.
32(eval-when-compile (require 'cl))
52 33
53;; --------------------------------------------------------------------------- 34;; ---------------------------------------------------------------------------
54;;; User options 35;;; User options
@@ -56,9 +37,86 @@
56(defgroup todos nil 37(defgroup todos nil
57 "Create and maintain categorized lists of todo items." 38 "Create and maintain categorized lists of todo items."
58 :link '(emacs-commentary-link "todos") 39 :link '(emacs-commentary-link "todos")
59 :version "24.1" 40 :version "24.2"
60 :group 'calendar) 41 :group 'calendar)
61 42
43(defcustom todos-files-directory (locate-user-emacs-file "todos/")
44 "Directory where user's Todos files are saved."
45 :type 'directory
46 :group 'todos)
47
48(defun todos-files (&optional archives)
49 "Default value of `todos-files-function'.
50This returns the case-insensitive alphabetically sorted list of
51file truenames in `todos-files-directory' with the extension
52\".todo\". With non-nil ARCHIVES return the list of archive file
53truenames (those with the extension \".toda\")."
54 (let ((files (if (file-exists-p todos-files-directory)
55 (mapcar 'file-truename
56 (directory-files todos-files-directory t
57 (if archives "\.toda$" "\.todo$") t)))))
58 (sort files (lambda (s1 s2) (let ((cis1 (upcase s1))
59 (cis2 (upcase s2)))
60 (string< cis1 cis2))))))
61
62(defcustom todos-files-function 'todos-files
63 "Function returning the value of the variable `todos-files'.
64This function should take an optional argument that, if non-nil,
65makes it return the value of the variable `todos-archives'."
66 :type 'function
67 :group 'todos)
68
69(defun todos-short-file-name (file)
70 "Return short form of Todos FILE.
71This lacks the extension and directory components."
72 (file-name-sans-extension (file-name-nondirectory file)))
73
74(defcustom todos-default-todos-file (car (funcall todos-files-function))
75 "Todos file visited by first session invocation of `todos-show'."
76 :type `(radio ,@(mapcar (lambda (f) (list 'const f))
77 (mapcar 'todos-short-file-name
78 (funcall todos-files-function))))
79 :group 'todos)
80
81;; FIXME: is there a better alternative to this?
82(defun todos-reevaluate-default-file-defcustom ()
83 "Reevaluate defcustom of `todos-default-todos-file'.
84Called after adding or deleting a Todos file."
85 (eval (defcustom todos-default-todos-file (car (funcall todos-files-function))
86 "Todos file visited by first session invocation of `todos-show'."
87 :type `(radio ,@(mapcar (lambda (f) (list 'const f))
88 (mapcar 'todos-short-file-name
89 (funcall todos-files-function))))
90 :group 'todos)))
91
92(defcustom todos-show-current-file t
93 "Non-nil to make `todos-show' visit the current Todos file.
94Otherwise, `todos-show' always visits `todos-default-todos-file'."
95 :type 'boolean
96 :initialize 'custom-initialize-default
97 :set 'todos-toggle-show-current-file
98 :group 'todos)
99
100(defun todos-toggle-show-current-file (symbol value)
101 "The :set function for user option `todos-show-current-file'."
102 (custom-set-default symbol value)
103 (if value
104 (add-hook 'pre-command-hook 'todos-show-current-file nil t)
105 (remove-hook 'pre-command-hook 'todos-show-current-file t)))
106
107(defcustom todos-visit-files-commands (list 'find-file 'dired-find-file)
108 "List of commands to visit files for `todos-after-find-file'.
109Invoking these commands to visit a Todos or Todos Archive file
110calls `todos-show' or `todos-show-archive', so that the file is
111displayed correctly."
112 :type '(repeat function)
113 :group 'todos)
114
115(defcustom todos-initial-file "Todo"
116 "Default file name offered on adding first Todos file."
117 :type 'string
118 :group 'todos)
119
62(defcustom todos-initial-category "Todo" 120(defcustom todos-initial-category "Todo"
63 "Default category name offered on initializing a new Todos file." 121 "Default category name offered on initializing a new Todos file."
64 :type 'string 122 :type 'string
@@ -84,17 +142,46 @@ These reflect the priorities of the items in each category."
84 :set 'todos-reset-prefix 142 :set 'todos-reset-prefix
85 :group 'todos) 143 :group 'todos)
86 144
145(defun todos-reset-prefix (symbol value)
146 "The :set function for `todos-prefix' and `todos-number-prefix'."
147 (let ((oldvalue (symbol-value symbol))
148 (files (append todos-files todos-archives)))
149 (custom-set-default symbol value)
150 (when (not (equal value oldvalue))
151 (dolist (f files)
152 (with-current-buffer (find-file-noselect f)
153 (save-window-excursion
154 (todos-show)
155 (save-excursion
156 (widen)
157 (goto-char (point-min))
158 (while (not (eobp))
159 (remove-overlays (point) (point)); 'before-string prefix)
160 (forward-line)))
161 ;; Activate the new setting (save-restriction does not help).
162 (save-excursion (todos-category-select))))))))
163
87;; FIXME: Update when window-width changes. Add todos-reset-separator to 164;; FIXME: Update when window-width changes. Add todos-reset-separator to
88;; window-configuration-change-hook in todos-mode? But this depends on the 165;; window-configuration-change-hook in todos-mode? But this depends on the
89;; value being window-width instead of a constant length. 166;; value being window-width instead of a constant length.
90(defcustom todos-done-separator (make-string (window-width) ?-) 167(defcustom todos-done-separator (make-string (window-width) ?_)
91 "String used to visual separate done from not done items. 168 "String used to visual separate done from not done items.
92Displayed in a before-string overlay by `todos-toggle-view-done-items'." 169Displayed in a before-string overlay by `todos-toggle-view-done-items'."
93 :type 'string 170 :type 'string
94 :initialize 'custom-initialize-default 171 :initialize 'custom-initialize-default
95 :set 'todos-reset-prefix 172 :set 'todos-reset-separator
96 :group 'todos) 173 :group 'todos)
97 174
175;; (defun todos-reset-separator (symbol value)
176;; "The :set function for `todos-done-separator'
177;; Also added to `window-configuration-change-hook' in Todos mode."
178;; (let ((oldvalue (symbol-value symbol)))
179;; (custom-set-default symbol value)
180;; (when (not (equal value oldvalue))
181;; (make-string (window-width) ?_)
182;; ;; (save-excursion (todos-category-select))
183;; )))
184
98(defcustom todos-done-string "DONE " 185(defcustom todos-done-string "DONE "
99 "Identifying string appended to the front of done todos items." 186 "Identifying string appended to the front of done todos items."
100 :type 'string 187 :type 'string
@@ -102,6 +189,29 @@ Displayed in a before-string overlay by `todos-toggle-view-done-items'."
102 :set 'todos-reset-done-string 189 :set 'todos-reset-done-string
103 :group 'todos) 190 :group 'todos)
104 191
192(defun todos-reset-done-string (symbol value)
193 "The :set function for user option `todos-done-string'."
194 (let ((oldvalue (symbol-value symbol))
195 (files (append todos-files todos-archives)))
196 (custom-set-default symbol value)
197 ;; Need to reset this to get font-locking right.
198 (setq todos-done-string-start
199 (concat "^\\[" (regexp-quote todos-done-string)))
200 (when (not (equal value oldvalue))
201 (dolist (f files)
202 (with-current-buffer (find-file-noselect f)
203 (let (buffer-read-only)
204 (widen)
205 (goto-char (point-min))
206 (while (not (eobp))
207 (if (re-search-forward
208 (concat "^" (regexp-quote todos-nondiary-start)
209 "\\(" (regexp-quote oldvalue) "\\)")
210 nil t)
211 (replace-match value t t nil 1)
212 (forward-line)))
213 (todos-category-select)))))))
214
105(defcustom todos-comment-string "COMMENT" 215(defcustom todos-comment-string "COMMENT"
106 "String inserted before optional comment appended to done item." 216 "String inserted before optional comment appended to done item."
107 :type 'string 217 :type 'string
@@ -109,6 +219,27 @@ Displayed in a before-string overlay by `todos-toggle-view-done-items'."
109 :set 'todos-reset-comment-string 219 :set 'todos-reset-comment-string
110 :group 'todos) 220 :group 'todos)
111 221
222(defun todos-reset-comment-string (symbol value)
223 "The :set function for user option `todos-comment-string'."
224 (let ((oldvalue (symbol-value symbol))
225 (files (append todos-files todos-archives)))
226 (custom-set-default symbol value)
227 (when (not (equal value oldvalue))
228 (dolist (f files)
229 (with-current-buffer (find-file-noselect f)
230 (let (buffer-read-only)
231 (save-excursion
232 (widen)
233 (goto-char (point-min))
234 (while (not (eobp))
235 (if (re-search-forward
236 (concat
237 "\\[\\(" (regexp-quote oldvalue) "\\): [^]]*\\]")
238 nil t)
239 (replace-match value t t nil 1)
240 (forward-line)))
241 (todos-category-select))))))))
242
112(defcustom todos-show-with-done nil 243(defcustom todos-show-with-done nil
113 "Non-nil to display done items in all categories." 244 "Non-nil to display done items in all categories."
114 :type 'boolean 245 :type 'boolean
@@ -119,147 +250,98 @@ Displayed in a before-string overlay by `todos-toggle-view-done-items'."
119Argument CAT is the name of the current Todos category. 250Argument CAT is the name of the current Todos category.
120This function is the value of the user variable 251This function is the value of the user variable
121`todos-mode-line-function'." 252`todos-mode-line-function'."
122 (let ((file (file-name-sans-extension 253 (let ((file (todos-short-file-name todos-current-todos-file)))
123 (file-name-nondirectory todos-current-todos-file)))) 254 (format "%s category %d: %s" file todos-category-number cat)))
124 (format "%s category %d: %s" file todos-category-number cat)))
125 255
126(defcustom todos-mode-line-function 'todos-mode-line-control 256(defcustom todos-mode-line-function 'todos-mode-line-control
127 "Function that returns a mode line control for Todos buffers. 257 "Function that returns a mode line control for Todos buffers.
128The function is expected to take one argument that holds the name 258The function expects one argument holding the name of the current
129of the current Todos category. The resulting control becomes the 259Todos category. The resulting control becomes the local value of
130local value of `mode-line-buffer-identification' in each Todos 260`mode-line-buffer-identification' in each Todos buffer."
131buffer."
132 :type 'function
133 :group 'todos)
134
135(defcustom todos-files-directory (locate-user-emacs-file "todos/")
136 "Directory where user's Todos files are saved."
137 :type 'directory
138 :group 'todos)
139
140(defun todos-files (&optional archives)
141 "Default value of `todos-files-function'.
142This returns the case-insensitive alphabetically sorted list of
143file truenames in `todos-files-directory' with the extension
144\".todo\". With non-nil ARCHIVES return the list of archive file
145truenames (those with the extension \".toda\")."
146 (let ((files (mapcar 'file-truename
147 (directory-files todos-files-directory t
148 (if archives "\.toda$" "\.todo$") t))))
149 (sort files (lambda (s1 s2) (let ((cis1 (upcase s1))
150 (cis2 (upcase s2)))
151 (string< cis1 cis2))))))
152
153(defcustom todos-files-function 'todos-files
154 "Function returning the value of the variable `todos-files'.
155This function should take an optional argument that, if non-nil,
156makes it return the value of the variable `todos-archives'."
157 :type 'function 261 :type 'function
158 :group 'todos) 262 :group 'todos)
159 263
160(defcustom todos-filter-function nil 264(defun todos-special-buffer-name (buffer-type file-list)
161 "" 265 "Rename Todos special buffer.
162 :type 'function 266The new name is concatenated from the string BUFFER-TYPE and the
163 :group 'todos) 267names of the files in FILE-LIST. Used in the mode-list of
164 268buffers displaying top priorities, diary items, regexp items
165(defcustom todos-priorities-rules (list) 269etc. for single and multiple files."
166 "List of rules for choosing top priorities of each Todos file. 270 (let* ((flist (if (listp file-list) file-list (list file-list)))
167The rules should be set interactively by invoking 271 (multi (> (length flist) 1))
168`todos-set-top-priorities'. 272 (fnames (mapconcat (lambda (f) (todos-short-file-name f))
169 273 flist ", ")))
170Each rule is a list whose first element is a member of 274 (rename-buffer (format (concat "%s for file" (if multi "s" "")
171`todos-files', whose second element is a number specifying the 275 " \"%s\"") buffer-type fnames))))
172default number of top priority items for the categories in that 276
173file, and whose third element is an alist whose elements are 277(defcustom todos-filter-buffer "Todos filtered items"
174conses of a category name in that file and the number of top 278 "Initial name of buffer in Todos Filter mode."
175priority items in that category that `todos-top-priorities' shows
176by default, which overrides the number for the file."
177 :type 'list
178 :group 'todos)
179
180(defcustom todos-merged-files nil
181 "List of files for `todos-merged-top-priorities'."
182 :type `(set ,@(mapcar (lambda (x) (list 'const x))
183 (funcall todos-files-function)))
184 :group 'todos)
185
186(defcustom todos-prompt-merged-files nil
187 "Non-nil to prompt for merging files for `todos-filter-items'."
188 :type 'boolean
189 :group 'todos)
190
191(defcustom todos-show-current-file t
192 "Non-nil to make `todos-show' visit the current Todos file.
193Otherwise, `todos-show' always visits `todos-default-todos-file'."
194 :type 'boolean
195 :initialize 'custom-initialize-default
196 :set 'todos-toggle-show-current-file
197 :group 'todos)
198
199;; FIXME: omit second sentence from doc string?
200(defcustom todos-default-todos-file (car (funcall todos-files-function))
201 "Todos file visited by first session invocation of `todos-show'.
202Normally this should be set by invoking `todos-change-default-file'
203either directly or as a side effect of `todos-add-file'."
204 :type `(radio ,@(mapcar (lambda (x) (list 'const x))
205 (funcall todos-files-function)))
206 :group 'todos)
207
208(defcustom todos-visit-files-commands (list 'find-file 'dired-find-file)
209 "List of commands to visit files for `todos-after-find-file'.
210Invoking these commands to visit a Todos or Todos Archive file
211calls `todos-show' or `todos-show-archive', so that the file is
212displayed correctly."
213 :type '(repeat function)
214 :group 'todos)
215
216(defcustom todos-categories-buffer "*Todos Categories*"
217 "Name of buffer displayed by `todos-display-categories'."
218 :type 'string 279 :type 'string
219 :group 'todos) 280 :group 'todos)
220 281
221(defcustom todos-categories-category-label "Category" 282(defcustom todos-top-priorities-buffer "Todos top priorities"
222 "Category button label in `todos-categories-buffer'." 283 "Name of buffer displaying top priorities in Todos Filter mode."
223 :type 'string 284 :type 'string
224 :group 'todos) 285 :group 'todos)
225 286
226(defcustom todos-categories-todo-label "Todo" 287(defcustom todos-diary-items-buffer "Todos diary items"
227 "Todo button label in `todos-categories-buffer'." 288 "Name of buffer displaying diary items in Todos Filter mode."
228 :type 'string 289 :type 'string
229 :group 'todos) 290 :group 'todos)
230 291
231(defcustom todos-categories-diary-label "Diary" 292(defcustom todos-regexp-items-buffer "Todos regexp items"
232 "Diary button label in `todos-categories-buffer'." 293 "Name of buffer displaying regexp items in Todos Filter mode."
233 :type 'string 294 :type 'string
234 :group 'todos) 295 :group 'todos)
235 296
236(defcustom todos-categories-done-label "Done" 297(defcustom todos-custom-items-buffer "Todos custom items"
237 "Done button label in `todos-categories-buffer'." 298 "Name of buffer displaying custom items in Todos Filter mode."
238 :type 'string 299 :type 'string
239 :group 'todos) 300 :group 'todos)
240 301
241(defcustom todos-categories-archived-label "Archived" 302(defcustom todos-priorities-rules nil
242 "Archived button label in `todos-categories-buffer'." 303 "List of rules giving how many items `todos-top-priorities' shows.
243 :type 'string 304This variable should be set interactively by
305`\\[todos-set-top-priorities-in-file]' or
306`\\[todos-set-top-priorities-in-category]'.
307
308Each rule is a list of the form (FILE NUM ALIST), where FILE is a
309member of `todos-files', NUM is a number specifying the default
310number of top priority items for each category in that file, and
311ALIST, when non-nil, consists of conses of a category name in
312FILE and a number specifying the default number of top priority
313items in that category, which overrides NUM."
314 :type 'list
244 :group 'todos) 315 :group 'todos)
245 316
246(defcustom todos-categories-totals-label "Totals" 317(defcustom todos-show-priorities 1
247 "String to label total item counts in `todos-categories-buffer'." 318 "Default number of top priorities shown by `todos-top-priorities'."
248 :type 'string 319 :type 'integer
249 :group 'todos) 320 :group 'todos)
250 321
251(defcustom todos-categories-number-separator " | " 322(defcustom todos-filter-function nil
252 "String between number and category in `todos-categories-buffer'. 323 ""
253This separates the number from the category name in the default 324 :type 'function
254categories display according to priority."
255 :type 'string
256 :group 'todos) 325 :group 'todos)
257 326
258(defcustom todos-categories-align 'center 327(defcustom todos-filter-files nil
259 "Alignment of category names in `todos-categories-buffer'." 328 "List of default files for multifile item filtering."
260 :type '(radio (const left) (const center) (const right)) 329 :type `(set ,@(mapcar (lambda (f) (list 'const f))
330 (mapcar 'todos-short-file-name
331 (funcall todos-files-function))))
261 :group 'todos) 332 :group 'todos)
262 333
334;; FIXME: is there a better alternative to this?
335(defun todos-reevaluate-filter-files-defcustom ()
336 "Reevaluate defcustom of `todos-filter-files'.
337Called after adding or deleting a Todos file."
338 (eval (defcustom todos-filter-files nil
339 "List of files for multifile item filtering."
340 :type `(set ,@(mapcar (lambda (f) (list 'const f))
341 (mapcar 'todos-short-file-name
342 (funcall todos-files-function))))
343 :group 'todos)))
344
263(defcustom todos-ignore-archived-categories nil 345(defcustom todos-ignore-archived-categories nil
264 "Non-nil to ignore categories with only archived items. 346 "Non-nil to ignore categories with only archived items.
265When non-nil such categories are omitted from `todos-categories' 347When non-nil such categories are omitted from `todos-categories'
@@ -273,21 +355,17 @@ archived categories."
273 :set 'todos-reset-categories 355 :set 'todos-reset-categories
274 :group 'todos) 356 :group 'todos)
275 357
276;; FIXME 358(defun todos-reset-categories (symbol value)
277(defcustom todos-edit-buffer "*Todos Edit*" 359 "The :set function for `todos-ignore-archived-categories'."
278 "Name of current buffer in Todos Edit mode." 360 (custom-set-default symbol value)
279 :type 'string 361 (dolist (f (funcall todos-files-function))
280 :group 'todos) 362 (with-current-buffer (find-file-noselect f)
281 363 (if value
282;; (defcustom todos-edit-buffer "*Todos Top Priorities*" 364 (setq todos-categories-full todos-categories
283;; "TODO Edit buffer name." 365 todos-categories (todos-truncate-categories-list))
284;; :type 'string 366 (setq todos-categories todos-categories-full
285;; :group 'todos) 367 todos-categories-full nil))
286 368 (todos-category-select))))
287;; (defcustom todos-edit-buffer "*Todos Diary Entries*"
288;; "TODO Edit buffer name."
289;; :type 'string
290;; :group 'todos)
291 369
292(defcustom todos-use-only-highlighted-region t 370(defcustom todos-use-only-highlighted-region t
293 "Non-nil to enable inserting only highlighted region as new item." 371 "Non-nil to enable inserting only highlighted region as new item."
@@ -317,29 +395,70 @@ the diary date."
317 :initialize 'custom-initialize-default 395 :initialize 'custom-initialize-default
318 :set 'todos-reset-nondiary-marker) 396 :set 'todos-reset-nondiary-marker)
319 397
398(defun todos-reset-nondiary-marker (symbol value)
399 "The :set function for user option `todos-nondiary-marker'."
400 (let ((oldvalue (symbol-value symbol))
401 (files (append todos-files todos-archives)))
402 (custom-set-default symbol value)
403 ;; Need to reset these to get font-locking right.
404 (setq todos-nondiary-start (nth 0 todos-nondiary-marker)
405 todos-nondiary-end (nth 1 todos-nondiary-marker)
406 todos-date-string-start
407 ;; See comment in defvar of `todos-date-string-start'.
408 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|"
409 (regexp-quote diary-nonmarking-symbol) "\\)?"))
410 (when (not (equal value oldvalue))
411 (dolist (f files)
412 (with-current-buffer (find-file-noselect f)
413 (let (buffer-read-only)
414 (widen)
415 (goto-char (point-min))
416 (while (not (eobp))
417 (if (re-search-forward
418 (concat "^\\(" todos-done-string-start "[^][]+] \\)?"
419 "\\(?1:" (regexp-quote (car oldvalue))
420 "\\)" todos-date-pattern "\\( "
421 diary-time-regexp "\\)?\\(?2:"
422 (regexp-quote (cadr oldvalue)) "\\)")
423 nil t)
424 (progn
425 (replace-match (nth 0 value) t t nil 1)
426 (replace-match (nth 1 value) t t nil 2))
427 (forward-line)))
428 (todos-category-select)))))))
429
320(defcustom todos-print-function 'ps-print-buffer-with-faces 430(defcustom todos-print-function 'ps-print-buffer-with-faces
321 "Function called to print buffer content; see `todos-print'." 431 "Function called to print buffer content; see `todos-print'."
322 :type 'symbol 432 :type 'symbol
323 :group 'todos) 433 :group 'todos)
324 434
325;; FIXME: rename, change meaning of zero, refer to todos-priorities-rules 435(defcustom todos-completion-ignore-case nil
326(defcustom todos-show-priorities 1 436 "Non-nil means case of user input in `todos-read-*' is ignored."
327 "Default number of priorities to show by `todos-top-priorities'. 437 :type 'boolean
3280 means show all entries."
329 :type 'integer
330 :group 'todos)
331
332(defcustom todos-print-priorities 0
333 "Default number of priorities to print by `todos-print'.
3340 means print all entries."
335 :type 'integer
336 :group 'todos) 438 :group 'todos)
337 439
338(defcustom todos-completion-ignore-case t ;; FIXME: nil for release? 440(defcustom todos-highlight-item nil
339 "Non-nil means don't consider case significant in `todos-read-category'." 441 "Non-nil means highlight items at point."
340 :type 'boolean 442 :type 'boolean
443 :initialize 'custom-initialize-default
444 :set 'todos-reset-highlight-item
341 :group 'todos) 445 :group 'todos)
342 446
447(defun todos-reset-highlight-item (symbol value)
448 "The :set function for `todos-highlight-item'."
449 (let ((oldvalue (symbol-value symbol))
450 (files (append todos-files todos-archives)))
451 (custom-set-default symbol value)
452 (when (not (equal value oldvalue))
453 (dolist (f files)
454 (let ((buf (get-file-buffer f)))
455 (when buf
456 (with-current-buffer buf
457 (require 'hl-line)
458 (if value
459 (hl-line-mode 1)
460 (hl-line-mode -1)))))))))
461
343(defcustom todos-always-add-time-string nil 462(defcustom todos-always-add-time-string nil
344 "Non-nil adds current time to a new item's date header by default. 463 "Non-nil adds current time to a new item's date header by default.
345When the Todos insertion commands have a non-nil \"maybe-notime\" 464When the Todos insertion commands have a non-nil \"maybe-notime\"
@@ -350,99 +469,341 @@ current time, if nil, they include it."
350 :group 'todos) 469 :group 'todos)
351 470
352(defcustom todos-wrap-lines t 471(defcustom todos-wrap-lines t
353 "Non-nil to wrap long lines by `todos-line-wrapping-function'." ;FIXME 472 "Non-nil to wrap long lines via `todos-line-wrapping-function'."
354 :group 'todos 473 :group 'todos
355 :type 'boolean) 474 :type 'boolean)
356 475
357(defcustom todos-line-wrapping-function 'todos-wrap-and-indent 476(defcustom todos-line-wrapping-function 'todos-wrap-and-indent
358 "Function called when `todos-wrap-lines' is non-nil." ;FIXME 477 "Line wrapping function used with non-nil `todos-wrap-lines'."
359 :group 'todos 478 :group 'todos
360 :type 'function) 479 :type 'function)
361 480
481(defun todos-wrap-and-indent ()
482 "Use word wrapping on long lines and indent with a wrap prefix.
483The amount of indentation is given by user option
484`todos-indent-to-here'."
485 (set (make-local-variable 'word-wrap) t)
486 (set (make-local-variable 'wrap-prefix) (make-string todos-indent-to-here 32))
487 (unless (member '(continuation) fringe-indicator-alist)
488 (push '(continuation) fringe-indicator-alist)))
489
490;; FIXME: :set function (otherwise change takes effect only after revisiting)
362(defcustom todos-indent-to-here 6 491(defcustom todos-indent-to-here 6
363 "Number of spaces `todos-line-wrapping-function' indents to." 492 "Number of spaces `todos-line-wrapping-function' indents to."
364 :type 'integer 493 :type '(integer :validate
494 (lambda (widget)
495 (unless (> (widget-value widget) 0)
496 (widget-put widget :error
497 "Invalid value: must be a positive integer")
498 widget)))
365 :group 'todos) 499 :group 'todos)
366 500
501(defun todos-indent ()
502 "Indent from point to `todos-indent-to-here'."
503 (indent-to todos-indent-to-here todos-indent-to-here))
504
505(defcustom todos-todo-mode-date-time-regexp
506 (concat "\\(?1:[0-9]\\{4\\}\\)-\\(?2:[0-9]\\{2\\}\\)-"
507 "\\(?3:[0-9]\\{2\\}\\) \\(?4:[0-9]\\{2\\}:[0-9]\\{2\\}\\)")
508 "Regexp matching legacy todo-mode.el item date-time strings.
509In order for `todos-convert-legacy-files' to correctly convert this
510string to the current Todos format, the regexp must contain four
511explicitly numbered groups (see `(elisp) Regexp Backslash'),
512where group 1 matches a string for the year, group 2 a string for
513the month, group 3 a string for the day and group 4 a string for
514the time. The default value converts date-time strings built
515using the default value of `todo-time-string-format' from
516todo-mode.el."
517 :type 'regexp
518 :group 'todos)
519
520(defgroup todos-categories nil
521 "Faces for Todos Categories mode."
522 :version "24.2"
523 :group 'todos)
524
525(defcustom todos-categories-category-label "Category"
526 "Category button label in Todos Categories mode."
527 :type 'string
528 :group 'todos-categories)
529
530(defcustom todos-categories-todo-label "Todo"
531 "Todo button label in Todos Categories mode."
532 :type 'string
533 :group 'todos-categories)
534
535(defcustom todos-categories-diary-label "Diary"
536 "Diary button label in Todos Categories mode."
537 :type 'string
538 :group 'todos-categories)
539
540(defcustom todos-categories-done-label "Done"
541 "Done button label in Todos Categories mode."
542 :type 'string
543 :group 'todos-categories)
544
545(defcustom todos-categories-archived-label "Archived"
546 "Archived button label in Todos Categories mode."
547 :type 'string
548 :group 'todos-categories)
549
550(defcustom todos-categories-totals-label "Totals"
551 "String to label total item counts in Todos Categories mode."
552 :type 'string
553 :group 'todos-categories)
554
555(defcustom todos-categories-number-separator " | "
556 "String between number and category in Todos Categories mode.
557This separates the number from the category name in the default
558categories display according to priority."
559 :type 'string
560 :group 'todos-categories)
561
562(defcustom todos-categories-align 'center
563 "Alignment of category names in Todos Categories mode."
564 :type '(radio (const left) (const center) (const right))
565 :group 'todos-categories)
566
367;; --------------------------------------------------------------------------- 567;; ---------------------------------------------------------------------------
368;;; Faces 568;;; Faces
369 569
370(defgroup todos-faces nil 570(defgroup todos-faces nil
371 "Faces for the Todos modes." 571 "Faces for the Todos modes."
372 :version "24.1" 572 :version "24.2"
373 :group 'todos) 573 :group 'todos)
374 574
375(defface todos-prefix-string 575(defface todos-prefix-string
376 '((t :inherit font-lock-constant-face)) 576 ;; '((t :inherit font-lock-constant-face))
577 '((((class grayscale) (background light))
578 (:foreground "LightGray" :weight bold :underline t))
579 (((class grayscale) (background dark))
580 (:foreground "Gray50" :weight bold :underline t))
581 (((class color) (min-colors 88) (background light)) (:foreground "dark cyan"))
582 (((class color) (min-colors 88) (background dark)) (:foreground "Aquamarine"))
583 (((class color) (min-colors 16) (background light)) (:foreground "CadetBlue"))
584 (((class color) (min-colors 16) (background dark)) (:foreground "Aquamarine"))
585 (((class color) (min-colors 8)) (:foreground "magenta"))
586 (t (:weight bold :underline t)))
377 "Face for Todos prefix string." 587 "Face for Todos prefix string."
378 :group 'todos-faces) 588 :group 'todos-faces)
379 589
380(defface todos-mark 590(defface todos-mark
381 '((t :inherit font-lock-warning-face)) 591 ;; '((t :inherit font-lock-warning-face))
592 '((((class color)
593 (min-colors 88)
594 (background light))
595 (:weight bold :foreground "Red1"))
596 (((class color)
597 (min-colors 88)
598 (background dark))
599 (:weight bold :foreground "Pink"))
600 (((class color)
601 (min-colors 16)
602 (background light))
603 (:weight bold :foreground "Red1"))
604 (((class color)
605 (min-colors 16)
606 (background dark))
607 (:weight bold :foreground "Pink"))
608 (((class color)
609 (min-colors 8))
610 (:foreground "red"))
611 (t
612 (:weight bold :inverse-video t)))
382 "Face for marks on Todos items." 613 "Face for marks on Todos items."
383 :group 'todos-faces) 614 :group 'todos-faces)
384 615
385(defface todos-button 616(defface todos-button
386 '((t :inherit widget-field)) 617 ;; '((t :inherit widget-field))
618 '((((type tty))
619 (:foreground "black" :background "yellow3"))
620 (((class grayscale color)
621 (background light))
622 (:background "gray85"))
623 (((class grayscale color)
624 (background dark))
625 (:background "dim gray"))
626 (t
627 (:slant italic)))
387 "Face for buttons in todos-display-categories." 628 "Face for buttons in todos-display-categories."
388 :group 'todos-faces) 629 :group 'todos-faces)
389 630
390(defface todos-sorted-column 631(defface todos-sorted-column
391 '((t :inherit fringe)) 632 ;; '((t :inherit fringe))
633 '((((class color)
634 (background light))
635 (:foreground "grey95"))
636 (((class color)
637 (background dark))
638 (:foreground "grey10"))
639 (t
640 (:foreground "gray")))
392 "Face for buttons in todos-display-categories." 641 "Face for buttons in todos-display-categories."
393 :group 'todos-faces) 642 :group 'todos-faces)
394 643
395(defface todos-archived-only 644(defface todos-archived-only
396 '((t (:inherit (shadow)))) 645 ;; '((t (:inherit (shadow))))
646 '((((class color)
647 (background light))
648 (:foreground "grey50"))
649 (((class color)
650 (background dark))
651 (:foreground "grey70"))
652 (t
653 (:foreground "gray")))
397 "Face for archived-only categories in todos-display-categories." 654 "Face for archived-only categories in todos-display-categories."
398 :group 'todos-faces) 655 :group 'todos-faces)
399 656
400(defface todos-search 657(defface todos-search
401 '((t :inherit match)) 658 ;; '((t :inherit match))
659 '((((class color)
660 (min-colors 88)
661 (background light))
662 (:background "yellow1"))
663 (((class color)
664 (min-colors 88)
665 (background dark))
666 (:background "RoyalBlue3"))
667 (((class color)
668 (min-colors 8)
669 (background light))
670 (:foreground "black" :background "yellow"))
671 (((class color)
672 (min-colors 8)
673 (background dark))
674 (:foreground "white" :background "blue"))
675 (((type tty)
676 (class mono))
677 (:inverse-video t))
678 (t
679 (:background "gray")))
402 "Face for matches found by todos-search." 680 "Face for matches found by todos-search."
403 :group 'todos-faces) 681 :group 'todos-faces)
404 682
683(defface todos-diary-expired
684 ;; '((t :inherit font-lock-warning-face))
685 '((((class color)
686 (min-colors 16))
687 (:weight bold :foreground "DarkOrange"))
688 (((class color))
689 (:weight bold :foreground "yellow"))
690 (t
691 (:weight bold)))
692 "Face for expired dates of diary items."
693 :group 'todos-faces)
694(defvar todos-diary-expired-face 'todos-diary-expired)
695
405(defface todos-date 696(defface todos-date
406 '((t :inherit diary)) 697 '((t :inherit diary))
407 "Face for Todos prefix string." 698 "Face for the date string of a Todos item."
408 :group 'todos-faces) 699 :group 'todos-faces)
409(defvar todos-date-face 'todos-date) 700(defvar todos-date-face 'todos-date)
410 701
411(defface todos-time 702(defface todos-time
412 '((t :inherit diary-time)) 703 '((t :inherit diary-time))
413 "Face for Todos prefix string." 704 "Face for the time string of a Todos item."
414 :group 'todos-faces) 705 :group 'todos-faces)
415(defvar todos-time-face 'todos-time) 706(defvar todos-time-face 'todos-time)
416 707
417(defface todos-done 708(defface todos-done
418 '((t :inherit font-lock-comment-face)) 709 ;; '((t :inherit font-lock-comment-face))
710 '((((class grayscale)
711 (background light))
712 (:slant italic :weight bold :foreground "DimGray"))
713 (((class grayscale)
714 (background dark))
715 (:slant italic :weight bold :foreground "LightGray"))
716 (((class color)
717 (min-colors 88)
718 (background light))
719 (:foreground "Firebrick"))
720 (((class color)
721 (min-colors 88)
722 (background dark))
723 (:foreground "chocolate1"))
724 (((class color)
725 (min-colors 16)
726 (background light))
727 (:foreground "red"))
728 (((class color)
729 (min-colors 16)
730 (background dark))
731 (:foreground "red1"))
732 (((class color)
733 (min-colors 8)
734 (background light))
735 (:foreground "red"))
736 (((class color)
737 (min-colors 8)
738 (background dark))
739 (:foreground "yellow"))
740 (t
741 (:slant italic :weight bold)))
419 "Face for done Todos item header string." 742 "Face for done Todos item header string."
420 :group 'todos-faces) 743 :group 'todos-faces)
421(defvar todos-done-face 'todos-done) 744(defvar todos-done-face 'todos-done)
422 745
423(defface todos-comment 746(defface todos-comment
424 '((t :inherit font-lock-comment-face)) 747 '((t :inherit todos-done))
425 "Face for comments appended to done Todos items." 748 "Face for comments appended to done Todos items."
426 :group 'todos-faces) 749 :group 'todos-faces)
427(defvar todos-comment-face 'todos-comment) 750(defvar todos-comment-face 'todos-comment)
428 751
429(defface todos-done-sep 752(defface todos-done-sep
430 '((t :inherit font-lock-type-face)) 753 ;; '((t :inherit font-lock-type-face))
754 '((((class grayscale)
755 (background light))
756 (:weight bold :foreground "Gray90"))
757 (((class grayscale)
758 (background dark))
759 (:weight bold :foreground "DimGray"))
760 (((class color)
761 (min-colors 88)
762 (background light))
763 (:foreground "ForestGreen"))
764 (((class color)
765 (min-colors 88)
766 (background dark))
767 (:foreground "PaleGreen"))
768 (((class color)
769 (min-colors 16)
770 (background light))
771 (:foreground "ForestGreen"))
772 (((class color)
773 (min-colors 16)
774 (background dark))
775 (:foreground "PaleGreen"))
776 (((class color)
777 (min-colors 8))
778 (:foreground "green"))
779 (t
780 (:underline t :weight bold)))
431 "Face for separator string bewteen done and not done Todos items." 781 "Face for separator string bewteen done and not done Todos items."
432 :group 'todos-faces) 782 :group 'todos-faces)
433(defvar todos-done-sep-face 'todos-done-sep) 783(defvar todos-done-sep-face 'todos-done-sep)
434 784
435(defvar todos-font-lock-keywords 785(defvar todos-font-lock-keywords
436 (list 786 (list
787 ;; '(todos-nondiary-marker-matcher 1 todos-nondiary-face t)
788 ;; '(todos-nondiary-marker-matcher 2 todos-nondiary-face t)
789 '(todos-nondiary-marker-matcher 1 todos-done-sep-face t)
790 '(todos-nondiary-marker-matcher 2 todos-done-sep-face t)
791 ;; This is the face used by diary-lib.el.
792 '(todos-diary-nonmarking-matcher 1 font-lock-constant-face t)
437 '(todos-date-string-matcher 1 todos-date-face t) 793 '(todos-date-string-matcher 1 todos-date-face t)
438 '(todos-time-string-matcher 1 todos-time-face t) 794 '(todos-time-string-matcher 1 todos-time-face t)
439 '(todos-done-string-matcher 0 todos-done-face t) 795 '(todos-done-string-matcher 0 todos-done-face t)
440 '(todos-comment-string-matcher 1 todos-done-face t) 796 '(todos-comment-string-matcher 1 todos-done-face t)
441 '(todos-category-string-matcher 1 todos-done-sep-face t)) 797 ;; '(todos-category-string-matcher 1 todos-done-sep-face t)
442 "Font-locking for Todos mode.") 798 '(todos-category-string-matcher-1 1 todos-done-sep-face t t)
799 '(todos-category-string-matcher-2 1 todos-done-sep-face t t)
800 '(todos-diary-expired-matcher 1 todos-diary-expired-face t)
801 '(todos-diary-expired-matcher 2 todos-diary-expired-face t t)
802 )
803 "Font-locking for Todos modes.")
443 804
444;; --------------------------------------------------------------------------- 805;; ---------------------------------------------------------------------------
445;;; Modes setup 806;;; Todos mode local variables and hook functions
446 807
447(defvar todos-files (funcall todos-files-function) 808(defvar todos-files (funcall todos-files-function)
448 "List of truenames of user's Todos files.") 809 "List of truenames of user's Todos files.")
@@ -450,34 +811,29 @@ current time, if nil, they include it."
450(defvar todos-archives (funcall todos-files-function t) 811(defvar todos-archives (funcall todos-files-function t)
451 "List of truenames of user's Todos archives.") 812 "List of truenames of user's Todos archives.")
452 813
453(defvar todos-categories nil
454 "Alist of categories in the current Todos file.
455The elements are cons cells whose car is a category name and
456whose cdr is a vector of the category's item counts. These are,
457in order, the numbers of todo items, todo items included in the
458Diary, done items and archived items.")
459
460(defvar todos-categories-full nil
461 "Variable holding non-truncated copy of `todos-categories'.
462Set when `todos-ignore-archived-categories' is set to non-nil, to
463restore full `todos-categories' list when
464`todos-ignore-archived-categories' is reset to nil.")
465
466(defvar todos-current-todos-file nil 814(defvar todos-current-todos-file nil
467 "Variable holding the name of the currently active Todos file.") 815 "Variable holding the name of the currently active Todos file.")
468;; Automatically set by `todos-switch-todos-file'.")
469 816
470;; FIXME: Add function to kill-buffer-hook that sets this to the latest 817(defun todos-show-current-file ()
471;; existing Todos file or else todos-default-todos-file on killing the buffer 818 "Visit current instead of default Todos file with `todos-show'.
472;; of a Todos file 819This function is added to `pre-command-hook' when user option
473(defvar todos-global-current-todos-file nil 820`todos-show-current-file' is set to non-nil."
474 "Variable holding name of current Todos file. 821 (setq todos-global-current-todos-file todos-current-todos-file))
475Used by functions called from outside of Todos mode to visit the 822 ;; (and (eq major-mode 'todos-mode)
476current Todos file rather than the default Todos file (i.e. when 823 ;; (setq todos-global-current-todos-file (buffer-file-name))))
477users option `todos-show-current-file' is non-nil).") 824
825(defun todos-after-find-file ()
826 "Show Todos files correctly when visited from outside of Todos mode."
827 (and (member this-command todos-visit-files-commands)
828 (= (- (point-max) (point-min)) (buffer-size))
829 (member major-mode '(todos-mode todos-archive-mode))
830 (todos-category-select)))
478 831
479(defun todos-reset-global-current-todos-file () 832(defun todos-reset-global-current-todos-file ()
480 "" 833 "Update the value of `todos-global-current-todos-file'.
834This becomes the latest existing Todos file or, if there is none,
835the value of `todos-default-todos-file'.
836This function is added to `kill-buffer-hook' in Todos mode."
481 (let ((buflist (copy-sequence (buffer-list))) 837 (let ((buflist (copy-sequence (buffer-list)))
482 (cur todos-global-current-todos-file)) 838 (cur todos-global-current-todos-file))
483 (catch 'done 839 (catch 'done
@@ -492,23 +848,77 @@ users option `todos-show-current-file' is non-nil).")
492 (if (equal cur todos-global-current-todos-file) 848 (if (equal cur todos-global-current-todos-file)
493 (setq todos-global-current-todos-file todos-default-todos-file)))) 849 (setq todos-global-current-todos-file todos-default-todos-file))))
494 850
851(defvar todos-categories nil
852 "Alist of categories in the current Todos file.
853The elements are cons cells whose car is a category name and
854whose cdr is a vector of the category's item counts. These are,
855in order, the numbers of todo items, todo items included in the
856Diary, done items and archived items.")
857
858(defvar todos-categories-full nil
859 "Variable holding non-truncated copy of `todos-categories'.
860Set when `todos-ignore-archived-categories' is set to non-nil, to
861restore full `todos-categories' list when
862`todos-ignore-archived-categories' is reset to nil.")
863
864(defvar todos-categories-with-marks nil
865 "Alist of categories and number of marked items they contain.")
866
495(defvar todos-category-number 1 867(defvar todos-category-number 1
496 "Variable holding the number of the current Todos category. 868 "Variable holding the number of the current Todos category.
497This number is one more than the index of the category in 869Todos categories are numbered starting from 1.")
498`todos-categories'.")
499 870
500(defvar todos-first-visit t 871(defvar todos-first-visit t
501 "Non-nil if first display of this file in the current session. 872 "Non-nil if first display of this file in the current session.
502See `todos-display-categories-first'.") 873See `todos-display-categories-first'.")
503 874
504;; FIXME: rename? 875(defvar todos-show-done-only nil
505(defvar todos-tmp-buffer-name " *todo tmp*") 876 "If non-nil display only done items in current category.
877Set by `todos-toggle-show-done-only' and used by
878`todos-category-select'.")
506 879
507(defvar todos-category-beg "--==-- " 880;; ---------------------------------------------------------------------------
508 "String marking beginning of category (inserted with its name).") 881;;; Global variables and helper functions
509 882
510(defvar todos-category-done "==--== DONE " 883(defvar todos-global-current-todos-file nil
511 "String marking beginning of category's done items.") 884 "Variable holding name of current Todos file.
885Used by functions called from outside of Todos mode to visit the
886current Todos file rather than the default Todos file (i.e. when
887users option `todos-show-current-file' is non-nil).")
888
889(defun todos-reevaluate-defcustoms ()
890 "Reevaluate defcustoms that show list of Todos files."
891 (custom-set-default 'todos-default-todos-file
892 (symbol-value 'todos-default-todos-file))
893 (todos-reevaluate-default-file-defcustom)
894 (custom-set-default 'todos-filter-files (symbol-value 'todos-filter-files))
895 (todos-reevaluate-filter-files-defcustom))
896
897(defvar todos-edit-buffer "*Todos Edit*"
898 "Name of current buffer in Todos Edit mode.")
899
900(defvar todos-categories-buffer "*Todos Categories*"
901 "Name of buffer in Todos Categories mode.")
902
903(defvar todos-print-buffer "*Todos Print*"
904 "Name of buffer containing printable Todos text.")
905
906(defvar todos-date-pattern
907 (let ((dayname (diary-name-pattern calendar-day-name-array nil t)))
908 (concat "\\(?:" dayname "\\|"
909 (let ((dayname)
910 ;; FIXME: how to choose between abbreviated and unabbreviated
911 ;; month name?
912 (monthname (format "\\(?:%s\\|\\*\\)"
913 (diary-name-pattern
914 calendar-month-name-array
915 calendar-month-abbrev-array t)))
916 (month "\\(?:[0-9]+\\|\\*\\)")
917 (day "\\(?:[0-9]+\\|\\*\\)")
918 (year "-?\\(?:[0-9]+\\|\\*\\)"))
919 (mapconcat 'eval calendar-date-display-form ""))
920 "\\)"))
921 "Regular expression matching a Todos date header.")
512 922
513(defvar todos-nondiary-start (nth 0 todos-nondiary-marker) 923(defvar todos-nondiary-start (nth 0 todos-nondiary-marker)
514 "String inserted before item date to block diary inclusion.") 924 "String inserted before item date to block diary inclusion.")
@@ -516,20 +926,1171 @@ See `todos-display-categories-first'.")
516(defvar todos-nondiary-end (nth 1 todos-nondiary-marker) 926(defvar todos-nondiary-end (nth 1 todos-nondiary-marker)
517 "String inserted after item date matching `todos-nondiary-start'.") 927 "String inserted after item date matching `todos-nondiary-start'.")
518 928
519(defvar todos-show-done-only nil 929;; By itself this matches anything, because of the `?'; however, it's only
520 "If non-nil display only done items in current category. 930;; used in the context of `todos-date-pattern' (but Emacs Lisp lacks
521Set by `todos-toggle-show-done-only' and used by 931;; lookahead).
522`todos-category-select'.") 932(defvar todos-date-string-start
933 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|"
934 (regexp-quote diary-nonmarking-symbol) "\\)?")
935 "Regular expression matching part of item header before the date.")
936
937(defvar todos-done-string-start
938 (concat "^\\[" (regexp-quote todos-done-string))
939 "Regular expression matching start of done item.")
940
941(defun todos-date-string-matcher (lim)
942 "Search for Todos date string within LIM for font-locking."
943 (re-search-forward
944 (concat todos-date-string-start "\\(?1:" todos-date-pattern "\\)") lim t))
945
946(defun todos-time-string-matcher (lim)
947 "Search for Todos time string within LIM for font-locking."
948 (re-search-forward (concat todos-date-string-start todos-date-pattern
949 " \\(?1:" diary-time-regexp "\\)") lim t))
950
951(defun todos-nondiary-marker-matcher (lim)
952 "Search for Todos nondiary markers within LIM for font-locking."
953 (re-search-forward (concat "^\\(?1:" (regexp-quote todos-nondiary-start) "\\)"
954 todos-date-pattern "\\(?: " diary-time-regexp
955 "\\)?\\(?2:" (regexp-quote todos-nondiary-end) "\\)")
956 lim t))
957
958(defun todos-diary-nonmarking-matcher (lim)
959 "Search for diary nonmarking symbol within LIM for font-locking."
960 (re-search-forward (concat "^\\(?1:" (regexp-quote diary-nonmarking-symbol)
961 "\\)" todos-date-pattern) lim t))
962
963(defun todos-diary-expired-matcher (lim)
964 "Search for expired diary item date within LIM for font-locking."
965 (when (re-search-forward (concat "^\\(?:"
966 (regexp-quote diary-nonmarking-symbol)
967 "\\)?\\(?1:" todos-date-pattern "\\) \\(?2:"
968 diary-time-regexp "\\)?") lim t)
969 (let* ((date (match-string-no-properties 1))
970 (time (match-string-no-properties 2))
971 ;; days-between needs a non-empty time string.
972 (date-time (concat date " " (or time "00:00"))))
973 (or (and (not (string-match ".+day\\|\\*" date))
974 (< (days-between date-time (current-time-string)) 0))
975 (todos-diary-expired-matcher lim)))))
976
977(defun todos-done-string-matcher (lim)
978 "Search for Todos done header within LIM for font-locking."
979 (re-search-forward (concat todos-done-string-start
980 "[^][]+]")
981 lim t))
982
983(defun todos-comment-string-matcher (lim)
984 "Search for Todos done comment within LIM for font-locking."
985 (re-search-forward (concat "\\[\\(?1:" todos-comment-string "\\):")
986 lim t))
523 987
988;; (defun todos-category-string-matcher (lim)
989;; "Search for Todos category name within LIM for font-locking.
990;; This is for fontifying category names appearing in Todos filter
991;; mode."
992;; (if (eq major-mode 'todos-filter-items-mode)
993;; (re-search-forward
994;; (concat "^\\(?:" todos-date-string-start "\\)?" todos-date-pattern
995;; "\\(?: " diary-time-regexp "\\)?\\(?:"
996;; (regexp-quote todos-nondiary-end) "\\)? \\(?1:\\[.+\\]\\)")
997;; lim t)))
998
999(defun todos-category-string-matcher-1 (lim)
1000 "Search for Todos category name within LIM for font-locking.
1001This is for fontifying category names appearing in Todos filter
1002mode following done items."
1003 (if (eq major-mode 'todos-filter-items-mode)
1004 (re-search-forward (concat todos-done-string-start todos-date-pattern
1005 "\\(?: " diary-time-regexp
1006 ;; Use non-greedy operator to prevent
1007 ;; capturing possible following non-diary
1008 ;; date string.
1009 "\\)?] \\(?1:\\[.+?\\]\\)")
1010 lim t)))
1011
1012(defun todos-category-string-matcher-2 (lim)
1013 "Search for Todos category name within LIM for font-locking.
1014This is for fontifying category names appearing in Todos filter
1015mode following todo (not done) items."
1016 (if (eq major-mode 'todos-filter-items-mode)
1017 (re-search-forward (concat todos-date-string-start todos-date-pattern
1018 "\\(?: " diary-time-regexp "\\)?\\(?:"
1019 (regexp-quote todos-nondiary-end)
1020 "\\)? \\(?1:\\[.+\\]\\)")
1021 lim t)))
1022
1023(defun todos-category-number (cat)
1024 "Return the number of category CAT in this Todos file.
1025The buffer-local variable `todos-category-number' holds this
1026number as its value."
1027 (let ((categories (mapcar 'car todos-categories)))
1028 (setq todos-category-number
1029 ;; Increment by one, so that the highest priority category in Todos
1030 ;; Categories mode is numbered one rather than zero.
1031 (1+ (- (length categories)
1032 (length (member cat categories)))))))
1033
1034(defun todos-current-category ()
1035 "Return the name of the current category."
1036 (car (nth (1- todos-category-number) todos-categories)))
1037
1038(defconst todos-category-beg "--==-- "
1039 "String marking beginning of category (inserted with its name).")
1040
1041(defconst todos-category-done "==--== DONE "
1042 "String marking beginning of category's done items.")
1043
1044(defun todos-category-select ()
1045 "Display the current category correctly."
1046 (let ((name (todos-current-category))
1047 cat-begin cat-end done-start done-sep-start done-end)
1048 (widen)
1049 (goto-char (point-min))
1050 (re-search-forward
1051 (concat "^" (regexp-quote (concat todos-category-beg name)) "$") nil t)
1052 (setq cat-begin (1+ (line-end-position)))
1053 (setq cat-end (if (re-search-forward
1054 (concat "^" (regexp-quote todos-category-beg)) nil t)
1055 (match-beginning 0)
1056 (point-max)))
1057 (setq mode-line-buffer-identification
1058 (funcall todos-mode-line-function name))
1059 (narrow-to-region cat-begin cat-end)
1060 (todos-prefix-overlays)
1061 (goto-char (point-min))
1062 (if (re-search-forward (concat "\n\\(" (regexp-quote todos-category-done)
1063 "\\)") nil t)
1064 (progn
1065 (setq done-start (match-beginning 0))
1066 (setq done-sep-start (match-beginning 1))
1067 (setq done-end (match-end 0)))
1068 (error "Category %s is missing todos-category-done string" name))
1069 (if todos-show-done-only
1070 (narrow-to-region (1+ done-end) (point-max))
1071 (when (and todos-show-with-done
1072 (re-search-forward todos-done-string-start nil t))
1073 ;; Now we want to see the done items, so reset displayed end to end of
1074 ;; done items.
1075 (setq done-start cat-end)
1076 ;; Make display overlay for done items separator string, unless there
1077 ;; already is one.
1078 (let* ((done-sep todos-done-separator)
1079 (ovs (overlays-at done-sep-start))
1080 ov-sep)
1081 (unless (and ovs (string= (overlay-get (car ovs) 'display) done-sep))
1082 (setq ov-sep (make-overlay done-sep-start done-end))
1083 (overlay-put ov-sep 'display done-sep))))
1084 (narrow-to-region (point-min) done-start)
1085 ;; Loading this from todos-mode, or adding it to the mode hook, causes
1086 ;; Emacs to hang in todos-item-start, at looking-at.
1087 (when todos-highlight-item
1088 (require 'hl-line)
1089 (hl-line-mode 1)))))
1090
1091(defun todos-get-count (type &optional category)
1092 "Return count of TYPE items in CATEGORY.
1093If CATEGORY is nil, default to the current category."
1094 (let* ((cat (or category (todos-current-category)))
1095 (counts (cdr (assoc cat todos-categories)))
1096 (idx (cond ((eq type 'todo) 0)
1097 ((eq type 'diary) 1)
1098 ((eq type 'done) 2)
1099 ((eq type 'archived) 3))))
1100 (aref counts idx)))
1101
1102(defun todos-set-count (type increment &optional category)
1103 "Increment count of TYPE items in CATEGORY by INCREMENT.
1104If CATEGORY is nil, default to the current category."
1105 (let* ((cat (or category (todos-current-category)))
1106 (counts (cdr (assoc cat todos-categories)))
1107 (idx (cond ((eq type 'todo) 0)
1108 ((eq type 'diary) 1)
1109 ((eq type 'done) 2)
1110 ((eq type 'archived) 3))))
1111 (aset counts idx (+ increment (aref counts idx)))))
1112
1113(defun todos-set-categories ()
1114 "Set `todos-categories' from the sexp at the top of the file."
1115 ;; New archive files created by `todos-move-category' are empty, which would
1116 ;; make the sexp test fail and raise an error, so in this case we skip it.
1117 (unless (zerop (buffer-size))
1118 (save-excursion
1119 (save-restriction
1120 (widen)
1121 (goto-char (point-min))
1122 ;; todos-truncate-categories-list needs non-nil todos-categories.
1123 (setq todos-categories-full
1124 (if (looking-at "\(\(\"")
1125 (read (buffer-substring-no-properties
1126 (line-beginning-position)
1127 (line-end-position)))
1128 (error "Invalid or missing todos-categories sexp"))
1129 todos-categories todos-categories-full)))
1130 (if (and todos-ignore-archived-categories
1131 (eq major-mode 'todos-mode))
1132 (todos-truncate-categories-list)
1133 todos-categories-full)))
1134
1135(defun todos-update-categories-sexp ()
1136 "Update the `todos-categories' sexp at the top of the file."
1137 (let (buffer-read-only)
1138 (save-excursion
1139 (save-restriction
1140 (widen)
1141 (goto-char (point-min))
1142 (if (looking-at (concat "^" (regexp-quote todos-category-beg)))
1143 (progn (newline) (goto-char (point-min)))
1144 ;; With empty buffer (e.g. with new archive in
1145 ;; `todos-move-category') `kill-line' signals end of buffer.
1146 (kill-region (line-beginning-position) (line-end-position)))
1147 ;; todos-categories-full is nil on adding first category.
1148 (prin1 (or todos-categories-full todos-categories)
1149 (current-buffer))))))
1150
1151(defun todos-make-categories-list (&optional force)
1152 "Return an alist of Todos categories and their item counts.
1153With non-nil argument FORCE parse the entire file to build the
1154list; otherwise, get the value by reading the sexp at the top of
1155the file."
1156 (setq todos-categories nil)
1157 (save-excursion
1158 (save-restriction
1159 (widen)
1160 (goto-char (point-min))
1161 (let (counts cat archive)
1162 (when buffer-file-name ; Don't check with `todos-convert-legacy-files'.
1163 ;; FIXME: can todos-archives be too old here?
1164 (unless (member buffer-file-name (funcall todos-files-function t))
1165 (setq archive (concat (file-name-sans-extension
1166 todos-current-todos-file) ".toda"))))
1167 (while (not (eobp))
1168 (cond ((looking-at (concat (regexp-quote todos-category-beg)
1169 "\\(.*\\)\n"))
1170 (setq cat (match-string-no-properties 1))
1171 ;; Counts for each category: [todo diary done archive]
1172 (setq counts (make-vector 4 0))
1173 (setq todos-categories
1174 (append todos-categories (list (cons cat counts))))
1175 ;; todos-archives may be too old here (e.g. during
1176 ;; todos-move-category).
1177 (when (member archive (funcall todos-files-function t))
1178 (let ((archive-count 0))
1179 (with-current-buffer (find-file-noselect archive)
1180 (widen)
1181 (goto-char (point-min))
1182 (when (re-search-forward
1183 (concat (regexp-quote todos-category-beg) cat)
1184 (point-max) t)
1185 (forward-line)
1186 (while (not (or (looking-at
1187 (concat
1188 (regexp-quote todos-category-beg)
1189 "\\(.*\\)\n"))
1190 (eobp)))
1191 (when (looking-at todos-done-string-start)
1192 (setq archive-count (1+ archive-count)))
1193 (forward-line))))
1194 (todos-set-count 'archived archive-count cat))))
1195 ((looking-at todos-done-string-start)
1196 (todos-set-count 'done 1 cat))
1197 ((looking-at (concat "^\\("
1198 (regexp-quote diary-nonmarking-symbol)
1199 "\\)?" todos-date-pattern))
1200 (todos-set-count 'diary 1 cat)
1201 (todos-set-count 'todo 1 cat))
1202 ((looking-at (concat todos-date-string-start todos-date-pattern))
1203 (todos-set-count 'todo 1 cat))
1204 ;; If first line is todos-categories list, use it and end loop
1205 ;; -- unless FORCEd to scan whole file.
1206 ((bobp)
1207 (unless force
1208 (setq todos-categories (read (buffer-substring-no-properties
1209 (line-beginning-position)
1210 (line-end-position))))
1211 (goto-char (1- (point-max))))))
1212 (forward-line)))))
1213 todos-categories)
1214
1215(defun todos-truncate-categories-list ()
1216 "Return a truncated alist of Todos categories plus item counts.
1217Categories containing only archived items are omitted. This list
1218is used in Todos mode when `todos-ignore-archived-categories' is
1219non-nil."
1220 (let (cats)
1221 (dolist (catcons todos-categories-full cats)
1222 (let ((cat (car catcons)))
1223 (setq cats
1224 (append cats
1225 (unless (and (zerop (todos-get-count 'todo cat))
1226 (zerop (todos-get-count 'done cat))
1227 (not (zerop (todos-get-count 'archived cat))))
1228 (list catcons))))))))
1229
1230(defun todos-check-format ()
1231 "Signal an error if the current Todos file is ill-formatted.
1232Otherwise return t. The error message gives the line number
1233where the invalid formatting was found."
1234 (save-excursion
1235 (save-restriction
1236 (widen)
1237 (goto-char (point-min))
1238 ;; Check for `todos-categories' sexp as the first line
1239 (let ((cats (prin1-to-string (or todos-categories-full todos-categories))))
1240 (unless (looking-at (regexp-quote cats))
1241 (error "Invalid or missing todos-categories sexp")))
1242 (forward-line)
1243 (let ((legit (concat "\\(^" (regexp-quote todos-category-beg) "\\)"
1244 "\\|\\(" todos-date-string-start todos-date-pattern "\\)"
1245 "\\|\\(^[ \t]+[^ \t]*\\)"
1246 "\\|^$"
1247 "\\|\\(^" (regexp-quote todos-category-done) "\\)"
1248 "\\|\\(" todos-done-string-start "\\)")))
1249 (while (not (eobp))
1250 (unless (looking-at legit)
1251 (error "Illegitimate Todos file format at line %d"
1252 (line-number-at-pos (point))))
1253 (forward-line)))))
1254 ;; (message "This Todos file is well-formatted.")
1255 t)
1256
1257(defun todos-repair-categories-sexp ()
1258 "Repair corrupt Todos categories sexp."
1259 (interactive)
1260 (let ((todos-categories-full (todos-make-categories-list t)))
1261 (todos-update-categories-sexp)))
1262
1263(defvar todos-item-start (concat "\\(" todos-date-string-start "\\|"
1264 todos-done-string-start "\\)"
1265 todos-date-pattern)
1266 "String identifying start of a Todos item.")
1267
1268(defun todos-item-start ()
1269 "Move to start of current Todos item and return its position."
1270 (unless (or
1271 ;; Point is either on last item in this category or on the empty
1272 ;; line between done and not done items.
1273 (looking-at "^$")
1274 ;; There are no done items in this category yet.
1275 (looking-at (regexp-quote todos-category-beg)))
1276 (goto-char (line-beginning-position))
1277 (while (not (looking-at todos-item-start))
1278 (forward-line -1))
1279 (point)))
1280
1281(defun todos-item-end ()
1282 "Move to end of current Todos item and return its position."
1283 ;; Items cannot end with a blank line.
1284 (unless (looking-at "^$")
1285 (let ((done (todos-done-item-p)))
1286 (todos-forward-item)
1287 ;; Adjust if item is last unfinished one before displayed done items.
1288 (when (and (not done) (todos-done-item-p))
1289 (forward-line -1))
1290 (backward-char))
1291 (point)))
1292
1293(defun todos-item-string ()
1294 "Return bare text of current item as a string."
1295 (let ((opoint (point))
1296 (start (todos-item-start))
1297 (end (todos-item-end)))
1298 (goto-char opoint)
1299 (and start end (buffer-substring-no-properties start end))))
1300
1301(defun todos-remove-item ()
1302 "Internal function called in editing, deleting or moving items."
1303 (let* ((beg (todos-item-start))
1304 (end (progn (todos-item-end) (1+ (point))))
1305 (ovs (overlays-in beg beg)))
1306 ;; There can be both prefix/number and mark overlays.
1307 (while ovs (delete-overlay (car ovs)) (pop ovs))
1308 (delete-region beg end)))
1309
1310(defun todos-diary-item-p ()
1311 "Return non-nil if item at point is marked for diary inclusion."
1312 (save-excursion
1313 (todos-item-start)
1314 ;; (looking-at todos-date-pattern)))
1315 (not (looking-at (regexp-quote todos-nondiary-start)))))
1316
1317(defun todos-done-item-p ()
1318 "Return non-nil if item at point is a done item."
1319 (save-excursion
1320 (todos-item-start)
1321 (looking-at todos-done-string-start)))
1322
1323(defvar todos-item-mark (propertize (if (equal todos-prefix "*") "@" "*")
1324 'face 'todos-mark)
1325 "String used to mark items.")
1326
1327(defun todos-marked-item-p ()
1328 "If this item is marked, return mark overlay."
1329 (let ((ovs (overlays-in (line-beginning-position) (line-beginning-position)))
1330 (mark todos-item-mark)
1331 ov marked)
1332 (catch 'stop
1333 (while ovs
1334 (setq ov (pop ovs))
1335 (and (equal (overlay-get ov 'before-string) mark)
1336 (throw 'stop (setq marked t)))))
1337 (when marked ov)))
1338
1339(defun todos-insert-with-overlays (item)
1340 "Insert ITEM at point and update prefix/priority number overlays."
1341 (todos-item-start)
1342 (insert item "\n")
1343 (todos-backward-item)
1344 (todos-prefix-overlays))
1345
1346(defun todos-prefix-overlays ()
1347 "Put before-string overlay in front of this category's items.
1348The overlay's value is the string `todos-prefix' or with non-nil
1349`todos-number-prefix' an integer in the sequence from 1 to the
1350number of todo or done items in the category indicating the
1351item's priority. Todo and done items are numbered independently
1352of each other."
1353 (when (or todos-number-prefix
1354 (not (string-match "^[[:space:]]*$" todos-prefix)))
1355 (let ((prefix (propertize (concat todos-prefix " ")
1356 'face 'todos-prefix-string))
1357 (num 0))
1358 (save-excursion
1359 (goto-char (point-min))
1360 (while (not (eobp))
1361 (when (or (todos-date-string-matcher (line-end-position))
1362 (todos-done-string-matcher (line-end-position)))
1363 (goto-char (match-beginning 0))
1364 (when todos-number-prefix
1365 (setq num (1+ num))
1366 ;; Reset number to 1 for first done item.
1367 (when (and (looking-at todos-done-string-start)
1368 (looking-back (concat "^"
1369 (regexp-quote todos-category-done)
1370 "\n")))
1371 (setq num 1))
1372 (setq prefix (propertize (concat (number-to-string num) " ")
1373 'face 'todos-prefix-string)))
1374 (let ((ovs (overlays-in (point) (point)))
1375 marked ov-pref)
1376 (if ovs
1377 (dolist (ov ovs)
1378 (let ((val (overlay-get ov 'before-string)))
1379 (if (equal val "*")
1380 (setq marked t)
1381 (setq ov-pref val)))))
1382 (unless (equal ov-pref prefix)
1383 ;; Why doesn't this work?
1384 ;; (remove-overlays (point) (point) 'before-string)
1385 (remove-overlays (point) (point))
1386 (overlay-put (make-overlay (point) (point))
1387 'before-string prefix)
1388 (and marked (overlay-put (make-overlay (point) (point))
1389 'before-string todos-item-mark)))))
1390 (forward-line))))))
1391
1392(defun todos-read-file-name (prompt &optional archive mustmatch)
1393 "Choose and return the name of a Todos file, prompting with PROMPT.
1394
1395Show completions with TAB or SPC; the names are shown in short
1396form but the absolute truename is returned. With non-nil ARCHIVE
1397return the absolute truename of a Todos archive file. With non-nil
1398MUSTMATCH the name of an existing file must be chosen;
1399otherwise, a new file name is allowed."
1400 (unless (file-exists-p todos-files-directory)
1401 (make-directory todos-files-directory))
1402 (let ((completion-ignore-case todos-completion-ignore-case)
1403 (files (mapcar 'file-name-sans-extension
1404 (directory-files todos-files-directory nil
1405 (if archive "\.toda$" "\.todo$"))))
1406 (file ""))
1407 (while (string= "" file)
1408 (setq file (completing-read prompt files nil mustmatch))
1409 (setq prompt "Enter a non-empty name (TAB for list of current files): "))
1410 (setq file (concat todos-files-directory file
1411 (if archive ".toda" ".todo")))
1412 (unless mustmatch
1413 (when (not (member file todos-files))
1414 (todos-validate-name file 'file)))
1415 (file-truename file)))
1416
1417(defun todos-read-category (prompt &optional mustmatch)
1418 "Choose and return a category name, prompting with PROMPT.
1419Show completions with TAB or SPC. With non-nil MUSTMATCH the
1420name must be that of an existing category; otherwise, a new
1421category name is allowed, after checking its validity."
1422 ;; Allow SPC to insert spaces, for adding new category names.
1423 (let ((map minibuffer-local-completion-map))
1424 (define-key map " " nil)
1425 ;; Make a copy of todos-categories in case history-delete-duplicates is
1426 ;; non-nil, which makes completing-read alter todos-categories.
1427 (let* ((categories (copy-sequence todos-categories))
1428 (history (cons 'todos-categories (1+ todos-category-number)))
1429 (completion-ignore-case todos-completion-ignore-case)
1430 (cat (completing-read prompt todos-categories nil
1431 mustmatch nil history
1432 ;; Default for existing categories is the
1433 ;; current category.
1434 (if todos-categories
1435 (todos-current-category)
1436 ;; Trigger prompt for initial category
1437 ""))))
1438 (unless mustmatch
1439 (when (not (assoc cat categories))
1440 (todos-validate-name cat 'category)
1441 (if (y-or-n-p (format (concat "There is no category \"%s\" in "
1442 "this file; add it? ") cat))
1443 (todos-add-category cat)
1444 (keyboard-quit))))
1445 ;; Restore the original value of todos-categories.
1446 (setq todos-categories categories)
1447 cat)))
1448
1449(defun todos-validate-name (name type)
1450 "Prompt for new NAME for TYPE until it is valid, then return it.
1451TYPE can be either a file or a category"
1452 (let (prompt file cat shortname)
1453 (while
1454 (and (cond ((string= "" name)
1455 (setq prompt
1456 (cond ((eq type 'file)
1457 ;; FIXME: just todos-files ?
1458 (if (funcall (todos-files))
1459 "Enter a non-empty file name: "
1460 ;; Empty string passed by todos-show to
1461 ;; prompt for initial Todos file.
1462 (concat "Initial file name ["
1463 todos-initial-file "]: ")))
1464 ((eq type 'category)
1465 (if todos-categories
1466 "Enter a non-empty category name: "
1467 ;; Empty string passed by todos-show to
1468 ;; prompt for initial category of a new
1469 ;; Todos file.
1470 (concat "Initial category name ["
1471 todos-initial-category "]: "))))))
1472 ((string-match "\\`\\s-+\\'" name)
1473 (setq prompt
1474 "Enter a name that does not contain only white space: "))
1475 ((and (eq type 'file) (member name todos-files))
1476 (setq prompt "Enter a non-existing file name: "))
1477 ((and (eq type 'category) (assoc name todos-categories))
1478 (setq prompt "Enter a non-existing category name: ")))
1479 (setq name (if (or (and (eq type 'file) todos-files)
1480 (and (eq type 'category) todos-categories))
1481 (read-from-minibuffer prompt)
1482 ;; Offer default initial name.
1483 (read-string prompt nil nil
1484 (cond ((eq type 'file)
1485 todos-initial-file)
1486 ((eq type 'category)
1487 todos-initial-category))))))))
1488 name)
1489
1490;; Adapted from calendar-read-date and calendar-date-string.
1491(defun todos-read-date ()
1492 "Prompt for Gregorian date and return it in the current format.
1493Also accepts `*' as an unspecified month, day, or year."
1494 (let* ((year (calendar-read
1495 ;; FIXME: maybe better like monthname with RET for current month
1496 "Year (>0 or * for any year): "
1497 (lambda (x) (or (eq x '*) (> x 0)))
1498 (number-to-string (calendar-extract-year
1499 (calendar-current-date)))))
1500 (month-array (vconcat calendar-month-name-array (vector "*")))
1501 (abbrevs (vconcat calendar-month-abbrev-array (vector "*")))
1502 (completion-ignore-case todos-completion-ignore-case)
1503 (monthname (completing-read
1504 "Month name (RET for current month, * for any month): "
1505 (mapcar 'list (append month-array nil))
1506 nil t nil nil
1507 (calendar-month-name (calendar-extract-month
1508 (calendar-current-date)) t)))
1509 (month (cdr (assoc-string
1510 monthname (calendar-make-alist month-array nil nil
1511 abbrevs))))
1512 (last (if (= month 13)
1513 31 ; FIXME: what about shorter months?
1514 (let ((yr (if (eq year '*)
1515 1999 ; FIXME: no Feb. 29
1516 year)))
1517 (calendar-last-day-of-month month yr))))
1518 day dayname)
1519 (while (if (numberp day) (or (< day 0) (< last day)) (not (eq day '*)))
1520 (setq day (read-from-minibuffer
1521 (format "Day (1-%d or RET for today or * for any day): " last)
1522 nil nil t nil
1523 (number-to-string
1524 (calendar-extract-day (calendar-current-date))))))
1525 (setq year (if (eq year '*) (symbol-name '*) (number-to-string year)))
1526 (setq day (if (eq day '*) (symbol-name '*) (number-to-string day)))
1527 ;; FIXME: make abbreviation customizable
1528 (setq monthname
1529 (or (and (= month 13) "*")
1530 (calendar-month-name (calendar-extract-month (list month day year))
1531 t)))
1532 (mapconcat 'eval calendar-date-display-form "")))
1533
1534(defun todos-read-dayname ()
1535 "Choose name of a day of the week with completion and return it."
1536 (let ((completion-ignore-case todos-completion-ignore-case))
1537 (completing-read "Enter a day name: "
1538 (append calendar-day-name-array nil)
1539 nil t)))
1540
1541(defun todos-read-time ()
1542 "Prompt for and return a valid clock time as a string.
1543
1544Valid time strings are those matching `diary-time-regexp'.
1545Typing `<return>' at the prompt returns the current time, if the
1546user option `todos-always-add-time-string' is non-nil, otherwise
1547the empty string (i.e., no time string)."
1548 (let (valid answer)
1549 (while (not valid)
1550 (setq answer (read-string "Enter a clock time: " nil nil
1551 (when todos-always-add-time-string
1552 (substring (current-time-string) 11 16))))
1553 (when (or (string= "" answer)
1554 (string-match diary-time-regexp answer))
1555 (setq valid t)))
1556 answer))
1557
1558(defun todos-convert-legacy-date-time ()
1559 "Return converted date-time string.
1560Helper function for `todos-convert-legacy-files'."
1561 (let* ((year (match-string 1))
1562 (month (match-string 2))
1563 (monthname (calendar-month-name (string-to-number month) t))
1564 (day (match-string 3))
1565 (time (match-string 4))
1566 dayname)
1567 (replace-match "")
1568 (insert (mapconcat 'eval calendar-date-display-form "")
1569 (when time (concat " " time)))))
1570
1571;; ---------------------------------------------------------------------------
1572;;; Item filtering
1573
1574(defvar todos-multiple-files nil
1575 "List of files returned by `todos-multiple-files' widget.")
1576
1577(defvar todos-multiple-files-widget nil
1578 "Variable holding widget created by `todos-multiple-files'.")
1579
1580(defun todos-multiple-files ()
1581 "Pop to a buffer with a widget for choosing multiple filter files."
1582 (require 'widget)
1583 (eval-when-compile
1584 (require 'wid-edit))
1585 (with-current-buffer (get-buffer-create "*Todos Filter Files*")
1586 (pop-to-buffer (current-buffer))
1587 (erase-buffer)
1588 (kill-all-local-variables)
1589 (widget-insert "Select files for generating the top priorities list.\n\n")
1590 (setq todos-multiple-files-widget
1591 (widget-create
1592 `(set ,@(mapcar (lambda (x) (list 'const x))
1593 (mapcar 'todos-short-file-name
1594 (funcall todos-files-function))))))
1595 (widget-insert "\n")
1596 (widget-create 'push-button
1597 :notify (lambda (widget &rest ignore)
1598 (setq todos-multiple-files 'quit)
1599 (quit-window t)
1600 (exit-recursive-edit))
1601 "Cancel")
1602 (widget-insert " ")
1603 (widget-create 'push-button
1604 :notify (lambda (&rest ignore)
1605 (setq todos-multiple-files
1606 (mapcar (lambda (f)
1607 (concat todos-files-directory
1608 f ".todo"))
1609 (widget-value
1610 todos-multiple-files-widget)))
1611 (quit-window t)
1612 (exit-recursive-edit))
1613 "Apply")
1614 (use-local-map widget-keymap)
1615 (widget-setup))
1616 (message "Click \"Apply\" after selecting files.")
1617 (recursive-edit))
1618
1619;; FIXME: should done and archived items be included? Maybe just for regexp
1620;; and custom filters?
1621(defun todos-filter-items (filter &optional multifile)
1622 "Build and display a list of items from different categories.
1623
1624The items are selected according to the value of FILTER, which
1625can be `top' for top priority items, `diary' for diary items,
1626`regexp' for items matching a regular expresion entered by the
1627user, or `custom' for items chosen by user-defined function
1628`todos-filter-function'.
1629
1630With non-nil argument MULTIFILE list top priorities of multiple
1631Todos files, by default those in `todos-filter-files'."
1632 (let ((num (if (consp filter) (cdr filter) todos-show-priorities))
1633 (buf (get-buffer-create todos-filter-buffer))
1634 (files (list todos-current-todos-file))
1635 regexp fname bufstr cat beg end done)
1636 (when multifile
1637 (setq files (if (or (consp filter) (null todos-filter-files))
1638 (progn (todos-multiple-files) todos-multiple-files)
1639 todos-filter-files)
1640 todos-multiple-files nil))
1641 (if (eq files 'quit) (keyboard-quit))
1642 (if (null files)
1643 (error "No files have been chosen for filtering")
1644 (with-current-buffer buf
1645 (erase-buffer)
1646 (kill-all-local-variables)
1647 (todos-filter-items-mode))
1648 (when (eq filter 'regexp)
1649 (setq regexp (read-string "Enter a regular expression: ")))
1650 (save-current-buffer
1651 (dolist (f files)
1652 ;; Before inserting file contents into temp buffer, save a modified
1653 ;; buffer visiting it.
1654 (let ((bf (find-buffer-visiting f)))
1655 (when (buffer-modified-p bf)
1656 (with-current-buffer bf (save-buffer))))
1657 (setq fname (todos-short-file-name f))
1658 (with-temp-buffer
1659 (insert-file-contents f)
1660 (goto-char (point-min))
1661 (let (fnum)
1662 ;; Unless the number of items to show was supplied by prefix
1663 ;; argument of caller, override `todos-show-priorities' with the
1664 ;; file-wide value from `todos-priorities-rules'.
1665 (unless (consp filter)
1666 (setq fnum (nth 1 (assoc f todos-priorities-rules))))
1667 (unless (looking-at (concat "^" (regexp-quote todos-category-beg)))
1668 (kill-line 1))
1669 (while (re-search-forward
1670 (concat "^" (regexp-quote todos-category-beg) "\\(.+\\)\n")
1671 nil t)
1672 (setq cat (match-string 1))
1673 (let (cnum)
1674 ;; Unless the number of items to show was supplied by prefix
1675 ;; argument of caller, override the file-wide value from
1676 ;; `todos-priorities-rules' if set, else
1677 ;; `todos-show-priorities' with non-nil category-wide value
1678 ;; from `todos-priorities-rules'.
1679 (unless (consp filter)
1680 (let ((cats (nth 2 (assoc f todos-priorities-rules))))
1681 (setq cnum (or (cdr (assoc cat cats))
1682 fnum
1683 ;; FIXME: need this?
1684 todos-show-priorities))))
1685 (delete-region (match-beginning 0) (match-end 0))
1686 (setq beg (point)) ; Start of first item.
1687 (setq end (if (re-search-forward
1688 (concat "^" (regexp-quote todos-category-beg))
1689 nil t)
1690 (match-beginning 0)
1691 (point-max)))
1692 (goto-char beg)
1693 (setq done
1694 (if (re-search-forward
1695 (concat "\n" (regexp-quote todos-category-done))
1696 end t)
1697 (match-beginning 0)
1698 end))
1699 ;; Leave done items only with regexp filter.
1700 ;; FIXME: and custom filter?
1701 (unless (eq filter 'regexp)
1702 (delete-region done end)
1703 (setq end done))
1704 (narrow-to-region beg end) ; Process current category.
1705 (goto-char (point-min))
1706 ;; Apply the filter.
1707 (cond ((eq filter 'diary)
1708 (while (not (eobp))
1709 (if (looking-at (regexp-quote todos-nondiary-start))
1710 (todos-remove-item)
1711 (todos-forward-item))))
1712 ((eq filter 'regexp)
1713 (while (not (eobp))
1714 (if (looking-at todos-item-start)
1715 (if (string-match regexp (todos-item-string))
1716 (todos-forward-item)
1717 (todos-remove-item))
1718 ;; Kill lines that aren't part of a todo or done
1719 ;; item (empty or todos-category-done).
1720 (delete-region (line-beginning-position)
1721 (1+ (line-end-position))))
1722 ;; If last todo item in file matches regexp and
1723 ;; there are no following done items,
1724 ;; todos-category-done string is left dangling,
1725 ;; because todos-forward-item jumps over it.
1726 (if (and (eobp) (looking-back
1727 (concat (regexp-quote todos-done-string)
1728 "\n")))
1729 (delete-region (point) (progn
1730 (forward-line -2)
1731 (point))))))
1732 ((eq filter 'custom)
1733 (if todos-filter-function
1734 (funcall todos-filter-function)
1735 (error "No custom filter function has been defined")))
1736 (t ; Filter top priority items.
1737 (setq num (or cnum fnum num))
1738 (unless (zerop num)
1739 (todos-forward-item num))))
1740 (setq beg (point))
1741 (unless (member filter '(diary regexp custom))
1742 (delete-region beg end))
1743 (goto-char (point-min))
1744 ;; Add file (if using multiple files) and category tags to
1745 ;; item.
1746 (while (not (eobp))
1747 (when (re-search-forward
1748 (concat "\\(" todos-done-string-start
1749 todos-date-pattern "\\( " diary-time-regexp
1750 "\\)?]\\)\\|\\("
1751 ;; todos-date-string-start doesn't work
1752 ;; here because of `^'
1753 "\\(" (regexp-quote todos-nondiary-start)
1754 "\\|" (regexp-quote diary-nonmarking-symbol)
1755 "\\)?" todos-date-pattern "\\( "
1756 diary-time-regexp "\\)?"
1757 (regexp-quote todos-nondiary-end) "?\\)")
1758 nil t)
1759 (insert (concat " [" (if multifile (concat fname ":"))
1760 cat "]")))
1761 (forward-line))
1762 (widen)))
1763 (setq bufstr (buffer-string))
1764 (with-current-buffer buf
1765 (let (buffer-read-only)
1766 (insert bufstr)))))))
1767 ;; FIXME: let-bind todos-mode-line-control according to FILTER?
1768 (set-window-buffer (selected-window) (set-buffer buf))
1769 (todos-prefix-overlays)
1770 (goto-char (point-min))
1771 ;; FIXME: this is necessary -- why?
1772 (font-lock-fontify-buffer))))
1773
1774(defun todos-set-top-priorities (&optional arg)
1775 "Set number of top priorities shown by `todos-top-priorities'.
1776With non-nil ARG, set the number only for the current Todos
1777category; otherwise, set the number for all categories in the
1778current Todos file.
1779
1780Calling this function via either of the commands
1781`todos-set-top-priorities-in-file' or
1782`todos-set-top-priorities-in-category' is the recommended way to
1783set the user customizable option `todos-priorities-rules'."
1784 (let* ((cat (todos-current-category))
1785 (file todos-current-todos-file)
1786 (rules todos-priorities-rules)
1787 (frule (assoc-string file rules))
1788 (crule (assoc-string cat (nth 2 frule)))
1789 (cur (or (if arg (cdr crule) (nth 1 frule))
1790 todos-show-priorities))
1791 (prompt (concat "Current number of top priorities in this "
1792 (if arg "category" "file") ": %d; "
1793 "enter new number: "))
1794 (new "-1")
1795 nrule)
1796 (while (or (not (string-match "[0-9]+" new)) ; Don't accept "" or "bla".
1797 (< (string-to-number new) 0))
1798 (let ((cur0 cur))
1799 (setq new (read-string (format prompt cur0) nil nil cur0)
1800 prompt "Enter a non-negative number: "
1801 cur0 nil)))
1802 (setq new (string-to-number new))
1803 (setq nrule (if arg
1804 (append (nth 2 (delete crule frule)) (list (cons cat new)))
1805 (append (list file new) (list (nth 2 frule)))))
1806 (setq rules (cons (if arg
1807 (list file cur nrule)
1808 nrule)
1809 (delete frule rules)))
1810 (customize-save-variable 'todos-priorities-rules rules)))
1811
1812
1813;; ---------------------------------------------------------------------------
1814;;; Sorting and display routines for Todos Categories mode.
1815
1816(defun todos-longest-category-name-length (categories)
1817 "Return the length of the longest name in list CATEGORIES."
1818 (let ((longest 0))
1819 (dolist (c categories longest)
1820 (setq longest (max longest (length c))))))
1821
1822(defun todos-padded-string (str)
1823 "Return string STR padded with spaces.
1824The placement of the padding is determined by the value of user
1825option `todos-categories-align'."
1826 (let* ((categories (mapcar 'car todos-categories))
1827 (len (max (todos-longest-category-name-length categories)
1828 (length todos-categories-category-label)))
1829 (strlen (length str))
1830 (strlen-odd (eq (logand strlen 1) 1)) ; oddp from cl.el
1831 (padding (max 0 (/ (- len strlen) 2)))
1832 (padding-left (cond ((eq todos-categories-align 'left) 0)
1833 ((eq todos-categories-align 'center) padding)
1834 ((eq todos-categories-align 'right)
1835 (if strlen-odd (1+ (* padding 2)) (* padding 2)))))
1836 (padding-right (cond ((eq todos-categories-align 'left)
1837 (if strlen-odd (1+ (* padding 2)) (* padding 2)))
1838 ((eq todos-categories-align 'center)
1839 (if strlen-odd (1+ padding) padding))
1840 ((eq todos-categories-align 'right) 0))))
1841 (concat (make-string padding-left 32) str (make-string padding-right 32))))
1842
1843(defvar todos-descending-counts nil
1844 "List of keys for category counts sorted in descending order.")
1845
1846(defun todos-sort (list &optional key)
1847 "Return a copy of LIST, possibly sorted according to KEY."
1848 (let* ((l (copy-sequence list))
1849 (fn (if (eq key 'alpha)
1850 (lambda (x) (upcase x)) ; Alphabetize case insensitively.
1851 (lambda (x) (todos-get-count key x))))
1852 (descending (member key todos-descending-counts))
1853 (cmp (if (eq key 'alpha)
1854 'string<
1855 (if descending '< '>)))
1856 (pred (lambda (s1 s2) (let ((t1 (funcall fn (car s1)))
1857 (t2 (funcall fn (car s2))))
1858 (funcall cmp t1 t2)))))
1859 (when key
1860 (setq l (sort l pred))
1861 (if descending
1862 (setq todos-descending-counts
1863 (delete key todos-descending-counts))
1864 (push key todos-descending-counts)))
1865 l))
1866
1867(defun todos-display-sorted (type)
1868 "Keep point on the TYPE count sorting button just clicked."
1869 (let ((opoint (point)))
1870 (todos-update-categories-display type)
1871 (goto-char opoint)))
1872
1873(defun todos-label-to-key (label)
1874 "Return symbol for sort key associated with LABEL."
1875 (let (key)
1876 (cond ((string= label todos-categories-category-label)
1877 (setq key 'alpha))
1878 ((string= label todos-categories-todo-label)
1879 (setq key 'todo))
1880 ((string= label todos-categories-diary-label)
1881 (setq key 'diary))
1882 ((string= label todos-categories-done-label)
1883 (setq key 'done))
1884 ((string= label todos-categories-archived-label)
1885 (setq key 'archived)))
1886 key))
1887
1888(defun todos-insert-sort-button (label)
1889 "Insert button for displaying categories sorted by item counts.
1890LABEL determines which type of count is sorted."
1891 (setq str (if (string= label todos-categories-category-label)
1892 (todos-padded-string label)
1893 label))
1894 (setq beg (point))
1895 (setq end (+ beg (length str)))
1896 (insert-button str 'face nil
1897 'action
1898 `(lambda (button)
1899 (let ((key (todos-label-to-key ,label)))
1900 (if (and (member key todos-descending-counts)
1901 (eq key 'alpha))
1902 (progn
1903 ;; If display is alphabetical, switch back to
1904 ;; category order.
1905 (todos-display-sorted nil)
1906 (setq todos-descending-counts
1907 (delete key todos-descending-counts)))
1908 (todos-display-sorted key)))))
1909 (setq ovl (make-overlay beg end))
1910 (overlay-put ovl 'face 'todos-button))
1911
1912(defun todos-total-item-counts ()
1913 "Return a list of total item counts for the current file."
1914 (mapcar (lambda (i) (apply '+ (mapcar (lambda (l) (aref l i))
1915 (mapcar 'cdr todos-categories))))
1916 (list 0 1 2 3)))
1917
1918(defun todos-insert-category-line (cat &optional nonum)
1919 "Insert button displaying category CAT's name and item counts.
1920With non-nil argument NONUM show only these; otherwise, insert a
1921number in front of the button indicating the category's priority.
1922The number and the category name are separated by the string
1923which is the value of the user option
1924`todos-categories-number-separator'."
1925 (let* ((archive (member todos-current-todos-file todos-archives))
1926 (str (todos-padded-string cat))
1927 (opoint (point)))
1928 ;; num is declared in caller.
1929 (setq num (1+ num))
1930 (insert-button
1931 (concat (if nonum
1932 (make-string (+ 4 (length todos-categories-number-separator))
1933 32)
1934 (format " %3d%s" num todos-categories-number-separator))
1935 str
1936 (mapconcat (lambda (elt)
1937 (concat
1938 (make-string (1+ (/ (length (car elt)) 2)) 32) ; label
1939 (format "%3d" (todos-get-count (cdr elt) cat)) ; count
1940 ;; Add an extra space if label length is odd
1941 ;; (using def of oddp from cl.el).
1942 (if (eq (logand (length (car elt)) 1) 1) " ")))
1943 (if archive
1944 (list (cons todos-categories-done-label 'done))
1945 (list (cons todos-categories-todo-label 'todo)
1946 (cons todos-categories-diary-label 'diary)
1947 (cons todos-categories-done-label 'done)
1948 (cons todos-categories-archived-label
1949 'archived)))
1950 ""))
1951 'face (if (and todos-ignore-archived-categories
1952 (zerop (todos-get-count 'todo cat))
1953 (zerop (todos-get-count 'done cat))
1954 (not (zerop (todos-get-count 'archived cat))))
1955 'todos-archived-only
1956 nil)
1957 'action `(lambda (button) (let ((buf (current-buffer)))
1958 (todos-jump-to-category ,cat)
1959 (kill-buffer buf))))
1960 ;; Highlight the sorted count column.
1961 (let* ((beg (+ opoint 6 (length str)))
1962 end ovl)
1963 (cond ((eq nonum 'todo)
1964 (setq beg (+ beg 1 (/ (length todos-categories-todo-label) 2))))
1965 ((eq nonum 'diary)
1966 (setq beg (+ beg 1 (length todos-categories-todo-label)
1967 2 (/ (length todos-categories-diary-label) 2))))
1968 ((eq nonum 'done)
1969 (setq beg (+ beg 1 (length todos-categories-todo-label)
1970 2 (length todos-categories-diary-label)
1971 2 (/ (length todos-categories-done-label) 2))))
1972 ((eq nonum 'archived)
1973 (setq beg (+ beg 1 (length todos-categories-todo-label)
1974 2 (length todos-categories-diary-label)
1975 2 (length todos-categories-done-label)
1976 2 (/ (length todos-categories-archived-label) 2)))))
1977 (unless (= beg (+ opoint 6 (length str)))
1978 (setq end (+ beg 4))
1979 (setq ovl (make-overlay beg end))
1980 (overlay-put ovl 'face 'todos-sorted-column)))
1981 (newline)))
1982
1983(defun todos-display-categories-1 ()
1984 "Prepare buffer for displaying table of categories and item counts."
1985 (unless (eq major-mode 'todos-categories-mode)
1986 (setq todos-global-current-todos-file (or todos-current-todos-file
1987 todos-default-todos-file))
1988 (set-window-buffer (selected-window)
1989 (set-buffer (get-buffer-create todos-categories-buffer)))
1990 (kill-all-local-variables)
1991 (todos-categories-mode)
1992 (let (buffer-read-only)
1993 (erase-buffer)
1994 ;; FIXME: add usage tips?
1995 (insert (format "Category counts for Todos file \"%s\"."
1996 (todos-short-file-name todos-current-todos-file)))
1997 (newline 2)
1998 ;; Make space for the column of category numbers.
1999 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32))
2000 ;; Add the category and item count buttons (if this is the list of
2001 ;; categories in an archive, show only done item counts).
2002 (todos-insert-sort-button todos-categories-category-label)
2003 (if (member todos-current-todos-file todos-archives)
2004 (insert (concat (make-string 6 32)
2005 (format "%s" todos-categories-archived-label)))
2006 (insert (make-string 3 32))
2007 (todos-insert-sort-button todos-categories-todo-label)
2008 (insert (make-string 2 32))
2009 (todos-insert-sort-button todos-categories-diary-label)
2010 (insert (make-string 2 32))
2011 (todos-insert-sort-button todos-categories-done-label)
2012 (insert (make-string 2 32))
2013 (todos-insert-sort-button todos-categories-archived-label))
2014 (newline 2))))
2015
2016(defun todos-update-categories-display (sortkey)
2017 ""
2018 (let* ((cats0 (if (and todos-ignore-archived-categories
2019 (not (eq major-mode 'todos-categories-mode)))
2020 todos-categories-full
2021 todos-categories))
2022 (cats (todos-sort cats0 sortkey))
2023 (archive (member todos-current-todos-file todos-archives))
2024 ;; `num' is used by todos-insert-category-line.
2025 (num 0)
2026 ;; Find start of Category button if we just entered Todos Categories
2027 ;; mode.
2028 (pt (if (eq (point) (point-max))
2029 (save-excursion
2030 (forward-line -2)
2031 (goto-char (next-single-char-property-change
2032 (point) 'face nil (line-end-position))))))
2033 (buffer-read-only))
2034 (forward-line 2)
2035 (delete-region (point) (point-max))
2036 ;; Fill in the table with buttonized lines, each showing a category and
2037 ;; its item counts.
2038 (mapc (lambda (cat) (todos-insert-category-line cat sortkey))
2039 (mapcar 'car cats))
2040 (newline)
2041 ;; Add a line showing item count totals.
2042 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32)
2043 (todos-padded-string todos-categories-totals-label)
2044 (mapconcat
2045 (lambda (elt)
2046 (concat
2047 (make-string (1+ (/ (length (car elt)) 2)) 32)
2048 (format "%3d" (nth (cdr elt) (todos-total-item-counts)))
2049 ;; Add an extra space if label length is odd (using
2050 ;; definition of oddp from cl.el).
2051 (if (eq (logand (length (car elt)) 1) 1) " ")))
2052 (if archive
2053 (list (cons todos-categories-done-label 2))
2054 (list (cons todos-categories-todo-label 0)
2055 (cons todos-categories-diary-label 1)
2056 (cons todos-categories-done-label 2)
2057 (cons todos-categories-archived-label 3)))
2058 ""))
2059 ;; Put cursor on Category button initially.
2060 (if pt (goto-char pt))
2061 (setq buffer-read-only t)))
2062
2063;; ---------------------------------------------------------------------------
524;;; Todos insertion commands, key bindings and keymap 2064;;; Todos insertion commands, key bindings and keymap
525 2065
526;; http://rosettacode.org/wiki/Power_set#Common_Lisp (GFDL) 2066;; Can either of these be included in Emacs? The originals are GFDL'd.
527(defun powerset (l) 2067;; Slightly reformulated from
528 (if (null l) 2068;; http://rosettacode.org/wiki/Power_set#Common_Lisp.
529 (list nil) 2069(defun powerset-recursive (l)
530 (let ((prev (powerset (cdr l)))) 2070 (cond ((null l)
531 (append (mapcar #'(lambda (elt) (cons (car l) elt)) prev) 2071 (list nil))
532 prev)))) 2072 (t
2073 (let ((prev (todos-powerset (cdr l))))
2074 (append (mapcar (lambda (elt) (cons (car l) elt)) prev)
2075 prev)))))
2076;; Elisp implementation of http://rosettacode.org/wiki/Power_set#C
2077(defun powerset-bitwise (l)
2078 (let ((binnum (lsh 1 (length l)))
2079 pset elt)
2080 (dotimes (i binnum)
2081 (let ((bits i)
2082 (ll l))
2083 (while (not (zerop bits))
2084 (let ((arg (pop ll)))
2085 (unless (zerop (logand bits 1))
2086 (setq elt (append elt (list arg))))
2087 (setq bits (lsh bits -1))))
2088 (setq pset (append pset (list elt)))
2089 (setq elt nil)))
2090 pset))
2091
2092;; (defalias 'todos-powerset 'powerset-recursive)
2093(defalias 'todos-powerset 'powerset-bitwise)
533 2094
534;; Return list of lists of non-nil atoms produced from ARGLIST. The elements 2095;; Return list of lists of non-nil atoms produced from ARGLIST. The elements
535;; of ARGLIST may be atoms or lists. 2096;; of ARGLIST may be atoms or lists.
@@ -554,13 +2115,11 @@ Set by `todos-toggle-show-done-only' and used by
554 '(diary nonmarking (calendar date dayname) time (here region)) 2115 '(diary nonmarking (calendar date dayname) time (here region))
555 "Generator list for argument lists of Todos insertion commands.") 2116 "Generator list for argument lists of Todos insertion commands.")
556 2117
557(eval-when-compile (require 'cl)) ; remove-duplicates
558
559(defvar todos-insertion-commands-args 2118(defvar todos-insertion-commands-args
560 (let ((argslist (todos-gen-arglists todos-insertion-commands-args-genlist)) 2119 (let ((argslist (todos-gen-arglists todos-insertion-commands-args-genlist))
561 res new) 2120 res new)
562 (setq res (remove-duplicates 2121 (setq res (remove-duplicates
563 (apply 'append (mapcar 'powerset argslist)) :test 'equal)) 2122 (apply 'append (mapcar 'todos-powerset argslist)) :test 'equal))
564 (dolist (l res) 2123 (dolist (l res)
565 (unless (= 5 (length l)) 2124 (unless (= 5 (length l))
566 (let ((v (make-vector 5 nil)) elt) 2125 (let ((v (make-vector 5 nil)) elt)
@@ -613,8 +2172,8 @@ Set by `todos-toggle-show-done-only' and used by
613 2172
614(defvar todos-insertion-commands 2173(defvar todos-insertion-commands
615 (mapcar (lambda (c) 2174 (mapcar (lambda (c)
616 (eval `(todos-define-insertion-command ,@c))) 2175 (eval `(todos-define-insertion-command ,@c)))
617 todos-insertion-commands-args) 2176 todos-insertion-commands-args)
618 "List of Todos insertion commands.") 2177 "List of Todos insertion commands.")
619 2178
620(defvar todos-insertion-commands-arg-key-list 2179(defvar todos-insertion-commands-arg-key-list
@@ -633,20 +2192,6 @@ Set by `todos-toggle-show-done-only' and used by
633 (dolist (c todos-insertion-commands) 2192 (dolist (c todos-insertion-commands)
634 (let* ((key "") 2193 (let* ((key "")
635 (cname (symbol-name c))) 2194 (cname (symbol-name c)))
636 ;; (if (string-match "diary\\_>" cname) (setq key (concat key "yy")))
637 ;; (if (string-match "diary.+" cname) (setq key (concat key "y")))
638 ;; (if (string-match "nonmarking\\_>" cname) (setq key (concat key "kk")))
639 ;; (if (string-match "nonmarking.+" cname) (setq key (concat key "k")))
640 ;; (if (string-match "calendar\\_>" cname) (setq key (concat key "cc")))
641 ;; (if (string-match "calendar.+" cname) (setq key (concat key "c")))
642 ;; (if (string-match "date\\_>" cname) (setq key (concat key "dd")))
643 ;; (if (string-match "date.+" cname) (setq key (concat key "d")))
644 ;; (if (string-match "dayname\\_>" cname) (setq key (concat key "nn")))
645 ;; (if (string-match "dayname.+" cname) (setq key (concat key "n")))
646 ;; (if (string-match "time\\_>" cname) (setq key (concat key "tt")))
647 ;; (if (string-match "time.+" cname) (setq key (concat key "t")))
648 ;; (if (string-match "here" cname) (setq key (concat key "h")))
649 ;; (if (string-match "region" cname) (setq key (concat key "r")))
650 (mapc (lambda (l) 2195 (mapc (lambda (l)
651 (let ((arg (nth 0 l)) 2196 (let ((arg (nth 0 l))
652 (key1 (nth 1 l)) 2197 (key1 (nth 1 l))
@@ -666,90 +2211,104 @@ Set by `todos-toggle-show-done-only' and used by
666 map) 2211 map)
667 "Keymap for Todos mode insertion commands.") 2212 "Keymap for Todos mode insertion commands.")
668 2213
2214;; ??FIXME: use easy-mmode-define-keymap and easy-mmode-defmap
2215(defvar todos-key-bindings
2216 `(
2217 ;; display
2218 ("Cd" . todos-display-categories) ;FIXME: Cs todos-show-categories?
2219 ;("" . todos-display-categories-alphabetically)
2220 ("H" . todos-highlight-item)
2221 ("N" . todos-toggle-item-numbering)
2222 ("D" . todos-toggle-display-date-time)
2223 ("*" . todos-toggle-mark-item)
2224 ("C*" . todos-mark-category)
2225 ("Cu" . todos-unmark-category)
2226 ("PP" . todos-print)
2227 ("PF" . todos-print-to-file)
2228 ("v" . todos-toggle-view-done-items)
2229 ("V" . todos-toggle-show-done-only)
2230 ("Av" . todos-view-archived-items)
2231 ("As" . todos-show-archive)
2232 ("Ac" . todos-choose-archive)
2233 ("Y" . todos-diary-items)
2234 ;;("" . todos-update-filter-files)
2235 ("Fe" . todos-edit-multiline)
2236 ("Fh" . todos-highlight-item)
2237 ("Fn" . todos-toggle-item-numbering)
2238 ("Fd" . todos-toggle-display-date-time)
2239 ("Ftt" . todos-top-priorities)
2240 ("Ftm" . todos-top-priorities-multifile)
2241 ("Fts" . todos-set-top-priorities-in-file)
2242 ("Cts" . todos-set-top-priorities-in-category)
2243 ("Fyy" . todos-diary-items)
2244 ("Fym" . todos-diary-items-multifile)
2245 ("Fxx" . todos-regexp-items)
2246 ("Fxm" . todos-regexp-items-multifile)
2247 ("Fcc" . todos-custom-items)
2248 ("Fcm" . todos-custom-items-multifile)
2249 ;;("" . todos-save-top-priorities)
2250 ;; navigation
2251 ("f" . todos-forward-category)
2252 ("b" . todos-backward-category)
2253 ("j" . todos-jump-to-category)
2254 ("J" . todos-jump-to-category-other-file)
2255 ("n" . todos-forward-item)
2256 ("p" . todos-backward-item)
2257 ("S" . todos-search)
2258 ("X" . todos-clear-matches)
2259 ;; editing
2260 ("Fa" . todos-add-file)
2261 ("Ca" . todos-add-category)
2262 ("Cr" . todos-rename-category)
2263 ("Cg" . todos-merge-category)
2264 ;;("" . todos-merge-categories)
2265 ("Cm" . todos-move-category)
2266 ("Ck" . todos-delete-category)
2267 ("d" . todos-item-done)
2268 ("ee" . todos-edit-item)
2269 ("em" . todos-edit-multiline-item)
2270 ("eh" . todos-edit-item-header)
2271 ("edd" . todos-edit-item-date)
2272 ("edc" . todos-edit-item-date-from-calendar)
2273 ("edt" . todos-edit-item-date-is-today)
2274 ("et" . todos-edit-item-time)
2275 ("eyy" . todos-edit-item-diary-inclusion)
2276 ;; ("" . todos-edit-category-diary-inclusion)
2277 ("eyn" . todos-edit-item-diary-nonmarking)
2278 ;;("" . todos-edit-category-diary-nonmarking)
2279 ("ec" . todos-comment-done-item) ;FIXME: or just "c"?
2280 ("i" . ,todos-insertion-map)
2281 ("k" . todos-delete-item)
2282 ("m" . todos-move-item)
2283 ("M" . todos-move-item-to-file)
2284 ;; FIXME: This binding prevents `-' from being used in a numerical prefix
2285 ;; argument without typing C-u
2286 ;; ("-" . todos-raise-item-priority)
2287 ("r" . todos-raise-item-priority)
2288 ;; ("+" . todos-lower-item-priority)
2289 ("l" . todos-lower-item-priority)
2290 ("#" . todos-set-item-priority)
2291 ("u" . todos-item-undo)
2292 ("Ad" . todos-archive-done-item-or-items) ;FIXME
2293 ("AD" . todos-archive-category-done-items) ;FIXME
2294 ("Au" . todos-unarchive-items)
2295 ("AU" . todos-unarchive-category)
2296 ("s" . todos-save)
2297 ("q" . todos-quit)
2298 ([remap newline] . newline-and-indent)
2299 )
2300 "Alist pairing keys defined in Todos modes and their bindings.")
2301
669(defvar todos-mode-map 2302(defvar todos-mode-map
670 (let ((map (make-keymap))) 2303 (let ((map (make-keymap)))
671 ;; Don't suppress digit keys, so they can supply prefix arguments. 2304 ;; Don't suppress digit keys, so they can supply prefix arguments.
672 (suppress-keymap map) 2305 (suppress-keymap map)
673 ;; display commands 2306 (dolist (ck todos-key-bindings)
674 (define-key map "Cd" 'todos-display-categories) ;FIXME: Cs todos-show-categories? 2307 (define-key map (car ck) (cdr ck)))
675 ;; (define-key map "" 'todos-display-categories-alphabetically)
676 (define-key map "H" 'todos-highlight-item)
677 (define-key map "N" 'todos-toggle-item-numbering)
678 (define-key map "D" 'todos-toggle-display-date-time)
679 (define-key map "*" 'todos-toggle-mark-item)
680 (define-key map "C*" 'todos-mark-category)
681 (define-key map "Cu" 'todos-unmark-category)
682 (define-key map "P" 'todos-print)
683 ;; (define-key map "" 'todos-print-to-file)
684 (define-key map "v" 'todos-toggle-view-done-items)
685 (define-key map "V" 'todos-toggle-show-done-only)
686 (define-key map "Av" 'todos-view-archived-items)
687 (define-key map "As" 'todos-show-archive)
688 (define-key map "Ac" 'todos-choose-archive)
689 (define-key map "Y" 'todos-diary-items)
690 ;; (define-key map "" 'todos-update-merged-files)
691 ;; (define-key map "" 'todos-set-top-priorities)
692 (define-key map "Ftt" 'todos-top-priorities)
693 (define-key map "Ftm" 'todos-merged-top-priorities)
694 (define-key map "Fdd" 'todos-diary-items)
695 (define-key map "Fdm" 'todos-merged-diary-items)
696 (define-key map "Frr" 'todos-regexp-items)
697 (define-key map "Frm" 'todos-merged-regexp-items)
698 (define-key map "Fcc" 'todos-custom-items)
699 (define-key map "Fcm" 'todos-merged-custom-items)
700 ;; (define-key map "" 'todos-save-top-priorities)
701 ;; navigation commands
702 (define-key map "f" 'todos-forward-category)
703 (define-key map "b" 'todos-backward-category)
704 (define-key map "j" 'todos-jump-to-category)
705 (define-key map "J" 'todos-jump-to-category-other-file)
706 (define-key map "n" 'todos-forward-item)
707 (define-key map "p" 'todos-backward-item)
708 (define-key map "S" 'todos-search)
709 (define-key map "X" 'todos-clear-matches)
710 ;; editing commands
711 (define-key map "Fa" 'todos-add-file)
712 ;; (define-key map "" 'todos-change-default-file)
713 (define-key map "Ca" 'todos-add-category)
714 (define-key map "Cr" 'todos-rename-category)
715 (define-key map "Cg" 'todos-merge-category)
716 ;; (define-key map "" 'todos-merge-categories)
717 (define-key map "Cm" 'todos-move-category)
718 (define-key map "Ck" 'todos-delete-category)
719 (define-key map "d" 'todos-item-done)
720 (define-key map "ee" 'todos-edit-item)
721 (define-key map "em" 'todos-edit-multiline)
722 (define-key map "eh" 'todos-edit-item-header)
723 (define-key map "ed" 'todos-edit-item-date)
724 (define-key map "ey" 'todos-edit-item-date-is-today)
725 (define-key map "et" 'todos-edit-item-time)
726 (define-key map "ec" 'todos-comment-done-item) ;FIXME: or just "c"?
727 (define-key map "i" todos-insertion-map)
728 (define-key map "k" 'todos-delete-item)
729 (define-key map "m" 'todos-move-item)
730 (define-key map "M" 'todos-move-item-to-file)
731 ;; FIXME: This prevents `-' from being used in a numerical prefix argument
732 ;; without typing C-u
733 (define-key map "-" 'todos-raise-item-priority)
734 (define-key map "r" 'todos-raise-item-priority)
735 (define-key map "+" 'todos-lower-item-priority)
736 (define-key map "l" 'todos-lower-item-priority)
737 (define-key map "#" 'todos-set-item-priority)
738 (define-key map "u" 'todos-item-undo)
739 (define-key map "Ad" 'todos-archive-done-item-or-items) ;FIXME
740 (define-key map "AD" 'todos-archive-category-done-items) ;FIXME
741 ;; (define-key map "" 'todos-unarchive-items)
742 ;; (define-key map "" 'todos-unarchive-category)
743 (define-key map "y" 'todos-toggle-diary-inclusion)
744 ;; (define-key map "" 'todos-toggle-diary-inclusion)
745 ;; (define-key map "" 'todos-toggle-item-diary-nonmarking)
746 ;; (define-key map "" 'todos-toggle-diary-nonmarking)
747 (define-key map "s" 'todos-save)
748 (define-key map "q" 'todos-quit)
749 (define-key map [remap newline] 'newline-and-indent)
750 map) 2308 map)
751 "Todos mode keymap.") 2309 "Todos mode keymap.")
752 2310
2311;; FIXME
753(easy-menu-define 2312(easy-menu-define
754 todos-menu todos-mode-map "Todos Menu" 2313 todos-menu todos-mode-map "Todos Menu"
755 '("Todos" 2314 '("Todos"
@@ -774,10 +2333,10 @@ Set by `todos-toggle-show-done-only' and used by
774 "---" 2333 "---"
775 ["View Diary Items" todos-diary-items t] 2334 ["View Diary Items" todos-diary-items t]
776 ["View Top Priority Items" todos-top-priorities t] 2335 ["View Top Priority Items" todos-top-priorities t]
777 ["View Merged Top Priority Items" todos-merged-top-priorities t] 2336 ["View Multifile Top Priority Items" todos-top-priorities-multifile t]
778 "---" 2337 "---"
779 ["View Archive" todos-view-archive t] 2338 ["View Archive" todos-view-archive t]
780 ["Print Category" todos-print t]) ;FIXME 2339 ["Print Category" todos-print t])
781 ("Editing" 2340 ("Editing"
782 ["Insert New Item" todos-insert-item t] 2341 ["Insert New Item" todos-insert-item t]
783 ["Insert Item Here" todos-insert-item-here t] 2342 ["Insert Item Here" todos-insert-item-here t]
@@ -795,9 +2354,9 @@ Set by `todos-toggle-show-done-only' and used by
795 ["Delete Item" todos-delete-item t] 2354 ["Delete Item" todos-delete-item t]
796 ["Undo Done Item" todos-item-undo t] 2355 ["Undo Done Item" todos-item-undo t]
797 ["Mark/Unmark Item for Diary" todos-toggle-item-diary-inclusion t] 2356 ["Mark/Unmark Item for Diary" todos-toggle-item-diary-inclusion t]
798 ["Mark/Unmark Items for Diary" todos-toggle-diary-inclusion t] 2357 ["Mark/Unmark Items for Diary" todos-edit-item-diary-inclusion t]
799 ["Mark & Hide Done Item" todos-item-done t] 2358 ["Mark & Hide Done Item" todos-item-done t]
800 ["Archive Done Items" todos-archive-category-done-items t] ;FIXME 2359 ["Archive Done Items" todos-archive-category-done-items t]
801 "---" 2360 "---"
802 ["Add New Todos File" todos-add-file t] 2361 ["Add New Todos File" todos-add-file t]
803 ["Add New Category" todos-add-category t] 2362 ["Add New Category" todos-add-category t]
@@ -829,7 +2388,7 @@ Set by `todos-toggle-show-done-only' and used by
829 (define-key map "s" 'todos-save) 2388 (define-key map "s" 'todos-save)
830 (define-key map "S" 'todos-search) 2389 (define-key map "S" 'todos-search)
831 (define-key map "t" 'todos-show) ;FIXME: should show same category 2390 (define-key map "t" 'todos-show) ;FIXME: should show same category
832 ;; (define-key map "u" 'todos-unarchive-item) 2391 (define-key map "u" 'todos-unarchive-item)
833 (define-key map "U" 'todos-unarchive-category) 2392 (define-key map "U" 'todos-unarchive-category)
834 map) 2393 map)
835 "Todos Archive mode keymap.") 2394 "Todos Archive mode keymap.")
@@ -846,7 +2405,9 @@ Set by `todos-toggle-show-done-only' and used by
846 (suppress-keymap map t) 2405 (suppress-keymap map t)
847 ;; (define-key map "a" 'todos-display-categories-alphabetically) 2406 ;; (define-key map "a" 'todos-display-categories-alphabetically)
848 (define-key map "c" 'todos-display-categories) 2407 (define-key map "c" 'todos-display-categories)
2408 (define-key map "l" 'todos-lower-category)
849 (define-key map "+" 'todos-lower-category) 2409 (define-key map "+" 'todos-lower-category)
2410 (define-key map "r" 'todos-raise-category)
850 (define-key map "-" 'todos-raise-category) 2411 (define-key map "-" 'todos-raise-category)
851 (define-key map "n" 'forward-button) 2412 (define-key map "n" 'forward-button)
852 (define-key map "p" 'backward-button) 2413 (define-key map "p" 'backward-button)
@@ -863,22 +2424,16 @@ Set by `todos-toggle-show-done-only' and used by
863 (let ((map (make-keymap))) 2424 (let ((map (make-keymap)))
864 (suppress-keymap map t) 2425 (suppress-keymap map t)
865 ;; navigation commands 2426 ;; navigation commands
866 (define-key map "j" 'todos-jump-to-category) 2427 (define-key map "j" 'todos-jump-to-item)
2428 (define-key map [remap newline] 'todos-jump-to-item)
867 (define-key map "n" 'todos-forward-item) 2429 (define-key map "n" 'todos-forward-item)
868 (define-key map "p" 'todos-backward-item) 2430 (define-key map "p" 'todos-backward-item)
869 ;; (define-key map "S" 'todos-search)
870 ;; display commands
871 (define-key map "C" 'todos-display-categories)
872 ;; (define-key map "" 'todos-display-categories-alphabetically)
873 (define-key map "H" 'todos-highlight-item) 2431 (define-key map "H" 'todos-highlight-item)
874 (define-key map "N" 'todos-toggle-item-numbering) 2432 (define-key map "N" 'todos-toggle-item-numbering)
875 ;; (define-key map "" 'todos-toggle-display-date-time) 2433 (define-key map "D" 'todos-toggle-display-date-time)
876 (define-key map "P" 'todos-print) 2434 (define-key map "P" 'todos-print)
877 (define-key map "q" 'todos-quit) 2435 (define-key map "q" 'todos-quit)
878 (define-key map "s" 'todos-save) 2436 (define-key map "s" 'todos-save)
879 (define-key map "V" 'todos-view-archive)
880 (define-key map "v" 'todos-toggle-view-done-items)
881 (define-key map "Y" 'todos-diary-items)
882 ;; (define-key map "S" 'todos-save-top-priorities) 2437 ;; (define-key map "S" 'todos-save-top-priorities)
883 ;; editing commands 2438 ;; editing commands
884 (define-key map "l" 'todos-lower-item-priority) 2439 (define-key map "l" 'todos-lower-item-priority)
@@ -895,8 +2450,7 @@ Set by `todos-toggle-show-done-only' and used by
895 "" 2450 ""
896 (set (make-local-variable 'font-lock-defaults) '(todos-font-lock-keywords t)) 2451 (set (make-local-variable 'font-lock-defaults) '(todos-font-lock-keywords t))
897 (set (make-local-variable 'indent-line-function) 'todos-indent) 2452 (set (make-local-variable 'indent-line-function) 'todos-indent)
898 (when todos-wrap-lines (funcall todos-line-wrapping-function)) 2453 (when todos-wrap-lines (funcall todos-line-wrapping-function)))
899)
900 2454
901(defun todos-modes-set-2 () 2455(defun todos-modes-set-2 ()
902 "" 2456 ""
@@ -904,75 +2458,72 @@ Set by `todos-toggle-show-done-only' and used by
904 (setq buffer-read-only t) 2458 (setq buffer-read-only t)
905 (set (make-local-variable 'hl-line-range-function) 2459 (set (make-local-variable 'hl-line-range-function)
906 (lambda() (when (todos-item-end) 2460 (lambda() (when (todos-item-end)
907 (cons (todos-item-start) (todos-item-end))))) 2461 (cons (todos-item-start) (todos-item-end))))))
908) 2462
2463(defun todos-modes-set-3 ()
2464 (set (make-local-variable 'todos-categories-full) nil)
2465 ;; todos-set-categories sets todos-categories-full.
2466 (set (make-local-variable 'todos-categories) (todos-set-categories))
2467 (set (make-local-variable 'todos-category-number) 1)
2468 (set (make-local-variable 'todos-first-visit) t)
2469 (add-hook 'post-command-hook 'todos-after-find-file nil t))
2470
2471(put 'todos-mode 'mode-class 'special)
909 2472
910;; Autoloading isn't needed if files are identified by auto-mode-alist 2473;; Autoloading isn't needed if files are identified by auto-mode-alist
911;; ;; As calendar reads included Todos file before todos-mode is loaded. 2474;; ;; As calendar reads included Todos file before todos-mode is loaded.
912;; ;;;###autoload 2475;; ;;;###autoload
913(define-derived-mode todos-mode nil "Todos" () ;FIXME: derive from special-mode? 2476(define-derived-mode todos-mode special-mode "Todos" ()
914 "Major mode for displaying, navigating and editing Todo lists. 2477 "Major mode for displaying, navigating and editing Todo lists.
915 2478
916\\{todos-mode-map}" 2479\\{todos-mode-map}"
917 (easy-menu-add todos-menu) 2480 (easy-menu-add todos-menu)
918 (todos-modes-set-1) 2481 (todos-modes-set-1)
919 (todos-modes-set-2) 2482 (todos-modes-set-2)
2483 (todos-modes-set-3)
2484 ;; Initialize todos-current-todos-file.
920 (when (member (file-truename (buffer-file-name)) 2485 (when (member (file-truename (buffer-file-name))
921 (funcall todos-files-function)) 2486 (funcall todos-files-function))
922 (set (make-local-variable 'todos-current-todos-file) 2487 (set (make-local-variable 'todos-current-todos-file)
923 (file-truename (buffer-file-name)))) 2488 (file-truename (buffer-file-name))))
924 (set (make-local-variable 'todos-categories-full) nil)
925 ;; todos-set-categories sets todos-categories-full.
926 (set (make-local-variable 'todos-categories) (todos-set-categories))
927 (set (make-local-variable 'todos-first-visit) t) 2489 (set (make-local-variable 'todos-first-visit) t)
928 (set (make-local-variable 'todos-category-number) 1) ;0)
929 (set (make-local-variable 'todos-show-done-only) nil) 2490 (set (make-local-variable 'todos-show-done-only) nil)
930 (set (make-local-variable 'todos-categories-with-marks) nil) 2491 (set (make-local-variable 'todos-categoreis-with-marks) nil)
931 (when todos-show-current-file 2492 (when todos-show-current-file
932 (add-hook 'pre-command-hook 'todos-show-current-file nil t)) 2493 (add-hook 'pre-command-hook 'todos-show-current-file nil t))
933 (add-hook 'post-command-hook 'todos-after-find-file nil t) 2494 ;; FIXME: works more or less, but should be tied to the defcustom
2495 (add-hook 'window-configuration-change-hook
2496 (lambda ()
2497 (setq todos-done-separator (make-string (window-width) ?_)))
2498 nil t)
934 (add-hook 'kill-buffer-hook 'todos-reset-global-current-todos-file nil t)) 2499 (add-hook 'kill-buffer-hook 'todos-reset-global-current-todos-file nil t))
935 2500
936;; FIXME: 2501;; FIXME: need this?
937(defun todos-unload-hook () 2502(defun todos-unload-hook ()
938 "" 2503 ""
939 (remove-hook 'pre-command-hook 'todos-show-current-file t) 2504 (remove-hook 'pre-command-hook 'todos-show-current-file t)
940 (remove-hook 'post-command-hook 'todos-after-find-file t) 2505 (remove-hook 'post-command-hook 'todos-after-find-file t)
2506 (remove-hook 'window-configuration-change-hook
2507 (lambda ()
2508 (setq todos-done-separator
2509 (make-string (window-width) ?_))) t)
941 (remove-hook 'kill-buffer-hook 'todos-reset-global-current-todos-file t)) 2510 (remove-hook 'kill-buffer-hook 'todos-reset-global-current-todos-file t))
942 2511
943(define-derived-mode todos-archive-mode nil "Todos-Arch" () 2512(put 'todos-archive-mode 'mode-class 'special)
2513
2514(define-derived-mode todos-archive-mode todos-mode "Todos-Arch" ()
944 "Major mode for archived Todos categories. 2515 "Major mode for archived Todos categories.
945 2516
946\\{todos-archive-mode-map}" 2517\\{todos-archive-mode-map}"
947 (todos-modes-set-1) 2518 (todos-modes-set-1)
948 (todos-modes-set-2) 2519 (todos-modes-set-2)
949 (set (make-local-variable 'todos-show-done-only) t) 2520 (todos-modes-set-3)
950 (set (make-local-variable 'todos-current-todos-file) 2521 (set (make-local-variable 'todos-current-todos-file)
951 (file-truename (buffer-file-name))) 2522 (file-truename (buffer-file-name)))
952 (set (make-local-variable 'todos-categories) (todos-set-categories)) 2523 (set (make-local-variable 'todos-show-done-only) t))
953 (set (make-local-variable 'todos-category-number) 1) ; 0)
954 (add-hook 'post-command-hook 'todos-after-find-file nil t))
955
956;; FIXME: return to Todos or Archive mode
957(define-derived-mode todos-raw-mode nil "Todos Raw" ()
958 "Emergency repair mode for Todos files."
959 (when (member major-mode '(todos-mode todos-archive-mode))
960 (setq buffer-read-only nil)
961 (set (make-local-variable 'font-lock-defaults) '(todos-font-lock-keywords t))
962 (widen)
963 ;; FIXME: doesn't DTRT here
964 (todos-prefix-overlays)))
965 2524
966(define-derived-mode todos-edit-mode nil "Todos-Ed" () 2525(defun todos-mode-external-set ()
967 "Major mode for editing multiline Todo items. 2526 ""
968
969\\{todos-edit-mode-map}"
970 (todos-modes-set-1))
971
972(define-derived-mode todos-categories-mode nil "Todos-Cats" ()
973 "Major mode for displaying and editing Todos categories.
974
975\\{todos-categories-mode-map}"
976 (set (make-local-variable 'todos-current-todos-file) 2527 (set (make-local-variable 'todos-current-todos-file)
977 todos-global-current-todos-file) 2528 todos-global-current-todos-file)
978 (let ((cats (with-current-buffer (get-file-buffer todos-current-todos-file) 2529 (let ((cats (with-current-buffer (get-file-buffer todos-current-todos-file)
@@ -981,7 +2532,24 @@ Set by `todos-toggle-show-done-only' and used by
981 (todos-set-categories))))) 2532 (todos-set-categories)))))
982 (set (make-local-variable 'todos-categories) cats))) 2533 (set (make-local-variable 'todos-categories) cats)))
983 2534
984(define-derived-mode todos-filter-items-mode nil "Todos-Top" () 2535(define-derived-mode todos-edit-mode text-mode "Todos-Ed" ()
2536 "Major mode for editing multiline Todo items.
2537
2538\\{todos-edit-mode-map}"{
2539 (todos-modes-set-1)
2540 (todos-mode-external-set))
2541
2542(put 'todos-categories-mode 'mode-class 'special)
2543
2544(define-derived-mode todos-categories-mode special-mode "Todos-Cats" ()
2545 "Major mode for displaying and editing Todos categories.
2546
2547\\{todos-categories-mode-map}"
2548 (todos-mode-external-set))
2549
2550(put 'todos-filter-mode 'mode-class 'special)
2551
2552(define-derived-mode todos-filter-items-mode special-mode "Todos-Fltr" ()
985 "Mode for displaying and reprioritizing top priority Todos. 2553 "Mode for displaying and reprioritizing top priority Todos.
986 2554
987\\{todos-filter-items-mode-map}" 2555\\{todos-filter-items-mode-map}"
@@ -992,15 +2560,14 @@ Set by `todos-toggle-show-done-only' and used by
992(defun todos-save () 2560(defun todos-save ()
993 "Save the current Todos file." 2561 "Save the current Todos file."
994 (interactive) 2562 (interactive)
995 ;; (todos-update-categories-sexp)
996 (save-buffer) 2563 (save-buffer)
997 ;; (if todos-save-top-priorities-too (todos-save-top-priorities)) 2564 ;; (if todos-save-top-priorities-too (todos-save-top-priorities))
998 ) 2565 )
999 2566
1000(defun todos-quit () 2567(defun todos-quit ()
1001 "Exit the current Todos-related buffer. 2568 "Exit the current Todos-related buffer.
1002Depending on the specific mode, this either kills and the buffer 2569Depending on the specific mode, this either kills the buffer or
1003or buries it." 2570buries it and restores state as needed."
1004 (interactive) 2571 (interactive)
1005 (cond ((eq major-mode 'todos-categories-mode) 2572 (cond ((eq major-mode 'todos-categories-mode)
1006 (kill-buffer) 2573 (kill-buffer)
@@ -1010,13 +2577,14 @@ or buries it."
1010 (kill-buffer) 2577 (kill-buffer)
1011 (todos-show)) 2578 (todos-show))
1012 ((member major-mode (list 'todos-mode 'todos-archive-mode)) 2579 ((member major-mode (list 'todos-mode 'todos-archive-mode))
1013 (todos-save) 2580 ;; Have to write previously nonexistant archives to file.
2581 (unless (file-exists-p (buffer-file-name)) (todos-save))
2582 ;; FIXME: or should it save unconditionally?
2583 ;; (todos-save)
1014 (bury-buffer)))) 2584 (bury-buffer))))
1015 2585
1016;; --------------------------------------------------------------------------- 2586;; ---------------------------------------------------------------------------
1017;;; Commands 2587;;; Display Commands
1018
1019;;; Display
1020 2588
1021;;;###autoload 2589;;;###autoload
1022(defun todos-show (&optional solicit-file) 2590(defun todos-show (&optional solicit-file)
@@ -1034,11 +2602,17 @@ of `todos-categories' for the current Todos file, on subsequent
1034invocations whichever category was displayed last. If 2602invocations whichever category was displayed last. If
1035`todos-display-categories-first' is non-nil, then the first 2603`todos-display-categories-first' is non-nil, then the first
1036invocation of `todos-show' displays a clickable listing of the 2604invocation of `todos-show' displays a clickable listing of the
1037categories in the current Todos file." 2605categories in the current Todos file.
2606
2607In Todos mode just the category's unfinished todo items are shown
2608by default. The done items are hidden, but typing
2609`\\[todos-toggle-view-done-items]' displays them below the todo
2610items. With non-nil user option `todos-show-with-done' both todo
2611and done items are always shown on visiting a category."
1038 (interactive "P") 2612 (interactive "P")
1039 (let ((file (cond (solicit-file 2613 (let ((file (cond (solicit-file
1040 (if (funcall todos-files-function) 2614 (if (funcall todos-files-function)
1041 (todos-read-file-name "Select a Todos file to visit: " 2615 (todos-read-file-name "Choose a Todos file to visit: "
1042 nil t) 2616 nil t)
1043 (error "There are no Todos files"))) 2617 (error "There are no Todos files")))
1044 ((eq major-mode 'todos-archive-mode) 2618 ((eq major-mode 'todos-archive-mode)
@@ -1062,6 +2636,61 @@ categories in the current Todos file."
1062 (save-excursion (todos-category-select))) 2636 (save-excursion (todos-category-select)))
1063 (setq todos-first-visit nil))) 2637 (setq todos-first-visit nil)))
1064 2638
2639(defun todos-display-categories ()
2640 "Display a table of the current file's categories and item counts.
2641
2642In the initial display the categories are numbered, indicating
2643their current order for navigating by \\[todos-forward-category]
2644and \\[todos-backward-category]. You can persistantly change the
2645order of the category at point by typing \\[todos-raise-category]
2646or \\[todos-lower-category].
2647
2648The labels above the category names and item counts are buttons,
2649and clicking these changes the display: sorted by category name
2650or by the respective item counts (alternately descending or
2651ascending). In these displays the categories are not numbered
2652and \\[todos-raise-category] and \\[todos-lower-category] are
2653disabled. (Programmatically, the sorting is triggered by passing
2654a non-nil SORTKEY argument.)
2655
2656In addition, the lines with the category names and item counts
2657are buttonized, and pressing one of these button jumps to the
2658category in Todos mode (or Todos Archive mode, for categories
2659containing only archived items, provided user option
2660`todos-ignore-archived-categories' is non-nil. These categories
2661are shown in `todos-archived-only' face."
2662 (interactive)
2663 (todos-display-categories-1)
2664 (let (sortkey)
2665 (todos-update-categories-display sortkey)))
2666
2667;; ;; FIXME: make this toggle with todos-display-categories
2668;; (defun todos-display-categories-alphabetically ()
2669;; ""
2670;; (interactive)
2671;; (todos-display-sorted 'alpha))
2672
2673;; ;; FIXME: provide key bindings for these or delete them
2674;; (defun todos-display-categories-sorted-by-todo ()
2675;; ""
2676;; (interactive)
2677;; (todos-display-sorted 'todo))
2678
2679;; (defun todos-display-categories-sorted-by-diary ()
2680;; ""
2681;; (interactive)
2682;; (todos-display-sorted 'diary))
2683
2684;; (defun todos-display-categories-sorted-by-done ()
2685;; ""
2686;; (interactive)
2687;; (todos-display-sorted 'done))
2688
2689;; (defun todos-display-categories-sorted-by-archived ()
2690;; ""
2691;; (interactive)
2692;; (todos-display-sorted 'archived))
2693
1065(defun todos-toggle-item-numbering () 2694(defun todos-toggle-item-numbering ()
1066 "" 2695 ""
1067 (interactive) 2696 (interactive)
@@ -1070,20 +2699,16 @@ categories in the current Todos file."
1070(defun todos-toggle-view-done-items () 2699(defun todos-toggle-view-done-items ()
1071 "Show hidden or hide visible done items in current category." 2700 "Show hidden or hide visible done items in current category."
1072 (interactive) 2701 (interactive)
1073 (save-excursion 2702 (if (zerop (todos-get-count 'done (todos-current-category)))
1074 (goto-char (point-min)) 2703 (message "There are no done items in this category.")
1075 (let ((todos-show-with-done 2704 (save-excursion
1076 (if (re-search-forward todos-done-string-start nil t) 2705 (goto-char (point-min))
1077 nil 2706 (let ((todos-show-with-done (not (re-search-forward
1078 t)) 2707 todos-done-string-start nil t))))
1079 (cat (todos-current-category))) 2708 (todos-category-select)))))
1080 (todos-category-select)
1081 (when (zerop (todos-get-count 'done cat))
1082 (message "There are no done items in this category.")))))
1083 2709
1084;; FIXME: should there be `todos-toggle-view-todo-items'?
1085(defun todos-toggle-show-done-only () 2710(defun todos-toggle-show-done-only ()
1086 "Make category display done or back to todo items." ;FIXME 2711 "Switch between displaying only done or only todo items."
1087 (interactive) 2712 (interactive)
1088 (setq todos-show-done-only (not todos-show-done-only)) 2713 (setq todos-show-done-only (not todos-show-done-only))
1089 (todos-category-select)) 2714 (todos-category-select))
@@ -1126,15 +2751,16 @@ last category displayed."
1126 (todos-show-archive t)) 2751 (todos-show-archive t))
1127 2752
1128(defun todos-highlight-item () 2753(defun todos-highlight-item ()
1129 "Highlight the todo item the cursor is on." 2754 "Toggle highlighting the todo item the cursor is on."
1130 (interactive) 2755 (interactive)
1131 (if hl-line-mode ; todos-highlight-item 2756 (require 'hl-line)
1132 (hl-line-mode 0) 2757 (if hl-line-mode
2758 (hl-line-mode -1)
1133 (hl-line-mode 1))) 2759 (hl-line-mode 1)))
1134 2760
1135(defun todos-toggle-display-date-time (&optional all) 2761(defun todos-toggle-display-date-time () ;(&optional all)
1136 "Hide or show date/time of todo items in current category. 2762 "Hide or show date-time header of todo items.";; in current category.
1137With non-nil prefix argument ALL do this in the whole file." 2763;; With non-nil prefix argument ALL do this in the whole file."
1138 (interactive "P") 2764 (interactive "P")
1139 (save-excursion 2765 (save-excursion
1140 (save-restriction 2766 (save-restriction
@@ -1145,7 +2771,9 @@ With non-nil prefix argument ALL do this in the whole file."
1145 (setq ov (pop ovs)) 2771 (setq ov (pop ovs))
1146 (if (equal (overlay-get ov 'display) "") 2772 (if (equal (overlay-get ov 'display) "")
1147 (setq ovs nil hidden t))) 2773 (setq ovs nil hidden t)))
1148 (when all (widen) (goto-char (point-min))) 2774 ;; (when all
2775 (widen)
2776 (goto-char (point-min));)
1149 (if hidden 2777 (if hidden
1150 (remove-overlays (point-min) (point-max) 'display "") 2778 (remove-overlays (point-min) (point-max) 'display "")
1151 (while (not (eobp)) 2779 (while (not (eobp))
@@ -1177,7 +2805,7 @@ is \"*\", then the mark is \"@\"."
1177 (while (or (and all (not (eobp))) 2805 (while (or (and all (not (eobp)))
1178 (< i n)) 2806 (< i n))
1179 (let* ((cat (todos-current-category)) 2807 (let* ((cat (todos-current-category))
1180 (ov (todos-item-marked-p)) 2808 (ov (todos-marked-item-p))
1181 (marked (assoc cat todos-categories-with-marks))) 2809 (marked (assoc cat todos-categories-with-marks)))
1182 (if (and ov (not all)) 2810 (if (and ov (not all))
1183 (progn 2811 (progn
@@ -1187,7 +2815,7 @@ is \"*\", then the mark is \"@\"."
1187 (assq-delete-all cat todos-categories-with-marks)) 2815 (assq-delete-all cat todos-categories-with-marks))
1188 (setcdr marked (1- (cdr marked))))) 2816 (setcdr marked (1- (cdr marked)))))
1189 (when (todos-item-start) 2817 (when (todos-item-start)
1190 (unless (and all (todos-item-marked-p)) 2818 (unless (and all (todos-marked-item-p))
1191 (setq ov (make-overlay (point) (point))) 2819 (setq ov (make-overlay (point) (point)))
1192 (overlay-put ov 'before-string todos-item-mark) 2820 (overlay-put ov 'before-string todos-item-mark)
1193 (if marked 2821 (if marked
@@ -1211,274 +2839,323 @@ is \"*\", then the mark is \"@\"."
1211 (delq (assoc (todos-current-category) todos-categories-with-marks) 2839 (delq (assoc (todos-current-category) todos-categories-with-marks)
1212 todos-categories-with-marks))) 2840 todos-categories-with-marks)))
1213 2841
1214(defun todos-update-merged-files () 2842(defun todos-set-top-priorities-in-file ()
1215 "Interactively add files to or remove from `todos-merged-files'. 2843 "Set number of top priorities for this file.
1216You can also customize `todos-merged-files' directly." 2844See `todos-set-top-priorities' for more details."
1217 (interactive) ;FIXME
1218 (let ((files (funcall todos-files-function)))
1219 (dolist (f files)
1220 (if (member f todos-merged-files)
1221 (and (y-or-n-p
1222 (format "Remove \"%s\" from list of merged Todos files? "
1223 (file-name-sans-extension (file-name-nondirectory f))))
1224 (setq todos-merged-files (delete f todos-merged-files)))
1225 (and (y-or-n-p
1226 (format "Add \"%s\" to list of merged Todos files? "
1227 (file-name-sans-extension (file-name-nondirectory f))))
1228 (setq todos-merged-files
1229 (append todos-merged-files (list f)))))))
1230 (customize-save-variable 'todos-merged-files todos-merged-files))
1231
1232(defvar todos-top-priorities-widgets nil
1233 "Widget placeholder used by `todos-set-top-priorities'.
1234This variable temporarily holds user changed values which are
1235saved to `todos-priorities-rules'.")
1236
1237(defun todos-set-top-priorities ()
1238 ""
1239 (interactive) 2845 (interactive)
1240 (let ((buf (get-buffer-create "*Todos Top Priorities*")) 2846 (todos-set-top-priorities))
1241 (files (funcall todos-files-function))
1242 file frules cats fwidget cwidgets rules)
1243 (with-current-buffer buf
1244 (let ((inhibit-read-only t))
1245 (erase-buffer))
1246 (remove-overlays)
1247 (kill-all-local-variables)
1248 (setq todos-top-priorities-widgets nil)
1249 (dolist (f files)
1250 (with-temp-buffer
1251 (insert-file-contents f)
1252 (setq file (file-name-sans-extension (file-name-nondirectory f))
1253 frules (assoc file todos-priorities-rules)
1254 cats (mapcar 'car (todos-set-categories))))
1255 (setq fwidget
1256 (widget-create 'editable-field
1257 :size 2
1258 :value (or (and frules (cadr frules))
1259 "")
1260 :tag file
1261 :format " %v : %t\n"))
1262 (dolist (c cats)
1263 (let ((tp-num (cdr (assoc c cats)))
1264 cwidget)
1265 (widget-insert " ")
1266 (setq cwidget (widget-create 'editable-field
1267 :size 2
1268 :value (or tp-num "")
1269 :tag c
1270 :format " %v : %t\n"))
1271 (push cwidget cwidgets)))
1272 (push (cons fwidget cwidgets) todos-top-priorities-widgets))
1273 (widget-insert "\n\n")
1274 (widget-create 'push-button
1275 :notify (lambda (widget &rest ignore)
1276 (kill-buffer))
1277 "Cancel")
1278 (widget-insert " ")
1279 (widget-create 'push-button
1280 :notify (lambda (&rest ignore)
1281 (let ((widgets todos-top-priorities-widgets)
1282 (rules todos-priorities-rules)
1283 tp-cats)
1284 (setq rules nil)
1285 (dolist (w widgets)
1286 (let* ((fwid (car w))
1287 (cwids (cdr w))
1288 (fname (widget-get fwid :tag))
1289 (fval (widget-value fwid)))
1290 (dolist (c cwids)
1291 (let ((cat (widget-get c :tag))
1292 (cval (widget-value c)))
1293 (push (cons cat cval) tp-cats)))
1294 (push (list fname fval tp-cats) rules)))
1295 (setq todos-priorities-rules rules)
1296 (customize-save-variable 'todos-priorities-rules
1297 todos-priorities-rules)))
1298 "Apply")
1299 (use-local-map widget-keymap)
1300 (widget-setup))
1301 (set-window-buffer (selected-window) (set-buffer buf))))
1302
1303(defun todos-filter-items (&optional filter merge)
1304 "Display a filtered list of items from different categories.
1305 2847
1306The special items are either the first NUM items (the top priority items) or the items marked as diary entries in each category of the current Todos file. 2848(defun todos-set-top-priorities-in-category ()
1307 2849 "Set number of top priorities for this category.
1308Number of entries for each category is given by NUM, which 2850See `todos-set-top-priorities' for more details."
1309defaults to `todos-show-priorities'. With non-nil argument 2851 (interactive)
1310MERGE list top priorities of all Todos files in 2852 (todos-set-top-priorities t))
1311`todos-merged-files'. If `todos-prompt-merged-files' is non-nil,
1312prompt to update the list of merged files."
1313 (let ((num (if (consp filter) (cdr filter) todos-show-priorities))
1314 (buf (get-buffer-create todos-tmp-buffer-name))
1315 (files (list todos-current-todos-file))
1316 regexp fname bufstr cat beg end done)
1317 (when merge
1318 ;; FIXME: same or different treatment for top priorities and other
1319 ;; filters? And what about todos-prompt-merged-files?
1320 (setq files (if (member filter '(diary regexp custom))
1321 (or (and todos-prompt-merged-files
1322 (todos-update-merged-files))
1323 todos-merged-files
1324 (todos-update-merged-files))
1325 ;; Set merged files for top priorities.
1326 (or (mapcar (lambda (f)
1327 (let ((file (car f))
1328 (val (nth 1 f)))
1329 (and val (not (zerop val))
1330 (push file files))))
1331 todos-priorities-rules)
1332 (if (y-or-n-p "Choose files for merging top priorities? ")
1333 (progn (todos-set-top-priorities) (error ""))
1334 (error "No files are set for merging top priorities"))))))
1335 (with-current-buffer buf
1336 (erase-buffer)
1337 (kill-all-local-variables)
1338 (todos-filter-items-mode))
1339 (when (eq filter 'regexp)
1340 (setq regexp (read-string "Enter a regular expression: ")))
1341 (save-current-buffer
1342 (dolist (f files)
1343 (setq fname (file-name-sans-extension (file-name-nondirectory f)))
1344 (with-temp-buffer
1345 (insert-file-contents f)
1346 (goto-char (point-min))
1347 ;; Unless the number of items to show was supplied by prefix
1348 ;; argument of caller, override `todos-show-priorities' with the
1349 ;; nonzero file-wide value from `todos-priorities-rules'.
1350 (unless (consp filter)
1351 (let ((tp-val (nth 1 (assoc fname todos-priorities-rules))))
1352 (unless (zerop (length tp-val))
1353 (setq num (string-to-number tp-val)))))
1354 (unless (looking-at (concat "^" (regexp-quote todos-category-beg)))
1355 (kill-line 1))
1356 (while (re-search-forward
1357 (concat "^" (regexp-quote todos-category-beg) "\\(.+\\)\n")
1358 nil t)
1359 (setq cat (match-string 1))
1360 ;; Unless the number of items to show was supplied by prefix
1361 ;; argument of caller, override `todos-show-priorities' with the
1362 ;; nonzero category-wide value from `todos-priorities-rules'.
1363 (unless (consp filter)
1364 (let* ((cats (nth 2 (assoc fname todos-priorities-rules)))
1365 (tp-val (cdr (assoc cat cats))))
1366 (unless (zerop (length tp-val))
1367 (setq num (string-to-number tp-val)))))
1368 (delete-region (match-beginning 0) (match-end 0))
1369 (setq beg (point)) ; Start of first entry.
1370 (setq end (if (re-search-forward
1371 (concat "^" (regexp-quote todos-category-beg)) nil t)
1372 (match-beginning 0)
1373 (point-max)))
1374 (goto-char beg)
1375 (setq done
1376 (if (re-search-forward
1377 (concat "\n" (regexp-quote todos-category-done)) end t)
1378 (match-beginning 0)
1379 end))
1380 (delete-region done end)
1381 (setq end done)
1382 (narrow-to-region beg end) ; Process current category.
1383 (goto-char (point-min))
1384 ;; Apply the filter.
1385 (cond ((eq filter 'diary)
1386 (while (not (eobp))
1387 (if (looking-at (regexp-quote todos-nondiary-start))
1388 (todos-remove-item)
1389 (todos-forward-item))))
1390 ((eq filter 'regexp)
1391 (while (not (eobp))
1392 (if (string-match regexp (todos-item-string))
1393 (todos-forward-item)
1394 (todos-remove-item))))
1395 ((eq filter 'custom)
1396 (if todos-filter-function
1397 (funcall todos-filter-function)
1398 (error "No custom filter function has been defined")))
1399 (t ; Filter top priority items.
1400 (todos-forward-item num)))
1401 (setq beg (point))
1402 (unless (member filter '(diary regexp custom))
1403 (delete-region beg end))
1404 (goto-char (point-min))
1405 ;; Add file (if using merged files) and category tags to item.
1406 (while (not (eobp))
1407 (when (re-search-forward
1408 (concat todos-date-string-start todos-date-pattern
1409 "\\( " diary-time-regexp "\\)?"
1410 (regexp-quote todos-nondiary-end) "?")
1411 nil t)
1412 (insert (concat " [" (if merge (concat fname ":")) cat "]")))
1413 (forward-line))
1414 (widen))
1415 (setq bufstr (buffer-string))
1416 (with-current-buffer buf
1417 (let (buffer-read-only)
1418 (insert bufstr))))))
1419 (set-window-buffer (selected-window) (set-buffer buf))
1420 (todos-prefix-overlays)
1421 (goto-char (point-min))
1422 ;; FIXME: this is necessary -- why?
1423 (font-lock-fontify-buffer)))
1424 2853
1425(defun todos-top-priorities (&optional num) 2854(defun todos-top-priorities (&optional num)
1426 "List top priorities of each category in `todos-merged-files'. 2855 "List top priorities of each category in `todos-filter-files'.
1427Number of entries for each category is given by NUM, which 2856Number of entries for each category is given by NUM, which
1428defaults to `todos-show-priorities'." 2857defaults to `todos-show-priorities'."
1429 (interactive "p") 2858 (interactive "P")
1430 (let ((arg (if num (cons 'top num) 'top))) 2859 (let ((arg (if num (cons 'top num) 'top))
1431 (todos-filter-items arg))) 2860 (buf todos-top-priorities-buffer)
1432 2861 (file todos-current-todos-file))
1433(defun todos-merged-top-priorities (&optional num) 2862 (todos-filter-items arg)
1434 "List top priorities of each category in `todos-merged-files'. 2863 (todos-special-buffer-name buf file)))
1435Number of entries for each category is given by NUM, which 2864
1436defaults to `todos-show-priorities'." 2865(defun todos-top-priorities-multifile (&optional arg)
1437 (interactive "p") 2866 "List top priorities of each category in `todos-filter-files'.
1438 (let ((arg (if num (cons 'top num) 'top))) 2867
1439 (todos-filter-items arg t))) 2868If the prefix argument ARG is a number, this is the maximum
2869number of top priorities to list in each category. If the prefix
2870argument is `C-u', prompt for which files to filter and use
2871`todos-show-priorities' as the number of top priorities to list
2872in each category. If the prefix argument is `C-uC-u', prompt
2873both for which files to filter and for how many top priorities to
2874list in each category."
2875 (interactive "P")
2876 (let* ((buf todos-top-priorities-buffer)
2877 files
2878 (pref (if (numberp arg)
2879 (cons 'top arg)
2880 (setq files (if (or (consp arg)
2881 (null todos-filter-files))
2882 (todos-multiple-files)
2883 todos-filter-files))
2884 (if (equal arg '(16))
2885 (cons 'top (read-number
2886 "Enter number of top priorities to show: "
2887 todos-show-priorities))
2888 'top))))
2889 (todos-filter-items pref t)
2890 (todos-special-buffer-name buf files)))
1440 2891
1441(defun todos-diary-items () 2892(defun todos-diary-items ()
1442 "Display todo items for diary inclusion in this Todos file." 2893 "Display todo items for diary inclusion in this Todos file."
1443 (interactive) 2894 (interactive)
1444 (todos-filter-items 'diary)) 2895 (let ((buf todos-diary-items-buffer)
2896 (file todos-current-todos-file))
2897 (todos-filter-items 'diary)
2898 (todos-special-buffer-name buf file)))
1445 2899
1446(defun todos-merged-diary-items () 2900(defun todos-diary-items-multifile (&optional arg)
1447 "Display todo items for diary inclusion in one or more Todos file. 2901 "Display todo items for diary inclusion in one or more Todos file.
1448The files are those listed in `todos-merged-files'." 2902The files are those listed in `todos-filter-files'."
1449 (interactive) 2903 (interactive "P")
1450 (todos-filter-items 'diary t)) 2904 (let ((buf todos-diary-items-buffer)
2905 (files (if (or arg (null todos-filter-files))
2906 (todos-multiple-files)
2907 todos-filter-files)))
2908 (todos-filter-items 'diary t)
2909 (todos-special-buffer-name buf files)))
1451 2910
1452(defun todos-regexp-items () 2911(defun todos-regexp-items ()
1453 "Display todo items matching a user-entered regular expression. 2912 "Display todo items matching a user-entered regular expression.
1454The items are those in the current Todos file." 2913The items are those in the current Todos file."
1455 (interactive) 2914 (interactive)
1456 (todos-filter-items 'regexp)) 2915 (let ((buf todos-regexp-items-buffer)
2916 (file todos-current-todos-file))
2917 (todos-filter-items 'regexp)
2918 (todos-special-buffer-name buf file)))
1457 2919
1458(defun todos-merged-regexp-items () 2920(defun todos-regexp-items-multifile (&optional arg)
1459 "Display todo items matching a user-entered regular expression. 2921 "Display todo items matching a user-entered regular expression.
1460The items are those in the files listed in `todos-merged-files'." 2922The items are those in the files listed in `todos-filter-files'."
1461 (interactive) 2923 (interactive "P")
1462 (todos-filter-items 'regexp t)) 2924 (let ((buf todos-regexp-items-buffer)
2925 (files (if (or arg (null todos-filter-files))
2926 (todos-multiple-files)
2927 todos-filter-files)))
2928 (todos-filter-items 'regexp t)
2929 (todos-special-buffer-name buf files)))
1463 2930
1464(defun todos-custom-items () 2931(defun todos-custom-items ()
1465 "Display todo items filtered by `todos-filter-function'. 2932 "Display todo items filtered by `todos-filter-function'.
1466The items are those in the current Todos file." 2933The items are those in the current Todos file."
1467 (interactive) 2934 (interactive)
1468 (todos-filter-items 'custom)) 2935 (let ((buf todos-custom-items-buffer)
2936 (file todos-current-todos-file))
2937 (todos-filter-items 'custom)
2938 (todos-special-buffer-name buf file)))
1469 2939
1470(defun todos-merged-custom-items () 2940(defun todos-custom-items-multifile (&optional arg)
1471 "Display todo items filtered by `todos-filter-function'. 2941 "Display todo items filtered by `todos-filter-function'.
1472The items are those in the files listed in `todos-merged-files'." 2942The items are those in the files listed in `todos-filter-files'."
2943 (interactive "P")
2944 (let ((buf todos-custom-items-buffer)
2945 (files (if (or arg (null todos-filter-files))
2946 (todos-multiple-files)
2947 todos-filter-files)))
2948 (todos-filter-items 'custom t)
2949 (todos-special-buffer-name buf files)))
2950
2951(defun todos-print (&optional to-file)
2952 "Produce a printable version of the current Todos buffer.
2953This converts overlays and soft line wrapping and, depending on
2954the value of `todos-print-function', includes faces. With
2955non-nil argument TO-FILE write the printable version to a file;
2956otherwise, send it to the default printer."
1473 (interactive) 2957 (interactive)
1474 (todos-filter-items 'custom t)) 2958 (let ((buf todos-print-buffer)
2959 (header (cond
2960 ((eq major-mode 'todos-mode)
2961 (concat "Todos File: "
2962 (todos-short-file-name todos-current-todos-file)
2963 "\nCategory: " (todos-current-category)))
2964 ((eq major-mode 'todos-filter-items-mode)
2965 "Todos Top Priorities")))
2966 (prefix (propertize (concat todos-prefix " ")
2967 'face 'todos-prefix-string))
2968 (num 0)
2969 (fill-prefix (make-string todos-indent-to-here 32))
2970 (content (buffer-string))
2971 file)
2972 (with-current-buffer (get-buffer-create buf)
2973 (insert content)
2974 (goto-char (point-min))
2975 (while (not (eobp))
2976 (let ((beg (point))
2977 (end (save-excursion (todos-item-end))))
2978 (when todos-number-prefix
2979 (setq num (1+ num))
2980 (setq prefix (propertize (concat (number-to-string num) " ")
2981 'face 'todos-prefix-string)))
2982 (insert prefix)
2983 (fill-region beg end))
2984 ;; Calling todos-forward-item infloops at todos-item-start due to
2985 ;; non-overlay prefix, so search for item start instead.
2986 (if (re-search-forward todos-item-start nil t)
2987 (beginning-of-line)
2988 (goto-char (point-max))))
2989 (if (re-search-backward (concat "^" (regexp-quote todos-category-done))
2990 nil t)
2991 (replace-match todos-done-separator))
2992 (goto-char (point-min))
2993 (insert header)
2994 (newline 2)
2995 (if to-file
2996 (let ((file (read-file-name "Print to file: ")))
2997 (funcall todos-print-function file))
2998 (funcall todos-print-function)))
2999 (kill-buffer buf)))
3000
3001(defun todos-print-to-file ()
3002 "Save printable version of this Todos buffer to a file."
3003 (interactive)
3004 (todos-print t))
3005
3006(defun todos-convert-legacy-files ()
3007 "Convert legacy Todo files to the current Todos format.
3008The files `todo-file-do' and `todo-file-done' are converted and
3009saved (the latter as a Todos Archive file) with a new name in
3010`todos-files-directory'. See also the documentation string of
3011`todos-todo-mode-date-time-regexp' for further details."
3012 (interactive)
3013 (if (fboundp 'todo-mode)
3014 (require 'todo-mode)
3015 (error "Void function `todo-mode'"))
3016 ;; Convert `todo-file-do'.
3017 (if (file-exists-p todo-file-do)
3018 (let ((default "todo-do-conv")
3019 file archive-sexp)
3020 (with-temp-buffer
3021 (insert-file-contents todo-file-do)
3022 (let ((end (search-forward ")" (line-end-position) t))
3023 (beg (search-backward "(" (line-beginning-position) t)))
3024 (setq todo-categories
3025 (read (buffer-substring-no-properties beg end))))
3026 (todo-mode)
3027 (delete-region (line-beginning-position) (1+ (line-end-position)))
3028 (while (not (eobp))
3029 (cond
3030 ((looking-at (regexp-quote (concat todo-prefix todo-category-beg)))
3031 (replace-match todos-category-beg))
3032 ((looking-at (regexp-quote todo-category-end))
3033 (replace-match ""))
3034 ((looking-at (regexp-quote (concat todo-prefix " "
3035 todo-category-sep)))
3036 (replace-match todos-category-done))
3037 ((looking-at (concat (regexp-quote todo-prefix) " "
3038 todos-todo-mode-date-time-regexp " "
3039 (regexp-quote todo-initials) ":"))
3040 (todos-convert-legacy-date-time)))
3041 (forward-line))
3042 (setq file (concat todos-files-directory
3043 (read-string
3044 (format "Save file as (default \"%s\"): " default)
3045 nil nil default)
3046 ".todo"))
3047 (write-region (point-min) (point-max) file nil 'nomessage nil t))
3048 (with-temp-buffer
3049 (insert-file-contents file)
3050 (let ((todos-categories (todos-make-categories-list t)))
3051 (todos-update-categories-sexp))
3052 (write-region (point-min) (point-max) file nil 'nomessage))
3053 ;; Convert `todo-file-done'.
3054 (when (file-exists-p todo-file-done)
3055 (with-temp-buffer
3056 (insert-file-contents todo-file-done)
3057 (let ((beg (make-marker))
3058 (end (make-marker))
3059 cat cats comment item)
3060 (while (not (eobp))
3061 (when (looking-at todos-todo-mode-date-time-regexp)
3062 (set-marker beg (point))
3063 (todos-convert-legacy-date-time)
3064 (set-marker end (point))
3065 (goto-char beg)
3066 (insert "[" todos-done-string)
3067 (goto-char end)
3068 (insert "]")
3069 (forward-char)
3070 (when (looking-at todos-todo-mode-date-time-regexp)
3071 (todos-convert-legacy-date-time))
3072 (when (looking-at (concat " " (regexp-quote todo-initials) ":"))
3073 (replace-match "")))
3074 (if (re-search-forward
3075 (concat "^" todos-todo-mode-date-time-regexp) nil t)
3076 (goto-char (match-beginning 0))
3077 (goto-char (point-max)))
3078 (backward-char)
3079 (when (looking-back "\\[\\([^][]+\\)\\]")
3080 (setq cat (match-string 1))
3081 (goto-char (match-beginning 0))
3082 (replace-match ""))
3083 ;; If the item ends with a non-comment parenthesis not
3084 ;; followed by a period, we lose (but we inherit that problem
3085 ;; from todo-mode.el).
3086 (when (looking-back "(\\(.*\\)) ")
3087 (setq comment (match-string 1))
3088 (replace-match "")
3089 (insert "[" todos-comment-string ": " comment "]"))
3090 (set-marker end (point))
3091 (if (member cat cats)
3092 ;; If item is already in its category, leave it there.
3093 (unless (save-excursion
3094 (re-search-backward
3095 (concat "^" (regexp-quote todos-category-beg)
3096 "\\(.*\\)$") nil t)
3097 (string= (match-string 1) cat))
3098 ;; Else move it to its category.
3099 (setq item (buffer-substring-no-properties beg end))
3100 (delete-region beg (1+ end))
3101 (set-marker beg (point))
3102 (re-search-backward
3103 (concat "^" (regexp-quote (concat todos-category-beg cat)))
3104 nil t)
3105 (forward-line)
3106 (if (re-search-forward
3107 (concat "^" (regexp-quote todos-category-beg)
3108 "\\(.*\\)$") nil t)
3109 (progn (goto-char (match-beginning 0))
3110 (newline)
3111 (forward-line -1))
3112 (goto-char (point-max)))
3113 (insert item "\n")
3114 (goto-char beg))
3115 (push cat cats)
3116 (goto-char beg)
3117 (insert todos-category-beg cat "\n\n" todos-category-done "\n"))
3118 (forward-line))
3119 (set-marker beg nil)
3120 (set-marker end nil))
3121 (setq file (concat (file-name-sans-extension file) ".toda"))
3122 (write-region (point-min) (point-max) file nil 'nomessage nil t))
3123 (with-temp-buffer
3124 (insert-file-contents file)
3125 (let ((todos-categories (todos-make-categories-list t)))
3126 (todos-update-categories-sexp))
3127 (write-region (point-min) (point-max) file nil 'nomessage)
3128 (setq archive-sexp (read (buffer-substring-no-properties
3129 (line-beginning-position)
3130 (line-end-position)))))
3131 (setq file (concat (file-name-sans-extension file) ".todo"))
3132 ;; Update categories sexp of converted Todos file again, adding
3133 ;; counts of archived items.
3134 (with-temp-buffer
3135 (insert-file-contents file)
3136 (let ((sexp (read (buffer-substring-no-properties
3137 (line-beginning-position)
3138 (line-end-position)))))
3139 (dolist (cat sexp)
3140 (let ((archive-cat (assoc (car cat) archive-sexp)))
3141 (if archive-cat
3142 (aset (cdr cat) 3 (aref (cdr archive-cat) 2)))))
3143 (delete-region (line-beginning-position) (line-end-position))
3144 (prin1 sexp (current-buffer)))
3145 (write-region (point-min) (point-max) file nil 'nomessage)))
3146 (todos-reevaluate-defcustoms)
3147 (message "Format conversion done."))
3148 (error "No legacy Todo file exists")))
1475 3149
1476;;; Navigation 3150;; ---------------------------------------------------------------------------
3151;;; Navigation Commands
1477 3152
1478(defun todos-forward-category (&optional back) 3153(defun todos-forward-category (&optional back)
1479 "Visit the numerically next category in this Todos file. 3154 "Visit the numerically next category in this Todos file.
1480With non-nil argument BACK, visit the numerically previous 3155If the current category is the highest numbered, visit the first
1481category." 3156category. With non-nil argument BACK, visit the numerically
3157previous category (the highest numbered one, if the current
3158category is the first)."
1482 (interactive) 3159 (interactive)
1483 (setq todos-category-number 3160 (setq todos-category-number
1484 (1+ (mod (- todos-category-number (if back 2 0)) 3161 (1+ (mod (- todos-category-number (if back 2 0))
@@ -1487,20 +3164,22 @@ category."
1487 (goto-char (point-min))) 3164 (goto-char (point-min)))
1488 3165
1489(defun todos-backward-category () 3166(defun todos-backward-category ()
1490 "Visit the numerically previous category in this Todos file." 3167 "Visit the numerically previous category in this Todos file.
3168If the current category is the highest numbered, visit the first
3169category."
1491 (interactive) 3170 (interactive)
1492 (todos-forward-category t)) 3171 (todos-forward-category t))
1493 3172
1494;; FIXME: autoload?
1495(defun todos-jump-to-category (&optional cat other-file) 3173(defun todos-jump-to-category (&optional cat other-file)
1496 "Jump to a category in this or another Todos file. 3174 "Jump to a category in this or another Todos file.
1497Optional argument CAT provides the category name. Otherwise, 3175
1498prompt for the category, with TAB completion on existing 3176Programmatically, optional argument CAT provides the category
1499categories. If a non-existing category name is entered, ask 3177name. When nil (as in interactive calls), prompt for the
1500whether to add a new category with this name, if affirmed, do so, 3178category, with TAB completion on existing categories. If a
1501then jump to that category. With non-nil argument OTHER-FILE, 3179non-existing category name is entered, ask whether to add a new
1502prompt for a Todos file, otherwise jump within the current Todos 3180category with this name; if affirmed, add it, then jump to that
1503file." 3181category. With non-nil argument OTHER-FILE, prompt for a Todos
3182file, otherwise jump within the current Todos file."
1504 (interactive) 3183 (interactive)
1505 (let ((file (or (and other-file 3184 (let ((file (or (and other-file
1506 (todos-read-file-name "Choose a Todos file: " nil t)) 3185 (todos-read-file-name "Choose a Todos file: " nil t))
@@ -1519,9 +3198,6 @@ file."
1519 (and other-file (setq todos-current-todos-file file)) 3198 (and other-file (setq todos-current-todos-file file))
1520 (let ((category (or (and (assoc cat todos-categories) cat) 3199 (let ((category (or (and (assoc cat todos-categories) cat)
1521 (todos-read-category "Jump to category: ")))) 3200 (todos-read-category "Jump to category: "))))
1522 ;; ;; FIXME: why is this needed?
1523 ;; (if (string= "" category)
1524 ;; (setq category (todos-current-category)))
1525 ;; Clean up after selecting category in Todos Categories mode. 3201 ;; Clean up after selecting category in Todos Categories mode.
1526 (if (string= (buffer-name) todos-categories-buffer) 3202 (if (string= (buffer-name) todos-categories-buffer)
1527 (kill-buffer)) 3203 (kill-buffer))
@@ -1542,6 +3218,37 @@ The category is chosen by prompt, with TAB completion."
1542 (interactive) 3218 (interactive)
1543 (todos-jump-to-category nil t)) 3219 (todos-jump-to-category nil t))
1544 3220
3221(defun todos-jump-to-item ()
3222 "Jump to the file and category of the filtered item at point."
3223 (interactive)
3224 (let ((str (todos-item-string))
3225 (buf (current-buffer))
3226 cat file beg)
3227 (string-match (concat todos-date-string-start todos-date-pattern
3228 "\\(?: " diary-time-regexp "\\)?"
3229 (regexp-quote todos-nondiary-end) "?"
3230 "\\(?3: \\[\\(?2:.*:\\)?\\(?1:.*\\)\\]\\).*$") str)
3231 (setq cat (match-string 1 str))
3232 (setq file (match-string 2 str))
3233 (setq str (replace-match "" nil nil str 3))
3234 (setq file (if file
3235 (concat todos-files-directory (substring file 0 -1) ".todo")
3236 todos-global-current-todos-file))
3237 (find-file-noselect file)
3238 (with-current-buffer (get-file-buffer file)
3239 (widen)
3240 (goto-char (point-min))
3241 (re-search-forward
3242 (concat "^" (regexp-quote (concat todos-category-beg cat))) nil t)
3243 (search-forward str)
3244 (setq beg (match-beginning 0)))
3245 (kill-buffer buf)
3246 (set-window-buffer (selected-window) (set-buffer (get-file-buffer file)))
3247 (setq todos-current-todos-file file)
3248 (setq todos-category-number (todos-category-number cat))
3249 (todos-category-select)
3250 (goto-char beg)))
3251
1545;; FIXME ? disallow prefix arg value < 1 (re-search-* allows these) 3252;; FIXME ? disallow prefix arg value < 1 (re-search-* allows these)
1546(defun todos-forward-item (&optional count) 3253(defun todos-forward-item (&optional count)
1547 "Move point down to start of item with next lower priority. 3254 "Move point down to start of item with next lower priority.
@@ -1579,6 +3286,8 @@ With numerical prefix COUNT, move point COUNT items upward,"
1579 (re-search-forward (concat "^" (regexp-quote todos-category-done)) nil t) 3286 (re-search-forward (concat "^" (regexp-quote todos-category-done)) nil t)
1580 (forward-line -1)))) 3287 (forward-line -1))))
1581 3288
3289;; FIXME: (i) Extend search to other Todos files. (ii) Allow navigating among
3290;; hits.
1582(defun todos-search () 3291(defun todos-search ()
1583 "Search for a regular expression in this Todos file. 3292 "Search for a regular expression in this Todos file.
1584The search runs through the whole file and encompasses all and 3293The search runs through the whole file and encompasses all and
@@ -1645,59 +3354,35 @@ face."
1645 (interactive) 3354 (interactive)
1646 (remove-overlays 1 (1+ (buffer-size)) 'face 'todos-search)) 3355 (remove-overlays 1 (1+ (buffer-size)) 'face 'todos-search))
1647 3356
1648;;; Editing 3357;; ---------------------------------------------------------------------------
3358;;; Editing Commands
1649 3359
3360;; FIXME: autoload?
3361;; FIXME: should there also be command todos-delete-file or is it enough to
3362;; delete empty file on deleting last category with todos-delete-category?
1650(defun todos-add-file () 3363(defun todos-add-file ()
1651 "Name and add a new Todos file. 3364 "Name and add a new Todos file.
1652Interactively, prompt for a category and display it. 3365Interactively, prompt for a category and display it.
1653Noninteractively, return the name of the new file." 3366Noninteractively, return the name of the new file."
1654 (interactive) 3367 (interactive)
1655 (let ((default-file (if todos-default-todos-file 3368 (let ((default-file (if todos-default-todos-file
1656 (file-name-sans-extension 3369 (todos-short-file-name todos-default-todos-file)))
1657 (file-name-nondirectory todos-default-todos-file)))) 3370 (prompt (concat "Enter name of new Todos file "
1658 file prompt shortname) 3371 "(TAB or SPC to see current names): "))
1659 (while 3372 file shortname)
1660 (and 3373 (setq file (todos-read-file-name prompt));))
1661 (cond 3374 (setq shortname (todos-short-file-name file))
1662 ((or (not file) (member file todos-files))
1663 (setq prompt (concat "Enter name of new Todos file "
1664 "(TAB or SPC to see existing Todos files): ")))
1665 ((string-equal file "")
1666 (setq prompt "Enter a non-empty name: "))
1667 ((string-match "\\`\\s-+\\'" file)
1668 (setq prompt "Enter a name that is not only white space: ")))
1669 (setq file (todos-read-file-name prompt))))
1670 (setq shortname (file-name-sans-extension (file-name-nondirectory file)))
1671 (with-current-buffer (get-buffer-create file) 3375 (with-current-buffer (get-buffer-create file)
1672 (erase-buffer) 3376 (erase-buffer)
1673 (write-region (point-min) (point-max) file nil 'nomessage nil t) 3377 (write-region (point-min) (point-max) file nil 'nomessage nil t)
1674 (kill-buffer file)) 3378 (kill-buffer file))
1675 ;; FIXME: todos-change-default-file yields a Custom mismatch 3379 (todos-reevaluate-defcustoms)
1676 ;; (if (or (not default-file)
1677 ;; (yes-or-no-p (concat (format "Make \"%s\" new default Todos file "
1678 ;; shortname)
1679 ;; (format "[current default is \"%s\"]? "
1680 ;; default-file))))
1681 ;; (todos-change-default-file file)
1682 ;; (message "\"%s\" remains the default Todos file." default-file))
1683 (if (called-interactively-p) 3380 (if (called-interactively-p)
1684 (progn 3381 (progn
1685 (setq todos-current-todos-file file) 3382 (setq todos-current-todos-file file)
1686 (todos-show)) 3383 (todos-show))
1687 file))) 3384 file)))
1688 3385
1689;; FIXME: omit this and just use defcustom? Says "changed outside of Custom
1690;; (mismatch)"
1691(defun todos-change-default-file (&optional file)
1692 ""
1693 (interactive)
1694 (let ((new-default (or file
1695 (todos-read-file-name "Choose new default Todos file: "
1696 nil t))))
1697 (customize-save-variable 'todos-default-todos-file new-default)
1698 (message "\"%s\" is new default Todos file."
1699 (file-name-sans-extension (file-name-nondirectory new-default)))))
1700
1701(defun todos-add-category (&optional cat) 3386(defun todos-add-category (&optional cat)
1702 "Add a new category to the current Todos file. 3387 "Add a new category to the current Todos file.
1703Called interactively, prompt for category name, then visit the 3388Called interactively, prompt for category name, then visit the
@@ -1716,8 +3401,11 @@ the category name, which is also the return value."
1716 todos-current-todos-file))) 3401 todos-current-todos-file)))
1717 (unless cat (setq cat (read-from-minibuffer "Enter new category name: "))) 3402 (unless cat (setq cat (read-from-minibuffer "Enter new category name: ")))
1718 (with-current-buffer buf 3403 (with-current-buffer buf
1719 (setq cat (todos-validate-category-name cat)) 3404 (setq cat (todos-validate-name cat 'category))
1720 (setq todos-categories (append todos-categories (list (cons cat counts)))) 3405 (setq todos-categories (append todos-categories (list (cons cat counts))))
3406 (if todos-categories-full
3407 (setq todos-categories-full (append todos-categories-full
3408 (list (cons cat counts)))))
1721 (widen) 3409 (widen)
1722 (goto-char (point-max)) 3410 (goto-char (point-max))
1723 (save-excursion ; Save point for todos-category-select. 3411 (save-excursion ; Save point for todos-category-select.
@@ -1738,7 +3426,7 @@ category there as well."
1738 (interactive) 3426 (interactive)
1739 (let* ((cat (todos-current-category)) 3427 (let* ((cat (todos-current-category))
1740 (new (read-from-minibuffer (format "Rename category \"%s\" to: " cat)))) 3428 (new (read-from-minibuffer (format "Rename category \"%s\" to: " cat))))
1741 (setq new (todos-validate-category-name new)) 3429 (setq new (todos-validate-name new 'category))
1742 (let* ((ofile todos-current-todos-file) 3430 (let* ((ofile todos-current-todos-file)
1743 (archive (concat (file-name-sans-extension ofile) ".toda")) 3431 (archive (concat (file-name-sans-extension ofile) ".toda"))
1744 (buffers (append (list ofile) 3432 (buffers (append (list ofile)
@@ -1758,6 +3446,7 @@ category there as well."
1758 "\\(" (regexp-quote cat) "\\)\n") 3446 "\\(" (regexp-quote cat) "\\)\n")
1759 nil t) 3447 nil t)
1760 (replace-match new t t nil 1))))))) 3448 (replace-match new t t nil 1)))))))
3449 ;; FIXME: use force-mode-line-update instead?
1761 (setq mode-line-buffer-identification 3450 (setq mode-line-buffer-identification
1762 (funcall todos-mode-line-function new))) 3451 (funcall todos-mode-line-function new)))
1763 (save-excursion (todos-category-select))) 3452 (save-excursion (todos-category-select)))
@@ -1771,42 +3460,58 @@ i.e. including all existing todo and done items."
1771 (todo (todos-get-count 'todo cat)) 3460 (todo (todos-get-count 'todo cat))
1772 (done (todos-get-count 'done cat)) 3461 (done (todos-get-count 'done cat))
1773 (archived (todos-get-count 'archived cat))) 3462 (archived (todos-get-count 'archived cat)))
1774 (if (and (not arg) 3463 (when (or (> (length todos-categories) 1)
1775 (or (> todo 0) (> done 0))) 3464 (y-or-n-p (concat "This is the only category in this file; "
1776 (message "To delete a non-empty category, type C-u D.") 3465 "deleting it will also delete the file.\n"
1777 (when (yes-or-no-p (concat "Permanently remove category \"" cat 3466 "Do you want to proceed? ")))
1778 "\"" (and arg " and all its entries") "? ")) 3467 (if (and (not arg)
1779 ;; FIXME ? optionally delete archived category as well? 3468 (or (> todo 0) (> done 0)))
1780 (when (and archived 3469 (message "To delete a non-empty category, type C-u %s."
1781 (y-or-n-p (concat "This category has archived items; " 3470 (key-description
1782 "the archived category will remain\n" 3471 (car (where-is-internal 'todos-delete-category))))
1783 "after deleting the todo category. " 3472 (when (yes-or-no-p (concat "Permanently remove category \"" cat
1784 "Do you still want to delete it\n" 3473 "\"" (and arg " and all its entries") "? "))
1785 "(see 'todos-ignore-archived-categories' " 3474 (when (and archived
1786 "for another option)? "))) 3475 (y-or-n-p (concat "This category has archived items; "
1787 (widen) 3476 "the archived category will remain\n"
1788 (let ((buffer-read-only) 3477 "after deleting the todo category. "
1789 (beg (re-search-backward 3478 "Do you still want to delete it\n"
1790 (concat "^" (regexp-quote (concat todos-category-beg cat)) 3479 "(see 'todos-ignore-archived-categories' "
1791 "\n") nil t)) 3480 "for another option)? ")))
1792 (end (if (re-search-forward 3481 (widen)
1793 (concat "\n\\(" (regexp-quote todos-category-beg) 3482 (let ((buffer-read-only)
1794 ".*\n\\)") nil t) 3483 (beg (re-search-backward
1795 (match-beginning 1) 3484 (concat "^" (regexp-quote (concat todos-category-beg cat))
1796 (point-max)))) 3485 "\n") nil t))
1797 (remove-overlays beg end) 3486 (end (if (re-search-forward
1798 (delete-region beg end) 3487 (concat "\n\\(" (regexp-quote todos-category-beg)
1799 (setq todos-categories (delete (assoc cat todos-categories) 3488 ".*\n\\)") nil t)
1800 todos-categories)) 3489 (match-beginning 1)
1801 (todos-update-categories-sexp) 3490 (point-max))))
1802 (setq todos-category-number 3491 (remove-overlays beg end)
1803 (1+ (mod todos-category-number (length todos-categories)))) 3492 (delete-region beg end)
1804 (todos-category-select) 3493 (if (= (length todos-categories) 1)
1805 (goto-char (point-min)) 3494 ;; If deleted category was the only one, delete the file.
1806 (message "Deleted category %s" cat))))))) 3495 (progn
3496 ;; FIXME: need this?
3497 (setq todos-categories nil)
3498 (todos-reevaluate-defcustoms)
3499 (delete-file todos-current-todos-file)
3500 (kill-buffer)
3501 (message "Deleted empty Todos file %s."
3502 todos-current-todos-file))
3503 ;; FIXME: what about todos-categories-full ?
3504 (setq todos-categories (delete (assoc cat todos-categories)
3505 todos-categories))
3506 (todos-update-categories-sexp)
3507 (setq todos-category-number
3508 (1+ (mod todos-category-number (length todos-categories))))
3509 (todos-category-select)
3510 (goto-char (point-min))
3511 (message "Deleted category %s." cat)))))))))
1807 3512
1808(defun todos-raise-category (&optional lower) 3513(defun todos-raise-category (&optional lower)
1809 "Raise priority of category point is on in Categories buffer. 3514 "Raise priority of category point is on in Todos Categories buffer.
1810With non-nil argument LOWER, lower the category's priority." 3515With non-nil argument LOWER, lower the category's priority."
1811 (interactive) 3516 (interactive)
1812 (let (num) 3517 (let (num)
@@ -1846,7 +3551,7 @@ With non-nil argument LOWER, lower the category's priority."
1846 (forward-char col))))) 3551 (forward-char col)))))
1847 3552
1848(defun todos-lower-category () 3553(defun todos-lower-category ()
1849 "Lower priority of category point is on in Categories buffer." 3554 "Lower priority of category point is on in Todos Categories buffer."
1850 (interactive) 3555 (interactive)
1851 (todos-raise-category t)) 3556 (todos-raise-category t))
1852 3557
@@ -1892,8 +3597,7 @@ archive of the file moved to, creating it if it does not exist."
1892 (if (member buf (funcall todos-files-function t)) 3597 (if (member buf (funcall todos-files-function t))
1893 (concat (file-name-sans-extension nfile) ".toda") 3598 (concat (file-name-sans-extension nfile) ".toda")
1894 nfile)) 3599 nfile))
1895 (let* ((nfile-short (file-name-sans-extension 3600 (let* ((nfile-short (todos-short-file-name nfile))
1896 (file-name-nondirectory nfile)))
1897 (prompt (concat 3601 (prompt (concat
1898 (format "Todos file \"%s\" already has " 3602 (format "Todos file \"%s\" already has "
1899 nfile-short) 3603 nfile-short)
@@ -1909,7 +3613,7 @@ archive of the file moved to, creating it if it does not exist."
1909 (unless (member (file-truename (buffer-file-name)) 3613 (unless (member (file-truename (buffer-file-name))
1910 (funcall todos-files-function t)) 3614 (funcall todos-files-function t))
1911 (setq new (read-from-minibuffer prompt)) 3615 (setq new (read-from-minibuffer prompt))
1912 (setq new (todos-validate-category-name new)))) 3616 (setq new (todos-validate-name new 'category))))
1913 ;; Replace old with new name in Todos and archive files. 3617 ;; Replace old with new name in Todos and archive files.
1914 (when new 3618 (when new
1915 (goto-char (point-max)) 3619 (goto-char (point-max))
@@ -1941,7 +3645,9 @@ archive of the file moved to, creating it if it does not exist."
1941 ;; Skip confirming killing the archive buffer. 3645 ;; Skip confirming killing the archive buffer.
1942 (set-buffer-modified-p nil) 3646 (set-buffer-modified-p nil)
1943 (delete-file todos-current-todos-file) 3647 (delete-file todos-current-todos-file)
1944 (kill-buffer)) 3648 (kill-buffer)
3649 (when (member todos-current-todos-file todos-files)
3650 (todos-reevaluate-defcustoms)))
1945 (setq todos-categories (delete (assoc cat todos-categories) 3651 (setq todos-categories (delete (assoc cat todos-categories)
1946 todos-categories)) 3652 todos-categories))
1947 (todos-update-categories-sexp) 3653 (todos-update-categories-sexp)
@@ -1952,10 +3658,11 @@ archive of the file moved to, creating it if it does not exist."
1952 (todos-category-select)))) 3658 (todos-category-select))))
1953 3659
1954(defun todos-merge-category () 3660(defun todos-merge-category ()
1955 "Merge this category with chosen category in this file. The 3661 "Merge current category into another category in this file.
1956current category's todo and done items are appended to the chosen 3662The current category's todo and done items are appended to the
1957category's todo and done items, respectively, which becomes the 3663chosen category's todo and done items, respectively, which
1958current category, and the category moved from is deleted." 3664becomes the current category, and the category moved from is
3665deleted."
1959 (interactive) 3666 (interactive)
1960 (let ((buffer-read-only nil) 3667 (let ((buffer-read-only nil)
1961 (cat (todos-current-category)) 3668 (cat (todos-current-category))
@@ -2022,15 +3729,13 @@ current category, and the category moved from is deleted."
2022 (widen) 3729 (widen)
2023 )) 3730 ))
2024 3731
2025;; FIXME: make insertion options customizable per category 3732;; FIXME: make insertion options customizable per category?
2026;;;###autoload 3733;;;###autoload
2027;; (defun todos-insert-item (&optional arg use-point date-type time
2028;; diary nonmarking)
2029(defun todos-insert-item (&optional arg diary nonmarking date-type time 3734(defun todos-insert-item (&optional arg diary nonmarking date-type time
2030 region-or-here) 3735 region-or-here)
2031 "Add a new Todo item to a category. 3736 "Add a new Todo item to a category.
2032See the note at the end of this document string about key 3737\(See the note at the end of this document string about key
2033bindings and convenience commands derived from this command. 3738bindings and convenience commands derived from this command.)
2034 3739
2035With no (or nil) prefix argument ARG, add the item to the current 3740With no (or nil) prefix argument ARG, add the item to the current
2036category; with one prefix argument (C-u), prompt for a category 3741category; with one prefix argument (C-u), prompt for a category
@@ -2070,17 +3775,21 @@ mandatory date header string and how it is added:
2070 header contains the current date (in the format set by 3775 header contains the current date (in the format set by
2071 `calendar-date-display-form'). 3776 `calendar-date-display-form').
2072 3777
2073With non-nil argument TIME prompt for a time string; this must 3778With non-nil argument TIME prompt for a time string, which must
2074either be empty or else match `diary-time-regexp'. If TIME is 3779match `diary-time-regexp'. Typing `<return>' at the prompt
2075nil, add or omit the current time according to value of the user 3780returns the current time, if the user option
2076option `todos-always-add-time-string'. 3781`todos-always-add-time-string' is non-nil, otherwise the empty
3782string (i.e., no time string). If TIME is absent or nil, add or
3783omit the current time string according as
3784`todos-always-add-time-string' is non-nil or nil, respectively.
2077 3785
2078The argument REGION-OR-HERE determines the source and location of 3786The argument REGION-OR-HERE determines the source and location of
2079the new item: 3787the new item:
2080- If the REGION-OR-HERE is the symbol `here', prompt for the text 3788- If the REGION-OR-HERE is the symbol `here', prompt for the text
2081 of the new item and insert it directly above the todo item at 3789 of the new item and insert it directly above the todo item at
2082 point, or if point is on the empty line below the last todo 3790 point (hence lowering the priority of the remaining items), or
2083 item, insert the new item there. An error is signalled if 3791 if point is on the empty line below the last todo item, insert
3792 the new item there. An error is signalled if
2084 `todos-insert-item' is invoked with `here' outside of the 3793 `todos-insert-item' is invoked with `here' outside of the
2085 current category. 3794 current category.
2086- If REGION-OR-HERE is the symbol `region', use the region of the 3795- If REGION-OR-HERE is the symbol `region', use the region of the
@@ -2099,11 +3808,18 @@ the new item:
2099To facilitate using these arguments when inserting a new todo 3808To facilitate using these arguments when inserting a new todo
2100item, convenience commands have been defined for all admissible 3809item, convenience commands have been defined for all admissible
2101combinations (96 in all!) together with mnenomic key bindings 3810combinations (96 in all!) together with mnenomic key bindings
2102based on on the name of the arguments and their order: _h_ere or 3811based on on the name of the arguments and their order in the
2103_r_egion - _c_alendar or _d_ate or day_n_ame - _t_ime - diar_y_ - 3812command's argument list: diar_y_ - nonmar_k_ing - _c_alendar or
2104nonmar_k_ing. An alternative interface for customizing key 3813_d_ate or day_n_ame - _t_ime - _r_egion or _h_ere. These key
2105binding is also provided with the function 3814combinations are appended to the basic insertion key (i) and keys
2106`todos-insertion-bindings'." ;FIXME 3815that allow a following key must be doubled when used finally.
3816For example, `iyh' will insert a new item with today's date,
3817marked according to the DIARY argument described above, and with
3818priority according to the HERE argument; while `iyy' does the
3819same except the priority is not given by HERE but by prompting."
3820;; An alternative interface for customizing key
3821;; binding is also provided with the function
3822;; `todos-insertion-bindings'." ;FIXME
2107 (interactive "P") 3823 (interactive "P")
2108 (let ((region (eq region-or-here 'region)) 3824 (let ((region (eq region-or-here 'region))
2109 (here (eq region-or-here 'here))) 3825 (here (eq region-or-here 'here)))
@@ -2127,15 +3843,8 @@ binding is also provided with the function
2127 (todos-read-dayname)) 3843 (todos-read-dayname))
2128 ((eq date-type 'calendar) 3844 ((eq date-type 'calendar)
2129 (setq todos-date-from-calendar t) 3845 (setq todos-date-from-calendar t)
2130 (let (calendar-view-diary-initially-flag) 3846 (todos-set-date-from-calendar))
2131 (calendar))
2132 (with-current-buffer "*Calendar*"
2133 (todos-set-date-from-calendar))
2134 todos-date-from-calendar)
2135 (t (calendar-date-string (calendar-current-date) t t)))) 3847 (t (calendar-date-string (calendar-current-date) t t))))
2136 ;; FIXME: should TIME override `todos-always-add-time-string'? But
2137 ;; then add another option to use current time or prompt for time
2138 ;; string?
2139 (time-string (or (and time (todos-read-time)) 3848 (time-string (or (and time (todos-read-time))
2140 (and todos-always-add-time-string 3849 (and todos-always-add-time-string
2141 (substring (current-time-string) 11 16))))) 3850 (substring (current-time-string) 11 16)))))
@@ -2159,7 +3868,8 @@ binding is also provided with the function
2159 todos-nondiary-start 3868 todos-nondiary-start
2160 (when (and nonmarking (not todos-diary-nonmarking)) 3869 (when (and nonmarking (not todos-diary-nonmarking))
2161 diary-nonmarking-symbol)) 3870 diary-nonmarking-symbol))
2162 date-string (when time-string 3871 date-string (unless (and time-string
3872 (string= time-string ""))
2163 (concat " " time-string)) 3873 (concat " " time-string))
2164 (when (not (and diary (not todos-include-in-diary))) 3874 (when (not (and diary (not todos-include-in-diary)))
2165 todos-nondiary-end) 3875 todos-nondiary-end)
@@ -2185,6 +3895,25 @@ binding is also provided with the function
2185 (if (or diary todos-include-in-diary) (todos-set-count 'diary 1)) 3895 (if (or diary todos-include-in-diary) (todos-set-count 'diary 1))
2186 (todos-update-categories-sexp))))) 3896 (todos-update-categories-sexp)))))
2187 3897
3898(defvar todos-date-from-calendar nil
3899 "Helper variable for setting item date from the Emacs Calendar.")
3900
3901(defun todos-set-date-from-calendar ()
3902 "Return string of date chosen from Calendar."
3903 (when todos-date-from-calendar
3904 (let (calendar-view-diary-initially-flag)
3905 (calendar))
3906 ;; *Calendar* is now current buffer.
3907 (local-set-key (kbd "RET") 'exit-recursive-edit)
3908 (message "Put cursor on a date and type <return> to set it.")
3909 ;; FIXME: is there a better way than recursive-edit?
3910 ;; FIXME: use unwind-protect? Check recursive-depth?
3911 (recursive-edit)
3912 (setq todos-date-from-calendar
3913 (calendar-date-string (calendar-cursor-to-date t) t t))
3914 (calendar-exit)
3915 todos-date-from-calendar))
3916
2188;; FIXME: autoload when key-binding is defined in calendar.el 3917;; FIXME: autoload when key-binding is defined in calendar.el
2189(defun todos-insert-item-from-calendar () 3918(defun todos-insert-item-from-calendar ()
2190 "" 3919 ""
@@ -2201,19 +3930,6 @@ binding is also provided with the function
2201 ;; (lambda () 3930 ;; (lambda ()
2202 (define-key calendar-mode-map "it" 'todos-insert-item-from-calendar);)) 3931 (define-key calendar-mode-map "it" 'todos-insert-item-from-calendar);))
2203 3932
2204(defvar todos-date-from-calendar nil)
2205(defun todos-set-date-from-calendar ()
2206 ""
2207 (when todos-date-from-calendar
2208 (local-set-key (kbd "RET") 'exit-recursive-edit)
2209 (message "Put cursor on a date and type <return> to set it.")
2210 ;; FIXME: is there a better way than recursive-edit?
2211 ;; FIXME: use unwind-protect? Check recursive-depth?
2212 (recursive-edit)
2213 (setq todos-date-from-calendar
2214 (calendar-date-string (calendar-cursor-to-date t) t t))
2215 (calendar-exit)))
2216
2217(defun todos-delete-item () 3933(defun todos-delete-item ()
2218 "Delete at least one item in this category. 3934 "Delete at least one item in this category.
2219 3935
@@ -2237,7 +3953,7 @@ the item at point."
2237 (and marked (goto-char (point-min))) 3953 (and marked (goto-char (point-min)))
2238 (catch 'done 3954 (catch 'done
2239 (while (not (eobp)) 3955 (while (not (eobp))
2240 (if (or (and marked (todos-item-marked-p)) item) 3956 (if (or (and marked (todos-marked-item-p)) item)
2241 (progn 3957 (progn
2242 (if (todos-done-item-p) 3958 (if (todos-done-item-p)
2243 (todos-set-count 'done -1) 3959 (todos-set-count 'done -1)
@@ -2261,7 +3977,9 @@ the item at point."
2261 (if ov (delete-overlay ov)))) 3977 (if ov (delete-overlay ov))))
2262 3978
2263(defun todos-edit-item () 3979(defun todos-edit-item ()
2264 "Edit current Todo item in the minibuffer." 3980 "Edit the Todo item at point.
3981If the item consists of only one logical line, edit it in the
3982minibuffer; otherwise, edit it in Todos Edit mode."
2265 (interactive) 3983 (interactive)
2266 (when (todos-item-string) 3984 (when (todos-item-string)
2267 (let* ((buffer-read-only) 3985 (let* ((buffer-read-only)
@@ -2277,7 +3995,7 @@ the item at point."
2277 (multiline (> (length (split-string item "\n")) 1)) 3995 (multiline (> (length (split-string item "\n")) 1))
2278 (opoint (point))) 3996 (opoint (point)))
2279 (if multiline 3997 (if multiline
2280 (todos-edit-multiline) 3998 (todos-edit-multiline t)
2281 (let ((new (read-string "Edit: " (cons item item-beg)))) 3999 (let ((new (read-string "Edit: " (cons item item-beg))))
2282 (while (not (string-match 4000 (while (not (string-match
2283 (concat todos-date-string-start todos-date-pattern) new)) 4001 (concat todos-date-string-start todos-date-pattern) new))
@@ -2293,29 +4011,50 @@ the item at point."
2293 (todos-remove-item) 4011 (todos-remove-item)
2294 (todos-insert-with-overlays new) 4012 (todos-insert-with-overlays new)
2295 (move-to-column item-beg)))))) 4013 (move-to-column item-beg))))))
2296 4014
2297;; FIXME: run todos-check-format on exiting buffer (or check for date string 4015(defun todos-edit-multiline-item ()
2298;; and indentation)
2299(defun todos-edit-multiline ()
2300 "Edit current Todo item in Todos Edit mode. 4016 "Edit current Todo item in Todos Edit mode.
2301Use of newlines invokes `todos-indent' to insure compliance with 4017Use of newlines invokes `todos-indent' to insure compliance with
2302the format of Diary entries." 4018the format of Diary entries."
2303 (interactive) 4019 (interactive)
4020 (todos-edit-multiline t))
4021
4022(defun todos-edit-multiline (&optional item)
4023 ""
4024 (interactive)
4025 ;; FIXME: should there be only one live Todos Edit buffer?
4026 ;; (let ((buffer-name todos-edit-buffer))
2304 (let ((buffer-name (generate-new-buffer-name todos-edit-buffer))) 4027 (let ((buffer-name (generate-new-buffer-name todos-edit-buffer)))
2305 (set-window-buffer 4028 (set-window-buffer
2306 (selected-window) 4029 (selected-window)
2307 (set-buffer (make-indirect-buffer 4030 (set-buffer (make-indirect-buffer
2308 (file-name-nondirectory todos-current-todos-file) 4031 (file-name-nondirectory todos-current-todos-file)
2309 buffer-name))) 4032 buffer-name)))
2310 (narrow-to-region (todos-item-start) (todos-item-end)) 4033 (if item
4034 (narrow-to-region (todos-item-start) (todos-item-end))
4035 (widen))
2311 (todos-edit-mode) 4036 (todos-edit-mode)
2312 (message "Type %s to return to Todos mode." 4037 ;; (message (concat "Type %s to check file format validity and "
2313 (key-description (car (where-is-internal 'todos-edit-quit)))))) 4038 ;; "return to Todos mode.\n")
4039 ;; (key-description (car (where-is-internal 'todos-edit-quit))))
4040 (message "%s" (substitute-command-keys
4041 (concat "Type \\[todos-edit-quit] to check file format "
4042 "validity and return to Todos mode.\n")))))
2314 4043
2315(defun todos-edit-quit () 4044(defun todos-edit-quit ()
2316 "Return from Todos Edit mode to Todos mode." 4045 "Return from Todos Edit mode to Todos mode.
4046
4047If the whole file was in Todos Edit mode, check before returning
4048whether the file is still a valid Todos file and if so, also
4049recalculate the Todos categories sexp, in case changes were made
4050in the number or names of categories."
2317 (interactive) 4051 (interactive)
4052 ;; FIXME: worth doing this only if file was actually changed?
4053 (when (eq (buffer-size) (- (point-max) (point-min)))
4054 (when (todos-check-format)
4055 (todos-make-categories-list t)))
2318 (kill-buffer) 4056 (kill-buffer)
4057 ;; In case next buffer is not the one holding todos-current-todos-file.
2319 (todos-show)) 4058 (todos-show))
2320 4059
2321(defun todos-edit-item-header (&optional what) 4060(defun todos-edit-item-header (&optional what)
@@ -2323,21 +4062,23 @@ the format of Diary entries."
2323 4062
2324Interactively, ask whether to edit year, month and day or day of 4063Interactively, ask whether to edit year, month and day or day of
2325the week, as well as time. If there are marked items, apply the 4064the week, as well as time. If there are marked items, apply the
2326changes to all of these; otherwise, edit just the item at point. 4065changes to all of these; otherwise, edit just the item at point.
2327 4066
2328Non-interactively, argument WHAT specifies whether to edit only 4067Non-interactively, argument WHAT specifies whether to set the
2329the date or only the time, or to set the date to today." 4068date from the Calendar or to today, or whether to edit only the
4069date or day, or only the time."
2330 (interactive) 4070 (interactive)
2331 (let* ((cat (todos-current-category)) 4071 (let* ((cat (todos-current-category))
2332 (marked (assoc cat todos-categories-with-marks)) 4072 (marked (assoc cat todos-categories-with-marks))
2333 (first t) 4073 (first t) ; Match only first of marked items.
4074 (todos-date-from-calendar t)
2334 ndate ntime nheader) 4075 ndate ntime nheader)
2335 (save-excursion 4076 (save-excursion
2336 (or (and marked (goto-char (point-min))) (todos-item-start)) 4077 (or (and marked (goto-char (point-min))) (todos-item-start))
2337 (catch 'stop 4078 (catch 'stop
2338 (while (not (eobp)) 4079 (while (not (eobp))
2339 (and marked 4080 (and marked
2340 (while (not (todos-item-marked-p)) 4081 (while (not (todos-marked-item-p))
2341 (todos-forward-item) 4082 (todos-forward-item)
2342 (and (eobp) (throw 'stop nil)))) 4083 (and (eobp) (throw 'stop nil))))
2343 (re-search-forward (concat todos-date-string-start "\\(?1:" 4084 (re-search-forward (concat todos-date-string-start "\\(?1:"
@@ -2347,38 +4088,51 @@ the date or only the time, or to set the date to today."
2347 (let* ((odate (match-string-no-properties 1)) 4088 (let* ((odate (match-string-no-properties 1))
2348 (otime (match-string-no-properties 2)) 4089 (otime (match-string-no-properties 2))
2349 (buffer-read-only)) 4090 (buffer-read-only))
2350 (if (eq what 'today) 4091 (cond ((eq what 'today)
2351 (progn 4092 (progn
2352 (setq ndate (calendar-date-string (calendar-current-date) t t)) 4093 (setq ndate (calendar-date-string
2353 (replace-match ndate nil nil nil 1)) 4094 (calendar-current-date) t t))
2354 (unless (eq what 'timeonly) 4095 (replace-match ndate nil nil nil 1)))
2355 (when first 4096 ((eq what 'calendar)
2356 (setq ndate (if (save-match-data (string-match "[0-9]+" odate)) 4097 (setq ndate (save-match-data (todos-set-date-from-calendar)))
2357 (if (y-or-n-p "Change date? ") 4098 (replace-match ndate nil nil nil 1))
2358 (todos-read-date) 4099 (t
2359 (todos-read-dayname)) 4100 (unless (eq what 'timeonly)
2360 (if (y-or-n-p "Change day? ") 4101 (when first
2361 (todos-read-dayname) 4102 (setq ndate (if (save-match-data
2362 (todos-read-date))))) 4103 (string-match "[0-9]+" odate))
2363 (replace-match ndate nil nil nil 1)) 4104 (if (y-or-n-p "Change date? ")
2364 (unless (eq what 'dateonly) 4105 (todos-read-date)
2365 (when first 4106 (todos-read-dayname))
2366 (setq ntime (save-match-data (todos-read-time))) 4107 (if (y-or-n-p "Change day? ")
2367 (when (< 0 (length ntime)) (setq ntime (concat " " ntime)))) 4108 (todos-read-dayname)
2368 (if otime 4109 (todos-read-date)))))
2369 (replace-match ntime nil nil nil 2) 4110 (replace-match ndate nil nil nil 1))
2370 (goto-char (match-end 1)) 4111 (unless (eq what 'dateonly)
2371 (insert ntime)))) 4112 (when first
4113 (setq ntime (save-match-data (todos-read-time)))
4114 (when (< 0 (length ntime))
4115 (setq ntime (concat " " ntime))))
4116 (if otime
4117 (replace-match ntime nil nil nil 2)
4118 (goto-char (match-end 1))
4119 (insert ntime)))))
4120 (setq todos-date-from-calendar nil)
2372 (setq first nil)) 4121 (setq first nil))
2373 (if marked 4122 (if marked
2374 (todos-forward-item) 4123 (todos-forward-item)
2375 (goto-char (point-max)))))))) 4124 (goto-char (point-max))))))))
2376 4125
2377(defun todos-edit-item-date () 4126(defun todos-edit-item-date ()
2378 "Prompt For and apply changes to current item's date." 4127 "Prompt for and apply changes to current item's date."
2379 (interactive) 4128 (interactive)
2380 (todos-edit-item-header 'dateonly)) 4129 (todos-edit-item-header 'dateonly))
2381 4130
4131(defun todos-edit-item-date-from-calendar ()
4132 "Prompt for changes to current item's date and apply from Calendar."
4133 (interactive)
4134 (todos-edit-item-header 'calendar))
4135
2382(defun todos-edit-item-date-is-today () 4136(defun todos-edit-item-date-is-today ()
2383 "Set item date to today's date." 4137 "Set item date to today's date."
2384 (interactive) 4138 (interactive)
@@ -2389,12 +4143,134 @@ the date or only the time, or to set the date to today."
2389 (interactive) 4143 (interactive)
2390 (todos-edit-item-header 'timeonly)) 4144 (todos-edit-item-header 'timeonly))
2391 4145
4146(defun todos-edit-item-diary-inclusion ()
4147 "Change diary status of one or more todo items in this category.
4148That is, insert `todos-nondiary-marker' if the candidate items
4149lack this marking; otherwise, remove it.
4150
4151If there are marked todo items, change the diary status of all
4152and only these, otherwise change the diary status of the item at
4153point."
4154 (interactive)
4155 (let ((buffer-read-only)
4156 (marked (assoc (todos-current-category)
4157 todos-categories-with-marks)))
4158 (catch 'stop
4159 (save-excursion
4160 (when marked (goto-char (point-min)))
4161 (while (not (eobp))
4162 (if (todos-done-item-p)
4163 (throw 'stop (message "Done items cannot be edited"))
4164 (unless (and marked (not (todos-marked-item-p)))
4165 (let* ((beg (todos-item-start))
4166 (lim (save-excursion (todos-item-end)))
4167 (end (save-excursion
4168 (or (todos-time-string-matcher lim)
4169 (todos-date-string-matcher lim)))))
4170 (if (looking-at (regexp-quote todos-nondiary-start))
4171 (progn
4172 (replace-match "")
4173 (search-forward todos-nondiary-end (1+ end) t)
4174 (replace-match "")
4175 (todos-set-count 'diary 1))
4176 (when end
4177 (insert todos-nondiary-start)
4178 (goto-char (1+ end))
4179 (insert todos-nondiary-end)
4180 (todos-set-count 'diary -1)))))
4181 (unless marked (throw 'stop nil))
4182 (todos-forward-item)))))
4183 (todos-update-categories-sexp)))
4184
4185(defun todos-edit-category-diary-inclusion (arg)
4186 "Make all items in this category diary items.
4187With prefix ARG, make all items in this category non-diary
4188items."
4189 (interactive "P")
4190 (save-excursion
4191 (goto-char (point-min))
4192 (let ((todo-count (todos-get-count 'todo))
4193 (diary-count (todos-get-count 'diary))
4194 (buffer-read-only))
4195 (catch 'stop
4196 (while (not (eobp))
4197 (if (todos-done-item-p) ; We've gone too far.
4198 (throw 'stop nil)
4199 (let* ((beg (todos-item-start))
4200 (lim (save-excursion (todos-item-end)))
4201 (end (save-excursion
4202 (or (todos-time-string-matcher lim)
4203 (todos-date-string-matcher lim)))))
4204 (if arg
4205 (unless (looking-at (regexp-quote todos-nondiary-start))
4206 (insert todos-nondiary-start)
4207 (goto-char (1+ end))
4208 (insert todos-nondiary-end))
4209 (when (looking-at (regexp-quote todos-nondiary-start))
4210 (replace-match "")
4211 (search-forward todos-nondiary-end (1+ end) t)
4212 (replace-match "")))))
4213 (todos-forward-item))
4214 (unless (if arg (zerop diary-count) (= diary-count todo-count))
4215 (todos-set-count 'diary (if arg
4216 (- diary-count)
4217 (- todo-count diary-count))))
4218 (todos-update-categories-sexp)))))
4219
4220(defun todos-edit-item-diary-nonmarking ()
4221 "Change non-marking of one or more diary items in this category.
4222That is, insert `diary-nonmarking-symbol' if the candidate items
4223lack this marking; otherwise, remove it.
4224
4225If there are marked todo items, change the non-marking status of
4226all and only these, otherwise change the non-marking status of
4227the item at point."
4228 (interactive)
4229 (let ((buffer-read-only)
4230 (marked (assoc (todos-current-category)
4231 todos-categories-with-marks)))
4232 (catch 'stop
4233 (save-excursion
4234 (when marked (goto-char (point-min)))
4235 (while (not (eobp))
4236 (if (todos-done-item-p)
4237 (throw 'stop (message "Done items cannot be edited"))
4238 (unless (and marked (not (todos-marked-item-p)))
4239 (todos-item-start)
4240 (unless (looking-at (regexp-quote todos-nondiary-start))
4241 (if (looking-at (regexp-quote diary-nonmarking-symbol))
4242 (replace-match "")
4243 (insert diary-nonmarking-symbol))))
4244 (unless marked (throw 'stop nil))
4245 (todos-forward-item)))))))
4246
4247(defun todos-edit-category-diary-nonmarking (arg)
4248 "Add `diary-nonmarking-symbol' to all diary items in this category.
4249With prefix ARG, remove `diary-nonmarking-symbol' from all diary
4250items in this category."
4251 (interactive "P")
4252 (save-excursion
4253 (goto-char (point-min))
4254 (let (buffer-read-only)
4255 (catch 'stop
4256 (while (not (eobp))
4257 (if (todos-done-item-p) ; We've gone too far.
4258 (throw 'stop nil)
4259 (unless (looking-at (regexp-quote todos-nondiary-start))
4260 (if arg
4261 (when (looking-at (regexp-quote diary-nonmarking-symbol))
4262 (replace-match ""))
4263 (unless (looking-at (regexp-quote diary-nonmarking-symbol))
4264 (insert diary-nonmarking-symbol))))
4265 (todos-forward-item)))))))
4266
2392(defun todos-raise-item-priority (&optional lower) 4267(defun todos-raise-item-priority (&optional lower)
2393 "Raise priority of current item by moving it up by one item. 4268 "Raise priority of current item by moving it up by one item.
2394With non-nil argument LOWER lower item's priority." 4269With non-nil argument LOWER lower item's priority."
2395 (interactive) 4270 (interactive)
2396 (unless (or (todos-done-item-p) 4271 (unless (or (todos-done-item-p)
2397 (looking-at "^$")) ; We're between todo and done items. 4272 ;; Point is between todo and done items.
4273 (looking-at "^$"))
2398 (let (buffer-read-only) 4274 (let (buffer-read-only)
2399 (if (or (and lower 4275 (if (or (and lower
2400 (save-excursion 4276 (save-excursion
@@ -2402,10 +4278,10 @@ With non-nil argument LOWER lower item's priority."
2402 (todos-forward-item) 4278 (todos-forward-item)
2403 (and (looking-at todos-item-start) 4279 (and (looking-at todos-item-start)
2404 (not (todos-done-item-p))))) 4280 (not (todos-done-item-p)))))
2405 ;; Can't raise or lower only todo item. 4281 ;; Can't raise or lower todo item when it's the only one.
2406 (> (count-lines (point-min) (point)) 0)) 4282 (> (count-lines (point-min) (point)) 0))
2407 (let ((item (todos-item-string)) 4283 (let ((item (todos-item-string))
2408 (marked (todos-item-marked-p))) 4284 (marked (todos-marked-item-p)))
2409 ;; In Todos Top Priorities mode, an item's priority can be changed 4285 ;; In Todos Top Priorities mode, an item's priority can be changed
2410 ;; wrt items in another category, but not wrt items in the same 4286 ;; wrt items in another category, but not wrt items in the same
2411 ;; category. 4287 ;; category.
@@ -2441,6 +4317,7 @@ With non-nil argument LOWER lower item's priority."
2441 (todos-raise-item-priority t)) 4317 (todos-raise-item-priority t))
2442 4318
2443;; FIXME: incorporate todos-(raise|lower)-item-priority ? 4319;; FIXME: incorporate todos-(raise|lower)-item-priority ?
4320;; FIXME: does this DTRT in todos-categories-mode?
2444(defun todos-set-item-priority (item cat &optional new) 4321(defun todos-set-item-priority (item cat &optional new)
2445 "Set todo ITEM's priority in category CAT, moving item as needed. 4322 "Set todo ITEM's priority in category CAT, moving item as needed.
2446Interactively, the item and the category are the current ones, 4323Interactively, the item and the category are the current ones,
@@ -2473,7 +4350,6 @@ priority is one more than the number of items in CAT."
2473 (unless (= priority 1) (todos-forward-item (1- priority)))) 4350 (unless (= priority 1) (todos-forward-item (1- priority))))
2474 (todos-insert-with-overlays item))) 4351 (todos-insert-with-overlays item)))
2475 4352
2476;; FIXME: apply to marked items?
2477(defun todos-move-item (&optional file) 4353(defun todos-move-item (&optional file)
2478 "Move at least one todo item to another category. 4354 "Move at least one todo item to another category.
2479 4355
@@ -2488,7 +4364,8 @@ then it is created and the item(s) become(s) the first
2488entry/entries in that category." 4364entry/entries in that category."
2489 (interactive) 4365 (interactive)
2490 (unless (or (todos-done-item-p) 4366 (unless (or (todos-done-item-p)
2491 (looking-at "^$")) ; We're between todo and done items. 4367 ;; Point is between todo and done items.
4368 (looking-at "^$"))
2492 (let* ((buffer-read-only) 4369 (let* ((buffer-read-only)
2493 (file1 todos-current-todos-file) 4370 (file1 todos-current-todos-file)
2494 (cat1 (todos-current-category)) 4371 (cat1 (todos-current-category))
@@ -2523,7 +4400,7 @@ entry/entries in that category."
2523 (setq item nil) 4400 (setq item nil)
2524 (goto-char (point-min)) 4401 (goto-char (point-min))
2525 (while (not (eobp)) 4402 (while (not (eobp))
2526 (when (todos-item-marked-p) 4403 (when (todos-marked-item-p)
2527 (setq item (concat item (todos-item-string) "\n")) 4404 (setq item (concat item (todos-item-string) "\n"))
2528 (setq count (1+ count)) 4405 (setq count (1+ count))
2529 (when (todos-diary-item-p) 4406 (when (todos-diary-item-p)
@@ -2558,7 +4435,7 @@ entry/entries in that category."
2558 (setq end (match-beginning 0)) 4435 (setq end (match-beginning 0))
2559 (goto-char beg) 4436 (goto-char beg)
2560 (while (< (point) end) 4437 (while (< (point) end)
2561 (if (todos-item-marked-p) 4438 (if (todos-marked-item-p)
2562 (todos-remove-item) 4439 (todos-remove-item)
2563 (todos-forward-item)))) 4440 (todos-forward-item))))
2564 (todos-remove-item)))) 4441 (todos-remove-item))))
@@ -2576,40 +4453,82 @@ entry/entries in that category."
2576 (interactive) 4453 (interactive)
2577 (todos-move-item t)) 4454 (todos-move-item t))
2578 4455
2579;; FIXME: apply to marked items? 4456(defun todos-move-item-to-diary ()
4457 "Move one or more items in current category to the diary file.
4458
4459If there are marked items, move all of these; otherwise, move
4460the item at point."
4461 (interactive)
4462 ;; FIXME
4463 )
4464
4465;; FIXME: make adding date customizable, and make this and time customization
4466;; overridable via double prefix arg ??
2580(defun todos-item-done (&optional arg) 4467(defun todos-item-done (&optional arg)
2581 "Tag this item as done and move it to category's done section. 4468 "Tag at least one item in this category as done and hide it.
2582With prefix argument ARG prompt for a comment and append it to the 4469
2583done item." 4470With prefix argument ARG prompt for a comment and append it to
4471the done item; this is only possible if there are no marked
4472items. If there are marked items, tag all of these with
4473`todos-done-string' plus the current date and, if
4474`todos-always-add-time-string' is non-nil, the current time;
4475otherwise, just tag the item at point. Items tagged as done are
4476relocated to the category's (by default hidden) done section."
2584 (interactive "P") 4477 (interactive "P")
2585 (unless (or (todos-done-item-p) 4478 (let* ((cat (todos-current-category))
2586 (looking-at "^$")) 4479 (marked (assoc cat todos-categories-with-marks)))
2587 (let* ((buffer-read-only) 4480 (unless (or (todos-done-item-p)
2588 (item (todos-item-string)) 4481 (and (looking-at "^$") (not marked)))
2589 (diary-item (todos-diary-item-p)) 4482 (let* ((date-string (calendar-date-string (calendar-current-date) t t))
2590 (date-string (calendar-date-string (calendar-current-date) t t)) 4483 (time-string (if todos-always-add-time-string
2591 (time-string (if todos-always-add-time-string ;FIXME: delete condition 4484 (concat " " (substring (current-time-string) 11 16))
2592 (concat " " (substring (current-time-string) 11 16)) 4485 ""))
2593 "")) 4486 (done-prefix (concat "[" todos-done-string date-string time-string
2594 ;; FIXME: todos-nondiary-* ? 4487 "] "))
2595 (done-item (concat "[" todos-done-string date-string time-string "] " 4488 (comment (and arg (not marked) (read-string "Enter a comment: ")))
2596 item)) 4489 (item-count 0)
2597 (comment (and arg (read-string "Enter a comment: ")))) 4490 (diary-count 0)
2598 (todos-remove-item) 4491 item done-item
2599 (unless (zerop (length comment)) 4492 (buffer-read-only))
2600 (setq done-item (concat done-item " [" todos-comment-string ": " 4493 (and marked (goto-char (point-min)))
2601 comment "]"))) 4494 (catch 'done
2602 (save-excursion 4495 (while (not (eobp))
2603 (widen) 4496 (if (or (not marked) (and marked (todos-marked-item-p)))
2604 (re-search-forward (concat "^" (regexp-quote todos-category-done)) nil t) 4497 (progn
2605 (forward-char) 4498 (setq item (todos-item-string))
2606 (todos-insert-with-overlays done-item)) 4499 (setq done-item (cond (marked
2607 (todos-set-count 'todo -1) 4500 (concat done-item done-prefix item "\n"))
2608 (todos-set-count 'done 1) 4501 (comment
2609 (and diary-item (todos-set-count 'diary -1)) 4502 (concat done-prefix item " ["
2610 (todos-update-categories-sexp) 4503 todos-comment-string
2611 (save-excursion (todos-category-select))))) 4504 ": " comment "]"))
2612 4505 (t
4506 (concat done-prefix item))))
4507 (setq item-count (1+ item-count))
4508 (when (todos-diary-item-p)
4509 (setq diary-count (1+ diary-count)))
4510 (todos-remove-item)
4511 (unless marked (throw 'done nil)))
4512 (todos-forward-item))))
4513 (when marked
4514 ;; Chop off last newline of done item string.
4515 (setq done-item (substring done-item 0 -1))
4516 (remove-overlays (point-min) (point-max) 'before-string todos-item-mark)
4517 (setq todos-categories-with-marks
4518 (assq-delete-all cat todos-categories-with-marks)))
4519 (save-excursion
4520 (widen)
4521 (re-search-forward
4522 (concat "^" (regexp-quote todos-category-done)) nil t)
4523 (forward-char)
4524 (insert done-item "\n"))
4525 (todos-set-count 'todo (- item-count))
4526 (todos-set-count 'done item-count)
4527 (todos-set-count 'diary (- diary-count))
4528 (todos-update-categories-sexp)
4529 (save-excursion (todos-category-select))))))
4530
4531;; FIXME: only if there's no comment, or edit an existing comment?
2613(defun todos-comment-done-item () 4532(defun todos-comment-done-item ()
2614 "Add a comment to this done item." 4533 "Add a comment to this done item."
2615 (interactive) 4534 (interactive)
@@ -2668,7 +4587,7 @@ If the archive of this file does not exist, it is created. If
2668this category does not exist in the archive, it is created." 4587this category does not exist in the archive, it is created."
2669 (interactive) 4588 (interactive)
2670 (when (not (member (buffer-file-name) (funcall todos-files-function t))) 4589 (when (not (member (buffer-file-name) (funcall todos-files-function t)))
2671 (if (and all (zerop (todos-get-count 'done cat))) 4590 (if (and all (zerop (todos-get-count 'done)))
2672 (message "No done items in this category") 4591 (message "No done items in this category")
2673 (catch 'end 4592 (catch 'end
2674 (let* ((cat (todos-current-category)) 4593 (let* ((cat (todos-current-category))
@@ -2697,7 +4616,7 @@ this category does not exist in the archive, it is created."
2697 (save-excursion 4616 (save-excursion
2698 (goto-char (point-min)) 4617 (goto-char (point-min))
2699 (while (not (eobp)) 4618 (while (not (eobp))
2700 (if (todos-item-marked-p) 4619 (if (todos-marked-item-p)
2701 (if (not (todos-done-item-p)) 4620 (if (not (todos-done-item-p))
2702 (throw 'end (message "Only done items can be archived")) 4621 (throw 'end (message "Only done items can be archived"))
2703 (concat marked-items (todos-item-string) "\n") 4622 (concat marked-items (todos-item-string) "\n")
@@ -2751,7 +4670,7 @@ this category does not exist in the archive, it is created."
2751 (and marked (goto-char (point-min))) 4670 (and marked (goto-char (point-min)))
2752 (catch 'done 4671 (catch 'done
2753 (while (not (eobp)) 4672 (while (not (eobp))
2754 (if (or (and marked (todos-item-marked-p)) item) 4673 (if (or (and marked (todos-marked-item-p)) item)
2755 (progn 4674 (progn
2756 (todos-remove-item) 4675 (todos-remove-item)
2757 (todos-set-count 'done -1) 4676 (todos-set-count 'done -1)
@@ -2816,7 +4735,7 @@ archive, the archive file is deleted."
2816 (save-excursion 4735 (save-excursion
2817 (goto-char (point-min)) 4736 (goto-char (point-min))
2818 (while (not (eobp)) 4737 (while (not (eobp))
2819 (when (todos-item-marked-p) 4738 (when (todos-marked-item-p)
2820 (concat marked-items (todos-item-string) "\n") 4739 (concat marked-items (todos-item-string) "\n")
2821 (setq marked-count (1+ marked-count))) 4740 (setq marked-count (1+ marked-count)))
2822 (todos-forward-item))) 4741 (todos-forward-item)))
@@ -2854,7 +4773,7 @@ archive, the archive file is deleted."
2854 (and marked (goto-char (point-min))) 4773 (and marked (goto-char (point-min)))
2855 (catch 'done 4774 (catch 'done
2856 (while (not (eobp)) 4775 (while (not (eobp))
2857 (if (or (and marked (todos-item-marked-p)) item) 4776 (if (or (and marked (todos-marked-item-p)) item)
2858 (progn 4777 (progn
2859 (todos-remove-item) 4778 (todos-remove-item)
2860 (todos-set-count 'done -1) 4779 (todos-set-count 'done -1)
@@ -2905,1191 +4824,11 @@ archive, the archive file is deleted."
2905 (interactive) 4824 (interactive)
2906 (todos-unarchive-items t)) 4825 (todos-unarchive-items t))
2907 4826
2908(defun todos-toggle-diary-inclusion (&optional all)
2909 "Toggle diary status of one or more todo items in this category.
2910
2911If a candidate item is marked with `todos-nondiary-marker',
2912remove this marker; otherwise, insert it.
2913
2914With non-nil argument ALL toggle the diary status of all todo
2915items in this category; otherwise, if there are marked todo
2916items, toggle the diary status of all and only these, otherwise
2917toggle the diary status of the item at point. "
2918 (interactive)
2919 (let ((marked (assoc (todos-current-category)
2920 todos-categories-with-marks)))
2921 (catch 'stop
2922 (save-excursion
2923 (save-restriction
2924 (when (or marked all) (goto-char (point-min)))
2925 (while (not (eobp))
2926 (if (todos-done-item-p)
2927 (throw 'stop (message "Done items cannot be changed"))
2928 (unless (and marked (not (todos-item-marked-p)))
2929 (save-excursion
2930 (let* ((buffer-read-only)
2931 (beg (todos-item-start))
2932 (lim (save-excursion (todos-item-end)))
2933 (end (save-excursion
2934 (or (todos-time-string-matcher lim)
2935 (todos-date-string-matcher lim)))))
2936 (if (looking-at (regexp-quote todos-nondiary-start))
2937 (progn
2938 (replace-match "")
2939 (search-forward todos-nondiary-end (1+ end) t)
2940 (replace-match "")
2941 (todos-set-count 'diary 1))
2942 (when end
2943 (insert todos-nondiary-start)
2944 (goto-char (1+ end))
2945 (insert todos-nondiary-end)
2946 (todos-set-count 'diary -1))))))
2947 (unless (or marked all) (throw 'stop nil))
2948 (todos-forward-item))))))
2949 (todos-update-categories-sexp)))
2950
2951(defun todos-toggle-item-diary-nonmarking ()
2952 "Mark or unmark this todos diary item for calendar display.
2953See `diary-nonmarking-symbol'."
2954 (interactive)
2955 (let ((buffer-read-only))
2956 (save-excursion
2957 (todos-item-start)
2958 (unless (looking-at (regexp-quote todos-nondiary-start))
2959 (if (looking-at (regexp-quote diary-nonmarking-symbol))
2960 (replace-match "")
2961 (insert diary-nonmarking-symbol))))))
2962
2963(defun todos-toggle-diary-nonmarking ()
2964 "Mark or unmark this category's todos diary items for calendar.
2965See `diary-nonmarking-symbol'."
2966 (interactive)
2967 (save-excursion
2968 (goto-char (point-min))
2969 (while (not (eobp))
2970 (todos-toggle-item-diary-nonmarking)
2971 (todos-forward-item))))
2972
2973(defun todos-print (&optional to-file)
2974 "Produce a printable version of the current Todos buffer.
2975This includes overlays, indentation, and, depending on the value
2976of `todos-print-function', faces. With non-nil argument TO-FILE
2977write the printable version to a file; otherwise, send it to the
2978default printer."
2979 (interactive)
2980 (let ((buf todos-tmp-buffer-name) ;FIXME
2981 (header (cond
2982 ((eq major-mode 'todos-mode)
2983 (concat "Todos File: "
2984 (file-name-sans-extension
2985 (file-name-nondirectory todos-current-todos-file))
2986 "\nCategory: " (todos-current-category)))
2987 ((eq major-mode 'todos-filter-items-mode)
2988 "Todos Top Priorities")))
2989 (prefix (propertize (concat todos-prefix " ")
2990 'face 'todos-prefix-string))
2991 (num 0)
2992 (fill-prefix (make-string todos-indent-to-here 32))
2993 (content (buffer-string))
2994 file)
2995 (with-current-buffer (get-buffer-create buf)
2996 (insert content)
2997 (goto-char (point-min))
2998 (while (not (eobp))
2999 (let ((beg (point))
3000 (end (save-excursion (todos-item-end))))
3001 (when todos-number-prefix
3002 (setq num (1+ num))
3003 (setq prefix (propertize (concat (number-to-string num) " ")
3004 'face 'todos-prefix-string)))
3005 (insert prefix)
3006 (fill-region beg end))
3007 ;; Calling todos-forward-item infloops at todos-item-start due to
3008 ;; non-overlay prefix, so search for item start instead.
3009 (if (re-search-forward todos-item-start nil t)
3010 (beginning-of-line)
3011 (goto-char (point-max))))
3012 (if (re-search-backward (concat "^" (regexp-quote todos-category-done))
3013 nil t)
3014 (replace-match todos-done-separator))
3015 (goto-char (point-min))
3016 (insert header)
3017 (newline 2)
3018 (if to-file
3019 (let ((file (read-file-name "Print to file: ")))
3020 (funcall todos-print-function file))
3021 (funcall todos-print-function)))
3022 (kill-buffer buf)))
3023
3024(defun todos-print-to-file ()
3025 "Save printable version of this Todos buffer to a file."
3026 (interactive)
3027 (todos-print t))
3028
3029;; ---------------------------------------------------------------------------
3030
3031;;; Internals
3032
3033(defvar todos-date-pattern ;FIXME: start with "^" ?
3034 (let ((dayname (diary-name-pattern calendar-day-name-array nil t)))
3035 (concat "\\(?:" dayname "\\|"
3036 (let ((dayname)
3037 (monthname (format "\\(?:%s\\|\\*\\)"
3038 (diary-name-pattern
3039 calendar-month-name-array
3040 calendar-month-abbrev-array t)))
3041 (month "\\(?:[0-9]+\\|\\*\\)")
3042 (day "\\(?:[0-9]+\\|\\*\\)")
3043 (year "-?\\(?:[0-9]+\\|\\*\\)"))
3044 (mapconcat 'eval calendar-date-display-form ""))
3045 "\\)"))
3046 "Regular expression matching a Todos date header.")
3047
3048(defvar todos-date-string-start
3049 ;; FIXME: with ? matches anything
3050 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|"
3051 (regexp-quote diary-nonmarking-symbol) "\\)?")
3052 "Regular expression matching part of item header before the date.")
3053
3054(defvar todos-done-string-start
3055 (concat "^\\[" (regexp-quote todos-done-string))
3056 "Regular expression matching start of done item.")
3057
3058(defun todos-date-string-matcher (lim)
3059 "Search for Todos date strings within LIM for font-locking."
3060 (re-search-forward
3061 (concat todos-date-string-start "\\(?1:" todos-date-pattern "\\)") lim t))
3062
3063(defun todos-time-string-matcher (lim)
3064 "Search for Todos time strings within LIM for font-locking."
3065 (re-search-forward (concat todos-date-string-start todos-date-pattern
3066 " \\(?1:" diary-time-regexp "\\)") lim t))
3067
3068(defun todos-done-string-matcher (lim)
3069 "Search for Todos done headers within LIM for font-locking."
3070 (re-search-forward (concat todos-done-string-start
3071 "[^][]+]")
3072 lim t))
3073
3074(defun todos-comment-string-matcher (lim)
3075 "Search for Todos done comment within LIM for font-locking."
3076 (re-search-forward (concat "\\[\\(?1:" todos-comment-string "\\):")
3077 lim t))
3078
3079(defun todos-category-string-matcher (lim)
3080 "Search for Todos category headers within LIM for font-locking."
3081 (if (eq major-mode 'todos-filter-items-mode)
3082 (re-search-forward
3083 ;; (concat "^\\(?1:" (regexp-quote todos-category-beg) ".*\\)$")
3084 (concat "\\(?:^\\[?" todos-date-pattern "\\(?: " diary-time-regexp
3085 "\\)?\\]?\\) \\(?1:\\[.+\\]\\)") lim t)))
3086
3087(defun todos-check-format ()
3088 "Signal an error if the current Todos file is ill-formatted."
3089 (save-excursion
3090 (save-restriction
3091 (widen)
3092 (goto-char (point-min))
3093 (let ((legit (concat "^\\(" (regexp-quote todos-category-beg) "\\)"
3094 "\\|\\(\\[?" todos-date-pattern "\\)"
3095 "\\|\\([ \t]+[^ \t]*\\)"
3096 "\\|$")))
3097 (while (not (eobp))
3098 (unless (looking-at legit)
3099 (error "Illegitimate Todos file format at line %d"
3100 (line-number-at-pos (point))))
3101 (forward-line)))))
3102 (message "This Todos file is well-formatted."))
3103
3104(defun todos-after-find-file ()
3105 "Show Todos files correctly when visited from outside of Todos mode."
3106 (and (member this-command todos-visit-files-commands)
3107 (= (- (point-max) (point-min)) (buffer-size))
3108 (member major-mode '(todos-mode todos-archive-mode))
3109 (todos-category-select)))
3110
3111(defun todos-wrap-and-indent ()
3112 "Use word wrapping on long lines and indent with a wrap prefix.
3113The amount of indentation is given by user option
3114`todos-indent-to-here'."
3115 (set (make-local-variable 'word-wrap) t)
3116 (set (make-local-variable 'wrap-prefix) (make-string todos-indent-to-here 32))
3117 (unless (member '(continuation) fringe-indicator-alist)
3118 (push '(continuation) fringe-indicator-alist)))
3119
3120(defun todos-indent ()
3121 "Indent from point to `todos-indent-to-here'."
3122 (indent-to todos-indent-to-here todos-indent-to-here))
3123
3124(defun todos-prefix-overlays ()
3125 "Put before-string overlay in front of this category's items.
3126The overlay's value is the string `todos-prefix' or with non-nil
3127`todos-number-prefix' an integer in the sequence from 1 to the
3128number of todo or done items in the category indicating the
3129item's priority. Todo and done items are numbered independently
3130of each other."
3131 (when (or todos-number-prefix
3132 (not (string-match "^[[:space:]]*$" todos-prefix)))
3133 (let ((prefix (propertize (concat todos-prefix " ")
3134 'face 'todos-prefix-string))
3135 (num 0))
3136 (save-excursion
3137 (goto-char (point-min))
3138 (while (not (eobp))
3139 (when (or (todos-date-string-matcher (line-end-position))
3140 (todos-done-string-matcher (line-end-position)))
3141 (goto-char (match-beginning 0))
3142 (when todos-number-prefix
3143 (setq num (1+ num))
3144 ;; Reset number for done items.
3145 (when
3146 ;; FIXME: really need this?
3147 ;; If last not done item is multiline, then
3148 ;; todos-done-string-matcher skips empty line, so have
3149 ;; to look back.
3150 (and (looking-at todos-done-string-start)
3151 (looking-back (concat "^"
3152 (regexp-quote todos-category-done)
3153 "\n")))
3154 (setq num 1))
3155 (setq prefix (propertize (concat (number-to-string num) " ")
3156 'face 'todos-prefix-string)))
3157 (let ((ovs (overlays-in (point) (point)))
3158 marked ov-pref)
3159 (if ovs
3160 (dolist (ov ovs)
3161 (let ((val (overlay-get ov 'before-string)))
3162 (if (equal val "*")
3163 (setq marked t)
3164 (setq ov-pref val)))))
3165 (unless (equal ov-pref prefix)
3166 (remove-overlays (point) (point)) ; 'before-string) doesn't work
3167 (overlay-put (make-overlay (point) (point))
3168 'before-string prefix)
3169 (and marked (overlay-put (make-overlay (point) (point))
3170 'before-string todos-item-mark)))))
3171 (forward-line))))))
3172
3173(defun todos-reset-prefix (symbol value)
3174 "The :set function for `todos-prefix' and `todos-number-prefix'."
3175 (let ((oldvalue (symbol-value symbol))
3176 (files (append todos-files todos-archives)))
3177 (custom-set-default symbol value)
3178 (when (not (equal value oldvalue))
3179 (dolist (f files)
3180 (with-current-buffer (find-file-noselect f)
3181 (save-window-excursion
3182 (todos-show)
3183 (save-excursion
3184 (widen)
3185 (goto-char (point-min))
3186 (while (not (eobp))
3187 (remove-overlays (point) (point)); 'before-string prefix)
3188 (forward-line)))
3189 ;; Activate the new setting (save-restriction does not help).
3190 (save-excursion (todos-category-select))))))))
3191
3192(defun todos-reset-nondiary-marker (symbol value)
3193 "The :set function for user option `todos-nondiary-marker'."
3194 (let ((oldvalue (symbol-value symbol))
3195 (files (append todos-files todos-archives)))
3196 (custom-set-default symbol value)
3197 ;; Need to reset these to get font-locking right.
3198 (setq todos-nondiary-start (nth 0 todos-nondiary-marker)
3199 todos-nondiary-end (nth 1 todos-nondiary-marker)
3200 todos-date-string-start
3201 ;; FIXME: with ? matches anything
3202 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|"
3203 (regexp-quote diary-nonmarking-symbol) "\\)?"))
3204 (when (not (equal value oldvalue))
3205 (dolist (f files)
3206 (with-current-buffer (find-file-noselect f)
3207 (let (buffer-read-only)
3208 (widen)
3209 (goto-char (point-min))
3210 (while (not (eobp))
3211 (if (re-search-forward
3212 (concat "^\\(" todos-done-string-start "[^][]+] \\)?"
3213 "\\(?1:" (regexp-quote (car oldvalue))
3214 "\\)" todos-date-pattern "\\( "
3215 diary-time-regexp "\\)?\\(?2:"
3216 (regexp-quote (cadr oldvalue)) "\\)")
3217 nil t)
3218 (progn
3219 (replace-match (nth 0 value) t t nil 1)
3220 (replace-match (nth 1 value) t t nil 2))
3221 (forward-line)))
3222 (todos-category-select)))))))
3223
3224(defun todos-reset-done-string (symbol value)
3225 "The :set function for user option `todos-done-string'."
3226 (let ((oldvalue (symbol-value symbol))
3227 (files (append todos-files todos-archives)))
3228 (custom-set-default symbol value)
3229 ;; Need to reset this to get font-locking right.
3230 (setq todos-done-string-start
3231 (concat "^\\[" (regexp-quote todos-done-string)))
3232 (when (not (equal value oldvalue))
3233 (dolist (f files)
3234 (with-current-buffer (find-file-noselect f)
3235 (let (buffer-read-only)
3236 (widen)
3237 (goto-char (point-min))
3238 (while (not (eobp))
3239 (if (re-search-forward
3240 (concat "^" (regexp-quote todos-nondiary-start)
3241 "\\(" (regexp-quote oldvalue) "\\)")
3242 nil t)
3243 (replace-match value t t nil 1)
3244 (forward-line)))
3245 (todos-category-select)))))))
3246
3247(defun todos-reset-comment-string (symbol value)
3248 "The :set function for user option `todos-comment-string'."
3249 (let ((oldvalue (symbol-value symbol))
3250 (files (append todos-files todos-archives)))
3251 (custom-set-default symbol value)
3252 (when (not (equal value oldvalue))
3253 (dolist (f files)
3254 (with-current-buffer (find-file-noselect f)
3255 (let (buffer-read-only)
3256 (save-excursion
3257 (widen)
3258 (goto-char (point-min))
3259 (while (not (eobp))
3260 (if (re-search-forward
3261 (concat
3262 "\\[\\(" (regexp-quote oldvalue) "\\): [^]]*\\]")
3263 nil t)
3264 (replace-match value t t nil 1)
3265 (forward-line)))
3266 (todos-category-select))))))))
3267
3268(defun todos-reset-categories (symbol value)
3269 "The :set function for `todos-ignore-archived-categories'."
3270 (custom-set-default symbol value)
3271 (dolist (f (funcall todos-files-function))
3272 (with-current-buffer (find-file-noselect f)
3273 (if value
3274 (setq todos-categories-full todos-categories
3275 todos-categories (todos-truncate-categories-list))
3276 (setq todos-categories todos-categories-full
3277 todos-categories-full nil))
3278 (todos-category-select))))
3279
3280(defun todos-toggle-show-current-file (symbol value)
3281 "The :set function for user option `todos-show-current-file'."
3282 (custom-set-default symbol value)
3283 (if value
3284 (add-hook 'pre-command-hook 'todos-show-current-file nil t)
3285 (remove-hook 'pre-command-hook 'todos-show-current-file t)))
3286
3287(defun todos-show-current-file ()
3288 "Visit current instead of default Todos file with `todos-show'.
3289This function is added to `pre-command-hook' when user option
3290`todos-show-current-file' is set to non-nil."
3291 (setq todos-global-current-todos-file todos-current-todos-file))
3292 ;; (and (eq major-mode 'todos-mode)
3293 ;; (setq todos-global-current-todos-file (buffer-file-name))))
3294
3295;; FIXME: rename to todos-set-category-number ?
3296(defun todos-category-number (cat)
3297 "Set and return buffer-local value of `todos-category-number'.
3298This value is one more than the index of category CAT, starting
3299with one instead of zero, so that the highest priority
3300category (see `todos-display-categories') has the number one."
3301 (let ((categories (mapcar 'car todos-categories)))
3302 (setq todos-category-number
3303 (1+ (- (length categories)
3304 (length (member cat categories)))))))
3305
3306(defun todos-current-category ()
3307 "Return the name of the current category."
3308 (car (nth (1- todos-category-number) todos-categories)))
3309
3310(defun todos-category-select ()
3311 "Display the current category correctly.
3312
3313With non-nil user option `todos-show-done-only' display only the
3314category's done (but not archived) items; else (the default)
3315display just the todo items, or with non-nil user option
3316`todos-show-with-done' also display the category's done items
3317below the todo items."
3318 (let ((name (todos-current-category))
3319 cat-begin cat-end done-start done-sep-start done-end)
3320 (widen)
3321 (goto-char (point-min))
3322 (re-search-forward
3323 (concat "^" (regexp-quote (concat todos-category-beg name)) "$") nil t)
3324 (setq cat-begin (1+ (line-end-position)))
3325 (setq cat-end (if (re-search-forward
3326 (concat "^" (regexp-quote todos-category-beg)) nil t)
3327 (match-beginning 0)
3328 (point-max)))
3329 (setq mode-line-buffer-identification
3330 (funcall todos-mode-line-function name))
3331 (narrow-to-region cat-begin cat-end)
3332 (todos-prefix-overlays)
3333 (goto-char (point-min))
3334 (if (re-search-forward (concat "\n\\(" (regexp-quote todos-category-done)
3335 "\\)") nil t)
3336 (progn
3337 (setq done-start (match-beginning 0))
3338 (setq done-sep-start (match-beginning 1))
3339 (setq done-end (match-end 0)))
3340 (error "Category %s is missing todos-category-done string" name))
3341 (if todos-show-done-only
3342 (narrow-to-region (1+ done-end) (point-max))
3343 ;; Display or hide done items as per todos-show-with-done.
3344 ;; FIXME: use todos-done-string-start ?
3345 (when (re-search-forward (concat "\n\\(\\["
3346 (regexp-quote todos-done-string)
3347 "\\)") nil t)
3348 (let (done-sep prefix ov-pref ov-done)
3349 ;; FIXME: delete overlay when not viewing done items?
3350 (when todos-show-with-done
3351 (setq done-sep todos-done-separator)
3352 (setq done-start cat-end)
3353 (setq ov-pref (make-overlay done-sep-start done-end))
3354 (overlay-put ov-pref 'display done-sep))))
3355 (narrow-to-region (point-min) done-start))))
3356
3357(defun todos-insert-with-overlays (item)
3358 "Insert ITEM and update prefix/priority number overlays."
3359 (todos-item-start)
3360 (insert item "\n")
3361 (todos-backward-item)
3362 (todos-prefix-overlays))
3363
3364(defvar todos-item-start ;; (concat "^\\(\\[\\(" (regexp-quote todos-done-string)
3365 ;; "\\)?\\)?" todos-date-pattern)
3366 (concat "\\(" todos-date-string-start "\\|" todos-done-string-start
3367 "\\)" todos-date-pattern)
3368 "String identifying start of a Todos item.")
3369
3370(defun todos-item-start ()
3371 "Move to start of current Todos item and return its position."
3372 (unless (or
3373 ;; Point is either on last item in this category or on the empty
3374 ;; line between done and not done items.
3375 (looking-at "^$")
3376 ;; There are no done items in this category yet.
3377 (looking-at (regexp-quote todos-category-beg)))
3378 (goto-char (line-beginning-position))
3379 (while (not (looking-at todos-item-start))
3380 (forward-line -1))
3381 (point)))
3382
3383(defun todos-item-end ()
3384 "Move to end of current Todos item and return its position."
3385 ;; Items cannot end with a blank line.
3386 (unless (looking-at "^$")
3387 (let ((done (todos-done-item-p)))
3388 (todos-forward-item)
3389 ;; Adjust if item is last unfinished one before displayed done items.
3390 (when (and (not done) (todos-done-item-p))
3391 (forward-line -1))
3392 (backward-char))
3393 (point)))
3394
3395(defun todos-remove-item ()
3396 "Internal function called in editing, deleting or moving items."
3397 (let* ((beg (todos-item-start))
3398 (end (progn (todos-item-end) (1+ (point))))
3399 (ovs (overlays-in beg beg)))
3400 ;; There can be both prefix/number and mark overlays.
3401 (while ovs (delete-overlay (car ovs)) (pop ovs))
3402 (delete-region beg end)))
3403
3404(defun todos-item-string ()
3405 "Return bare text of current item as a string."
3406 (let ((opoint (point))
3407 (start (todos-item-start))
3408 (end (todos-item-end)))
3409 (goto-char opoint)
3410 (and start end (buffer-substring-no-properties start end))))
3411
3412(defun todos-diary-item-p ()
3413 "Return non-nil if item at point is marked for diary inclusion."
3414 (save-excursion
3415 (todos-item-start)
3416 (looking-at todos-date-pattern)))
3417
3418(defun todos-done-item-p ()
3419 "Return non-nil if item at point is a done item."
3420 (save-excursion
3421 (todos-item-start)
3422 (looking-at todos-done-string-start)))
3423
3424(defvar todos-item-mark (propertize (if (equal todos-prefix "*") "@" "*")
3425 'face 'todos-mark)
3426 "String used to mark items.")
3427
3428(defun todos-item-marked-p ()
3429 "If this item is marked, return mark overlay."
3430 (let ((ovs (overlays-in (line-beginning-position) (line-beginning-position)))
3431 (mark todos-item-mark)
3432 ov marked)
3433 (catch 'stop
3434 (while ovs
3435 (setq ov (pop ovs))
3436 (and (equal (overlay-get ov 'before-string) mark)
3437 (throw 'stop (setq marked t)))))
3438 (when marked ov)))
3439
3440(defvar todos-categories-with-marks nil
3441 "Alist of categories and number of marked items they contain.")
3442
3443(defun todos-get-count (type &optional category)
3444 "Return count of TYPE items in CATEGORY.
3445If CATEGORY is nil, default to the current category."
3446 (let* ((cat (or category (todos-current-category)))
3447 (counts (cdr (assoc cat todos-categories)))
3448 (idx (cond ((eq type 'todo) 0)
3449 ((eq type 'diary) 1)
3450 ((eq type 'done) 2)
3451 ((eq type 'archived) 3))))
3452 (aref counts idx)))
3453
3454(defun todos-set-count (type increment &optional category)
3455 "Increment count of TYPE items in CATEGORY by INCREMENT.
3456If CATEGORY is nil, default to the current category."
3457 (let* ((cat (or category (todos-current-category)))
3458 (counts (cdr (assoc cat todos-categories)))
3459 (idx (cond ((eq type 'todo) 0)
3460 ((eq type 'diary) 1)
3461 ((eq type 'done) 2)
3462 ((eq type 'archived) 3))))
3463 (aset counts idx (+ increment (aref counts idx)))))
3464
3465;; (defun todos-item-counts (operation &optional cat1 cat2)
3466;; "Update item counts in category CAT1 changed by OPERATION.
3467;; If CAT1 is nil, update counts from the current category. With
3468;; non-nil CAT2 include specified counts from that category in the
3469;; calculation for CAT1.
3470;; After updating the item counts, update the `todos-categories' sexp."
3471;; (let* ((cat (or cat1 (todos-current-category))))
3472;; (cond ((eq type 'insert)
3473;; (todos-set-count 'todo 1 cat))
3474;; ((eq type 'diary)
3475;; (todos-set-count 'diary 1 cat))
3476;; ((eq type 'nondiary)
3477;; (todos-set-count 'diary -1 cat))
3478;; ((eq type 'delete)
3479;; ;; FIXME: ok if last done item was deleted?
3480;; (if (save-excursion
3481;; (re-search-backward (concat "^" (regexp-quote
3482;; todos-category-done)) nil t))
3483;; (todos-set-count 'done -1 cat)
3484;; (todos-set-count 'todo -1 cat)))
3485;; ((eq type 'done)
3486;; (unless (member (buffer-file-name) (funcall todos-files-function t))
3487;; (todos-set-count 'todo -1 cat))
3488;; (todos-set-count 'done 1 cat))
3489;; ((eq type 'undo)
3490;; (todos-set-count 'todo 1 cat)
3491;; (todos-set-count 'done -1 cat))
3492;; ((eq type 'archive1)
3493;; (todos-set-count 'archived 1 cat)
3494;; (todos-set-count 'done -1 cat))
3495;; ((eq type 'archive)
3496;; (if (member (buffer-file-name) (funcall todos-files-function t))
3497;; ;; In Archive file augment done count with cat's previous
3498;; ;; done count,
3499;; (todos-set-count 'done (todos-get-count 'done cat) cat)
3500;; ;; In Todos file augment archive count with cat's previous
3501;; ;; done count, and make the latter zero.
3502;; (todos-set-count 'archived (todos-get-count 'done cat) cat)
3503;; (todos-set-count 'done (- (todos-get-count 'done cat)) cat)))
3504;; ((eq type 'merge)
3505;; ;; Augment todo and done counts of cat by those of cat2.
3506;; (todos-set-count 'todo (todos-get-count 'todo cat2) cat)
3507;; (todos-set-count 'done (todos-get-count 'done cat2) cat)))
3508;; (todos-update-categories-sexp)))
3509
3510(defun todos-set-categories ()
3511 "Set `todos-categories' from the sexp at the top of the file."
3512 ;; New archive files created by `todos-move-category' are empty, which would
3513 ;; make the sexp test fail and raise an error, so in this case we skip it.
3514 (unless (zerop (buffer-size))
3515 (save-excursion
3516 (save-restriction
3517 (widen)
3518 (goto-char (point-min))
3519 ;; todos-truncate-categories-list needs non-nil todos-categories.
3520 (setq todos-categories-full
3521 (if (looking-at "\(\(\"")
3522 (read (buffer-substring-no-properties
3523 (line-beginning-position)
3524 (line-end-position)))
3525 (error "Invalid or missing todos-categories sexp"))
3526 todos-categories todos-categories-full)))
3527 (if (and todos-ignore-archived-categories
3528 (eq major-mode 'todos-mode))
3529 (todos-truncate-categories-list)
3530 todos-categories-full)))
3531
3532;; FIXME: currently unused -- make this a command to rebuild a corrupted
3533;; todos-cats sexp ?
3534(defun todos-make-categories-list (&optional force)
3535 "Return an alist of Todos categories and their item counts.
3536With non-nil argument FORCE parse the entire file to build the
3537list; otherwise, get the value by reading the sexp at the top of
3538the file."
3539 (setq todos-categories nil)
3540 (save-excursion
3541 (save-restriction
3542 (widen)
3543 (goto-char (point-min))
3544 (let (counts cat archive)
3545 ;; FIXME: can todos-archives be too old here?
3546 (unless (member buffer-file-name (funcall todos-files-function t))
3547 (setq archive (concat (file-name-sans-extension
3548 todos-current-todos-file) ".toda")))
3549 (while (not (eobp))
3550 (cond ((looking-at (concat (regexp-quote todos-category-beg)
3551 "\\(.*\\)\n"))
3552 (setq cat (match-string-no-properties 1))
3553 ;; Counts for each category: [todo diary done archive]
3554 (setq counts (make-vector 4 0))
3555 (setq todos-categories
3556 (append todos-categories (list (cons cat counts))))
3557 ;; todos-archives may be too old here (e.g. during
3558 ;; todos-move-category).
3559 (when (member archive (funcall todos-files-function t))
3560 (with-current-buffer (find-file-noselect archive)
3561 (widen)
3562 (goto-char (point-min))
3563 (when (re-search-forward
3564 (concat (regexp-quote todos-category-beg) cat)
3565 (point-max) t)
3566 (forward-line)
3567 (while (not (or (looking-at
3568 (concat
3569 (regexp-quote todos-category-beg)
3570 "\\(.*\\)\n"))
3571 (eobp)))
3572 (when (looking-at todos-done-string-start)
3573 (todos-set-count 'archived 1 cat))
3574 (forward-line))))))
3575 ((looking-at todos-done-string-start)
3576 (todos-set-count 'done 1 cat))
3577 ((looking-at (concat "^\\("
3578 (regexp-quote diary-nonmarking-symbol)
3579 "\\)?" todos-date-pattern))
3580 (todos-set-count 'diary 1 cat)
3581 (todos-set-count 'todo 1 cat))
3582 ((looking-at (concat todos-date-string-start todos-date-pattern))
3583 (todos-set-count 'todo 1 cat))
3584 ;; If first line is todos-categories list, use it and end loop
3585 ;; unless forced by non-nil parameter `force' to scan whole file.
3586 ((bobp)
3587 (unless force
3588 (setq todos-categories (read (buffer-substring-no-properties
3589 (line-beginning-position)
3590 (line-end-position))))
3591 (goto-char (1- (point-max))))))
3592 (forward-line)))))
3593 todos-categories)
3594
3595(defun todos-truncate-categories-list ()
3596 "Return a truncated alist of Todos categories plus item counts.
3597Categories containing only archived items are omitted. This list
3598is used in Todos mode when `todos-ignore-archived-categories' is
3599non-nil."
3600 (let (cats)
3601 (dolist (catcons todos-categories-full cats)
3602 (let ((cat (car catcons)))
3603 (setq cats
3604 (append cats
3605 (unless (and (zerop (todos-get-count 'todo cat))
3606 (zerop (todos-get-count 'done cat))
3607 (not (zerop (todos-get-count 'archived cat))))
3608 (list catcons))))))))
3609
3610(defun todos-update-categories-sexp ()
3611 "Update the `todos-categories' sexp at the top of the file."
3612 (let (buffer-read-only)
3613 (save-excursion
3614 (save-restriction
3615 (widen)
3616 (goto-char (point-min))
3617 (if (looking-at (concat "^" (regexp-quote todos-category-beg)))
3618 (progn (newline) (goto-char (point-min)))
3619 ;; With empty buffer (e.g. with new archive in
3620 ;; `todos-move-category') `kill-line' signals end of buffer.
3621 (kill-region (line-beginning-position) (line-end-position)))
3622 ;; FIXME
3623 ;; (prin1 todos-categories (current-buffer))))))
3624 (prin1 todos-categories-full (current-buffer))))))
3625
3626(defun todos-read-file-name (prompt &optional archive mustmatch)
3627 "Choose and return the name of a Todos file, prompting with PROMPT.
3628Show completions with TAB or SPC; the names are shown in short
3629form but the absolute truename is returned. With non-nil ARCHIVE
3630return the absolute truename of a Todos archive file. With non-nil
3631MUSTMATCH the name of an existing file must be chosen;
3632otherwise, a new file name is allowed." ;FIXME: is this possible?
3633 (unless (file-exists-p todos-files-directory)
3634 (make-directory todos-files-directory))
3635 (let* ((completion-ignore-case t)
3636 (files (mapcar 'file-name-sans-extension
3637 (directory-files todos-files-directory nil
3638 (if archive "\.toda$" "\.todo$"))))
3639 (file (concat todos-files-directory
3640 (completing-read prompt files nil mustmatch)
3641 (if archive ".toda" ".todo"))))
3642 (file-truename file)))
3643
3644(defun todos-read-category (prompt &optional mustmatch)
3645 "Choose and return a category name, prompting with PROMPT.
3646Show completions with TAB or SPC. With non-nil MUSTMATCH the
3647name must be that of an existing category; otherwise, a new
3648category name is allowed, after checking its validity."
3649 ;; Allow SPC to insert spaces, for adding new category names.
3650 (let ((map minibuffer-local-completion-map))
3651 (define-key map " " nil)
3652 ;; Make a copy of todos-categories in case history-delete-duplicates is
3653 ;; non-nil, which makes completing-read alter todos-categories.
3654 (let* ((categories (copy-sequence todos-categories))
3655 (history (cons 'todos-categories (1+ todos-category-number)))
3656 (completion-ignore-case todos-completion-ignore-case)
3657 (category (completing-read prompt todos-categories nil
3658 mustmatch nil history
3659 (if todos-categories
3660 (todos-current-category)
3661 ;; Trigger prompt for initial category
3662 ""))))
3663 ;; FIXME: let "" return todos-current-category
3664 (unless mustmatch
3665 (when (and (not (assoc category categories))
3666 (y-or-n-p (format (concat "There is no category \"%s\" in "
3667 "this file; add it? ") category)))
3668 (todos-validate-category-name category)
3669 (todos-add-category category)))
3670 ;; Restore the original value of todos-categories.
3671 (setq todos-categories categories)
3672 category)))
3673
3674(defun todos-validate-category-name (cat)
3675 "Check new category name CAT and when valid return it."
3676 (let (prompt)
3677 (while
3678 (and (cond ((string= "" cat)
3679 ;; (if todos-categories
3680 ;; (setq prompt "Enter a non-empty category name: ")
3681 ;; Prompt for initial category of a new Todos file.
3682 (setq prompt (concat "Initial category name ["
3683 todos-initial-category "]: ")));)
3684 ((string-match "\\`\\s-+\\'" cat)
3685 (setq prompt
3686 "Enter a category name that is not only white space: "))
3687 ;; FIXME: add completion
3688 ((assoc cat todos-categories)
3689 (setq prompt "Enter a non-existing category name: ")))
3690 (setq cat (if todos-categories
3691 (read-from-minibuffer prompt)
3692 ;; Offer default initial category name.
3693 (prin1-to-string
3694 (read-from-minibuffer prompt nil nil t nil
3695 (list todos-initial-category))))))))
3696 cat)
3697
3698;; (defun todos-read-category (prompt)
3699;; "Prompt with PROMPT for an existing category name and return it.
3700;; Show completions with TAB or SPC."
3701;; ;; Make a copy of todos-categories in case history-delete-duplicates is
3702;; ;; non-nil, which makes completing-read alter todos-categories.
3703;; (let* ((categories (copy-sequence todos-categories))
3704;; (history (cons 'todos-categories (1+ todos-category-number)))
3705;; (completion-ignore-case todos-completion-ignore-case)
3706;; (category (completing-read prompt todos-categories nil
3707;; mustmatch nil history)))
3708;; (setq category (completing-read prompt todos-categories nil t))
3709;; ;; Restore the original value of todos-categories.
3710;; (setq todos-categories categories)
3711;; category))
3712
3713;; (defun todos-new-category-name (prompt)
3714;; "Prompt with PROMPT for a new category name and return it."
3715;; (let ((map minibuffer-local-completion-map)
3716;; prompt-n)
3717;; ;; Allow SPC to insert spaces, for adding new category names.
3718;; (define-key map " " nil)
3719;; (while
3720;; ;; Validate entered category name.
3721;; (and (cond ((string= "" cat)
3722;; (setq prompt-n
3723;; (if todos-categories
3724;; "Enter a non-empty category name: "
3725;; ;; Prompt for initial category of a new Todos file.
3726;; (concat "Initial category name ["
3727;; todos-initial-category "]: "))))
3728;; ((string-match "\\`\\s-+\\'" cat)
3729;; (setq prompt-n
3730;; "Enter a category name that is not only white space: "))
3731;; ((assoc cat todos-categories)
3732;; (setq prompt-n "Enter a non-existing category name: ")))
3733;; (setq cat (if todos-categories
3734;; (read-from-minibuffer prompt)
3735;; ;; Offer default initial category name.
3736;; (prin1-to-string
3737;; (read-from-minibuffer
3738;; (or prompt prompt-n) nil nil t nil
3739;; (list todos-initial-category))))))
3740;; (setq prompt nil)))
3741;; cat)
3742
3743;; ;; Adapted from calendar-read-date and calendar-date-string.
3744(defun todos-read-date ()
3745 "Prompt for Gregorian date and return it in the current format.
3746Also accepts `*' as an unspecified month, day, or year."
3747 (let* ((year (calendar-read
3748 ;; FIXME: maybe better like monthname with RET for current month
3749 "Year (>0 or * for any year): "
3750 (lambda (x) (or (eq x '*) (> x 0)))
3751 (number-to-string (calendar-extract-year
3752 (calendar-current-date)))))
3753 (month-array (vconcat calendar-month-name-array (vector "*")))
3754 (abbrevs (vconcat calendar-month-abbrev-array (vector "*")))
3755 (completion-ignore-case t)
3756 (monthname (completing-read
3757 "Month name (RET for current month, * for any month): "
3758 (mapcar 'list (append month-array nil))
3759 nil t nil nil
3760 (calendar-month-name (calendar-extract-month
3761 (calendar-current-date)) t)))
3762 (month (cdr (assoc-string
3763 monthname (calendar-make-alist month-array nil nil
3764 abbrevs))))
3765 (last (if (= month 13)
3766 31 ; FIXME: what about shorter months?
3767 (let ((yr (if (eq year '*)
3768 1999 ; FIXME: no Feb. 29
3769 year)))
3770 (calendar-last-day-of-month month yr))))
3771 day dayname)
3772 (while (if (numberp day) (or (< day 0) (< last day)) (not (eq day '*)))
3773 (setq day (read-from-minibuffer
3774 (format "Day (1-%d or RET for today or * for any day): " last)
3775 nil nil t nil
3776 (number-to-string
3777 (calendar-extract-day (calendar-current-date))))))
3778 (setq year (if (eq year '*) (symbol-name '*) (number-to-string year)))
3779 (setq day (if (eq day '*) (symbol-name '*) (number-to-string day)))
3780 ;; FIXME: make abbreviation customizable
3781 (setq monthname
3782 (or (and (= month 13) "*")
3783 (calendar-month-name (calendar-extract-month (list month day year))
3784 t)))
3785 (mapconcat 'eval calendar-date-display-form "")))
3786
3787(defun todos-read-dayname ()
3788 "Choose name of a day of the week with completion and return it."
3789 (let ((completion-ignore-case t))
3790 (completing-read "Enter a day name: "
3791 (append calendar-day-name-array nil)
3792 nil t)))
3793
3794(defun todos-read-time ()
3795 "Prompt for and return a valid clock time as a string.
3796Valid time strings are those matching `diary-time-regexp'."
3797 (let (valid answer)
3798 (while (not valid)
3799 (setq answer (read-from-minibuffer
3800 "Enter a clock time (or return for none): "))
3801 (when (or (string= "" answer)
3802 (string-match diary-time-regexp answer))
3803 (setq valid t)))
3804 answer))
3805
3806;;; Sorting and display routines for todos-categories-mode.
3807
3808(defun todos-display-categories (&optional sortkey)
3809 "Display a table of the current file's categories and item counts.
3810
3811In the initial display the categories are numbered, indicating
3812their current order for navigating by \\[todos-forward-category]
3813and \\[todos-backward-category]. You can persistantly change the
3814order of the category at point by typing \\[todos-raise-category]
3815or \\[todos-lower-category].
3816
3817The labels above the category names and item counts are buttons,
3818and clicking these changes the display: sorted by category name
3819or by the respective item counts (alternately descending or
3820ascending). In these displays the categories are not numbered
3821and \\[todos-raise-category] and \\[todos-lower-category] are
3822disabled. (Programmatically, the sorting is triggered by passing
3823a non-nil SORTKEY argument.)
3824
3825In addition, the lines with the category names and item counts
3826are buttonized, and pressing one of these button jumps to the
3827category in Todos mode (or Todos Archive mode, for categories
3828containing only archived items, provided user option
3829`todos-ignore-archived-categories' is non-nil. These categories
3830are shown in `todos-archived-only' face."
3831 (interactive)
3832 (unless (eq major-mode 'todos-categories-mode)
3833 (setq todos-global-current-todos-file (or todos-current-todos-file
3834 todos-default-todos-file)))
3835 (let* ((cats0 (if (and todos-ignore-archived-categories
3836 (not (eq major-mode 'todos-categories-mode)))
3837 todos-categories-full
3838 todos-categories))
3839 (cats (todos-sort cats0 sortkey))
3840 (archive (member todos-current-todos-file todos-archives))
3841 ;; `num' is used by todos-insert-category-line.
3842 (num 0))
3843 (set-window-buffer (selected-window)
3844 (set-buffer (get-buffer-create todos-categories-buffer)))
3845 (let (buffer-read-only)
3846 (erase-buffer)
3847 (kill-all-local-variables)
3848 (todos-categories-mode)
3849 ;; FIXME: add usage tips?
3850 (insert (format "Category counts for Todos file \"%s\"."
3851 (file-name-sans-extension
3852 (file-name-nondirectory todos-current-todos-file))))
3853 (newline 2)
3854 ;; Make space for the column of category numbers.
3855 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32))
3856 ;; Add the category and item count buttons (if this is the list of
3857 ;; categories in an archive, show only done item counts).
3858 (save-excursion
3859 (todos-insert-sort-button todos-categories-category-label)
3860 (if (member todos-current-todos-file todos-archives)
3861 (insert (concat (make-string 6 32)
3862 (format "%s" todos-categories-archived-label)))
3863 (insert (make-string 3 32))
3864 (todos-insert-sort-button todos-categories-todo-label)
3865 (insert (make-string 2 32))
3866 (todos-insert-sort-button todos-categories-diary-label)
3867 (insert (make-string 2 32))
3868 (todos-insert-sort-button todos-categories-done-label)
3869 (insert (make-string 2 32))
3870 (todos-insert-sort-button todos-categories-archived-label))
3871 (newline 2)
3872 ;; Fill in the table with buttonized lines, each showing a category and
3873 ;; its item counts.
3874 (mapc (lambda (cat) (todos-insert-category-line cat sortkey))
3875 (mapcar 'car cats))
3876 (newline)
3877 ;; Add a line showing item count totals.
3878 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32)
3879 (todos-padded-string todos-categories-totals-label)
3880 (mapconcat
3881 (lambda (elt)
3882 (concat
3883 (make-string (1+ (/ (length (car elt)) 2)) 32)
3884 (format "%3d" (nth (cdr elt) (todos-total-item-counts)))
3885 ;; Add an extra space if label length is odd (using
3886 ;; definition of oddp from cl.el).
3887 (if (eq (logand (length (car elt)) 1) 1) " ")))
3888 (if archive
3889 (list (cons todos-categories-done-label 2))
3890 (list (cons todos-categories-todo-label 0)
3891 (cons todos-categories-diary-label 1)
3892 (cons todos-categories-done-label 2)
3893 (cons todos-categories-archived-label 3)))
3894 ""))))
3895 (setq buffer-read-only t)))
3896
3897;; ;; FIXME: make this toggle with todos-display-categories
3898;; (defun todos-display-categories-alphabetically ()
3899;; ""
3900;; (interactive)
3901;; (todos-display-sorted 'alpha))
3902
3903;; ;; FIXME: provide key bindings for these or delete them
3904;; (defun todos-display-categories-sorted-by-todo ()
3905;; ""
3906;; (interactive)
3907;; (todos-display-sorted 'todo))
3908
3909;; (defun todos-display-categories-sorted-by-diary ()
3910;; ""
3911;; (interactive)
3912;; (todos-display-sorted 'diary))
3913
3914;; (defun todos-display-categories-sorted-by-done ()
3915;; ""
3916;; (interactive)
3917;; (todos-display-sorted 'done))
3918
3919;; (defun todos-display-categories-sorted-by-archived ()
3920;; ""
3921;; (interactive)
3922;; (todos-display-sorted 'archived))
3923
3924(defun todos-longest-category-name-length (categories)
3925 "Return the length of the longest name in list CATEGORIES."
3926 (let ((longest 0))
3927 (dolist (c categories longest)
3928 (setq longest (max longest (length c))))))
3929
3930(defun todos-padded-string (str)
3931 "Return string STR padded with spaces.
3932The placement of the padding is determined by the value of user
3933option `todos-categories-align'."
3934 (let* ((categories (mapcar 'car todos-categories))
3935 (len (max (todos-longest-category-name-length categories)
3936 (length todos-categories-category-label)))
3937 (strlen (length str))
3938 (strlen-odd (eq (logand strlen 1) 1)) ; oddp from cl.el
3939 (padding (max 0 (/ (- len strlen) 2)))
3940 (padding-left (cond ((eq todos-categories-align 'left) 0)
3941 ((eq todos-categories-align 'center) padding)
3942 ((eq todos-categories-align 'right)
3943 (if strlen-odd (1+ (* padding 2)) (* padding 2)))))
3944 (padding-right (cond ((eq todos-categories-align 'left)
3945 (if strlen-odd (1+ (* padding 2)) (* padding 2)))
3946 ((eq todos-categories-align 'center)
3947 (if strlen-odd (1+ padding) padding))
3948 ((eq todos-categories-align 'right) 0))))
3949 (concat (make-string padding-left 32) str (make-string padding-right 32))))
3950
3951(defvar todos-descending-counts nil
3952 "List of keys for category counts sorted in descending order.")
3953
3954(defun todos-sort (list &optional key)
3955 "Return a copy of LIST, possibly sorted according to KEY."
3956 (let* ((l (copy-sequence list))
3957 (fn (if (eq key 'alpha)
3958 (lambda (x) (upcase x)) ; Alphabetize case insensitively.
3959 (lambda (x) (todos-get-count key x))))
3960 (descending (member key todos-descending-counts))
3961 (cmp (if (eq key 'alpha)
3962 'string<
3963 (if descending '< '>)))
3964 (pred (lambda (s1 s2) (let ((t1 (funcall fn (car s1)))
3965 (t2 (funcall fn (car s2))))
3966 (funcall cmp t1 t2)))))
3967 (when key
3968 (setq l (sort l pred))
3969 (if descending
3970 (setq todos-descending-counts
3971 (delete key todos-descending-counts))
3972 (push key todos-descending-counts)))
3973 l))
3974
3975(defun todos-display-sorted (type)
3976 "Keep point on the TYPE count sorting button just clicked."
3977 (let ((opoint (point)))
3978 (todos-display-categories type)
3979 (goto-char opoint)))
3980
3981(defun todos-label-to-key (label)
3982 "Return symbol for sort key associated with LABEL."
3983 (let (key)
3984 (cond ((string= label todos-categories-category-label)
3985 (setq key 'alpha))
3986 ((string= label todos-categories-todo-label)
3987 (setq key 'todo))
3988 ((string= label todos-categories-diary-label)
3989 (setq key 'diary))
3990 ((string= label todos-categories-done-label)
3991 (setq key 'done))
3992 ((string= label todos-categories-archived-label)
3993 (setq key 'archived)))
3994 key))
3995
3996(defun todos-insert-sort-button (label)
3997 "Insert button for displaying categories sorted by item counts.
3998LABEL determines which type of count is sorted."
3999 (setq str (if (string= label todos-categories-category-label)
4000 (todos-padded-string label)
4001 label))
4002 (setq beg (point))
4003 (setq end (+ beg (length str)))
4004 (insert-button str 'face nil
4005 'action
4006 `(lambda (button)
4007 (let ((key (todos-label-to-key ,label)))
4008 (if (and (member key todos-descending-counts)
4009 (eq key 'alpha))
4010 (progn
4011 (todos-display-categories)
4012 (setq todos-descending-counts
4013 (delete key todos-descending-counts)))
4014 (todos-display-sorted key)))))
4015 (setq ovl (make-overlay beg end))
4016 (overlay-put ovl 'face 'todos-button))
4017
4018(defun todos-total-item-counts ()
4019 "Return a list of total item counts for the current file."
4020 (mapcar (lambda (i) (apply '+ (mapcar (lambda (l) (aref l i))
4021 (mapcar 'cdr todos-categories))))
4022 (list 0 1 2 3)))
4023
4024(defun todos-insert-category-line (cat &optional nonum)
4025 "Insert button displaying category CAT's name and item counts.
4026With non-nil argument NONUM show only these; otherwise, insert a
4027number in front of the button indicating the category's priority.
4028The number and the category name are separated by the string
4029which is the value of the user option
4030`todos-categories-number-separator'."
4031 (let* ((archive (member todos-current-todos-file todos-archives))
4032 (str (todos-padded-string cat))
4033 (opoint (point)))
4034 ;; num is declared in caller.
4035 (setq num (1+ num))
4036 (insert-button
4037 (concat (if nonum
4038 (make-string (+ 4 (length todos-categories-number-separator))
4039 32)
4040 (format " %3d%s" num todos-categories-number-separator))
4041 str
4042 (mapconcat (lambda (elt)
4043 (concat
4044 (make-string (1+ (/ (length (car elt)) 2)) 32) ; label
4045 (format "%3d" (todos-get-count (cdr elt) cat)) ; count
4046 ;; Add an extra space if label length is odd
4047 ;; (using def of oddp from cl.el).
4048 (if (eq (logand (length (car elt)) 1) 1) " ")))
4049 (if archive
4050 (list (cons todos-categories-done-label 'done))
4051 (list (cons todos-categories-todo-label 'todo)
4052 (cons todos-categories-diary-label 'diary)
4053 (cons todos-categories-done-label 'done)
4054 (cons todos-categories-archived-label
4055 'archived)))
4056 ""))
4057 'face (if (and todos-ignore-archived-categories
4058 (zerop (todos-get-count 'todo cat))
4059 (zerop (todos-get-count 'done cat))
4060 (not (zerop (todos-get-count 'archived cat))))
4061 'todos-archived-only
4062 nil)
4063 'action `(lambda (button) (let ((buf (current-buffer)))
4064 (todos-jump-to-category ,cat)
4065 (kill-buffer buf))))
4066 ;; Highlight the sorted count column.
4067 (let* ((beg (+ opoint 6 (length str)))
4068 end ovl)
4069 (cond ((eq nonum 'todo)
4070 (setq beg (+ beg 1 (/ (length todos-categories-todo-label) 2))))
4071 ((eq nonum 'diary)
4072 (setq beg (+ beg 1 (length todos-categories-todo-label)
4073 2 (/ (length todos-categories-diary-label) 2))))
4074 ((eq nonum 'done)
4075 (setq beg (+ beg 1 (length todos-categories-todo-label)
4076 2 (length todos-categories-diary-label)
4077 2 (/ (length todos-categories-done-label) 2))))
4078 ((eq nonum 'archived)
4079 (setq beg (+ beg 1 (length todos-categories-todo-label)
4080 2 (length todos-categories-diary-label)
4081 2 (length todos-categories-done-label)
4082 2 (/ (length todos-categories-archived-label) 2)))))
4083 (unless (= beg (+ opoint 6 (length str)))
4084 (setq end (+ beg 4))
4085 (setq ovl (make-overlay beg end))
4086 (overlay-put ovl 'face 'todos-sorted-column)))
4087 (newline)))
4088
4089(provide 'todos) 4827(provide 'todos)
4090 4828
4091;;; todos.el ends here 4829;;; todos.el ends here
4092 4830
4831;; ---------------------------------------------------------------------------
4093;;; necessitated adaptations to diary-lib.el 4832;;; necessitated adaptations to diary-lib.el
4094 4833
4095;; (defun diary-goto-entry (button) 4834;; (defun diary-goto-entry (button)