aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Berman2013-06-08 00:55:42 +0200
committerStephen Berman2013-06-08 00:55:42 +0200
commit8b27b080c11a6980d4e639ae5fae2fbd8fc766c1 (patch)
tree3689692d3aeed5e4089016ee34ca0416a0cd77e7
parent1d59b7236c9c023671717bcf77ddcc0f70f0a01e (diff)
downloademacs-8b27b080c11a6980d4e639ae5fae2fbd8fc766c1.tar.gz
emacs-8b27b080c11a6980d4e639ae5fae2fbd8fc766c1.zip
* todos.el: Reorganize file structure again, to pacify byte-compiler.
-rw-r--r--lisp/calendar/ChangeLog11
-rw-r--r--lisp/calendar/todos.el3155
2 files changed, 1562 insertions, 1604 deletions
diff --git a/lisp/calendar/ChangeLog b/lisp/calendar/ChangeLog
index 829b60c5637..86f268816cb 100644
--- a/lisp/calendar/ChangeLog
+++ b/lisp/calendar/ChangeLog
@@ -1,3 +1,14 @@
12013-06-07 Stephen Berman <stephen.berman@gmx.net>
2
3 * todos.el: Reorganize file structure again, to pacify byte-compiler.
4
52013-06-06 Stephen Berman <stephen.berman@gmx.net>
6
7 * todos.el: Fix more byte-compiler warnings.
8 (todos-jump-to-category): Let-bind variable that was mistakenly free.
9 (todos-toggle-item-highlighting, todos-convert-legacy-files):
10 Use eval-when-compile.
11
12013-06-05 Stephen Berman <stephen.berman@gmx.net> 122013-06-05 Stephen Berman <stephen.berman@gmx.net>
2 13
3 * todos.el: Fix byte-compiler warnings. 14 * todos.el: Fix byte-compiler warnings.
diff --git a/lisp/calendar/todos.el b/lisp/calendar/todos.el
index a345fdcfd08..f2c31a33aea 100644
--- a/lisp/calendar/todos.el
+++ b/lisp/calendar/todos.el
@@ -22,47 +22,44 @@
22 22
23;;; Commentary: 23;;; Commentary:
24 24
25;; This package provides facilities for making, displaying, navigating 25;; This package provides facilities for making, displaying, navigating and
26;; and editing todo lists, which are prioritized lists of todo items. 26;; editing todo lists, which are prioritized lists of todo items. Todo lists
27;; Todo lists are identified with named categories, so you can group 27;; are identified with named categories, so you can group together and
28;; together thematically related todo items. Each category is stored 28;; separately prioritize thematically related todo items. Each category is
29;; in a file, which thus provides a further level of organization. 29;; stored in a file, which thus provides a further level of organization. You
30;; You can create as many todo files, and in each as many categories, 30;; can create as many todo files, and in each as many categories, as you want.
31;; as you want. 31
32 32;; With Todos you can navigate among the items of a category, and between
33;; With Todos you can navigate among the items of a category, and 33;; categories in the same and in different todo files. You can edit todo
34;; between categories in the same and in different todo files. You 34;; items, reprioritize them within their category, move them to another
35;; can edit todo items, reprioritize them within their category, move 35;; category, delete them, or mark items as done and store them separately from
36;; them to another category, delete them, or mark items as done and 36;; the not yet done items in a category. You can add new todo files and
37;; store them separately from the not yet done items in a category. 37;; categories, rename categories, move them to another file or delete them.
38;; You can add new todo files and categories, rename categories, move 38;; You can also display summary tables of the categories in a file and the
39;; them to another file or delete them. You can also display summary 39;; types of items they contain. And you can build cross-categorial lists of
40;; tables of the categories in a file and the types of items they 40;; items that satisfy various criteria.
41;; contain. And you can build cross-categorial lists of items that 41
42;; satisfy various criteria. 42;; To get started, load this package and type `M-x todos-show'. This will
43 43;; prompt you for the name of the first todo file, its first category and the
44;; To get started, load this package and type `M-x todos-show'. This 44;; category's first item, create these and display them in Todos mode. Now
45;; will prompt you for the name of the first todo file, its first 45;; you can insert further items into the list (i.e., the category) and assign
46;; category and the category's first item, create these and display 46;; them priorities by typing `i i'.
47;; them in Todos mode. Now you can insert further items into the list 47
48;; (i.e., the category) and assign them priorities by typing `i i'. 48;; You will probably find it convenient to give `todos-show' a global key
49 49;; binding in your init file, since it is one of the entry points to Todos
50;; You will probably find it convenient to give `todos-show' a global 50;; mode; a good choice is `C-c t', since `todos-show' is bound to `t' in Todos
51;; key binding in your init file, since it is one of the entry points 51;; mode.
52;; to Todos mode; a good choice is `C-c t', since `todos-show' is 52
53;; bound to `t' in Todos mode. 53;; To see a list of all Todos mode commands and their key bindings, including
54 54;; other entry points, type `C-h m' in Todos mode. Consult the document
55;; To see a list of all Todos mode commands and their key bindings, 55;; strings of the commands for details of their use. The `todos'
56;; including other entry points, type `C-h m' in Todos mode. Consult 56;; customization group and its subgroups list the options you can set to alter
57;; the document strings of the commands for details of their use. The 57;; the behavior of many commands and various aspects of the display.
58;; `todos' customization group and its subgroups list the options you 58
59;; can set to alter the behavior of many commands and various aspects 59;; This package is a new version of Oliver Seidel's todo-mode.el, which
60;; of the display. 60;; retains the same basic organization and handling of todo lists and the
61 61;; basic UI, but extends these in many ways and reimplements most of the
62;; This package is a new version of Oliver Seidel's todo-mode.el, 62;; internals.
63;; which retains the same basic organization and handling of todo
64;; lists and the basic UI, but extends these in many ways and
65;; reimplements most of the internals.
66 63
67;;; Code: 64;;; Code:
68 65
@@ -70,11 +67,8 @@
70;; For cl-remove-duplicates (in todos-insertion-commands-args) and cl-oddp. 67;; For cl-remove-duplicates (in todos-insertion-commands-args) and cl-oddp.
71(require 'cl-lib) 68(require 'cl-lib)
72 69
73;; =============================================================================
74;;; User interface
75;; =============================================================================
76;; ----------------------------------------------------------------------------- 70;; -----------------------------------------------------------------------------
77;;; Options for file and category selection 71;;; Setting up Todos files, categories, and items
78;; ----------------------------------------------------------------------------- 72;; -----------------------------------------------------------------------------
79 73
80(defcustom todos-directory (locate-user-emacs-file "todos/") 74(defcustom todos-directory (locate-user-emacs-file "todos/")
@@ -103,11 +97,459 @@ makes it return the value of the variable `todos-archives'."
103 :type 'function 97 :type 'function
104 :group 'todos) 98 :group 'todos)
105 99
106(defun todos-short-file-name (file) 100(defvar todos-files (funcall todos-files-function)
107 "Return short form of Todos FILE. 101 "List of truenames of user's Todos files.")
108This lacks the extension and directory components." 102
109 (when (stringp file) 103(defvar todos-archives (funcall todos-files-function t)
110 (file-name-sans-extension (file-name-nondirectory file)))) 104 "List of truenames of user's Todos archives.")
105
106(defvar todos-visited nil
107 "List of Todos files visited in this session by `todos-show'.
108Used to determine initial display according to the value of
109`todos-show-first'.")
110
111(defvar todos-file-buffers nil
112 "List of file names of live Todos mode buffers.")
113
114(defvar todos-global-current-todos-file nil
115 "Variable holding name of current Todos file.
116Used by functions called from outside of Todos mode to visit the
117current Todos file rather than the default Todos file (i.e. when
118users option `todos-show-current-file' is non-nil).")
119
120(defvar todos-current-todos-file nil
121 "Variable holding the name of the currently active Todos file.")
122
123(defvar todos-categories nil
124 "Alist of categories in the current Todos file.
125The elements are cons cells whose car is a category name and
126whose cdr is a vector of the category's item counts. These are,
127in order, the numbers of todo items, of todo items included in
128the Diary, of done items and of archived items.")
129
130(defvar todos-category-number 1
131 "Variable holding the number of the current Todos category.
132Todos categories are numbered starting from 1.")
133
134(defvar todos-categories-with-marks nil
135 "Alist of categories and number of marked items they contain.")
136
137(defconst todos-category-beg "--==-- "
138 "String marking beginning of category (inserted with its name).")
139
140(defconst todos-category-done "==--== DONE "
141 "String marking beginning of category's done items.")
142
143(defcustom todos-done-separator-string "="
144 "String determining the value of variable `todos-done-separator'.
145If the string consists of a single character,
146`todos-done-separator' will be the string made by repeating this
147character for the width of the window, and the length is
148automatically recalculated when the window width changes. If the
149string consists of more (or less) than one character, it will be
150the value of `todos-done-separator'."
151 :type 'string
152 :initialize 'custom-initialize-default
153 :set 'todos-reset-done-separator-string
154 :group 'todos-display)
155
156(defun todos-done-separator ()
157 "Return string used as value of variable `todos-done-separator'."
158 (let ((sep todos-done-separator-string))
159 (propertize (if (= 1 (length sep))
160 ;; Until bug#2749 is fixed, if separator's length
161 ;; is window-width and todos-wrap-lines is
162 ;; non-nil, an indented empty line appears between
163 ;; the separator and the first done item.
164 ;; (make-string (window-width) (string-to-char sep))
165 (make-string (1- (window-width)) (string-to-char sep))
166 todos-done-separator-string)
167 'face 'todos-done-sep)))
168
169(defvar todos-done-separator (todos-done-separator)
170 "String used to visually separate done from not done items.
171Displayed as an overlay instead of `todos-category-done' when
172done items are shown. Its value is determined by user option
173`todos-done-separator-string'.")
174
175(defvar todos-show-done-only nil
176 "If non-nil display only done items in current category.
177Set by the command `todos-toggle-view-done-only' and used by
178`todos-category-select'.")
179
180(defcustom todos-nondiary-marker '("[" "]")
181 "List of strings surrounding item date to block diary inclusion.
182The first string is inserted before the item date and must be a
183non-empty string that does not match a diary date in order to
184have its intended effect. The second string is inserted after
185the diary date."
186 :type '(list string string)
187 :group 'todos-edit
188 :initialize 'custom-initialize-default
189 :set 'todos-reset-nondiary-marker)
190
191(defconst todos-nondiary-start (nth 0 todos-nondiary-marker)
192 "String inserted before item date to block diary inclusion.")
193
194(defconst todos-nondiary-end (nth 1 todos-nondiary-marker)
195 "String inserted after item date matching `todos-nondiary-start'.")
196
197(defconst todos-month-name-array
198 (vconcat calendar-month-name-array (vector "*"))
199 "Array of month names, in order.
200The final element is \"*\", indicating an unspecified month.")
201
202(defconst todos-month-abbrev-array
203 (vconcat calendar-month-abbrev-array (vector "*"))
204 "Array of abbreviated month names, in order.
205The final element is \"*\", indicating an unspecified month.")
206
207(defconst todos-date-pattern
208 (let ((dayname (diary-name-pattern calendar-day-name-array nil t)))
209 (concat "\\(?5:" dayname "\\|"
210 (let ((dayname)
211 (monthname (format "\\(?6:%s\\)" (diary-name-pattern
212 todos-month-name-array
213 todos-month-abbrev-array)))
214 (month "\\(?7:[0-9]+\\|\\*\\)")
215 (day "\\(?8:[0-9]+\\|\\*\\)")
216 (year "-?\\(?9:[0-9]+\\|\\*\\)"))
217 (mapconcat 'eval calendar-date-display-form ""))
218 "\\)"))
219 "Regular expression matching a Todos date header.")
220
221;; By itself this matches anything, because of the `?'; however, it's only
222;; used in the context of `todos-date-pattern' (but Emacs Lisp lacks
223;; lookahead).
224(defconst todos-date-string-start
225 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|"
226 (regexp-quote diary-nonmarking-symbol) "\\)?")
227 "Regular expression matching part of item header before the date.")
228
229(defcustom todos-done-string "DONE "
230 "Identifying string appended to the front of done todos items."
231 :type 'string
232 :initialize 'custom-initialize-default
233 :set 'todos-reset-done-string
234 :group 'todos-edit)
235
236(defconst todos-done-string-start
237 (concat "^\\[" (regexp-quote todos-done-string))
238 "Regular expression matching start of done item.")
239
240(defconst todos-item-start (concat "\\(" todos-date-string-start "\\|"
241 todos-done-string-start "\\)"
242 todos-date-pattern)
243 "String identifying start of a Todos item.")
244
245;; -----------------------------------------------------------------------------
246;;; Todos mode display options
247;; -----------------------------------------------------------------------------
248
249(defcustom todos-prefix ""
250 "String prefixed to todo items for visual distinction."
251 :type '(string :validate
252 (lambda (widget)
253 (when (string= (widget-value widget) todos-item-mark)
254 (widget-put
255 widget :error
256 "Invalid value: must be distinct from `todos-item-mark'")
257 widget)))
258 :initialize 'custom-initialize-default
259 :set 'todos-reset-prefix
260 :group 'todos-display)
261
262(defcustom todos-number-prefix t
263 "Non-nil to prefix items with consecutively increasing integers.
264These reflect the priorities of the items in each category."
265 :type 'boolean
266 :initialize 'custom-initialize-default
267 :set 'todos-reset-prefix
268 :group 'todos-display)
269
270(defun todos-mode-line-control (cat)
271 "Return a mode line control for todo or archive file buffers.
272Argument CAT is the name of the current Todos category.
273This function is the value of the user variable
274`todos-mode-line-function'."
275 (let ((file (todos-short-file-name todos-current-todos-file)))
276 (format "%s category %d: %s" file todos-category-number cat)))
277
278(defcustom todos-mode-line-function 'todos-mode-line-control
279 "Function that returns a mode line control for Todos buffers.
280The function expects one argument holding the name of the current
281Todos category. The resulting control becomes the local value of
282`mode-line-buffer-identification' in each Todos buffer."
283 :type 'function
284 :group 'todos-display)
285
286(defcustom todos-highlight-item nil
287 "Non-nil means highlight items at point."
288 :type 'boolean
289 :initialize 'custom-initialize-default
290 :set 'todos-reset-highlight-item
291 :group 'todos-display)
292
293(defcustom todos-wrap-lines t
294 "Non-nil to activate Visual Line mode and use wrap prefix."
295 :type 'boolean
296 :group 'todos-display)
297
298(defcustom todos-indent-to-here 3
299 "Number of spaces to indent continuation lines of items.
300This must be a positive number to ensure such items are fully
301shown in the Fancy Diary display."
302 :type '(integer :validate
303 (lambda (widget)
304 (unless (> (widget-value widget) 0)
305 (widget-put widget :error
306 "Invalid value: must be a positive integer")
307 widget)))
308 :group 'todos-display)
309
310(defun todos-indent ()
311 "Indent from point to `todos-indent-to-here'."
312 (indent-to todos-indent-to-here todos-indent-to-here))
313
314(defcustom todos-show-with-done nil
315 "Non-nil to display done items in all categories."
316 :type 'boolean
317 :group 'todos-display)
318
319;; -----------------------------------------------------------------------------
320;;; Faces
321;; -----------------------------------------------------------------------------
322
323(defface todos-mark
324 ;; '((t :inherit font-lock-warning-face))
325 '((((class color)
326 (min-colors 88)
327 (background light))
328 (:weight bold :foreground "Red1"))
329 (((class color)
330 (min-colors 88)
331 (background dark))
332 (:weight bold :foreground "Pink"))
333 (((class color)
334 (min-colors 16)
335 (background light))
336 (:weight bold :foreground "Red1"))
337 (((class color)
338 (min-colors 16)
339 (background dark))
340 (:weight bold :foreground "Pink"))
341 (((class color)
342 (min-colors 8))
343 (:foreground "red"))
344 (t
345 (:weight bold :inverse-video t)))
346 "Face for marks on marked items."
347 :group 'todos-faces)
348
349(defface todos-prefix-string
350 ;; '((t :inherit font-lock-constant-face))
351 '((((class grayscale) (background light))
352 (:foreground "LightGray" :weight bold :underline t))
353 (((class grayscale) (background dark))
354 (:foreground "Gray50" :weight bold :underline t))
355 (((class color) (min-colors 88) (background light)) (:foreground "dark cyan"))
356 (((class color) (min-colors 88) (background dark)) (:foreground "Aquamarine"))
357 (((class color) (min-colors 16) (background light)) (:foreground "CadetBlue"))
358 (((class color) (min-colors 16) (background dark)) (:foreground "Aquamarine"))
359 (((class color) (min-colors 8)) (:foreground "magenta"))
360 (t (:weight bold :underline t)))
361 "Face for Todos prefix or numerical priority string."
362 :group 'todos-faces)
363
364(defface todos-top-priority
365 ;; bold font-lock-comment-face
366 '((default :weight bold)
367 (((class grayscale) (background light)) :foreground "DimGray" :slant italic)
368 (((class grayscale) (background dark)) :foreground "LightGray" :slant italic)
369 (((class color) (min-colors 88) (background light)) :foreground "Firebrick")
370 (((class color) (min-colors 88) (background dark)) :foreground "chocolate1")
371 (((class color) (min-colors 16) (background light)) :foreground "red")
372 (((class color) (min-colors 16) (background dark)) :foreground "red1")
373 (((class color) (min-colors 8) (background light)) :foreground "red")
374 (((class color) (min-colors 8) (background dark)) :foreground "yellow")
375 (t :slant italic))
376 "Face for top priority Todos item numerical priority string.
377The item's priority number string has this face if the number is
378less than or equal the category's top priority setting."
379 :group 'todos-faces)
380
381(defface todos-nondiary
382 ;; '((t :inherit font-lock-type-face))
383 '((((class grayscale) (background light)) :foreground "Gray90" :weight bold)
384 (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
385 (((class color) (min-colors 88) (background light)) :foreground "ForestGreen")
386 (((class color) (min-colors 88) (background dark)) :foreground "PaleGreen")
387 (((class color) (min-colors 16) (background light)) :foreground "ForestGreen")
388 (((class color) (min-colors 16) (background dark)) :foreground "PaleGreen")
389 (((class color) (min-colors 8)) :foreground "green")
390 (t :weight bold :underline t))
391 "Face for non-diary markers around todo item date/time header."
392 :group 'todos-faces)
393
394(defface todos-date
395 '((t :inherit diary))
396 "Face for the date string of a Todos item."
397 :group 'todos-faces)
398
399(defface todos-time
400 '((t :inherit diary-time))
401 "Face for the time string of a Todos item."
402 :group 'todos-faces)
403
404(defface todos-diary-expired
405 ;; Doesn't contrast enough with todos-date (= diary) face.
406 ;; ;; '((t :inherit warning))
407 ;; '((default :weight bold)
408 ;; (((class color) (min-colors 16)) :foreground "DarkOrange")
409 ;; (((class color)) :foreground "yellow"))
410 ;; bold font-lock-function-name-face
411 '((default :weight bold)
412 (((class color) (min-colors 88) (background light)) :foreground "Blue1")
413 (((class color) (min-colors 88) (background dark)) :foreground "LightSkyBlue")
414 (((class color) (min-colors 16) (background light)) :foreground "Blue")
415 (((class color) (min-colors 16) (background dark)) :foreground "LightSkyBlue")
416 (((class color) (min-colors 8)) :foreground "blue")
417 (t :inverse-video t))
418 "Face for expired dates of diary items."
419 :group 'todos-faces)
420
421(defface todos-done-sep
422 ;; '((t :inherit font-lock-builtin-face))
423 '((((class grayscale) (background light)) :foreground "LightGray" :weight bold)
424 (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
425 (((class color) (min-colors 88) (background light)) :foreground "dark slate blue")
426 (((class color) (min-colors 88) (background dark)) :foreground "LightSteelBlue")
427 (((class color) (min-colors 16) (background light)) :foreground "Orchid")
428 (((class color) (min-colors 16) (background dark)) :foreground "LightSteelBlue")
429 (((class color) (min-colors 8)) :foreground "blue" :weight bold)
430 (t :weight bold))
431 "Face for separator string bewteen done and not done Todos items."
432 :group 'todos-faces)
433
434(defface todos-done
435 ;; '((t :inherit font-lock-keyword-face))
436 '((((class grayscale) (background light)) :foreground "LightGray" :weight bold)
437 (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
438 (((class color) (min-colors 88) (background light)) :foreground "Purple")
439 (((class color) (min-colors 88) (background dark)) :foreground "Cyan1")
440 (((class color) (min-colors 16) (background light)) :foreground "Purple")
441 (((class color) (min-colors 16) (background dark)) :foreground "Cyan")
442 (((class color) (min-colors 8)) :foreground "cyan" :weight bold)
443 (t :weight bold))
444 "Face for done Todos item header string."
445 :group 'todos-faces)
446
447(defface todos-comment
448 ;; '((t :inherit font-lock-comment-face))
449 '((((class grayscale) (background light))
450 :foreground "DimGray" :weight bold :slant italic)
451 (((class grayscale) (background dark))
452 :foreground "LightGray" :weight bold :slant italic)
453 (((class color) (min-colors 88) (background light))
454 :foreground "Firebrick")
455 (((class color) (min-colors 88) (background dark))
456 :foreground "chocolate1")
457 (((class color) (min-colors 16) (background light))
458 :foreground "red")
459 (((class color) (min-colors 16) (background dark))
460 :foreground "red1")
461 (((class color) (min-colors 8) (background light))
462 :foreground "red")
463 (((class color) (min-colors 8) (background dark))
464 :foreground "yellow")
465 (t :weight bold :slant italic))
466 "Face for comments appended to done Todos items."
467 :group 'todos-faces)
468
469(defface todos-search
470 ;; '((t :inherit match))
471 '((((class color)
472 (min-colors 88)
473 (background light))
474 (:background "yellow1"))
475 (((class color)
476 (min-colors 88)
477 (background dark))
478 (:background "RoyalBlue3"))
479 (((class color)
480 (min-colors 8)
481 (background light))
482 (:foreground "black" :background "yellow"))
483 (((class color)
484 (min-colors 8)
485 (background dark))
486 (:foreground "white" :background "blue"))
487 (((type tty)
488 (class mono))
489 (:inverse-video t))
490 (t
491 (:background "gray")))
492 "Face for matches found by `todos-search'."
493 :group 'todos-faces)
494
495(defface todos-button
496 ;; '((t :inherit widget-field))
497 '((((type tty))
498 (:foreground "black" :background "yellow3"))
499 (((class grayscale color)
500 (background light))
501 (:background "gray85"))
502 (((class grayscale color)
503 (background dark))
504 (:background "dim gray"))
505 (t
506 (:slant italic)))
507 "Face for buttons in table of categories."
508 :group 'todos-faces)
509
510(defface todos-sorted-column
511 '((((type tty))
512 (:inverse-video t))
513 (((class color)
514 (background light))
515 (:background "grey85"))
516 (((class color)
517 (background dark))
518 (:background "grey85" :foreground "grey10"))
519 (t
520 (:background "gray")))
521 "Face for sorted column in table of categories."
522 :group 'todos-faces)
523
524(defface todos-archived-only
525 ;; '((t (:inherit (shadow))))
526 '((((class color)
527 (background light))
528 (:foreground "grey50"))
529 (((class color)
530 (background dark))
531 (:foreground "grey70"))
532 (t
533 (:foreground "gray")))
534 "Face for archived-only category names in table of categories."
535 :group 'todos-faces)
536
537(defface todos-category-string
538 ;; '((t :inherit font-lock-type-face))
539 '((((class grayscale) (background light)) :foreground "Gray90" :weight bold)
540 (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
541 (((class color) (min-colors 88) (background light)) :foreground "ForestGreen")
542 (((class color) (min-colors 88) (background dark)) :foreground "PaleGreen")
543 (((class color) (min-colors 16) (background light)) :foreground "ForestGreen")
544 (((class color) (min-colors 16) (background dark)) :foreground "PaleGreen")
545 (((class color) (min-colors 8)) :foreground "green")
546 (t :weight bold :underline t))
547 "Face for category-file header in Todos Filtered Items mode."
548 :group 'todos-faces)
549
550;; -----------------------------------------------------------------------------
551;;; Entering and exiting Todos
552;; -----------------------------------------------------------------------------
111 553
112(defcustom todos-visit-files-commands (list 'find-file 'dired-find-file) 554(defcustom todos-visit-files-commands (list 'find-file 'dired-find-file)
113 "List of file finding commands for `todos-display-as-todos-file'. 555 "List of file finding commands for `todos-display-as-todos-file'.
@@ -117,6 +559,12 @@ displayed correctly."
117 :type '(repeat function) 559 :type '(repeat function)
118 :group 'todos) 560 :group 'todos)
119 561
562(defun todos-short-file-name (file)
563 "Return short form of Todos FILE.
564This lacks the extension and directory components."
565 (when (stringp file)
566 (file-name-sans-extension (file-name-nondirectory file))))
567
120(defcustom todos-default-todos-file (todos-short-file-name 568(defcustom todos-default-todos-file (todos-short-file-name
121 (car (funcall todos-files-function))) 569 (car (funcall todos-files-function)))
122 "Todos file visited by first session invocation of `todos-show'." 570 "Todos file visited by first session invocation of `todos-show'."
@@ -142,6 +590,11 @@ Otherwise, `todos-show' always visits `todos-default-todos-file'."
142 (const :tag "Show regexp items" regexp)) 590 (const :tag "Show regexp items" regexp))
143 :group 'todos) 591 :group 'todos)
144 592
593(defcustom todos-add-item-if-new-category t
594 "Non-nil to prompt for an item after adding a new category."
595 :type 'boolean
596 :group 'todos-edit)
597
145(defcustom todos-initial-file "Todo" 598(defcustom todos-initial-file "Todo"
146 "Default file name offered on adding first Todos file." 599 "Default file name offered on adding first Todos file."
147 :type 'string 600 :type 'string
@@ -164,10 +617,6 @@ Otherwise, `todos-show' always visits `todos-default-todos-file'."
164 :type 'boolean 617 :type 'boolean
165 :group 'todos) 618 :group 'todos)
166 619
167;; -----------------------------------------------------------------------------
168;;; Entering and exiting Todos mode
169;; -----------------------------------------------------------------------------
170
171(defun todos-show (&optional solicit-file) 620(defun todos-show (&optional solicit-file)
172 "Visit a Todos file and display one of its categories. 621 "Visit a Todos file and display one of its categories.
173 622
@@ -312,6 +761,8 @@ corresponding Todos file, displaying the corresponding category."
312 (t 761 (t
313 (save-buffer)))) 762 (save-buffer))))
314 763
764(defvar todos-descending-counts)
765
315(defun todos-quit () 766(defun todos-quit ()
316 "Exit the current Todos-related buffer. 767 "Exit the current Todos-related buffer.
317Depending on the specific mode, this either kills the buffer or 768Depending on the specific mode, this either kills the buffer or
@@ -346,9 +797,22 @@ buries it and restores state as needed."
346 (bury-buffer buf))))) 797 (bury-buffer buf)))))
347 798
348;; ----------------------------------------------------------------------------- 799;; -----------------------------------------------------------------------------
349;;; Navigation commands 800;;; Navigation between and within categories
350;; ----------------------------------------------------------------------------- 801;; -----------------------------------------------------------------------------
351 802
803(defcustom todos-skip-archived-categories nil
804 "Non-nil to handle categories with only archived items specially.
805
806Sequential category navigation using \\[todos-forward-category]
807or \\[todos-backward-category] skips categories that contain only
808archived items. Other commands still recognize these categories.
809In Todos Categories mode (\\[todos-show-categories-table]) these
810categories shown in `todos-archived-only' face and pressing the
811category button visits the category in the archive instead of the
812todo file."
813 :type 'boolean
814 :group 'todos-display)
815
352(defun todos-forward-category (&optional back) 816(defun todos-forward-category (&optional back)
353 "Visit the numerically next category in this Todos file. 817 "Visit the numerically next category in this Todos file.
354If the current category is the highest numbered, visit the first 818If the current category is the highest numbered, visit the first
@@ -375,6 +839,8 @@ category."
375 (interactive) 839 (interactive)
376 (todos-forward-category t)) 840 (todos-forward-category t))
377 841
842(defvar todos-categories-buffer)
843
378(defun todos-jump-to-category (&optional file where) 844(defun todos-jump-to-category (&optional file where)
379 "Prompt for a category in a Todos file and jump to it. 845 "Prompt for a category in a Todos file and jump to it.
380 846
@@ -420,8 +886,8 @@ Categories mode."
420 (todos-read-category "Jump to category: " 886 (todos-read-category "Jump to category: "
421 (if archive 'archive) file))) 887 (if archive 'archive) file)))
422 (add-item (and todos-add-item-if-new-category 888 (add-item (and todos-add-item-if-new-category
423 (> (length todos-categories) len)))) 889 (> (length todos-categories) len)))
424 (setq category (or cat (car cat+file))) 890 (category (or cat (car cat+file))))
425 (unless cat (setq file0 (cdr cat+file))) 891 (unless cat (setq file0 (cdr cat+file)))
426 (with-current-buffer (find-file-noselect file0 'nowarn) 892 (with-current-buffer (find-file-noselect file0 'nowarn)
427 (setq todos-current-todos-file file0) 893 (setq todos-current-todos-file file0)
@@ -485,7 +951,87 @@ empty line above the done items separator."
485 (todos-backward-item))))) 951 (todos-backward-item)))))
486 952
487;; ----------------------------------------------------------------------------- 953;; -----------------------------------------------------------------------------
488;;; File editing commands 954;;; Display toggle commands
955;; -----------------------------------------------------------------------------
956
957(defun todos-toggle-prefix-numbers ()
958 "Hide item numbering if shown, show if hidden."
959 (interactive)
960 (save-excursion
961 (save-restriction
962 (goto-char (point-min))
963 (let* ((ov (todos-get-overlay 'prefix))
964 (show-done (re-search-forward todos-done-string-start nil t))
965 (todos-show-with-done show-done)
966 (todos-number-prefix (not (equal (overlay-get ov 'before-string)
967 "1 "))))
968 (if (eq major-mode 'todos-filtered-items-mode)
969 (todos-prefix-overlays)
970 (todos-category-select))))))
971
972(defun todos-toggle-view-done-items ()
973 "Show hidden or hide visible done items in current category."
974 (interactive)
975 (if (zerop (todos-get-count 'done (todos-current-category)))
976 (message "There are no done items in this category.")
977 (let ((opoint (point)))
978 (goto-char (point-min))
979 (let* ((shown (re-search-forward todos-done-string-start nil t))
980 (todos-show-with-done (not shown)))
981 (todos-category-select)
982 (goto-char opoint)
983 ;; If start of done items sections is below the bottom of the
984 ;; window, make it visible.
985 (unless shown
986 (setq shown (progn
987 (goto-char (point-min))
988 (re-search-forward todos-done-string-start nil t)))
989 (if (not (pos-visible-in-window-p shown))
990 (recenter)
991 (goto-char opoint)))))))
992
993(defun todos-toggle-view-done-only ()
994 "Switch between displaying only done or only todo items."
995 (interactive)
996 (setq todos-show-done-only (not todos-show-done-only))
997 (todos-category-select))
998
999(defun todos-toggle-item-highlighting ()
1000 "Highlight or unhighlight the todo item the cursor is on."
1001 (interactive)
1002 (eval-when-compile (require 'hl-line))
1003 (when (memq major-mode
1004 '(todos-mode todos-archive-mode todos-filtered-items-mode))
1005 (if hl-line-mode
1006 (hl-line-mode -1)
1007 (hl-line-mode 1))))
1008
1009(defun todos-toggle-item-header ()
1010 "Hide or show item date-time headers in the current file.
1011With done items, this hides only the done date-time string, not
1012the the original date-time string."
1013 (interactive)
1014 (save-excursion
1015 (save-restriction
1016 (goto-char (point-min))
1017 (let ((ov (todos-get-overlay 'header)))
1018 (if ov
1019 (remove-overlays 1 (1+ (buffer-size)) 'todos 'header)
1020 (widen)
1021 (goto-char (point-min))
1022 (while (not (eobp))
1023 (when (re-search-forward
1024 (concat todos-item-start
1025 "\\( " diary-time-regexp "\\)?"
1026 (regexp-quote todos-nondiary-end) "? ")
1027 nil t)
1028 (setq ov (make-overlay (match-beginning 0) (match-end 0) nil t))
1029 (overlay-put ov 'todos 'header)
1030 (overlay-put ov 'display ""))
1031 (todos-forward-item)))))))
1032
1033;; -----------------------------------------------------------------------------
1034;;; File and category editing
489;; ----------------------------------------------------------------------------- 1035;; -----------------------------------------------------------------------------
490 1036
491(defun todos-add-file () 1037(defun todos-add-file ()
@@ -493,10 +1039,7 @@ empty line above the done items separator."
493Interactively, prompt for a category and display it, and if 1039Interactively, prompt for a category and display it, and if
494option `todos-add-item-if-new-category' is non-nil (the default), 1040option `todos-add-item-if-new-category' is non-nil (the default),
495prompt for the first item. 1041prompt for the first item.
496Noninteractively, return the name of the new file. 1042Noninteractively, return the name of the new file."
497
498This command does not save the file to disk; to do that type
499\\[todos-save] or \\[todos-quit]."
500 (interactive) 1043 (interactive)
501 (let ((prompt (concat "Enter name of new Todos file " 1044 (let ((prompt (concat "Enter name of new Todos file "
502 "(TAB or SPC to see current names): ")) 1045 "(TAB or SPC to see current names): "))
@@ -537,10 +1080,6 @@ this command should be used with caution."
537 (concat "Type \\[todos-edit-quit] to check file format " 1080 (concat "Type \\[todos-edit-quit] to check file format "
538 "validity and return to Todos mode.\n")))) 1081 "validity and return to Todos mode.\n"))))
539 1082
540;; -----------------------------------------------------------------------------
541;;; Category editing commands
542;; -----------------------------------------------------------------------------
543
544(defun todos-add-category (&optional file cat) 1083(defun todos-add-category (&optional file cat)
545 "Add a new category to a Todos file. 1084 "Add a new category to a Todos file.
546 1085
@@ -930,9 +1469,35 @@ category."
930 (set-marker here nil))) 1469 (set-marker here nil)))
931 1470
932;; ----------------------------------------------------------------------------- 1471;; -----------------------------------------------------------------------------
933;;; Item marking 1472;;; Item editing
934;; ----------------------------------------------------------------------------- 1473;; -----------------------------------------------------------------------------
935 1474
1475(defcustom todos-include-in-diary nil
1476 "Non-nil to allow new Todo items to be included in the diary."
1477 :type 'boolean
1478 :group 'todos-edit)
1479
1480(defcustom todos-diary-nonmarking nil
1481 "Non-nil to insert new Todo diary items as nonmarking by default.
1482This appends `diary-nonmarking-symbol' to the front of an item on
1483insertion provided it doesn't begin with `todos-nondiary-marker'."
1484 :type 'boolean
1485 :group 'todos-edit)
1486
1487(defcustom todos-always-add-time-string nil
1488 "Non-nil adds current time to a new item's date header by default.
1489When the Todos insertion commands have a non-nil \"maybe-notime\"
1490argument, this reverses the effect of
1491`todos-always-add-time-string': if t, these commands omit the
1492current time, if nil, they include it."
1493 :type 'boolean
1494 :group 'todos-edit)
1495
1496(defcustom todos-use-only-highlighted-region t
1497 "Non-nil to enable inserting only highlighted region as new item."
1498 :type 'boolean
1499 :group 'todos-edit)
1500
936(defcustom todos-item-mark "*" 1501(defcustom todos-item-mark "*"
937 "String used to mark items. 1502 "String used to mark items.
938To ensure item marking works, change the value of this option 1503To ensure item marking works, change the value of this option
@@ -948,6 +1513,22 @@ only when no items are marked."
948 (custom-set-default symbol (propertize value 'face 'todos-mark))) 1513 (custom-set-default symbol (propertize value 'face 'todos-mark)))
949 :group 'todos-edit) 1514 :group 'todos-edit)
950 1515
1516(defcustom todos-comment-string "COMMENT"
1517 "String inserted before optional comment appended to done item."
1518 :type 'string
1519 :initialize 'custom-initialize-default
1520 :set 'todos-reset-comment-string
1521 :group 'todos-edit)
1522
1523(defcustom todos-undo-item-omit-comment 'ask
1524 "Whether to omit done item comment on undoing the item.
1525Nil means never omit the comment, t means always omit it, `ask'
1526means prompt user and omit comment only on confirmation."
1527 :type '(choice (const :tag "Never" nil)
1528 (const :tag "Always" t)
1529 (const :tag "Ask" ask))
1530 :group 'todos-edit)
1531
951(defun todos-toggle-mark-item (&optional n) 1532(defun todos-toggle-mark-item (&optional n)
952 "Mark item with `todos-item-mark' if unmarked, otherwise unmark it. 1533 "Mark item with `todos-item-mark' if unmarked, otherwise unmark it.
953With a positive numerical prefix argument N, change the 1534With a positive numerical prefix argument N, change the
@@ -1010,64 +1591,8 @@ marking of the next N items."
1010 (setq todos-categories-with-marks 1591 (setq todos-categories-with-marks
1011 (delq marks todos-categories-with-marks)))) 1592 (delq marks todos-categories-with-marks))))
1012 1593
1013;; ----------------------------------------------------------------------------- 1594(defvar todos-date-from-calendar nil
1014;;; Item editing options 1595 "Helper variable for setting item date from the Emacs Calendar.")
1015;; -----------------------------------------------------------------------------
1016
1017(defcustom todos-add-item-if-new-category t
1018 "Non-nil to prompt for an item after adding a new category."
1019 :type 'boolean
1020 :group 'todos-edit)
1021
1022(defcustom todos-include-in-diary nil
1023 "Non-nil to allow new Todo items to be included in the diary."
1024 :type 'boolean
1025 :group 'todos-edit)
1026
1027(defcustom todos-diary-nonmarking nil
1028 "Non-nil to insert new Todo diary items as nonmarking by default.
1029This appends `diary-nonmarking-symbol' to the front of an item on
1030insertion provided it doesn't begin with `todos-nondiary-marker'."
1031 :type 'boolean
1032 :group 'todos-edit)
1033
1034(defcustom todos-nondiary-marker '("[" "]")
1035 "List of strings surrounding item date to block diary inclusion.
1036The first string is inserted before the item date and must be a
1037non-empty string that does not match a diary date in order to
1038have its intended effect. The second string is inserted after
1039the diary date."
1040 :type '(list string string)
1041 :group 'todos-edit
1042 :initialize 'custom-initialize-default
1043 :set 'todos-reset-nondiary-marker)
1044
1045(defcustom todos-always-add-time-string nil
1046 "Non-nil adds current time to a new item's date header by default.
1047When the Todos insertion commands have a non-nil \"maybe-notime\"
1048argument, this reverses the effect of
1049`todos-always-add-time-string': if t, these commands omit the
1050current time, if nil, they include it."
1051 :type 'boolean
1052 :group 'todos-edit)
1053
1054(defcustom todos-use-only-highlighted-region t
1055 "Non-nil to enable inserting only highlighted region as new item."
1056 :type 'boolean
1057 :group 'todos-edit)
1058
1059(defcustom todos-undo-item-omit-comment 'ask
1060 "Whether to omit done item comment on undoing the item.
1061Nil means never omit the comment, t means always omit it, `ask'
1062means prompt user and omit comment only on confirmation."
1063 :type '(choice (const :tag "Never" nil)
1064 (const :tag "Always" t)
1065 (const :tag "Ask" ask))
1066 :group 'todos-edit)
1067
1068;; -----------------------------------------------------------------------------
1069;;; Item editing commands
1070;; -----------------------------------------------------------------------------
1071 1596
1072(defun todos-basic-insert-item (&optional arg diary nonmarking date-type time 1597(defun todos-basic-insert-item (&optional arg diary nonmarking date-type time
1073 region-or-here) 1598 region-or-here)
@@ -1310,9 +1835,6 @@ the new item:
1310 (if (or diary todos-include-in-diary) (todos-update-count 'diary 1)) 1835 (if (or diary todos-include-in-diary) (todos-update-count 'diary 1))
1311 (todos-update-categories-sexp)))))) 1836 (todos-update-categories-sexp))))))
1312 1837
1313(defvar todos-date-from-calendar nil
1314 "Helper variable for setting item date from the Emacs Calendar.")
1315
1316(defun todos-set-date-from-calendar () 1838(defun todos-set-date-from-calendar ()
1317 "Return string of date chosen from Calendar." 1839 "Return string of date chosen from Calendar."
1318 (cond ((and (stringp todos-date-from-calendar) 1840 (cond ((and (stringp todos-date-from-calendar)
@@ -1371,7 +1893,6 @@ prompt for a todo file and then for a category in it."
1371 1893
1372(defun todos-delete-item () 1894(defun todos-delete-item ()
1373 "Delete at least one item in this category. 1895 "Delete at least one item in this category.
1374
1375If there are marked items, delete all of these; otherwise, delete 1896If there are marked items, delete all of these; otherwise, delete
1376the item at point." 1897the item at point."
1377 (interactive) 1898 (interactive)
@@ -1418,7 +1939,6 @@ the item at point."
1418 1939
1419(defun todos-edit-item (&optional arg) 1940(defun todos-edit-item (&optional arg)
1420 "Edit the Todo item at point. 1941 "Edit the Todo item at point.
1421
1422With non-nil prefix argument ARG, include the item's date/time 1942With non-nil prefix argument ARG, include the item's date/time
1423header, making it also editable; otherwise, include only the item 1943header, making it also editable; otherwise, include only the item
1424content. 1944content.
@@ -2397,22 +2917,9 @@ comments without asking."
2397 (set-marker omark nil))))) 2917 (set-marker omark nil)))))
2398 2918
2399;; ----------------------------------------------------------------------------- 2919;; -----------------------------------------------------------------------------
2400;;; Done Item Archives 2920;;; Done item archives
2401;; ----------------------------------------------------------------------------- 2921;; -----------------------------------------------------------------------------
2402 2922
2403(defcustom todos-skip-archived-categories nil
2404 "Non-nil to handle categories with only archived items specially.
2405
2406Sequential category navigation using \\[todos-forward-category]
2407or \\[todos-backward-category] skips categories that contain only
2408archived items. Other commands still recognize these categories.
2409In Todos Categories mode (\\[todos-show-categories-table]) these
2410categories shown in `todos-archived-only' face and pressing the
2411category button visits the category in the archive instead of the
2412todo file."
2413 :type 'boolean
2414 :group 'todos-display)
2415
2416(defun todos-find-archive (&optional ask) 2923(defun todos-find-archive (&optional ask)
2417 "Visit the archive of the current Todos category, if it exists. 2924 "Visit the archive of the current Todos category, if it exists.
2418If the category has no archived items, prompt to visit the 2925If the category has no archived items, prompt to visit the
@@ -2704,420 +3211,7 @@ and jump to any category in the current archive."
2704 (todos-jump-to-category file 'archive)) 3211 (todos-jump-to-category file 'archive))
2705 3212
2706;; ----------------------------------------------------------------------------- 3213;; -----------------------------------------------------------------------------
2707;;; Todos mode display options 3214;;; Displaying and sorting tables of categories
2708;; -----------------------------------------------------------------------------
2709
2710(defcustom todos-prefix ""
2711 "String prefixed to todo items for visual distinction."
2712 :type '(string :validate
2713 (lambda (widget)
2714 (when (string= (widget-value widget) todos-item-mark)
2715 (widget-put
2716 widget :error
2717 "Invalid value: must be distinct from `todos-item-mark'")
2718 widget)))
2719 :initialize 'custom-initialize-default
2720 :set 'todos-reset-prefix
2721 :group 'todos-display)
2722
2723(defcustom todos-number-prefix t
2724 "Non-nil to prefix items with consecutively increasing integers.
2725These reflect the priorities of the items in each category."
2726 :type 'boolean
2727 :initialize 'custom-initialize-default
2728 :set 'todos-reset-prefix
2729 :group 'todos-display)
2730
2731(defcustom todos-done-separator-string "="
2732 "String determining the value of variable `todos-done-separator'.
2733
2734If the string consists of a single character,
2735`todos-done-separator' will be the string made by repeating this
2736character for the width of the window, and the length is
2737automatically recalculated when the window width changes. If the
2738string consists of more (or less) than one character, it will be
2739the value of `todos-done-separator'."
2740 :type 'string
2741 :initialize 'custom-initialize-default
2742 :set 'todos-reset-done-separator-string
2743 :group 'todos-display)
2744
2745(defcustom todos-done-string "DONE "
2746 "Identifying string appended to the front of done todos items."
2747 :type 'string
2748 :initialize 'custom-initialize-default
2749 :set 'todos-reset-done-string
2750 :group 'todos-display)
2751
2752(defcustom todos-comment-string "COMMENT"
2753 "String inserted before optional comment appended to done item."
2754 :type 'string
2755 :initialize 'custom-initialize-default
2756 :set 'todos-reset-comment-string
2757 :group 'todos-display)
2758
2759(defcustom todos-show-with-done nil
2760 "Non-nil to display done items in all categories."
2761 :type 'boolean
2762 :group 'todos-display)
2763
2764(defun todos-mode-line-control (cat)
2765 "Return a mode line control for todo or archive file buffers.
2766Argument CAT is the name of the current Todos category.
2767This function is the value of the user variable
2768`todos-mode-line-function'."
2769 (let ((file (todos-short-file-name todos-current-todos-file)))
2770 (format "%s category %d: %s" file todos-category-number cat)))
2771
2772(defcustom todos-mode-line-function 'todos-mode-line-control
2773 "Function that returns a mode line control for Todos buffers.
2774The function expects one argument holding the name of the current
2775Todos category. The resulting control becomes the local value of
2776`mode-line-buffer-identification' in each Todos buffer."
2777 :type 'function
2778 :group 'todos-display)
2779
2780(defcustom todos-highlight-item nil
2781 "Non-nil means highlight items at point."
2782 :type 'boolean
2783 :initialize 'custom-initialize-default
2784 :set 'todos-reset-highlight-item
2785 :group 'todos-display)
2786
2787(defcustom todos-wrap-lines t
2788 "Non-nil to activate Visual Line mode and use wrap prefix."
2789 :type 'boolean
2790 :group 'todos-display)
2791
2792(defcustom todos-indent-to-here 3
2793 "Number of spaces to indent continuation lines of items.
2794This must be a positive number to ensure such items are fully
2795shown in the Fancy Diary display."
2796 :type '(integer :validate
2797 (lambda (widget)
2798 (unless (> (widget-value widget) 0)
2799 (widget-put widget :error
2800 "Invalid value: must be a positive integer")
2801 widget)))
2802 :group 'todos-display)
2803
2804(defun todos-indent ()
2805 "Indent from point to `todos-indent-to-here'."
2806 (indent-to todos-indent-to-here todos-indent-to-here))
2807
2808;; -----------------------------------------------------------------------------
2809;;; Display Commands
2810;; -----------------------------------------------------------------------------
2811
2812(defun todos-toggle-prefix-numbers ()
2813 "Hide item numbering if shown, show if hidden."
2814 (interactive)
2815 (save-excursion
2816 (save-restriction
2817 (goto-char (point-min))
2818 (let* ((ov (todos-get-overlay 'prefix))
2819 (show-done (re-search-forward todos-done-string-start nil t))
2820 (todos-show-with-done show-done)
2821 (todos-number-prefix (not (equal (overlay-get ov 'before-string)
2822 "1 "))))
2823 (if (eq major-mode 'todos-filtered-items-mode)
2824 (todos-prefix-overlays)
2825 (todos-category-select))))))
2826
2827(defun todos-toggle-view-done-items ()
2828 "Show hidden or hide visible done items in current category."
2829 (interactive)
2830 (if (zerop (todos-get-count 'done (todos-current-category)))
2831 (message "There are no done items in this category.")
2832 (let ((opoint (point)))
2833 (goto-char (point-min))
2834 (let* ((shown (re-search-forward todos-done-string-start nil t))
2835 (todos-show-with-done (not shown)))
2836 (todos-category-select)
2837 (goto-char opoint)
2838 ;; If start of done items sections is below the bottom of the
2839 ;; window, make it visible.
2840 (unless shown
2841 (setq shown (progn
2842 (goto-char (point-min))
2843 (re-search-forward todos-done-string-start nil t)))
2844 (if (not (pos-visible-in-window-p shown))
2845 (recenter)
2846 (goto-char opoint)))))))
2847
2848(defun todos-toggle-view-done-only ()
2849 "Switch between displaying only done or only todo items."
2850 (interactive)
2851 (setq todos-show-done-only (not todos-show-done-only))
2852 (todos-category-select))
2853
2854(defun todos-toggle-item-highlighting ()
2855 "Highlight or unhighlight the todo item the cursor is on."
2856 (interactive)
2857 (require 'hl-line)
2858 (when (memq major-mode '(todos-mode todos-archive-mode
2859 todos-filtered-items-mode))
2860 (if hl-line-mode
2861 (hl-line-mode -1)
2862 (hl-line-mode 1))))
2863
2864(defun todos-toggle-item-header ()
2865 "Hide or show item date-time headers in the current file.
2866With done items, this hides only the done date-time string, not
2867the the original date-time string."
2868 (interactive)
2869 (save-excursion
2870 (save-restriction
2871 (goto-char (point-min))
2872 (let ((ov (todos-get-overlay 'header)))
2873 (if ov
2874 (remove-overlays 1 (1+ (buffer-size)) 'todos 'header)
2875 (widen)
2876 (goto-char (point-min))
2877 (while (not (eobp))
2878 (when (re-search-forward
2879 (concat todos-item-start
2880 "\\( " diary-time-regexp "\\)?"
2881 (regexp-quote todos-nondiary-end) "? ")
2882 nil t)
2883 (setq ov (make-overlay (match-beginning 0) (match-end 0) nil t))
2884 (overlay-put ov 'todos 'header)
2885 (overlay-put ov 'display ""))
2886 (todos-forward-item)))))))
2887
2888;; -----------------------------------------------------------------------------
2889;;; Faces
2890;; -----------------------------------------------------------------------------
2891
2892(defface todos-prefix-string
2893 ;; '((t :inherit font-lock-constant-face))
2894 '((((class grayscale) (background light))
2895 (:foreground "LightGray" :weight bold :underline t))
2896 (((class grayscale) (background dark))
2897 (:foreground "Gray50" :weight bold :underline t))
2898 (((class color) (min-colors 88) (background light)) (:foreground "dark cyan"))
2899 (((class color) (min-colors 88) (background dark)) (:foreground "Aquamarine"))
2900 (((class color) (min-colors 16) (background light)) (:foreground "CadetBlue"))
2901 (((class color) (min-colors 16) (background dark)) (:foreground "Aquamarine"))
2902 (((class color) (min-colors 8)) (:foreground "magenta"))
2903 (t (:weight bold :underline t)))
2904 "Face for Todos prefix or numerical priority string."
2905 :group 'todos-faces)
2906
2907(defface todos-top-priority
2908 ;; bold font-lock-comment-face
2909 '((default :weight bold)
2910 (((class grayscale) (background light)) :foreground "DimGray" :slant italic)
2911 (((class grayscale) (background dark)) :foreground "LightGray" :slant italic)
2912 (((class color) (min-colors 88) (background light)) :foreground "Firebrick")
2913 (((class color) (min-colors 88) (background dark)) :foreground "chocolate1")
2914 (((class color) (min-colors 16) (background light)) :foreground "red")
2915 (((class color) (min-colors 16) (background dark)) :foreground "red1")
2916 (((class color) (min-colors 8) (background light)) :foreground "red")
2917 (((class color) (min-colors 8) (background dark)) :foreground "yellow")
2918 (t :slant italic))
2919 "Face for top priority Todos item numerical priority string.
2920The item's priority number string has this face if the number is
2921less than or equal the category's top priority setting."
2922 :group 'todos-faces)
2923
2924(defface todos-mark
2925 ;; '((t :inherit font-lock-warning-face))
2926 '((((class color)
2927 (min-colors 88)
2928 (background light))
2929 (:weight bold :foreground "Red1"))
2930 (((class color)
2931 (min-colors 88)
2932 (background dark))
2933 (:weight bold :foreground "Pink"))
2934 (((class color)
2935 (min-colors 16)
2936 (background light))
2937 (:weight bold :foreground "Red1"))
2938 (((class color)
2939 (min-colors 16)
2940 (background dark))
2941 (:weight bold :foreground "Pink"))
2942 (((class color)
2943 (min-colors 8))
2944 (:foreground "red"))
2945 (t
2946 (:weight bold :inverse-video t)))
2947 "Face for marks on marked items."
2948 :group 'todos-faces)
2949
2950(defface todos-button
2951 ;; '((t :inherit widget-field))
2952 '((((type tty))
2953 (:foreground "black" :background "yellow3"))
2954 (((class grayscale color)
2955 (background light))
2956 (:background "gray85"))
2957 (((class grayscale color)
2958 (background dark))
2959 (:background "dim gray"))
2960 (t
2961 (:slant italic)))
2962 "Face for buttons in table of categories."
2963 :group 'todos-faces)
2964
2965(defface todos-sorted-column
2966 '((((type tty))
2967 (:inverse-video t))
2968 (((class color)
2969 (background light))
2970 (:background "grey85"))
2971 (((class color)
2972 (background dark))
2973 (:background "grey85" :foreground "grey10"))
2974 (t
2975 (:background "gray")))
2976 "Face for sorted column in table of categories."
2977 :group 'todos-faces)
2978
2979(defface todos-archived-only
2980 ;; '((t (:inherit (shadow))))
2981 '((((class color)
2982 (background light))
2983 (:foreground "grey50"))
2984 (((class color)
2985 (background dark))
2986 (:foreground "grey70"))
2987 (t
2988 (:foreground "gray")))
2989 "Face for archived-only category names in table of categories."
2990 :group 'todos-faces)
2991
2992(defface todos-search
2993 ;; '((t :inherit match))
2994 '((((class color)
2995 (min-colors 88)
2996 (background light))
2997 (:background "yellow1"))
2998 (((class color)
2999 (min-colors 88)
3000 (background dark))
3001 (:background "RoyalBlue3"))
3002 (((class color)
3003 (min-colors 8)
3004 (background light))
3005 (:foreground "black" :background "yellow"))
3006 (((class color)
3007 (min-colors 8)
3008 (background dark))
3009 (:foreground "white" :background "blue"))
3010 (((type tty)
3011 (class mono))
3012 (:inverse-video t))
3013 (t
3014 (:background "gray")))
3015 "Face for matches found by `todos-search'."
3016 :group 'todos-faces)
3017
3018(defface todos-diary-expired
3019 ;; Doesn't contrast enough with todos-date (= diary) face.
3020 ;; ;; '((t :inherit warning))
3021 ;; '((default :weight bold)
3022 ;; (((class color) (min-colors 16)) :foreground "DarkOrange")
3023 ;; (((class color)) :foreground "yellow"))
3024 ;; bold font-lock-function-name-face
3025 '((default :weight bold)
3026 (((class color) (min-colors 88) (background light)) :foreground "Blue1")
3027 (((class color) (min-colors 88) (background dark)) :foreground "LightSkyBlue")
3028 (((class color) (min-colors 16) (background light)) :foreground "Blue")
3029 (((class color) (min-colors 16) (background dark)) :foreground "LightSkyBlue")
3030 (((class color) (min-colors 8)) :foreground "blue")
3031 (t :inverse-video t))
3032 "Face for expired dates of diary items."
3033 :group 'todos-faces)
3034
3035(defface todos-date
3036 '((t :inherit diary))
3037 "Face for the date string of a Todos item."
3038 :group 'todos-faces)
3039
3040(defface todos-time
3041 '((t :inherit diary-time))
3042 "Face for the time string of a Todos item."
3043 :group 'todos-faces)
3044
3045(defface todos-nondiary
3046 ;; '((t :inherit font-lock-type-face))
3047 '((((class grayscale) (background light)) :foreground "Gray90" :weight bold)
3048 (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
3049 (((class color) (min-colors 88) (background light)) :foreground "ForestGreen")
3050 (((class color) (min-colors 88) (background dark)) :foreground "PaleGreen")
3051 (((class color) (min-colors 16) (background light)) :foreground "ForestGreen")
3052 (((class color) (min-colors 16) (background dark)) :foreground "PaleGreen")
3053 (((class color) (min-colors 8)) :foreground "green")
3054 (t :weight bold :underline t))
3055 "Face for non-diary markers around todo item date/time header."
3056 :group 'todos-faces)
3057
3058(defface todos-category-string
3059 ;; '((t :inherit font-lock-type-face))
3060 '((((class grayscale) (background light)) :foreground "Gray90" :weight bold)
3061 (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
3062 (((class color) (min-colors 88) (background light)) :foreground "ForestGreen")
3063 (((class color) (min-colors 88) (background dark)) :foreground "PaleGreen")
3064 (((class color) (min-colors 16) (background light)) :foreground "ForestGreen")
3065 (((class color) (min-colors 16) (background dark)) :foreground "PaleGreen")
3066 (((class color) (min-colors 8)) :foreground "green")
3067 (t :weight bold :underline t))
3068 "Face for category-file header in Todos Filtered Items mode."
3069 :group 'todos-faces)
3070
3071(defface todos-done
3072 ;; '((t :inherit font-lock-keyword-face))
3073 '((((class grayscale) (background light)) :foreground "LightGray" :weight bold)
3074 (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
3075 (((class color) (min-colors 88) (background light)) :foreground "Purple")
3076 (((class color) (min-colors 88) (background dark)) :foreground "Cyan1")
3077 (((class color) (min-colors 16) (background light)) :foreground "Purple")
3078 (((class color) (min-colors 16) (background dark)) :foreground "Cyan")
3079 (((class color) (min-colors 8)) :foreground "cyan" :weight bold)
3080 (t :weight bold))
3081 "Face for done Todos item header string."
3082 :group 'todos-faces)
3083
3084(defface todos-comment
3085 ;; '((t :inherit font-lock-comment-face))
3086 '((((class grayscale) (background light))
3087 :foreground "DimGray" :weight bold :slant italic)
3088 (((class grayscale) (background dark))
3089 :foreground "LightGray" :weight bold :slant italic)
3090 (((class color) (min-colors 88) (background light))
3091 :foreground "Firebrick")
3092 (((class color) (min-colors 88) (background dark))
3093 :foreground "chocolate1")
3094 (((class color) (min-colors 16) (background light))
3095 :foreground "red")
3096 (((class color) (min-colors 16) (background dark))
3097 :foreground "red1")
3098 (((class color) (min-colors 8) (background light))
3099 :foreground "red")
3100 (((class color) (min-colors 8) (background dark))
3101 :foreground "yellow")
3102 (t :weight bold :slant italic))
3103 "Face for comments appended to done Todos items."
3104 :group 'todos-faces)
3105
3106(defface todos-done-sep
3107 ;; '((t :inherit font-lock-builtin-face))
3108 '((((class grayscale) (background light)) :foreground "LightGray" :weight bold)
3109 (((class grayscale) (background dark)) :foreground "DimGray" :weight bold)
3110 (((class color) (min-colors 88) (background light)) :foreground "dark slate blue")
3111 (((class color) (min-colors 88) (background dark)) :foreground "LightSteelBlue")
3112 (((class color) (min-colors 16) (background light)) :foreground "Orchid")
3113 (((class color) (min-colors 16) (background dark)) :foreground "LightSteelBlue")
3114 (((class color) (min-colors 8)) :foreground "blue" :weight bold)
3115 (t :weight bold))
3116 "Face for separator string bewteen done and not done Todos items."
3117 :group 'todos-faces)
3118
3119;; -----------------------------------------------------------------------------
3120;;; Todos Categories mode options
3121;; ----------------------------------------------------------------------------- 3215;; -----------------------------------------------------------------------------
3122 3216
3123(defcustom todos-categories-category-label "Category" 3217(defcustom todos-categories-category-label "Category"
@@ -3162,10 +3256,6 @@ categories display according to priority."
3162 :type '(radio (const left) (const center) (const right)) 3256 :type '(radio (const left) (const center) (const right))
3163 :group 'todos-categories) 3257 :group 'todos-categories)
3164 3258
3165;; -----------------------------------------------------------------------------
3166;;; Entering and using Todos Categories mode
3167;; -----------------------------------------------------------------------------
3168
3169(defun todos-show-categories-table () 3259(defun todos-show-categories-table ()
3170 "Display a table of the current file's categories and item counts. 3260 "Display a table of the current file's categories and item counts.
3171 3261
@@ -3195,51 +3285,6 @@ are shown in `todos-archived-only' face."
3195 (let (sortkey) 3285 (let (sortkey)
3196 (todos-update-categories-display sortkey))) 3286 (todos-update-categories-display sortkey)))
3197 3287
3198(defun todos-sort-categories-alphabetically-or-numerically ()
3199 "Sort table of categories alphabetically or numerically."
3200 (interactive)
3201 (save-excursion
3202 (goto-char (point-min))
3203 (forward-line 2)
3204 (if (member 'alpha todos-descending-counts)
3205 (progn
3206 (todos-update-categories-display nil)
3207 (setq todos-descending-counts
3208 (delete 'alpha todos-descending-counts)))
3209 (todos-update-categories-display 'alpha))))
3210
3211(defun todos-sort-categories-by-todo ()
3212 "Sort table of categories by number of todo items."
3213 (interactive)
3214 (save-excursion
3215 (goto-char (point-min))
3216 (forward-line 2)
3217 (todos-update-categories-display 'todo)))
3218
3219(defun todos-sort-categories-by-diary ()
3220 "Sort table of categories by number of diary items."
3221 (interactive)
3222 (save-excursion
3223 (goto-char (point-min))
3224 (forward-line 2)
3225 (todos-update-categories-display 'diary)))
3226
3227(defun todos-sort-categories-by-done ()
3228 "Sort table of categories by number of non-archived done items."
3229 (interactive)
3230 (save-excursion
3231 (goto-char (point-min))
3232 (forward-line 2)
3233 (todos-update-categories-display 'done)))
3234
3235(defun todos-sort-categories-by-archived ()
3236 "Sort table of categories by number of archived items."
3237 (interactive)
3238 (save-excursion
3239 (goto-char (point-min))
3240 (forward-line 2)
3241 (todos-update-categories-display 'archived)))
3242
3243(defun todos-next-button (n) 3288(defun todos-next-button (n)
3244 "Move point to the Nth next button in the table of categories." 3289 "Move point to the Nth next button in the table of categories."
3245 (interactive "p") 3290 (interactive "p")
@@ -3321,8 +3366,319 @@ decreasing or increasing its number."
3321 (interactive) 3366 (interactive)
3322 (todos-set-category-number 'lower)) 3367 (todos-set-category-number 'lower))
3323 3368
3369(defun todos-sort-categories-alphabetically-or-numerically ()
3370 "Sort table of categories alphabetically or numerically."
3371 (interactive)
3372 (save-excursion
3373 (goto-char (point-min))
3374 (forward-line 2)
3375 (if (member 'alpha todos-descending-counts)
3376 (progn
3377 (todos-update-categories-display nil)
3378 (setq todos-descending-counts
3379 (delete 'alpha todos-descending-counts)))
3380 (todos-update-categories-display 'alpha))))
3381
3382(defun todos-sort-categories-by-todo ()
3383 "Sort table of categories by number of todo items."
3384 (interactive)
3385 (save-excursion
3386 (goto-char (point-min))
3387 (forward-line 2)
3388 (todos-update-categories-display 'todo)))
3389
3390(defun todos-sort-categories-by-diary ()
3391 "Sort table of categories by number of diary items."
3392 (interactive)
3393 (save-excursion
3394 (goto-char (point-min))
3395 (forward-line 2)
3396 (todos-update-categories-display 'diary)))
3397
3398(defun todos-sort-categories-by-done ()
3399 "Sort table of categories by number of non-archived done items."
3400 (interactive)
3401 (save-excursion
3402 (goto-char (point-min))
3403 (forward-line 2)
3404 (todos-update-categories-display 'done)))
3405
3406(defun todos-sort-categories-by-archived ()
3407 "Sort table of categories by number of archived items."
3408 (interactive)
3409 (save-excursion
3410 (goto-char (point-min))
3411 (forward-line 2)
3412 (todos-update-categories-display 'archived)))
3413
3414(defvar todos-categories-buffer "*Todos Categories*"
3415 "Name of buffer in Todos Categories mode.")
3416
3417(defun todos-longest-category-name-length (categories)
3418 "Return the length of the longest name in list CATEGORIES."
3419 (let ((longest 0))
3420 (dolist (c categories longest)
3421 (setq longest (max longest (length c))))))
3422
3423(defun todos-adjusted-category-label-length ()
3424 "Return adjusted length of category label button.
3425The adjustment ensures proper tabular alignment in Todos
3426Categories mode."
3427 (let* ((categories (mapcar 'car todos-categories))
3428 (longest (todos-longest-category-name-length categories))
3429 (catlablen (length todos-categories-category-label))
3430 (lc-diff (- longest catlablen)))
3431 (if (and (natnump lc-diff) (cl-oddp lc-diff))
3432 (1+ longest)
3433 (max longest catlablen))))
3434
3435(defun todos-padded-string (str)
3436 "Return category name or label string STR padded with spaces.
3437The placement of the padding is determined by the value of user
3438option `todos-categories-align'."
3439 (let* ((len (todos-adjusted-category-label-length))
3440 (strlen (length str))
3441 (strlen-odd (eq (logand strlen 1) 1))
3442 (padding (max 0 (/ (- len strlen) 2)))
3443 (padding-left (cond ((eq todos-categories-align 'left) 0)
3444 ((eq todos-categories-align 'center) padding)
3445 ((eq todos-categories-align 'right)
3446 (if strlen-odd (1+ (* padding 2)) (* padding 2)))))
3447 (padding-right (cond ((eq todos-categories-align 'left)
3448 (if strlen-odd (1+ (* padding 2)) (* padding 2)))
3449 ((eq todos-categories-align 'center)
3450 (if strlen-odd (1+ padding) padding))
3451 ((eq todos-categories-align 'right) 0))))
3452 (concat (make-string padding-left 32) str (make-string padding-right 32))))
3453
3454(defvar todos-descending-counts nil
3455 "List of keys for category counts sorted in descending order.")
3456
3457(defun todos-sort (list &optional key)
3458 "Return a copy of LIST, possibly sorted according to KEY."
3459 (let* ((l (copy-sequence list))
3460 (fn (if (eq key 'alpha)
3461 (lambda (x) (upcase x)) ; Alphabetize case insensitively.
3462 (lambda (x) (todos-get-count key x))))
3463 ;; Keep track of whether the last sort by key was descending or
3464 ;; ascending.
3465 (descending (member key todos-descending-counts))
3466 (cmp (if (eq key 'alpha)
3467 'string<
3468 (if descending '< '>)))
3469 (pred (lambda (s1 s2) (let ((t1 (funcall fn (car s1)))
3470 (t2 (funcall fn (car s2))))
3471 (funcall cmp t1 t2)))))
3472 (when key
3473 (setq l (sort l pred))
3474 ;; Switch between descending and ascending sort order.
3475 (if descending
3476 (setq todos-descending-counts
3477 (delete key todos-descending-counts))
3478 (push key todos-descending-counts)))
3479 l))
3480
3481(defun todos-display-sorted (type)
3482 "Keep point on the TYPE count sorting button just clicked."
3483 (let ((opoint (point)))
3484 (todos-update-categories-display type)
3485 (goto-char opoint)))
3486
3487(defun todos-label-to-key (label)
3488 "Return symbol for sort key associated with LABEL."
3489 (let (key)
3490 (cond ((string= label todos-categories-category-label)
3491 (setq key 'alpha))
3492 ((string= label todos-categories-todo-label)
3493 (setq key 'todo))
3494 ((string= label todos-categories-diary-label)
3495 (setq key 'diary))
3496 ((string= label todos-categories-done-label)
3497 (setq key 'done))
3498 ((string= label todos-categories-archived-label)
3499 (setq key 'archived)))
3500 key))
3501
3502(defun todos-insert-sort-button (label)
3503 "Insert button for displaying categories sorted by item counts.
3504LABEL determines which type of count is sorted."
3505 (let* ((str (if (string= label todos-categories-category-label)
3506 (todos-padded-string label)
3507 label))
3508 (beg (point))
3509 (end (+ beg (length str)))
3510 ov)
3511 (insert-button str 'face nil
3512 'action
3513 `(lambda (button)
3514 (let ((key (todos-label-to-key ,label)))
3515 (if (and (member key todos-descending-counts)
3516 (eq key 'alpha))
3517 (progn
3518 ;; If display is alphabetical, switch back to
3519 ;; category priority order.
3520 (todos-display-sorted nil)
3521 (setq todos-descending-counts
3522 (delete key todos-descending-counts)))
3523 (todos-display-sorted key)))))
3524 (setq ov (make-overlay beg end))
3525 (overlay-put ov 'face 'todos-button)))
3526
3527(defun todos-total-item-counts ()
3528 "Return a list of total item counts for the current file."
3529 (mapcar (lambda (i) (apply '+ (mapcar (lambda (l) (aref l i))
3530 (mapcar 'cdr todos-categories))))
3531 (list 0 1 2 3)))
3532
3533(defvar todos-categories-category-number 0
3534 "Variable for numbering categories in Todos Categories mode.")
3535
3536(defun todos-insert-category-line (cat &optional nonum)
3537 "Insert button with category CAT's name and item counts.
3538With non-nil argument NONUM show only these; otherwise, insert a
3539number in front of the button indicating the category's priority.
3540The number and the category name are separated by the string
3541which is the value of the user option
3542`todos-categories-number-separator'."
3543 (let ((archive (member todos-current-todos-file todos-archives))
3544 (num todos-categories-category-number)
3545 (str (todos-padded-string cat))
3546 (opoint (point)))
3547 (setq num (1+ num) todos-categories-category-number num)
3548 (insert-button
3549 (concat (if nonum
3550 (make-string (+ 4 (length todos-categories-number-separator))
3551 32)
3552 (format " %3d%s" num todos-categories-number-separator))
3553 str
3554 (mapconcat (lambda (elt)
3555 (concat
3556 (make-string (1+ (/ (length (car elt)) 2)) 32) ; label
3557 (format "%3d" (todos-get-count (cdr elt) cat)) ; count
3558 ;; Add an extra space if label length is odd.
3559 (when (cl-oddp (length (car elt))) " ")))
3560 (if archive
3561 (list (cons todos-categories-done-label 'done))
3562 (list (cons todos-categories-todo-label 'todo)
3563 (cons todos-categories-diary-label 'diary)
3564 (cons todos-categories-done-label 'done)
3565 (cons todos-categories-archived-label
3566 'archived)))
3567 "")
3568 " ") ; Make highlighting on last column look better.
3569 'face (if (and todos-skip-archived-categories
3570 (zerop (todos-get-count 'todo cat))
3571 (zerop (todos-get-count 'done cat))
3572 (not (zerop (todos-get-count 'archived cat))))
3573 'todos-archived-only
3574 nil)
3575 'action `(lambda (button) (let ((buf (current-buffer)))
3576 (todos-jump-to-category nil ,cat)
3577 (kill-buffer buf))))
3578 ;; Highlight the sorted count column.
3579 (let* ((beg (+ opoint 7 (length str)))
3580 end ovl)
3581 (cond ((eq nonum 'todo)
3582 (setq beg (+ beg 1 (/ (length todos-categories-todo-label) 2))))
3583 ((eq nonum 'diary)
3584 (setq beg (+ beg 1 (length todos-categories-todo-label)
3585 2 (/ (length todos-categories-diary-label) 2))))
3586 ((eq nonum 'done)
3587 (setq beg (+ beg 1 (length todos-categories-todo-label)
3588 2 (length todos-categories-diary-label)
3589 2 (/ (length todos-categories-done-label) 2))))
3590 ((eq nonum 'archived)
3591 (setq beg (+ beg 1 (length todos-categories-todo-label)
3592 2 (length todos-categories-diary-label)
3593 2 (length todos-categories-done-label)
3594 2 (/ (length todos-categories-archived-label) 2)))))
3595 (unless (= beg (+ opoint 7 (length str))) ; Don't highlight categories.
3596 (setq end (+ beg 4))
3597 (setq ovl (make-overlay beg end))
3598 (overlay-put ovl 'face 'todos-sorted-column)))
3599 (newline)))
3600
3601(defun todos-display-categories ()
3602 "Prepare buffer for displaying table of categories and item counts."
3603 (unless (eq major-mode 'todos-categories-mode)
3604 (setq todos-global-current-todos-file
3605 (or todos-current-todos-file
3606 (todos-absolute-file-name todos-default-todos-file)))
3607 (set-window-buffer (selected-window)
3608 (set-buffer (get-buffer-create todos-categories-buffer)))
3609 (kill-all-local-variables)
3610 (todos-categories-mode)
3611 (let ((archive (member todos-current-todos-file todos-archives))
3612 buffer-read-only)
3613 (erase-buffer)
3614 (insert (format (concat "Category counts for Todos "
3615 (if archive "archive" "file")
3616 " \"%s\".")
3617 (todos-short-file-name todos-current-todos-file)))
3618 (newline 2)
3619 ;; Make space for the column of category numbers.
3620 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32))
3621 ;; Add the category and item count buttons (if this is the list of
3622 ;; categories in an archive, show only done item counts).
3623 (todos-insert-sort-button todos-categories-category-label)
3624 (if archive
3625 (progn
3626 (insert (make-string 3 32))
3627 (todos-insert-sort-button todos-categories-done-label))
3628 (insert (make-string 3 32))
3629 (todos-insert-sort-button todos-categories-todo-label)
3630 (insert (make-string 2 32))
3631 (todos-insert-sort-button todos-categories-diary-label)
3632 (insert (make-string 2 32))
3633 (todos-insert-sort-button todos-categories-done-label)
3634 (insert (make-string 2 32))
3635 (todos-insert-sort-button todos-categories-archived-label))
3636 (newline 2))))
3637
3638(defun todos-update-categories-display (sortkey)
3639 "Populate table of categories and sort by SORTKEY."
3640 (let* ((cats0 todos-categories)
3641 (cats (todos-sort cats0 sortkey))
3642 (archive (member todos-current-todos-file todos-archives))
3643 (todos-categories-category-number 0)
3644 ;; Find start of Category button if we just entered Todos Categories
3645 ;; mode.
3646 (pt (if (eq (point) (point-max))
3647 (save-excursion
3648 (forward-line -2)
3649 (goto-char (next-single-char-property-change
3650 (point) 'face nil (line-end-position))))))
3651 (buffer-read-only))
3652 (forward-line 2)
3653 (delete-region (point) (point-max))
3654 ;; Fill in the table with buttonized lines, each showing a category and
3655 ;; its item counts.
3656 (mapc (lambda (cat) (todos-insert-category-line cat sortkey))
3657 (mapcar 'car cats))
3658 (newline)
3659 ;; Add a line showing item count totals.
3660 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32)
3661 (todos-padded-string todos-categories-totals-label)
3662 (mapconcat
3663 (lambda (elt)
3664 (concat
3665 (make-string (1+ (/ (length (car elt)) 2)) 32)
3666 (format "%3d" (nth (cdr elt) (todos-total-item-counts)))
3667 ;; Add an extra space if label length is odd.
3668 (when (cl-oddp (length (car elt))) " ")))
3669 (if archive
3670 (list (cons todos-categories-done-label 2))
3671 (list (cons todos-categories-todo-label 0)
3672 (cons todos-categories-diary-label 1)
3673 (cons todos-categories-done-label 2)
3674 (cons todos-categories-archived-label 3)))
3675 ""))
3676 ;; Put cursor on Category button initially.
3677 (if pt (goto-char pt))
3678 (setq buffer-read-only t)))
3679
3324;; ----------------------------------------------------------------------------- 3680;; -----------------------------------------------------------------------------
3325;;; Searching 3681;;; Searching and item filtering
3326;; ----------------------------------------------------------------------------- 3682;; -----------------------------------------------------------------------------
3327 3683
3328(defun todos-search () 3684(defun todos-search ()
@@ -3391,10 +3747,6 @@ face."
3391 (interactive) 3747 (interactive)
3392 (remove-overlays 1 (1+ (buffer-size)) 'face 'todos-search)) 3748 (remove-overlays 1 (1+ (buffer-size)) 'face 'todos-search))
3393 3749
3394;; -----------------------------------------------------------------------------
3395;;; Item filtering options
3396;; -----------------------------------------------------------------------------
3397
3398(defcustom todos-top-priorities-overrides nil 3750(defcustom todos-top-priorities-overrides nil
3399 "List of rules specifying number of top priority items to show. 3751 "List of rules specifying number of top priority items to show.
3400These rules override `todos-top-priorities' on invocations of 3752These rules override `todos-top-priorities' on invocations of
@@ -3431,10 +3783,6 @@ Done items from corresponding archive files are also included."
3431 :type 'boolean 3783 :type 'boolean
3432 :group 'todos-filtered) 3784 :group 'todos-filtered)
3433 3785
3434;; -----------------------------------------------------------------------------
3435;;; Item filtering commands
3436;; -----------------------------------------------------------------------------
3437
3438(defun todos-set-top-priorities-in-file () 3786(defun todos-set-top-priorities-in-file ()
3439 "Set number of top priorities for this file. 3787 "Set number of top priorities for this file.
3440See `todos-set-top-priorities' for more details." 3788See `todos-set-top-priorities' for more details."
@@ -3585,8 +3933,446 @@ regexp items."
3585 (todos-category-select)) 3933 (todos-category-select))
3586 (goto-char (car found))))) 3934 (goto-char (car found)))))
3587 3935
3936(defvar todos-multiple-filter-files nil
3937 "List of files selected from `todos-multiple-filter-files' widget.")
3938
3939(defvar todos-multiple-filter-files-widget nil
3940 "Variable holding widget created by `todos-multiple-filter-files'.")
3941
3942(defun todos-multiple-filter-files ()
3943 "Pop to a buffer with a widget for choosing multiple filter files."
3944 (require 'widget)
3945 (eval-when-compile
3946 (require 'wid-edit))
3947 (with-current-buffer (get-buffer-create "*Todos Filter Files*")
3948 (pop-to-buffer (current-buffer))
3949 (erase-buffer)
3950 (kill-all-local-variables)
3951 (widget-insert "Select files for generating the top priorities list.\n\n")
3952 (setq todos-multiple-filter-files-widget
3953 (widget-create
3954 `(set ,@(mapcar (lambda (x) (list 'const x))
3955 (mapcar 'todos-short-file-name
3956 (funcall todos-files-function))))))
3957 (widget-insert "\n")
3958 (widget-create 'push-button
3959 :notify (lambda (widget &rest ignore)
3960 (setq todos-multiple-filter-files 'quit)
3961 (quit-window t)
3962 (exit-recursive-edit))
3963 "Cancel")
3964 (widget-insert " ")
3965 (widget-create 'push-button
3966 :notify (lambda (&rest ignore)
3967 (setq todos-multiple-filter-files
3968 (mapcar (lambda (f)
3969 (file-truename
3970 (concat todos-directory
3971 f ".todo")))
3972 (widget-value
3973 todos-multiple-filter-files-widget)))
3974 (quit-window t)
3975 (exit-recursive-edit))
3976 "Apply")
3977 (use-local-map widget-keymap)
3978 (widget-setup))
3979 (message "Click \"Apply\" after selecting files.")
3980 (recursive-edit))
3981
3982(defconst todos-filtered-items-buffer "Todos filtered items"
3983 "Initial name of buffer in Todos Filter Items mode.")
3984
3985(defconst todos-top-priorities-buffer "Todos top priorities"
3986 "Buffer type string for `todos-filter-items'.")
3987
3988(defconst todos-diary-items-buffer "Todos diary items"
3989 "Buffer type string for `todos-filter-items'.")
3990
3991(defconst todos-regexp-items-buffer "Todos regexp items"
3992 "Buffer type string for `todos-filter-items'.")
3993
3994(defun todos-filter-items (filter &optional new multifile)
3995 "Display a cross-categorial list of items filtered by FILTER.
3996The values of FILTER can be `top' for top priority items, a cons
3997of `top' and a number passed by the caller, `diary' for diary
3998items, or `regexp' for items matching a regular expresion entered
3999by the user. The items can be from any categories in the current
4000todo file or, with non-nil MULTIFILE, from several files. If NEW
4001is nil, visit an appropriate file containing the list of filtered
4002items; if there is no such file, or with non-nil NEW, build the
4003list and display it.
4004
4005See the document strings of the commands
4006`todos-filter-top-priorities', `todos-filter-diary-items',
4007`todos-filter-regexp-items', and those of the corresponding
4008multifile commands for further details."
4009 (let* ((top (eq filter 'top))
4010 (diary (eq filter 'diary))
4011 (regexp (eq filter 'regexp))
4012 (buf (cond (top todos-top-priorities-buffer)
4013 (diary todos-diary-items-buffer)
4014 (regexp todos-regexp-items-buffer)))
4015 (flist (if multifile
4016 (or todos-filter-files
4017 (progn (todos-multiple-filter-files)
4018 todos-multiple-filter-files))
4019 (list todos-current-todos-file)))
4020 (multi (> (length flist) 1))
4021 (fname (if (equal flist 'quit)
4022 ;; Pressed `cancel' in t-m-f-f file selection dialog.
4023 (keyboard-quit)
4024 (concat todos-directory
4025 (mapconcat 'todos-short-file-name flist "-")
4026 (cond (top ".todt")
4027 (diary ".tody")
4028 (regexp ".todr")))))
4029 (rxfiles (when regexp
4030 (directory-files todos-directory t ".*\\.todr$" t)))
4031 (file-exists (or (file-exists-p fname) rxfiles)))
4032 (cond ((and top new (natnump new))
4033 (todos-filter-items-1 (cons 'top new) flist))
4034 ((and (not new) file-exists)
4035 (when (and rxfiles (> (length rxfiles) 1))
4036 (let ((rxf (mapcar 'todos-short-file-name rxfiles)))
4037 (setq fname (todos-absolute-file-name
4038 (completing-read "Choose a regexp items file: "
4039 rxf) 'regexp))))
4040 (find-file fname)
4041 (todos-prefix-overlays)
4042 (todos-check-filtered-items-file))
4043 (t
4044 (todos-filter-items-1 filter flist)))
4045 (setq fname (replace-regexp-in-string "-" ", "
4046 (todos-short-file-name fname)))
4047 (rename-buffer (format (concat "%s for file" (if multi "s" "")
4048 " \"%s\"") buf fname))))
4049
4050(defun todos-filter-items-1 (filter file-list)
4051 "Build a list of items by applying FILTER to FILE-LIST.
4052Internal subroutine called by `todos-filter-items', which passes
4053the values of FILTER and FILE-LIST."
4054 (let ((num (if (consp filter) (cdr filter) todos-top-priorities))
4055 (buf (get-buffer-create todos-filtered-items-buffer))
4056 (multifile (> (length file-list) 1))
4057 regexp fname bufstr cat beg end done)
4058 (if (null file-list)
4059 (user-error "No files have been chosen for filtering")
4060 (with-current-buffer buf
4061 (erase-buffer)
4062 (kill-all-local-variables)
4063 (todos-filtered-items-mode))
4064 (when (eq filter 'regexp)
4065 (setq regexp (read-string "Enter a regular expression: ")))
4066 (save-current-buffer
4067 (dolist (f file-list)
4068 ;; Before inserting file contents into temp buffer, save a modified
4069 ;; buffer visiting it.
4070 (let ((bf (find-buffer-visiting f)))
4071 (when (buffer-modified-p bf)
4072 (with-current-buffer bf (save-buffer))))
4073 (setq fname (todos-short-file-name f))
4074 (with-temp-buffer
4075 (when (and todos-filter-done-items (eq filter 'regexp))
4076 ;; If there is a corresponding archive file for the
4077 ;; Todos file, insert it first and add identifiers for
4078 ;; todos-go-to-source-item.
4079 (let ((arch (concat (file-name-sans-extension f) ".toda")))
4080 (when (file-exists-p arch)
4081 (insert-file-contents arch)
4082 ;; Delete Todos archive file categories sexp.
4083 (delete-region (line-beginning-position)
4084 (1+ (line-end-position)))
4085 (save-excursion
4086 (while (not (eobp))
4087 (when (re-search-forward
4088 (concat (if todos-filter-done-items
4089 (concat "\\(?:" todos-done-string-start
4090 "\\|" todos-date-string-start
4091 "\\)")
4092 todos-date-string-start)
4093 todos-date-pattern "\\(?: "
4094 diary-time-regexp "\\)?"
4095 (if todos-filter-done-items
4096 "\\]"
4097 (regexp-quote todos-nondiary-end)) "?")
4098 nil t)
4099 (insert "(archive) "))
4100 (forward-line))))))
4101 (insert-file-contents f)
4102 ;; Delete Todos file categories sexp.
4103 (delete-region (line-beginning-position) (1+ (line-end-position)))
4104 (let (fnum)
4105 ;; Unless the number of top priorities to show was
4106 ;; passed by the caller, the file-wide value from
4107 ;; `todos-top-priorities-overrides', if non-nil, overrides
4108 ;; `todos-top-priorities'.
4109 (unless (consp filter)
4110 (setq fnum (or (nth 1 (assoc f todos-top-priorities-overrides))
4111 todos-top-priorities)))
4112 (while (re-search-forward
4113 (concat "^" (regexp-quote todos-category-beg)
4114 "\\(.+\\)\n") nil t)
4115 (setq cat (match-string 1))
4116 (let (cnum)
4117 ;; Unless the number of top priorities to show was
4118 ;; passed by the caller, the category-wide value
4119 ;; from `todos-top-priorities-overrides', if non-nil,
4120 ;; overrides a non-nil file-wide value from
4121 ;; `todos-top-priorities-overrides' as well as
4122 ;; `todos-top-priorities'.
4123 (unless (consp filter)
4124 (let ((cats (nth 2 (assoc f todos-top-priorities-overrides))))
4125 (setq cnum (or (cdr (assoc cat cats)) fnum))))
4126 (delete-region (match-beginning 0) (match-end 0))
4127 (setq beg (point)) ; First item in the current category.
4128 (setq end (if (re-search-forward
4129 (concat "^" (regexp-quote todos-category-beg))
4130 nil t)
4131 (match-beginning 0)
4132 (point-max)))
4133 (goto-char beg)
4134 (setq done
4135 (if (re-search-forward
4136 (concat "\n" (regexp-quote todos-category-done))
4137 end t)
4138 (match-beginning 0)
4139 end))
4140 (unless (and todos-filter-done-items (eq filter 'regexp))
4141 ;; Leave done items.
4142 (delete-region done end)
4143 (setq end done))
4144 (narrow-to-region beg end) ; Process only current category.
4145 (goto-char (point-min))
4146 ;; Apply the filter.
4147 (cond ((eq filter 'diary)
4148 (while (not (eobp))
4149 (if (looking-at (regexp-quote todos-nondiary-start))
4150 (todos-remove-item)
4151 (todos-forward-item))))
4152 ((eq filter 'regexp)
4153 (while (not (eobp))
4154 (if (looking-at todos-item-start)
4155 (if (string-match regexp (todos-item-string))
4156 (todos-forward-item)
4157 (todos-remove-item))
4158 ;; Kill lines that aren't part of a todo or done
4159 ;; item (empty or todos-category-done).
4160 (delete-region (line-beginning-position)
4161 (1+ (line-end-position))))
4162 ;; If last todo item in file matches regexp and
4163 ;; there are no following done items,
4164 ;; todos-category-done string is left dangling,
4165 ;; because todos-forward-item jumps over it.
4166 (if (and (eobp)
4167 (looking-back
4168 (concat (regexp-quote todos-done-string)
4169 "\n")))
4170 (delete-region (point) (progn
4171 (forward-line -2)
4172 (point))))))
4173 (t ; Filter top priority items.
4174 (setq num (or cnum fnum num))
4175 (unless (zerop num)
4176 (todos-forward-item num))))
4177 (setq beg (point))
4178 ;; Delete non-top-priority items.
4179 (unless (member filter '(diary regexp))
4180 (delete-region beg end))
4181 (goto-char (point-min))
4182 ;; Add file (if using multiple files) and category tags to
4183 ;; item.
4184 (while (not (eobp))
4185 (when (re-search-forward
4186 (concat (if todos-filter-done-items
4187 (concat "\\(?:" todos-done-string-start
4188 "\\|" todos-date-string-start
4189 "\\)")
4190 todos-date-string-start)
4191 todos-date-pattern "\\(?: " diary-time-regexp
4192 "\\)?" (if todos-filter-done-items
4193 "\\]"
4194 (regexp-quote todos-nondiary-end))
4195 "?")
4196 nil t)
4197 (insert " [")
4198 (when (looking-at "(archive) ") (goto-char (match-end 0)))
4199 (insert (if multifile (concat fname ":") "") cat "]"))
4200 (forward-line))
4201 (widen)))
4202 (setq bufstr (buffer-string))
4203 (with-current-buffer buf
4204 (let (buffer-read-only)
4205 (insert bufstr)))))))
4206 (set-window-buffer (selected-window) (set-buffer buf))
4207 (todos-prefix-overlays)
4208 (goto-char (point-min)))))
4209
4210(defun todos-set-top-priorities (&optional arg)
4211 "Set number of top priorities shown by `todos-filter-top-priorities'.
4212With non-nil ARG, set the number only for the current Todos
4213category; otherwise, set the number for all categories in the
4214current Todos file.
4215
4216Calling this function via either of the commands
4217`todos-set-top-priorities-in-file' or
4218`todos-set-top-priorities-in-category' is the recommended way to
4219set the user customizable option `todos-top-priorities-overrides'."
4220 (let* ((cat (todos-current-category))
4221 (file todos-current-todos-file)
4222 (rules todos-top-priorities-overrides)
4223 (frule (assoc-string file rules))
4224 (crule (assoc-string cat (nth 2 frule)))
4225 (crules (nth 2 frule))
4226 (cur (or (if arg (cdr crule) (nth 1 frule))
4227 todos-top-priorities))
4228 (prompt (if arg (concat "Number of top priorities in this category"
4229 " (currently %d): ")
4230 (concat "Default number of top priorities per category"
4231 " in this file (currently %d): ")))
4232 (new -1)
4233 nrule)
4234 (while (< new 0)
4235 (let ((cur0 cur))
4236 (setq new (read-number (format prompt cur0))
4237 prompt "Enter a non-negative number: "
4238 cur0 nil)))
4239 (setq nrule (if arg
4240 (append (delete crule crules) (list (cons cat new)))
4241 (append (list file new) (list crules))))
4242 (setq rules (cons (if arg
4243 (list file cur nrule)
4244 nrule)
4245 (delete frule rules)))
4246 (customize-save-variable 'todos-top-priorities-overrides rules)
4247 (todos-prefix-overlays)))
4248
4249(defun todos-find-item (str)
4250 "Search for filtered item STR in its saved Todos file.
4251Return the list (FOUND FILE CAT), where CAT and FILE are the
4252item's category and file, and FOUND is a cons cell if the search
4253succeeds, whose car is the start of the item in FILE and whose
4254cdr is `done', if the item is now a done item, `changed', if its
4255text was truncated or augmented or, for a top priority item, if
4256its priority has changed, and `same' otherwise."
4257 (string-match (concat (if todos-filter-done-items
4258 (concat "\\(?:" todos-done-string-start "\\|"
4259 todos-date-string-start "\\)")
4260 todos-date-string-start)
4261 todos-date-pattern "\\(?: " diary-time-regexp "\\)?"
4262 (if todos-filter-done-items
4263 "\\]"
4264 (regexp-quote todos-nondiary-end)) "?"
4265 "\\(?4: \\[\\(?3:(archive) \\)?\\(?2:.*:\\)?"
4266 "\\(?1:.*\\)\\]\\).*$") str)
4267 (let ((cat (match-string 1 str))
4268 (file (match-string 2 str))
4269 (archive (string= (match-string 3 str) "(archive) "))
4270 (filcat (match-string 4 str))
4271 (tpriority 1)
4272 (tpbuf (save-match-data (string-match "top" (buffer-name))))
4273 found)
4274 (setq str (replace-match "" nil nil str 4))
4275 (when tpbuf
4276 ;; Calculate priority of STR wrt its category.
4277 (save-excursion
4278 (while (search-backward filcat nil t)
4279 (setq tpriority (1+ tpriority)))))
4280 (setq file (if file
4281 (concat todos-directory (substring file 0 -1)
4282 (if archive ".toda" ".todo"))
4283 (if archive
4284 (concat (file-name-sans-extension
4285 todos-global-current-todos-file) ".toda")
4286 todos-global-current-todos-file)))
4287 (find-file-noselect file)
4288 (with-current-buffer (find-buffer-visiting file)
4289 (save-restriction
4290 (widen)
4291 (goto-char (point-min))
4292 (let ((beg (re-search-forward
4293 (concat "^" (regexp-quote (concat todos-category-beg cat))
4294 "$")
4295 nil t))
4296 (done (save-excursion
4297 (re-search-forward
4298 (concat "^" (regexp-quote todos-category-done)) nil t)))
4299 (end (save-excursion
4300 (or (re-search-forward
4301 (concat "^" (regexp-quote todos-category-beg))
4302 nil t)
4303 (point-max)))))
4304 (setq found (when (search-forward str end t)
4305 (goto-char (match-beginning 0))))
4306 (when found
4307 (setq found
4308 (cons found (if (> (point) done)
4309 'done
4310 (let ((cpriority 1))
4311 (when tpbuf
4312 (save-excursion
4313 ;; Not top item in category.
4314 (while (> (point) (1+ beg))
4315 (let ((opoint (point)))
4316 (todos-backward-item)
4317 ;; Can't move backward beyond
4318 ;; first item in file.
4319 (unless (= (point) opoint)
4320 (setq cpriority (1+ cpriority)))))))
4321 (if (and (= tpriority cpriority)
4322 ;; Proper substring is not the same.
4323 (string= (todos-item-string)
4324 str))
4325 'same
4326 'changed)))))))))
4327 (list found file cat)))
4328
4329(defun todos-check-filtered-items-file ()
4330 "Check if filtered items file is up to date and a show suitable message."
4331 ;; (catch 'old
4332 (let ((count 0))
4333 (while (not (eobp))
4334 (let* ((item (todos-item-string))
4335 (found (car (todos-find-item item))))
4336 (unless (eq (cdr found) 'same)
4337 (save-excursion
4338 (overlay-put (make-overlay (todos-item-start) (todos-item-end))
4339 'face 'todos-search))
4340 (setq count (1+ count))))
4341 ;; (throw 'old (message "The marked item is not up to date.")))
4342 (todos-forward-item))
4343 (if (zerop count)
4344 (message "Filtered items file is up to date.")
4345 (message (concat "The highlighted item" (if (= count 1) " is " "s are ")
4346 "not up to date."
4347 ;; "\nType <return> on item for details."
4348 )))))
4349
4350(defun todos-filter-items-filename ()
4351 "Return absolute file name for saving this Filtered Items buffer."
4352 (let ((bufname (buffer-name)))
4353 (string-match "\"\\([^\"]+\\)\"" bufname)
4354 (let* ((filename-str (substring bufname (match-beginning 1) (match-end 1)))
4355 (filename-base (replace-regexp-in-string ", " "-" filename-str))
4356 (top-priorities (string-match "top priorities" bufname))
4357 (diary-items (string-match "diary items" bufname))
4358 (regexp-items (string-match "regexp items" bufname)))
4359 (when regexp-items
4360 (let ((prompt (concat "Enter a short identifying string"
4361 " to make this file name unique: ")))
4362 (setq filename-base (concat filename-base "-" (read-string prompt)))))
4363 (concat todos-directory filename-base
4364 (cond (top-priorities ".todt")
4365 (diary-items ".tody")
4366 (regexp-items ".todr"))))))
4367
4368(defun todos-save-filtered-items-buffer ()
4369 "Save current Filtered Items buffer to a file.
4370If the file already exists, overwrite it only on confirmation."
4371 (let ((filename (or (buffer-file-name) (todos-filter-items-filename))))
4372 (write-file filename t)))
4373
3588;; ----------------------------------------------------------------------------- 4374;; -----------------------------------------------------------------------------
3589;;; Printing Todos Buffers 4375;;; Printing Todos buffers
3590;; ----------------------------------------------------------------------------- 4376;; -----------------------------------------------------------------------------
3591 4377
3592(defcustom todos-print-buffer-function 'ps-print-buffer-with-faces 4378(defcustom todos-print-buffer-function 'ps-print-buffer-with-faces
@@ -3653,7 +4439,7 @@ otherwise, send it to the default printer."
3653 (todos-print-buffer t)) 4439 (todos-print-buffer t))
3654 4440
3655;; ----------------------------------------------------------------------------- 4441;; -----------------------------------------------------------------------------
3656;;; Legacy Todo Mode Files 4442;;; Legacy Todo mode files
3657;; ----------------------------------------------------------------------------- 4443;; -----------------------------------------------------------------------------
3658 4444
3659(defcustom todos-todo-mode-date-time-regexp 4445(defcustom todos-todo-mode-date-time-regexp
@@ -3691,7 +4477,7 @@ saved (the latter as a Todos Archive file) with a new name in
3691`todos-directory'. See also the documentation string of 4477`todos-directory'. See also the documentation string of
3692`todos-todo-mode-date-time-regexp' for further details." 4478`todos-todo-mode-date-time-regexp' for further details."
3693 (interactive) 4479 (interactive)
3694 (require 'todo-mode) 4480 (eval-when-compile (require 'todo-mode))
3695 ;; Convert `todo-file-do'. 4481 ;; Convert `todo-file-do'.
3696 (if (file-exists-p todo-file-do) 4482 (if (file-exists-p todo-file-do)
3697 (let ((default "todo-do-conv") 4483 (let ((default "todo-do-conv")
@@ -3829,50 +4615,10 @@ saved (the latter as a Todos Archive file) with a new name in
3829 (message "Format conversion done.")) 4615 (message "Format conversion done."))
3830 (user-error "No legacy Todo file exists"))) 4616 (user-error "No legacy Todo file exists")))
3831 4617
3832;; =============================================================================
3833;;; Todos utilities and internals
3834;; =============================================================================
3835
3836(defcustom todos-y-with-space nil
3837 "Non-nil means allow SPC to affirm a \"y or n\" question."
3838 :type 'boolean
3839 :group 'todos)
3840
3841(defun todos-y-or-n-p (prompt)
3842 "Ask \"y or n\" question PROMPT and return t if answer is \"y\".
3843Also return t if answer is \"Y\", but unlike `y-or-n-p', allow
3844SPC to affirm the question only if option `todos-y-with-space' is
3845non-nil."
3846 (unless todos-y-with-space
3847 (define-key query-replace-map " " 'ignore))
3848 (prog1
3849 (y-or-n-p prompt)
3850 (define-key query-replace-map " " 'act)))
3851
3852;; ----------------------------------------------------------------------------- 4618;; -----------------------------------------------------------------------------
3853;;; File-level global variables and support functions 4619;;; Utility functions for Todos files, categories and items
3854;; ----------------------------------------------------------------------------- 4620;; -----------------------------------------------------------------------------
3855 4621
3856(defvar todos-files (funcall todos-files-function)
3857 "List of truenames of user's Todos files.")
3858
3859(defvar todos-archives (funcall todos-files-function t)
3860 "List of truenames of user's Todos archives.")
3861
3862(defvar todos-visited nil
3863 "List of Todos files visited in this session by `todos-show'.
3864Used to determine initial display according to the value of
3865`todos-show-first'.")
3866
3867(defvar todos-file-buffers nil
3868 "List of file names of live Todos mode buffers.")
3869
3870(defvar todos-global-current-todos-file nil
3871 "Variable holding name of current Todos file.
3872Used by functions called from outside of Todos mode to visit the
3873current Todos file rather than the default Todos file (i.e. when
3874users option `todos-show-current-file' is non-nil).")
3875
3876(defun todos-absolute-file-name (name &optional type) 4622(defun todos-absolute-file-name (name &optional type)
3877 "Return the absolute file name of short Todos file NAME. 4623 "Return the absolute file name of short Todos file NAME.
3878With TYPE `archive' or `top' return the absolute file name of the 4624With TYPE `archive' or `top' return the absolute file name of the
@@ -3887,95 +4633,6 @@ short Todos Archive or Top Priorities file name, respectively."
3887 ((eq type 'regexp) ".todr") 4633 ((eq type 'regexp) ".todr")
3888 (t ".todo")))))) 4634 (t ".todo"))))))
3889 4635
3890(defun todos-check-format ()
3891 "Signal an error if the current Todos file is ill-formatted.
3892Otherwise return t. Display a message if the file is well-formed
3893but the categories sexp differs from the current value of
3894`todos-categories'."
3895 (save-excursion
3896 (save-restriction
3897 (widen)
3898 (goto-char (point-min))
3899 (let* ((cats (prin1-to-string todos-categories))
3900 (ssexp (buffer-substring-no-properties (line-beginning-position)
3901 (line-end-position)))
3902 (sexp (read ssexp)))
3903 ;; Check the first line for `todos-categories' sexp.
3904 (dolist (c sexp)
3905 (let ((v (cdr c)))
3906 (unless (and (stringp (car c))
3907 (vectorp v)
3908 (= 4 (length v)))
3909 (user-error "Invalid or missing todos-categories sexp"))))
3910 (forward-line)
3911 ;; Check well-formedness of categories.
3912 (let ((legit (concat
3913 "\\(^" (regexp-quote todos-category-beg) "\\)"
3914 "\\|\\(" todos-date-string-start todos-date-pattern "\\)"
3915 "\\|\\(^[ \t]+[^ \t]*\\)"
3916 "\\|^$"
3917 "\\|\\(^" (regexp-quote todos-category-done) "\\)"
3918 "\\|\\(" todos-done-string-start "\\)")))
3919 (while (not (eobp))
3920 (unless (looking-at legit)
3921 (user-error "Illegitimate Todos file format at line %d"
3922 (line-number-at-pos (point))))
3923 (forward-line)))
3924 ;; Warn user if categories sexp has changed.
3925 (unless (string= ssexp cats)
3926 (message (concat "The sexp at the beginning of the file differs "
3927 "from the value of `todos-categories.\n"
3928 "If the sexp is wrong, you can fix it with "
3929 "M-x todos-repair-categories-sexp,\n"
3930 "but note this reverts any changes you have "
3931 "made in the order of the categories."))))))
3932 t)
3933
3934(defun todos-reevaluate-filelist-defcustoms ()
3935 "Reevaluate defcustoms that provide choice list of Todos files."
3936 (custom-set-default 'todos-default-todos-file
3937 (symbol-value 'todos-default-todos-file))
3938 (todos-reevaluate-default-file-defcustom)
3939 (custom-set-default 'todos-filter-files (symbol-value 'todos-filter-files))
3940 (todos-reevaluate-filter-files-defcustom)
3941 (custom-set-default 'todos-category-completions-files
3942 (symbol-value 'todos-category-completions-files))
3943 (todos-reevaluate-category-completions-files-defcustom))
3944
3945(defun todos-reevaluate-default-file-defcustom ()
3946 "Reevaluate defcustom of `todos-default-todos-file'.
3947Called after adding or deleting a Todos file."
3948 (eval (defcustom todos-default-todos-file (car (funcall todos-files-function))
3949 "Todos file visited by first session invocation of `todos-show'."
3950 :type `(radio ,@(mapcar (lambda (f) (list 'const f))
3951 (mapcar 'todos-short-file-name
3952 (funcall todos-files-function))))
3953 :group 'todos)))
3954
3955(defun todos-reevaluate-category-completions-files-defcustom ()
3956 "Reevaluate defcustom of `todos-category-completions-files'.
3957Called after adding or deleting a Todos file."
3958 (eval (defcustom todos-category-completions-files nil
3959 "List of files for building `todos-read-category' completions."
3960 :type `(set ,@(mapcar (lambda (f) (list 'const f))
3961 (mapcar 'todos-short-file-name
3962 (funcall todos-files-function))))
3963 :group 'todos)))
3964
3965(defun todos-reevaluate-filter-files-defcustom ()
3966 "Reevaluate defcustom of `todos-filter-files'.
3967Called after adding or deleting a Todos file."
3968 (eval (defcustom todos-filter-files nil
3969 "List of files for multifile item filtering."
3970 :type `(set ,@(mapcar (lambda (f) (list 'const f))
3971 (mapcar 'todos-short-file-name
3972 (funcall todos-files-function))))
3973 :group 'todos)))
3974
3975;; -----------------------------------------------------------------------------
3976;;; Category-level global variables and support functions
3977;; -----------------------------------------------------------------------------
3978
3979(defun todos-category-number (cat) 4636(defun todos-category-number (cat)
3980 "Return the number of category CAT in this Todos file. 4637 "Return the number of category CAT in this Todos file.
3981The buffer-local variable `todos-category-number' holds this 4638The buffer-local variable `todos-category-number' holds this
@@ -4039,52 +4696,6 @@ number as its value."
4039 (require 'hl-line) 4696 (require 'hl-line)
4040 (hl-line-mode 1))))) 4697 (hl-line-mode 1)))))
4041 4698
4042(defconst todos-category-beg "--==-- "
4043 "String marking beginning of category (inserted with its name).")
4044
4045(defconst todos-category-done "==--== DONE "
4046 "String marking beginning of category's done items.")
4047
4048(defun todos-done-separator ()
4049 "Return string used as value of variable `todos-done-separator'."
4050 (let ((sep todos-done-separator-string))
4051 (propertize (if (= 1 (length sep))
4052 ;; Until bug#2749 is fixed, if separator's length
4053 ;; is window-width and todos-wrap-lines is
4054 ;; non-nil, an indented empty line appears between
4055 ;; the separator and the first done item.
4056 ;; (make-string (window-width) (string-to-char sep))
4057 (make-string (1- (window-width)) (string-to-char sep))
4058 todos-done-separator-string)
4059 'face 'todos-done-sep)))
4060
4061(defvar todos-done-separator (todos-done-separator)
4062 "String used to visually separate done from not done items.
4063Displayed as an overlay instead of `todos-category-done' when
4064done items are shown. Its value is determined by user option
4065`todos-done-separator-string'.")
4066
4067(defun todos-reset-done-separator (sep)
4068 "Replace existing overlays of done items separator string SEP."
4069 (save-excursion
4070 (save-restriction
4071 (widen)
4072 (goto-char (point-min))
4073 (while (re-search-forward
4074 (concat "\n\\(" (regexp-quote todos-category-done) "\\)") nil t)
4075 (let* ((beg (match-beginning 1))
4076 (end (match-end 0))
4077 (ov (progn (goto-char beg)
4078 (todos-get-overlay 'separator)))
4079 (old-sep (when ov (overlay-get ov 'display)))
4080 new-ov)
4081 (when old-sep
4082 (unless (string= old-sep sep)
4083 (setq new-ov (make-overlay beg end))
4084 (overlay-put new-ov 'todos 'separator)
4085 (overlay-put new-ov 'display todos-done-separator)
4086 (delete-overlay ov))))))))
4087
4088(defun todos-get-count (type &optional category) 4699(defun todos-get-count (type &optional category)
4089 "Return count of TYPE items in CATEGORY. 4700 "Return count of TYPE items in CATEGORY.
4090If CATEGORY is nil, default to the current category." 4701If CATEGORY is nil, default to the current category."
@@ -4219,56 +4830,49 @@ changes made in Todos Categories mode will have to be made again."
4219 (let ((todos-categories (todos-make-categories-list t))) 4830 (let ((todos-categories (todos-make-categories-list t)))
4220 (todos-update-categories-sexp))) 4831 (todos-update-categories-sexp)))
4221 4832
4222;; ----------------------------------------------------------------------------- 4833(defun todos-check-format ()
4223;;; Item-level global variables and support functions 4834 "Signal an error if the current Todos file is ill-formatted.
4224;; ----------------------------------------------------------------------------- 4835Otherwise return t. Display a message if the file is well-formed
4225 4836but the categories sexp differs from the current value of
4226(defconst todos-month-name-array 4837`todos-categories'."
4227 (vconcat calendar-month-name-array (vector "*")) 4838 (save-excursion
4228 "Array of month names, in order. 4839 (save-restriction
4229The final element is \"*\", indicating an unspecified month.") 4840 (widen)
4230 4841 (goto-char (point-min))
4231(defconst todos-month-abbrev-array 4842 (let* ((cats (prin1-to-string todos-categories))
4232 (vconcat calendar-month-abbrev-array (vector "*")) 4843 (ssexp (buffer-substring-no-properties (line-beginning-position)
4233 "Array of abbreviated month names, in order. 4844 (line-end-position)))
4234The final element is \"*\", indicating an unspecified month.") 4845 (sexp (read ssexp)))
4235 4846 ;; Check the first line for `todos-categories' sexp.
4236(defconst todos-date-pattern 4847 (dolist (c sexp)
4237 (let ((dayname (diary-name-pattern calendar-day-name-array nil t))) 4848 (let ((v (cdr c)))
4238 (concat "\\(?5:" dayname "\\|" 4849 (unless (and (stringp (car c))
4239 (let ((dayname) 4850 (vectorp v)
4240 (monthname (format "\\(?6:%s\\)" (diary-name-pattern 4851 (= 4 (length v)))
4241 todos-month-name-array 4852 (user-error "Invalid or missing todos-categories sexp"))))
4242 todos-month-abbrev-array))) 4853 (forward-line)
4243 (month "\\(?7:[0-9]+\\|\\*\\)") 4854 ;; Check well-formedness of categories.
4244 (day "\\(?8:[0-9]+\\|\\*\\)") 4855 (let ((legit (concat
4245 (year "-?\\(?9:[0-9]+\\|\\*\\)")) 4856 "\\(^" (regexp-quote todos-category-beg) "\\)"
4246 (mapconcat 'eval calendar-date-display-form "")) 4857 "\\|\\(" todos-date-string-start todos-date-pattern "\\)"
4247 "\\)")) 4858 "\\|\\(^[ \t]+[^ \t]*\\)"
4248 "Regular expression matching a Todos date header.") 4859 "\\|^$"
4249 4860 "\\|\\(^" (regexp-quote todos-category-done) "\\)"
4250(defconst todos-nondiary-start (nth 0 todos-nondiary-marker) 4861 "\\|\\(" todos-done-string-start "\\)")))
4251 "String inserted before item date to block diary inclusion.") 4862 (while (not (eobp))
4252 4863 (unless (looking-at legit)
4253(defconst todos-nondiary-end (nth 1 todos-nondiary-marker) 4864 (user-error "Illegitimate Todos file format at line %d"
4254 "String inserted after item date matching `todos-nondiary-start'.") 4865 (line-number-at-pos (point))))
4255 4866 (forward-line)))
4256;; By itself this matches anything, because of the `?'; however, it's only 4867 ;; Warn user if categories sexp has changed.
4257;; used in the context of `todos-date-pattern' (but Emacs Lisp lacks 4868 (unless (string= ssexp cats)
4258;; lookahead). 4869 (message (concat "The sexp at the beginning of the file differs "
4259(defconst todos-date-string-start 4870 "from the value of `todos-categories.\n"
4260 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|" 4871 "If the sexp is wrong, you can fix it with "
4261 (regexp-quote diary-nonmarking-symbol) "\\)?") 4872 "M-x todos-repair-categories-sexp,\n"
4262 "Regular expression matching part of item header before the date.") 4873 "but note this reverts any changes you have "
4263 4874 "made in the order of the categories."))))))
4264(defconst todos-done-string-start 4875 t)
4265 (concat "^\\[" (regexp-quote todos-done-string))
4266 "Regular expression matching start of done item.")
4267
4268(defconst todos-item-start (concat "\\(" todos-date-string-start "\\|"
4269 todos-done-string-start "\\)"
4270 todos-date-pattern)
4271 "String identifying start of a Todos item.")
4272 4876
4273(defun todos-item-start () 4877(defun todos-item-start ()
4274 "Move to start of current Todos item and return its position." 4878 "Move to start of current Todos item and return its position."
@@ -4314,7 +4918,10 @@ The final element is \"*\", indicating an unspecified month.")
4314 (if to-lim lim (point-max)))) 4918 (if to-lim lim (point-max))))
4315 ;; For last todo item, skip back over the empty line before the done 4919 ;; For last todo item, skip back over the empty line before the done
4316 ;; items section, else just back to the end of the previous line. 4920 ;; items section, else just back to the end of the previous line.
4317 (backward-char (when (and to-lim (not done) (eq (point) lim)) 2)) 4921 ;; (When byte-comiled, backward-char barfs on an argument that evaluates
4922 ;; to nil (bug#14565).)
4923 ;; (backward-char (when (and to-lim (not done) (eq (point) lim)) 2))
4924 (backward-char (if (and to-lim (not done) (eq (point) lim)) 2 1))
4318 (point)))) 4925 (point))))
4319 4926
4320(defun todos-item-string () 4927(defun todos-item-string ()
@@ -4422,6 +5029,27 @@ Helper function for `diary-goto-entry'."
4422 (progn (goto-char (point-min)) 5029 (progn (goto-char (point-min))
4423 (looking-at todos-done-string-start))))) 5030 (looking-at todos-done-string-start)))))
4424 5031
5032(defun todos-reset-done-separator (sep)
5033 "Replace existing overlays of done items separator string SEP."
5034 (save-excursion
5035 (save-restriction
5036 (widen)
5037 (goto-char (point-min))
5038 (while (re-search-forward
5039 (concat "\n\\(" (regexp-quote todos-category-done) "\\)") nil t)
5040 (let* ((beg (match-beginning 1))
5041 (end (match-end 0))
5042 (ov (progn (goto-char beg)
5043 (todos-get-overlay 'separator)))
5044 (old-sep (when ov (overlay-get ov 'display)))
5045 new-ov)
5046 (when old-sep
5047 (unless (string= old-sep sep)
5048 (setq new-ov (make-overlay beg end))
5049 (overlay-put new-ov 'todos 'separator)
5050 (overlay-put new-ov 'display todos-done-separator)
5051 (delete-overlay ov))))))))
5052
4425(defun todos-get-overlay (val) 5053(defun todos-get-overlay (val)
4426 "Return the overlay at point whose `todos' property has value VAL." 5054 "Return the overlay at point whose `todos' property has value VAL."
4427 ;; Use overlays-in to find prefix overlays and check over two 5055 ;; Use overlays-in to find prefix overlays and check over two
@@ -4514,7 +5142,7 @@ of each other."
4514 (forward-line))))) 5142 (forward-line)))))
4515 5143
4516;; ----------------------------------------------------------------------------- 5144;; -----------------------------------------------------------------------------
4517;;; Generation of item insertion commands and key bindings 5145;;; Utilities for generating item insertion commands and key bindings
4518;; ----------------------------------------------------------------------------- 5146;; -----------------------------------------------------------------------------
4519 5147
4520;; These two powerset definitions are adaptations of code published at 5148;; These two powerset definitions are adaptations of code published at
@@ -4671,9 +5299,25 @@ their relation to key bindings, see `todos-basic-insert-item'."
4671 (define-key map key c)))) 5299 (define-key map key c))))
4672 5300
4673;; ----------------------------------------------------------------------------- 5301;; -----------------------------------------------------------------------------
4674;;; Todos minibuffer completion 5302;;; Todos minibuffer utilities
4675;; ----------------------------------------------------------------------------- 5303;; -----------------------------------------------------------------------------
4676 5304
5305(defcustom todos-y-with-space nil
5306 "Non-nil means allow SPC to affirm a \"y or n\" question."
5307 :type 'boolean
5308 :group 'todos)
5309
5310(defun todos-y-or-n-p (prompt)
5311 "Ask \"y or n\" question PROMPT and return t if answer is \"y\".
5312Also return t if answer is \"Y\", but unlike `y-or-n-p', allow
5313SPC to affirm the question only if option `todos-y-with-space' is
5314non-nil."
5315 (unless todos-y-with-space
5316 (define-key query-replace-map " " 'ignore))
5317 (prog1
5318 (y-or-n-p prompt)
5319 (define-key query-replace-map " " 'act)))
5320
4677(defun todos-category-completions (&optional archive) 5321(defun todos-category-completions (&optional archive)
4678 "Return a list of completions for `todos-read-category'. 5322 "Return a list of completions for `todos-read-category'.
4679Each element of the list is a cons of a category name and the 5323Each element of the list is a cons of a category name and the
@@ -4996,719 +5640,7 @@ the empty string (i.e., no time string)."
4996 answer)) 5640 answer))
4997 5641
4998;; ----------------------------------------------------------------------------- 5642;; -----------------------------------------------------------------------------
4999;;; Todos Categories mode tabulation and sorting 5643;;; Customization groups and utilities
5000;; -----------------------------------------------------------------------------
5001
5002(defvar todos-categories-buffer "*Todos Categories*"
5003 "Name of buffer in Todos Categories mode.")
5004
5005(defun todos-longest-category-name-length (categories)
5006 "Return the length of the longest name in list CATEGORIES."
5007 (let ((longest 0))
5008 (dolist (c categories longest)
5009 (setq longest (max longest (length c))))))
5010
5011(defun todos-adjusted-category-label-length ()
5012 "Return adjusted length of category label button.
5013The adjustment ensures proper tabular alignment in Todos
5014Categories mode."
5015 (let* ((categories (mapcar 'car todos-categories))
5016 (longest (todos-longest-category-name-length categories))
5017 (catlablen (length todos-categories-category-label))
5018 (lc-diff (- longest catlablen)))
5019 (if (and (natnump lc-diff) (cl-oddp lc-diff))
5020 (1+ longest)
5021 (max longest catlablen))))
5022
5023(defun todos-padded-string (str)
5024 "Return category name or label string STR padded with spaces.
5025The placement of the padding is determined by the value of user
5026option `todos-categories-align'."
5027 (let* ((len (todos-adjusted-category-label-length))
5028 (strlen (length str))
5029 (strlen-odd (eq (logand strlen 1) 1))
5030 (padding (max 0 (/ (- len strlen) 2)))
5031 (padding-left (cond ((eq todos-categories-align 'left) 0)
5032 ((eq todos-categories-align 'center) padding)
5033 ((eq todos-categories-align 'right)
5034 (if strlen-odd (1+ (* padding 2)) (* padding 2)))))
5035 (padding-right (cond ((eq todos-categories-align 'left)
5036 (if strlen-odd (1+ (* padding 2)) (* padding 2)))
5037 ((eq todos-categories-align 'center)
5038 (if strlen-odd (1+ padding) padding))
5039 ((eq todos-categories-align 'right) 0))))
5040 (concat (make-string padding-left 32) str (make-string padding-right 32))))
5041
5042(defvar todos-descending-counts nil
5043 "List of keys for category counts sorted in descending order.")
5044
5045(defun todos-sort (list &optional key)
5046 "Return a copy of LIST, possibly sorted according to KEY."
5047 (let* ((l (copy-sequence list))
5048 (fn (if (eq key 'alpha)
5049 (lambda (x) (upcase x)) ; Alphabetize case insensitively.
5050 (lambda (x) (todos-get-count key x))))
5051 ;; Keep track of whether the last sort by key was descending or
5052 ;; ascending.
5053 (descending (member key todos-descending-counts))
5054 (cmp (if (eq key 'alpha)
5055 'string<
5056 (if descending '< '>)))
5057 (pred (lambda (s1 s2) (let ((t1 (funcall fn (car s1)))
5058 (t2 (funcall fn (car s2))))
5059 (funcall cmp t1 t2)))))
5060 (when key
5061 (setq l (sort l pred))
5062 ;; Switch between descending and ascending sort order.
5063 (if descending
5064 (setq todos-descending-counts
5065 (delete key todos-descending-counts))
5066 (push key todos-descending-counts)))
5067 l))
5068
5069(defun todos-display-sorted (type)
5070 "Keep point on the TYPE count sorting button just clicked."
5071 (let ((opoint (point)))
5072 (todos-update-categories-display type)
5073 (goto-char opoint)))
5074
5075(defun todos-label-to-key (label)
5076 "Return symbol for sort key associated with LABEL."
5077 (let (key)
5078 (cond ((string= label todos-categories-category-label)
5079 (setq key 'alpha))
5080 ((string= label todos-categories-todo-label)
5081 (setq key 'todo))
5082 ((string= label todos-categories-diary-label)
5083 (setq key 'diary))
5084 ((string= label todos-categories-done-label)
5085 (setq key 'done))
5086 ((string= label todos-categories-archived-label)
5087 (setq key 'archived)))
5088 key))
5089
5090(defun todos-insert-sort-button (label)
5091 "Insert button for displaying categories sorted by item counts.
5092LABEL determines which type of count is sorted."
5093 (let* ((str (if (string= label todos-categories-category-label)
5094 (todos-padded-string label)
5095 label))
5096 (beg (point))
5097 (end (+ beg (length str)))
5098 ov)
5099 (insert-button str 'face nil
5100 'action
5101 `(lambda (button)
5102 (let ((key (todos-label-to-key ,label)))
5103 (if (and (member key todos-descending-counts)
5104 (eq key 'alpha))
5105 (progn
5106 ;; If display is alphabetical, switch back to
5107 ;; category priority order.
5108 (todos-display-sorted nil)
5109 (setq todos-descending-counts
5110 (delete key todos-descending-counts)))
5111 (todos-display-sorted key)))))
5112 (setq ov (make-overlay beg end))
5113 (overlay-put ov 'face 'todos-button)))
5114
5115(defun todos-total-item-counts ()
5116 "Return a list of total item counts for the current file."
5117 (mapcar (lambda (i) (apply '+ (mapcar (lambda (l) (aref l i))
5118 (mapcar 'cdr todos-categories))))
5119 (list 0 1 2 3)))
5120
5121(defvar todos-categories-category-number 0
5122 "Variable for numbering categories in Todos Categories mode.")
5123
5124(defun todos-insert-category-line (cat &optional nonum)
5125 "Insert button with category CAT's name and item counts.
5126With non-nil argument NONUM show only these; otherwise, insert a
5127number in front of the button indicating the category's priority.
5128The number and the category name are separated by the string
5129which is the value of the user option
5130`todos-categories-number-separator'."
5131 (let ((archive (member todos-current-todos-file todos-archives))
5132 (num todos-categories-category-number)
5133 (str (todos-padded-string cat))
5134 (opoint (point)))
5135 (setq num (1+ num) todos-categories-category-number num)
5136 (insert-button
5137 (concat (if nonum
5138 (make-string (+ 4 (length todos-categories-number-separator))
5139 32)
5140 (format " %3d%s" num todos-categories-number-separator))
5141 str
5142 (mapconcat (lambda (elt)
5143 (concat
5144 (make-string (1+ (/ (length (car elt)) 2)) 32) ; label
5145 (format "%3d" (todos-get-count (cdr elt) cat)) ; count
5146 ;; Add an extra space if label length is odd.
5147 (when (cl-oddp (length (car elt))) " ")))
5148 (if archive
5149 (list (cons todos-categories-done-label 'done))
5150 (list (cons todos-categories-todo-label 'todo)
5151 (cons todos-categories-diary-label 'diary)
5152 (cons todos-categories-done-label 'done)
5153 (cons todos-categories-archived-label
5154 'archived)))
5155 "")
5156 " ") ; Make highlighting on last column look better.
5157 'face (if (and todos-skip-archived-categories
5158 (zerop (todos-get-count 'todo cat))
5159 (zerop (todos-get-count 'done cat))
5160 (not (zerop (todos-get-count 'archived cat))))
5161 'todos-archived-only
5162 nil)
5163 'action `(lambda (button) (let ((buf (current-buffer)))
5164 (todos-jump-to-category nil ,cat)
5165 (kill-buffer buf))))
5166 ;; Highlight the sorted count column.
5167 (let* ((beg (+ opoint 7 (length str)))
5168 end ovl)
5169 (cond ((eq nonum 'todo)
5170 (setq beg (+ beg 1 (/ (length todos-categories-todo-label) 2))))
5171 ((eq nonum 'diary)
5172 (setq beg (+ beg 1 (length todos-categories-todo-label)
5173 2 (/ (length todos-categories-diary-label) 2))))
5174 ((eq nonum 'done)
5175 (setq beg (+ beg 1 (length todos-categories-todo-label)
5176 2 (length todos-categories-diary-label)
5177 2 (/ (length todos-categories-done-label) 2))))
5178 ((eq nonum 'archived)
5179 (setq beg (+ beg 1 (length todos-categories-todo-label)
5180 2 (length todos-categories-diary-label)
5181 2 (length todos-categories-done-label)
5182 2 (/ (length todos-categories-archived-label) 2)))))
5183 (unless (= beg (+ opoint 7 (length str))) ; Don't highlight categories.
5184 (setq end (+ beg 4))
5185 (setq ovl (make-overlay beg end))
5186 (overlay-put ovl 'face 'todos-sorted-column)))
5187 (newline)))
5188
5189(defun todos-display-categories ()
5190 "Prepare buffer for displaying table of categories and item counts."
5191 (unless (eq major-mode 'todos-categories-mode)
5192 (setq todos-global-current-todos-file
5193 (or todos-current-todos-file
5194 (todos-absolute-file-name todos-default-todos-file)))
5195 (set-window-buffer (selected-window)
5196 (set-buffer (get-buffer-create todos-categories-buffer)))
5197 (kill-all-local-variables)
5198 (todos-categories-mode)
5199 (let ((archive (member todos-current-todos-file todos-archives))
5200 buffer-read-only)
5201 (erase-buffer)
5202 (insert (format (concat "Category counts for Todos "
5203 (if archive "archive" "file")
5204 " \"%s\".")
5205 (todos-short-file-name todos-current-todos-file)))
5206 (newline 2)
5207 ;; Make space for the column of category numbers.
5208 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32))
5209 ;; Add the category and item count buttons (if this is the list of
5210 ;; categories in an archive, show only done item counts).
5211 (todos-insert-sort-button todos-categories-category-label)
5212 (if archive
5213 (progn
5214 (insert (make-string 3 32))
5215 (todos-insert-sort-button todos-categories-done-label))
5216 (insert (make-string 3 32))
5217 (todos-insert-sort-button todos-categories-todo-label)
5218 (insert (make-string 2 32))
5219 (todos-insert-sort-button todos-categories-diary-label)
5220 (insert (make-string 2 32))
5221 (todos-insert-sort-button todos-categories-done-label)
5222 (insert (make-string 2 32))
5223 (todos-insert-sort-button todos-categories-archived-label))
5224 (newline 2))))
5225
5226(defun todos-update-categories-display (sortkey)
5227 "Populate table of categories and sort by SORTKEY."
5228 (let* ((cats0 todos-categories)
5229 (cats (todos-sort cats0 sortkey))
5230 (archive (member todos-current-todos-file todos-archives))
5231 (todos-categories-category-number 0)
5232 ;; Find start of Category button if we just entered Todos Categories
5233 ;; mode.
5234 (pt (if (eq (point) (point-max))
5235 (save-excursion
5236 (forward-line -2)
5237 (goto-char (next-single-char-property-change
5238 (point) 'face nil (line-end-position))))))
5239 (buffer-read-only))
5240 (forward-line 2)
5241 (delete-region (point) (point-max))
5242 ;; Fill in the table with buttonized lines, each showing a category and
5243 ;; its item counts.
5244 (mapc (lambda (cat) (todos-insert-category-line cat sortkey))
5245 (mapcar 'car cats))
5246 (newline)
5247 ;; Add a line showing item count totals.
5248 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32)
5249 (todos-padded-string todos-categories-totals-label)
5250 (mapconcat
5251 (lambda (elt)
5252 (concat
5253 (make-string (1+ (/ (length (car elt)) 2)) 32)
5254 (format "%3d" (nth (cdr elt) (todos-total-item-counts)))
5255 ;; Add an extra space if label length is odd.
5256 (when (cl-oddp (length (car elt))) " ")))
5257 (if archive
5258 (list (cons todos-categories-done-label 2))
5259 (list (cons todos-categories-todo-label 0)
5260 (cons todos-categories-diary-label 1)
5261 (cons todos-categories-done-label 2)
5262 (cons todos-categories-archived-label 3)))
5263 ""))
5264 ;; Put cursor on Category button initially.
5265 (if pt (goto-char pt))
5266 (setq buffer-read-only t)))
5267
5268;; -----------------------------------------------------------------------------
5269;;; Item filtering selection and display
5270;; -----------------------------------------------------------------------------
5271
5272(defvar todos-multiple-filter-files nil
5273 "List of files selected from `todos-multiple-filter-files' widget.")
5274
5275(defvar todos-multiple-filter-files-widget nil
5276 "Variable holding widget created by `todos-multiple-filter-files'.")
5277
5278(defun todos-multiple-filter-files ()
5279 "Pop to a buffer with a widget for choosing multiple filter files."
5280 (require 'widget)
5281 (eval-when-compile
5282 (require 'wid-edit))
5283 (with-current-buffer (get-buffer-create "*Todos Filter Files*")
5284 (pop-to-buffer (current-buffer))
5285 (erase-buffer)
5286 (kill-all-local-variables)
5287 (widget-insert "Select files for generating the top priorities list.\n\n")
5288 (setq todos-multiple-filter-files-widget
5289 (widget-create
5290 `(set ,@(mapcar (lambda (x) (list 'const x))
5291 (mapcar 'todos-short-file-name
5292 (funcall todos-files-function))))))
5293 (widget-insert "\n")
5294 (widget-create 'push-button
5295 :notify (lambda (widget &rest ignore)
5296 (setq todos-multiple-filter-files 'quit)
5297 (quit-window t)
5298 (exit-recursive-edit))
5299 "Cancel")
5300 (widget-insert " ")
5301 (widget-create 'push-button
5302 :notify (lambda (&rest ignore)
5303 (setq todos-multiple-filter-files
5304 (mapcar (lambda (f)
5305 (file-truename
5306 (concat todos-directory
5307 f ".todo")))
5308 (widget-value
5309 todos-multiple-filter-files-widget)))
5310 (quit-window t)
5311 (exit-recursive-edit))
5312 "Apply")
5313 (use-local-map widget-keymap)
5314 (widget-setup))
5315 (message "Click \"Apply\" after selecting files.")
5316 (recursive-edit))
5317
5318(defun todos-filter-items (filter &optional new multifile)
5319 "Display a cross-categorial list of items filtered by FILTER.
5320The values of FILTER can be `top' for top priority items, a cons
5321of `top' and a number passed by the caller, `diary' for diary
5322items, or `regexp' for items matching a regular expresion entered
5323by the user. The items can be from any categories in the current
5324todo file or, with non-nil MULTIFILE, from several files. If NEW
5325is nil, visit an appropriate file containing the list of filtered
5326items; if there is no such file, or with non-nil NEW, build the
5327list and display it.
5328
5329See the document strings of the commands
5330`todos-filter-top-priorities', `todos-filter-diary-items',
5331`todos-filter-regexp-items', and those of the corresponding
5332multifile commands for further details."
5333 (let* ((top (eq filter 'top))
5334 (diary (eq filter 'diary))
5335 (regexp (eq filter 'regexp))
5336 (buf (cond (top todos-top-priorities-buffer)
5337 (diary todos-diary-items-buffer)
5338 (regexp todos-regexp-items-buffer)))
5339 (flist (if multifile
5340 (or todos-filter-files
5341 (progn (todos-multiple-filter-files)
5342 todos-multiple-filter-files))
5343 (list todos-current-todos-file)))
5344 (multi (> (length flist) 1))
5345 (fname (if (equal flist 'quit)
5346 ;; Pressed `cancel' in t-m-f-f file selection dialog.
5347 (keyboard-quit)
5348 (concat todos-directory
5349 (mapconcat 'todos-short-file-name flist "-")
5350 (cond (top ".todt")
5351 (diary ".tody")
5352 (regexp ".todr")))))
5353 (rxfiles (when regexp
5354 (directory-files todos-directory t ".*\\.todr$" t)))
5355 (file-exists (or (file-exists-p fname) rxfiles)))
5356 (cond ((and top new (natnump new))
5357 (todos-filter-items-1 (cons 'top new) flist))
5358 ((and (not new) file-exists)
5359 (when (and rxfiles (> (length rxfiles) 1))
5360 (let ((rxf (mapcar 'todos-short-file-name rxfiles)))
5361 (setq fname (todos-absolute-file-name
5362 (completing-read "Choose a regexp items file: "
5363 rxf) 'regexp))))
5364 (find-file fname)
5365 (todos-prefix-overlays)
5366 (todos-check-filtered-items-file))
5367 (t
5368 (todos-filter-items-1 filter flist)))
5369 (setq fname (replace-regexp-in-string "-" ", "
5370 (todos-short-file-name fname)))
5371 (rename-buffer (format (concat "%s for file" (if multi "s" "")
5372 " \"%s\"") buf fname))))
5373
5374(defun todos-filter-items-1 (filter file-list)
5375 "Build a list of items by applying FILTER to FILE-LIST.
5376Internal subroutine called by `todos-filter-items', which passes
5377the values of FILTER and FILE-LIST."
5378 (let ((num (if (consp filter) (cdr filter) todos-top-priorities))
5379 (buf (get-buffer-create todos-filtered-items-buffer))
5380 (multifile (> (length file-list) 1))
5381 regexp fname bufstr cat beg end done)
5382 (if (null file-list)
5383 (user-error "No files have been chosen for filtering")
5384 (with-current-buffer buf
5385 (erase-buffer)
5386 (kill-all-local-variables)
5387 (todos-filtered-items-mode))
5388 (when (eq filter 'regexp)
5389 (setq regexp (read-string "Enter a regular expression: ")))
5390 (save-current-buffer
5391 (dolist (f file-list)
5392 ;; Before inserting file contents into temp buffer, save a modified
5393 ;; buffer visiting it.
5394 (let ((bf (find-buffer-visiting f)))
5395 (when (buffer-modified-p bf)
5396 (with-current-buffer bf (save-buffer))))
5397 (setq fname (todos-short-file-name f))
5398 (with-temp-buffer
5399 (when (and todos-filter-done-items (eq filter 'regexp))
5400 ;; If there is a corresponding archive file for the
5401 ;; Todos file, insert it first and add identifiers for
5402 ;; todos-go-to-source-item.
5403 (let ((arch (concat (file-name-sans-extension f) ".toda")))
5404 (when (file-exists-p arch)
5405 (insert-file-contents arch)
5406 ;; Delete Todos archive file categories sexp.
5407 (delete-region (line-beginning-position)
5408 (1+ (line-end-position)))
5409 (save-excursion
5410 (while (not (eobp))
5411 (when (re-search-forward
5412 (concat (if todos-filter-done-items
5413 (concat "\\(?:" todos-done-string-start
5414 "\\|" todos-date-string-start
5415 "\\)")
5416 todos-date-string-start)
5417 todos-date-pattern "\\(?: "
5418 diary-time-regexp "\\)?"
5419 (if todos-filter-done-items
5420 "\\]"
5421 (regexp-quote todos-nondiary-end)) "?")
5422 nil t)
5423 (insert "(archive) "))
5424 (forward-line))))))
5425 (insert-file-contents f)
5426 ;; Delete Todos file categories sexp.
5427 (delete-region (line-beginning-position) (1+ (line-end-position)))
5428 (let (fnum)
5429 ;; Unless the number of top priorities to show was
5430 ;; passed by the caller, the file-wide value from
5431 ;; `todos-top-priorities-overrides', if non-nil, overrides
5432 ;; `todos-top-priorities'.
5433 (unless (consp filter)
5434 (setq fnum (or (nth 1 (assoc f todos-top-priorities-overrides))
5435 todos-top-priorities)))
5436 (while (re-search-forward
5437 (concat "^" (regexp-quote todos-category-beg)
5438 "\\(.+\\)\n") nil t)
5439 (setq cat (match-string 1))
5440 (let (cnum)
5441 ;; Unless the number of top priorities to show was
5442 ;; passed by the caller, the category-wide value
5443 ;; from `todos-top-priorities-overrides', if non-nil,
5444 ;; overrides a non-nil file-wide value from
5445 ;; `todos-top-priorities-overrides' as well as
5446 ;; `todos-top-priorities'.
5447 (unless (consp filter)
5448 (let ((cats (nth 2 (assoc f todos-top-priorities-overrides))))
5449 (setq cnum (or (cdr (assoc cat cats)) fnum))))
5450 (delete-region (match-beginning 0) (match-end 0))
5451 (setq beg (point)) ; First item in the current category.
5452 (setq end (if (re-search-forward
5453 (concat "^" (regexp-quote todos-category-beg))
5454 nil t)
5455 (match-beginning 0)
5456 (point-max)))
5457 (goto-char beg)
5458 (setq done
5459 (if (re-search-forward
5460 (concat "\n" (regexp-quote todos-category-done))
5461 end t)
5462 (match-beginning 0)
5463 end))
5464 (unless (and todos-filter-done-items (eq filter 'regexp))
5465 ;; Leave done items.
5466 (delete-region done end)
5467 (setq end done))
5468 (narrow-to-region beg end) ; Process only current category.
5469 (goto-char (point-min))
5470 ;; Apply the filter.
5471 (cond ((eq filter 'diary)
5472 (while (not (eobp))
5473 (if (looking-at (regexp-quote todos-nondiary-start))
5474 (todos-remove-item)
5475 (todos-forward-item))))
5476 ((eq filter 'regexp)
5477 (while (not (eobp))
5478 (if (looking-at todos-item-start)
5479 (if (string-match regexp (todos-item-string))
5480 (todos-forward-item)
5481 (todos-remove-item))
5482 ;; Kill lines that aren't part of a todo or done
5483 ;; item (empty or todos-category-done).
5484 (delete-region (line-beginning-position)
5485 (1+ (line-end-position))))
5486 ;; If last todo item in file matches regexp and
5487 ;; there are no following done items,
5488 ;; todos-category-done string is left dangling,
5489 ;; because todos-forward-item jumps over it.
5490 (if (and (eobp)
5491 (looking-back
5492 (concat (regexp-quote todos-done-string)
5493 "\n")))
5494 (delete-region (point) (progn
5495 (forward-line -2)
5496 (point))))))
5497 (t ; Filter top priority items.
5498 (setq num (or cnum fnum num))
5499 (unless (zerop num)
5500 (todos-forward-item num))))
5501 (setq beg (point))
5502 ;; Delete non-top-priority items.
5503 (unless (member filter '(diary regexp))
5504 (delete-region beg end))
5505 (goto-char (point-min))
5506 ;; Add file (if using multiple files) and category tags to
5507 ;; item.
5508 (while (not (eobp))
5509 (when (re-search-forward
5510 (concat (if todos-filter-done-items
5511 (concat "\\(?:" todos-done-string-start
5512 "\\|" todos-date-string-start
5513 "\\)")
5514 todos-date-string-start)
5515 todos-date-pattern "\\(?: " diary-time-regexp
5516 "\\)?" (if todos-filter-done-items
5517 "\\]"
5518 (regexp-quote todos-nondiary-end))
5519 "?")
5520 nil t)
5521 (insert " [")
5522 (when (looking-at "(archive) ") (goto-char (match-end 0)))
5523 (insert (if multifile (concat fname ":") "") cat "]"))
5524 (forward-line))
5525 (widen)))
5526 (setq bufstr (buffer-string))
5527 (with-current-buffer buf
5528 (let (buffer-read-only)
5529 (insert bufstr)))))))
5530 (set-window-buffer (selected-window) (set-buffer buf))
5531 (todos-prefix-overlays)
5532 (goto-char (point-min)))))
5533
5534(defun todos-set-top-priorities (&optional arg)
5535 "Set number of top priorities shown by `todos-filter-top-priorities'.
5536With non-nil ARG, set the number only for the current Todos
5537category; otherwise, set the number for all categories in the
5538current Todos file.
5539
5540Calling this function via either of the commands
5541`todos-set-top-priorities-in-file' or
5542`todos-set-top-priorities-in-category' is the recommended way to
5543set the user customizable option `todos-top-priorities-overrides'."
5544 (let* ((cat (todos-current-category))
5545 (file todos-current-todos-file)
5546 (rules todos-top-priorities-overrides)
5547 (frule (assoc-string file rules))
5548 (crule (assoc-string cat (nth 2 frule)))
5549 (crules (nth 2 frule))
5550 (cur (or (if arg (cdr crule) (nth 1 frule))
5551 todos-top-priorities))
5552 (prompt (if arg (concat "Number of top priorities in this category"
5553 " (currently %d): ")
5554 (concat "Default number of top priorities per category"
5555 " in this file (currently %d): ")))
5556 (new -1)
5557 nrule)
5558 (while (< new 0)
5559 (let ((cur0 cur))
5560 (setq new (read-number (format prompt cur0))
5561 prompt "Enter a non-negative number: "
5562 cur0 nil)))
5563 (setq nrule (if arg
5564 (append (delete crule crules) (list (cons cat new)))
5565 (append (list file new) (list crules))))
5566 (setq rules (cons (if arg
5567 (list file cur nrule)
5568 nrule)
5569 (delete frule rules)))
5570 (customize-save-variable 'todos-top-priorities-overrides rules)
5571 (todos-prefix-overlays)))
5572
5573(defconst todos-filtered-items-buffer "Todos filtered items"
5574 "Initial name of buffer in Todos Filter Items mode.")
5575
5576(defconst todos-top-priorities-buffer "Todos top priorities"
5577 "Buffer type string for `todos-filter-items'.")
5578
5579(defconst todos-diary-items-buffer "Todos diary items"
5580 "Buffer type string for `todos-filter-items'.")
5581
5582(defconst todos-regexp-items-buffer "Todos regexp items"
5583 "Buffer type string for `todos-filter-items'.")
5584
5585(defun todos-find-item (str)
5586 "Search for filtered item STR in its saved Todos file.
5587Return the list (FOUND FILE CAT), where CAT and FILE are the
5588item's category and file, and FOUND is a cons cell if the search
5589succeeds, whose car is the start of the item in FILE and whose
5590cdr is `done', if the item is now a done item, `changed', if its
5591text was truncated or augmented or, for a top priority item, if
5592its priority has changed, and `same' otherwise."
5593 (string-match (concat (if todos-filter-done-items
5594 (concat "\\(?:" todos-done-string-start "\\|"
5595 todos-date-string-start "\\)")
5596 todos-date-string-start)
5597 todos-date-pattern "\\(?: " diary-time-regexp "\\)?"
5598 (if todos-filter-done-items
5599 "\\]"
5600 (regexp-quote todos-nondiary-end)) "?"
5601 "\\(?4: \\[\\(?3:(archive) \\)?\\(?2:.*:\\)?"
5602 "\\(?1:.*\\)\\]\\).*$") str)
5603 (let ((cat (match-string 1 str))
5604 (file (match-string 2 str))
5605 (archive (string= (match-string 3 str) "(archive) "))
5606 (filcat (match-string 4 str))
5607 (tpriority 1)
5608 (tpbuf (save-match-data (string-match "top" (buffer-name))))
5609 found)
5610 (setq str (replace-match "" nil nil str 4))
5611 (when tpbuf
5612 ;; Calculate priority of STR wrt its category.
5613 (save-excursion
5614 (while (search-backward filcat nil t)
5615 (setq tpriority (1+ tpriority)))))
5616 (setq file (if file
5617 (concat todos-directory (substring file 0 -1)
5618 (if archive ".toda" ".todo"))
5619 (if archive
5620 (concat (file-name-sans-extension
5621 todos-global-current-todos-file) ".toda")
5622 todos-global-current-todos-file)))
5623 (find-file-noselect file)
5624 (with-current-buffer (find-buffer-visiting file)
5625 (save-restriction
5626 (widen)
5627 (goto-char (point-min))
5628 (let ((beg (re-search-forward
5629 (concat "^" (regexp-quote (concat todos-category-beg cat))
5630 "$")
5631 nil t))
5632 (done (save-excursion
5633 (re-search-forward
5634 (concat "^" (regexp-quote todos-category-done)) nil t)))
5635 (end (save-excursion
5636 (or (re-search-forward
5637 (concat "^" (regexp-quote todos-category-beg))
5638 nil t)
5639 (point-max)))))
5640 (setq found (when (search-forward str end t)
5641 (goto-char (match-beginning 0))))
5642 (when found
5643 (setq found
5644 (cons found (if (> (point) done)
5645 'done
5646 (let ((cpriority 1))
5647 (when tpbuf
5648 (save-excursion
5649 ;; Not top item in category.
5650 (while (> (point) (1+ beg))
5651 (let ((opoint (point)))
5652 (todos-backward-item)
5653 ;; Can't move backward beyond
5654 ;; first item in file.
5655 (unless (= (point) opoint)
5656 (setq cpriority (1+ cpriority)))))))
5657 (if (and (= tpriority cpriority)
5658 ;; Proper substring is not the same.
5659 (string= (todos-item-string)
5660 str))
5661 'same
5662 'changed)))))))))
5663 (list found file cat)))
5664
5665(defun todos-check-filtered-items-file ()
5666 "Check if filtered items file is up to date and a show suitable message."
5667 ;; (catch 'old
5668 (let ((count 0))
5669 (while (not (eobp))
5670 (let* ((item (todos-item-string))
5671 (found (car (todos-find-item item))))
5672 (unless (eq (cdr found) 'same)
5673 (save-excursion
5674 (overlay-put (make-overlay (todos-item-start) (todos-item-end))
5675 'face 'todos-search))
5676 (setq count (1+ count))))
5677 ;; (throw 'old (message "The marked item is not up to date.")))
5678 (todos-forward-item))
5679 (if (zerop count)
5680 (message "Filtered items file is up to date.")
5681 (message (concat "The highlighted item" (if (= count 1) " is " "s are ")
5682 "not up to date."
5683 ;; "\nType <return> on item for details."
5684 )))))
5685
5686(defun todos-filter-items-filename ()
5687 "Return absolute file name for saving this Filtered Items buffer."
5688 (let ((bufname (buffer-name)))
5689 (string-match "\"\\([^\"]+\\)\"" bufname)
5690 (let* ((filename-str (substring bufname (match-beginning 1) (match-end 1)))
5691 (filename-base (replace-regexp-in-string ", " "-" filename-str))
5692 (top-priorities (string-match "top priorities" bufname))
5693 (diary-items (string-match "diary items" bufname))
5694 (regexp-items (string-match "regexp items" bufname)))
5695 (when regexp-items
5696 (let ((prompt (concat "Enter a short identifying string"
5697 " to make this file name unique: ")))
5698 (setq filename-base (concat filename-base "-" (read-string prompt)))))
5699 (concat todos-directory filename-base
5700 (cond (top-priorities ".todt")
5701 (diary-items ".tody")
5702 (regexp-items ".todr"))))))
5703
5704(defun todos-save-filtered-items-buffer ()
5705 "Save current Filtered Items buffer to a file.
5706If the file already exists, overwrite it only on confirmation."
5707 (let ((filename (or (buffer-file-name) (todos-filter-items-filename))))
5708 (write-file filename t)))
5709
5710;; -----------------------------------------------------------------------------
5711;;; Customization groups and set functions
5712;; ----------------------------------------------------------------------------- 5644;; -----------------------------------------------------------------------------
5713 5645
5714(defgroup todos nil 5646(defgroup todos nil
@@ -5866,20 +5798,51 @@ If the file already exists, overwrite it only on confirmation."
5866 (hl-line-mode 1) 5798 (hl-line-mode 1)
5867 (hl-line-mode -1))))))))) 5799 (hl-line-mode -1)))))))))
5868 5800
5801(defun todos-reevaluate-filelist-defcustoms ()
5802 "Reevaluate defcustoms that provide choice list of Todos files."
5803 (custom-set-default 'todos-default-todos-file
5804 (symbol-value 'todos-default-todos-file))
5805 (todos-reevaluate-default-file-defcustom)
5806 (custom-set-default 'todos-filter-files (symbol-value 'todos-filter-files))
5807 (todos-reevaluate-filter-files-defcustom)
5808 (custom-set-default 'todos-category-completions-files
5809 (symbol-value 'todos-category-completions-files))
5810 (todos-reevaluate-category-completions-files-defcustom))
5811
5812(defun todos-reevaluate-default-file-defcustom ()
5813 "Reevaluate defcustom of `todos-default-todos-file'.
5814Called after adding or deleting a Todos file."
5815 (eval (defcustom todos-default-todos-file (car (funcall todos-files-function))
5816 "Todos file visited by first session invocation of `todos-show'."
5817 :type `(radio ,@(mapcar (lambda (f) (list 'const f))
5818 (mapcar 'todos-short-file-name
5819 (funcall todos-files-function))))
5820 :group 'todos)))
5821
5822(defun todos-reevaluate-category-completions-files-defcustom ()
5823 "Reevaluate defcustom of `todos-category-completions-files'.
5824Called after adding or deleting a Todos file."
5825 (eval (defcustom todos-category-completions-files nil
5826 "List of files for building `todos-read-category' completions."
5827 :type `(set ,@(mapcar (lambda (f) (list 'const f))
5828 (mapcar 'todos-short-file-name
5829 (funcall todos-files-function))))
5830 :group 'todos)))
5831
5832(defun todos-reevaluate-filter-files-defcustom ()
5833 "Reevaluate defcustom of `todos-filter-files'.
5834Called after adding or deleting a Todos file."
5835 (eval (defcustom todos-filter-files nil
5836 "List of files for multifile item filtering."
5837 :type `(set ,@(mapcar (lambda (f) (list 'const f))
5838 (mapcar 'todos-short-file-name
5839 (funcall todos-files-function))))
5840 :group 'todos)))
5841
5869;; ----------------------------------------------------------------------------- 5842;; -----------------------------------------------------------------------------
5870;;; Font locking 5843;;; Font locking
5871;; ----------------------------------------------------------------------------- 5844;; -----------------------------------------------------------------------------
5872 5845
5873(defun todos-date-string-matcher (lim)
5874 "Search for Todos date string within LIM for font-locking."
5875 (re-search-forward
5876 (concat todos-date-string-start "\\(?1:" todos-date-pattern "\\)") lim t))
5877
5878(defun todos-time-string-matcher (lim)
5879 "Search for Todos time string within LIM for font-locking."
5880 (re-search-forward (concat todos-date-string-start todos-date-pattern
5881 " \\(?1:" diary-time-regexp "\\)") lim t))
5882
5883(defun todos-nondiary-marker-matcher (lim) 5846(defun todos-nondiary-marker-matcher (lim)
5884 "Search for Todos nondiary markers within LIM for font-locking." 5847 "Search for Todos nondiary markers within LIM for font-locking."
5885 (re-search-forward (concat "^\\(?1:" (regexp-quote todos-nondiary-start) "\\)" 5848 (re-search-forward (concat "^\\(?1:" (regexp-quote todos-nondiary-start) "\\)"
@@ -5892,6 +5855,16 @@ If the file already exists, overwrite it only on confirmation."
5892 (re-search-forward (concat "^\\(?1:" (regexp-quote diary-nonmarking-symbol) 5855 (re-search-forward (concat "^\\(?1:" (regexp-quote diary-nonmarking-symbol)
5893 "\\)" todos-date-pattern) lim t)) 5856 "\\)" todos-date-pattern) lim t))
5894 5857
5858(defun todos-date-string-matcher (lim)
5859 "Search for Todos date string within LIM for font-locking."
5860 (re-search-forward
5861 (concat todos-date-string-start "\\(?1:" todos-date-pattern "\\)") lim t))
5862
5863(defun todos-time-string-matcher (lim)
5864 "Search for Todos time string within LIM for font-locking."
5865 (re-search-forward (concat todos-date-string-start todos-date-pattern
5866 " \\(?1:" diary-time-regexp "\\)") lim t))
5867
5895(defun todos-diary-expired-matcher (lim) 5868(defun todos-diary-expired-matcher (lim)
5896 "Search for expired diary item date within LIM for font-locking." 5869 "Search for expired diary item date within LIM for font-locking."
5897 (when (re-search-forward (concat "^\\(?:" 5870 (when (re-search-forward (concat "^\\(?:"
@@ -5941,15 +5914,14 @@ Filtered Items mode following todo (not done) items."
5941 "\\)? \\(?1:\\[.+\\]\\)") 5914 "\\)? \\(?1:\\[.+\\]\\)")
5942 lim t))) 5915 lim t)))
5943 5916
5944(defvar todos-diary-expired-face 'todos-diary-expired) 5917(defvar todos-nondiary-face 'todos-nondiary)
5945(defvar todos-date-face 'todos-date) 5918(defvar todos-date-face 'todos-date)
5946(defvar todos-time-face 'todos-time) 5919(defvar todos-time-face 'todos-time)
5947(defvar todos-nondiary-face 'todos-nondiary) 5920(defvar todos-diary-expired-face 'todos-diary-expired)
5948(defvar todos-category-string-face 'todos-category-string) 5921(defvar todos-done-sep-face 'todos-done-sep)
5949(defvar todos-done-face 'todos-done) 5922(defvar todos-done-face 'todos-done)
5950(defvar todos-comment-face 'todos-comment) 5923(defvar todos-comment-face 'todos-comment)
5951(defvar todos-done-sep-face 'todos-done-sep) 5924(defvar todos-category-string-face 'todos-category-string)
5952
5953(defvar todos-font-lock-keywords 5925(defvar todos-font-lock-keywords
5954 (list 5926 (list
5955 '(todos-nondiary-marker-matcher 1 todos-nondiary-face t) 5927 '(todos-nondiary-marker-matcher 1 todos-nondiary-face t)
@@ -5968,7 +5940,7 @@ Filtered Items mode following todo (not done) items."
5968 "Font-locking for Todos modes.") 5940 "Font-locking for Todos modes.")
5969 5941
5970;; ----------------------------------------------------------------------------- 5942;; -----------------------------------------------------------------------------
5971;;; Key maps and menus 5943;;; Key binding
5972;; ----------------------------------------------------------------------------- 5944;; -----------------------------------------------------------------------------
5973 5945
5974(defvar todos-insertion-map 5946(defvar todos-insertion-map
@@ -6133,6 +6105,7 @@ Filtered Items mode following todo (not done) items."
6133 map) 6105 map)
6134 "Todos Filtered Items mode keymap.") 6106 "Todos Filtered Items mode keymap.")
6135 6107
6108;; FIXME: Is it worth having a menu and if so, which commands?
6136;; (easy-menu-define 6109;; (easy-menu-define
6137;; todos-menu todos-mode-map "Todos Menu" 6110;; todos-menu todos-mode-map "Todos Menu"
6138;; '("Todos" 6111;; '("Todos"
@@ -6192,12 +6165,9 @@ Filtered Items mode following todo (not done) items."
6192;; )) 6165;; ))
6193 6166
6194;; ----------------------------------------------------------------------------- 6167;; -----------------------------------------------------------------------------
6195;;; Mode local variables and hook functions 6168;;; Hook functions and mode definitions
6196;; ----------------------------------------------------------------------------- 6169;; -----------------------------------------------------------------------------
6197 6170
6198(defvar todos-current-todos-file nil
6199 "Variable holding the name of the currently active Todos file.")
6200
6201(defun todos-show-current-file () 6171(defun todos-show-current-file ()
6202 "Visit current instead of default Todos file with `todos-show'. 6172 "Visit current instead of default Todos file with `todos-show'.
6203This function is added to `pre-command-hook' when user option 6173This function is added to `pre-command-hook' when user option
@@ -6237,25 +6207,6 @@ This function is added to `kill-buffer-hook' in Todos mode."
6237 (or (car todos-file-buffers) 6207 (or (car todos-file-buffers)
6238 (todos-absolute-file-name todos-default-todos-file))))) 6208 (todos-absolute-file-name todos-default-todos-file)))))
6239 6209
6240(defvar todos-categories nil
6241 "Alist of categories in the current Todos file.
6242The elements are cons cells whose car is a category name and
6243whose cdr is a vector of the category's item counts. These are,
6244in order, the numbers of todo items, of todo items included in
6245the Diary, of done items and of archived items.")
6246
6247(defvar todos-categories-with-marks nil
6248 "Alist of categories and number of marked items they contain.")
6249
6250(defvar todos-category-number 1
6251 "Variable holding the number of the current Todos category.
6252Todos categories are numbered starting from 1.")
6253
6254(defvar todos-show-done-only nil
6255 "If non-nil display only done items in current category.
6256Set by the command `todos-toggle-view-done-only' and used by
6257`todos-category-select'.")
6258
6259(defun todos-reset-and-enable-done-separator () 6210(defun todos-reset-and-enable-done-separator ()
6260 "Show resized done items separator overlay after window change. 6211 "Show resized done items separator overlay after window change.
6261Added to `window-configuration-change-hook' in `todos-mode'." 6212Added to `window-configuration-change-hook' in `todos-mode'."
@@ -6264,10 +6215,6 @@ Added to `window-configuration-change-hook' in `todos-mode'."
6264 (setq todos-done-separator (todos-done-separator)) 6215 (setq todos-done-separator (todos-done-separator))
6265 (save-match-data (todos-reset-done-separator sep))))) 6216 (save-match-data (todos-reset-done-separator sep)))))
6266 6217
6267;; -----------------------------------------------------------------------------
6268;;; Mode definitions
6269;; -----------------------------------------------------------------------------
6270
6271(defun todos-modes-set-1 () 6218(defun todos-modes-set-1 ()
6272 "Make some settings that apply to multiple Todos modes." 6219 "Make some settings that apply to multiple Todos modes."
6273 (setq-local font-lock-defaults '(todos-font-lock-keywords t)) 6220 (setq-local font-lock-defaults '(todos-font-lock-keywords t))