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