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