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