]> code.delx.au - gnu-emacs/blob - lisp/calendar/todos.el
* calendar/todos.el (todos-undo-item-omit-comment): New defcustom.
[gnu-emacs] / lisp / calendar / todos.el
1 ;;; Todos.el --- facilities for making and maintaining Todo lists
2
3 ;; Copyright (C) 1997, 1999, 2001-2012 Free Software Foundation, Inc.
4
5 ;; Author: Oliver Seidel <privat@os10000.net>
6 ;; Stephen Berman <stephen.berman@gmx.net>
7 ;; Maintainer: Stephen Berman <stephen.berman@gmx.net>
8 ;; Created: 2 Aug 1997
9 ;; Keywords: calendar, todo
10
11 ;; This file is [not yet] part of GNU Emacs.
12
13 ;; GNU Emacs is free software: you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
17
18 ;; GNU Emacs is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
22
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
25
26 ;;; Commentary:
27
28 ;;; Code:
29
30 (require 'diary-lib)
31 ;; For remove-duplicates in todos-insertion-commands-args.
32 (eval-when-compile (require 'cl))
33
34 ;; ---------------------------------------------------------------------------
35 ;;; User options
36
37 (defgroup todos nil
38 "Create and maintain categorized lists of todo items."
39 :link '(emacs-commentary-link "todos")
40 :version "24.2"
41 :group 'calendar)
42
43 (defcustom todos-files-directory (locate-user-emacs-file "todos/")
44 "Directory where user's Todos files are saved."
45 :type 'directory
46 :group 'todos)
47
48 (defun todos-files (&optional archives)
49 "Default value of `todos-files-function'.
50 This returns the case-insensitive alphabetically sorted list of
51 file truenames in `todos-files-directory' with the extension
52 \".todo\". With non-nil ARCHIVES return the list of archive file
53 truenames (those with the extension \".toda\")."
54 (let ((files (if (file-exists-p todos-files-directory)
55 (mapcar 'file-truename
56 (directory-files todos-files-directory t
57 (if archives "\.toda$" "\.todo$") t)))))
58 (sort files (lambda (s1 s2) (let ((cis1 (upcase s1))
59 (cis2 (upcase s2)))
60 (string< cis1 cis2))))))
61
62 (defcustom todos-files-function 'todos-files
63 "Function returning the value of the variable `todos-files'.
64 This function should take an optional argument that, if non-nil,
65 makes it return the value of the variable `todos-archives'."
66 :type 'function
67 :group 'todos)
68
69 (defun todos-short-file-name (file)
70 "Return short form of Todos FILE.
71 This lacks the extension and directory components."
72 (file-name-sans-extension (file-name-nondirectory file)))
73
74 (defcustom todos-default-todos-file (car (funcall todos-files-function))
75 "Todos file visited by first session invocation of `todos-show'."
76 :type `(radio ,@(mapcar (lambda (f) (list 'const f))
77 (mapcar 'todos-short-file-name
78 (funcall todos-files-function))))
79 :group 'todos)
80
81 ;; FIXME: is there a better alternative to this?
82 (defun todos-reevaluate-default-file-defcustom ()
83 "Reevaluate defcustom of `todos-default-todos-file'.
84 Called after adding or deleting a Todos file."
85 (eval (defcustom todos-default-todos-file (car (funcall todos-files-function))
86 "Todos file visited by first session invocation of `todos-show'."
87 :type `(radio ,@(mapcar (lambda (f) (list 'const f))
88 (mapcar 'todos-short-file-name
89 (funcall todos-files-function))))
90 :group 'todos)))
91
92 (defcustom todos-show-current-file t
93 "Non-nil to make `todos-show' visit the current Todos file.
94 Otherwise, `todos-show' always visits `todos-default-todos-file'."
95 :type 'boolean
96 :initialize 'custom-initialize-default
97 :set 'todos-set-show-current-file
98 :group 'todos)
99
100 (defun todos-set-show-current-file (symbol value)
101 "The :set function for user option `todos-show-current-file'."
102 (custom-set-default symbol value)
103 (if value
104 (add-hook 'pre-command-hook 'todos-show-current-file nil t)
105 (remove-hook 'pre-command-hook 'todos-show-current-file t)))
106
107 (defcustom todos-visit-files-commands (list 'find-file 'dired-find-file)
108 "List of file finding commands for `todos-display-as-todos-file'.
109 Invoking these commands to visit a Todos or Todos Archive file
110 calls `todos-show' or `todos-show-archive', so that the file is
111 displayed correctly."
112 :type '(repeat function)
113 :group 'todos)
114
115 (defcustom todos-initial-file "Todo"
116 "Default file name offered on adding first Todos file."
117 :type 'string
118 :group 'todos)
119
120 (defcustom todos-initial-category "Todo"
121 "Default category name offered on initializing a new Todos file."
122 :type 'string
123 :group 'todos)
124
125 (defcustom todos-display-categories-first nil
126 "Non-nil to display category list on first visit to a Todos file."
127 :type 'boolean
128 :group 'todos)
129
130 (defcustom todos-completion-ignore-case nil
131 "Non-nil means case is ignored by `todos-read-*' functions."
132 :type 'boolean
133 :group 'todos)
134
135 (defcustom todos-undo-item-omit-comment 'ask
136 "Whether to omit done item comment on undoing the item.
137 Nil means never omit the comment, t means always omit it, `ask'
138 means prompt user and omit comment only on confirmation."
139 :type '(choice (const :tag "Never" nil)
140 (const :tag "Always" t)
141 (const :tag "Ask" ask))
142 :group 'todos)
143
144 (defcustom todos-print-function 'ps-print-buffer-with-faces
145 "Function called to print buffer content; see `todos-print'."
146 :type 'symbol
147 :group 'todos)
148
149 (defcustom todos-todo-mode-date-time-regexp
150 (concat "\\(?1:[0-9]\\{4\\}\\)-\\(?2:[0-9]\\{2\\}\\)-"
151 "\\(?3:[0-9]\\{2\\}\\) \\(?4:[0-9]\\{2\\}:[0-9]\\{2\\}\\)")
152 "Regexp matching legacy todo-mode.el item date-time strings.
153 In order for `todos-convert-legacy-files' to correctly convert this
154 string to the current Todos format, the regexp must contain four
155 explicitly numbered groups (see `(elisp) Regexp Backslash'),
156 where group 1 matches a string for the year, group 2 a string for
157 the month, group 3 a string for the day and group 4 a string for
158 the time. The default value converts date-time strings built
159 using the default value of `todo-time-string-format' from
160 todo-mode.el."
161 :type 'regexp
162 :group 'todos)
163
164 ;; ---------------------------------------------------------------------------
165 ;;; Todos mode display options
166
167 (defgroup todos-mode-display nil
168 "User display options for Todos mode."
169 :version "24.2"
170 :group 'todos)
171
172 (defcustom todos-prefix ""
173 "String prefixed to todo items for visual distinction."
174 :type 'string
175 :initialize 'custom-initialize-default
176 :set 'todos-reset-prefix
177 :group 'todos-mode-display)
178
179 (defcustom todos-number-priorities t
180 "Non-nil to prefix items with consecutively increasing integers.
181 These reflect the priorities of the items in each category."
182 :type 'boolean
183 :initialize 'custom-initialize-default
184 :set 'todos-reset-prefix
185 :group 'todos-mode-display)
186
187 (defun todos-reset-prefix (symbol value)
188 "The :set function for `todos-prefix' and `todos-number-priorities'."
189 (let ((oldvalue (symbol-value symbol))
190 (files (append todos-files todos-archives)))
191 (custom-set-default symbol value)
192 (when (not (equal value oldvalue))
193 (dolist (f files)
194 (with-current-buffer (find-file-noselect f)
195 (save-window-excursion
196 (todos-show)
197 (save-excursion
198 (widen)
199 (goto-char (point-min))
200 (while (not (eobp))
201 (remove-overlays (point) (point)); 'before-string prefix)
202 (forward-line)))
203 ;; Activate the new setting (save-restriction does not help).
204 (save-excursion (todos-category-select))))))))
205
206 (defcustom todos-done-separator-string "_"
207 "String for generating `todos-done-separator'.
208
209 If the string consists of a single character,
210 `todos-done-separator' will be the string made by repeating this
211 character for the width of the window, and the length is
212 automatically recalculated when the window width changes. If the
213 string consists of more (or less) than one character, it will be
214 the value of `todos-done-separator'."
215 :type 'string
216 :initialize 'custom-initialize-default
217 :set 'todos-reset-done-separator-string
218 :group 'todos-mode-display)
219
220 (defun todos-reset-done-separator-string (symbol value)
221 "The :set function for `todos-done-separator-string'."
222 (let ((oldvalue (symbol-value symbol))
223 (files todos-file-buffers)
224 (sep todos-done-separator))
225 (custom-set-default symbol value)
226 (setq todos-done-separator (todos-done-separator))
227 (when (= 1 (length value))
228 (todos-reset-done-separator sep))))
229
230 (defcustom todos-done-string "DONE "
231 "Identifying string appended to the front of done todos items."
232 :type 'string
233 :initialize 'custom-initialize-default
234 :set 'todos-reset-done-string
235 :group 'todos-mode-display)
236
237 (defun todos-reset-done-string (symbol value)
238 "The :set function for user option `todos-done-string'."
239 (let ((oldvalue (symbol-value symbol))
240 (files (append todos-files todos-archives)))
241 (custom-set-default symbol value)
242 ;; Need to reset this to get font-locking right.
243 (setq todos-done-string-start
244 (concat "^\\[" (regexp-quote todos-done-string)))
245 (when (not (equal value oldvalue))
246 (dolist (f files)
247 (with-current-buffer (find-file-noselect f)
248 (let (buffer-read-only)
249 (widen)
250 (goto-char (point-min))
251 (while (not (eobp))
252 (if (re-search-forward
253 (concat "^" (regexp-quote todos-nondiary-start)
254 "\\(" (regexp-quote oldvalue) "\\)")
255 nil t)
256 (replace-match value t t nil 1)
257 (forward-line)))
258 (todos-category-select)))))))
259
260 (defcustom todos-comment-string "COMMENT"
261 "String inserted before optional comment appended to done item."
262 :type 'string
263 :initialize 'custom-initialize-default
264 :set 'todos-reset-comment-string
265 :group 'todos-mode-display)
266
267 (defun todos-reset-comment-string (symbol value)
268 "The :set function for user option `todos-comment-string'."
269 (let ((oldvalue (symbol-value symbol))
270 (files (append todos-files todos-archives)))
271 (custom-set-default symbol value)
272 (when (not (equal value oldvalue))
273 (dolist (f files)
274 (with-current-buffer (find-file-noselect f)
275 (let (buffer-read-only)
276 (save-excursion
277 (widen)
278 (goto-char (point-min))
279 (while (not (eobp))
280 (if (re-search-forward
281 (concat
282 "\\[\\(" (regexp-quote oldvalue) "\\): [^]]*\\]")
283 nil t)
284 (replace-match value t t nil 1)
285 (forward-line)))
286 (todos-category-select))))))))
287
288 (defcustom todos-show-with-done nil
289 "Non-nil to display done items in all categories."
290 :type 'boolean
291 :group 'todos-mode-display)
292
293 (defun todos-mode-line-control (cat)
294 "Return a mode line control for Todos buffers.
295 Argument CAT is the name of the current Todos category.
296 This function is the value of the user variable
297 `todos-mode-line-function'."
298 (let ((file (todos-short-file-name todos-current-todos-file)))
299 (format "%s category %d: %s" file todos-category-number cat)))
300
301 (defcustom todos-mode-line-function 'todos-mode-line-control
302 "Function that returns a mode line control for Todos buffers.
303 The function expects one argument holding the name of the current
304 Todos category. The resulting control becomes the local value of
305 `mode-line-buffer-identification' in each Todos buffer."
306 :type 'function
307 :group 'todos-mode-display)
308
309 (defcustom todos-skip-archived-categories nil
310 "Non-nil to skip categories with only archived items when browsing.
311
312 Moving by category todos or archive file (with
313 \\[todos-forward-category] and \\[todos-backward-category]) skips
314 categories that contain only archived items. Other commands
315 still recognize these categories. In Todos Categories
316 mode (reached with \\[todos-display-categories]) these categories
317 shown in `todos-archived-only' face and clicking them in Todos
318 Categories mode visits the archived categories."
319 :type 'boolean
320 :group 'todos-mode-display)
321
322 (defcustom todos-highlight-item nil
323 "Non-nil means highlight items at point."
324 :type 'boolean
325 :initialize 'custom-initialize-default
326 :set 'todos-reset-highlight-item
327 :group 'todos-mode-display)
328
329 (defun todos-reset-highlight-item (symbol value)
330 "The :set function for `todos-highlight-item'."
331 (let ((oldvalue (symbol-value symbol))
332 (files (append todos-files todos-archives)))
333 (custom-set-default symbol value)
334 (when (not (equal value oldvalue))
335 (dolist (f files)
336 (let ((buf (find-buffer-visiting f)))
337 (when buf
338 (with-current-buffer buf
339 (require 'hl-line)
340 (if value
341 (hl-line-mode 1)
342 (hl-line-mode -1)))))))))
343
344 (defcustom todos-wrap-lines t
345 "Non-nil to wrap long lines via `todos-line-wrapping-function'."
346 :group 'todos-mode-display
347 :type 'boolean)
348
349 (defcustom todos-line-wrapping-function 'todos-wrap-and-indent
350 "Line wrapping function used with non-nil `todos-wrap-lines'."
351 :group 'todos-mode-display
352 :type 'function)
353
354 (defun todos-wrap-and-indent ()
355 "Use word wrapping on long lines and indent with a wrap prefix.
356 The amount of indentation is given by user option
357 `todos-indent-to-here'."
358 (set (make-local-variable 'word-wrap) t)
359 (set (make-local-variable 'wrap-prefix) (make-string todos-indent-to-here 32))
360 (unless (member '(continuation) fringe-indicator-alist)
361 (push '(continuation) fringe-indicator-alist)))
362
363 ;; FIXME: :set function to refill items with hard newlines and to immediately
364 ;; update wrapped prefix display
365 (defcustom todos-indent-to-here 6
366 "Number of spaces `todos-line-wrapping-function' indents to."
367 :type '(integer :validate
368 (lambda (widget)
369 (unless (> (widget-value widget) 0)
370 (widget-put widget :error
371 "Invalid value: must be a positive integer")
372 widget)))
373 :group 'todos)
374
375 (defun todos-indent ()
376 "Indent from point to `todos-indent-to-here'."
377 (indent-to todos-indent-to-here todos-indent-to-here))
378
379 ;; ---------------------------------------------------------------------------
380 ;;; Item insertion options
381
382 (defgroup todos-item-insertion nil
383 "User options for adding new todo items."
384 :version "24.2"
385 :group 'todos)
386
387 (defcustom todos-include-in-diary nil
388 "Non-nil to allow new Todo items to be included in the diary."
389 :type 'boolean
390 :group 'todos-item-insertion)
391
392 (defcustom todos-diary-nonmarking nil
393 "Non-nil to insert new Todo diary items as nonmarking by default.
394 This appends `diary-nonmarking-symbol' to the front of an item on
395 insertion provided it doesn't begin with `todos-nondiary-marker'."
396 :type 'boolean
397 :group 'todos-item-insertion)
398
399 (defcustom todos-nondiary-marker '("[" "]")
400 "List of strings surrounding item date to block diary inclusion.
401 The first string is inserted before the item date and must be a
402 non-empty string that does not match a diary date in order to
403 have its intended effect. The second string is inserted after
404 the diary date."
405 :type '(list string string)
406 :group 'todos-item-insertion
407 :initialize 'custom-initialize-default
408 :set 'todos-reset-nondiary-marker)
409
410 (defun todos-reset-nondiary-marker (symbol value)
411 "The :set function for user option `todos-nondiary-marker'."
412 (let ((oldvalue (symbol-value symbol))
413 (files (append todos-files todos-archives)))
414 (custom-set-default symbol value)
415 ;; Need to reset these to get font-locking right.
416 (setq todos-nondiary-start (nth 0 todos-nondiary-marker)
417 todos-nondiary-end (nth 1 todos-nondiary-marker)
418 todos-date-string-start
419 ;; See comment in defvar of `todos-date-string-start'.
420 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|"
421 (regexp-quote diary-nonmarking-symbol) "\\)?"))
422 (when (not (equal value oldvalue))
423 (dolist (f files)
424 (with-current-buffer (find-file-noselect f)
425 (let (buffer-read-only)
426 (widen)
427 (goto-char (point-min))
428 (while (not (eobp))
429 (if (re-search-forward
430 (concat "^\\(" todos-done-string-start "[^][]+] \\)?"
431 "\\(?1:" (regexp-quote (car oldvalue))
432 "\\)" todos-date-pattern "\\( "
433 diary-time-regexp "\\)?\\(?2:"
434 (regexp-quote (cadr oldvalue)) "\\)")
435 nil t)
436 (progn
437 (replace-match (nth 0 value) t t nil 1)
438 (replace-match (nth 1 value) t t nil 2))
439 (forward-line)))
440 (todos-category-select)))))))
441
442 (defcustom todos-always-add-time-string nil
443 "Non-nil adds current time to a new item's date header by default.
444 When the Todos insertion commands have a non-nil \"maybe-notime\"
445 argument, this reverses the effect of
446 `todos-always-add-time-string': if t, these commands omit the
447 current time, if nil, they include it."
448 :type 'boolean
449 :group 'todos-item-insertion)
450
451 (defcustom todos-use-only-highlighted-region t
452 "Non-nil to enable inserting only highlighted region as new item."
453 :type 'boolean
454 :group 'todos-item-insertion)
455
456 ;; ---------------------------------------------------------------------------
457 ;;; Todos Filter Items mode options
458
459 (defgroup todos-filtered nil
460 "User options for Todos Filter Items mode."
461 :version "24.2"
462 :group 'todos)
463
464 (defcustom todos-filtered-items-buffer "Todos filtered items"
465 "Initial name of buffer in Todos Filter Items mode."
466 :type 'string
467 :group 'todos-filtered)
468
469 (defcustom todos-top-priorities-buffer "Todos top priorities"
470 "Buffer type string for `todos-filtered-buffer-name'."
471 :type 'string
472 :group 'todos-filtered)
473
474 (defcustom todos-diary-items-buffer "Todos diary items"
475 "Buffer type string for `todos-filtered-buffer-name'."
476 :type 'string
477 :group 'todos-filtered)
478
479 (defcustom todos-regexp-items-buffer "Todos regexp items"
480 "Buffer type string for `todos-filtered-buffer-name'."
481 :type 'string
482 :group 'todos-filtered)
483
484 (defcustom todos-priorities-rules nil
485 "List of rules giving how many items `todos-top-priorities' shows.
486 This variable should be set interactively by
487 `\\[todos-set-top-priorities-in-file]' or
488 `\\[todos-set-top-priorities-in-category]'.
489
490 Each rule is a list of the form (FILE NUM ALIST), where FILE is a
491 member of `todos-files', NUM is a number specifying the default
492 number of top priority items for each category in that file, and
493 ALIST, when non-nil, consists of conses of a category name in
494 FILE and a number specifying the default number of top priority
495 items in that category, which overrides NUM."
496 :type 'list
497 :group 'todos-filtered)
498
499 (defcustom todos-show-priorities 1
500 "Default number of top priorities shown by `todos-top-priorities'."
501 :type 'integer
502 :group 'todos-filtered)
503
504 (defcustom todos-filter-files nil
505 "List of default files for multifile item filtering."
506 :type `(set ,@(mapcar (lambda (f) (list 'const f))
507 (mapcar 'todos-short-file-name
508 (funcall todos-files-function))))
509 :group 'todos-filtered)
510
511 ;; FIXME: is there a better alternative to this?
512 (defun todos-reevaluate-filter-files-defcustom ()
513 "Reevaluate defcustom of `todos-filter-files'.
514 Called after adding or deleting a Todos file."
515 (eval (defcustom todos-filter-files nil
516 "List of files for multifile item filtering."
517 :type `(set ,@(mapcar (lambda (f) (list 'const f))
518 (mapcar 'todos-short-file-name
519 (funcall todos-files-function))))
520 :group 'todos)))
521
522 (defcustom todos-filter-done-items nil
523 "Non-nil to include done items when processing regexp filters.
524 Done items from corresponding archive files are also included."
525 :type 'boolean
526 :group 'todos-filtered)
527
528 ;; ---------------------------------------------------------------------------
529 ;;; Todos Categories mode options
530
531 (defgroup todos-categories nil
532 "User options for Todos Categories mode."
533 :version "24.2"
534 :group 'todos)
535
536 (defcustom todos-categories-category-label "Category"
537 "Category button label in Todos Categories mode."
538 :type 'string
539 :group 'todos-categories)
540
541 (defcustom todos-categories-todo-label "Todo"
542 "Todo button label in Todos Categories mode."
543 :type 'string
544 :group 'todos-categories)
545
546 (defcustom todos-categories-diary-label "Diary"
547 "Diary button label in Todos Categories mode."
548 :type 'string
549 :group 'todos-categories)
550
551 (defcustom todos-categories-done-label "Done"
552 "Done button label in Todos Categories mode."
553 :type 'string
554 :group 'todos-categories)
555
556 (defcustom todos-categories-archived-label "Archived"
557 "Archived button label in Todos Categories mode."
558 :type 'string
559 :group 'todos-categories)
560
561 (defcustom todos-categories-totals-label "Totals"
562 "String to label total item counts in Todos Categories mode."
563 :type 'string
564 :group 'todos-categories)
565
566 (defcustom todos-categories-number-separator " | "
567 "String between number and category in Todos Categories mode.
568 This separates the number from the category name in the default
569 categories display according to priority."
570 :type 'string
571 :group 'todos-categories)
572
573 (defcustom todos-categories-align 'center
574 "Alignment of category names in Todos Categories mode."
575 :type '(radio (const left) (const center) (const right))
576 :group 'todos-categories)
577
578 ;; ---------------------------------------------------------------------------
579 ;;; Faces and font-lock matcher functions
580
581 (defgroup todos-faces nil
582 "Faces for the Todos modes."
583 :version "24.2"
584 :group 'todos)
585
586 (defface todos-prefix-string
587 ;; '((t :inherit font-lock-constant-face))
588 '((((class grayscale) (background light))
589 (:foreground "LightGray" :weight bold :underline t))
590 (((class grayscale) (background dark))
591 (:foreground "Gray50" :weight bold :underline t))
592 (((class color) (min-colors 88) (background light)) (:foreground "dark cyan"))
593 (((class color) (min-colors 88) (background dark)) (:foreground "Aquamarine"))
594 (((class color) (min-colors 16) (background light)) (:foreground "CadetBlue"))
595 (((class color) (min-colors 16) (background dark)) (:foreground "Aquamarine"))
596 (((class color) (min-colors 8)) (:foreground "magenta"))
597 (t (:weight bold :underline t)))
598 "Face for Todos prefix string."
599 :group 'todos-faces)
600
601 (defface todos-mark
602 ;; '((t :inherit font-lock-warning-face))
603 '((((class color)
604 (min-colors 88)
605 (background light))
606 (:weight bold :foreground "Red1"))
607 (((class color)
608 (min-colors 88)
609 (background dark))
610 (:weight bold :foreground "Pink"))
611 (((class color)
612 (min-colors 16)
613 (background light))
614 (:weight bold :foreground "Red1"))
615 (((class color)
616 (min-colors 16)
617 (background dark))
618 (:weight bold :foreground "Pink"))
619 (((class color)
620 (min-colors 8))
621 (:foreground "red"))
622 (t
623 (:weight bold :inverse-video t)))
624 "Face for marks on Todos items."
625 :group 'todos-faces)
626
627 (defface todos-button
628 ;; '((t :inherit widget-field))
629 '((((type tty))
630 (:foreground "black" :background "yellow3"))
631 (((class grayscale color)
632 (background light))
633 (:background "gray85"))
634 (((class grayscale color)
635 (background dark))
636 (:background "dim gray"))
637 (t
638 (:slant italic)))
639 "Face for buttons in todos-display-categories."
640 :group 'todos-faces)
641
642 (defface todos-sorted-column
643 '((((type tty))
644 (:inverse-video t))
645 (((class color)
646 (background light))
647 (:background "grey85"))
648 (((class color)
649 (background dark))
650 (:background "grey85" :foreground "grey10"))
651 (t
652 (:background "gray")))
653 "Face for buttons in todos-display-categories."
654 :group 'todos-faces)
655
656 (defface todos-archived-only
657 ;; '((t (:inherit (shadow))))
658 '((((class color)
659 (background light))
660 (:foreground "grey50"))
661 (((class color)
662 (background dark))
663 (:foreground "grey70"))
664 (t
665 (:foreground "gray")))
666 "Face for archived-only categories in todos-display-categories."
667 :group 'todos-faces)
668
669 (defface todos-search
670 ;; '((t :inherit match))
671 '((((class color)
672 (min-colors 88)
673 (background light))
674 (:background "yellow1"))
675 (((class color)
676 (min-colors 88)
677 (background dark))
678 (:background "RoyalBlue3"))
679 (((class color)
680 (min-colors 8)
681 (background light))
682 (:foreground "black" :background "yellow"))
683 (((class color)
684 (min-colors 8)
685 (background dark))
686 (:foreground "white" :background "blue"))
687 (((type tty)
688 (class mono))
689 (:inverse-video t))
690 (t
691 (:background "gray")))
692 "Face for matches found by todos-search."
693 :group 'todos-faces)
694
695 (defface todos-diary-expired
696 ;; '((t :inherit font-lock-warning-face))
697 '((((class color)
698 (min-colors 16))
699 (:weight bold :foreground "DarkOrange"))
700 (((class color))
701 (:weight bold :foreground "yellow"))
702 (t
703 (:weight bold)))
704 "Face for expired dates of diary items."
705 :group 'todos-faces)
706 (defvar todos-diary-expired-face 'todos-diary-expired)
707
708 (defface todos-date
709 '((t :inherit diary))
710 "Face for the date string of a Todos item."
711 :group 'todos-faces)
712 (defvar todos-date-face 'todos-date)
713
714 (defface todos-time
715 '((t :inherit diary-time))
716 "Face for the time string of a Todos item."
717 :group 'todos-faces)
718 (defvar todos-time-face 'todos-time)
719
720 (defface todos-done
721 ;; '((t :inherit font-lock-comment-face))
722 '((((class grayscale)
723 (background light))
724 (:slant italic :weight bold :foreground "DimGray"))
725 (((class grayscale)
726 (background dark))
727 (:slant italic :weight bold :foreground "LightGray"))
728 (((class color)
729 (min-colors 88)
730 (background light))
731 (:foreground "Firebrick"))
732 (((class color)
733 (min-colors 88)
734 (background dark))
735 (:foreground "chocolate1"))
736 (((class color)
737 (min-colors 16)
738 (background light))
739 (:foreground "red"))
740 (((class color)
741 (min-colors 16)
742 (background dark))
743 (:foreground "red1"))
744 (((class color)
745 (min-colors 8)
746 (background light))
747 (:foreground "red"))
748 (((class color)
749 (min-colors 8)
750 (background dark))
751 (:foreground "yellow"))
752 (t
753 (:slant italic :weight bold)))
754 "Face for done Todos item header string."
755 :group 'todos-faces)
756 (defvar todos-done-face 'todos-done)
757
758 (defface todos-comment
759 '((t :inherit todos-done))
760 "Face for comments appended to done Todos items."
761 :group 'todos-faces)
762 (defvar todos-comment-face 'todos-comment)
763
764 (defface todos-done-sep
765 ;; '((t :inherit font-lock-type-face))
766 '((((class grayscale)
767 (background light))
768 (:weight bold :foreground "Gray90"))
769 (((class grayscale)
770 (background dark))
771 (:weight bold :foreground "DimGray"))
772 (((class color)
773 (min-colors 88)
774 (background light))
775 (:foreground "ForestGreen"))
776 (((class color)
777 (min-colors 88)
778 (background dark))
779 (:foreground "PaleGreen"))
780 (((class color)
781 (min-colors 16)
782 (background light))
783 (:foreground "ForestGreen"))
784 (((class color)
785 (min-colors 16)
786 (background dark))
787 (:foreground "PaleGreen"))
788 (((class color)
789 (min-colors 8))
790 (:foreground "green"))
791 (t
792 (:underline t :weight bold)))
793 "Face for separator string bewteen done and not done Todos items."
794 :group 'todos-faces)
795 (defvar todos-done-sep-face 'todos-done-sep)
796
797 (defun todos-date-string-matcher (lim)
798 "Search for Todos date string within LIM for font-locking."
799 (re-search-forward
800 (concat todos-date-string-start "\\(?1:" todos-date-pattern "\\)") lim t))
801
802 (defun todos-time-string-matcher (lim)
803 "Search for Todos time string within LIM for font-locking."
804 (re-search-forward (concat todos-date-string-start todos-date-pattern
805 " \\(?1:" diary-time-regexp "\\)") lim t))
806
807 (defun todos-nondiary-marker-matcher (lim)
808 "Search for Todos nondiary markers within LIM for font-locking."
809 (re-search-forward (concat "^\\(?1:" (regexp-quote todos-nondiary-start) "\\)"
810 todos-date-pattern "\\(?: " diary-time-regexp
811 "\\)?\\(?2:" (regexp-quote todos-nondiary-end) "\\)")
812 lim t))
813
814 (defun todos-diary-nonmarking-matcher (lim)
815 "Search for diary nonmarking symbol within LIM for font-locking."
816 (re-search-forward (concat "^\\(?1:" (regexp-quote diary-nonmarking-symbol)
817 "\\)" todos-date-pattern) lim t))
818
819 (defun todos-diary-expired-matcher (lim)
820 "Search for expired diary item date within LIM for font-locking."
821 (when (re-search-forward (concat "^\\(?:"
822 (regexp-quote diary-nonmarking-symbol)
823 "\\)?\\(?1:" todos-date-pattern "\\) \\(?2:"
824 diary-time-regexp "\\)?") lim t)
825 (let* ((date (match-string-no-properties 1))
826 (time (match-string-no-properties 2))
827 ;; Function days-between requires a non-empty time string.
828 (date-time (concat date " " (or time "00:00"))))
829 (or (and (not (string-match ".+day\\|\\*" date))
830 (< (days-between date-time (current-time-string)) 0))
831 (todos-diary-expired-matcher lim)))))
832
833 (defun todos-done-string-matcher (lim)
834 "Search for Todos done header within LIM for font-locking."
835 (re-search-forward (concat todos-done-string-start
836 "[^][]+]")
837 lim t))
838
839 (defun todos-comment-string-matcher (lim)
840 "Search for Todos done comment within LIM for font-locking."
841 (re-search-forward (concat "\\[\\(?1:" todos-comment-string "\\):")
842 lim t))
843
844 ;; (defun todos-category-string-matcher (lim)
845 ;; "Search for Todos category name within LIM for font-locking.
846 ;; This is for fontifying category names appearing in Todos filter
847 ;; mode."
848 ;; (if (eq major-mode 'todos-filtered-items-mode)
849 ;; (re-search-forward
850 ;; (concat "^\\(?:" todos-date-string-start "\\)?" todos-date-pattern
851 ;; "\\(?: " diary-time-regexp "\\)?\\(?:"
852 ;; (regexp-quote todos-nondiary-end) "\\)? \\(?1:\\[.+\\]\\)")
853 ;; lim t)))
854
855 (defun todos-category-string-matcher-1 (lim)
856 "Search for Todos category name within LIM for font-locking.
857 This is for fontifying category names appearing in Todos filter
858 mode following done items."
859 (if (eq major-mode 'todos-filtered-items-mode)
860 (re-search-forward (concat todos-done-string-start todos-date-pattern
861 "\\(?: " diary-time-regexp
862 ;; Use non-greedy operator to prevent
863 ;; capturing possible following non-diary
864 ;; date string.
865 "\\)?] \\(?1:\\[.+?\\]\\)")
866 lim t)))
867
868 (defun todos-category-string-matcher-2 (lim)
869 "Search for Todos category name within LIM for font-locking.
870 This is for fontifying category names appearing in Todos filter
871 mode following todo (not done) items."
872 (if (eq major-mode 'todos-filtered-items-mode)
873 (re-search-forward (concat todos-date-string-start todos-date-pattern
874 "\\(?: " diary-time-regexp "\\)?\\(?:"
875 (regexp-quote todos-nondiary-end)
876 "\\)? \\(?1:\\[.+\\]\\)")
877 lim t)))
878
879 (defvar todos-font-lock-keywords
880 (list
881 '(todos-nondiary-marker-matcher 1 todos-done-sep-face t)
882 '(todos-nondiary-marker-matcher 2 todos-done-sep-face t)
883 ;; This is the face used by diary-lib.el.
884 '(todos-diary-nonmarking-matcher 1 font-lock-constant-face t)
885 '(todos-date-string-matcher 1 todos-date-face t)
886 '(todos-time-string-matcher 1 todos-time-face t)
887 '(todos-done-string-matcher 0 todos-done-face t)
888 '(todos-comment-string-matcher 1 todos-done-face t)
889 ;; '(todos-category-string-matcher 1 todos-done-sep-face t)
890 '(todos-category-string-matcher-1 1 todos-done-sep-face t t)
891 '(todos-category-string-matcher-2 1 todos-done-sep-face t t)
892 '(todos-diary-expired-matcher 1 todos-diary-expired-face t)
893 '(todos-diary-expired-matcher 2 todos-diary-expired-face t t)
894 )
895 "Font-locking for Todos modes.")
896
897 ;; ---------------------------------------------------------------------------
898 ;;; Todos mode local variables and hook functions
899
900 (defvar todos-current-todos-file nil
901 "Variable holding the name of the currently active Todos file.")
902
903 (defun todos-show-current-file ()
904 "Visit current instead of default Todos file with `todos-show'.
905 This function is added to `pre-command-hook' when user option
906 `todos-show-current-file' is set to non-nil."
907 (setq todos-global-current-todos-file todos-current-todos-file))
908
909 (defun todos-display-as-todos-file ()
910 "Show Todos files correctly when visited from outside of Todos mode."
911 (and (member this-command todos-visit-files-commands)
912 (= (- (point-max) (point-min)) (buffer-size))
913 (member major-mode '(todos-mode todos-archive-mode))
914 (todos-category-select)))
915
916 (defun todos-add-to-buffer-list ()
917 "Add name of just visited Todos file to `todos-file-buffers'.
918 This function is added to `find-file-hook' in Todos mode."
919 (let ((filename (file-truename (buffer-file-name))))
920 (when (member filename todos-files)
921 (add-to-list 'todos-file-buffers filename))))
922
923 (defun todos-update-buffer-list ()
924 "Make current Todos mode buffer file car of `todos-file-buffers'.
925 This function is added to `post-command-hook' in Todos mode."
926 (let ((filename (file-truename (buffer-file-name))))
927 (unless (eq (car todos-file-buffers) filename)
928 (setq todos-file-buffers
929 (cons filename (delete filename todos-file-buffers))))))
930
931 (defun todos-reset-global-current-todos-file ()
932 "Update the value of `todos-global-current-todos-file'.
933 This becomes the latest existing Todos file or, if there is none,
934 the value of `todos-default-todos-file'.
935 This function is added to `kill-buffer-hook' in Todos mode."
936 (let ((filename (file-truename (buffer-file-name))))
937 (setq todos-file-buffers (delete filename todos-file-buffers))
938 (setq todos-global-current-todos-file (or (car todos-file-buffers)
939 todos-default-todos-file))))
940
941 (defvar todos-categories nil
942 "Alist of categories in the current Todos file.
943 The elements are cons cells whose car is a category name and
944 whose cdr is a vector of the category's item counts. These are,
945 in order, the numbers of todo items, of todo items included in
946 the Diary, of done items and of archived items.")
947
948 (defvar todos-categories-with-marks nil
949 "Alist of categories and number of marked items they contain.")
950
951 (defvar todos-category-number 1
952 "Variable holding the number of the current Todos category.
953 Todos categories are numbered starting from 1.")
954
955 (defvar todos-first-visit t
956 "Non-nil if first display of this file in the current session.
957 See `todos-display-categories-first'.")
958
959 (defvar todos-show-done-only nil
960 "If non-nil display only done items in current category.
961 Set by the command `todos-show-done-only' and used by
962 `todos-category-select'.")
963
964 (defun todos-reset-and-enable-done-separator ()
965 "Show resized catagory separator overlay after window size change.
966 Added to `window-configuration-change-hook' in `todos-mode'."
967 (when (= 1 (length todos-done-separator-string))
968 (let ((sep todos-done-separator))
969 (setq todos-done-separator (todos-done-separator))
970 (save-match-data (todos-reset-done-separator sep)))
971 ;; FIXME: If this is called while the separator overlay is shown, the
972 ;; separator with deleted overlay becomes visible when waiting for user
973 ;; input and remains so. The following workaround prevents this, but it
974 ;; also prevents widening category when edebugging todos.el.
975 ;; (save-excursion
976 ;; (goto-char (point-min))
977 ;; (when (re-search-forward todos-done-string-start nil t)
978 ;; (let ((todos-show-with-done nil))
979 ;; (todos-category-select))
980 ;; (let ((todos-show-with-done t))
981 ;; (todos-category-select))))
982 ))
983
984 ;; ---------------------------------------------------------------------------
985 ;;; Global variables and helper functions for files and buffers
986
987 (defvar todos-files (funcall todos-files-function)
988 "List of truenames of user's Todos files.")
989
990 (defvar todos-archives (funcall todos-files-function t)
991 "List of truenames of user's Todos archives.")
992
993 (defvar todos-file-buffers nil
994 "List of file names of live Todos mode buffers.")
995
996 (defvar todos-global-current-todos-file nil
997 "Variable holding name of current Todos file.
998 Used by functions called from outside of Todos mode to visit the
999 current Todos file rather than the default Todos file (i.e. when
1000 users option `todos-show-current-file' is non-nil).")
1001
1002 (defun todos-reevaluate-filelist-defcustoms ()
1003 "Reevaluate defcustoms that provide choice list of Todos files."
1004 (custom-set-default 'todos-default-todos-file
1005 (symbol-value 'todos-default-todos-file))
1006 (todos-reevaluate-default-file-defcustom)
1007 (custom-set-default 'todos-filter-files (symbol-value 'todos-filter-files))
1008 (todos-reevaluate-filter-files-defcustom))
1009
1010 (defvar todos-edit-buffer "*Todos Edit*"
1011 "Name of current buffer in Todos Edit mode.")
1012
1013 (defvar todos-categories-buffer "*Todos Categories*"
1014 "Name of buffer in Todos Categories mode.")
1015
1016 (defvar todos-print-buffer "*Todos Print*"
1017 "Name of buffer containing printable Todos text.")
1018
1019 (defun todos-check-format ()
1020 "Signal an error if the current Todos file is ill-formatted.
1021 Otherwise return t. The error message gives the line number
1022 where the invalid formatting was found."
1023 (save-excursion
1024 (save-restriction
1025 (widen)
1026 (goto-char (point-min))
1027 ;; Check for `todos-categories' sexp as the first line
1028 (let ((cats (prin1-to-string todos-categories)))
1029 (unless (looking-at (regexp-quote cats))
1030 (error "Invalid or missing todos-categories sexp")))
1031 (forward-line)
1032 (let ((legit (concat "\\(^" (regexp-quote todos-category-beg) "\\)"
1033 "\\|\\(" todos-date-string-start todos-date-pattern "\\)"
1034 "\\|\\(^[ \t]+[^ \t]*\\)"
1035 "\\|^$"
1036 "\\|\\(^" (regexp-quote todos-category-done) "\\)"
1037 "\\|\\(" todos-done-string-start "\\)")))
1038 (while (not (eobp))
1039 (unless (looking-at legit)
1040 (error "Illegitimate Todos file format at line %d"
1041 (line-number-at-pos (point))))
1042 (forward-line)))))
1043 ;; (message "This Todos file is well-formatted.")
1044 t)
1045
1046 ;; ---------------------------------------------------------------------------
1047 (defun todos-convert-legacy-date-time ()
1048 "Return converted date-time string.
1049 Helper function for `todos-convert-legacy-files'."
1050 (let* ((year (match-string 1))
1051 (month (match-string 2))
1052 (monthname (calendar-month-name (string-to-number month) t))
1053 (day (match-string 3))
1054 (time (match-string 4))
1055 dayname)
1056 (replace-match "")
1057 (insert (mapconcat 'eval calendar-date-display-form "")
1058 (when time (concat " " time)))))
1059
1060 ;; ---------------------------------------------------------------------------
1061 ;;; Global variables and helper functions for categories
1062
1063 (defun todos-category-number (cat)
1064 "Return the number of category CAT in this Todos file.
1065 The buffer-local variable `todos-category-number' holds this
1066 number as its value."
1067 (let ((categories (mapcar 'car todos-categories)))
1068 (setq todos-category-number
1069 ;; Increment by one, so that the highest priority category in Todos
1070 ;; Categories mode is numbered one rather than zero.
1071 (1+ (- (length categories)
1072 (length (member cat categories)))))))
1073
1074 (defun todos-current-category ()
1075 "Return the name of the current category."
1076 (car (nth (1- todos-category-number) todos-categories)))
1077
1078 (defconst todos-category-beg "--==-- "
1079 "String marking beginning of category (inserted with its name).")
1080
1081 (defconst todos-category-done "==--== DONE "
1082 "String marking beginning of category's done items.")
1083
1084 (defun todos-done-separator ()
1085 "Return string used as value of variable `todos-done-separator'."
1086 (let ((sep todos-done-separator-string))
1087 (if (= 1 (length sep))
1088 (make-string (window-width) (string-to-char sep))
1089 todos-done-separator-string)))
1090
1091 (defvar todos-done-separator (todos-done-separator)
1092 "String used to visually separate done from not done items.
1093 Displayed as an overlay instead of `todos-category-done' when
1094 done items are shown. Its value is determined by user option
1095 `todos-done-separator-string'.")
1096
1097 (defun todos-reset-done-separator (sep)
1098 "Replace existing overlays of done items separator string SEP."
1099 (save-excursion
1100 (save-restriction
1101 (widen)
1102 (goto-char (point-min))
1103 (while (re-search-forward
1104 (concat "\n\\(" (regexp-quote todos-category-done) "\\)") nil t)
1105 (let* ((beg (match-beginning 1))
1106 (end (match-end 0))
1107 (ovs (overlays-at beg))
1108 old-sep new-sep)
1109 (and ovs
1110 (setq old-sep (overlay-get (car ovs) 'display))
1111 (string= old-sep sep)
1112 (delete-overlay (car ovs))
1113 (setq new-sep (make-overlay beg end))
1114 (overlay-put new-sep 'display
1115 todos-done-separator)))))))
1116
1117 (defun todos-category-select ()
1118 "Display the current category correctly."
1119 (let ((name (todos-current-category))
1120 cat-begin cat-end done-start done-sep-start done-end)
1121 (widen)
1122 (goto-char (point-min))
1123 (re-search-forward
1124 (concat "^" (regexp-quote (concat todos-category-beg name)) "$") nil t)
1125 (setq cat-begin (1+ (line-end-position)))
1126 (setq cat-end (if (re-search-forward
1127 (concat "^" (regexp-quote todos-category-beg)) nil t)
1128 (match-beginning 0)
1129 (point-max)))
1130 (setq mode-line-buffer-identification
1131 (funcall todos-mode-line-function name))
1132 (narrow-to-region cat-begin cat-end)
1133 (todos-prefix-overlays)
1134 (goto-char (point-min))
1135 (if (re-search-forward (concat "\n\\(" (regexp-quote todos-category-done)
1136 "\\)") nil t)
1137 (progn
1138 (setq done-start (match-beginning 0))
1139 (setq done-sep-start (match-beginning 1))
1140 (setq done-end (match-end 0)))
1141 (error "Category %s is missing todos-category-done string" name))
1142 (if todos-show-done-only
1143 (narrow-to-region (1+ done-end) (point-max))
1144 (when (and todos-show-with-done
1145 (re-search-forward todos-done-string-start nil t))
1146 ;; Now we want to see the done items, so reset displayed end to end of
1147 ;; done items.
1148 (setq done-start cat-end)
1149 ;; Make display overlay for done items separator string, unless there
1150 ;; already is one.
1151 (let* ((done-sep todos-done-separator)
1152 (ovs (overlays-at done-sep-start))
1153 ov-sep)
1154 ;; There should never be more than one overlay here, so car suffices.
1155 (unless (and ovs (string= (overlay-get (car ovs) 'display) done-sep))
1156 (setq ov-sep (make-overlay done-sep-start done-end))
1157 (overlay-put ov-sep 'display done-sep))))
1158 (narrow-to-region (point-min) done-start)
1159 ;; Loading this from todos-mode, or adding it to the mode hook, causes
1160 ;; Emacs to hang in todos-item-start, at (looking-at todos-item-start).
1161 (when todos-highlight-item
1162 (require 'hl-line)
1163 (hl-line-mode 1)))))
1164
1165 (defun todos-get-count (type &optional category)
1166 "Return count of TYPE items in CATEGORY.
1167 If CATEGORY is nil, default to the current category."
1168 (let* ((cat (or category (todos-current-category)))
1169 (counts (cdr (assoc cat todos-categories)))
1170 (idx (cond ((eq type 'todo) 0)
1171 ((eq type 'diary) 1)
1172 ((eq type 'done) 2)
1173 ((eq type 'archived) 3))))
1174 (aref counts idx)))
1175
1176 (defun todos-update-count (type increment &optional category)
1177 "Change count of TYPE items in CATEGORY by integer INCREMENT.
1178 With nil or omitted CATEGORY, default to the current category."
1179 (let* ((cat (or category (todos-current-category)))
1180 (counts (cdr (assoc cat todos-categories)))
1181 (idx (cond ((eq type 'todo) 0)
1182 ((eq type 'diary) 1)
1183 ((eq type 'done) 2)
1184 ((eq type 'archived) 3))))
1185 (aset counts idx (+ increment (aref counts idx)))))
1186
1187 (defun todos-set-categories ()
1188 "Set `todos-categories' from the sexp at the top of the file."
1189 ;; New archive files created by `todos-move-category' are empty, which would
1190 ;; make the sexp test fail and raise an error, so in this case we skip it.
1191 (unless (zerop (buffer-size))
1192 (save-excursion
1193 (save-restriction
1194 (widen)
1195 (goto-char (point-min))
1196 (setq todos-categories
1197 (if (looking-at "\(\(\"")
1198 (read (buffer-substring-no-properties
1199 (line-beginning-position)
1200 (line-end-position)))
1201 (error "Invalid or missing todos-categories sexp")))))))
1202
1203 (defun todos-update-categories-sexp ()
1204 "Update the `todos-categories' sexp at the top of the file."
1205 (let (buffer-read-only)
1206 (save-excursion
1207 (save-restriction
1208 (widen)
1209 (goto-char (point-min))
1210 (if (looking-at (concat "^" (regexp-quote todos-category-beg)))
1211 (progn (newline) (goto-char (point-min)) ; Make space for sexp.
1212 ;; No categories sexp means the first item was just added
1213 ;; to this file, so have to initialize Todos file and
1214 ;; categories variables in order e.g. to enable categories
1215 ;; display.
1216 (setq todos-default-todos-file (buffer-file-name))
1217 (setq todos-categories (todos-make-categories-list t)))
1218 ;; With empty buffer (e.g. with new archive in
1219 ;; `todos-move-category') `kill-line' signals end of buffer.
1220 (kill-region (line-beginning-position) (line-end-position)))
1221 (prin1 todos-categories (current-buffer))))))
1222
1223 (defun todos-make-categories-list (&optional force)
1224 "Return an alist of Todos categories and their item counts.
1225 With non-nil argument FORCE parse the entire file to build the
1226 list; otherwise, get the value by reading the sexp at the top of
1227 the file."
1228 (setq todos-categories nil)
1229 (save-excursion
1230 (save-restriction
1231 (widen)
1232 (goto-char (point-min))
1233 (let (counts cat archive)
1234 ;; If the file is a todo file and has archived items, identify the
1235 ;; archive, in order to count its items. But skip this with
1236 ;; `todos-convert-legacy-files', since that converts filed items to
1237 ;; archived items.
1238 (when buffer-file-name ; During conversion there is no file yet.
1239 ;; If the file is an archive, it doesn't have an archive.
1240 (unless (member (file-truename buffer-file-name)
1241 (funcall todos-files-function t))
1242 (setq archive (concat (file-name-sans-extension
1243 todos-current-todos-file) ".toda"))))
1244 (while (not (eobp))
1245 (cond ((looking-at (concat (regexp-quote todos-category-beg)
1246 "\\(.*\\)\n"))
1247 (setq cat (match-string-no-properties 1))
1248 ;; Counts for each category: [todo diary done archive]
1249 (setq counts (make-vector 4 0))
1250 (setq todos-categories
1251 (append todos-categories (list (cons cat counts))))
1252 ;; Add archived item count to the todo file item counts.
1253 ;; Make sure to include newly created archives, e.g. due to
1254 ;; todos-move-category.
1255 (when (member archive (funcall todos-files-function t))
1256 (let ((archive-count 0))
1257 (with-current-buffer (find-file-noselect archive)
1258 (widen)
1259 (goto-char (point-min))
1260 (when (re-search-forward
1261 (concat (regexp-quote todos-category-beg) cat)
1262 (point-max) t)
1263 (forward-line)
1264 (while (not (or (looking-at
1265 (concat
1266 (regexp-quote todos-category-beg)
1267 "\\(.*\\)\n"))
1268 (eobp)))
1269 (when (looking-at todos-done-string-start)
1270 (setq archive-count (1+ archive-count)))
1271 (forward-line))))
1272 (todos-update-count 'archived archive-count cat))))
1273 ((looking-at todos-done-string-start)
1274 (todos-update-count 'done 1 cat))
1275 ((looking-at (concat "^\\("
1276 (regexp-quote diary-nonmarking-symbol)
1277 "\\)?" todos-date-pattern))
1278 (todos-update-count 'diary 1 cat)
1279 (todos-update-count 'todo 1 cat))
1280 ((looking-at (concat todos-date-string-start todos-date-pattern))
1281 (todos-update-count 'todo 1 cat))
1282 ;; If first line is todos-categories list, use it and end loop
1283 ;; -- unless FORCEd to scan whole file.
1284 ((bobp)
1285 (unless force
1286 (setq todos-categories (read (buffer-substring-no-properties
1287 (line-beginning-position)
1288 (line-end-position))))
1289 (goto-char (1- (point-max))))))
1290 (forward-line)))))
1291 todos-categories)
1292
1293 (defun todos-repair-categories-sexp ()
1294 "Repair corrupt Todos categories sexp.
1295 This should only be needed as a consequence of careless manual
1296 editing or a bug in todos.el."
1297 (interactive)
1298 (let ((todos-categories (todos-make-categories-list t)))
1299 (todos-update-categories-sexp)))
1300
1301 (defvar todos-allcats-file (concat todos-files-directory "todos-allcats.el")
1302 "Name of file containing the value of `todos-all-categories-alist'.
1303 The contents of this file are automatically generated and
1304 executed when todos.el is loaded, hence users should not edit
1305 it.")
1306
1307 (defun todos-all-categories-alist ()
1308 ""
1309 ;; FIXME: loop through archive files for categories not in todo files?
1310 (let ((files todos-files)
1311 allcats)
1312 (dolist (f files)
1313 ;; FIXME: If file buffer is modified, save first.
1314 (with-temp-buffer
1315 (insert-file-contents f)
1316 (let ((cats (read (buffer-substring-no-properties
1317 (line-beginning-position)
1318 (line-end-position)))))
1319 (dolist (c cats)
1320 (let* ((cat (assoc (car c) allcats))
1321 (catcdr (cdr cat)))
1322 (unless (listp catcdr) (setq catcdr (list catcdr)))
1323 (if cat
1324 (setcdr cat (append catcdr (list (todos-short-file-name f))))
1325 (setq allcats (append allcats
1326 (list
1327 (cons (car c)
1328 (todos-short-file-name f)))))))))))
1329 allcats))
1330
1331 (defvar todos-all-categories-alist (if (file-exists-p todos-allcats-file)
1332 (load-file todos-allcats-file)
1333 (todos-all-categories-alist))
1334 "Alist of names of all Todos categories and their files.")
1335
1336 ;;; Global variables and helper functions for items
1337
1338 (defconst todos-date-pattern
1339 (let ((dayname (diary-name-pattern calendar-day-name-array nil t)))
1340 (concat "\\(?:" dayname "\\|"
1341 (let ((dayname)
1342 ;; FIXME: how to choose between abbreviated and unabbreviated
1343 ;; month name?
1344 (monthname (format "\\(?:%s\\|\\*\\)"
1345 (diary-name-pattern
1346 calendar-month-name-array
1347 calendar-month-abbrev-array t)))
1348 (month "\\(?:[0-9]+\\|\\*\\)")
1349 (day "\\(?:[0-9]+\\|\\*\\)")
1350 (year "-?\\(?:[0-9]+\\|\\*\\)"))
1351 (mapconcat 'eval calendar-date-display-form ""))
1352 "\\)"))
1353 "Regular expression matching a Todos date header.")
1354
1355 (defconst todos-nondiary-start (nth 0 todos-nondiary-marker)
1356 "String inserted before item date to block diary inclusion.")
1357
1358 (defconst todos-nondiary-end (nth 1 todos-nondiary-marker)
1359 "String inserted after item date matching `todos-nondiary-start'.")
1360
1361 ;; By itself this matches anything, because of the `?'; however, it's only
1362 ;; used in the context of `todos-date-pattern' (but Emacs Lisp lacks
1363 ;; lookahead).
1364 (defconst todos-date-string-start
1365 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|"
1366 (regexp-quote diary-nonmarking-symbol) "\\)?")
1367 "Regular expression matching part of item header before the date.")
1368
1369 (defconst todos-done-string-start
1370 (concat "^\\[" (regexp-quote todos-done-string))
1371 "Regular expression matching start of done item.")
1372
1373 (defconst todos-item-start (concat "\\(" todos-date-string-start "\\|"
1374 todos-done-string-start "\\)"
1375 todos-date-pattern)
1376 "String identifying start of a Todos item.")
1377
1378 (defun todos-item-start ()
1379 "Move to start of current Todos item and return its position."
1380 (unless (or
1381 ;; Buffer is empty (invocation possible e.g. via todos-forward-item
1382 ;; from todos-filter-items when processing category with no todo
1383 ;; items).
1384 (eq (point-min) (point-max))
1385 ;; Point is on the empty line below category's last todo item...
1386 (and (looking-at "^$")
1387 (or (eobp) ; ...and done items are hidden...
1388 (save-excursion ; ...or done items are visible.
1389 (forward-line)
1390 (looking-at (concat "^"
1391 (regexp-quote todos-category-done))))))
1392 ;; Buffer is widened.
1393 (looking-at (regexp-quote todos-category-beg)))
1394 (goto-char (line-beginning-position))
1395 (while (not (looking-at todos-item-start))
1396 (forward-line -1))
1397 (point)))
1398
1399 (defun todos-item-end ()
1400 "Move to end of current Todos item and return its position."
1401 ;; Items cannot end with a blank line.
1402 (unless (looking-at "^$")
1403 (let* ((done (todos-done-item-p))
1404 (to-lim nil)
1405 ;; For todo items, end is before the done items section, for done
1406 ;; items, end is before the next category. If these limits are
1407 ;; missing or inaccessible, end it before the end of the buffer.
1408 (lim (if (save-excursion
1409 (re-search-forward
1410 (concat "^" (regexp-quote (if done
1411 todos-category-beg
1412 todos-category-done)))
1413 nil t))
1414 (progn (setq to-lim t) (match-beginning 0))
1415 (point-max))))
1416 (when (bolp) (forward-char)) ; Find start of next item.
1417 (goto-char (if (re-search-forward todos-item-start lim t)
1418 (match-beginning 0)
1419 (if to-lim lim (point-max))))
1420 ;; For last todo item, skip back over the empty line before the done
1421 ;; items section, else just back to the end of the previous line.
1422 (backward-char (when (and to-lim (not done) (eq (point) lim)) 2))
1423 (point))))
1424
1425 (defun todos-item-string ()
1426 "Return bare text of current item as a string."
1427 (let ((opoint (point))
1428 (start (todos-item-start))
1429 (end (todos-item-end)))
1430 (goto-char opoint)
1431 (and start end (buffer-substring-no-properties start end))))
1432
1433 (defun todos-remove-item ()
1434 "Internal function called in editing, deleting or moving items."
1435 (let* ((beg (todos-item-start))
1436 (end (progn (todos-item-end) (1+ (point))))
1437 (ovs (overlays-in beg beg)))
1438 ;; There can be both prefix/number and mark overlays.
1439 (while ovs (delete-overlay (car ovs)) (pop ovs))
1440 (delete-region beg end)))
1441
1442 (defun todos-diary-item-p ()
1443 "Return non-nil if item at point has diary entry format."
1444 (save-excursion
1445 (todos-item-start)
1446 (not (looking-at (regexp-quote todos-nondiary-start)))))
1447
1448 (defun todos-done-item-p ()
1449 "Return non-nil if item at point is a done item."
1450 (save-excursion
1451 (todos-item-start)
1452 (looking-at todos-done-string-start)))
1453
1454 (defvar todos-item-mark (propertize (if (equal todos-prefix "*") "@" "*")
1455 'face 'todos-mark)
1456 "String used to mark items.")
1457
1458 (defun todos-marked-item-p ()
1459 "If this item begins with `todos-item-mark', return mark overlay."
1460 (let ((ovs (overlays-in (line-beginning-position) (line-beginning-position)))
1461 (mark todos-item-mark)
1462 ov marked)
1463 (catch 'stop
1464 (while ovs
1465 (setq ov (pop ovs))
1466 (and (equal (overlay-get ov 'before-string) mark)
1467 (throw 'stop (setq marked t)))))
1468 (when marked ov)))
1469
1470 (defun todos-insert-with-overlays (item)
1471 "Insert ITEM at point and update prefix/priority number overlays."
1472 (todos-item-start)
1473 (insert item "\n")
1474 (todos-backward-item)
1475 (todos-prefix-overlays))
1476
1477 (defun todos-prefix-overlays ()
1478 "Put before-string overlay in front of this category's items.
1479 The overlay's value is the string `todos-prefix' or with non-nil
1480 `todos-number-priorities' an integer in the sequence from 1 to
1481 the number of todo or done items in the category indicating the
1482 item's priority. Todo and done items are numbered independently
1483 of each other."
1484 (when (or todos-number-priorities
1485 (not (string-match "^[[:space:]]*$" todos-prefix)))
1486 (let ((prefix (propertize (concat todos-prefix " ")
1487 'face 'todos-prefix-string))
1488 (num 0))
1489 (save-excursion
1490 (goto-char (point-min))
1491 (while (not (eobp))
1492 (when (or (todos-date-string-matcher (line-end-position))
1493 (todos-done-string-matcher (line-end-position)))
1494 (goto-char (match-beginning 0))
1495 (when todos-number-priorities
1496 (setq num (1+ num))
1497 ;; Reset number to 1 for first done item.
1498 (when (and (looking-at todos-done-string-start)
1499 (looking-back (concat "^"
1500 (regexp-quote todos-category-done)
1501 "\n")))
1502 (setq num 1))
1503 (setq prefix (propertize (concat (number-to-string num) " ")
1504 'face 'todos-prefix-string)))
1505 (let ((ovs (overlays-in (point) (point)))
1506 marked ov-pref)
1507 (if ovs
1508 (dolist (ov ovs)
1509 (let ((val (overlay-get ov 'before-string)))
1510 (if (equal val "*")
1511 (setq marked t)
1512 (setq ov-pref val)))))
1513 (unless (equal ov-pref prefix)
1514 ;; Why doesn't this work?
1515 ;; (remove-overlays (point) (point) 'before-string)
1516 (remove-overlays (point) (point))
1517 (overlay-put (make-overlay (point) (point))
1518 'before-string prefix)
1519 (and marked (overlay-put (make-overlay (point) (point))
1520 'before-string todos-item-mark)))))
1521 (forward-line))))))
1522
1523 ;; ---------------------------------------------------------------------------
1524 ;;; Helper functions for user input with prompting and completion
1525
1526 (defun todos-read-file-name (prompt &optional archive mustmatch)
1527 "Choose and return the name of a Todos file, prompting with PROMPT.
1528
1529 Show completions with TAB or SPC; the names are shown in short
1530 form but the absolute truename is returned. With non-nil ARCHIVE
1531 return the absolute truename of a Todos archive file. With non-nil
1532 MUSTMATCH the name of an existing file must be chosen;
1533 otherwise, a new file name is allowed."
1534 (let* ((completion-ignore-case todos-completion-ignore-case)
1535 (files (mapcar 'todos-short-file-name
1536 (if archive todos-archives todos-files)))
1537 (file (completing-read prompt files nil mustmatch nil nil
1538 (unless files
1539 ;; Trigger prompt for initial file.
1540 ""))))
1541 (unless (file-exists-p todos-files-directory)
1542 (make-directory todos-files-directory))
1543 (unless mustmatch
1544 (setq file (todos-validate-name file 'file)))
1545 (setq file (file-truename (concat todos-files-directory file
1546 (if archive ".toda" ".todo"))))))
1547
1548 (defun todos-read-category (prompt &optional mustmatch added)
1549 "Choose and return a category name, prompting with PROMPT.
1550 Show completions with TAB or SPC. With non-nil MUSTMATCH the
1551 name must be that of an existing category; otherwise, a new
1552 category name is allowed, after checking its validity. Non-nil
1553 argument ADDED means the caller is todos-add-category, so don't
1554 ask whether to add the category."
1555 ;; Allow SPC to insert spaces, for adding new category names.
1556 (let ((map minibuffer-local-completion-map))
1557 (define-key map " " nil)
1558 ;; Make a copy of todos-categories in case history-delete-duplicates is
1559 ;; non-nil, which makes completing-read alter todos-categories.
1560 (let* ((categories (copy-sequence todos-categories))
1561 (history (cons 'todos-categories (1+ todos-category-number)))
1562 (completion-ignore-case todos-completion-ignore-case)
1563 (cat (completing-read prompt todos-categories nil
1564 mustmatch nil history
1565 ;; Default for existing categories is the
1566 ;; current category.
1567 (if todos-categories
1568 (todos-current-category)
1569 ;; Trigger prompt for initial category.
1570 ""))))
1571 (unless (or mustmatch (assoc cat todos-categories))
1572 (todos-validate-name cat 'category)
1573 (unless added
1574 (if (y-or-n-p (format (concat "There is no category \"%s\" in "
1575 "this file; add it? ") cat))
1576 (todos-add-category cat)
1577 (keyboard-quit))))
1578 ;; Restore the original value of todos-categories unless a new category
1579 ;; was added (since todos-add-category changes todos-categories).
1580 (unless added (setq todos-categories categories))
1581 cat)))
1582
1583 (defun todos-validate-name (name type)
1584 "Prompt for new NAME for TYPE until it is valid, then return it.
1585 TYPE can be either a file or a category"
1586 (let ((categories todos-categories)
1587 (files (mapcar 'todos-short-file-name todos-files))
1588 prompt)
1589 (while
1590 (and (cond ((string= "" name)
1591 (setq prompt
1592 (cond ((eq type 'file)
1593 (if todos-files
1594 "Enter a non-empty file name: "
1595 ;; Empty string passed by todos-show to
1596 ;; prompt for initial Todos file.
1597 (concat "Initial file name ["
1598 todos-initial-file "]: ")))
1599 ((eq type 'category)
1600 (if todos-categories
1601 "Enter a non-empty category name: "
1602 ;; Empty string passed by todos-show to
1603 ;; prompt for initial category of a new
1604 ;; Todos file.
1605 (concat "Initial category name ["
1606 todos-initial-category "]: "))))))
1607 ((string-match "\\`\\s-+\\'" name)
1608 (setq prompt
1609 "Enter a name that does not contain only white space: "))
1610 ((and (eq type 'file) (member name todos-files))
1611 (setq prompt "Enter a non-existing file name: "))
1612 ((and (eq type 'category) (assoc name todos-categories))
1613 (setq prompt "Enter a non-existing category name: ")))
1614 (setq name (if (or (and (eq type 'file) todos-files)
1615 (and (eq type 'category) todos-categories))
1616 (completing-read prompt (cond ((eq type 'file)
1617 todos-files)
1618 ((eq type 'category)
1619 todos-categories)))
1620 ;; Offer default initial name.
1621 (completing-read prompt (if (eq type 'file)
1622 todos-files
1623 todos-categories)
1624 nil nil (if (eq type 'file)
1625 todos-initial-file
1626 todos-initial-category))))))
1627 name))
1628
1629 ;; Adapted from calendar-read-date and calendar-date-string.
1630 (defun todos-read-date ()
1631 "Prompt for Gregorian date and return it in the current format.
1632 Also accepts `*' as an unspecified month, day, or year."
1633 (let* ((year (let (x)
1634 (while (if (numberp x) (< x 0) (not (eq x '*)))
1635 (setq x (read-from-minibuffer
1636 "Year (>0 or RET for this year or * for any year): "
1637 nil nil t nil (number-to-string
1638 (calendar-extract-year
1639 (calendar-current-date))))))
1640 x))
1641 (month-array (vconcat calendar-month-name-array (vector "*")))
1642 (abbrevs (vconcat calendar-month-abbrev-array (vector "*")))
1643 (completion-ignore-case todos-completion-ignore-case)
1644 (monthname (completing-read
1645 "Month name (RET for current month, * for any month): "
1646 (mapcar 'list (append month-array nil))
1647 nil t nil nil
1648 (calendar-month-name (calendar-extract-month
1649 (calendar-current-date)) t)))
1650 (month (cdr (assoc-string
1651 monthname (calendar-make-alist month-array nil nil
1652 abbrevs))))
1653 (last (if (= month 13)
1654 ;; Use longest possible month for checking day number
1655 ;; input. Does Calendar do anything special when * is
1656 ;; currently a shorter month?
1657 31
1658 (let ((yr (if (eq year '*)
1659 ;; Use a leap year to allow Feb. 29.
1660 2012
1661 year)))
1662 (calendar-last-day-of-month month yr))))
1663 (day (let (x)
1664 (while (if (numberp x) (or (< x 0) (< last x)) (not (eq x '*)))
1665 (setq x (read-from-minibuffer
1666 (format
1667 "Day (1-%d or RET for today or * for any day): "
1668 last) nil nil t nil (number-to-string
1669 (calendar-extract-day
1670 (calendar-current-date))))))
1671 x))
1672 dayname) ; Needed by calendar-date-display-form.
1673 (setq year (if (eq year '*) (symbol-name '*) (number-to-string year)))
1674 (setq day (if (eq day '*) (symbol-name '*) (number-to-string day)))
1675 ;; FIXME: make abbreviation customizable
1676 (setq monthname
1677 (or (and (= month 13) "*")
1678 (calendar-month-name (calendar-extract-month (list month day year))
1679 t)))
1680 (mapconcat 'eval calendar-date-display-form "")))
1681
1682 (defun todos-read-dayname ()
1683 "Choose name of a day of the week with completion and return it."
1684 (let ((completion-ignore-case todos-completion-ignore-case))
1685 (completing-read "Enter a day name: "
1686 (append calendar-day-name-array nil)
1687 nil t)))
1688
1689 (defun todos-read-time ()
1690 "Prompt for and return a valid clock time as a string.
1691
1692 Valid time strings are those matching `diary-time-regexp'.
1693 Typing `<return>' at the prompt returns the current time, if the
1694 user option `todos-always-add-time-string' is non-nil, otherwise
1695 the empty string (i.e., no time string)."
1696 (let (valid answer)
1697 (while (not valid)
1698 (setq answer (read-string "Enter a clock time: " nil nil
1699 (when todos-always-add-time-string
1700 (substring (current-time-string) 11 16))))
1701 (when (or (string= "" answer)
1702 (string-match diary-time-regexp answer))
1703 (setq valid t)))
1704 answer))
1705
1706 ;; ---------------------------------------------------------------------------
1707 ;;; Item filtering infrastructure
1708
1709 (defvar todos-multiple-filter-files nil
1710 "List of files selected from `todos-multiple-filter-files' widget.")
1711
1712 (defvar todos-multiple-filter-files-widget nil
1713 "Variable holding widget created by `todos-multiple-filter-files'.")
1714
1715 (defun todos-multiple-filter-files ()
1716 "Pop to a buffer with a widget for choosing multiple filter files."
1717 (require 'widget)
1718 (eval-when-compile
1719 (require 'wid-edit))
1720 (with-current-buffer (get-buffer-create "*Todos Filter Files*")
1721 (pop-to-buffer (current-buffer))
1722 (erase-buffer)
1723 (kill-all-local-variables)
1724 (widget-insert "Select files for generating the top priorities list.\n\n")
1725 (setq todos-multiple-filter-files-widget
1726 (widget-create
1727 `(set ,@(mapcar (lambda (x) (list 'const x))
1728 (mapcar 'todos-short-file-name
1729 (funcall todos-files-function))))))
1730 (widget-insert "\n")
1731 (widget-create 'push-button
1732 :notify (lambda (widget &rest ignore)
1733 (setq todos-multiple-filter-files 'quit)
1734 (quit-window t)
1735 (exit-recursive-edit))
1736 "Cancel")
1737 (widget-insert " ")
1738 (widget-create 'push-button
1739 :notify (lambda (&rest ignore)
1740 (setq todos-multiple-filter-files
1741 (mapcar (lambda (f)
1742 (concat todos-files-directory
1743 f ".todo"))
1744 (widget-value
1745 todos-multiple-filter-files-widget)))
1746 (quit-window t)
1747 (exit-recursive-edit))
1748 "Apply")
1749 (use-local-map widget-keymap)
1750 (widget-setup))
1751 (message "Click \"Apply\" after selecting files.")
1752 (recursive-edit))
1753
1754 (defun todos-filter-items (filter &optional multifile)
1755 "Build and display a list of items from different categories.
1756
1757 The items are selected according to the value of FILTER, which
1758 can be `top' for top priority items, `diary' for diary items,
1759 `regexp' for items matching a regular expresion entered by the
1760 user, or a cons cell of one of these symbols and a number set by
1761 the calling command, which overrides `todos-show-priorities'.
1762
1763 With non-nil argument MULTIFILE list top priorities of multiple
1764 Todos files, by default those in `todos-filter-files'."
1765 (let ((num (if (consp filter) (cdr filter) todos-show-priorities))
1766 (buf (get-buffer-create todos-filtered-items-buffer))
1767 (files (list todos-current-todos-file))
1768 regexp fname bufstr cat beg end done)
1769 (when multifile
1770 (setq files (or todos-multiple-filter-files ; Passed from todos-*-multifile.
1771 (if (or (consp filter)
1772 (null todos-filter-files))
1773 (progn (todos-multiple-filter-files)
1774 todos-multiple-filter-files)
1775 todos-filter-files))
1776 todos-multiple-filter-files nil))
1777 (if (eq files 'quit) (keyboard-quit))
1778 (if (null files)
1779 (error "No files have been chosen for filtering")
1780 (with-current-buffer buf
1781 (erase-buffer)
1782 (kill-all-local-variables)
1783 (todos-filtered-items-mode))
1784 (when (eq filter 'regexp)
1785 (setq regexp (read-string "Enter a regular expression: ")))
1786 (save-current-buffer
1787 (dolist (f files)
1788 ;; Before inserting file contents into temp buffer, save a modified
1789 ;; buffer visiting it.
1790 (let ((bf (find-buffer-visiting f)))
1791 (when (buffer-modified-p bf)
1792 (with-current-buffer bf (save-buffer))))
1793 (setq fname (todos-short-file-name f))
1794 (with-temp-buffer
1795 (when (and todos-filter-done-items (eq filter 'regexp))
1796 ;; If there is a corresponding archive file for the Todos file,
1797 ;; insert it first and add identifiers for todos-jump-to-item.
1798 (let ((arch (concat (file-name-sans-extension f) ".toda")))
1799 (when (file-exists-p arch)
1800 (insert-file-contents arch)
1801 ;; Delete Todos archive file categories sexp.
1802 (delete-region (line-beginning-position)
1803 (1+ (line-end-position)))
1804 (save-excursion
1805 (while (not (eobp))
1806 (when (re-search-forward
1807 (concat (if todos-filter-done-items
1808 (concat "\\(?:" todos-done-string-start
1809 "\\|" todos-date-string-start
1810 "\\)")
1811 todos-date-string-start)
1812 todos-date-pattern "\\(?: "
1813 diary-time-regexp "\\)?"
1814 (if todos-filter-done-items
1815 "\\]"
1816 (regexp-quote todos-nondiary-end)) "?")
1817 nil t)
1818 (insert "(archive) "))
1819 (forward-line))))))
1820 (insert-file-contents f)
1821 ;; Delete Todos file categories sexp.
1822 (delete-region (line-beginning-position) (1+ (line-end-position)))
1823 (let (fnum)
1824 ;; Unless the number of items to show was supplied by prefix
1825 ;; argument of caller, the file-wide value from
1826 ;; `todos-priorities-rules', if non-nil, overrides
1827 ;; `todos-show-priorities'.
1828 (unless (consp filter)
1829 (setq fnum (nth 1 (assoc f todos-priorities-rules))))
1830 (while (re-search-forward
1831 (concat "^" (regexp-quote todos-category-beg) "\\(.+\\)\n")
1832 nil t)
1833 (setq cat (match-string 1))
1834 (let (cnum)
1835 ;; Unless the number of items to show was supplied by prefix
1836 ;; argument of caller, the category-wide value from
1837 ;; `todos-priorities-rules', if non-nil, overrides a non-nil
1838 ;; file-wide value from `todos-priorities-rules' as well as
1839 ;; `todos-show-priorities'.
1840 (unless (consp filter)
1841 (let ((cats (nth 2 (assoc f todos-priorities-rules))))
1842 (setq cnum (or (cdr (assoc cat cats)) fnum))))
1843 (delete-region (match-beginning 0) (match-end 0))
1844 (setq beg (point)) ; First item in the current category.
1845 (setq end (if (re-search-forward
1846 (concat "^" (regexp-quote todos-category-beg))
1847 nil t)
1848 (match-beginning 0)
1849 (point-max)))
1850 (goto-char beg)
1851 (setq done
1852 (if (re-search-forward
1853 (concat "\n" (regexp-quote todos-category-done))
1854 end t)
1855 (match-beginning 0)
1856 end))
1857 (unless (and todos-filter-done-items (eq filter 'regexp))
1858 ;; Leave done items.
1859 (delete-region done end)
1860 (setq end done))
1861 (narrow-to-region beg end) ; Process only current category.
1862 (goto-char (point-min))
1863 ;; Apply the filter.
1864 (cond ((eq filter 'diary)
1865 (while (not (eobp))
1866 (if (looking-at (regexp-quote todos-nondiary-start))
1867 (todos-remove-item)
1868 (todos-forward-item))))
1869 ((eq filter 'regexp)
1870 (while (not (eobp))
1871 (if (looking-at todos-item-start)
1872 (if (string-match regexp (todos-item-string))
1873 (todos-forward-item)
1874 (todos-remove-item))
1875 ;; Kill lines that aren't part of a todo or done
1876 ;; item (empty or todos-category-done).
1877 (delete-region (line-beginning-position)
1878 (1+ (line-end-position))))
1879 ;; If last todo item in file matches regexp and
1880 ;; there are no following done items,
1881 ;; todos-category-done string is left dangling,
1882 ;; because todos-forward-item jumps over it.
1883 (if (and (eobp)
1884 (looking-back
1885 (concat (regexp-quote todos-done-string)
1886 "\n")))
1887 (delete-region (point) (progn
1888 (forward-line -2)
1889 (point))))))
1890 (t ; Filter top priority items.
1891 (setq num (or cnum fnum num))
1892 (unless (zerop num)
1893 (todos-forward-item num))))
1894 (setq beg (point))
1895 ;; Delete non-top-priority items.
1896 (unless (member filter '(diary regexp))
1897 (delete-region beg end))
1898 (goto-char (point-min))
1899 ;; Add file (if using multiple files) and category tags to
1900 ;; item.
1901 (while (not (eobp))
1902 (when (re-search-forward
1903 (concat (if todos-filter-done-items
1904 (concat "\\(?:" todos-done-string-start
1905 "\\|" todos-date-string-start
1906 "\\)")
1907 todos-date-string-start)
1908 todos-date-pattern "\\(?: " diary-time-regexp
1909 "\\)?" (if todos-filter-done-items
1910 "\\]"
1911 (regexp-quote todos-nondiary-end))
1912 "?")
1913 nil t)
1914 (insert " [")
1915 (when (looking-at "(archive) ") (goto-char (match-end 0)))
1916 (insert (if multifile (concat fname ":") "") cat "]"))
1917 (forward-line))
1918 (widen)))
1919 (setq bufstr (buffer-string))
1920 (with-current-buffer buf
1921 (let (buffer-read-only)
1922 (insert bufstr)))))))
1923 (set-window-buffer (selected-window) (set-buffer buf))
1924 (todos-prefix-overlays)
1925 (goto-char (point-min)))))
1926
1927 (defun todos-set-top-priorities (&optional arg)
1928 "Set number of top priorities shown by `todos-top-priorities'.
1929 With non-nil ARG, set the number only for the current Todos
1930 category; otherwise, set the number for all categories in the
1931 current Todos file.
1932
1933 Calling this function via either of the commands
1934 `todos-set-top-priorities-in-file' or
1935 `todos-set-top-priorities-in-category' is the recommended way to
1936 set the user customizable option `todos-priorities-rules'."
1937 (let* ((cat (todos-current-category))
1938 (file todos-current-todos-file)
1939 (rules todos-priorities-rules)
1940 (frule (assoc-string file rules))
1941 (crule (assoc-string cat (nth 2 frule)))
1942 (cur (or (if arg (cdr crule) (nth 1 frule))
1943 todos-show-priorities))
1944 (prompt (concat "Current number of top priorities in this "
1945 (if arg "category" "file") ": %d; "
1946 "enter new number: "))
1947 (new "-1")
1948 nrule)
1949 (while (< (string-to-number new) 0)
1950 (let ((cur0 cur))
1951 (setq new (read-number (format prompt cur0) cur0)
1952 prompt "Enter a non-negative number: "
1953 cur0 nil)))
1954 (setq nrule (if arg
1955 (append (nth 2 (delete crule frule)) (list (cons cat new)))
1956 (append (list file new) (list (nth 2 frule)))))
1957 (setq rules (cons (if arg
1958 (list file cur nrule)
1959 nrule)
1960 (delete frule rules)))
1961 (customize-save-variable 'todos-priorities-rules rules)))
1962
1963 (defun todos-filtered-buffer-name (buffer-type file-list)
1964 "Rename Todos filtered buffer using BUFFER-TYPE and FILE-LIST.
1965
1966 The new name is constructed from the string BUFFER-TYPE, which
1967 refers to one of the top priorities, diary or regexp item
1968 filters, and the names of the filtered files in FILE-LIST. Used
1969 in Todos Filter Items mode."
1970 (let* ((flist (if (listp file-list) file-list (list file-list)))
1971 (multi (> (length flist) 1))
1972 (fnames (mapconcat (lambda (f) (todos-short-file-name f))
1973 flist ", ")))
1974 (rename-buffer (format (concat "%s for file" (if multi "s" "")
1975 " \"%s\"") buffer-type fnames))))
1976
1977 ;; ---------------------------------------------------------------------------
1978 ;;; Sorting and display routines for Todos Categories mode.
1979
1980 (defun todos-longest-category-name-length (categories)
1981 "Return the length of the longest name in list CATEGORIES."
1982 (let ((longest 0))
1983 (dolist (c categories longest)
1984 (setq longest (max longest (length c))))))
1985
1986 (defun todos-padded-string (str)
1987 "Return string STR padded with spaces.
1988 The placement of the padding is determined by the value of user
1989 option `todos-categories-align'."
1990 (let* ((categories (mapcar 'car todos-categories))
1991 (len (max (todos-longest-category-name-length categories)
1992 (length todos-categories-category-label)))
1993 (strlen (length str))
1994 (strlen-odd (eq (logand strlen 1) 1)) ; oddp from cl.el
1995 (padding (max 0 (/ (- len strlen) 2)))
1996 (padding-left (cond ((eq todos-categories-align 'left) 0)
1997 ((eq todos-categories-align 'center) padding)
1998 ((eq todos-categories-align 'right)
1999 (if strlen-odd (1+ (* padding 2)) (* padding 2)))))
2000 (padding-right (cond ((eq todos-categories-align 'left)
2001 (if strlen-odd (1+ (* padding 2)) (* padding 2)))
2002 ((eq todos-categories-align 'center)
2003 (if strlen-odd (1+ padding) padding))
2004 ((eq todos-categories-align 'right) 0))))
2005 (concat (make-string padding-left 32) str (make-string padding-right 32))))
2006
2007 (defvar todos-descending-counts nil
2008 "List of keys for category counts sorted in descending order.")
2009
2010 (defun todos-sort (list &optional key)
2011 "Return a copy of LIST, possibly sorted according to KEY."
2012 (let* ((l (copy-sequence list))
2013 (fn (if (eq key 'alpha)
2014 (lambda (x) (upcase x)) ; Alphabetize case insensitively.
2015 (lambda (x) (todos-get-count key x))))
2016 ;; Keep track of whether the last sort by key was descending or
2017 ;; ascending.
2018 (descending (member key todos-descending-counts))
2019 (cmp (if (eq key 'alpha)
2020 'string<
2021 (if descending '< '>)))
2022 (pred (lambda (s1 s2) (let ((t1 (funcall fn (car s1)))
2023 (t2 (funcall fn (car s2))))
2024 (funcall cmp t1 t2)))))
2025 (when key
2026 (setq l (sort l pred))
2027 ;; Switch between descending and ascending sort order.
2028 (if descending
2029 (setq todos-descending-counts
2030 (delete key todos-descending-counts))
2031 (push key todos-descending-counts)))
2032 l))
2033
2034 (defun todos-display-sorted (type)
2035 "Keep point on the TYPE count sorting button just clicked."
2036 (let ((opoint (point)))
2037 (todos-update-categories-display type)
2038 (goto-char opoint)))
2039
2040 (defun todos-label-to-key (label)
2041 "Return symbol for sort key associated with LABEL."
2042 (let (key)
2043 (cond ((string= label todos-categories-category-label)
2044 (setq key 'alpha))
2045 ((string= label todos-categories-todo-label)
2046 (setq key 'todo))
2047 ((string= label todos-categories-diary-label)
2048 (setq key 'diary))
2049 ((string= label todos-categories-done-label)
2050 (setq key 'done))
2051 ((string= label todos-categories-archived-label)
2052 (setq key 'archived)))
2053 key))
2054
2055 (defun todos-insert-sort-button (label)
2056 "Insert button for displaying categories sorted by item counts.
2057 LABEL determines which type of count is sorted."
2058 (setq str (if (string= label todos-categories-category-label)
2059 (todos-padded-string label)
2060 label))
2061 (setq beg (point))
2062 (setq end (+ beg (length str)))
2063 (insert-button str 'face nil
2064 'action
2065 `(lambda (button)
2066 (let ((key (todos-label-to-key ,label)))
2067 (if (and (member key todos-descending-counts)
2068 (eq key 'alpha))
2069 (progn
2070 ;; If display is alphabetical, switch back to
2071 ;; category priority order.
2072 (todos-display-sorted nil)
2073 (setq todos-descending-counts
2074 (delete key todos-descending-counts)))
2075 (todos-display-sorted key)))))
2076 (setq ovl (make-overlay beg end))
2077 (overlay-put ovl 'face 'todos-button))
2078
2079 (defun todos-total-item-counts ()
2080 "Return a list of total item counts for the current file."
2081 (mapcar (lambda (i) (apply '+ (mapcar (lambda (l) (aref l i))
2082 (mapcar 'cdr todos-categories))))
2083 (list 0 1 2 3)))
2084
2085 (defvar todos-categories-category-number 0
2086 "Variable for numbering categories in Todos Categories mode.")
2087
2088 (defun todos-insert-category-line (cat &optional nonum)
2089 "Insert button with category CAT's name and item counts.
2090 With non-nil argument NONUM show only these; otherwise, insert a
2091 number in front of the button indicating the category's priority.
2092 The number and the category name are separated by the string
2093 which is the value of the user option
2094 `todos-categories-number-separator'."
2095 (let ((archive (member todos-current-todos-file todos-archives))
2096 (num todos-categories-category-number)
2097 (str (todos-padded-string cat))
2098 (opoint (point)))
2099 (setq num (1+ num) todos-categories-category-number num)
2100 (insert-button
2101 (concat (if nonum
2102 (make-string (+ 4 (length todos-categories-number-separator))
2103 32)
2104 (format " %3d%s" num todos-categories-number-separator))
2105 str
2106 (mapconcat (lambda (elt)
2107 (concat
2108 (make-string (1+ (/ (length (car elt)) 2)) 32) ; label
2109 (format "%3d" (todos-get-count (cdr elt) cat)) ; count
2110 ;; Add an extra space if label length is odd
2111 ;; (using def of oddp from cl.el).
2112 (if (eq (logand (length (car elt)) 1) 1) " ")))
2113 (if archive
2114 (list (cons todos-categories-done-label 'done))
2115 (list (cons todos-categories-todo-label 'todo)
2116 (cons todos-categories-diary-label 'diary)
2117 (cons todos-categories-done-label 'done)
2118 (cons todos-categories-archived-label
2119 'archived)))
2120 "")
2121 " ") ; So highlighting of last column is consistent with the others.
2122 'face (if (and todos-skip-archived-categories
2123 (zerop (todos-get-count 'todo cat))
2124 (zerop (todos-get-count 'done cat))
2125 (not (zerop (todos-get-count 'archived cat))))
2126 'todos-archived-only
2127 nil)
2128 'action `(lambda (button) (let ((buf (current-buffer)))
2129 (todos-jump-to-category ,cat)
2130 (kill-buffer buf))))
2131 ;; Highlight the sorted count column.
2132 (let* ((beg (+ opoint 7 (length str)))
2133 end ovl)
2134 (cond ((eq nonum 'todo)
2135 (setq beg (+ beg 1 (/ (length todos-categories-todo-label) 2))))
2136 ((eq nonum 'diary)
2137 (setq beg (+ beg 1 (length todos-categories-todo-label)
2138 2 (/ (length todos-categories-diary-label) 2))))
2139 ((eq nonum 'done)
2140 (setq beg (+ beg 1 (length todos-categories-todo-label)
2141 2 (length todos-categories-diary-label)
2142 2 (/ (length todos-categories-done-label) 2))))
2143 ((eq nonum 'archived)
2144 (setq beg (+ beg 1 (length todos-categories-todo-label)
2145 2 (length todos-categories-diary-label)
2146 2 (length todos-categories-done-label)
2147 2 (/ (length todos-categories-archived-label) 2)))))
2148 (unless (= beg (+ opoint 7 (length str))) ; Don't highlight categories.
2149 (setq end (+ beg 4))
2150 (setq ovl (make-overlay beg end))
2151 (overlay-put ovl 'face 'todos-sorted-column)))
2152 (newline)))
2153
2154 (defun todos-display-categories-1 ()
2155 "Prepare buffer for displaying table of categories and item counts."
2156 (unless (eq major-mode 'todos-categories-mode)
2157 (setq todos-global-current-todos-file (or todos-current-todos-file
2158 todos-default-todos-file))
2159 (set-window-buffer (selected-window)
2160 (set-buffer (get-buffer-create todos-categories-buffer)))
2161 (kill-all-local-variables)
2162 (todos-categories-mode)
2163 (let ((archive (member todos-current-todos-file todos-archives))
2164 buffer-read-only)
2165 (erase-buffer)
2166 ;; FIXME: add usage tips?
2167 (insert (format (concat "Category counts for Todos "
2168 (if archive "archive" "file")
2169 " \"%s\".")
2170 (todos-short-file-name todos-current-todos-file)))
2171 (newline 2)
2172 ;; Make space for the column of category numbers.
2173 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32))
2174 ;; Add the category and item count buttons (if this is the list of
2175 ;; categories in an archive, show only done item counts).
2176 (todos-insert-sort-button todos-categories-category-label)
2177 (if archive
2178 (progn
2179 (insert (make-string 3 32))
2180 (todos-insert-sort-button todos-categories-done-label))
2181 (insert (make-string 3 32))
2182 (todos-insert-sort-button todos-categories-todo-label)
2183 (insert (make-string 2 32))
2184 (todos-insert-sort-button todos-categories-diary-label)
2185 (insert (make-string 2 32))
2186 (todos-insert-sort-button todos-categories-done-label)
2187 (insert (make-string 2 32))
2188 (todos-insert-sort-button todos-categories-archived-label))
2189 (newline 2))))
2190
2191 (defun todos-update-categories-display (sortkey)
2192 ""
2193 (let* ((cats0 todos-categories)
2194 (cats (todos-sort cats0 sortkey))
2195 (archive (member todos-current-todos-file todos-archives))
2196 (todos-categories-category-number 0)
2197 ;; Find start of Category button if we just entered Todos Categories
2198 ;; mode.
2199 (pt (if (eq (point) (point-max))
2200 (save-excursion
2201 (forward-line -2)
2202 (goto-char (next-single-char-property-change
2203 (point) 'face nil (line-end-position))))))
2204 (buffer-read-only))
2205 (forward-line 2)
2206 (delete-region (point) (point-max))
2207 ;; Fill in the table with buttonized lines, each showing a category and
2208 ;; its item counts.
2209 (mapc (lambda (cat) (todos-insert-category-line cat sortkey))
2210 (mapcar 'car cats))
2211 (newline)
2212 ;; Add a line showing item count totals.
2213 (insert (make-string (+ 4 (length todos-categories-number-separator)) 32)
2214 (todos-padded-string todos-categories-totals-label)
2215 (mapconcat
2216 (lambda (elt)
2217 (concat
2218 (make-string (1+ (/ (length (car elt)) 2)) 32)
2219 (format "%3d" (nth (cdr elt) (todos-total-item-counts)))
2220 ;; Add an extra space if label length is odd (using
2221 ;; definition of oddp from cl.el).
2222 (if (eq (logand (length (car elt)) 1) 1) " ")))
2223 (if archive
2224 (list (cons todos-categories-done-label 2))
2225 (list (cons todos-categories-todo-label 0)
2226 (cons todos-categories-diary-label 1)
2227 (cons todos-categories-done-label 2)
2228 (cons todos-categories-archived-label 3)))
2229 ""))
2230 ;; Put cursor on Category button initially.
2231 (if pt (goto-char pt))
2232 (setq buffer-read-only t)))
2233
2234 ;; ---------------------------------------------------------------------------
2235 ;;; Routines for generating Todos insertion commands and key bindings
2236
2237 ;; Can either of these be included in Emacs? The originals are GFDL'd.
2238
2239 ;; Slightly reformulated from
2240 ;; http://rosettacode.org/wiki/Power_set#Common_Lisp.
2241 (defun powerset-recursive (l)
2242 (cond ((null l)
2243 (list nil))
2244 (t
2245 (let ((prev (powerset-recursive (cdr l))))
2246 (append (mapcar (lambda (elt) (cons (car l) elt))
2247 prev)
2248 prev)))))
2249
2250 ;; Elisp implementation of http://rosettacode.org/wiki/Power_set#C
2251 (defun powerset-bitwise (l)
2252 (let ((binnum (lsh 1 (length l)))
2253 pset elt)
2254 (dotimes (i binnum)
2255 (let ((bits i)
2256 (ll l))
2257 (while (not (zerop bits))
2258 (let ((arg (pop ll)))
2259 (unless (zerop (logand bits 1))
2260 (setq elt (append elt (list arg))))
2261 (setq bits (lsh bits -1))))
2262 (setq pset (append pset (list elt)))
2263 (setq elt nil)))
2264 pset))
2265
2266 ;; (defalias 'todos-powerset 'powerset-recursive)
2267 (defalias 'todos-powerset 'powerset-bitwise)
2268
2269 ;; Return list of lists of non-nil atoms produced from ARGLIST. The elements
2270 ;; of ARGLIST may be atoms or lists.
2271 (defun todos-gen-arglists (arglist)
2272 (let (arglists)
2273 (while arglist
2274 (let ((arg (pop arglist)))
2275 (cond ((symbolp arg)
2276 (setq arglists (if arglists
2277 (mapcar (lambda (l) (push arg l)) arglists)
2278 (list (push arg arglists)))))
2279 ((listp arg)
2280 (setq arglists
2281 (mapcar (lambda (a)
2282 (if (= 1 (length arglists))
2283 (apply (lambda (l) (push a l)) arglists)
2284 (mapcar (lambda (l) (push a l)) arglists)))
2285 arg))))))
2286 (setq arglists (mapcar 'reverse (apply 'append (mapc 'car arglists))))))
2287
2288 (defvar todos-insertion-commands-args-genlist
2289 '(diary nonmarking (calendar date dayname) time (here region))
2290 "Generator list for argument lists of Todos insertion commands.")
2291
2292 (defvar todos-insertion-commands-args
2293 (let ((argslist (todos-gen-arglists todos-insertion-commands-args-genlist))
2294 res new)
2295 (setq res (remove-duplicates
2296 (apply 'append (mapcar 'todos-powerset argslist)) :test 'equal))
2297 (dolist (l res)
2298 (unless (= 5 (length l))
2299 (let ((v (make-vector 5 nil)) elt)
2300 (while l
2301 (setq elt (pop l))
2302 (cond ((eq elt 'diary)
2303 (aset v 0 elt))
2304 ((eq elt 'nonmarking)
2305 (aset v 1 elt))
2306 ((or (eq elt 'calendar)
2307 (eq elt 'date)
2308 (eq elt 'dayname))
2309 (aset v 2 elt))
2310 ((eq elt 'time)
2311 (aset v 3 elt))
2312 ((or (eq elt 'here)
2313 (eq elt 'region))
2314 (aset v 4 elt))))
2315 (setq l (append v nil))))
2316 (setq new (append new (list l))))
2317 new)
2318 "List of all argument lists for Todos insertion commands.")
2319
2320 (defun todos-insertion-command-name (arglist)
2321 "Generate Todos insertion command name from ARGLIST."
2322 (replace-regexp-in-string
2323 "-\\_>" ""
2324 (replace-regexp-in-string
2325 "-+" "-"
2326 (concat "todos-item-insert-"
2327 (mapconcat (lambda (e) (if e (symbol-name e))) arglist "-")))))
2328
2329 (defvar todos-insertion-commands-names
2330 (mapcar (lambda (l)
2331 (todos-insertion-command-name l))
2332 todos-insertion-commands-args)
2333 "List of names of Todos insertion commands.")
2334
2335 (defmacro todos-define-insertion-command (&rest args)
2336 (let ((name (intern (todos-insertion-command-name args)))
2337 (arg0 (nth 0 args))
2338 (arg1 (nth 1 args))
2339 (arg2 (nth 2 args))
2340 (arg3 (nth 3 args))
2341 (arg4 (nth 4 args)))
2342 `(defun ,name (&optional arg)
2343 "Todos item insertion command generated from ARGS."
2344 (interactive)
2345 (todos-insert-item arg ',arg0 ',arg1 ',arg2 ',arg3 ',arg4))))
2346
2347 (defvar todos-insertion-commands
2348 (mapcar (lambda (c)
2349 (eval `(todos-define-insertion-command ,@c)))
2350 todos-insertion-commands-args)
2351 "List of Todos insertion commands.")
2352
2353 (defvar todos-insertion-commands-arg-key-list
2354 '(("diary" "y" "yy")
2355 ("nonmarking" "k" "kk")
2356 ("calendar" "c" "cc")
2357 ("date" "d" "dd")
2358 ("dayname" "n" "nn")
2359 ("time" "t" "tt")
2360 ("here" "h" "h")
2361 ("region" "r" "r"))
2362 "")
2363
2364 (defun todos-insertion-key-bindings (map)
2365 ""
2366 (dolist (c todos-insertion-commands)
2367 (let* ((key "")
2368 (cname (symbol-name c)))
2369 (mapc (lambda (l)
2370 (let ((arg (nth 0 l))
2371 (key1 (nth 1 l))
2372 (key2 (nth 2 l)))
2373 (if (string-match (concat (regexp-quote arg) "\\_>") cname)
2374 (setq key (concat key key2)))
2375 (if (string-match (concat (regexp-quote arg) ".+") cname)
2376 (setq key (concat key key1)))))
2377 todos-insertion-commands-arg-key-list)
2378 (if (string-match (concat (regexp-quote "todos-item-insert") "\\_>") cname)
2379 (setq key (concat key "i")))
2380 (define-key map key c))))
2381
2382 (defvar todos-insertion-map
2383 (let ((map (make-keymap)))
2384 (todos-insertion-key-bindings map)
2385 map)
2386 "Keymap for Todos mode insertion commands.")
2387
2388 ;; ---------------------------------------------------------------------------
2389 ;;; Key maps and menus
2390
2391 (defvar todos-key-bindings
2392 `(
2393 ;; display
2394 ("Cd" . todos-display-categories) ;FIXME: Cs todos-show-categories?
2395 ("H" . todos-highlight-item)
2396 ("N" . todos-hide-show-item-numbering)
2397 ("D" . todos-hide-show-date-time)
2398 ("*" . todos-mark-unmark-item)
2399 ("C*" . todos-mark-category)
2400 ("Cu" . todos-unmark-category)
2401 ("PP" . todos-print)
2402 ("PF" . todos-print-to-file)
2403 ("v" . todos-hide-show-done-items)
2404 ("V" . todos-show-done-only)
2405 ("As" . todos-show-archive)
2406 ("Ac" . todos-choose-archive)
2407 ("Y" . todos-diary-items)
2408 ("Fe" . todos-edit-multiline)
2409 ("Fh" . todos-highlight-item)
2410 ("Fn" . todos-hide-show-item-numbering)
2411 ("Fd" . todos-hide-show-date-time)
2412 ("Ftt" . todos-top-priorities)
2413 ("Ftm" . todos-top-priorities-multifile)
2414 ("Fts" . todos-set-top-priorities-in-file)
2415 ("Cts" . todos-set-top-priorities-in-category)
2416 ("Fyy" . todos-diary-items)
2417 ("Fym" . todos-diary-items-multifile)
2418 ("Fxx" . todos-regexp-items)
2419 ("Fxm" . todos-regexp-items-multifile)
2420 ;; navigation
2421 ("f" . todos-forward-category)
2422 ("b" . todos-backward-category)
2423 ("j" . todos-jump-to-category)
2424 ("J" . todos-jump-to-category-other-file)
2425 ("n" . todos-forward-item)
2426 ("p" . todos-backward-item)
2427 ("S" . todos-search)
2428 ("X" . todos-clear-matches)
2429 ;; editing
2430 ("Fa" . todos-add-file)
2431 ("Ca" . todos-add-category)
2432 ("Cr" . todos-rename-category)
2433 ("Cg" . todos-merge-category)
2434 ("Cm" . todos-move-category)
2435 ("Ck" . todos-delete-category)
2436 ("d" . todos-item-done)
2437 ("ee" . todos-edit-item)
2438 ("em" . todos-edit-multiline-item)
2439 ("eh" . todos-edit-item-header)
2440 ("edd" . todos-edit-item-date)
2441 ("edc" . todos-edit-item-date-from-calendar)
2442 ("edt" . todos-edit-item-date-is-today)
2443 ("et" . todos-edit-item-time)
2444 ("eyy" . todos-edit-item-diary-inclusion)
2445 ;; ("" . todos-edit-category-diary-inclusion)
2446 ("eyn" . todos-edit-item-diary-nonmarking)
2447 ;;("" . todos-edit-category-diary-nonmarking)
2448 ("ec" . todos-done-item-add-edit-or-delete-comment)
2449 ("i" . ,todos-insertion-map)
2450 ("k" . todos-delete-item) ;FIXME: not single letter?
2451 ("m" . todos-move-item)
2452 ("M" . todos-move-item-to-file)
2453 ("r" . todos-raise-item-priority)
2454 ("l" . todos-lower-item-priority)
2455 ("#" . todos-set-item-priority)
2456 ("u" . todos-item-undo)
2457 ("Ad" . todos-archive-done-item) ;FIXME: ad
2458 ("AD" . todos-archive-category-done-items) ;FIXME: aD or C-u ad ?
2459 ("s" . todos-save)
2460 ("q" . todos-quit)
2461 ([remap newline] . newline-and-indent)
2462 )
2463 "Alist pairing keys defined in Todos modes and their bindings.")
2464
2465 (defvar todos-mode-map
2466 (let ((map (make-keymap)))
2467 ;; Don't suppress digit keys, so they can supply prefix arguments.
2468 (suppress-keymap map)
2469 (dolist (ck todos-key-bindings)
2470 (define-key map (car ck) (cdr ck)))
2471 map)
2472 "Todos mode keymap.")
2473
2474 ;; FIXME
2475 (easy-menu-define
2476 todos-menu todos-mode-map "Todos Menu"
2477 '("Todos"
2478 ("Navigation"
2479 ["Next Item" todos-forward-item t]
2480 ["Previous Item" todos-backward-item t]
2481 "---"
2482 ["Next Category" todos-forward-category t]
2483 ["Previous Category" todos-backward-category t]
2484 ["Jump to Category" todos-jump-to-category t]
2485 ["Jump to Category in Other File" todos-jump-to-category-other-file t]
2486 "---"
2487 ["Search Todos File" todos-search t]
2488 ["Clear Highlighting on Search Matches" todos-category-done t])
2489 ("Display"
2490 ["List Current Categories" todos-display-categories t]
2491 ;; ["List Categories Alphabetically" todos-display-categories-alphabetically t]
2492 ["Turn Item Highlighting on/off" todos-highlight-item t]
2493 ["Turn Item Numbering on/off" todos-hide-show-item-numbering t]
2494 ["Turn Item Time Stamp on/off" todos-hide-show-date-time t]
2495 ["View/Hide Done Items" todos-hide-show-done-items t]
2496 "---"
2497 ["View Diary Items" todos-diary-items t]
2498 ["View Top Priority Items" todos-top-priorities t]
2499 ["View Multifile Top Priority Items" todos-top-priorities-multifile t]
2500 "---"
2501 ["Print Category" todos-print t])
2502 ("Editing"
2503 ["Insert New Item" todos-insert-item t]
2504 ["Insert Item Here" todos-insert-item-here t]
2505 ("More Insertion Commands")
2506 ["Edit Item" todos-edit-item t]
2507 ["Edit Multiline Item" todos-edit-multiline t]
2508 ["Edit Item Header" todos-edit-item-header t]
2509 ["Edit Item Date" todos-edit-item-date t]
2510 ["Edit Item Time" todos-edit-item-time t]
2511 "---"
2512 ["Lower Item Priority" todos-lower-item-priority t]
2513 ["Raise Item Priority" todos-raise-item-priority t]
2514 ["Set Item Priority" todos-set-item-priority t]
2515 ["Move (Recategorize) Item" todos-move-item t]
2516 ["Delete Item" todos-delete-item t]
2517 ["Undo Done Item" todos-item-undo t]
2518 ["Mark/Unmark Item for Diary" todos-toggle-item-diary-inclusion t]
2519 ["Mark/Unmark Items for Diary" todos-edit-item-diary-inclusion t]
2520 ["Mark & Hide Done Item" todos-item-done t]
2521 ["Archive Done Items" todos-archive-category-done-items t]
2522 "---"
2523 ["Add New Todos File" todos-add-file t]
2524 ["Add New Category" todos-add-category t]
2525 ["Delete Current Category" todos-delete-category t]
2526 ["Rename Current Category" todos-rename-category t]
2527 "---"
2528 ["Save Todos File" todos-save t]
2529 )
2530 "---"
2531 ["Quit" todos-quit t]
2532 ))
2533
2534 (defvar todos-archive-mode-map
2535 (let ((map (make-sparse-keymap)))
2536 (suppress-keymap map t)
2537 ;; navigation commands
2538 (define-key map "f" 'todos-forward-category)
2539 (define-key map "b" 'todos-backward-category)
2540 (define-key map "j" 'todos-jump-to-category)
2541 (define-key map "n" 'todos-forward-item)
2542 (define-key map "p" 'todos-backward-item)
2543 ;; display commands
2544 (define-key map "C" 'todos-display-categories)
2545 (define-key map "H" 'todos-highlight-item)
2546 (define-key map "N" 'todos-hide-show-item-numbering)
2547 ;; (define-key map "" 'todos-hide-show-date-time)
2548 (define-key map "P" 'todos-print)
2549 (define-key map "q" 'todos-quit)
2550 (define-key map "s" 'todos-save)
2551 (define-key map "S" 'todos-search)
2552 (define-key map "t" 'todos-show)
2553 (define-key map "u" 'todos-unarchive-items)
2554 (define-key map "U" 'todos-unarchive-category)
2555 map)
2556 "Todos Archive mode keymap.")
2557
2558 (defvar todos-edit-mode-map
2559 (let ((map (make-sparse-keymap)))
2560 (define-key map "\C-x\C-q" 'todos-edit-quit)
2561 (define-key map [remap newline] 'newline-and-indent)
2562 map)
2563 "Todos Edit mode keymap.")
2564
2565 (defvar todos-categories-mode-map
2566 (let ((map (make-sparse-keymap)))
2567 (suppress-keymap map t)
2568 (define-key map "c" 'todos-display-categories-alphabetically-or-by-priority)
2569 (define-key map "t" 'todos-display-categories-sorted-by-todo)
2570 (define-key map "y" 'todos-display-categories-sorted-by-diary)
2571 (define-key map "d" 'todos-display-categories-sorted-by-done)
2572 (define-key map "a" 'todos-display-categories-sorted-by-archived)
2573 (define-key map "l" 'todos-lower-category-priority)
2574 (define-key map "+" 'todos-lower-category-priority)
2575 (define-key map "r" 'todos-raise-category-priority)
2576 (define-key map "-" 'todos-raise-category-priority)
2577 (define-key map "n" 'todos-forward-button)
2578 (define-key map "p" 'todos-backward-button)
2579 (define-key map [tab] 'todos-forward-button)
2580 (define-key map [backtab] 'todos-backward-button)
2581 (define-key map "q" 'todos-quit)
2582 ;; (define-key map "A" 'todos-add-category)
2583 ;; (define-key map "D" 'todos-delete-category)
2584 ;; (define-key map "R" 'todos-rename-category)
2585 map)
2586 "Todos Categories mode keymap.")
2587
2588 (defvar todos-filtered-items-mode-map
2589 (let ((map (make-keymap)))
2590 (suppress-keymap map t)
2591 ;; navigation commands
2592 (define-key map "j" 'todos-jump-to-item)
2593 (define-key map [remap newline] 'todos-jump-to-item)
2594 (define-key map "n" 'todos-forward-item)
2595 (define-key map "p" 'todos-backward-item)
2596 (define-key map "H" 'todos-highlight-item)
2597 (define-key map "N" 'todos-hide-show-item-numbering)
2598 (define-key map "D" 'todos-hide-show-date-time)
2599 (define-key map "P" 'todos-print)
2600 (define-key map "q" 'todos-quit)
2601 (define-key map "s" 'todos-save)
2602 ;; editing commands
2603 (define-key map "l" 'todos-lower-item-priority)
2604 (define-key map "r" 'todos-raise-item-priority)
2605 (define-key map "#" 'todos-set-item-priority)
2606 map)
2607 "Todos Top Priorities mode keymap.")
2608
2609 ;; ---------------------------------------------------------------------------
2610 ;;; Mode definitions
2611
2612 (defun todos-modes-set-1 ()
2613 ""
2614 (set (make-local-variable 'font-lock-defaults) '(todos-font-lock-keywords t))
2615 (set (make-local-variable 'indent-line-function) 'todos-indent)
2616 (when todos-wrap-lines (funcall todos-line-wrapping-function)))
2617
2618 (defun todos-modes-set-2 ()
2619 ""
2620 (add-to-invisibility-spec 'todos)
2621 (setq buffer-read-only t)
2622 (set (make-local-variable 'hl-line-range-function)
2623 (lambda() (when (todos-item-end)
2624 (cons (todos-item-start) (todos-item-end))))))
2625
2626 (defun todos-modes-set-3 ()
2627 ""
2628 (set (make-local-variable 'todos-categories) (todos-set-categories))
2629 (set (make-local-variable 'todos-category-number) 1)
2630 (set (make-local-variable 'todos-first-visit) t)
2631 (add-hook 'find-file-hook 'todos-display-as-todos-file nil t))
2632
2633 (put 'todos-mode 'mode-class 'special)
2634
2635 (define-derived-mode todos-mode special-mode "Todos"
2636 "Major mode for displaying, navigating and editing Todo lists.
2637
2638 \\{todos-mode-map}"
2639 (easy-menu-add todos-menu)
2640 (todos-modes-set-1)
2641 (todos-modes-set-2)
2642 (todos-modes-set-3)
2643 ;; Initialize todos-current-todos-file.
2644 (when (member (file-truename (buffer-file-name))
2645 (funcall todos-files-function))
2646 (set (make-local-variable 'todos-current-todos-file)
2647 (file-truename (buffer-file-name))))
2648 (set (make-local-variable 'todos-first-visit) t)
2649 (set (make-local-variable 'todos-show-done-only) nil)
2650 (set (make-local-variable 'todos-categories-with-marks) nil)
2651 (add-hook 'find-file-hook 'todos-add-to-buffer-list nil t)
2652 (add-hook 'post-command-hook 'todos-update-buffer-list nil t)
2653 (when todos-show-current-file
2654 (add-hook 'pre-command-hook 'todos-show-current-file nil t))
2655 (add-hook 'window-configuration-change-hook
2656 'todos-reset-and-enable-done-separator nil t)
2657 (add-hook 'kill-buffer-hook 'todos-reset-global-current-todos-file nil t))
2658
2659 (defun todos-unload-hook ()
2660 ""
2661 (remove-hook 'pre-command-hook 'todos-show-current-file t)
2662 (remove-hook 'post-command-hook 'todos-update-buffer-list t)
2663 (remove-hook 'find-file-hook 'todos-display-as-todos-file t)
2664 (remove-hook 'find-file-hook 'todos-add-to-buffer-list t)
2665 (remove-hook 'window-configuration-change-hook
2666 'todos-reset-and-enable-done-separator t)
2667 (remove-hook 'kill-buffer-hook 'todos-reset-global-current-todos-file t))
2668
2669 (put 'todos-archive-mode 'mode-class 'special)
2670
2671 ;; If todos-mode is parent, all todos-mode key bindings appear to be
2672 ;; available in todos-archive-mode (e.g. shown by C-h m).
2673 (define-derived-mode todos-archive-mode special-mode "Todos-Arch"
2674 "Major mode for archived Todos categories.
2675
2676 \\{todos-archive-mode-map}"
2677 (todos-modes-set-1)
2678 (todos-modes-set-2)
2679 (todos-modes-set-3)
2680 (set (make-local-variable 'todos-current-todos-file)
2681 (file-truename (buffer-file-name)))
2682 (set (make-local-variable 'todos-show-done-only) t))
2683
2684 (defun todos-mode-external-set ()
2685 ""
2686 (set (make-local-variable 'todos-current-todos-file)
2687 todos-global-current-todos-file)
2688 (let ((cats (with-current-buffer
2689 (find-buffer-visiting todos-current-todos-file)
2690 todos-categories)))
2691 (set (make-local-variable 'todos-categories) cats)))
2692
2693 (define-derived-mode todos-edit-mode text-mode "Todos-Ed"
2694 "Major mode for editing multiline Todo items.
2695
2696 \\{todos-edit-mode-map}"
2697 (todos-modes-set-1)
2698 (todos-mode-external-set))
2699
2700 (put 'todos-categories-mode 'mode-class 'special)
2701
2702 (define-derived-mode todos-categories-mode special-mode "Todos-Cats"
2703 "Major mode for displaying and editing Todos categories.
2704
2705 \\{todos-categories-mode-map}"
2706 (todos-mode-external-set))
2707
2708 (put 'todos-filter-mode 'mode-class 'special)
2709
2710 (define-derived-mode todos-filtered-items-mode special-mode "Todos-Fltr"
2711 "Mode for displaying and reprioritizing top priority Todos.
2712
2713 \\{todos-filtered-items-mode-map}"
2714 (todos-modes-set-1)
2715 (todos-modes-set-2))
2716
2717 ;; ---------------------------------------------------------------------------
2718 ;;; Todos Commands
2719
2720 ;; ---------------------------------------------------------------------------
2721 ;;; Entering and Exiting
2722
2723 ;;;###autoload
2724 (defun todos-show (&optional solicit-file)
2725 "Visit the current Todos file and display one of its categories.
2726 With non-nil prefix argument SOLICIT-FILE prompt for which todo
2727 file to visit.
2728
2729 Without a prefix argument, the first invocation of this command
2730 in a session visits `todos-default-todos-file' (creating it if it
2731 does not yet exist); subsequent invocations from outside of Todos
2732 mode revisit this file or, if the user option
2733 `todos-show-current-file' is non-nil, whichever Todos file
2734 \(either a todo or an archive file) was visited last.
2735
2736 The category displayed on initial invocation is the first member
2737 of `todos-categories' for the current Todos file, on subsequent
2738 invocations whichever category was displayed last. If
2739 `todos-display-categories-first' is non-nil, then the first
2740 invocation of `todos-show' displays a clickable listing of the
2741 categories in the current Todos file.
2742
2743 In Todos mode just the category's unfinished todo items are shown
2744 by default. The done items are hidden, but typing
2745 `\\[todos-hide-show-done-items]' displays them below the todo
2746 items. With non-nil user option `todos-show-with-done' both todo
2747 and done items are always shown on visiting a category.
2748
2749 If this command is invoked in Todos Archive mode, it visits the
2750 corresponding Todos file, displaying the corresponding category."
2751 (interactive "P")
2752 (let* ((cat)
2753 (file (cond (solicit-file
2754 (if (funcall todos-files-function)
2755 (todos-read-file-name "Choose a Todos file to visit: "
2756 nil t)
2757 (error "There are no Todos files")))
2758 ((and (eq major-mode 'todos-archive-mode)
2759 ;; Called noninteractively via todos-quit from
2760 ;; Todos Categories mode to return to archive file.
2761 (called-interactively-p 'any))
2762 (setq cat (todos-current-category))
2763 (concat (file-name-sans-extension todos-current-todos-file)
2764 ".todo"))
2765 (t
2766 (or todos-current-todos-file
2767 (and todos-show-current-file
2768 todos-global-current-todos-file)
2769 todos-default-todos-file
2770 (todos-add-file))))))
2771 (if (and todos-first-visit todos-display-categories-first)
2772 (todos-display-categories)
2773 (set-window-buffer (selected-window)
2774 (set-buffer (find-file-noselect file)))
2775 ;; If called from archive file, show corresponding category in Todos
2776 ;; file, if it exists.
2777 (when (assoc cat todos-categories)
2778 (setq todos-category-number (todos-category-number cat)))
2779 ;; If no Todos file exists, initialize one.
2780 (when (zerop (buffer-size))
2781 ;; Call with empty category name to get initial prompt.
2782 (setq todos-category-number (todos-add-category "")))
2783 (save-excursion (todos-category-select)))
2784 (setq todos-first-visit nil)))
2785
2786 (defun todos-display-categories ()
2787 "Display a table of the current file's categories and item counts.
2788
2789 In the initial display the categories are numbered, indicating
2790 their current order for navigating by \\[todos-forward-category]
2791 and \\[todos-backward-category]. You can persistantly change the
2792 order of the category at point by typing
2793 \\[todos-raise-category-priority] or
2794 \\[todos-lower-category-priority].
2795
2796 The labels above the category names and item counts are buttons,
2797 and clicking these changes the display: sorted by category name
2798 or by the respective item counts (alternately descending or
2799 ascending). In these displays the categories are not numbered
2800 and \\[todos-raise-category-priority] and
2801 \\[todos-lower-category-priority] are
2802 disabled. (Programmatically, the sorting is triggered by passing
2803 a non-nil SORTKEY argument.)
2804
2805 In addition, the lines with the category names and item counts
2806 are buttonized, and pressing one of these button jumps to the
2807 category in Todos mode (or Todos Archive mode, for categories
2808 containing only archived items, provided user option
2809 `todos-skip-archived-categories' is non-nil. These categories
2810 are shown in `todos-archived-only' face."
2811 (interactive)
2812 (todos-display-categories-1)
2813 (let (sortkey)
2814 (todos-update-categories-display sortkey)))
2815
2816 (defun todos-display-categories-alphabetically-or-by-priority ()
2817 ""
2818 (interactive)
2819 (save-excursion
2820 (goto-char (point-min))
2821 (forward-line 2)
2822 (if (member 'alpha todos-descending-counts)
2823 (progn
2824 (todos-update-categories-display nil)
2825 (setq todos-descending-counts
2826 (delete 'alpha todos-descending-counts)))
2827 (todos-update-categories-display 'alpha))))
2828
2829 (defun todos-display-categories-sorted-by-todo ()
2830 ""
2831 (interactive)
2832 (save-excursion
2833 (goto-char (point-min))
2834 (forward-line 2)
2835 (todos-update-categories-display 'todo)))
2836
2837 (defun todos-display-categories-sorted-by-diary ()
2838 ""
2839 (interactive)
2840 (save-excursion
2841 (goto-char (point-min))
2842 (forward-line 2)
2843 (todos-update-categories-display 'diary)))
2844
2845 (defun todos-display-categories-sorted-by-done ()
2846 ""
2847 (interactive)
2848 (save-excursion
2849 (goto-char (point-min))
2850 (forward-line 2)
2851 (todos-update-categories-display 'done)))
2852
2853 (defun todos-display-categories-sorted-by-archived ()
2854 ""
2855 (interactive)
2856 (save-excursion
2857 (goto-char (point-min))
2858 (forward-line 2)
2859 (todos-update-categories-display 'archived)))
2860
2861 (defun todos-show-archive (&optional ask)
2862 "Visit the archive of the current Todos category, if it exists.
2863 If the category has no archived items, prompt to visit the
2864 archive anyway. If there is no archive for this file or with
2865 non-nil argument ASK, prompt to visit another archive.
2866
2867 The buffer showing the archive is in Todos Archive mode. The
2868 first visit in a session displays the first category in the
2869 archive, subsequent visits return to the last category
2870 displayed."
2871 (interactive)
2872 (let* ((cat (todos-current-category))
2873 (count (todos-get-count 'archived cat))
2874 (archive (concat (file-name-sans-extension todos-current-todos-file)
2875 ".toda"))
2876 place)
2877 (setq place (cond (ask 'other-archive)
2878 ((file-exists-p archive) 'this-archive)
2879 (t (when (y-or-n-p (concat "This file has no archive; "
2880 "visit another archive? "))
2881 'other-archive))))
2882 (when (eq place 'other-archive)
2883 (setq archive (todos-read-file-name "Choose a Todos archive: " t t)))
2884 (when (and (eq place 'this-archive) (zerop count))
2885 (setq place (when (y-or-n-p
2886 (concat "This category has no archived items;"
2887 " visit archive anyway? "))
2888 'other-cat)))
2889 (when place
2890 (set-window-buffer (selected-window)
2891 (set-buffer (find-file-noselect archive)))
2892 (if (member place '(other-archive other-cat))
2893 (setq todos-category-number 1)
2894 (todos-category-number cat))
2895 (todos-category-select))))
2896
2897 (defun todos-choose-archive ()
2898 "Choose an archive and visit it."
2899 (interactive)
2900 (todos-show-archive t))
2901
2902 ;; FIXME: need this?
2903 (defun todos-save ()
2904 "Save the current Todos file."
2905 (interactive)
2906 (save-buffer))
2907
2908 (defun todos-quit ()
2909 "Exit the current Todos-related buffer.
2910 Depending on the specific mode, this either kills the buffer or
2911 buries it and restores state as needed."
2912 (interactive)
2913 (cond ((eq major-mode 'todos-categories-mode)
2914 (kill-buffer)
2915 (setq todos-descending-counts nil)
2916 (todos-show))
2917 ((eq major-mode 'todos-filtered-items-mode)
2918 (kill-buffer)
2919 (todos-show))
2920 ((member major-mode (list 'todos-mode 'todos-archive-mode))
2921 ;; Have to write previously nonexistant archives to file, and might
2922 ;; as well save Todos file also.
2923 (todos-save)
2924 (bury-buffer))))
2925
2926 (defun todos-print (&optional to-file)
2927 "Produce a printable version of the current Todos buffer.
2928 This converts overlays and soft line wrapping and, depending on
2929 the value of `todos-print-function', includes faces. With
2930 non-nil argument TO-FILE write the printable version to a file;
2931 otherwise, send it to the default printer."
2932 (interactive)
2933 (let ((buf todos-print-buffer)
2934 (header (cond
2935 ((eq major-mode 'todos-mode)
2936 (concat "Todos File: "
2937 (todos-short-file-name todos-current-todos-file)
2938 "\nCategory: " (todos-current-category)))
2939 ((eq major-mode 'todos-filtered-items-mode)
2940 "Todos Top Priorities")))
2941 (prefix (propertize (concat todos-prefix " ")
2942 'face 'todos-prefix-string))
2943 (num 0)
2944 (fill-prefix (make-string todos-indent-to-here 32))
2945 (content (buffer-string))
2946 file)
2947 (with-current-buffer (get-buffer-create buf)
2948 (insert content)
2949 (goto-char (point-min))
2950 (while (not (eobp))
2951 (let ((beg (point))
2952 (end (save-excursion (todos-item-end))))
2953 (when todos-number-priorities
2954 (setq num (1+ num))
2955 (setq prefix (propertize (concat (number-to-string num) " ")
2956 'face 'todos-prefix-string)))
2957 (insert prefix)
2958 (fill-region beg end))
2959 ;; Calling todos-forward-item infloops at todos-item-start due to
2960 ;; non-overlay prefix, so search for item start instead.
2961 (if (re-search-forward todos-item-start nil t)
2962 (beginning-of-line)
2963 (goto-char (point-max))))
2964 (if (re-search-backward (concat "^" (regexp-quote todos-category-done))
2965 nil t)
2966 (replace-match todos-done-separator))
2967 (goto-char (point-min))
2968 (insert header)
2969 (newline 2)
2970 (if to-file
2971 (let ((file (read-file-name "Print to file: ")))
2972 (funcall todos-print-function file))
2973 (funcall todos-print-function)))
2974 (kill-buffer buf)))
2975
2976 (defun todos-print-to-file ()
2977 "Save printable version of this Todos buffer to a file."
2978 (interactive)
2979 (todos-print t))
2980
2981 (defun todos-convert-legacy-files ()
2982 "Convert legacy Todo files to the current Todos format.
2983 The files `todo-file-do' and `todo-file-done' are converted and
2984 saved (the latter as a Todos Archive file) with a new name in
2985 `todos-files-directory'. See also the documentation string of
2986 `todos-todo-mode-date-time-regexp' for further details."
2987 (interactive)
2988 (if (fboundp 'todo-mode)
2989 (require 'todo-mode)
2990 (error "Void function `todo-mode'"))
2991 ;; Convert `todo-file-do'.
2992 (if (file-exists-p todo-file-do)
2993 (let ((default "todo-do-conv")
2994 file archive-sexp)
2995 (with-temp-buffer
2996 (insert-file-contents todo-file-do)
2997 (let ((end (search-forward ")" (line-end-position) t))
2998 (beg (search-backward "(" (line-beginning-position) t)))
2999 (setq todo-categories
3000 (read (buffer-substring-no-properties beg end))))
3001 (todo-mode)
3002 (delete-region (line-beginning-position) (1+ (line-end-position)))
3003 (while (not (eobp))
3004 (cond
3005 ((looking-at (regexp-quote (concat todo-prefix todo-category-beg)))
3006 (replace-match todos-category-beg))
3007 ((looking-at (regexp-quote todo-category-end))
3008 (replace-match ""))
3009 ((looking-at (regexp-quote (concat todo-prefix " "
3010 todo-category-sep)))
3011 (replace-match todos-category-done))
3012 ((looking-at (concat (regexp-quote todo-prefix) " "
3013 todos-todo-mode-date-time-regexp " "
3014 (regexp-quote todo-initials) ":"))
3015 (todos-convert-legacy-date-time)))
3016 (forward-line))
3017 (setq file (concat todos-files-directory
3018 (read-string
3019 (format "Save file as (default \"%s\"): " default)
3020 nil nil default)
3021 ".todo"))
3022 (write-region (point-min) (point-max) file nil 'nomessage nil t))
3023 (with-temp-buffer
3024 (insert-file-contents file)
3025 (let ((todos-categories (todos-make-categories-list t)))
3026 (todos-update-categories-sexp))
3027 (write-region (point-min) (point-max) file nil 'nomessage))
3028 ;; Convert `todo-file-done'.
3029 (when (file-exists-p todo-file-done)
3030 (with-temp-buffer
3031 (insert-file-contents todo-file-done)
3032 (let ((beg (make-marker))
3033 (end (make-marker))
3034 cat cats comment item)
3035 (while (not (eobp))
3036 (when (looking-at todos-todo-mode-date-time-regexp)
3037 (set-marker beg (point))
3038 (todos-convert-legacy-date-time)
3039 (set-marker end (point))
3040 (goto-char beg)
3041 (insert "[" todos-done-string)
3042 (goto-char end)
3043 (insert "]")
3044 (forward-char)
3045 (when (looking-at todos-todo-mode-date-time-regexp)
3046 (todos-convert-legacy-date-time))
3047 (when (looking-at (concat " " (regexp-quote todo-initials) ":"))
3048 (replace-match "")))
3049 (if (re-search-forward
3050 (concat "^" todos-todo-mode-date-time-regexp) nil t)
3051 (goto-char (match-beginning 0))
3052 (goto-char (point-max)))
3053 (backward-char)
3054 (when (looking-back "\\[\\([^][]+\\)\\]")
3055 (setq cat (match-string 1))
3056 (goto-char (match-beginning 0))
3057 (replace-match ""))
3058 ;; If the item ends with a non-comment parenthesis not
3059 ;; followed by a period, we lose (but we inherit that problem
3060 ;; from todo-mode.el).
3061 (when (looking-back "(\\(.*\\)) ")
3062 (setq comment (match-string 1))
3063 (replace-match "")
3064 (insert "[" todos-comment-string ": " comment "]"))
3065 (set-marker end (point))
3066 (if (member cat cats)
3067 ;; If item is already in its category, leave it there.
3068 (unless (save-excursion
3069 (re-search-backward
3070 (concat "^" (regexp-quote todos-category-beg)
3071 "\\(.*\\)$") nil t)
3072 (string= (match-string 1) cat))
3073 ;; Else move it to its category.
3074 (setq item (buffer-substring-no-properties beg end))
3075 (delete-region beg (1+ end))
3076 (set-marker beg (point))
3077 (re-search-backward
3078 (concat "^" (regexp-quote (concat todos-category-beg cat)))
3079 nil t)
3080 (forward-line)
3081 (if (re-search-forward
3082 (concat "^" (regexp-quote todos-category-beg)
3083 "\\(.*\\)$") nil t)
3084 (progn (goto-char (match-beginning 0))
3085 (newline)
3086 (forward-line -1))
3087 (goto-char (point-max)))
3088 (insert item "\n")
3089 (goto-char beg))
3090 (push cat cats)
3091 (goto-char beg)
3092 (insert todos-category-beg cat "\n\n" todos-category-done "\n"))
3093 (forward-line))
3094 (set-marker beg nil)
3095 (set-marker end nil))
3096 (setq file (concat (file-name-sans-extension file) ".toda"))
3097 (write-region (point-min) (point-max) file nil 'nomessage nil t))
3098 (with-temp-buffer
3099 (insert-file-contents file)
3100 (let ((todos-categories (todos-make-categories-list t)))
3101 (todos-update-categories-sexp))
3102 (write-region (point-min) (point-max) file nil 'nomessage)
3103 (setq archive-sexp (read (buffer-substring-no-properties
3104 (line-beginning-position)
3105 (line-end-position)))))
3106 (setq file (concat (file-name-sans-extension file) ".todo"))
3107 ;; Update categories sexp of converted Todos file again, adding
3108 ;; counts of archived items.
3109 (with-temp-buffer
3110 (insert-file-contents file)
3111 (let ((sexp (read (buffer-substring-no-properties
3112 (line-beginning-position)
3113 (line-end-position)))))
3114 (dolist (cat sexp)
3115 (let ((archive-cat (assoc (car cat) archive-sexp)))
3116 (if archive-cat
3117 (aset (cdr cat) 3 (aref (cdr archive-cat) 2)))))
3118 (delete-region (line-beginning-position) (line-end-position))
3119 (prin1 sexp (current-buffer)))
3120 (write-region (point-min) (point-max) file nil 'nomessage)))
3121 (todos-reevaluate-filelist-defcustoms)
3122 (message "Format conversion done."))
3123 (error "No legacy Todo file exists")))
3124
3125 ;; ---------------------------------------------------------------------------
3126 ;;; Navigation Commands
3127
3128 (defun todos-forward-category (&optional back)
3129 "Visit the numerically next category in this Todos file.
3130 If the current category is the highest numbered, visit the first
3131 category. With non-nil argument BACK, visit the numerically
3132 previous category (the highest numbered one, if the current
3133 category is the first)."
3134 (interactive)
3135 (setq todos-category-number
3136 (1+ (mod (- todos-category-number (if back 2 0))
3137 (length todos-categories))))
3138 (when todos-skip-archived-categories
3139 (while (and (zerop (todos-get-count 'todo))
3140 (zerop (todos-get-count 'done))
3141 (not (zerop (todos-get-count 'archive))))
3142 (setq todos-category-number
3143 (apply (if back '1- '1+) (list todos-category-number)))))
3144 (todos-category-select)
3145 (goto-char (point-min)))
3146
3147 (defun todos-backward-category ()
3148 "Visit the numerically previous category in this Todos file.
3149 If the current category is the highest numbered, visit the first
3150 category."
3151 (interactive)
3152 (todos-forward-category t))
3153
3154 (defun todos-jump-to-category (&optional cat other-file)
3155 "Jump to a category in this or another Todos file.
3156
3157 Programmatically, optional argument CAT provides the category
3158 name. When nil (as in interactive calls), prompt for the
3159 category, with TAB completion on existing categories. If a
3160 non-existing category name is entered, ask whether to add a new
3161 category with this name; if affirmed, add it, then jump to that
3162 category. With non-nil argument OTHER-FILE, prompt for a Todos
3163 file, otherwise jump within the current Todos file."
3164 (interactive)
3165 (let ((file (or (and other-file
3166 (todos-read-file-name "Choose a Todos file: " nil t))
3167 ;; Jump to archived-only Categories from Todos Categories
3168 ;; mode.
3169 (and cat
3170 todos-skip-archived-categories
3171 (zerop (todos-get-count 'todo cat))
3172 (zerop (todos-get-count 'done cat))
3173 (not (zerop (todos-get-count 'archived cat)))
3174 (concat (file-name-sans-extension
3175 todos-current-todos-file) ".toda"))
3176 todos-current-todos-file
3177 ;; If invoked from outside of Todos mode before
3178 ;; todos-show...
3179 todos-default-todos-file)))
3180 (with-current-buffer (find-file-noselect file)
3181 (and other-file (setq todos-current-todos-file file))
3182 (let ((category (or (and (assoc cat todos-categories) cat)
3183 (todos-read-category "Jump to category: "))))
3184 ;; Clean up after selecting category in Todos Categories mode.
3185 (if (string= (buffer-name) todos-categories-buffer)
3186 (kill-buffer))
3187 (if (or cat other-file)
3188 (set-window-buffer (selected-window)
3189 (set-buffer (find-buffer-visiting file))))
3190 (unless todos-global-current-todos-file
3191 (setq todos-global-current-todos-file todos-current-todos-file))
3192 (todos-category-number category) ; (1+ (length t-c)) if new category.
3193 ;; (if (> todos-category-number (length todos-categories))
3194 ;; (setq todos-category-number (todos-add-category category)))
3195 (todos-category-select)
3196 (goto-char (point-min))))))
3197
3198 (defun todos-jump-to-category-other-file ()
3199 "Jump to a category in another Todos file.
3200 The category is chosen by prompt, with TAB completion."
3201 (interactive)
3202 (todos-jump-to-category nil t))
3203
3204 (defun todos-jump-to-any-category ()
3205 ""
3206 (interactive)
3207 (let* ((cats-alist todos-all-categories-alist)
3208 (cats (mapcar 'car cats-alist))
3209 (completion-ignore-case todos-completion-ignore-case)
3210 (cat (completing-read "Jump to category: " cats nil t))
3211 (files (if (zerop (length cat))
3212 (keyboard-quit)
3213 (cdr (assoc cat cats-alist))))
3214 (file (if (nlistp files)
3215 files
3216 (completing-read (format "Jump to \"%s\" in which file? " cat)
3217 files nil t))))
3218 (if (zerop (length file))
3219 (keyboard-quit)
3220 (setq file (concat todos-files-directory file ".todo"))
3221 (set-window-buffer (selected-window)
3222 (set-buffer (find-file-noselect file)))
3223 (unless todos-global-current-todos-file
3224 (setq todos-global-current-todos-file todos-current-todos-file))
3225 (todos-category-number cat)
3226 (todos-category-select)
3227 (goto-char (point-min)))))
3228
3229 (defun todos-jump-to-item ()
3230 "Jump to the file and category of the filtered item at point."
3231 (interactive)
3232 (let ((str (todos-item-string))
3233 (buf (current-buffer))
3234 cat file archive beg)
3235 (string-match (concat (if todos-filter-done-items
3236 (concat "\\(?:" todos-done-string-start "\\|"
3237 todos-date-string-start "\\)")
3238 todos-date-string-start)
3239 todos-date-pattern "\\(?: " diary-time-regexp "\\)?"
3240 (if todos-filter-done-items
3241 "\\]"
3242 (regexp-quote todos-nondiary-end)) "?"
3243 "\\(?4: \\[\\(?3:(archive) \\)?\\(?2:.*:\\)?"
3244 "\\(?1:.*\\)\\]\\).*$") str)
3245 (setq cat (match-string 1 str))
3246 (setq file (match-string 2 str))
3247 (setq archive (string= (match-string 3 str) "(archive) "))
3248 (setq str (replace-match "" nil nil str 4))
3249 (setq file (if file
3250 (concat todos-files-directory (substring file 0 -1)
3251 (if archive ".toda" ".todo"))
3252 (if archive
3253 (concat (file-name-sans-extension
3254 todos-global-current-todos-file) ".toda")
3255 todos-global-current-todos-file)))
3256 (find-file-noselect file)
3257 (with-current-buffer (find-buffer-visiting file)
3258 (widen)
3259 (goto-char (point-min))
3260 (re-search-forward
3261 (concat "^" (regexp-quote (concat todos-category-beg cat))) nil t)
3262 (search-forward str)
3263 (setq beg (match-beginning 0)))
3264 (kill-buffer buf)
3265 (set-window-buffer (selected-window) (set-buffer (find-buffer-visiting file)))
3266 (setq todos-current-todos-file file)
3267 (setq todos-category-number (todos-category-number cat))
3268 (let ((todos-show-with-done (if todos-filter-done-items t
3269 todos-show-with-done)))
3270 (todos-category-select))
3271 (goto-char beg)))
3272
3273 (defun todos-forward-item (&optional count)
3274 "Move point down to start of item with next lower priority.
3275 With positive numerical prefix COUNT, move point COUNT items
3276 downward."
3277 (interactive "P")
3278 ;; It's not worth the trouble to allow prefix arg value < 1, since we have
3279 ;; the corresponding command.
3280 (if (and count (> 1 count))
3281 (error "This command only accepts a positive numerical prefix argument")
3282 (let* ((not-done (not (or (todos-done-item-p) (looking-at "^$"))))
3283 (start (line-end-position)))
3284 (goto-char start)
3285 (if (re-search-forward todos-item-start nil t (or count 1))
3286 (goto-char (match-beginning 0))
3287 (goto-char (point-max)))
3288 ;; If points advances by one from a todo to a done item, go back to the
3289 ;; space above todos-done-separator, since that is a legitimate place to
3290 ;; insert an item. But skip this space if count > 1, since that should
3291 ;; only stop on an item.
3292 (when (and not-done (todos-done-item-p))
3293 (if (or (not count) (= count 1))
3294 (re-search-backward "^$" start t))))))
3295 ;; FIXME: The preceding sexp is insufficient when buffer is not narrowed,
3296 ;; since there could be no done items in this category, so the search puts
3297 ;; us on first todo item of next category. Does this ever happen? If so:
3298 ;; (let ((opoint) (point))
3299 ;; (forward-line -1)
3300 ;; (when (or (not count) (= count 1))
3301 ;; (cond ((looking-at (concat "^" (regexp-quote todos-category-beg)))
3302 ;; (forward-line -2))
3303 ;; ((looking-at (concat "^" (regexp-quote todos-category-done)))
3304 ;; (forward-line -1))
3305 ;; (t
3306 ;; (goto-char opoint)))))))
3307
3308 (defun todos-backward-item (&optional count)
3309 "Move point up to start of item with next higher priority.
3310 With positive numerical prefix COUNT, move point COUNT items
3311 upward."
3312 (interactive "P")
3313 ;; Avoid moving to bob if on the first item but not at bob.
3314 (when (> (line-number-at-pos) 1)
3315 ;; It's not worth the trouble to allow prefix arg value < 1, since we have
3316 ;; the corresponding command.
3317 (if (and count (> 1 count))
3318 (error "This command only accepts a positive numerical prefix argument")
3319 (let* ((done (todos-done-item-p)))
3320 (todos-item-start)
3321 (unless (bobp)
3322 (re-search-backward todos-item-start nil t (or count 1)))
3323 ;; Unless this is a regexp filtered items buffer (which can contain
3324 ;; intermixed todo and done items), if points advances by one from a
3325 ;; done to a todo item, go back to the space above
3326 ;; todos-done-separator, since that is a legitimate place to insert an
3327 ;; item. But skip this space if count > 1, since that should only
3328 ;; stop on an item.
3329 (when (and done (not (todos-done-item-p)) (or (not count) (= count 1))
3330 (not (equal (buffer-name) todos-regexp-items-buffer)))
3331 (re-search-forward (concat "^" (regexp-quote todos-category-done))
3332 nil t)
3333 (forward-line -1))))))
3334
3335 (defun todos-forward-button (n &optional wrap display-message)
3336 ""
3337 (interactive "p\nd\nd")
3338 (forward-button n wrap display-message)
3339 (and (bolp) (button-at (point))
3340 ;; Align with beginning of category label.
3341 (forward-char (+ 4 (length todos-categories-number-separator)))))
3342
3343 (defun todos-backward-button (n &optional wrap display-message)
3344 ""
3345 (interactive "p\nd\nd")
3346 (backward-button n wrap display-message)
3347 (and (bolp) (button-at (point))
3348 ;; Align with beginning of category label.
3349 (forward-char (+ 4 (length todos-categories-number-separator)))))
3350
3351 ;; FIXME: (i) Extend search to other Todos files. (ii) Allow navigating among
3352 ;; hits. (But these features are effectively available with
3353 ;; todos-regexp-items-multifile, so maybe it's not worth the trouble here.)
3354 (defun todos-search ()
3355 "Search for a regular expression in this Todos file.
3356 The search runs through the whole file and encompasses all and
3357 only todo and done items; it excludes category names. Multiple
3358 matches are shown sequentially, highlighted in `todos-search'
3359 face."
3360 (interactive)
3361 (let ((regex (read-from-minibuffer "Enter a search string (regexp): "))
3362 (opoint (point))
3363 matches match cat in-done ov mlen msg)
3364 (widen)
3365 (goto-char (point-min))
3366 (while (not (eobp))
3367 (setq match (re-search-forward regex nil t))
3368 (goto-char (line-beginning-position))
3369 (unless (or (equal (point) 1)
3370 (looking-at (concat "^" (regexp-quote todos-category-beg))))
3371 (if match (push match matches)))
3372 (forward-line))
3373 (setq matches (reverse matches))
3374 (if matches
3375 (catch 'stop
3376 (while matches
3377 (setq match (pop matches))
3378 (goto-char match)
3379 (todos-item-start)
3380 (when (looking-at todos-done-string-start)
3381 (setq in-done t))
3382 (re-search-backward (concat "^" (regexp-quote todos-category-beg)
3383 "\\(.*\\)\n") nil t)
3384 (setq cat (match-string-no-properties 1))
3385 (todos-category-number cat)
3386 (todos-category-select)
3387 (if in-done
3388 (unless todos-show-with-done (todos-hide-show-done-items)))
3389 (goto-char match)
3390 (setq ov (make-overlay (- (point) (length regex)) (point)))
3391 (overlay-put ov 'face 'todos-search)
3392 (when matches
3393 (setq mlen (length matches))
3394 (if (y-or-n-p
3395 (if (> mlen 1)
3396 (format "There are %d more matches; go to next match? "
3397 mlen)
3398 "There is one more match; go to it? "))
3399 (widen)
3400 (throw 'stop (setq msg (if (> mlen 1)
3401 (format "There are %d more matches."
3402 mlen)
3403 "There is one more match."))))))
3404 (setq msg "There are no more matches."))
3405 (todos-category-select)
3406 (goto-char opoint)
3407 (message "No match for \"%s\"" regex))
3408 (when msg
3409 (if (y-or-n-p (concat msg "\nUnhighlight matches? "))
3410 (todos-clear-matches)
3411 (message "You can unhighlight the matches later by typing %s"
3412 (key-description (car (where-is-internal
3413 'todos-clear-matches))))))))
3414
3415 (defun todos-clear-matches ()
3416 "Remove highlighting on matches found by todos-search."
3417 (interactive)
3418 (remove-overlays 1 (1+ (buffer-size)) 'face 'todos-search))
3419
3420 ;; ---------------------------------------------------------------------------
3421 ;;; Display Commands
3422
3423 (defun todos-hide-show-item-numbering ()
3424 ""
3425 (interactive)
3426 (todos-reset-prefix 'todos-number-priorities (not todos-number-priorities)))
3427
3428 (defun todos-hide-show-done-items ()
3429 "Show hidden or hide visible done items in current category."
3430 (interactive)
3431 (if (zerop (todos-get-count 'done (todos-current-category)))
3432 (message "There are no done items in this category.")
3433 (save-excursion
3434 (goto-char (point-min))
3435 (let ((todos-show-with-done (not (re-search-forward
3436 todos-done-string-start nil t))))
3437 (todos-category-select)))))
3438
3439 (defun todos-show-done-only ()
3440 "Switch between displaying only done or only todo items."
3441 (interactive)
3442 (setq todos-show-done-only (not todos-show-done-only))
3443 (todos-category-select))
3444
3445 (defun todos-highlight-item ()
3446 "Highlight or unhighlight the todo item the cursor is on."
3447 (interactive)
3448 (require 'hl-line)
3449 (if hl-line-mode
3450 (hl-line-mode -1)
3451 (hl-line-mode 1)))
3452
3453 (defun todos-hide-show-date-time ()
3454 "Hide or show date-time header of todo items in the current file."
3455 (interactive)
3456 (save-excursion
3457 (save-restriction
3458 (goto-char (point-min))
3459 (let ((ovs (overlays-in (point) (1+ (point))))
3460 ov hidden)
3461 (while ovs
3462 (setq ov (pop ovs))
3463 (if (equal (overlay-get ov 'display) "")
3464 (setq ovs nil hidden t)))
3465 (widen)
3466 (goto-char (point-min))
3467 (if hidden
3468 (remove-overlays (point-min) (point-max) 'display "")
3469 (while (not (eobp))
3470 (when (re-search-forward
3471 (concat todos-date-string-start todos-date-pattern
3472 "\\( " diary-time-regexp "\\)?"
3473 (regexp-quote todos-nondiary-end) "? ")
3474 nil t)
3475 (unless (save-match-data (todos-done-item-p))
3476 (setq ov (make-overlay (match-beginning 0) (match-end 0) nil t))
3477 (overlay-put ov 'display "")))
3478 (todos-forward-item)))))))
3479
3480 (defun todos-mark-unmark-item (&optional n all)
3481 "Mark item at point if unmarked, or unmark it if marked.
3482
3483 With a positive numerical prefix argument N, change the
3484 markedness of the next N items. With non-nil argument ALL, mark
3485 all visible items in the category (depending on visibility, all
3486 todo and done items, or just todo or just done items).
3487
3488 The mark is the character \"*\" inserted in front of the item's
3489 priority number or the `todos-prefix' string; if `todos-prefix'
3490 is \"*\", then the mark is \"@\"."
3491 (interactive "p")
3492 (if all (goto-char (point-min)))
3493 (unless (> n 0) (setq n 1))
3494 (let ((i 0))
3495 (while (or (and all (not (eobp)))
3496 (< i n))
3497 (let* ((cat (todos-current-category))
3498 (ov (todos-marked-item-p))
3499 (marked (assoc cat todos-categories-with-marks)))
3500 (if (and ov (not all))
3501 (progn
3502 (delete-overlay ov)
3503 (if (= (cdr marked) 1) ; Deleted last mark in this category.
3504 (setq todos-categories-with-marks
3505 (assq-delete-all cat todos-categories-with-marks))
3506 (setcdr marked (1- (cdr marked)))))
3507 (when (todos-item-start)
3508 (unless (and all (todos-marked-item-p))
3509 (setq ov (make-overlay (point) (point)))
3510 (overlay-put ov 'before-string todos-item-mark)
3511 (if marked
3512 (setcdr marked (1+ (cdr marked)))
3513 (push (cons cat 1) todos-categories-with-marks))))))
3514 (todos-forward-item)
3515 (setq i (1+ i)))))
3516
3517 (defun todos-mark-category ()
3518 "Put the \"*\" mark on all items in this category.
3519 \(If `todos-prefix' is \"*\", then the mark is \"@\".)"
3520 (interactive)
3521 (todos-mark-unmark-item 0 t))
3522
3523 (defun todos-unmark-category ()
3524 "Remove the \"*\" mark from all items in this category.
3525 \(If `todos-prefix' is \"*\", then the mark is \"@\".)"
3526 (interactive)
3527 (remove-overlays (point-min) (point-max) 'before-string todos-item-mark)
3528 (setq todos-categories-with-marks
3529 (delq (assoc (todos-current-category) todos-categories-with-marks)
3530 todos-categories-with-marks)))
3531
3532 ;; ---------------------------------------------------------------------------
3533 ;;; Item filtering commands
3534
3535 (defun todos-set-top-priorities-in-file ()
3536 "Set number of top priorities for this file.
3537 See `todos-set-top-priorities' for more details."
3538 (interactive)
3539 (todos-set-top-priorities))
3540
3541 (defun todos-set-top-priorities-in-category ()
3542 "Set number of top priorities for this category.
3543 See `todos-set-top-priorities' for more details."
3544 (interactive)
3545 (todos-set-top-priorities t))
3546
3547 (defun todos-top-priorities (&optional num)
3548 "List top priorities of each category in `todos-filter-files'.
3549 Number of entries for each category is given by NUM, which
3550 defaults to `todos-show-priorities'."
3551 (interactive "P")
3552 (let ((arg (if num (cons 'top num) 'top))
3553 (buf todos-top-priorities-buffer)
3554 (file todos-current-todos-file))
3555 (todos-filter-items arg)
3556 (todos-filtered-buffer-name buf file)))
3557
3558 (defun todos-top-priorities-multifile (&optional arg)
3559 "List top priorities of each category in `todos-filter-files'.
3560
3561 If the prefix argument ARG is a number, this is the maximum
3562 number of top priorities to list in each category. If the prefix
3563 argument is `C-u', prompt for which files to filter and use
3564 `todos-show-priorities' as the number of top priorities to list
3565 in each category. If the prefix argument is `C-uC-u', prompt
3566 both for which files to filter and for how many top priorities to
3567 list in each category."
3568 (interactive "P")
3569 (let* ((buf todos-top-priorities-buffer)
3570 files
3571 (pref (if (numberp arg)
3572 (cons 'top arg)
3573 (setq files (if (or (consp arg)
3574 (null todos-filter-files))
3575 (progn (todos-multiple-filter-files)
3576 todos-multiple-filter-files)
3577 todos-filter-files))
3578 (if (equal arg '(16))
3579 (cons 'top (read-number
3580 "Enter number of top priorities to show: "
3581 todos-show-priorities))
3582 'top))))
3583 (todos-filter-items pref t)
3584 (todos-filtered-buffer-name buf files)))
3585
3586 (defun todos-diary-items ()
3587 "Display todo items for diary inclusion in this Todos file."
3588 (interactive)
3589 (let ((buf todos-diary-items-buffer)
3590 (file todos-current-todos-file))
3591 (todos-filter-items 'diary)
3592 (todos-filtered-buffer-name buf file)))
3593
3594 (defun todos-diary-items-multifile (&optional arg)
3595 "Display todo items for diary inclusion in one or more Todos file.
3596 The files are those listed in `todos-filter-files'."
3597 (interactive "P")
3598 (let ((buf todos-diary-items-buffer)
3599 (files (if (or arg (null todos-filter-files))
3600 (progn (todos-multiple-filter-files)
3601 todos-multiple-filter-files)
3602 todos-filter-files)))
3603 (todos-filter-items 'diary t)
3604 (todos-filtered-buffer-name buf files)))
3605
3606 (defun todos-regexp-items ()
3607 "Display todo items matching a user-entered regular expression.
3608 The items are those in the current Todos file."
3609 (interactive)
3610 (let ((buf todos-regexp-items-buffer)
3611 (file todos-current-todos-file))
3612 (todos-filter-items 'regexp)
3613 (todos-filtered-buffer-name buf file)))
3614
3615 (defun todos-regexp-items-multifile (&optional arg)
3616 "Display todo items matching a user-entered regular expression.
3617 The items are those in the files listed in `todos-filter-files'."
3618 (interactive "P")
3619 (let ((buf todos-regexp-items-buffer)
3620 (files (if (or arg (null todos-filter-files))
3621 (progn (todos-multiple-filter-files)
3622 todos-multiple-filter-files)
3623 todos-filter-files)))
3624 (todos-filter-items 'regexp t)
3625 (todos-filtered-buffer-name buf files)))
3626
3627 ;;; Editing Commands
3628
3629 (defun todos-add-file ()
3630 "Name and add a new Todos file.
3631 Interactively, prompt for a category and display it.
3632 Noninteractively, return the name of the new file."
3633 (interactive)
3634 (let ((prompt (concat "Enter name of new Todos file "
3635 "(TAB or SPC to see current names): "))
3636 file)
3637 (setq file (todos-read-file-name prompt))
3638 (with-current-buffer (get-buffer-create file)
3639 (erase-buffer)
3640 (write-region (point-min) (point-max) file nil 'nomessage nil t)
3641 (kill-buffer file))
3642 (todos-reevaluate-filelist-defcustoms)
3643 (if (called-interactively-p)
3644 (progn
3645 (set-window-buffer (selected-window)
3646 (set-buffer (find-file-noselect file)))
3647 (setq todos-current-todos-file file)
3648 (todos-show))
3649 file)))
3650
3651 ;; ---------------------------------------------------------------------------
3652 ;;; Category editing commands
3653
3654 (defun todos-add-category (&optional cat)
3655 "Add a new category to the current Todos file.
3656 Called interactively, prompts for category name, then visits the
3657 category in Todos mode. Non-interactively, argument CAT provides
3658 the category name and the return value is the category number."
3659 (interactive)
3660 (let* ((buffer-read-only)
3661 (num (1+ (length todos-categories)))
3662 (counts (make-vector 4 0))) ; [todo diary done archived]
3663 (unless cat
3664 (setq cat (todos-read-category "Enter new category name: " nil t)))
3665 (setq todos-categories (append todos-categories (list (cons cat counts))))
3666 (widen)
3667 (goto-char (point-max))
3668 (save-excursion ; Save point for todos-category-select.
3669 (insert todos-category-beg cat "\n\n" todos-category-done "\n"))
3670 (todos-update-categories-sexp)
3671 ;; If invoked by user, display the newly added category, if called
3672 ;; programmatically return the category number to the caller.
3673 (if (called-interactively-p 'any)
3674 (progn
3675 (setq todos-category-number num)
3676 (todos-category-select))
3677 num)))
3678
3679 (defun todos-rename-category ()
3680 "Rename current Todos category.
3681 If this file has an archive containing this category, rename the
3682 category there as well."
3683 (interactive)
3684 (let* ((cat (todos-current-category))
3685 (new (read-from-minibuffer (format "Rename category \"%s\" to: " cat))))
3686 (setq new (todos-validate-name new 'category))
3687 (let* ((ofile todos-current-todos-file)
3688 (archive (concat (file-name-sans-extension ofile) ".toda"))
3689 (buffers (append (list ofile)
3690 (unless (zerop (todos-get-count 'archived cat))
3691 (list archive)))))
3692 (dolist (buf buffers)
3693 (with-current-buffer (find-file-noselect buf)
3694 (let (buffer-read-only)
3695 (setq todos-categories (todos-set-categories))
3696 (save-excursion
3697 (save-restriction
3698 (setcar (assoc cat todos-categories) new)
3699 (widen)
3700 (goto-char (point-min))
3701 (todos-update-categories-sexp)
3702 (re-search-forward (concat (regexp-quote todos-category-beg)
3703 "\\(" (regexp-quote cat) "\\)\n")
3704 nil t)
3705 (replace-match new t t nil 1)))))))
3706 (force-mode-line-update))
3707 (save-excursion (todos-category-select)))
3708
3709 (defun todos-delete-category (&optional arg)
3710 "Delete current Todos category provided it is empty.
3711 With ARG non-nil delete the category unconditionally,
3712 i.e. including all existing todo and done items."
3713 (interactive "P")
3714 (let* ((file todos-current-todos-file)
3715 (cat (todos-current-category))
3716 (todo (todos-get-count 'todo cat))
3717 (done (todos-get-count 'done cat))
3718 (archived (todos-get-count 'archived cat)))
3719 (if (and (not arg)
3720 (or (> todo 0) (> done 0)))
3721 (message "%s" (substitute-command-keys
3722 (concat "To delete a non-empty category, "
3723 "type C-u \\[todos-delete-category].")))
3724 (when (cond ((= (length todos-categories) 1)
3725 (y-or-n-p (concat "This is the only category in this file; "
3726 "deleting it will also delete the file.\n"
3727 "Do you want to proceed? ")))
3728 ((> archived 0)
3729 (y-or-n-p (concat "This category has archived items; "
3730 "the archived category will remain\n"
3731 "after deleting the todo category. "
3732 "Do you still want to delete it\n"
3733 "(see 'todos-skip-archived-categories' "
3734 "for another option)? ")))
3735 (t
3736 (y-or-n-p (concat "Permanently remove category \"" cat
3737 "\"" (and arg " and all its entries")
3738 "? "))))
3739 (widen)
3740 (let ((buffer-read-only)
3741 (beg (re-search-backward
3742 (concat "^" (regexp-quote (concat todos-category-beg cat))
3743 "\n") nil t))
3744 (end (if (re-search-forward
3745 (concat "\n\\(" (regexp-quote todos-category-beg)
3746 ".*\n\\)") nil t)
3747 (match-beginning 1)
3748 (point-max))))
3749 (remove-overlays beg end)
3750 (delete-region beg end)
3751 (if (= (length todos-categories) 1)
3752 ;; If deleted category was the only one, delete the file.
3753 (progn
3754 (todos-reevaluate-filelist-defcustoms)
3755 ;; Skip confirming killing the archive buffer if it has been
3756 ;; modified and not saved.
3757 (set-buffer-modified-p nil)
3758 (delete-file file)
3759 (kill-buffer)
3760 (message "Deleted Todos file %s." file))
3761 (setq todos-categories (delete (assoc cat todos-categories)
3762 todos-categories))
3763 (todos-update-categories-sexp)
3764 (setq todos-category-number
3765 (1+ (mod todos-category-number (length todos-categories))))
3766 (todos-category-select)
3767 (goto-char (point-min))
3768 (message "Deleted category %s." cat)))))))
3769
3770 (defun todos-move-category ()
3771 "Move current category to a different Todos file.
3772 If current category has archived items, also move those to the
3773 archive of the file moved to, creating it if it does not exist."
3774 (interactive)
3775 (when (or (> (length todos-categories) 1)
3776 (y-or-n-p (concat "This is the only category in this file; "
3777 "moving it will also delete the file.\n"
3778 "Do you want to proceed? ")))
3779 (let* ((ofile todos-current-todos-file)
3780 (cat (todos-current-category))
3781 (nfile (todos-read-file-name
3782 "Choose a Todos file to move this category to: " nil t))
3783 (archive (concat (file-name-sans-extension ofile) ".toda"))
3784 (buffers (append (list ofile)
3785 (unless (zerop (todos-get-count 'archived cat))
3786 (list archive))))
3787 new)
3788 (while (equal (file-truename nfile) (file-truename ofile))
3789 (setq nfile (todos-read-file-name
3790 "Choose a file distinct from this file: " nil t)))
3791 (dolist (buf buffers)
3792 (with-current-buffer (find-file-noselect buf)
3793 (widen)
3794 (goto-char (point-max))
3795 (let* ((beg (re-search-backward
3796 (concat "^"
3797 (regexp-quote (concat todos-category-beg cat)))
3798 nil t))
3799 (end (if (re-search-forward
3800 (concat "^" (regexp-quote todos-category-beg))
3801 nil t 2)
3802 (match-beginning 0)
3803 (point-max)))
3804 (content (buffer-substring-no-properties beg end))
3805 (counts (cdr (assoc cat todos-categories)))
3806 buffer-read-only)
3807 ;; Move the category to the new file. Also update or create
3808 ;; archive file if necessary.
3809 (with-current-buffer
3810 (find-file-noselect
3811 ;; Regenerate todos-archives in case there
3812 ;; is a newly created archive.
3813 (if (member buf (funcall todos-files-function t))
3814 (concat (file-name-sans-extension nfile) ".toda")
3815 nfile))
3816 (let* ((nfile-short (todos-short-file-name nfile))
3817 (prompt (concat
3818 (format "Todos file \"%s\" already has "
3819 nfile-short)
3820 (format "the category \"%s\";\n" cat)
3821 "enter a new category name: "))
3822 buffer-read-only)
3823 (widen)
3824 (goto-char (point-max))
3825 (insert content)
3826 ;; If the file moved to has a category with the same
3827 ;; name, rename the moved category.
3828 (when (assoc cat todos-categories)
3829 (unless (member (file-truename (buffer-file-name))
3830 (funcall todos-files-function t))
3831 (setq new (read-from-minibuffer prompt))
3832 (setq new (todos-validate-name new 'category))))
3833 ;; Replace old with new name in Todos and archive files.
3834 (when new
3835 (goto-char (point-max))
3836 (re-search-backward
3837 (concat "^" (regexp-quote todos-category-beg)
3838 "\\(" (regexp-quote cat) "\\)") nil t)
3839 (replace-match new nil nil nil 1)))
3840 (setq todos-categories
3841 (append todos-categories (list (cons new counts))))
3842 (todos-update-categories-sexp)
3843 ;; If archive was just created, save it to avoid "File <xyz> no
3844 ;; longer exists!" message on invoking
3845 ;; `todos-view-archived-items'. FIXME: maybe better to save
3846 ;; unconditionally?
3847 (unless (file-exists-p (buffer-file-name))
3848 (save-buffer))
3849 (todos-category-number (or new cat))
3850 (todos-category-select))
3851 ;; Delete the category from the old file, and if that was the
3852 ;; last category, delete the file. Also handle archive file
3853 ;; if necessary.
3854 (remove-overlays beg end)
3855 (delete-region beg end)
3856 (goto-char (point-min))
3857 ;; Put point after todos-categories sexp.
3858 (forward-line)
3859 (if (eobp) ; Aside from sexp, file is empty.
3860 (progn
3861 ;; Skip confirming killing the archive buffer.
3862 (set-buffer-modified-p nil)
3863 (delete-file todos-current-todos-file)
3864 (kill-buffer)
3865 (when (member todos-current-todos-file todos-files)
3866 (todos-reevaluate-filelist-defcustoms)))
3867 (setq todos-categories (delete (assoc cat todos-categories)
3868 todos-categories))
3869 (todos-update-categories-sexp)
3870 (todos-category-select)))))
3871 (set-window-buffer (selected-window)
3872 (set-buffer (find-file-noselect nfile)))
3873 (todos-category-number (or new cat))
3874 (todos-category-select))))
3875
3876 (defun todos-merge-category ()
3877 "Merge current category into another category in this file.
3878
3879 The current category's todo and done items are appended to the
3880 chosen goal category's todo and done items, respectively. The
3881 goal category becomes the current category, and the previous
3882 current category is deleted.
3883
3884 If both the first and goal categories also have archived items,
3885 the former are merged to the latter. If only the first category
3886 has archived items, the archived category is renamed to the goal
3887 category."
3888 (interactive)
3889 (let* ((tfile todos-current-todos-file)
3890 (archive (concat (file-name-sans-extension tfile) ".toda"))
3891 (cat (todos-current-category))
3892 (goal (todos-read-category "Category to merge to: " t))
3893 archived-count here)
3894 ;; Merge in todo file.
3895 (with-current-buffer (get-buffer (find-file-noselect tfile))
3896 (widen)
3897 (let* ((buffer-read-only nil)
3898 (cbeg (progn
3899 (re-search-backward
3900 (concat "^" (regexp-quote todos-category-beg)) nil t)
3901 (point-marker)))
3902 (tbeg (progn (forward-line) (point-marker)))
3903 (dbeg (progn
3904 (re-search-forward
3905 (concat "^" (regexp-quote todos-category-done)) nil t)
3906 (forward-line) (point-marker)))
3907 ;; Omit empty line between todo and done items.
3908 (tend (progn (forward-line -2) (point-marker)))
3909 (cend (progn
3910 (if (re-search-forward
3911 (concat "^" (regexp-quote todos-category-beg)) nil t)
3912 (progn
3913 (goto-char (match-beginning 0))
3914 (point-marker))
3915 (point-max-marker))))
3916 (todo (buffer-substring-no-properties tbeg tend))
3917 (done (buffer-substring-no-properties dbeg cend)))
3918 (goto-char (point-min))
3919 ;; Merge any todo items.
3920 (unless (zerop (length todo))
3921 (re-search-forward
3922 (concat "^" (regexp-quote (concat todos-category-beg goal))) nil t)
3923 (re-search-forward
3924 (concat "^" (regexp-quote todos-category-done)) nil t)
3925 (forward-line -1)
3926 (setq here (point-marker))
3927 (insert todo)
3928 (todos-update-count 'todo (todos-get-count 'todo cat) goal))
3929 ;; Merge any done items.
3930 (unless (zerop (length done))
3931 (goto-char (if (re-search-forward
3932 (concat "^" (regexp-quote todos-category-beg)) nil t)
3933 (match-beginning 0)
3934 (point-max)))
3935 (when (zerop (length todo)) (setq here (point-marker)))
3936 (insert done)
3937 (todos-update-count 'done (todos-get-count 'done cat) goal))
3938 (remove-overlays cbeg cend)
3939 (delete-region cbeg cend)
3940 (setq todos-categories (delete (assoc cat todos-categories)
3941 todos-categories))
3942 (todos-update-categories-sexp)
3943 (mapc (lambda (m) (set-marker m nil)) (list cbeg tbeg dbeg tend cend))))
3944 (when (file-exists-p archive)
3945 ;; Merge in archive file.
3946 (with-current-buffer (get-buffer (find-file-noselect archive))
3947 (widen)
3948 (goto-char (point-min))
3949 (let ((buffer-read-only nil)
3950 (cbeg (save-excursion
3951 (when (re-search-forward
3952 (concat "^" (regexp-quote
3953 (concat todos-category-beg cat)))
3954 nil t)
3955 (goto-char (match-beginning 0))
3956 (point-marker))))
3957 (gbeg (save-excursion
3958 (when (re-search-forward
3959 (concat "^" (regexp-quote
3960 (concat todos-category-beg goal)))
3961 nil t)
3962 (goto-char (match-beginning 0))
3963 (point-marker))))
3964 cend carch)
3965 (when cbeg
3966 (setq archived-count (todos-get-count 'done cat))
3967 (setq cend (save-excursion
3968 (if (re-search-forward
3969 (concat "^" (regexp-quote todos-category-beg))
3970 nil t)
3971 (match-beginning 0)
3972 (point-max))))
3973 (setq carch (save-excursion (goto-char cbeg) (forward-line)
3974 (buffer-substring-no-properties (point) cend)))
3975 ;; If both categories of the merge have archived items, merge the
3976 ;; source items to the goal items, else "merge" by renaming the
3977 ;; source category to goal.
3978 (if gbeg
3979 (progn
3980 (goto-char (if (re-search-forward
3981 (concat "^" (regexp-quote todos-category-beg))
3982 nil t)
3983 (match-beginning 0)
3984 (point-max)))
3985 (insert carch)
3986 (remove-overlays cbeg cend)
3987 (delete-region cbeg cend))
3988 (goto-char cbeg)
3989 (search-forward cat)
3990 (replace-match goal))
3991 (setq todos-categories (todos-make-categories-list t))
3992 (todos-update-categories-sexp)))))
3993 (with-current-buffer (get-file-buffer tfile)
3994 (when archived-count
3995 (unless (zerop archived-count)
3996 (todos-update-count 'archived archived-count goal)
3997 (todos-update-categories-sexp)))
3998 (todos-category-number goal)
3999 ;; If there are only merged done items, show them.
4000 (let ((todos-show-with-done (zerop (todos-get-count 'todo goal))))
4001 (todos-category-select)
4002 ;; Put point on the first merged item.
4003 (goto-char here)))
4004 (set-marker here nil)))
4005
4006 (defun todos-set-category-priority (&optional arg)
4007 "Change priority of category at point in Todos Categories buffer.
4008
4009 With ARG nil, prompt for the new priority number. Alternatively,
4010 the new priority can be provided by a numerical prefix ARG.
4011 Otherwise, if ARG is either of the symbols `raise' or `lower',
4012 raise or lower the category's priority by one."
4013 (interactive "P")
4014 (let ((curnum (save-excursion
4015 ;; Get the number representing the priority of the category
4016 ;; on the current line.
4017 (forward-line 0) (skip-chars-forward " ") (number-at-point))))
4018 (when curnum ; Do nothing if we're not on a category line.
4019 (let* ((maxnum (length todos-categories))
4020 (prompt (format "Set category priority (1-%d): " maxnum))
4021 (col (current-column))
4022 (buffer-read-only nil)
4023 (priority (cond ((and (eq arg 'raise) (> curnum 1))
4024 (1- curnum))
4025 ((and (eq arg 'lower) (< curnum maxnum))
4026 (1+ curnum))))
4027 candidate)
4028 (while (not priority)
4029 (setq candidate (or arg (read-number prompt)))
4030 (setq arg nil)
4031 (setq prompt
4032 (cond ((or (< candidate 1) (> candidate maxnum))
4033 (format "Priority must be an integer between 1 and %d: "
4034 maxnum))
4035 ((= candidate curnum)
4036 "Choose a different priority than the current one: ")))
4037 (unless prompt (setq priority candidate)))
4038 (let* ((lower (< curnum priority)) ; Priority is being lowered.
4039 (head (butlast todos-categories
4040 (apply (if lower 'identity '1+)
4041 (list (- maxnum priority)))))
4042 (tail (nthcdr (apply (if lower 'identity '1-) (list priority))
4043 todos-categories))
4044 ;; Category's name and items counts list.
4045 (catcons (nth (1- curnum) todos-categories))
4046 (todos-categories (nconc head (list catcons) tail))
4047 newcats)
4048 (when lower (setq todos-categories (nreverse todos-categories)))
4049 (setq todos-categories (delete-dups todos-categories))
4050 (when lower (setq todos-categories (nreverse todos-categories)))
4051 (setq newcats todos-categories)
4052 (kill-buffer)
4053 (with-current-buffer (find-buffer-visiting todos-current-todos-file)
4054 (setq todos-categories newcats)
4055 (todos-update-categories-sexp))
4056 (todos-display-categories)
4057 (forward-line (1+ priority))
4058 (forward-char col))))))
4059
4060 (defun todos-raise-category-priority ()
4061 "Raise priority of category at point in Todos Categories buffer."
4062 (interactive)
4063 (todos-set-category-priority 'raise))
4064
4065 (defun todos-lower-category-priority ()
4066 "Lower priority of category at point in Todos Categories buffer."
4067 (interactive)
4068 (todos-set-category-priority 'lower))
4069
4070 ;; ---------------------------------------------------------------------------
4071 ;;; Item editing commands
4072
4073 ;; FIXME: make insertion options customizable per category?
4074 ;;;###autoload
4075 (defun todos-insert-item (&optional arg diary nonmarking date-type time
4076 region-or-here)
4077 "Add a new Todo item to a category.
4078 \(See the note at the end of this document string about key
4079 bindings and convenience commands derived from this command.)
4080
4081 With no (or nil) prefix argument ARG, add the item to the current
4082 category; with one prefix argument (C-u), prompt for a category
4083 from the current Todos file; with two prefix arguments (C-u C-u),
4084 first prompt for a Todos file, then a category in that file. If
4085 a non-existing category is entered, ask whether to add it to the
4086 Todos file; if answered affirmatively, add the category and
4087 insert the item there.
4088
4089 When argument DIARY is non-nil, this overrides the intent of the
4090 user option `todos-include-in-diary' for this item: if
4091 `todos-include-in-diary' is nil, include the item in the Fancy
4092 Diary display, and if it is non-nil, exclude the item from the
4093 Fancy Diary display. When DIARY is nil, `todos-include-in-diary'
4094 has its intended effect.
4095
4096 When the item is included in the Fancy Diary display and the
4097 argument NONMARKING is non-nil, this overrides the intent of the
4098 user option `todos-diary-nonmarking' for this item: if
4099 `todos-diary-nonmarking' is nil, append `diary-nonmarking-symbol'
4100 to the item, and if it is non-nil, omit `diary-nonmarking-symbol'.
4101
4102 The argument DATE-TYPE determines the content of the item's
4103 mandatory date header string and how it is added:
4104 - If DATE-TYPE is the symbol `calendar', the Calendar pops up and
4105 when the user puts the cursor on a date and hits RET, that
4106 date, in the format set by `calendar-date-display-form',
4107 becomes the date in the header.
4108 - If DATE-TYPE is a string matching the regexp
4109 `todos-date-pattern', that string becomes the date in the
4110 header. This case is for the command
4111 `todos-insert-item-from-calendar' which is called from the
4112 Calendar.
4113 - If DATE-TYPE is the symbol `date', the header contains the date
4114 in the format set by `calendar-date-display-form', with year,
4115 month and day individually prompted for (month with tab
4116 completion).
4117 - If DATE-TYPE is the symbol `dayname' the header contains a
4118 weekday name instead of a date, prompted for with tab
4119 completion.
4120 - If DATE-TYPE has any other value (including nil or none) the
4121 header contains the current date (in the format set by
4122 `calendar-date-display-form').
4123
4124 With non-nil argument TIME prompt for a time string, which must
4125 match `diary-time-regexp'. Typing `<return>' at the prompt
4126 returns the current time, if the user option
4127 `todos-always-add-time-string' is non-nil, otherwise the empty
4128 string (i.e., no time string). If TIME is absent or nil, add or
4129 omit the current time string according as
4130 `todos-always-add-time-string' is non-nil or nil, respectively.
4131
4132 The argument REGION-OR-HERE determines the source and location of
4133 the new item:
4134 - If the REGION-OR-HERE is the symbol `here', prompt for the text
4135 of the new item and insert it directly above the todo item at
4136 point (hence lowering the priority of the remaining items), or
4137 if point is on the empty line below the last todo item, insert
4138 the new item there. An error is signalled if
4139 `todos-insert-item' is invoked with `here' outside of the
4140 current category.
4141 - If REGION-OR-HERE is the symbol `region', use the region of the
4142 current buffer as the text of the new item, depending on the
4143 value of user option `todos-use-only-highlighted-region': if
4144 this is non-nil, then use the region only when it is
4145 highlighted; otherwise, use the region regardless of
4146 highlighting. An error is signalled if there is no region in
4147 the current buffer. Prompt for the item's priority in the
4148 category (an integer between 1 and one more than the number of
4149 items in the category), and insert the item accordingly.
4150 - If REGION-OR-HERE has any other value (in particular, nil or
4151 none), prompt for the text and the item's priority, and insert
4152 the item accordingly.
4153
4154 To facilitate using these arguments when inserting a new todo
4155 item, convenience commands have been defined for all admissible
4156 combinations together with mnenomic key bindings based on on the
4157 name of the arguments and their order in the command's argument
4158 list: diar_y_ - nonmar_k_ing - _c_alendar or _d_ate or day_n_ame
4159 - _t_ime - _r_egion or _h_ere. These key combinations are
4160 appended to the basic insertion key (i) and keys that allow a
4161 following key must be doubled when used finally. For example,
4162 `iyh' will insert a new item with today's date, marked according
4163 to the DIARY argument described above, and with priority
4164 according to the HERE argument; while `iyy' does the same except
4165 the priority is not given by HERE but by prompting."
4166 ;; An alternative interface for customizing key
4167 ;; binding is also provided with the function
4168 ;; `todos-insertion-bindings'." ;FIXME
4169 (interactive "P")
4170 (let ((region (eq region-or-here 'region))
4171 (here (eq region-or-here 'here)))
4172 (when region
4173 (let (use-empty-active-region)
4174 (unless (and todos-use-only-highlighted-region (use-region-p))
4175 (error "There is no active region"))))
4176 (let* ((buf (current-buffer))
4177 (new-item (if region
4178 (buffer-substring-no-properties
4179 (region-beginning) (region-end))
4180 (read-from-minibuffer "Todo item: ")))
4181 (date-string (cond
4182 ((eq date-type 'date)
4183 (todos-read-date))
4184 ((eq date-type 'dayname)
4185 (todos-read-dayname))
4186 ((eq date-type 'calendar)
4187 (setq todos-date-from-calendar t)
4188 (todos-set-date-from-calendar))
4189 ((and (stringp date-type)
4190 (string-match todos-date-pattern date-type))
4191 (setq todos-date-from-calendar date-type)
4192 (todos-set-date-from-calendar))
4193 (t (calendar-date-string (calendar-current-date) t t))))
4194 (time-string (or (and time (todos-read-time))
4195 (and todos-always-add-time-string
4196 (substring (current-time-string) 11 16)))))
4197 (setq todos-date-from-calendar nil)
4198 (cond ((equal arg '(16))
4199 (todos-jump-to-category nil t)
4200 (set-window-buffer
4201 (selected-window)
4202 (set-buffer (find-buffer-visiting todos-global-current-todos-file))))
4203 ((equal arg '(4))
4204 (todos-jump-to-category)
4205 (set-window-buffer
4206 (selected-window)
4207 (set-buffer (find-buffer-visiting todos-global-current-todos-file))))
4208 (t
4209 (when (not (derived-mode-p 'todos-mode)) (todos-show))))
4210 (let (buffer-read-only)
4211 (setq new-item
4212 ;; Add date, time and diary marking as required.
4213 (concat (if (not (and diary (not todos-include-in-diary)))
4214 todos-nondiary-start
4215 (when (and nonmarking (not todos-diary-nonmarking))
4216 diary-nonmarking-symbol))
4217 date-string (when (and time-string ; Can be empty string.
4218 (not (zerop (length time-string))))
4219 (concat " " time-string))
4220 (when (not (and diary (not todos-include-in-diary)))
4221 todos-nondiary-end)
4222 " " new-item))
4223 ;; Indent newlines inserted by C-q C-j if nonspace char follows.
4224 (setq new-item (replace-regexp-in-string
4225 "\\(\n\\)[^[:blank:]]"
4226 (concat "\n" (make-string todos-indent-to-here 32))
4227 new-item nil nil 1))
4228 (if here
4229 (cond ((not (eq major-mode 'todos-mode))
4230 (error "Cannot insert a todo item here outside of Todos mode"))
4231 ((not (eq buf (current-buffer)))
4232 (error "Cannot insert an item here after changing buffer"))
4233 ((or (todos-done-item-p)
4234 ;; Point on last blank line.
4235 (save-excursion (forward-line -1) (todos-done-item-p)))
4236 (error "Cannot insert a new item in the done item section"))
4237 (t
4238 (todos-insert-with-overlays new-item)))
4239 (todos-set-item-priority new-item (todos-current-category) t))
4240 (todos-update-count 'todo 1)
4241 (if (or diary todos-include-in-diary) (todos-update-count 'diary 1))
4242 (todos-update-categories-sexp)))))
4243
4244 (defvar todos-date-from-calendar nil
4245 "Helper variable for setting item date from the Emacs Calendar.")
4246
4247 (defun todos-set-date-from-calendar ()
4248 "Return string of date chosen from Calendar."
4249 (cond ((and (stringp todos-date-from-calendar)
4250 (string-match todos-date-pattern todos-date-from-calendar))
4251 todos-date-from-calendar)
4252 (todos-date-from-calendar
4253 (let (calendar-view-diary-initially-flag)
4254 (calendar))
4255 ;; *Calendar* is now current buffer.
4256 (local-set-key (kbd "RET") 'exit-recursive-edit)
4257 (message "Put cursor on a date and type <return> to set it.")
4258 ;; FIXME: is there a better way than recursive-edit?
4259 (recursive-edit)
4260 (setq todos-date-from-calendar
4261 (calendar-date-string (calendar-cursor-to-date t) t t))
4262 (calendar-exit)
4263 todos-date-from-calendar)))
4264
4265 (defun todos-delete-item ()
4266 "Delete at least one item in this category.
4267
4268 If there are marked items, delete all of these; otherwise, delete
4269 the item at point."
4270 (interactive)
4271 (let (ov)
4272 (unwind-protect
4273 (let* ((cat (todos-current-category))
4274 (marked (assoc cat todos-categories-with-marks))
4275 (item (unless marked (todos-item-string)))
4276 ;; FIXME: make confirmation an option?
4277 (answer (if marked
4278 (y-or-n-p "Permanently delete all marked items? ")
4279 (when item
4280 (setq ov (make-overlay
4281 (save-excursion (todos-item-start))
4282 (save-excursion (todos-item-end))))
4283 (overlay-put ov 'face 'todos-search)
4284 (y-or-n-p (concat "Permanently delete this item? ")))))
4285 (opoint (point))
4286 buffer-read-only)
4287 (when answer
4288 (and marked (goto-char (point-min)))
4289 (catch 'done
4290 (while (not (eobp))
4291 (if (or (and marked (todos-marked-item-p)) item)
4292 (progn
4293 (if (todos-done-item-p)
4294 (todos-update-count 'done -1)
4295 (todos-update-count 'todo -1 cat)
4296 (and (todos-diary-item-p) (todos-update-count 'diary -1)))
4297 (if ov (delete-overlay ov))
4298 (todos-remove-item)
4299 ;; Don't leave point below last item.
4300 (and item (bolp) (eolp) (< (point-min) (point-max))
4301 (todos-backward-item))
4302 (when item
4303 (throw 'done (setq item nil))))
4304 (todos-forward-item))))
4305 (when marked
4306 (remove-overlays (point-min) (point-max)
4307 'before-string todos-item-mark)
4308 (setq todos-categories-with-marks
4309 (assq-delete-all cat todos-categories-with-marks))
4310 (goto-char opoint))
4311 (todos-update-categories-sexp)
4312 (todos-prefix-overlays)))
4313 (if ov (delete-overlay ov)))))
4314
4315 (defun todos-edit-item ()
4316 "Edit the Todo item at point.
4317 If the item consists of only one logical line, edit it in the
4318 minibuffer; otherwise, edit it in Todos Edit mode."
4319 (interactive)
4320 (when (todos-item-string)
4321 (let* ((buffer-read-only)
4322 (start (todos-item-start))
4323 (item-beg (progn
4324 (re-search-forward
4325 (concat todos-date-string-start todos-date-pattern
4326 "\\( " diary-time-regexp "\\)?"
4327 (regexp-quote todos-nondiary-end) "?")
4328 (line-end-position) t)
4329 (1+ (- (point) start))))
4330 (item (todos-item-string))
4331 (multiline (> (length (split-string item "\n")) 1))
4332 (opoint (point)))
4333 (if multiline
4334 (todos-edit-multiline t)
4335 (let ((new (read-string "Edit: " (cons item item-beg))))
4336 (while (not (string-match
4337 (concat todos-date-string-start todos-date-pattern) new))
4338 (setq new (read-from-minibuffer
4339 "Item must start with a date: " new)))
4340 ;; Indent newlines inserted by C-q C-j if nonspace char follows.
4341 (setq new (replace-regexp-in-string
4342 "\\(\n\\)[^[:blank:]]"
4343 (concat "\n" (make-string todos-indent-to-here 32)) new
4344 nil nil 1))
4345 ;; If user moved point during editing, make sure it moves back.
4346 (goto-char opoint)
4347 (todos-remove-item)
4348 (todos-insert-with-overlays new)
4349 (move-to-column item-beg))))))
4350
4351 (defun todos-edit-multiline-item ()
4352 "Edit current Todo item in Todos Edit mode.
4353 Use of newlines invokes `todos-indent' to insure compliance with
4354 the format of Diary entries."
4355 (interactive)
4356 (todos-edit-multiline t))
4357
4358 (defun todos-edit-multiline (&optional item)
4359 ""
4360 (interactive)
4361 ;; FIXME: should there be only one live Todos Edit buffer?
4362 ;; (let ((buffer-name todos-edit-buffer))
4363 (let ((buffer-name (generate-new-buffer-name todos-edit-buffer)))
4364 (set-window-buffer
4365 (selected-window)
4366 (set-buffer (make-indirect-buffer
4367 (file-name-nondirectory todos-current-todos-file)
4368 buffer-name)))
4369 (if item
4370 (narrow-to-region (todos-item-start) (todos-item-end))
4371 (widen))
4372 (todos-edit-mode)
4373 (message "%s" (substitute-command-keys
4374 (concat "Type \\[todos-edit-quit] to check file format "
4375 "validity and return to Todos mode.\n")))))
4376
4377 (defun todos-edit-quit ()
4378 "Return from Todos Edit mode to Todos mode.
4379
4380 If the whole file was in Todos Edit mode, check before returning
4381 whether the file is still a valid Todos file and if so, also
4382 recalculate the Todos categories sexp, in case changes were made
4383 in the number or names of categories."
4384 (interactive)
4385 ;; FIXME: Should do todos-check-format only if file was actually changed --
4386 ;; but how to tell?
4387 (when (eq (buffer-size) (- (point-max) (point-min)))
4388 (when (todos-check-format) (todos-repair-categories-sexp)))
4389 (kill-buffer)
4390 ;; In case next buffer is not the one holding todos-current-todos-file.
4391 (todos-show))
4392
4393 (defun todos-edit-item-header (&optional what)
4394 "Edit date/time header of at least one item.
4395
4396 Interactively, ask whether to edit year, month and day or day of
4397 the week, as well as time. If there are marked items, apply the
4398 changes to all of these; otherwise, edit just the item at point.
4399
4400 Non-interactively, argument WHAT specifies whether to set the
4401 date from the Calendar or to today, or whether to edit only the
4402 date or day, or only the time."
4403 (interactive)
4404 (let* ((cat (todos-current-category))
4405 (marked (assoc cat todos-categories-with-marks))
4406 (first t) ; Match only first of marked items.
4407 (todos-date-from-calendar t)
4408 ndate ntime nheader)
4409 (save-excursion
4410 (or (and marked (goto-char (point-min))) (todos-item-start))
4411 (catch 'stop
4412 (while (not (eobp))
4413 (and marked
4414 (while (not (todos-marked-item-p))
4415 (todos-forward-item)
4416 (and (eobp) (throw 'stop nil))))
4417 (re-search-forward (concat todos-date-string-start "\\(?1:"
4418 todos-date-pattern
4419 "\\)\\(?2: " diary-time-regexp "\\)?")
4420 (line-end-position) t)
4421 (let* ((odate (match-string-no-properties 1))
4422 (otime (match-string-no-properties 2))
4423 (buffer-read-only))
4424 (cond ((eq what 'today)
4425 (progn
4426 (setq ndate (calendar-date-string
4427 (calendar-current-date) t t))
4428 (replace-match ndate nil nil nil 1)))
4429 ((eq what 'calendar)
4430 (setq ndate (save-match-data (todos-set-date-from-calendar)))
4431 (replace-match ndate nil nil nil 1))
4432 (t
4433 (unless (eq what 'timeonly)
4434 (when first
4435 (setq ndate (if (save-match-data
4436 (string-match "[0-9]+" odate))
4437 (if (y-or-n-p "Change date? ")
4438 (todos-read-date)
4439 (todos-read-dayname))
4440 (if (y-or-n-p "Change day? ")
4441 (todos-read-dayname)
4442 (todos-read-date)))))
4443 (replace-match ndate nil nil nil 1))
4444 (unless (eq what 'dateonly)
4445 (when first
4446 (setq ntime (save-match-data (todos-read-time)))
4447 (when (< 0 (length ntime))
4448 (setq ntime (concat " " ntime))))
4449 (if otime
4450 (replace-match ntime nil nil nil 2)
4451 (goto-char (match-end 1))
4452 (insert ntime)))))
4453 (setq todos-date-from-calendar nil)
4454 (setq first nil))
4455 (if marked
4456 (todos-forward-item)
4457 (goto-char (point-max))))))))
4458
4459 (defun todos-edit-item-date ()
4460 "Prompt for and apply changes to current item's date."
4461 (interactive)
4462 (todos-edit-item-header 'dateonly))
4463
4464 (defun todos-edit-item-date-from-calendar ()
4465 "Prompt for changes to current item's date and apply from Calendar."
4466 (interactive)
4467 (todos-edit-item-header 'calendar))
4468
4469 (defun todos-edit-item-date-is-today ()
4470 "Set item date to today's date."
4471 (interactive)
4472 (todos-edit-item-header 'today))
4473
4474 (defun todos-edit-item-time ()
4475 "Prompt For and apply changes to current item's time."
4476 (interactive)
4477 (todos-edit-item-header 'timeonly))
4478
4479 (defun todos-edit-item-diary-inclusion ()
4480 "Change diary status of one or more todo items in this category.
4481 That is, insert `todos-nondiary-marker' if the candidate items
4482 lack this marking; otherwise, remove it.
4483
4484 If there are marked todo items, change the diary status of all
4485 and only these, otherwise change the diary status of the item at
4486 point."
4487 (interactive)
4488 (let ((buffer-read-only)
4489 (marked (assoc (todos-current-category)
4490 todos-categories-with-marks)))
4491 (catch 'stop
4492 (save-excursion
4493 (when marked (goto-char (point-min)))
4494 (while (not (eobp))
4495 (if (todos-done-item-p)
4496 (throw 'stop (message "Done items cannot be edited"))
4497 (unless (and marked (not (todos-marked-item-p)))
4498 (let* ((beg (todos-item-start))
4499 (lim (save-excursion (todos-item-end)))
4500 (end (save-excursion
4501 (or (todos-time-string-matcher lim)
4502 (todos-date-string-matcher lim)))))
4503 (if (looking-at (regexp-quote todos-nondiary-start))
4504 (progn
4505 (replace-match "")
4506 (search-forward todos-nondiary-end (1+ end) t)
4507 (replace-match "")
4508 (todos-update-count 'diary 1))
4509 (when end
4510 (insert todos-nondiary-start)
4511 (goto-char (1+ end))
4512 (insert todos-nondiary-end)
4513 (todos-update-count 'diary -1)))))
4514 (unless marked (throw 'stop nil))
4515 (todos-forward-item)))))
4516 (todos-update-categories-sexp)))
4517
4518 (defun todos-edit-category-diary-inclusion (arg)
4519 "Make all items in this category diary items.
4520 With prefix ARG, make all items in this category non-diary
4521 items."
4522 (interactive "P")
4523 (save-excursion
4524 (goto-char (point-min))
4525 (let ((todo-count (todos-get-count 'todo))
4526 (diary-count (todos-get-count 'diary))
4527 (buffer-read-only))
4528 (catch 'stop
4529 (while (not (eobp))
4530 (if (todos-done-item-p) ; We've gone too far.
4531 (throw 'stop nil)
4532 (let* ((beg (todos-item-start))
4533 (lim (save-excursion (todos-item-end)))
4534 (end (save-excursion
4535 (or (todos-time-string-matcher lim)
4536 (todos-date-string-matcher lim)))))
4537 (if arg
4538 (unless (looking-at (regexp-quote todos-nondiary-start))
4539 (insert todos-nondiary-start)
4540 (goto-char (1+ end))
4541 (insert todos-nondiary-end))
4542 (when (looking-at (regexp-quote todos-nondiary-start))
4543 (replace-match "")
4544 (search-forward todos-nondiary-end (1+ end) t)
4545 (replace-match "")))))
4546 (todos-forward-item))
4547 (unless (if arg (zerop diary-count) (= diary-count todo-count))
4548 (todos-update-count 'diary (if arg
4549 (- diary-count)
4550 (- todo-count diary-count))))
4551 (todos-update-categories-sexp)))))
4552
4553 (defun todos-edit-item-diary-nonmarking ()
4554 "Change non-marking of one or more diary items in this category.
4555 That is, insert `diary-nonmarking-symbol' if the candidate items
4556 lack this marking; otherwise, remove it.
4557
4558 If there are marked todo items, change the non-marking status of
4559 all and only these, otherwise change the non-marking status of
4560 the item at point."
4561 (interactive)
4562 (let ((buffer-read-only)
4563 (marked (assoc (todos-current-category)
4564 todos-categories-with-marks)))
4565 (catch 'stop
4566 (save-excursion
4567 (when marked (goto-char (point-min)))
4568 (while (not (eobp))
4569 (if (todos-done-item-p)
4570 (throw 'stop (message "Done items cannot be edited"))
4571 (unless (and marked (not (todos-marked-item-p)))
4572 (todos-item-start)
4573 (unless (looking-at (regexp-quote todos-nondiary-start))
4574 (if (looking-at (regexp-quote diary-nonmarking-symbol))
4575 (replace-match "")
4576 (insert diary-nonmarking-symbol))))
4577 (unless marked (throw 'stop nil))
4578 (todos-forward-item)))))))
4579
4580 (defun todos-edit-category-diary-nonmarking (arg)
4581 "Add `diary-nonmarking-symbol' to all diary items in this category.
4582 With prefix ARG, remove `diary-nonmarking-symbol' from all diary
4583 items in this category."
4584 (interactive "P")
4585 (save-excursion
4586 (goto-char (point-min))
4587 (let (buffer-read-only)
4588 (catch 'stop
4589 (while (not (eobp))
4590 (if (todos-done-item-p) ; We've gone too far.
4591 (throw 'stop nil)
4592 (unless (looking-at (regexp-quote todos-nondiary-start))
4593 (if arg
4594 (when (looking-at (regexp-quote diary-nonmarking-symbol))
4595 (replace-match ""))
4596 (unless (looking-at (regexp-quote diary-nonmarking-symbol))
4597 (insert diary-nonmarking-symbol))))
4598 (todos-forward-item)))))))
4599
4600 (defun todos-set-item-priority (&optional item cat new arg)
4601 "Set todo ITEM's priority in CATegory and move item accordingly.
4602
4603 Interactively, ITEM defaults to the item at point, CAT to the
4604 current category in Todos mode, and the priority is a number
4605 between 1 and the number of items in the category.
4606 Non-interactively, non-nil NEW means ITEM is a new item and the
4607 lowest priority is one more than the number of items in CAT.
4608
4609 The new priority is set either interactively by prompt or by a
4610 numerical prefix argument, or noninteractively by argument ARG,
4611 whose value can be either of the symbols `raise' or `lower',
4612 meaning to raise or lower the item's priority by one."
4613 (interactive)
4614 (let* ((item (or item (todos-item-string)))
4615 (marked (todos-marked-item-p))
4616 (cat (or cat (cond ((eq major-mode 'todos-mode)
4617 (todos-current-category))
4618 ((eq major-mode 'todos-filtered-items-mode)
4619 (let* ((regexp1
4620 (concat todos-date-string-start
4621 todos-date-pattern
4622 "\\( " diary-time-regexp "\\)?"
4623 (regexp-quote todos-nondiary-end)
4624 "?\\(?1: \\[\\(.+:\\)?.+\\]\\)")))
4625 (save-excursion
4626 (re-search-forward regexp1 nil t)
4627 (match-string-no-properties 1)))))))
4628 curnum
4629 (todo (cond ((or (eq arg 'raise) (eq arg 'lower)
4630 (eq major-mode 'todos-filtered-items-mode))
4631 (save-excursion
4632 (let ((curstart (todos-item-start))
4633 (count 0))
4634 (goto-char (point-min))
4635 (while (looking-at todos-item-start)
4636 (setq count (1+ count))
4637 (when (= (point) curstart) (setq curnum count))
4638 (todos-forward-item))
4639 count)))
4640 ((eq major-mode 'todos-mode)
4641 (todos-get-count 'todo cat))))
4642 (maxnum (if new (1+ todo) todo))
4643 (prompt (format "Set item priority (1-%d): " maxnum))
4644 (priority (cond ((numberp current-prefix-arg)
4645 current-prefix-arg)
4646 ((and (eq arg 'raise) (>= curnum 1))
4647 (1- curnum))
4648 ((and (eq arg 'lower) (<= curnum maxnum))
4649 (1+ curnum))))
4650 candidate
4651 buffer-read-only)
4652 (unless (and priority
4653 (or (and (eq arg 'raise) (zerop priority))
4654 (and (eq arg 'lower) (> priority maxnum))))
4655 ;; When moving item to another category, show the category before
4656 ;; prompting for its priority.
4657 (unless (or arg (called-interactively-p t))
4658 (todos-category-number cat)
4659 (todos-category-select))
4660 (while (not priority)
4661 (setq candidate (read-number prompt))
4662 (setq prompt (when (or (< candidate 1) (> candidate maxnum))
4663 (format "Priority must be an integer between 1 and %d.\n"
4664 maxnum)))
4665 (unless prompt (setq priority candidate)))
4666 ;; In Top Priorities buffer, an item's priority can be changed
4667 ;; wrt items in another category, but not wrt items in the same
4668 ;; category.
4669 (when (eq major-mode 'todos-filtered-items-mode)
4670 (let* ((regexp2 (concat todos-date-string-start todos-date-pattern
4671 "\\( " diary-time-regexp "\\)?"
4672 (regexp-quote todos-nondiary-end)
4673 "?\\(?1:" (regexp-quote cat) "\\)"))
4674 (end (cond ((< curnum priority)
4675 (save-excursion (todos-item-end)))
4676 ((> curnum priority)
4677 (save-excursion (todos-item-start)))))
4678 (match (save-excursion
4679 (cond ((< curnum priority)
4680 (todos-forward-item (1+ (- priority curnum)))
4681 (when (re-search-backward regexp2 end t)
4682 (match-string-no-properties 1)))
4683 ((> curnum priority)
4684 (todos-backward-item (- curnum priority))
4685 (when (re-search-forward regexp2 end t)
4686 (match-string-no-properties 1)))))))
4687 (when match
4688 (error (concat "Cannot reprioritize items from the same "
4689 "category in this mode, only in Todos mode")))))
4690 ;; Interactively or with non-nil ARG, relocate the item within its
4691 ;; category.
4692 (when (or arg (called-interactively-p))
4693 (todos-remove-item))
4694 (goto-char (point-min))
4695 (unless (= priority 1) (todos-forward-item (1- priority)))
4696 (todos-insert-with-overlays item)
4697 ;; If item was marked, restore the mark.
4698 (and marked (overlay-put (make-overlay (point) (point))
4699 'before-string todos-item-mark)))))
4700
4701 (defun todos-raise-item-priority ()
4702 "Raise priority of current item by moving it up by one item."
4703 (interactive)
4704 (todos-set-item-priority nil nil nil 'raise))
4705
4706 (defun todos-lower-item-priority ()
4707 "Lower priority of current item by moving it down by one item."
4708 (interactive)
4709 (todos-set-item-priority nil nil nil 'lower))
4710
4711 (defun todos-move-item (&optional file)
4712 "Move at least one todo item to another category.
4713
4714 If there are marked items, move all of these; otherwise, move
4715 the item at point.
4716
4717 With non-nil argument FILE, first prompt for another Todos file and
4718 then a category in that file to move the item or items to.
4719
4720 If the chosen category is not one of the existing categories,
4721 then it is created and the item(s) become(s) the first
4722 entry/entries in that category."
4723 (interactive)
4724 (unless (or (todos-done-item-p)
4725 ;; Point is between todo and done items.
4726 (looking-at "^$"))
4727 (let* ((buffer-read-only)
4728 (file1 todos-current-todos-file)
4729 (cat1 (todos-current-category))
4730 (marked (assoc cat1 todos-categories-with-marks))
4731 (num todos-category-number)
4732 (item (todos-item-string))
4733 (diary-item (todos-diary-item-p))
4734 (omark (save-excursion (todos-item-start) (point-marker)))
4735 (file2 (if file
4736 (todos-read-file-name "Choose a Todos file: " nil t)
4737 file1))
4738 (count 0)
4739 (count-diary 0)
4740 ov cat2 nmark)
4741 (set-buffer (find-file-noselect file2))
4742 (unwind-protect
4743 (progn
4744 (unless marked
4745 (setq ov (make-overlay (save-excursion (todos-item-start))
4746 (save-excursion (todos-item-end))))
4747 (overlay-put ov 'face 'todos-search))
4748 (setq cat2 (let* ((pl (if (and marked (> (cdr marked) 1)) "s" ""))
4749 (name (todos-read-category
4750 (concat "Move item" pl " to category: ")))
4751 (prompt (concat "Choose a different category than "
4752 "the current one\n(type `"
4753 (key-description
4754 (car (where-is-internal
4755 'todos-set-item-priority)))
4756 "' to reprioritize item "
4757 "within the same category): ")))
4758 (while (equal name cat1)
4759 (setq name (todos-read-category prompt)))
4760 name)))
4761 (if ov (delete-overlay ov)))
4762 (set-buffer (find-buffer-visiting file1))
4763 (if marked
4764 (progn
4765 (setq item nil)
4766 (goto-char (point-min))
4767 (while (not (eobp))
4768 (when (todos-marked-item-p)
4769 (setq item (concat item (todos-item-string) "\n"))
4770 (setq count (1+ count))
4771 (when (todos-diary-item-p)
4772 (setq count-diary (1+ count-diary))))
4773 (todos-forward-item))
4774 ;; Chop off last newline.
4775 (setq item (substring item 0 -1)))
4776 (setq count 1)
4777 (when (todos-diary-item-p) (setq count-diary 1)))
4778 (set-window-buffer (selected-window)
4779 (set-buffer (find-file-noselect file2)))
4780 (unless (assoc cat2 todos-categories) (todos-add-category cat2))
4781 (todos-set-item-priority item cat2 t)
4782 (setq nmark (point-marker))
4783 (todos-update-count 'todo count)
4784 (todos-update-count 'diary count-diary)
4785 (todos-update-categories-sexp)
4786 (with-current-buffer (find-buffer-visiting file1)
4787 (save-excursion
4788 (save-restriction
4789 (widen)
4790 (goto-char omark)
4791 (if marked
4792 (let (beg end)
4793 (setq item nil)
4794 (re-search-backward
4795 (concat "^" (regexp-quote todos-category-beg)) nil t)
4796 (forward-line)
4797 (setq beg (point))
4798 (re-search-forward
4799 (concat "^" (regexp-quote todos-category-done)) nil t)
4800 (setq end (match-beginning 0))
4801 (goto-char beg)
4802 (while (< (point) end)
4803 (if (todos-marked-item-p)
4804 (todos-remove-item)
4805 (todos-forward-item)))
4806 ;; FIXME: does this work?
4807 (remove-overlays (point-min) (point-max)
4808 'before-string todos-item-mark)
4809 (setq todos-categories-with-marks
4810 (assq-delete-all cat todos-categories-with-marks)))
4811 (if ov (delete-overlay ov))
4812 (todos-remove-item))))
4813 (todos-update-count 'todo (- count) cat1)
4814 (todos-update-count 'diary (- count-diary) cat1)
4815 (todos-update-categories-sexp))
4816 (set-window-buffer (selected-window)
4817 (set-buffer (find-file-noselect file2)))
4818 (setq todos-category-number (todos-category-number cat2))
4819 (todos-category-select)
4820 (goto-char nmark))))
4821
4822 (defun todos-move-item-to-file ()
4823 "Move the current todo item to a category in another Todos file."
4824 (interactive)
4825 (todos-move-item t))
4826
4827 ;; (defun todos-move-item-to-diary ()
4828 ;; "Move one or more items in current category to the diary file.
4829 ;;
4830 ;; If there are marked items, move all of these; otherwise, move
4831 ;; the item at point."
4832 ;; (interactive)
4833 ;; ;; FIXME
4834 ;; )
4835
4836 ;; FIXME: make adding date customizable, and make this and time customization
4837 ;; overridable via double prefix arg ??
4838 (defun todos-item-done (&optional arg)
4839 "Tag at least one item in this category as done and hide it.
4840
4841 With prefix argument ARG prompt for a comment and append it to
4842 the done item; this is only possible if there are no marked
4843 items. If there are marked items, tag all of these with
4844 `todos-done-string' plus the current date and, if
4845 `todos-always-add-time-string' is non-nil, the current time;
4846 otherwise, just tag the item at point. Items tagged as done are
4847 relocated to the category's (by default hidden) done section."
4848 (interactive "P")
4849 (let* ((cat (todos-current-category))
4850 (marked (assoc cat todos-categories-with-marks)))
4851 (unless (or (todos-done-item-p)
4852 (and (looking-at "^$") (not marked)))
4853 (let* ((date-string (calendar-date-string (calendar-current-date) t t))
4854 (time-string (if todos-always-add-time-string
4855 (concat " " (substring (current-time-string) 11 16))
4856 ""))
4857 (done-prefix (concat "[" todos-done-string date-string time-string
4858 "] "))
4859 (comment (and arg (not marked) (read-string "Enter a comment: ")))
4860 (item-count 0)
4861 (diary-count 0)
4862 item done-item
4863 (buffer-read-only))
4864 (and marked (goto-char (point-min)))
4865 (catch 'done
4866 (while (not (eobp))
4867 (if (or (not marked) (and marked (todos-marked-item-p)))
4868 (progn
4869 (setq item (todos-item-string))
4870 (setq done-item (cond (marked
4871 (concat done-item done-prefix item "\n"))
4872 (comment
4873 (concat done-prefix item " ["
4874 todos-comment-string
4875 ": " comment "]"))
4876 (t
4877 (concat done-prefix item))))
4878 (setq item-count (1+ item-count))
4879 (when (todos-diary-item-p)
4880 (setq diary-count (1+ diary-count)))
4881 (todos-remove-item)
4882 (unless marked (throw 'done nil)))
4883 (todos-forward-item))))
4884 (when marked
4885 ;; Chop off last newline of done item string.
4886 (setq done-item (substring done-item 0 -1))
4887 (remove-overlays (point-min) (point-max) 'before-string todos-item-mark)
4888 (setq todos-categories-with-marks
4889 (assq-delete-all cat todos-categories-with-marks)))
4890 (save-excursion
4891 (widen)
4892 (re-search-forward
4893 (concat "^" (regexp-quote todos-category-done)) nil t)
4894 (forward-char)
4895 (insert done-item "\n"))
4896 (todos-update-count 'todo (- item-count))
4897 (todos-update-count 'done item-count)
4898 (todos-update-count 'diary (- diary-count))
4899 (todos-update-categories-sexp)
4900 (save-excursion (todos-category-select))))))
4901
4902 (defun todos-done-item-add-edit-or-delete-comment (&optional arg)
4903 "Add a comment to this done item or edit an existing comment.
4904 With prefix ARG delete an existing comment."
4905 (interactive "P")
4906 (when (todos-done-item-p)
4907 (let ((item (todos-item-string))
4908 (end (save-excursion (todos-item-end)))
4909 comment buffer-read-only)
4910 (save-excursion
4911 (todos-item-start)
4912 (if (re-search-forward (concat " \\["
4913 (regexp-quote todos-comment-string)
4914 ": \\([^]]+\\)\\]") end t)
4915 (if arg
4916 (when (y-or-n-p "Delete comment? ")
4917 (delete-region (match-beginning 0) (match-end 0)))
4918 (setq comment (read-string "Edit comment: "
4919 (cons (match-string 1) 1)))
4920 (replace-match comment nil nil nil 1))
4921 (setq comment (read-string "Enter a comment: "))
4922 (todos-item-end)
4923 (insert " [" todos-comment-string ": " comment "]"))))))
4924
4925 (defun todos-item-undo ()
4926 "Restore this done item to the todo section of this category.
4927 If done item has a comment, ask whether to omit the comment from
4928 the restored item."
4929 (interactive)
4930 (let* ((cat (todos-current-category))
4931 (marked (assoc cat todos-categories-with-marks)))
4932 (when (or marked (todos-done-item-p))
4933 (let ((buffer-read-only)
4934 (bufmod (buffer-modified-p))
4935 (opoint (point))
4936 (orig-mrk (progn (todos-item-start) (point-marker)))
4937 (orig-item (todos-item-string))
4938 (first 'first)
4939 (item-count 0)
4940 (diary-count 0)
4941 start end item undone)
4942 (and marked (goto-char (point-min)))
4943 (catch 'done
4944 (while (not (eobp))
4945 (if (or (not marked) (and marked (todos-marked-item-p)))
4946 (if (not (todos-done-item-p))
4947 (error "Only done items can be undone")
4948 (todos-item-start)
4949 ;; Find the end of the date string added upon tagging item as
4950 ;; done.
4951 (setq start (search-forward "] "))
4952 (setq item-count (1+ item-count))
4953 (unless (looking-at (regexp-quote todos-nondiary-start))
4954 (setq diary-count (1+ diary-count)))
4955 (setq end (save-excursion (todos-item-end)))
4956 ;; Ask (once) whether to omit done item's comment. If
4957 ;; affirmed, omit subsequent comments without asking.
4958 (when (re-search-forward
4959 (concat " \\[" (regexp-quote todos-comment-string)
4960 ": [^]]+\\]") end t)
4961 (if (eq first 'first)
4962 (setq first
4963 (if (eq todos-undo-item-omit-comment 'ask)
4964 (when (y-or-n-p
4965 "Omit comment from restored item? ")
4966 'omit)
4967 (when todos-undo-item-omit-comment 'omit)))
4968 t)
4969 (when (eq first 'omit)
4970 (delete-region (match-beginning 0) (match-end 0))
4971 (setq end (point))))
4972 (setq item (concat item
4973 (buffer-substring-no-properties start end)
4974 (when marked "\n")))
4975 (todos-remove-item)
4976 (unless marked (throw 'done nil)))
4977 (todos-forward-item))))
4978 (if marked
4979 (progn
4980 ;; (remove-overlays (point-min) (point-max)
4981 ;; 'before-string todos-item-mark)
4982 (setq todos-categories-with-marks
4983 (assq-delete-all cat todos-categories-with-marks))
4984 ;; Insert undone items that were marked at end of todo item list.
4985 (goto-char (point-min))
4986 (re-search-forward (concat "^" (regexp-quote todos-category-done))
4987 nil t)
4988 (forward-line -1)
4989 (insert item)
4990 (todos-update-count 'todo item-count)
4991 (todos-update-count 'done (- item-count))
4992 (when diary-count (todos-update-count 'diary diary-count))
4993 (todos-update-categories-sexp)
4994 (let ((todos-show-with-done (> (todos-get-count 'done) 0)))
4995 (todos-category-select)))
4996 ;; With an unmarked undone item, prompt for its priority. If user
4997 ;; cancels before setting new priority, then leave the done item
4998 ;; unchanged.
4999 (unwind-protect
5000 (progn
5001 (todos-set-item-priority item (todos-current-category) t)
5002 (setq undone t)
5003 (todos-update-count 'todo 1)
5004 (todos-update-count 'done -1)
5005 (and (todos-diary-item-p) (todos-update-count 'diary 1))
5006 (todos-update-categories-sexp)
5007 (let ((todos-show-with-done (> (todos-get-count 'done) 0)))
5008 (todos-category-select)))
5009 (unless undone
5010 (let ((todos-show-with-done t))
5011 (widen)
5012 (goto-char orig-mrk)
5013 (todos-insert-with-overlays orig-item)
5014 (set-buffer-modified-p bufmod)
5015 (todos-category-select))
5016 (goto-char opoint))))
5017 (set-marker orig-mrk nil)))))
5018
5019 (defun todos-archive-done-item (&optional all)
5020 "Archive at least one done item in this category.
5021
5022 If there are marked done items (and no marked todo items),
5023 archive all of these; otherwise, with non-nil argument ALL,
5024 archive all done items in this category; otherwise, archive the
5025 done item at point.
5026
5027 If the archive of this file does not exist, it is created. If
5028 this category does not exist in the archive, it is created."
5029 (interactive)
5030 (when (eq major-mode 'todos-mode)
5031 (if (and all (zerop (todos-get-count 'done)))
5032 (message "No done items in this category")
5033 (catch 'end
5034 (let* ((cat (todos-current-category))
5035 (tbuf (current-buffer))
5036 (marked (assoc cat todos-categories-with-marks))
5037 (afile (concat (file-name-sans-extension
5038 todos-current-todos-file) ".toda"))
5039 (archive (if (file-exists-p afile)
5040 (find-file-noselect afile t)
5041 (get-buffer-create afile)))
5042 (item (and (todos-done-item-p) (concat (todos-item-string) "\n")))
5043 (count 0)
5044 marked-items beg end all-done
5045 buffer-read-only)
5046 (cond
5047 (marked
5048 (save-excursion
5049 (goto-char (point-min))
5050 (while (not (eobp))
5051 (when (todos-marked-item-p)
5052 (if (not (todos-done-item-p))
5053 (throw 'end (message "Only done items can be archived"))
5054 (setq marked-items
5055 (concat marked-items (todos-item-string) "\n"))
5056 (setq count (1+ count))))
5057 (todos-forward-item))))
5058 (all
5059 (if (y-or-n-p "Archive all done items in this category? ")
5060 (save-excursion
5061 (save-restriction
5062 (goto-char (point-min))
5063 (widen)
5064 (setq beg (progn
5065 (re-search-forward todos-done-string-start nil t)
5066 (match-beginning 0))
5067 end (if (re-search-forward
5068 (concat "^" (regexp-quote todos-category-beg))
5069 nil t)
5070 (match-beginning 0)
5071 (point-max))
5072 all-done (buffer-substring-no-properties beg end)
5073 count (todos-get-count 'done))))
5074 (throw 'end nil))))
5075 (when (or marked all item)
5076 (with-current-buffer archive
5077 (unless buffer-file-name (erase-buffer))
5078 (let (buffer-read-only)
5079 (widen)
5080 (goto-char (point-min))
5081 (if (and (re-search-forward (concat "^"
5082 (regexp-quote
5083 (concat todos-category-beg
5084 cat)))
5085 nil t)
5086 (re-search-forward (regexp-quote todos-category-done)
5087 nil t))
5088 ;; Start of done items section in existing category.
5089 (forward-char)
5090 (todos-add-category cat)
5091 ;; Start of done items section in new category.
5092 (goto-char (point-max)))
5093 (insert (cond (marked marked-items)
5094 (all all-done)
5095 (item)))
5096 (todos-update-count 'done (if (or marked all) count 1) cat)
5097 (todos-update-categories-sexp)
5098 ;; If archive is new, save to file now (using write-region in
5099 ;; order not to get prompted for file to save to), to let
5100 ;; auto-mode-alist take effect below.
5101 (unless buffer-file-name
5102 (write-region nil nil afile)
5103 (kill-buffer))))
5104 (with-current-buffer tbuf
5105 (cond ((or marked item)
5106 (and marked (goto-char (point-min)))
5107 (catch 'done
5108 (while (not (eobp))
5109 (if (or (and marked (todos-marked-item-p)) item)
5110 (progn
5111 (todos-remove-item)
5112 (todos-update-count 'done -1)
5113 (todos-update-count 'archived 1)
5114 ;; Don't leave point below last item.
5115 (and item (bolp) (eolp) (< (point-min) (point-max))
5116 (todos-backward-item))
5117 (when item
5118 (throw 'done (setq item nil))))
5119 (todos-forward-item)))))
5120 (all
5121 (remove-overlays beg end)
5122 (delete-region beg end)
5123 (todos-update-count 'done (- count))
5124 (todos-update-count 'archived count)))
5125 (when marked
5126 (remove-overlays (point-min) (point-max)
5127 'before-string todos-item-mark)
5128 (setq todos-categories-with-marks
5129 (assq-delete-all cat todos-categories-with-marks)))
5130 (todos-update-categories-sexp)
5131 (todos-prefix-overlays)))
5132 (find-file afile)
5133 (todos-category-number cat)
5134 (todos-category-select)
5135 (split-window-below)
5136 (set-window-buffer (selected-window) tbuf))))))
5137
5138 (defun todos-archive-category-done-items ()
5139 "Move all done items in this category to its archive."
5140 (interactive)
5141 (todos-archive-done-item t))
5142
5143 (defun todos-unarchive-items (&optional all)
5144 "Unarchive at least one item in this archive category.
5145
5146 If there are marked items, unarchive all of these; otherwise,
5147 with non-nil argument ALL, unarchive all items in this category;
5148 otherwise, unarchive the item at point.
5149
5150 Unarchived items are restored as done items to the corresponding
5151 category in the Todos file, inserted at the end of done section.
5152 If all items in the archive category were restored, the category
5153 is deleted from the archive. If this was the only category in the
5154 archive, the archive file is deleted."
5155 (interactive)
5156 (when (eq major-mode 'todos-archive-mode)
5157 (catch 'end
5158 (let* ((cat (todos-current-category))
5159 (tbuf (find-file-noselect
5160 (concat (file-name-sans-extension todos-current-todos-file)
5161 ".todo") t))
5162 (marked (assoc cat todos-categories-with-marks))
5163 (item (concat (todos-item-string) "\n"))
5164 (all-items (when all (buffer-substring-no-properties
5165 (point-min) (point-max))))
5166 (all-count (when all (todos-get-count 'done)))
5167 marked-items marked-count
5168 buffer-read-only)
5169 (when marked
5170 (save-excursion
5171 (goto-char (point-min))
5172 (while (not (eobp))
5173 (when (todos-marked-item-p)
5174 (concat marked-items (todos-item-string) "\n")
5175 (setq marked-count (1+ marked-count)))
5176 (todos-forward-item))))
5177 ;; Restore items to end of category's done section and update counts.
5178 (with-current-buffer tbuf
5179 (let (buffer-read-only)
5180 (widen)
5181 (goto-char (point-min))
5182 (re-search-forward (concat "^" (regexp-quote
5183 (concat todos-category-beg cat)))
5184 nil t)
5185 ;; Go to end of category's done section.
5186 (if (re-search-forward (concat "^" (regexp-quote todos-category-beg))
5187 nil t)
5188 (goto-char (match-beginning 0))
5189 (goto-char (point-max)))
5190 (cond (marked
5191 (insert marked-items)
5192 (todos-update-count 'done marked-count cat)
5193 (todos-update-count 'archived (- marked-count) cat))
5194 (all
5195 (insert all-items)
5196 (todos-update-count 'done all-count cat)
5197 (todos-update-count 'archived (- all-count) cat))
5198 (t
5199 (insert item)
5200 (todos-update-count 'done 1 cat)
5201 (todos-update-count 'archived -1 cat)))
5202 (todos-update-categories-sexp)))
5203 ;; Delete restored items from archive.
5204 (cond ((or marked item)
5205 (and marked (goto-char (point-min)))
5206 (catch 'done
5207 (while (not (eobp))
5208 (if (or (and marked (todos-marked-item-p)) item)
5209 (progn
5210 (todos-remove-item)
5211 ;; Don't leave point below last item.
5212 (and item (bolp) (eolp) (< (point-min) (point-max))
5213 (todos-backward-item))
5214 (when item
5215 (throw 'done (setq item nil))))
5216 (todos-forward-item))))
5217 (todos-update-count 'done (if marked (- marked-count) -1) cat))
5218 (all
5219 (remove-overlays (point-min) (point-max))
5220 (delete-region (point-min) (point-max))))
5221 ;; If that was the last category in the archive, delete the whole file.
5222 (if (= (length todos-categories) 1)
5223 (progn
5224 (delete-file todos-current-todos-file)
5225 ;; Don't bother confirming killing the archive buffer.
5226 (set-buffer-modified-p nil)
5227 (kill-buffer))
5228 ;; Otherwise, if the archive category is now empty, delete it.
5229 (when (eq (point-min) (point-max))
5230 (widen)
5231 (let ((beg (re-search-backward
5232 (concat "^" (regexp-quote todos-category-beg) cat)
5233 nil t))
5234 (end (if (re-search-forward
5235 (concat "^" (regexp-quote todos-category-beg))
5236 nil t 2)
5237 (match-beginning 0)
5238 (point-max))))
5239 (remove-overlays beg end)
5240 (delete-region beg end)
5241 (setq todos-categories (delete (assoc cat todos-categories)
5242 todos-categories))
5243 (todos-update-categories-sexp))))
5244 ;; Visit category in Todos file and show restored done items.
5245 (let ((tfile (buffer-file-name tbuf))
5246 (todos-show-with-done t))
5247 (set-window-buffer (selected-window)
5248 (set-buffer (find-file-noselect tfile)))
5249 (todos-category-number cat)
5250 (todos-show)
5251 (message "Items unarchived."))))))
5252
5253 (defun todos-unarchive-category ()
5254 "Unarchive all items in this category. See `todos-unarchive-items'."
5255 (interactive)
5256 (todos-unarchive-items t))
5257
5258 (provide 'todos)
5259
5260 ;;; todos.el ends here
5261
5262 ;; FIXME: remove when part of Emacs
5263 ;; ---------------------------------------------------------------------------
5264 (add-to-list 'auto-mode-alist '("\\.todo\\'" . todos-mode))
5265 (add-to-list 'auto-mode-alist '("\\.toda\\'" . todos-archive-mode))
5266
5267 ;;; Addition to calendar.el
5268 ;; FIXME: autoload when key-binding is defined in calendar.el
5269 (defun todos-insert-item-from-calendar (&optional arg)
5270 ""
5271 (interactive "P")
5272 (setq todos-date-from-calendar
5273 (calendar-date-string (calendar-cursor-to-date t) t t))
5274 (calendar-exit)
5275 (todos-show)
5276 (todos-insert-item arg nil nil todos-date-from-calendar))
5277
5278 (define-key calendar-mode-map "it" 'todos-insert-item-from-calendar)
5279
5280 ;;; necessitated adaptations to diary-lib.el
5281
5282 ;; (defun diary-goto-entry (button)
5283 ;; "Jump to the diary entry for the BUTTON at point."
5284 ;; (let* ((locator (button-get button 'locator))
5285 ;; (marker (car locator))
5286 ;; markbuf file opoint)
5287 ;; ;; If marker pointing to diary location is valid, use that.
5288 ;; (if (and marker (setq markbuf (marker-buffer marker)))
5289 ;; (progn
5290 ;; (pop-to-buffer markbuf)
5291 ;; (goto-char (marker-position marker)))
5292 ;; ;; Marker is invalid (eg buffer has been killed, as is the case with
5293 ;; ;; included diary files).
5294 ;; (or (and (setq file (cadr locator))
5295 ;; (file-exists-p file)
5296 ;; (find-file-other-window file)
5297 ;; (progn
5298 ;; (when (eq major-mode (default-value 'major-mode)) (diary-mode))
5299 ;; (when (eq major-mode 'todos-mode) (widen))
5300 ;; (goto-char (point-min))
5301 ;; (when (re-search-forward (format "%s.*\\(%s\\)"
5302 ;; (regexp-quote (nth 2 locator))
5303 ;; (regexp-quote (nth 3 locator)))
5304 ;; nil t)
5305 ;; (goto-char (match-beginning 1))
5306 ;; (when (eq major-mode 'todos-mode)
5307 ;; (setq opoint (point))
5308 ;; (re-search-backward (concat "^"
5309 ;; (regexp-quote todos-category-beg)
5310 ;; "\\(.*\\)\n")
5311 ;; nil t)
5312 ;; (todos-category-number (match-string 1))
5313 ;; (todos-category-select)
5314 ;; (goto-char opoint)))))
5315 ;; (message "Unable to locate this diary entry")))))