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