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