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