* calendar/todos.el Add and revise various doc strings, remove
[bpt/emacs.git] / lisp / calendar / todos.el
CommitLineData
ee7412e4 1;;; Todos.el --- major mode for displaying and editing Todo lists
3f031767
SB
2
3;; Copyright (C) 1997, 1999, 2001, 2002, 2003, 2004, 2005, 2006, 2007,
4;; 2008, 2009 Free Software Foundation, Inc.
5
6;; Author: Oliver Seidel <privat@os10000.net>
7;; Maintainer: Stephen Berman <stephen.berman@gmx.net>
8;; Created: 2 Aug 1997
9;; Keywords: calendar, todo
10
11;; This file is part of GNU Emacs.
12
13;; GNU Emacs is free software: you can redistribute it and/or modify
14;; it under the terms of the GNU General Public License as published by
15;; the Free Software Foundation, either version 3 of the License, or
16;; (at your option) any later version.
17
18;; GNU Emacs is distributed in the hope that it will be useful,
19;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21;; GNU General Public License for more details.
22
23;; You should have received a copy of the GNU General Public License
24;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
25
26;; ---------------------------------------------------------------------------
27
28;;; Commentary:
29
30;; Mode Description
31;;
32;; TODO is a major mode for EMACS which offers functionality to
33;; treat most lines in one buffer as a list of items one has to
34;; do. There are facilities to add new items, which are
35;; categorised, to edit or even delete items from the buffer.
36;; The buffer contents are currently compatible with the diary,
37;; so that the list of todos-items will show up in the FANCY diary
38;; mode.
39;;
40;; Notice: Besides the major mode, this file also exports the
41;; function `todos-show' which will change to the one specific
42;; TODO file that has been specified in the todos-file-do
43;; variable. If this file does not conform to the TODO mode
44;; conventions, the todos-show function will add the appropriate
45;; header and footer. I don't anticipate this to cause much
46;; grief, but be warned, in case you attempt to read a plain text
47;; file.
48;;
49;; Preface, Quickstart Installation
50;;
ee7412e4 51;; To get this to work, make Emacs execute the line
3f031767
SB
52;;
53;; (autoload 'todos "todos"
54;; "Major mode for editing TODO lists." t)
55;; (autoload 'todos-show "todos"
56;; "Show TODO items." t)
57;; (autoload 'todos-insert-item "todos"
58;; "Add TODO item." t)
59;;
60;; You may now enter new items by typing "M-x todos-insert-item",
61;; or enter your TODO list file by typing "M-x todos-show".
62;;
63;; The TODO list file has a special format and some auxiliary
64;; information, which will be added by the todos-show function if
65;; it attempts to visit an un-initialised file. Hence it is
66;; recommended to use the todos-show function for the first time,
67;; in order to initialise the file, but it is not necessary
68;; afterwards.
69;;
70;; As these commands are quite long to type, I would recommend
71;; the addition of two bindings to your to your global keymap. I
72;; personally have the following in my initialisation file:
73;;
74;; (global-set-key "\C-ct" 'todos-show) ; switch to TODO buffer
75;; (global-set-key "\C-ci" 'todos-insert-item) ; insert new item
76;;
77;; Note, however, that this recommendation has prompted some
78;; criticism, since the keys C-c LETTER are reserved for user
79;; functions. I believe my recommendation is acceptable, since
80;; the Emacs Lisp Manual *Tips* section also details that the
81;; mode itself should not bind any functions to those keys. The
82;; express aim of the above two bindings is to work outside the
83;; mode, which doesn't need the show function and offers a
84;; different binding for the insert function. They serve as
85;; shortcuts and are not even needed (since the TODO mode will be
86;; entered by visiting the TODO file, and later by switching to
87;; its buffer).
88;;
89;; If you are an advanced user of this package, please consult
90;; the whole source code for autoloads, because there are several
91;; extensions that are not explicitly listed in the above quick
92;; installation.
93;;
94;; Pre-Requisites
95;;
96;; This package will require the following packages to be
97;; available on the load-path:
98;;
99;; time-stamp
100;; easymenu
101;;
102;; Operation
103;;
104;; You will have the following facilities available:
105;;
106;; M-x todos-show will enter the todo list screen, here type
107;;
108;; + to go to next category
109;; - to go to previous category
110;; d to file the current entry, including a
111;; comment and timestamp
112;; e to edit the current entry
113;; E to edit a multi-line entry
114;; f to file the current entry, including a
115;; comment and timestamp
116;; i to insert a new entry, with prefix, omit category
117;; I to insert a new entry at current cursor position
118;; j jump to category
119;; k to kill the current entry
120;; l to lower the current entry's priority
121;; n for the next entry
122;; p for the previous entry
123;; P print
124;; q to save the list and exit the buffer
125;; r to raise the current entry's priority
126;; s to save the list
127;; S to save the list of top priorities
128;; t show top priority items for each category
129;;
130;; When you add a new entry, you are asked for the text and then
131;; for the category. I for example have categories for things
132;; that I want to do in the office (like mail my mum), that I
133;; want to do in town (like buy cornflakes) and things I want to
134;; do at home (move my suitcases). The categories can be
135;; selected with the cursor keys and if you type in the name of a
136;; category which didn't exist before, an empty category of the
137;; desired name will be added and filled with the new entry.
138;;
139;; Configuration
140;;
141;; Variable todos-prefix
142;;
143;; I would like to recommend that you use the prefix "*/*" (by
144;; leaving the variable 'todos-prefix' untouched) so that the
145;; diary displays each entry every day.
146;;
147;; To understand what I mean, please read the documentation that
148;; goes with the calendar since that will tell you how you can
149;; set up the fancy diary display and use the #include command to
150;; include your todo list file as part of your diary.
151;;
152;; If you have the diary package set up to usually display more
153;; than one day's entries at once, consider using
154;;
155;; "&%%(equal (calendar-current-date) date)"
156;;
157;; as the value of `todos-prefix'. Please note that this may slow
158;; down the processing of your diary file some.
159;;
160;; Carsten Dominik <dominik@strw.LeidenUniv.nl> suggested that
161;;
162;; "&%%(todos-cp)"
163;;
164;; might be nicer and to that effect a function has been declared
165;; further down in the code. You may wish to auto-load this.
166;;
167;; Carsten also writes that that *changing* the prefix after the
168;; todo list is already established is not as simple as changing
169;; the variable - the todo files have to be changed by hand.
170;;
171;; Variable todos-file-do
172;;
173;; This variable is fairly self-explanatory. You have to store
174;; your TODO list somewhere. This variable tells the package
175;; where to go and find this file.
176;;
177;; Variable todos-file-done
178;;
179;; Even when you're done, you may wish to retain the entries.
180;; Given that they're timestamped and you are offered to add a
181;; comment, this can make a useful diary of past events. It will
182;; even blend in with the EMACS diary package. So anyway, this
183;; variable holds the name of the file for the filed todos-items.
184;;
185;; Variable todos-file-top
186;;
187;; File storing the top priorities of your TODO list when
188;; todos-save-top-priorities is non-nil. Nice to include in your
189;; diary instead of the complete TODO list.
190;;
191;; Variable todos-mode-hook
192;;
193;; Just like other modes, too, this mode offers to call your
194;; functions before it goes about its business. This variable
195;; will be inspected for any functions you may wish to have
196;; called once the other TODO mode preparations have been
197;; completed.
198;;
199;; Variable todos-insert-threshold
200;;
201;; Another nifty feature is the insertion accuracy. If you have
202;; 8 items in your TODO list, then you may get asked 4 questions
203;; by the binary insertion algorithm. However, you may not
204;; really have a need for such accurate priorities amongst your
205;; TODO items. If you now think about the binary insertion
206;; halving the size of the window each time, then the threshold
207;; is the window size at which it will stop. If you set the
208;; threshold to zero, the upper and lower bound will coincide at
209;; the end of the loop and you will insert your item just before
210;; that point. If you set the threshold to, e.g. 8, it will stop
211;; as soon as the window size drops below that amount and will
212;; insert the item in the approximate center of that window. I
213;; got the idea for this feature after reading a very helpful
214;; e-mail reply from Trey Jackson <trey@cs.berkeley.edu> who
215;; corrected some of my awful coding and pointed me towards some
216;; good reading. Thanks Trey!
217;;
218;; Things to do
219;;
220;; These originally were my ideas, but now also include all the
221;; suggestions that I included before forgetting them:
222;;
223;; o Fancy fonts for todo/top-priority buffer
224;; o Remove todos-prefix option in todos-top-priorities
225;; o Rename category
226;; o Move entry from one category to another one
227;; o Entries which both have the generic */* prefix and a
228;; "deadline" entry which are understood by diary, indicating
229;; an event (unless marked by &)
230;; o The optional COUNT variable of todos-forward-item should be
231;; applied to the other functions performing similar tasks
232;; o Modularization could be done for repeated elements of
233;; the code, like the completing-read lines of code.
234;; o license / version function
235;; o export to diary file
236;; o todos-report-bug
237;; o GNATS support
238;; o elide multiline (as in bbdb, or, to a lesser degree, in
239;; outline mode)
ee7412e4 240;; o rewrite complete package to store data as Lisp objects
3f031767
SB
241;; and have display modes for display, for diary export,
242;; etc. (Richard Stallman pointed out this is a bad idea)
243;; o so base todos.el on generic-mode.el instead
244;;
245;; History and Gossip
246;;
247;; Many thanks to all the ones who have contributed to the
248;; evolution of this package! I hope I have listed all of you
249;; somewhere in the documentation or at least in the RCS history!
250;;
251;; Enjoy this package and express your gratitude by sending nice
252;; things to my parents' address!
253;;
254;; Oliver Seidel
255;; (Lessingstr. 8, 65760 Eschborn, Federal Republic of Germany)
256
257;;; Code:
258
b28025ed 259;; (require 'time-stamp)
2c173503 260;; (require 'calendar) ; required by diary-lib
b28025ed 261(require 'diary-lib)
3f031767 262
2c173503 263;; ---------------------------------------------------------------------------
2c173503 264;;; Customizable options
ee7412e4 265
3f031767 266(defgroup todos nil
d04d6b95 267 "Maintain categorized lists of todo items."
3f031767 268 :link '(emacs-commentary-link "todos")
d04d6b95 269 :version "24.1"
3f031767
SB
270 :group 'calendar)
271
d04d6b95
SB
272;; FIXME: need this?
273(defcustom todos-initial-category "Todo"
274 "Default category name offered on initializing a new Todos file."
275 :type 'string
276 :group 'todos)
277
278(defcustom todos-display-categories-first nil
279 "Non-nil to display category list on first visit to a Todos file."
280 :type 'boolean
281 :group 'todos)
282
283(defcustom todos-prefix ""
b28025ed
SB
284 "String prefixed to todo items for visual distinction."
285 :type 'string
286 :initialize 'custom-initialize-default
287 :set 'todos-reset-prefix
288 :group 'todos)
2c173503
SB
289
290(defcustom todos-number-prefix t
d04d6b95
SB
291 "Non-nil to show item prefixes as consecutively increasing integers.
292These reflect the priorities of the items in each category."
2c173503
SB
293 :type 'boolean
294 :initialize 'custom-initialize-default
295 :set 'todos-reset-prefix
296 :group 'todos)
297
d04d6b95
SB
298;; FIXME: Update when window-width changes (add todos-reset-separator to
299;; window-configuration-change-hook in todos-mode?)
300(defcustom todos-done-separator (make-string (window-width) ?-)
2c173503
SB
301 "String used to visual separate done from not done items.
302Displayed in a before-string overlay by `todos-toggle-view-done-items'."
303 :type 'string
304 :initialize 'custom-initialize-default
305 :set 'todos-reset-separator
306 :group 'todos)
307
308(defcustom todos-done-string "DONE "
309 "Identifying string appended to the front of done todos items."
310 :type 'string
311 ;; :initialize 'custom-initialize-default
d04d6b95 312 ;; :set 'todos-reset-done-string
2c173503
SB
313 :group 'todos)
314
315(defcustom todos-show-with-done nil
316 "Non-nil to display done items in all categories."
317 :type 'boolean
318 :group 'todos)
319
d04d6b95
SB
320(defcustom todos-files-directory (locate-user-emacs-file "todos/")
321 "Directory where user's Todos files are saved."
322 :type 'directory
3f031767 323 :group 'todos)
2c173503 324
d04d6b95
SB
325(defun todos-files (&optional archives)
326 "Default value of `todos-files-function'.
327This returns the case-insensitive alphabetically sorted list of
328files in `todos-files-directory' with the extension \".todo\".
329With non-nil ARCHIVES return the list of archive files."
330 (sort (directory-files todos-files-directory t
331 (if archives "\.toda$" "\.todo$") t)
332 (lambda (s1 s2) (let ((cis1 (upcase s1))
333 (cis2 (upcase s2)))
334 (string< cis1 cis2)))))
335
336(defcustom todos-files-function 'todos-files
337 "Function returning the value of the variable `todos-files'.
338If this function is called with an optional non-nil argument,
339then it returns the value of the variable `todos-archives'."
340 :type 'function
2c173503
SB
341 :group 'todos)
342
d04d6b95
SB
343(defcustom todos-merged-files nil
344 "List of files for `todos-merged-top-priorities'."
345 :type `(set ,@(mapcar (lambda (x) (list 'const x))
346 (funcall todos-files-function)))
3f031767 347 :group 'todos)
2c173503 348
d04d6b95
SB
349(defcustom todos-prompt-merged-files nil
350 "Non-nil to prompt for merging files for `todos-top-priorities'."
351 :type 'boolean
352 :group 'todos)
353
354(defcustom todos-auto-switch-todos-file nil ;FIXME: t by default?
355 "Non-nil to make a Todos file current upon changing to it."
356 :type 'boolean
357 :initialize 'custom-initialize-default
358 :set 'todos-toggle-switch-todos-file-noninteractively
359 :group 'todos)
360
361(defcustom todos-default-todos-file (car (funcall todos-files-function))
362 "Todos file visited by first session invocation of `todos-show'.
363Normally this should be set by invoking `todos-change-default-file'
364either directly or as a side effect of `todos-add-file'."
365 :type `(radio ,@(mapcar (lambda (x) (list 'const x))
366 (funcall todos-files-function)))
3f031767 367 :group 'todos)
2c173503 368
d04d6b95
SB
369;; FIXME: make a defvar instead of a defcustom, and one for each member of todos-file
370(defcustom todos-file-top "~/todos.todt" ;FIXME
371 "TODO mode top priorities file."
372 :type 'file
3f031767 373 :group 'todos)
2c173503 374
d04d6b95 375(defcustom todos-categories-buffer "*Todos Categories*"
ee7412e4 376 "Name of buffer displayed by `todos-display-categories'."
2c173503
SB
377 :type 'string
378 :group 'todos)
379
d04d6b95
SB
380(defcustom todos-categories-category-label "Category"
381 "Category button label in `todos-categories-buffer'."
2c173503
SB
382 :type 'string
383 :group 'todos)
384
d04d6b95
SB
385(defcustom todos-categories-todo-label "Todo"
386 "Todo button label in `todos-categories-buffer'."
2c173503 387 :type 'string
3f031767 388 :group 'todos)
2c173503 389
d04d6b95
SB
390(defcustom todos-categories-diary-label "Diary"
391 "Diary button label in `todos-categories-buffer'."
392 :type 'string
393 :group 'todos)
3f031767 394
d04d6b95
SB
395(defcustom todos-categories-done-label "Done"
396 "Done button label in `todos-categories-buffer'."
3f031767
SB
397 :type 'string
398 :group 'todos)
399
d04d6b95
SB
400(defcustom todos-categories-archived-label "Archived"
401 "Archived button label in `todos-categories-buffer'."
402 :type 'string
403 :group 'todos)
404
405(defcustom todos-categories-number-separator " | "
406 "String between number and category in `todos-categories-mode'.
407This separates the number from the category name in the default
408categories display according to priority."
409 :type 'string
410 :group 'todos)
411
412(defcustom todos-categories-align 'center
413 ""
414 :type '(radio (const left) (const center) (const right))
415 :group 'todos)
416
417;; FIXME: set for each Todos file?
418(defcustom todos-ignore-archived-categories nil
419 "Non-nil to ignore categories with only archived items.
420When non-nil such categories are omitted from `todos-categories'
421and hence from commands that use this variable. An exception is
422\\[todos-display-categories], which displays all categories; but
423those with only archived items are shown in `todos-archived-only'
424face and clicking them in Todos Categories mode visits the
425archived categories."
2c173503 426 :type 'boolean
d04d6b95
SB
427 :initialize 'custom-initialize-default
428 :set 'todos-reset-categories
2c173503
SB
429 :group 'todos)
430
d04d6b95
SB
431(defcustom todos-archived-categories-buffer "*Todos Archived Categories*"
432 "Name of buffer displayed by `todos-display-categories'."
2c173503 433 :type 'string
d04d6b95 434 :group 'todos)
2c173503 435
d04d6b95
SB
436(defcustom todos-edit-buffer "*Todos Edit*"
437 "TODO Edit buffer name."
2c173503 438 :type 'string
d04d6b95
SB
439 :group 'todos)
440
441(defcustom todos-include-in-diary nil
442 "Non-nil to allow new Todo items to be included in the diary."
443 :type 'boolean
444 :group 'todos)
445
446(defcustom todos-nondiary-marker '("[" "]")
447 "List of strings surrounding item date to block diary inclusion.
448The first string is inserted before the item date and must be a
449non-empty string that does not match a diary date in order to
450have its intended effect. The second string is inserted after
451the diary date."
452 :type '(list string string)
2c173503 453 :group 'todos
d04d6b95
SB
454 :initialize 'custom-initialize-default
455 :set 'todos-reset-nondiary-marker)
2c173503 456
3f031767
SB
457(defcustom todos-print-function 'ps-print-buffer-with-faces
458 "Function to print the current buffer."
459 :type 'symbol
460 :group 'todos)
2c173503 461
3f031767
SB
462(defcustom todos-show-priorities 1
463 "Default number of priorities to show by \\[todos-top-priorities].
4640 means show all entries."
465 :type 'integer
466 :group 'todos)
2c173503 467
3f031767
SB
468(defcustom todos-print-priorities 0
469 "Default number of priorities to print by \\[todos-print].
4700 means print all entries."
471 :type 'integer
472 :group 'todos)
2c173503 473
3f031767
SB
474(defcustom todos-save-top-priorities-too t
475 "Non-nil makes `todos-save' automatically save top-priorities in `todos-file-top'."
476 :type 'boolean
477 :group 'todos)
2c173503 478
db2c5d34 479(defcustom todos-completion-ignore-case t ;; FIXME: nil for release
ee7412e4 480 "Non-nil means don't consider case significant in `todos-read-category'."
db2c5d34
SB
481 :type 'boolean
482 :group 'todos)
3f031767 483
d04d6b95
SB
484(defcustom todos-always-add-time-string nil
485 "Non-nil adds current time to a new item's date header by default.
486When the Todos insertion commands have a non-nil \"maybe-notime\"
487argument, this reverses the effect of
488`todos-always-add-time-string': if t, these commands omit the
489current time, if nil, they include it."
b28025ed 490 :type 'boolean
3f031767
SB
491 :group 'todos)
492
2c173503 493(defcustom todos-wrap-lines t
ee7412e4 494 ""
2c173503
SB
495 :group 'todos
496 :type 'boolean)
497
498(defcustom todos-line-wrapping-function 'todos-wrap-and-indent
ee7412e4 499 ""
2c173503
SB
500 :group 'todos
501 :type 'function)
502
ee7412e4
SB
503(defcustom todos-indent-to-here 6
504 ""
505 :type 'integer
506 :group 'todos)
3f031767 507
ee7412e4 508;; ---------------------------------------------------------------------------
2c173503 509;;; Faces
ee7412e4 510
d04d6b95
SB
511(defgroup todos-faces nil
512 "Faces for the Todos modes."
513 :version "24.1"
514 :group 'todos)
515
db2c5d34
SB
516(defface todos-prefix-string
517 '((t
b28025ed 518 :inherit font-lock-constant-face
db2c5d34
SB
519 ))
520 "Face for Todos prefix string."
d04d6b95 521 :group 'todos-faces)
db2c5d34 522
ee7412e4
SB
523(defface todos-button
524 '((t
d04d6b95 525 :inherit widget-field
ee7412e4
SB
526 ))
527 "Face for buttons in todos-display-categories."
d04d6b95
SB
528 :group 'todos-faces)
529
530(defface todos-sorted-column
531 '((t
532 :inherit fringe
533 ))
534 "Face for buttons in todos-display-categories."
535 :group 'todos-faces)
536
537(defface todos-archived-only
538 '((t
539 (:inherit (shadow))
540 ))
541 "Face for archived-only categories in todos-display-categories."
542 :group 'todos-faces)
543
544(defface todos-search
545 '((t
546 :inherit match
547 ))
548 "Face for matches found by todos-search."
549 :group 'todos-faces)
ee7412e4 550
b28025ed 551(defface todos-date
db2c5d34 552 '((t
b28025ed 553 :inherit diary
db2c5d34 554 ))
b28025ed 555 "Face for Todos prefix string."
d04d6b95 556 :group 'todos-faces)
b28025ed
SB
557(defvar todos-date-face 'todos-date)
558
559(defface todos-time
560 '((t
561 :inherit diary-time
562 ))
563 "Face for Todos prefix string."
d04d6b95 564 :group 'todos-faces)
b28025ed
SB
565(defvar todos-time-face 'todos-time)
566
2c173503
SB
567(defface todos-done
568 '((t
569 :inherit font-lock-comment-face
570 ))
571 "Face for done Todos item header string."
d04d6b95 572 :group 'todos-faces)
2c173503 573(defvar todos-done-face 'todos-done)
b28025ed 574
2c173503
SB
575(defface todos-done-sep
576 '((t
577 :inherit font-lock-type-face
578 ))
579 "Face for separator string bewteen done and not done Todos items."
d04d6b95 580 :group 'todos-faces)
2c173503 581(defvar todos-done-sep-face 'todos-done-sep)
db2c5d34
SB
582
583(defvar todos-font-lock-keywords
584 (list
b28025ed 585 '(todos-date-string-match 1 todos-date-face t)
2c173503
SB
586 '(todos-time-string-match 1 todos-time-face t)
587 '(todos-done-string-match 0 todos-done-face t)
d04d6b95 588 '(todos-category-string-match 1 todos-done-sep-face t))
db2c5d34
SB
589 "Font-locking for Todos mode.")
590
3f031767 591;; ---------------------------------------------------------------------------
d04d6b95 592;;; Modes setup
3f031767 593
d04d6b95
SB
594(defvar todos-files (funcall todos-files-function)
595 "List of user's Todos files.")
596
597(defvar todos-archives (funcall todos-files-function t)
598 "List of user's Todos archives.")
f730d273 599
3f031767 600(defvar todos-categories nil
d04d6b95
SB
601 "List of categories in the current Todos file.
602The elements are lists whose car is a category name and whose cdr
603is the category's property list.")
604
605(defvar todos-insertion-map
606 (let ((map (make-keymap)))
607 (define-key map "i" 'todos-insert-item)
608 (define-key map "h" 'todos-insert-item-here)
609 (define-key map "dd" 'todos-insert-item-ask-date)
610 (define-key map "dtt" 'todos-insert-item-ask-date-time)
611 (define-key map "dtyy" 'todos-insert-item-ask-date-time-for-diary)
612 (define-key map "dtyh" 'todos-insert-item-ask-date-time-for-diary-here)
613 (define-key map "dth" 'todos-insert-item-ask-date-time-here)
614 (define-key map "dmm" 'todos-insert-item-ask-date-maybe-notime)
615 (define-key map "dmyy" 'todos-insert-item-ask-date-maybe-notime-for-diary)
616 (define-key map "dmyh" 'todos-insert-item-ask-date-maybe-notime-for-diary-here)
617 (define-key map "dmh" 'todos-insert-item-ask-date-maybe-notime-here)
618 (define-key map "dyy" 'todos-insert-item-ask-date-for-diary)
619 (define-key map "dyh" 'todos-insert-item-ask-date-for-diary-here)
620 (define-key map "dh" 'todos-insert-item-ask-date-here)
621 (define-key map "nn" 'todos-insert-item-ask-dayname)
622 (define-key map "ntt" 'todos-insert-item-ask-dayname-time)
623 (define-key map "ntyy" 'todos-insert-item-ask-dayname-time-for-diary)
624 (define-key map "ntyh" 'todos-insert-item-ask-dayname-time-for-diary-here)
625 (define-key map "nth" 'todos-insert-item-ask-dayname-time-here)
626 (define-key map "nmm" 'todos-insert-item-ask-dayname-maybe-notime)
627 (define-key map "nmyy" 'todos-insert-item-ask-dayname-maybe-notime-for-diary)
628 (define-key map "nmyh" 'todos-insert-item-ask-dayname-maybe-notime-for-diary-here)
629 (define-key map "nmh" 'todos-insert-item-ask-dayname-maybe-notime-here)
630 (define-key map "nyy" 'todos-insert-item-ask-dayname-for-diary)
631 (define-key map "nyh" 'todos-insert-item-ask-dayname-for-diary-here)
632 (define-key map "nh" 'todos-insert-item-ask-dayname-here)
633 (define-key map "tt" 'todos-insert-item-ask-time)
634 (define-key map "tyy" 'todos-insert-item-ask-time-for-diary)
635 (define-key map "tyh" 'todos-insert-item-ask-time-for-diary-here)
636 (define-key map "th" 'todos-insert-item-ask-time-here)
637 (define-key map "mm" 'todos-insert-item-maybe-notime)
638 (define-key map "myy" 'todos-insert-item-maybe-notime-for-diary)
639 (define-key map "myh" 'todos-insert-item-maybe-notime-for-diary-here)
640 (define-key map "mh" 'todos-insert-item-maybe-notime-here)
641 (define-key map "yy" 'todos-insert-item-for-diary)
642 (define-key map "yh" 'todos-insert-item-for-diary-here)
643 map)
644 "Keymap for Todos mode insertion commands.")
3f031767 645
3f031767
SB
646(defvar todos-mode-map
647 (let ((map (make-keymap)))
648 (suppress-keymap map t)
2c173503 649 ;; navigation commands
d04d6b95
SB
650 (define-key map "f" 'todos-forward-category)
651 (define-key map "b" 'todos-backward-category)
2c173503 652 (define-key map "j" 'todos-jump-to-category)
d04d6b95 653 (define-key map "J" 'todos-jump-to-category-other-file)
2c173503
SB
654 (define-key map "n" 'todos-forward-item)
655 (define-key map "p" 'todos-backward-item)
656 (define-key map "S" 'todos-search)
d04d6b95 657 (define-key map "X" 'todos-clear-matches)
2c173503 658 ;; display commands
d04d6b95 659 (define-key map "Cd" 'todos-display-categories) ;FIXME: Cs todos-show-categories?
f730d273 660 ;; (define-key map "" 'todos-display-categories-alphabetically)
d04d6b95 661 (define-key map "H" 'todos-highlight-item)
2c173503
SB
662 (define-key map "N" 'todos-toggle-item-numbering)
663 ;; (define-key map "" 'todos-toggle-display-date-time)
664 (define-key map "P" 'todos-print)
2c173503 665 (define-key map "v" 'todos-toggle-view-done-items)
d04d6b95
SB
666 (define-key map "V" 'todos-toggle-show-done-only)
667 (define-key map "Av" 'todos-view-archived-items)
668 (define-key map "As" 'todos-switch-to-archive)
669 (define-key map "Ac" 'todos-choose-archive)
2c173503 670 (define-key map "Y" 'todos-diary-items)
2c173503 671 (define-key map "t" 'todos-top-priorities)
d04d6b95
SB
672 (define-key map "T" 'todos-merged-top-priorities)
673 ;; (define-key map "" 'todos-save-top-priorities)
2c173503 674 ;; editing commands
d04d6b95
SB
675 (define-key map "Fa" 'todos-add-file)
676 (define-key map "Ca" 'todos-add-category)
677 (define-key map "Cr" 'todos-rename-category)
678 (define-key map "Cm" 'todos-move-category)
679 (define-key map "Ck" 'todos-delete-category)
2c173503 680 (define-key map "d" 'todos-item-done)
d04d6b95
SB
681 (define-key map "ee" 'todos-edit-item)
682 (define-key map "em" 'todos-edit-multiline)
683 (define-key map "eh" 'todos-edit-item-header)
684 (define-key map "ed" 'todos-edit-item-date)
685 (define-key map "et" 'todos-edit-item-time)
686 (define-key map "i" todos-insertion-map)
3f031767 687 (define-key map "k" 'todos-delete-item)
db2c5d34 688 (define-key map "m" 'todos-move-item)
d04d6b95
SB
689 (define-key map "M" 'todos-move-item-to-file)
690 (define-key map "-" 'todos-raise-item-priority)
691 (define-key map "+" 'todos-lower-item-priority)
692 (define-key map "#" 'todos-set-item-priority)
2c173503 693 (define-key map "u" 'todos-item-undo)
d04d6b95 694 (define-key map "Ad" 'todos-archive-done-items)
2c173503
SB
695 (define-key map "y" 'todos-toggle-item-diary-inclusion)
696 ;; (define-key map "" 'todos-toggle-diary-inclusion)
d04d6b95
SB
697 (define-key map "s" 'todos-save)
698 (define-key map "q" 'todos-quit)
ee7412e4 699 (define-key map [remap newline] 'newline-and-indent)
2c173503
SB
700 map)
701 "Todos mode keymap.")
702
703(defvar todos-archive-mode-map
704 (let ((map (make-sparse-keymap)))
705 (suppress-keymap map t)
706 ;; navigation commands
d04d6b95
SB
707 (define-key map "f" 'todos-forward-category)
708 (define-key map "b" 'todos-backward-category)
2c173503 709 (define-key map "j" 'todos-jump-to-category)
3f031767
SB
710 (define-key map "n" 'todos-forward-item)
711 (define-key map "p" 'todos-backward-item)
2c173503
SB
712 ;; display commands
713 (define-key map "C" 'todos-display-categories)
d04d6b95 714 (define-key map "H" 'todos-highlight-item)
2c173503
SB
715 (define-key map "N" 'todos-toggle-item-numbering)
716 ;; (define-key map "" 'todos-toggle-display-date-time)
3f031767
SB
717 (define-key map "P" 'todos-print)
718 (define-key map "q" 'todos-quit)
3f031767 719 (define-key map "s" 'todos-save)
2c173503 720 (define-key map "S" 'todos-search)
d04d6b95
SB
721 (define-key map "t" 'todos-show) ;FIXME: should show same category
722 (define-key map "u" 'todos-unarchive-category)
2c173503
SB
723 map)
724 "Todos Archive mode keymap.")
725
726(defvar todos-edit-mode-map
ee7412e4 727 (let ((map (make-sparse-keymap)))
d04d6b95 728 (define-key map "\C-x\C-q" 'todos-edit-quit)
ee7412e4 729 (define-key map [remap newline] 'newline-and-indent)
3f031767 730 map)
2c173503 731 "Todos Edit mode keymap.")
3f031767 732
ee7412e4
SB
733(defvar todos-categories-mode-map
734 (let ((map (make-sparse-keymap)))
735 (suppress-keymap map t)
736 (define-key map "a" 'todos-display-categories-alphabetically)
737 (define-key map "c" 'todos-display-categories)
d04d6b95
SB
738 (define-key map "+" 'todos-lower-category)
739 (define-key map "-" 'todos-raise-category)
740 (define-key map "n" 'forward-button)
741 (define-key map "p" 'backward-button)
742 (define-key map [tab] 'forward-button)
743 (define-key map [backtab] 'backward-button)
744 (define-key map "q" 'todos-quit)
ee7412e4
SB
745 ;; (define-key map "A" 'todos-add-category)
746 ;; (define-key map "D" 'todos-delete-category)
747 ;; (define-key map "R" 'todos-rename-category)
748 map)
749 "Todos Categories mode keymap.")
750
d04d6b95
SB
751(defvar todos-top-priorities-mode-map
752 (let ((map (make-keymap)))
753 (suppress-keymap map t)
754 ;; navigation commands
755 (define-key map "j" 'todos-jump-to-category)
756 (define-key map "n" 'todos-forward-item)
757 (define-key map "p" 'todos-backward-item)
758 ;; (define-key map "S" 'todos-search)
759 ;; display commands
760 (define-key map "C" 'todos-display-categories)
761 ;; (define-key map "" 'todos-display-categories-alphabetically)
762 (define-key map "H" 'todos-highlight-item)
763 (define-key map "N" 'todos-toggle-item-numbering)
764 ;; (define-key map "" 'todos-toggle-display-date-time)
765 (define-key map "P" 'todos-print)
766 (define-key map "q" 'todos-quit)
767 (define-key map "s" 'todos-save)
768 (define-key map "V" 'todos-view-archive)
769 (define-key map "v" 'todos-toggle-view-done-items)
770 (define-key map "Y" 'todos-diary-items)
771 ;; (define-key map "S" 'todos-save-top-priorities)
772 ;; editing commands
773 (define-key map "l" 'todos-lower-item-priority)
774 (define-key map "r" 'todos-raise-item-priority)
775 (define-key map "#" 'todos-set-item-priority)
776 map)
777 "Todos Top Priorities mode keymap.")
778
779(defvar todos-current-todos-file nil
780 "Variable holding the name of the currently active Todos file.
781Automatically set by `todos-switch-todos-file'.")
782
783(defvar todos-category-number 0
784 "Number.")
3f031767
SB
785
786(defvar todos-tmp-buffer-name " *todo tmp*")
787
2c173503 788(defvar todos-category-beg "--==-- "
d04d6b95
SB
789 "String marking beginning of category (inserted with its name).")
790
791(defvar todos-category-done "==--== DONE "
792 "String marking beginning of category's done items.")
793
794(defvar todos-nondiary-start (nth 0 todos-nondiary-marker)
795 "String inserted before item date to block diary inclusion.")
796
797(defvar todos-nondiary-end (nth 1 todos-nondiary-marker)
798 "String inserted after item date matching todos-nondiary-start.")
799
800(defvar todos-show-done-only nil
801 "If non-nil display only done items in current category.
802Set by `todos-toggle-show-done-only' and used by
803`todos-category-select'.")
804
805(easy-menu-define
806 todos-menu todos-mode-map "Todos Menu"
807 '("Todos"
808 ("Navigation"
809 ["Next Item" todos-forward-item t]
810 ["Previous Item" todos-backward-item t]
811 "---"
812 ["Next Category" todos-forward-category t]
813 ["Previous Category" todos-backward-category t]
814 ["Jump to Category" todos-jump-to-category t]
815 ["Jump to Category in Other File" todos-jump-to-category-other-file t]
816 "---"
817 ["Search Todos File" todos-search t]
818 ["Clear Highlighting on Search Matches" todos-category-done t])
819 ("Display"
820 ["List Current Categories" todos-display-categories t]
821 ["List Categories Alphabetically" todos-display-categories-alphabetically t]
822 ["Turn Item Highlighting on/off" todos-highlight-item t]
823 ["Turn Item Numbering on/off" todos-toggle-item-numbering t]
824 ["Turn Item Time Stamp on/off" todos-toggle-display-date-time t]
825 ["View/Hide Done Items" todos-toggle-view-done-items t]
826 "---"
827 ["View Diary Items" todos-diary-items t]
828 ["View Top Priority Items" todos-top-priorities t]
829 ["View Merged Top Priority Items" todos-merged-top-priorities t]
830 "---"
831 ["View Archive" todos-view-archive t]
832 ["Print Category" todos-print-category t])
833 ("Editing"
834 ["Insert New Item" todos-insert-item t]
835 ["Insert Item Here" todos-insert-item-here t]
836 ("More Insertion Commands")
837 ["Edit Item" todos-edit-item t]
838 ["Edit Multiline Item" todos-edit-multiline t]
839 ["Edit Item Header" todos-edit-item-header t]
840 ["Edit Item Date" todos-edit-item-date t]
841 ["Edit Item Time" todos-edit-item-time t]
842 "---"
843 ["Lower Item Priority" todos-lower-item-priority t]
844 ["Raise Item Priority" todos-raise-item-priority t]
845 ["Set Item Priority" todos-set-item-priority t]
846 ["Move (Recategorize) Item" todos-move-item t]
847 ["Delete Item" todos-delete-item t]
848 ["Undo Done Item" todos-item-undo t]
849 ["Mark/Unmark Item for Diary" todos-toggle-item-diary-inclusion t]
850 ["Mark/Unmark Items for Diary" todos-toggle-diary-inclusion t]
851 ["Mark & Hide Done Item" todos-item-done t]
852 ["Archive Done Items" todos-archive-done-items t]
853 "---"
854 ["Add New Todos File" todos-add-file t]
855 ["Add New Category" todos-add-category t]
856 ["Delete Current Category" todos-delete-category t]
857 ["Rename Current Category" todos-rename-category t]
858 "---"
859 ["Save Todos File" todos-save t]
860 ["Save Top Priorities" todos-save-top-priorities t])
861 "---"
862 ["Quit" todos-quit t]
863 ))
b28025ed 864
d04d6b95
SB
865;; FIXME: remove when part of Emacs
866(add-to-list 'auto-mode-alist '("\\.todo\\'" . todos-mode))
867(add-to-list 'auto-mode-alist '("\\.toda\\'" . todos-archive-mode))
868
869(defun todos-modes-set-1 ()
870 ""
871 (set (make-local-variable 'font-lock-defaults) '(todos-font-lock-keywords t))
872 (set (make-local-variable 'indent-line-function) 'todos-indent)
ee7412e4 873 (when todos-wrap-lines (funcall todos-line-wrapping-function))
d04d6b95
SB
874)
875
876(defun todos-modes-set-2 ()
877 ""
ee7412e4
SB
878 (add-to-invisibility-spec 'todos)
879 (setq buffer-read-only t)
d04d6b95
SB
880 (set (make-local-variable 'hl-line-range-function)
881 (lambda() (when (todos-item-end)
882 (cons (todos-item-start) (todos-item-end)))))
883)
884
885;; ;; As calendar reads included Todos file before todos-mode is loaded.
886;; ;;;###autoload
887(define-derived-mode todos-mode nil "Todos" ()
888 "Major mode for displaying, navigating and editing Todo lists.
3f031767 889
d04d6b95
SB
890\\{todos-mode-map}"
891 (easy-menu-add todos-menu)
892 (todos-modes-set-1)
893 (todos-modes-set-2)
894 (set (make-local-variable 'todos-show-done-only) nil)
895 (when todos-auto-switch-todos-file
896 (add-hook 'post-command-hook
897 'todos-switch-todos-file nil t)))
898
899(define-derived-mode todos-archive-mode nil "Todos-Arch" ()
ee7412e4
SB
900 "Major mode for archived Todos categories.
901
902\\{todos-archive-mode-map}"
d04d6b95
SB
903 (todos-modes-set-1)
904 (todos-modes-set-2)
905 (set (make-local-variable 'todos-show-done-only) t)
906 (when todos-auto-switch-todos-file
907 (add-hook 'post-command-hook
908 'todos-switch-todos-file nil t)))
909
910(define-derived-mode todos-edit-mode nil "Todos-Ed" ()
ee7412e4
SB
911 "Major mode for editing multiline Todo items.
912
913\\{todos-edit-mode-map}"
d04d6b95 914 (todos-modes-set-1))
3f031767 915
d04d6b95 916(define-derived-mode todos-categories-mode nil "Todos-Cats" ()
ee7412e4
SB
917 "Major mode for displaying and editing Todos categories.
918
919\\{todos-categories-mode-map}"
ee7412e4
SB
920 (make-local-variable 'font-lock-defaults)
921 (setq font-lock-defaults '(todos-font-lock-keywords t))
d04d6b95
SB
922 (setq buffer-read-only t))
923
924(define-derived-mode todos-top-priorities-mode nil "Todos-Top" ()
925 "Mode for displaying and reprioritizing top priority Todos.
926
927\\{todos-top-priorites-mode-map}"
928 (todos-modes-set-1)
929 (todos-modes-set-2))
2c173503 930
ee7412e4
SB
931(defun todos-save ()
932 "Save the TODO list."
933 (interactive)
d04d6b95
SB
934 ;; (todos-update-categories-sexp)
935 (save-buffer)
936 ;; (if todos-save-top-priorities-too (todos-save-top-priorities))
937 )
3f031767 938
ee7412e4
SB
939(defun todos-quit ()
940 "Done with TODO list for now."
941 (interactive)
d04d6b95
SB
942 (cond ((eq major-mode 'todos-categories-mode)
943 (kill-buffer)
944 (setq todos-descending-counts-store nil)
945 (setq todos-categories nil)
946 (todos-show))
947 ((member major-mode (list 'todos-mode 'todos-archive-mode))
948 (todos-save)
949 (bury-buffer))))
2c173503 950
ee7412e4
SB
951;; ---------------------------------------------------------------------------
952;;; Commands
2c173503 953
ee7412e4
SB
954;;; Display
955
956;;;###autoload
d04d6b95
SB
957(defun todos-show (&optional solicit-file)
958 "Visit the current Todos file and display one of its categories.
959
960With non-nil prefix argument SOLICIT-FILE ask for file to visit,
961otherwise the first invocation of this command in a session
962visits `todos-default-todos-file' (creating it if it does not yet
963exist). Subsequent invocations from outside of Todos mode
964revisit this file or whichever Todos file has been made
965current (e.g. by calling `todos-switch-todos-file').
966
967The category displayed is initially the first member of
968`todos-categories' for the current Todos file, subsequently
969whichever category is current. If
970`todos-display-categories-first' is non-nil, then the first
971invocation of `todos-show' displays a clickable listing of the
972categories in the current Todos file."
973 (interactive "P")
974 ;; ;; Make this a no-op if called interactively in narrowed Todos mode, since
975 ;; ;; it is redundant in that case, but in particular to work around the bug of
976 ;; ;; item prefix reduplication with show-paren-mode enabled.
977 ;; (unless (and (called-interactively-p)
978 ;; (or (eq major-mode 'todos-mode) (eq major-mode 'todos-archive-mode))
979 ;; (< (- ( point-max) (point-min)) (buffer-size)))
980 (when (and (called-interactively-p)
981 (or solicit-file
982 (member todos-current-todos-file todos-archives)))
983 (setq todos-current-todos-file nil
984 todos-categories nil
985 todos-category-number 0))
986 (let ((first-visit (or (not todos-current-todos-file) ;first call
987 ;; after switching to a not yet visited Todos file
988 (not (buffer-live-p
989 (get-file-buffer todos-current-todos-file))))))
990 (if solicit-file
991 (setq todos-current-todos-file
992 (todos-read-file-name "Select a Todos file to visit: "))
993 (or todos-current-todos-file
994 (setq todos-current-todos-file (or todos-default-todos-file
995 (todos-add-file)))))
996 (if (and first-visit todos-display-categories-first)
997 (todos-display-categories)
998 (find-file todos-current-todos-file)
999 ;; (or (eq major-mode 'todos-mode) (todos-mode))
1000 ;; initialize new Todos file
1001 (if (zerop (buffer-size))
1002 (setq todos-category-number (todos-add-category))
1003 ;; FIXME: let user choose category?
1004 (if (zerop todos-category-number) (setq todos-category-number 1)))
1005 (or todos-categories
1006 (setq todos-categories (if todos-ignore-archived-categories
1007 (todos-truncate-categories-list)
1008 (todos-make-categories-list))))
1009 (save-excursion (todos-category-select)))));)
1010
1011;; FIXME: make core of this internal?
1012(defun todos-display-categories (&optional sortkey)
1013 "Display the category names of the current Todos file.
1014The numbers indicate the current order of the categories.
1015
1016With non-nil SORTKEY display a non-numbered alphabetical list.
ee7412e4 1017The lists are in Todos Categories mode.
3f031767 1018
ee7412e4
SB
1019The category names are buttonized, and pressing a button displays
1020the category in Todos mode."
3f031767 1021 (interactive)
d04d6b95
SB
1022 (let* ((cats0 (if (and todos-ignore-archived-categories
1023 (not (eq major-mode 'todos-categories-mode)))
1024 (todos-make-categories-list t)
1025 todos-categories))
1026 (cats (todos-sort cats0 sortkey))
1027 ;; used by todos-insert-category-line
1028 (num 0))
2c173503
SB
1029 (with-current-buffer (get-buffer-create todos-categories-buffer)
1030 (switch-to-buffer (current-buffer))
ee7412e4
SB
1031 (let (buffer-read-only)
1032 (erase-buffer)
1033 (kill-all-local-variables)
d04d6b95
SB
1034 (insert (format "Category counts for Todos file \"%s\"."
1035 (file-name-sans-extension
1036 (file-name-nondirectory todos-current-todos-file))))
1037 (newline 2)
1038 ;; FIXME: abstract format from here and todos-insert-category-line
1039 (insert (make-string (+ 3 (length todos-categories-number-separator)) 32))
ee7412e4 1040 (save-excursion
d04d6b95
SB
1041 (todos-insert-sort-button todos-categories-category-label)
1042 (if (member todos-current-todos-file todos-archives)
1043 (insert (concat (make-string 6 32)
1044 (format "%s" todos-categories-archived-label)))
1045 (insert (make-string 3 32))
1046 (todos-insert-sort-button todos-categories-todo-label)
1047 (insert (make-string 2 32))
1048 (todos-insert-sort-button todos-categories-diary-label)
1049 (insert (make-string 2 32))
1050 (todos-insert-sort-button todos-categories-done-label)
1051 (insert (make-string 2 32))
1052 (todos-insert-sort-button todos-categories-archived-label))
1053 (newline 2)
1054 (mapc (lambda (cat) (todos-insert-category-line cat sortkey))
1055 (mapcar 'car cats))))
ee7412e4
SB
1056 (todos-categories-mode))))
1057
d04d6b95 1058;; FIXME: make this toggle with todos-display-categories
ee7412e4
SB
1059(defun todos-display-categories-alphabetically ()
1060 ""
1061 (interactive)
d04d6b95
SB
1062 (todos-display-sorted 'alpha))
1063
1064;; FIXME: provide key bindings for these or delete them
1065(defun todos-display-categories-sorted-by-todo ()
1066 ""
1067 (interactive)
1068 (todos-display-sorted 'todo))
1069
1070(defun todos-display-categories-sorted-by-diary ()
1071 ""
1072 (interactive)
1073 (todos-display-sorted 'diary))
1074
1075(defun todos-display-categories-sorted-by-done ()
1076 ""
1077 (interactive)
1078 (todos-display-sorted 'done))
1079
1080(defun todos-display-categories-sorted-by-archived ()
1081 ""
1082 (interactive)
1083 (todos-display-sorted 'archived))
2c173503
SB
1084
1085(defun todos-toggle-item-numbering ()
ee7412e4 1086 ""
2c173503
SB
1087 (interactive)
1088 (todos-reset-prefix 'todos-number-prefix (not todos-number-prefix)))
3f031767 1089
2c173503 1090(defun todos-toggle-view-done-items ()
ee7412e4 1091 ""
3f031767 1092 (interactive)
ee7412e4
SB
1093 (save-excursion
1094 (goto-char (point-min))
d04d6b95
SB
1095 (let ((todos-show-with-done
1096 (if (re-search-forward todos-done-string-start nil t)
1097 nil
1098 t))
1099 (cat (todos-current-category)))
f730d273 1100 (todos-category-select)
d04d6b95 1101 (when (zerop (todos-get-count 'done cat))
f730d273 1102 (message "There are no done items in this category.")))))
2c173503 1103
d04d6b95 1104(defun todos-toggle-show-done-only ()
2c173503
SB
1105 ""
1106 (interactive)
d04d6b95
SB
1107 (setq todos-show-done-only (not todos-show-done-only))
1108 (todos-category-select))
1109
1110(defun todos-view-archived-items ()
1111 "Display the archived items of the current category.
1112The buffer showing these items is in Todos Archive mode."
1113 (interactive)
1114 (let ((cat (todos-current-category)))
1115 (if (zerop (todos-get-count 'archived cat))
1116 (message "There are no archived items from this category.")
1117 (let* ((tfile-base (file-name-sans-extension todos-current-todos-file))
1118 (afile (concat tfile-base ".toda")))
1119 (find-file afile)
f730d273 1120 (todos-archive-mode)
d04d6b95
SB
1121 (unless (string= todos-current-todos-file afile)
1122 (setq todos-current-todos-file afile)
f730d273
SB
1123 (setq todos-categories nil))
1124 (unless todos-categories
1125 (setq todos-categories (todos-make-categories-list)))
d04d6b95
SB
1126 (setq todos-category-number
1127 (- (length todos-categories)
1128 (length (member cat todos-categories)))) ;FIXME
1129 (todos-jump-to-category cat)))))
1130
1131(defun todos-switch-to-archive (&optional ask)
1132 "Visit the archive of the current Todos file, if it exists.
1133The buffer showing the archive is in Todos Archive mode. The
1134first visit in a session displays the first category in the
1135archive, subsequent visits return to the last category
1136displayed."
1137 (interactive)
1138 (let* ((tfile-base (file-name-sans-extension todos-current-todos-file))
1139 (afile (if ask
1140 (todos-read-file-name "Choose a Todos archive: " t)
1141 (concat tfile-base ".toda"))))
1142 (if (not (file-exists-p afile))
1143 (message "There is currently no Todos archive for this file.")
1144 (find-file afile)
1145 (todos-archive-mode)
1146 (unless (string= todos-current-todos-file afile)
1147 (setq todos-current-todos-file afile)
1148 (setq todos-categories nil))
1149 (unless todos-categories
1150 (setq todos-categories (todos-make-categories-list))
1151 (setq todos-category-number 1))
1152 (todos-category-select))))
1153
1154(defun todos-choose-archive ()
1155 "Choose an archive and visit it."
1156 (interactive)
1157 (todos-switch-to-archive t))
3f031767 1158
2c173503
SB
1159(defun todos-highlight-item ()
1160 "Highlight the todo item the cursor is on."
3f031767 1161 (interactive)
2c173503
SB
1162 (if hl-line-mode ; todos-highlight-item
1163 (hl-line-mode 0)
1164 (hl-line-mode 1)))
1165
1166;; FIXME: make this a customizable option for whole Todos file
1167(defun todos-toggle-display-date-time ()
ee7412e4 1168 ""
2c173503
SB
1169 (interactive)
1170 (save-excursion
1171 (goto-char (point-min))
1172 (let ((ovs (overlays-in (point) (line-end-position)))
1173 ov hidden)
1174 (while ovs
1175 (setq ov (car ovs))
1176 (if (equal (overlay-get ov 'display) "")
1177 (setq ovs nil
1178 hidden t)
1179 (setq ovs (cdr ovs))))
1180 (if hidden (remove-overlays (point-min) (point-max) 'display "")
1181 (while (not (eobp))
d04d6b95 1182 (re-search-forward (concat todos-date-string-start todos-date-pattern
ee7412e4 1183 "\\( " diary-time-regexp "\\)?\\]? ")
2c173503
SB
1184 ; FIXME: this space in header? ^
1185 nil t)
f730d273 1186 ;; FIXME: wrong match data if search fails
2c173503
SB
1187 (setq ov (make-overlay (match-beginning 0) (match-end 0) nil t))
1188 (overlay-put ov 'display "")
f730d273 1189 (forward-line))))))
2c173503 1190
d04d6b95
SB
1191(defun todos-update-merged-files ()
1192 ""
1193 (interactive)
1194 (let ((files (funcall todos-files-function)))
1195 (dolist (f files)
1196 (if (member f todos-merged-files)
1197 (and (y-or-n-p
1198 (format "Remove \"%s\" from list of merged Todos files? "
1199 (file-name-sans-extension (file-name-nondirectory f))))
1200 (setq todos-merged-files (delete f todos-merged-files)))
1201 (and (y-or-n-p
1202 (format "Add \"%s\" to list of merged Todos files? "
1203 (file-name-sans-extension (file-name-nondirectory f))))
1204 (setq todos-merged-files
1205 (append todos-merged-files (list f)))))))
1206 (customize-save-variable 'todos-merged-files todos-merged-files))
1207
1208(defun todos-top-priorities (&optional num merge) ;FIXME: rename b/c of diary items
1209 "List top priorities for each category.
2c173503 1210
d04d6b95
SB
1211Number of entries for each category is given by NUM which
1212defaults to \'todos-show-priorities\'. With non-nil argument
1213MERGE list top priorities of all Todos files in
1214`todos-merged-files'. If `todos-prompt-merged-files' is non-nil,
1215prompt to update the list of merged files."
1216 (interactive "p")
1217 (or num (setq num todos-show-priorities))
2c173503 1218 (let ((todos-print-buffer-name todos-tmp-buffer-name)
d04d6b95
SB
1219 (files (list todos-current-todos-file))
1220 file bufstr cat beg end done)
1221 (when merge
1222 (if (or todos-prompt-merged-files (null todos-merged-files))
1223 (todos-update-merged-files))
1224 (setq files todos-merged-files))
1225 (if (buffer-live-p (get-buffer todos-print-buffer-name))
1226 (kill-buffer todos-print-buffer-name))
1227 (save-current-buffer
1228 (dolist (f files)
1229 (find-file f)
1230 (todos-switch-todos-file)
1231 (setq file (file-name-sans-extension
1232 (file-name-nondirectory todos-current-todos-file)))
1233 (with-current-buffer (get-file-buffer f)
1234 (save-restriction
1235 (widen)
1236 (setq bufstr (buffer-string))))
1237 (with-temp-buffer
1238 (insert bufstr)
1239 (goto-char (point-min))
1240 (unless (looking-at (concat "^" (regexp-quote todos-category-beg)))
1241 (kill-line 1))
1242 (while (re-search-forward
1243 (concat "^" (regexp-quote todos-category-beg) "\\(.+\\)\n")
1244 nil t)
1245 (setq cat (match-string 1))
1246 (delete-region (match-beginning 0) (match-end 0))
1247 (setq beg (point)) ;Start of first entry.
1248 (setq end (if (re-search-forward
1249 (concat "^" (regexp-quote todos-category-beg)) nil t)
1250 (match-beginning 0)
1251 (point-max)))
1252 (goto-char beg)
1253 (setq done
1254 (if (re-search-forward
1255 (concat "\n" (regexp-quote todos-category-done)) end t)
2c173503 1256 (match-beginning 0)
d04d6b95
SB
1257 end))
1258 (delete-region done end)
1259 (setq end done)
1260 (narrow-to-region beg end) ;In case we have too few entries.
1261 (goto-char (point-min))
1262 (cond ((< num 0) ; get only diary items
1263 (while (not (eobp))
1264 (if (looking-at (regexp-quote todos-nondiary-start))
1265 (todos-remove-item)
1266 (todos-forward-item))))
1267 ((zerop num) ; keep all items
1268 (goto-char end))
1269 (t
1270 (todos-forward-item num)))
1271 (setq beg (point))
1272 (if (>= num 0) (delete-region beg end))
1273 (goto-char (point-min))
1274 (while (not (eobp))
1275 (when (re-search-forward (concat todos-date-string-start
1276 todos-date-pattern
1277 "\\( " diary-time-regexp "\\)?\\]?")
1278 nil t)
1279 (insert (concat " [" (if merge (concat file ":")) cat "]")))
1280 (forward-line))
1281 (widen))
1282 (append-to-buffer todos-print-buffer-name (point-min) (point-max)))))
1283 (with-current-buffer todos-print-buffer-name
1284 (todos-prefix-overlays)
1285 (todos-top-priorities-mode)
2c173503 1286 (goto-char (point-min)) ;Due to display buffer
d04d6b95
SB
1287 ;; (make-local-variable 'font-lock-defaults)
1288 ;; (setq font-lock-defaults '(todos-font-lock-keywords t))
1289 (font-lock-fontify-buffer))
1290 ;; (setq buffer-read-only t))
2c173503
SB
1291 ;; Could have used switch-to-buffer as it has a norecord argument,
1292 ;; which is nice when we are called from e.g. todos-print.
1293 ;; Else we could have used pop-to-buffer.
2c173503
SB
1294 (display-buffer todos-print-buffer-name)
1295 (message "Type C-x 1 to remove %s window. M-C-v to scroll the help."
1296 todos-print-buffer-name)))
1297
d04d6b95
SB
1298(defun todos-merged-top-priorities (&optional num)
1299 ""
1300 (interactive "p")
1301 (todos-top-priorities num t))
1302
1303(defun todos-diary-items (&optional merge)
1304 "Display todo items marked for diary inclusion.
1305The items are those in the current Todos file, or with prefix
1306argument MERGE those in all Todos files in `todos-merged-files'."
1307 (interactive "P")
1308 (todos-top-priorities -1 merge))
1309
ee7412e4
SB
1310;;; Navigation
1311
1312(defun todos-forward-category ()
1313 "Go forward to TODO list of next category."
1314 (interactive)
1315 (setq todos-category-number
d04d6b95
SB
1316 (1+ (mod todos-category-number (length todos-categories))))
1317 (todos-category-select)
1318 (goto-char (point-min)))
ee7412e4
SB
1319
1320(defun todos-backward-category ()
1321 "Go back to TODO list of previous category."
1322 (interactive)
1323 (setq todos-category-number
d04d6b95
SB
1324 (1+ (mod (- todos-category-number 2) (length todos-categories))))
1325 (todos-category-select)
1326 (goto-char (point-min)))
ee7412e4
SB
1327
1328;; FIXME: Document that a non-existing name creates that category, and add
1329;; y-or-n-p confirmation -- or eliminate this possibility?
d04d6b95
SB
1330(defun todos-jump-to-category (&optional cat other-file)
1331 "Jump to a category in a Todos file.
1332When called interactively, prompt for the category.
1333Non-interactively, the argument CAT provides the category. With
1334non-nil argument OTHER-FILE, prompt for a Todos file, otherwise
1335stay with the current Todos file. See also
1336`todos-jump-to-category-other-file'."
ee7412e4 1337 (interactive)
d04d6b95
SB
1338 (when (or (and other-file
1339 (setq todos-current-todos-file
1340 (todos-read-file-name "Choose a Todos file: ")))
1341 (and cat
1342 todos-ignore-archived-categories
1343 (zerop (todos-get-count 'todo cat))
1344 (zerop (todos-get-count 'done cat))
1345 (not (zerop (todos-get-count 'archived cat)))
1346 (setq todos-current-todos-file
1347 (concat (file-name-sans-extension todos-current-todos-file)
1348 ".toda"))))
1349 (with-current-buffer (find-file-noselect todos-current-todos-file)
1350 ;; (or (eq major-mode 'todos-mode) (todos-mode))
1351 (setq todos-categories (todos-make-categories-list))))
1352 (let ((category (or (and (assoc cat todos-categories) cat)
1353 (todos-read-category "Jump to category: "))))
ee7412e4
SB
1354 (if (string= "" category)
1355 (setq category (todos-current-category)))
d04d6b95
SB
1356 (if (string= (buffer-name) todos-categories-buffer)
1357 (kill-buffer))
1358 (if (or cat other-file)
1359 (switch-to-buffer (get-file-buffer todos-current-todos-file)))
ee7412e4 1360 (setq todos-category-number
d04d6b95
SB
1361 (or (todos-category-number category)
1362 (todos-add-category category)))
1363 (todos-category-select)
1364 (goto-char (point-min))))
1365
1366(defun todos-jump-to-category-other-file ()
1367 ""
1368 (interactive)
1369 (todos-jump-to-category nil t))
ee7412e4
SB
1370
1371;; FIXME ? todos-{backward,forward}-item skip over empty line between done and
1372;; not done items (but todos-forward-item gets there when done items are not
1373;; displayed). Also disallow prefix arg value < 1 (re-search-* allows these)
1374(defun todos-backward-item (&optional count)
1375 "Select COUNT-th previous entry of TODO list."
1376 (interactive "P")
1377 ;; FIXME ? this moves to bob if on the first item (but so does previous-line)
1378 (todos-item-start)
1379 (unless (bobp)
1380 (re-search-backward todos-item-start nil t (or count 1))))
1381
1382(defun todos-forward-item (&optional count)
1383 "Select COUNT-th next entry of TODO list."
1384 (interactive "P")
1385 (goto-char (line-end-position))
1386 (if (re-search-forward todos-item-start nil t (or count 1))
1387 (goto-char (match-beginning 0))
1388 (goto-char (point-max))))
1389
ee7412e4 1390(defun todos-search ()
d04d6b95
SB
1391 "Perform a search for a regular expression, with repetition.
1392The search encompasses all todo and done items within the current Todos file; it excludes category names. Matches are highlighted
1393"
ee7412e4
SB
1394 (interactive)
1395 (let ((regex (read-from-minibuffer "Enter a search string (regexp): "))
d04d6b95
SB
1396 (opoint (point))
1397 matches match cat in-done ov mlen msg)
ee7412e4
SB
1398 (widen)
1399 (goto-char (point-min))
d04d6b95
SB
1400 (while (not (eobp))
1401 (setq match (re-search-forward regex nil t))
1402 (goto-char (line-beginning-position))
1403 (unless (or (equal (point) 1)
1404 (looking-at (concat "^" (regexp-quote todos-category-beg))))
1405 (if match (push match matches)))
ee7412e4 1406 (forward-line))
d04d6b95
SB
1407 (setq matches (reverse matches))
1408 (if matches
1409 (catch 'stop
1410 (while matches
1411 (setq match (pop matches))
1412 (goto-char match)
1413 (todos-item-start)
1414 (when (looking-at todos-done-string-start)
1415 (setq in-done t))
1416 (re-search-backward (concat "^" (regexp-quote todos-category-beg)
1417 "\\(.*\\)\n") nil t)
1418 (setq cat (match-string-no-properties 1))
1419 (todos-category-number cat)
1420 (todos-category-select)
1421 (if in-done (unless todos-show-with-done (todos-toggle-view-done-items)))
1422 (goto-char match)
1423 (setq ov (make-overlay (- (point) (length regex)) (point)))
1424 (overlay-put ov 'face 'todos-search)
1425 (when matches
1426 (setq mlen (length matches))
1427 (if (y-or-n-p
1428 (if (> mlen 1)
1429 (format "There are %d more matches; go to next match? " mlen)
1430 "There is one more match; go to it? "))
1431 (widen)
1432 (throw 'stop (setq msg (if (> mlen 1)
1433 (format "There are %d more matches." mlen)
1434 "There is one more match."))))))
1435 (setq msg "There are no more matches."))
ee7412e4 1436 (todos-category-select)
d04d6b95
SB
1437 (goto-char opoint)
1438 (message "No match for \"%s\"" regex))
1439 (when msg
1440 (if (y-or-n-p (concat msg "\nUnhighlight matches? "))
1441 (todos-clear-matches)
1442 (message "You can unhighlight the matches later by typing %s"
1443 (key-description (car (where-is-internal
1444 'todos-clear-matches))))))))
1445
1446(defun todos-clear-matches ()
1447 "Removing highlighting on matches found by todos-search."
1448 (interactive)
1449 (remove-overlays 1 (1+ (buffer-size)) 'face 'todos-search))
ee7412e4 1450
2c173503 1451;;; Editing
3f031767 1452
d04d6b95
SB
1453(defun todos-add-file (&optional arg)
1454 ""
1455 (interactive "p")
1456 (let ((default-file (if todos-default-todos-file
1457 (file-name-sans-extension
1458 (file-name-nondirectory todos-default-todos-file))))
1459 file prompt)
1460 (while
1461 (and
1462 (cond
1463 ((or (not file) (member file todos-files))
1464 (setq prompt (concat "Enter name of new Todos file "
1465 "(TAB or SPC to see existing Todos files): ")))
1466 ((string-equal file "")
1467 (setq prompt "Enter a non-empty name: "))
1468 ((string-match "\\`\\s-+\\'" file)
1469 (setq prompt "Enter a name that is not only white space: ")))
1470 (setq file (todos-read-file-name prompt))))
1471 (if (or (not default-file)
1472 (yes-or-no-p (concat "Make %s new default Todos file "
1473 "[current default is \"%s\"]? ")
1474 file default-file))
1475 (todos-change-default-file file)
1476 (message "\"%s\" remains the default Todos file." default-file))
1477 (with-current-buffer (get-buffer-create todos-default-todos-file)
1478 (erase-buffer)
1479 (write-region (point-min) (point-max) todos-default-todos-file
1480 nil 'nomessage nil t))
1481 (if arg (todos-show) file)))
1482
1483;; FIXME: omit this and just use defcustom?
1484(defun todos-change-default-file (&optional file)
1485 ""
1486 (interactive)
1487 (let ((new-default (or file
1488 (todos-read-file-name "Choose new default Todos file: "))))
1489 (customize-save-variable 'todos-default-todos-file new-default)
1490 (message "\"%s\" is new default Todos file."
1491 (file-name-sans-extension (file-name-nondirectory new-default)))))
1492
3f031767
SB
1493(defun todos-add-category (&optional cat)
1494 "Add new category CAT to the TODO list."
1495 (interactive)
d04d6b95
SB
1496 (let* ((buffer-read-only)
1497 (buf (find-file-noselect todos-current-todos-file t))
1498 (num (1+ (length todos-categories)))
1499 (counts (make-vector 4 0))) ; [todo diary done archived]
1500 ;; (counts (list 'todo 0 'diary 0 'done 0 'archived 0)))
3f031767
SB
1501 (unless (zerop (buffer-size buf))
1502 (and (null todos-categories)
ee7412e4 1503 (error "Error in %s: File is non-empty but contains no category"
d04d6b95
SB
1504 todos-current-todos-file)))
1505 (unless cat (setq cat (read-from-minibuffer "Enter new category name: ")))
3f031767 1506 (with-current-buffer buf
d04d6b95
SB
1507 (setq cat (todos-validate-category-name cat))
1508 (setq todos-categories (append todos-categories (list (cons cat counts))))
3f031767 1509 (widen)
d04d6b95
SB
1510 (goto-char (point-max))
1511 (save-excursion ; for subsequent todos-category-select
1512 (insert todos-category-beg cat "\n\n" todos-category-done "\n"))
1513 (todos-update-categories-sexp)
1514 (if (called-interactively-p 'any) ; FIXME
3f031767 1515 ;; properly display the newly added category
d04d6b95
SB
1516 (progn
1517 (setq todos-category-number num)
1518 (todos-category-select))
1519 num))))
3f031767 1520
2c173503 1521(defun todos-rename-category ()
db2c5d34 1522 "Rename current Todos category."
2c173503 1523 (interactive)
d04d6b95 1524 (let* ((cat (todos-current-category))
ee7412e4 1525 (new (read-from-minibuffer (format "Rename category \"%s\" to: " cat))))
d04d6b95
SB
1526 (setq new (todos-validate-category-name new))
1527 (let* ((ofile (buffer-file-name))
1528 (archive (concat (file-name-sans-extension ofile) ".toda"))
1529 (buffers (append (list ofile)
1530 (unless (zerop (todos-get-count 'archived cat))
1531 (list archive)))))
1532 (dolist (buf buffers)
1533 (with-current-buffer (find-file-noselect buf)
1534 (let (buffer-read-only)
1535 ;; (setq todos-categories (if (string= buf archive)
1536 ;; (todos-make-categories-list t)
1537 ;; todos-categories))
1538 (todos-set-categories)
1539 (save-excursion
1540 (save-restriction
1541 (setcar (assoc cat todos-categories) new)
1542 (widen)
1543 (goto-char (point-min))
1544 (todos-update-categories-sexp)
1545 (re-search-forward (concat (regexp-quote todos-category-beg) "\\("
1546 (regexp-quote cat) "\\)\n") nil t)
1547 (replace-match new t t nil 1)))))))
1548 (setq mode-line-buffer-identification
1549 (format "Category %d: %s" todos-category-number new)))
1550 (save-excursion (todos-category-select)))
db2c5d34 1551
d04d6b95 1552;; FIXME: what if cat has archived items?
b28025ed
SB
1553(defun todos-delete-category (&optional arg)
1554 "Delete current Todos category provided it is empty.
1555With ARG non-nil delete the category unconditionally,
1556i.e. including all existing entries."
1557 (interactive "P")
ee7412e4 1558 (let* ((cat (todos-current-category))
d04d6b95
SB
1559 (todo (todos-get-count 'todo cat))
1560 (done (todos-get-count 'done cat)))
1561 (if (and (not arg)
f730d273 1562 (or (> todo 0) (> done 0)))
ee7412e4 1563 (message "To delete a non-empty category, type C-u D.")
b28025ed
SB
1564 (when (y-or-n-p (concat "Permanently remove category \"" cat
1565 "\"" (and arg " and all its entries") "? "))
d04d6b95
SB
1566 (widen)
1567 (let ((buffer-read-only)
1568 (beg (re-search-backward
1569 (concat "^" (regexp-quote (concat todos-category-beg cat))
1570 "\n") nil t))
1571 (end (if (re-search-forward
1572 (concat "\n\\(" (regexp-quote todos-category-beg)
1573 ".*\n\\)") nil t)
1574 (match-beginning 1)
1575 (point-max))))
2c173503 1576 (remove-overlays beg end)
d04d6b95
SB
1577 (delete-region beg end)
1578 (setq todos-categories (delete (assoc cat todos-categories)
1579 todos-categories))
1580 (todos-update-categories-sexp)
f730d273 1581 (setq todos-category-number
d04d6b95 1582 (1+ (mod todos-category-number (length todos-categories))))
2c173503 1583 (todos-category-select)
d04d6b95 1584 (goto-char (point-min))
2c173503 1585 (message "Deleted category %s" cat))))))
db2c5d34 1586
ee7412e4
SB
1587(defun todos-raise-category (&optional lower)
1588 "Raise priority of category point is on in Categories buffer.
1589With non-nil argument LOWER, lower the category's priority."
1590 (interactive)
1591 (let (num)
1592 (save-excursion
1593 (forward-line 0)
1594 (skip-chars-forward " ")
1595 (setq num (number-at-point)))
1596 (when (and num (if lower
1597 (< num (length todos-categories))
1598 (> num 1)))
1599 (let* ((col (current-column))
1600 (beg (progn (forward-line (if lower 0 -1)) (point)))
1601 (num1 (progn (skip-chars-forward " ") (1- (number-at-point))))
1602 (num2 (1+ num1))
1603 (end (progn (forward-line 2) (point)))
1604 (catvec (vconcat todos-categories))
d04d6b95
SB
1605 (cat1-list (aref catvec num1))
1606 (cat2-list (aref catvec num2))
1607 (cat1 (car cat1-list))
1608 (cat2 (car cat2-list))
ee7412e4
SB
1609 (buffer-read-only))
1610 (delete-region beg end)
d04d6b95
SB
1611 (setq num1 (1+ num1))
1612 (setq num2 (1- num2))
ee7412e4 1613 (setq num num2)
d04d6b95 1614 (todos-insert-category-line cat2)
ee7412e4 1615 (setq num num1)
d04d6b95
SB
1616 (todos-insert-category-line cat1)
1617 (aset catvec num2 (cons cat2 (cdr cat2-list)))
1618 (aset catvec num1 (cons cat1 (cdr cat1-list)))
ee7412e4 1619 (setq todos-categories (append catvec nil))
d04d6b95
SB
1620 (with-current-buffer (get-file-buffer todos-current-todos-file)
1621 (todos-update-categories-sexp))
ee7412e4
SB
1622 (forward-line (if lower -1 -2))
1623 (forward-char col)))))
1624
1625(defun todos-lower-category ()
1626 "Lower priority of category point is on in Categories buffer."
1627 (interactive)
1628 (todos-raise-category t))
1629
d04d6b95
SB
1630;; FIXME: use save-restriction?
1631(defun todos-move-category ()
1632 "Move current category to a different Todos file.
1633If current category has archived items, also move those to the
1634archive of the file moved to, creating it if it does not exist."
1635 (interactive)
1636 ;; FIXME: warn if only category in file? If so, delete file after moving category
1637 (when (or (> (length todos-categories) 1)
1638 (y-or-n-p (concat "This is the only category in this file; "
1639 "moving it will delete the file.\n"
1640 "Do you want to proceed? ")))
1641 (let* ((ofile (buffer-file-name))
1642 (cat (todos-current-category))
1643 ;; FIXME: check if cat exists in nfile, and if so rename it
1644 (nfile (todos-read-file-name "Choose a Todos file: "))
1645 (archive (concat (file-name-sans-extension ofile) ".toda"))
1646 (buffers (append (list ofile)
1647 (unless (zerop (todos-get-count 'archived cat))
1648 (list archive)))))
1649 (dolist (buf buffers)
1650 (with-current-buffer (find-file-noselect buf)
1651 (save-excursion
1652 (save-restriction
1653 (widen)
1654 (goto-char (point-max))
1655 (let ((buffer-read-only nil)
1656 (beg (re-search-backward
1657 (concat "^"
1658 (regexp-quote (concat todos-category-beg cat)))
1659 nil t))
1660 (end (if (re-search-forward
1661 (concat "^" (regexp-quote todos-category-beg))
1662 nil t 2)
1663 (match-beginning 0)
1664 (point-max)))
1665 (content (buffer-substring-no-properties beg end)))
1666 (with-current-buffer
1667 (find-file-noselect
1668 ;; regenerate todos-archives in case there
1669 ;; is a newly created archive
1670 (if (member buf (funcall todos-files-function t))
1671 (concat (file-name-sans-extension nfile) ".toda")
1672 nfile))
1673 (let (buffer-read-only)
1674 (save-excursion
1675 (save-restriction
1676 (widen)
1677 (goto-char (point-max))
1678 (insert content)
1679 (goto-char (point-min))
1680 (if (zerop (buffer-size))
1681 (progn
1682 (set-buffer-modified-p nil) ; no questions
1683 (delete-file (buffer-file-name))
1684 (kill-buffer))
1685 (unless (looking-at
1686 (concat "^" (regexp-quote todos-category-beg)))
1687 (kill-whole-line))
1688 (save-buffer)))))
1689 (remove-overlays beg end)
1690 (delete-region beg end)
1691 (goto-char (point-min))
1692 (if (zerop (buffer-size))
1693 (progn
1694 (set-buffer-modified-p nil)
1695 (delete-file (buffer-file-name))
1696 (kill-buffer))
1697 (unless (looking-at
1698 (concat "^" (regexp-quote todos-category-beg)))
1699 (kill-whole-line))
1700 (save-buffer))))))))
1701 ;; (todos-switch-todos-file nfile))))
1702 (find-file nfile)
1703 (setq todos-current-todos-file nfile
1704 todos-categories (todos-make-categories-list t)
1705 todos-category-number (todos-category-number cat))
1706 (todos-category-select))))
1707
1708(defun todos-merge-category ()
1709 "Merge this category's items to another category in this file.
1710The todo and done items are appended to the todo and done items,
1711respectively, of the category merged to, which becomes the
1712current category, and the category merged from is deleted."
1713 (interactive)
1714 (let ((buffer-read-only nil)
1715 (cat (todos-current-category))
1716 (goal (todos-read-category "Category to merge to: ")))
1717 (widen)
1718 ;; FIXME: what if cat has archived items?
1719 (let* ((cbeg (progn
1720 (re-search-backward
1721 (concat "^" (regexp-quote todos-category-beg)) nil t)
1722 (point)))
1723 (tbeg (progn (forward-line) (point)))
1724 (dbeg (progn
1725 (re-search-forward
1726 (concat "^" (regexp-quote todos-category-done)) nil t)
1727 (match-beginning 0)))
1728 (tend (forward-line -1))
1729 (cend (progn
1730 (if (re-search-forward
1731 (concat "^" (regexp-quote todos-category-beg)) nil t)
1732 (match-beginning 0)
1733 (point-max))))
1734 (todo (buffer-substring-no-properties tbeg tend))
1735 (done (buffer-substring-no-properties dbeg cend))
1736 here)
1737 (goto-char (point-min))
1738 (re-search-forward
1739 (concat "^" (regexp-quote todos-category-beg goal)) nil t)
1740 (re-search-forward
1741 (concat "^" (regexp-quote todos-category-done)) nil t)
1742 (forward-line -1)
1743 (setq here (point))
1744 (insert todo)
1745 (goto-char (if (re-search-forward
1746 (concat "^" (regexp-quote todos-category-beg)) nil t)
1747 (match-beginning 0)
1748 (point-max)))
1749 (insert done)
1750 (remove-overlays cbeg cend)
1751 (delete-region cbeg cend)
1752 (setq todos-categories (delete (assoc cat todos-categories)
1753 todos-categories))
1754 (todos-update-categories-sexp)
1755 (setq todos-category-number (todos-category-number goal))
1756 (todos-category-select)
1757 ;; Put point at the start of the merged todo items
1758 ;; FIXME: what if there are no merged todo items but only done items?
1759 (goto-char here))))
1760
1761(defun todos-merge-categories ()
1762 ""
1763 (interactive)
1764 (let* ((cats (mapcar 'car todos-categories))
1765 (goal (todos-read-category "Category to merge to: "))
1766 (prompt (format "Merge to %s (type C-g to finish)? " goal))
1767 (source (let ((inhibit-quit t) l)
1768 (while (not (eq last-input-event 7))
1769 (dolist (c cats)
1770 (when (y-or-n-p prompt)
1771 (push c l)
1772 (setq cats (delete c cats))))))))
1773 (widen)
1774 ))
1775
2c173503 1776;;;###autoload
ee7412e4 1777(defun todos-insert-item (&optional arg date-type time diary here)
2c173503 1778 "Insert new TODO list item.
db2c5d34 1779
2c173503
SB
1780With prefix argument ARG solicit the category, otherwise use the
1781current category.
db2c5d34 1782
ee7412e4
SB
1783Argument DATE-TYPE sets the form of the item's mandatory date
1784string. With the value `date' this is the full date (whose
1785format is set by `calendar-date-display-form', with year, month
1786and day individually solicited (month with tab completion). With
1787the value `dayname' a weekday name is used, solicited with tab
1788completion. With the value `calendar' the full date string is
1789used and set by selecting from the Calendar. With any other
1790value (including none) the full current date is used.
1791
1792Argument TIME determines the occurrence and value of the time
1793string. With the value `omit' insert the item without a time
1794string. With the value `ask' solicit a time string; this may be
1795empty or else must match `date-time-regexp'. With any other
1796value add or omit the current time in accordance with
1797`todos-always-add-time-string'.
1798
1799With non-nil argument DIARY mark item for inclusion in user's diary. If `todos-include-in-diary' is non-nil
1800
2c173503
SB
1801With non-nil argument HERE insert the new item directly above the
1802item at point. If point is on an empty line, insert the new item
ee7412e4 1803there."
3f031767 1804 (interactive "P")
2c173503
SB
1805 (unless (or (todos-done-item-p)
1806 (save-excursion (forward-line -1) (todos-done-item-p)))
d04d6b95 1807 ;; FIXME: deletable if command not autoloaded
f730d273 1808 (when (not (derived-mode-p 'todos-mode)) (todos-show))
2c173503 1809 (let* ((buffer-read-only)
ee7412e4
SB
1810 (date-string (cond
1811 ((eq date-type 'ask-date)
1812 (todos-read-date))
1813 ((eq date-type 'ask-dayname)
1814 (todos-read-dayname))
1815 ((eq date-type 'calendar)
1816 ;; FIXME: should only be executed from Calendar
2c173503
SB
1817 (with-current-buffer "*Calendar*"
1818 (calendar-date-string (calendar-cursor-to-date t) t t)))
1819 (t (calendar-date-string (calendar-current-date) t t))))
d04d6b95 1820 (time-string (cond ((eq time 'ask-time)
ee7412e4
SB
1821 (todos-read-time))
1822 (todos-always-add-time-string
d04d6b95
SB
1823 (substring (current-time-string) 11 16))
1824 (t nil)))
1825 (new-item (concat (unless (or diary todos-include-in-diary)
1826 todos-nondiary-start)
ee7412e4 1827 date-string (when time-string (concat " " time-string))
d04d6b95
SB
1828 (unless (or diary todos-include-in-diary)
1829 todos-nondiary-end)
1830 " "
98c97dee 1831 (read-from-minibuffer "New TODO entry: ")))
d04d6b95
SB
1832 (cat (if arg (todos-read-category "Insert item in category: ")
1833 (todos-current-category))))
ee7412e4
SB
1834 ;; indent newlines inserted by C-q C-j if nonspace char follows
1835 (setq new-item (replace-regexp-in-string
1836 "\\(\n\\)[^[:blank:]]"
1837 (concat "\n" (make-string todos-indent-to-here 32)) new-item
1838 nil nil 1))
d04d6b95
SB
1839 (unless (assoc cat todos-categories) (todos-add-category cat))
1840 ;; (unless here (todos-set-item-priority new-item cat))
1841 ;; (todos-insert-with-overlays new-item)
1842 (if here
1843 (todos-insert-with-overlays new-item)
1844 (todos-set-item-priority new-item cat))
1845 (todos-item-counts cat 'insert)
1846 (if (or diary todos-include-in-diary) (todos-item-counts cat 'diary))
1847 (todos-update-categories-sexp))))
ee7412e4
SB
1848
1849;; FIXME: make insertion options customizable per category
ee7412e4 1850
f730d273 1851;; current date ~ current day ~ ask date ~ ask day
d04d6b95 1852;; current time ~ ask time ~ maybe no time
f730d273
SB
1853;; for diary ~ not for diary
1854;; here ~ ask priority
ee7412e4 1855
d04d6b95 1856;; date-type: date name (calendar) - (maybe-no)time - diary - here
ee7412e4 1857
d04d6b95
SB
1858;; ii todos-insert-item + current-date/dayname + current/no-time
1859;; ih todos-insert-item-here
f730d273
SB
1860;; idd todos-insert-item-ask-date
1861;; idtt todos-insert-item-ask-date-time
1862;; idtyy todos-insert-item-ask-date-time-for-diary
1863;; idtyh todos-insert-item-ask-date-time-for-diary-here
1864;; idth todos-insert-item-ask-date-time-here
d04d6b95
SB
1865;; idmm todos-insert-item-ask-date-maybe-notime
1866;; idmyy todos-insert-item-ask-date-maybe-notime-for-diary
1867;; idmyh todos-insert-item-ask-date-maybe-notime-for-diary-here
1868;; idmh todos-insert-item-ask-date-maybe-notime-here
f730d273
SB
1869;; idyy todos-insert-item-ask-date-for-diary
1870;; idyh todos-insert-item-ask-date-for-diary-here
1871;; idh todos-insert-item-ask-date-here
1872;; inn todos-insert-item-ask-dayname
1873;; intt todos-insert-item-ask-dayname-time
1874;; intyy todos-insert-item-ask-dayname-time-for-diary
1875;; intyh todos-insert-item-ask-dayname-time-for-diary-here
1876;; inth todos-insert-item-ask-dayname-time-here
d04d6b95
SB
1877;; inmm todos-insert-item-ask-dayname-maybe-notime
1878;; inmyy todos-insert-item-ask-dayname-maybe-notime-for-diary
1879;; inmyh todos-insert-item-ask-dayname-maybe-notime-for-diary-here
1880;; inmh todos-insert-item-ask-dayname-maybe-notime-here
f730d273
SB
1881;; inyy todos-insert-item-ask-dayname-for-diary
1882;; inyh todos-insert-item-ask-dayname-for-diary-here
1883;; inh todos-insert-item-ask-dayname-here
d04d6b95
SB
1884;; itt todos-insert-item-ask-time
1885;; ityy todos-insert-item-ask-time-for-diary
1886;; ityh todos-insert-item-ask-time-for-diary-here
1887;; ith todos-insert-item-ask-time-here
1888;; im todos-insert-item-maybe-notime
1889;; imyy todos-insert-item-maybe-notime-for-diary
1890;; imyh todos-insert-item-maybe-notime-for-diary-here
1891;; imh todos-insert-item-maybe-notime-here
f730d273
SB
1892;; iyy todos-insert-item-for-diary
1893;; iyh todos-insert-item-for-diary-here
2c173503 1894
d04d6b95 1895(defun todos-insert-item-ask-date (&optional arg)
ee7412e4 1896 ""
d04d6b95
SB
1897 (interactive "P")
1898 (todos-insert-item arg 'ask-date))
2c173503 1899
d04d6b95
SB
1900(defun todos-insert-item-ask-date-time (&optional arg)
1901 ""
1902 (interactive "P")
1903 (todos-insert-item arg 'ask-date 'ask-time))
1904
1905(defun todos-insert-item-ask-date-time-for-diary (&optional arg)
1906 ""
1907 (interactive "P")
1908 (todos-insert-item arg 'ask-date 'ask-time t))
1909
1910(defun todos-insert-item-ask-date-time-for-diary-here ()
1911 ""
1912 (interactive)
1913 (todos-insert-item nil 'ask-date 'ask-time t t))
1914
1915(defun todos-insert-item-ask-date-time-here ()
1916 ""
1917 (interactive)
1918 (todos-insert-item nil 'ask-date 'ask-time nil t))
1919
1920(defun todos-insert-item-ask-date-maybe-notime (&optional arg)
1921 ""
1922 (interactive "P")
1923 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
1924 (todos-insert-item arg 'ask-date)))
1925
1926(defun todos-insert-item-ask-date-maybe-notime-for-diary (&optional arg)
1927 ""
1928 (interactive "P")
1929 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
1930 (todos-insert-item arg 'ask-date nil t)))
1931
1932(defun todos-insert-item-ask-date-maybe-notime-for-diary-here ()
1933 ""
1934 (interactive)
1935 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
1936 (todos-insert-item nil 'ask-date nil t t)))
1937
1938(defun todos-insert-item-ask-date-maybe-notime-here ()
1939 ""
1940 (interactive)
1941 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
1942 (todos-insert-item nil 'ask-date nil nil nil t)))
1943
1944(defun todos-insert-item-ask-date-for-diary (&optional arg)
1945 ""
1946 (interactive "P")
1947 (todos-insert-item arg 'ask-date nil t))
1948
1949(defun todos-insert-item-ask-date-for-diary-here ()
1950 ""
1951 (interactive)
1952 (todos-insert-item nil 'ask-date nil t t))
1953
1954(defun todos-insert-item-ask-date-here ()
1955 ""
1956 (interactive)
1957 (todos-insert-item nil 'ask-date nil nil t))
1958
1959(defun todos-insert-item-ask-dayname (&optional arg)
1960 ""
1961 (interactive "P")
1962 (todos-insert-item arg 'ask-dayname))
1963
1964(defun todos-insert-item-ask-dayname-time (&optional arg)
1965 ""
1966 (interactive "P")
1967 (todos-insert-item arg 'ask-dayname 'ask-time))
1968
1969(defun todos-insert-item-ask-dayname-time-for-diary (&optional arg)
1970 ""
1971 (interactive "P")
1972 (todos-insert-item arg 'ask-dayname 'ask-time t))
1973
1974(defun todos-insert-item-ask-dayname-time-for-diary-here ()
1975 ""
1976 (interactive)
1977 (todos-insert-item nil 'ask-dayname 'ask-time t t))
1978
1979(defun todos-insert-item-ask-dayname-time-here ()
1980 ""
1981 (interactive)
1982 (todos-insert-item nil 'ask-dayname 'ask-time nil t))
1983
1984(defun todos-insert-item-ask-dayname-maybe-notime (&optional arg)
1985 ""
1986 (interactive "P")
1987 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
1988 (todos-insert-item arg 'ask-dayname)))
1989
1990(defun todos-insert-item-ask-dayname-maybe-notime-for-diary (&optional arg)
1991 ""
1992 (interactive "P")
1993 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
1994 (todos-insert-item arg 'ask-dayname nil t)))
1995
1996(defun todos-insert-item-ask-dayname-maybe-notime-for-diary-here ()
1997 ""
1998 (interactive)
1999 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
2000 (todos-insert-item nil 'ask-dayname nil t t)))
2001
2002(defun todos-insert-item-ask-dayname-maybe-notime-here ()
2003 ""
2004 (interactive)
2005 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
2006 (todos-insert-item nil 'ask-dayname nil nil t)))
2007
2008(defun todos-insert-item-ask-dayname-for-diary (&optional arg)
2009 ""
2010 (interactive "P")
2011 (todos-insert-item arg 'ask-dayname nil t))
2012
2013(defun todos-insert-item-ask-dayname-for-diary-here ()
2014 ""
2015 (interactive)
2016 (todos-insert-item nil 'ask-dayname nil t t))
2017
2018(defun todos-insert-item-ask-dayname-here ()
2019 ""
2020 (interactive)
2021 (todos-insert-item nil 'ask-dayname nil nil t))
2022
2023(defun todos-insert-item-ask-time (&optional arg)
2024 ""
2025 (interactive "P")
2026 (todos-insert-item arg nil 'ask-time))
2027
2028(defun todos-insert-item-ask-time-for-diary (&optional arg)
2029 ""
2030 (interactive "P")
2031 (todos-insert-item arg nil 'ask-time t))
2032
2033(defun todos-insert-item-ask-time-for-diary-here ()
2034 ""
2035 (interactive)
2036 (todos-insert-item nil nil 'ask-time t t))
2037
2038(defun todos-insert-item-ask-time-here ()
2039 ""
2040 (interactive)
2041 (todos-insert-item nil nil 'ask-time nil t))
2042
2043(defun todos-insert-item-maybe-notime (&optional arg)
2044 ""
2045 (interactive "P")
2046 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
2047 (todos-insert-item arg)))
2048
2049(defun todos-insert-item-maybe-notime-for-diary (&optional arg)
2050 ""
2051 (interactive "P")
2052 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
2053 (todos-insert-item arg nil nil t)))
2054
2055(defun todos-insert-item-maybe-notime-for-diary-here ()
2056 ""
2057 (interactive)
2058 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
2059 (todos-insert-item nil nil nil t t)))
2060
2061(defun todos-insert-item-maybe-notime-here ()
2062 ""
2063 (interactive)
2064 (let ((todos-always-add-time-string (not todos-always-add-time-string)))
2065 (todos-insert-item nil nil nil nil t)))
2066
2067(defun todos-insert-item-for-diary (&optional arg)
2068 ""
2069 (interactive "P")
2070 (todos-insert-item nil nil nil t))
2071
2072(defun todos-insert-item-for-diary-here ()
2073 ""
2074 (interactive)
2075 (todos-insert-item nil nil nil t t))
2076
2077(defun todos-insert-item-here ()
2078 "Insert new Todo item directly above the item at point.
2079If point is on an empty line, insert the new item there."
2080 (interactive)
2081 (todos-insert-item nil nil nil nil t))
2082
2083;; FIXME: autoload when key-binding is defined in calendar.el
2084(defun todos-insert-item-from-calendar ()
ee7412e4 2085 ""
2c173503 2086 (interactive)
d04d6b95
SB
2087 (pop-to-buffer (file-name-nondirectory todos-current-todos-file))
2088 (todos-show)
ee7412e4 2089 (todos-insert-item t 'calendar))
2c173503
SB
2090
2091;; FIXME: calendar is loaded before todos
2092;; (add-hook 'calendar-load-hook
2093 ;; (lambda ()
2094 (define-key calendar-mode-map "it" 'todos-insert-item-from-calendar);))
3f031767
SB
2095
2096(defun todos-delete-item ()
2097 "Delete current TODO list entry."
2098 (interactive)
2099 (if (> (count-lines (point-min) (point-max)) 0)
2c173503 2100 (let* ((buffer-read-only)
d04d6b95
SB
2101 (item (todos-item-string-start))
2102 (diary-item (todos-diary-item-p))
2103 (cat (todos-current-category))
2104 (answer (y-or-n-p (concat "Permanently remove '" item "'? "))))
2105 (when answer
3f031767 2106 (todos-remove-item)
2c173503
SB
2107 (when (and (bolp) (eolp)
2108 ;; not if last item was deleted
2109 (< (point-min) (point-max)))
2110 (todos-backward-item))
d04d6b95
SB
2111 (todos-item-counts cat 'delete)
2112 (and diary-item (todos-item-counts cat 'nondiary))
2113 (todos-update-categories-sexp)
f730d273
SB
2114 (todos-prefix-overlays)))
2115 (message "No TODO list entry to delete"))) ;FIXME: better message
3f031767 2116
2c173503
SB
2117(defun todos-edit-item ()
2118 "Edit current TODO list entry."
2119 (interactive)
d04d6b95
SB
2120 (when (todos-item-string)
2121 (let* ((buffer-read-only)
2122 (start (todos-item-start))
2123 (item-beg (progn
2124 (re-search-forward
2125 (concat todos-date-string-start todos-date-pattern
2126 "\\( " diary-time-regexp "\\)?"
2127 (regexp-quote todos-nondiary-end) "?")
2128 (line-end-position) t)
2129 (1+ (- (point) start))))
2130 (item (todos-item-string))
2131 (opoint (point)))
2132 (if (todos-string-multiline-p item)
2133 (todos-edit-multiline)
2134 (let ((new (read-string "Edit: " (cons item item-beg))))
2135 (while (not (string-match (concat todos-date-string-start
2136 todos-date-pattern) new))
2137 (setq new (read-from-minibuffer "Item must start with a date: " new)))
2138 ;; indent newlines inserted by C-q C-j if nonspace char follows
2139 (setq new (replace-regexp-in-string
2140 "\\(\n\\)[^[:blank:]]"
2141 (concat "\n" (make-string todos-indent-to-here 32)) new
2142 nil nil 1))
2143 ;; If user moved point during editing, make sure it moves back.
2144 (goto-char opoint)
2145 (todos-remove-item)
2146 (todos-insert-with-overlays new)
2147 (move-to-column item-beg))))))
2c173503
SB
2148
2149;; FIXME: run todos-check-format on exiting buffer (or check for date string
2150;; and indentation)
2151(defun todos-edit-multiline ()
2152 "Set up a buffer for editing a multiline TODO list entry."
2153 (interactive)
2154 (let ((buffer-name (generate-new-buffer-name todos-edit-buffer)))
2155 (switch-to-buffer
2156 (make-indirect-buffer
d04d6b95
SB
2157 (file-name-nondirectory todos-current-todos-file) buffer-name))
2158 (narrow-to-region (todos-item-start) (todos-item-end))
2c173503 2159 (todos-edit-mode)
d04d6b95
SB
2160 (message "Type %s to return to Todos mode."
2161 (key-description (car (where-is-internal 'todos-edit-quit))))))
2c173503
SB
2162
2163(defun todos-edit-quit ()
ee7412e4 2164 ""
2c173503 2165 (interactive)
d04d6b95
SB
2166 (todos-save)
2167 ;; (unlock-buffer)
2168 (kill-buffer)
2c173503
SB
2169 (save-excursion (todos-category-select)))
2170
d04d6b95 2171(defun todos-edit-item-header (&optional part)
ee7412e4 2172 ""
2c173503 2173 (interactive)
ee7412e4 2174 (todos-item-start)
d04d6b95
SB
2175 (re-search-forward (concat todos-date-string-start "\\(?1:" todos-date-pattern
2176 "\\)\\(?2: " diary-time-regexp "\\)?")
ee7412e4 2177 (line-end-position) t)
d04d6b95
SB
2178 (let* ((odate (match-string-no-properties 1))
2179 (otime (match-string-no-properties 2))
2180 (buffer-read-only)
2181 ndate ntime nheader)
2182 (unless (eq part 'timeonly)
2183 (setq ndate (if (save-match-data (string-match "[0-9]+" odate))
2184 (if (y-or-n-p "Change date? ")
2185 (todos-read-date)
2186 (todos-read-dayname))
2187 (if (y-or-n-p "Change day? ")
2188 (todos-read-dayname)
2189 (todos-read-date))))
2190 (replace-match ndate nil nil nil 1))
2191 (unless (eq part 'dateonly)
2192 (setq ntime (save-match-data (todos-read-time)))
2193 (when (< 0 (length ntime)) (setq ntime (concat " " ntime)))
2194 (if otime
2195 (replace-match ntime nil nil nil 2)
2196 (goto-char (match-end 1))
2197 (insert ntime)))))
2198
2199(defun todos-edit-item-date ()
2200 ""
2201 (interactive)
2202 (todos-edit-item-header 'dateonly))
2203
2204(defun todos-edit-item-date-is-today ()
2205 ""
2206 (interactive)
2207 (todos-edit-item-header 'today))
2208
2209(defun todos-edit-item-time ()
2210 ""
2211 (interactive)
2212 (todos-edit-item-header 'timeonly))
2213
2214;; (progn
2215;; (re-search-forward "\\(?1:foo\\)\\(ba\\)\\(?2:z\\)?" nil t)
2216;; (goto-char (point-max))
2217;; (concat (match-string-no-properties 1) ", " (match-string-no-properties 2)))
2218
2219;; foobaz
2220
2c173503 2221
d04d6b95 2222(defun todos-raise-item-priority ()
3f031767
SB
2223 "Raise priority of current entry."
2224 (interactive)
2c173503
SB
2225 (unless (or (todos-done-item-p)
2226 (looking-at "^$")) ; between done and not done items
2227 (let (buffer-read-only)
2228 (if (> (count-lines (point-min) (point)) 0)
2229 (let ((item (todos-item-string)))
d04d6b95
SB
2230 (when (eq major-mode 'todos-top-priorities-mode)
2231 (let ((cat1 (save-excursion
2232 (re-search-forward
2233 (concat todos-date-string-start todos-date-pattern
2234 "\\( " diary-time-regexp
2235 "\\)?\\]?\\(?1: \\[\\(.+:\\)?.+\\]\\)")
2236 nil t)
2237 (match-string 1)))
2238 (cat2 (save-excursion
2239 (todos-backward-item)
2240 (re-search-forward
2241 (concat todos-date-string-start todos-date-pattern
2242 "\\( " diary-time-regexp
2243 "\\)?\\]?\\(?1: \\[\\(.+:\\)?.+\\]\\)")
2244 nil t)
2245 (match-string 1))))
2246 (if (string= cat1 cat2)
2247 (error "Cannot change item's priority in its category; do this in Todos mode"))))
2c173503
SB
2248 (todos-remove-item)
2249 (todos-backward-item)
2250 (todos-insert-with-overlays item))
f730d273 2251 (message "No TODO list entry to raise"))))) ;FIXME: better message
3f031767 2252
d04d6b95 2253(defun todos-lower-item-priority ()
3f031767
SB
2254 "Lower priority of current entry."
2255 (interactive)
2c173503
SB
2256 (unless (or (todos-done-item-p)
2257 (looking-at "^$")) ; between done and not done items
d04d6b95 2258 (let (buffer-read-only)
ee7412e4
SB
2259 (if (save-excursion
2260 ;; can only lower non-final unfinished item
2261 (todos-forward-item)
2262 (and (looking-at todos-item-start)
2263 (not (todos-done-item-p))))
2c173503 2264 ;; Assume there is a final newline
ee7412e4 2265 (let ((item (todos-item-string)))
d04d6b95
SB
2266 (when (eq major-mode 'todos-top-priorities-mode)
2267 (let ((cat1 (save-excursion
2268 (re-search-forward
2269 (concat todos-date-string-start todos-date-pattern
2270 "\\( " diary-time-regexp
2271 "\\)?\\]?\\(?1: \\[\\(.+:\\)?.+\\]\\)")
2272 nil t)
2273 (match-string 1)))
2274 (cat2 (save-excursion
2275 (todos-forward-item)
2276 (re-search-forward
2277 (concat todos-date-string-start todos-date-pattern
2278 "\\( " diary-time-regexp
2279 "\\)?\\]?\\(?1: \\[\\(.+:\\)?.+\\]\\)")
2280 nil t)
2281 (match-string 1))))
2282 (if (string= cat1 cat2)
2283 (error "Cannot change item's priority in its category; do this in Todos mode"))))
2c173503
SB
2284 (todos-remove-item)
2285 (todos-forward-item)
ee7412e4 2286 (when (todos-done-item-p) (forward-line -1))
2c173503 2287 (todos-insert-with-overlays item))
f730d273 2288 (message "No TODO list entry to lower"))))) ;FIXME: better message
3f031767 2289
d04d6b95
SB
2290(defun todos-set-item-priority (item cat)
2291 "Set priority of todo ITEM in category CAT and move item to suit."
2292 (interactive (list (todos-item-string) (todos-current-category)))
2293 (unless (called-interactively-p t)
2294 (todos-category-number cat)
2295 (todos-category-select))
2296 (let* ((todo (todos-get-count 'todo cat))
2297 (maxnum (1+ todo))
2298 (buffer-read-only)
2299 priority candidate prompt)
2300 (unless (zerop todo)
2301 (while (not priority)
2302 (setq candidate
2303 (string-to-number (read-from-minibuffer
2304 (concat prompt
2305 (format "Set item priority (1-%d): "
2306 maxnum)))))
2307 (setq prompt
2308 (when (or (< candidate 1) (> candidate maxnum))
2309 (format "Priority must be an integer between 1 and %d.\n" maxnum)))
2310 (unless prompt (setq priority candidate)))
2311 ;; interactively, just relocate the item within its category
2312 (when (called-interactively-p) (todos-remove-item))
2313 (goto-char (point-min))
2314 (unless (= priority 1) (todos-forward-item (1- priority))))
2315 (todos-insert-with-overlays item)))
2316
2317;; (defun todos-set-item-top-priority ()
2318;; "Set priority of item at point in the top priorities listing."
2319;; (interactive)
2320;; (let* ((item (todos-item-string))
2321;; (cat (save-excursion
2322;; (re-search-forward
2323;; (concat todos-date-string-start todos-date-pattern
2324;; "\\( " diary-time-regexp
2325;; "\\)?\\]?\\(?1: \\[\\(.+:\\)?.+\\]\\)")
2326;; nil t)
2327;; (match-string 1)))
2328;; (opoint (point))
2329;; (count 1)
2330;; (old-priority (save-excursion
2331;; (goto-char (point-min))
2332;; (while (< (point) opoint)
2333;; (todos-forward-item)
2334;; (setq count (1+ count))))))
2335;; )
2336
2337(defun todos-move-item (&optional file)
db2c5d34
SB
2338 "Move the current todo item to another, interactively named, category.
2339
d04d6b95
SB
2340If the named category is not one of the current todo categories,
2341then it is created and the item becomes the first entry in that
2342category.
2343
2344With optional non-nil argument FILE, first ask for another Todos
2345file and then solicit a category within that file to move the
2346item to."
db2c5d34 2347 (interactive)
2c173503
SB
2348 (unless (or (todos-done-item-p)
2349 (looking-at "^$")) ; between done and not done items
2350 (let ((buffer-read-only)
d04d6b95
SB
2351 (modified (buffer-modified-p))
2352 (oldfile todos-current-todos-file)
2c173503 2353 (oldnum todos-category-number)
ee7412e4 2354 (oldcat (todos-current-category))
2c173503 2355 (item (todos-item-string))
d04d6b95
SB
2356 (diary-item (todos-diary-item-p))
2357 (newfile (if file (todos-read-file-name "Choose a Todos file: ")))
2c173503 2358 (opoint (point))
ee7412e4 2359 (orig-mrk (progn (todos-item-start) (point-marker)))
d04d6b95 2360 newcat moved)
2c173503
SB
2361 (unwind-protect
2362 (progn
d04d6b95
SB
2363 (todos-remove-item)
2364 (todos-item-counts oldcat 'delete)
2365 (and diary-item (todos-item-counts oldcat 'nondiary))
2366 (when newfile
2367 (find-file-existing newfile)
2368 (setq todos-current-todos-file newfile
2369 todos-categories (todos-make-categories-list)))
2370 (setq newcat (todos-read-category "Move item to category: "))
2371 (unless (assoc newcat todos-categories) (todos-add-category newcat))
f730d273 2372 (todos-set-item-priority item newcat)
ee7412e4 2373 (setq moved t)
d04d6b95
SB
2374 (todos-item-counts newcat 'insert)
2375 (and diary-item (todos-item-counts newcat 'diary)))
2c173503 2376 (unless moved
d04d6b95
SB
2377 (if newfile
2378 (find-file-existing oldfile)
2379 (setq todos-current-todos-file oldfile
2380 todos-categories (todos-make-categories-list)))
2c173503
SB
2381 (widen)
2382 (goto-char orig-mrk)
2383 (todos-insert-with-overlays item)
2384 (setq todos-category-number oldnum)
d04d6b95
SB
2385 (todos-item-counts oldcat 'insert)
2386 (and diary-item (todos-item-counts oldcat 'diary))
2c173503 2387 (todos-category-select)
d04d6b95 2388 (set-buffer-modified-p modified)
2c173503
SB
2389 (goto-char opoint))
2390 (set-marker orig-mrk nil)))))
2391
d04d6b95
SB
2392(defun todos-move-item-to-file ()
2393 ""
2394 (interactive)
2395 (todos-move-item t))
2396
2c173503
SB
2397(defun todos-item-done ()
2398 "Mark current item as done and move it to category's done section."
b28025ed 2399 (interactive)
2c173503
SB
2400 (unless (or (todos-done-item-p)
2401 (looking-at "^$"))
2402 (let* ((buffer-read-only)
d04d6b95 2403 (cat (todos-current-category))
2c173503 2404 (item (todos-item-string))
d04d6b95 2405 (diary-item (todos-diary-item-p))
2c173503 2406 (date-string (calendar-date-string (calendar-current-date) t t))
ee7412e4 2407 (time-string (if todos-always-add-time-string ;FIXME: delete condition
2c173503
SB
2408 (concat " " (substring (current-time-string) 11 16))
2409 ""))
d04d6b95
SB
2410 ;; FIXME: todos-nondiary-*
2411 (done-item (concat "[" todos-done-string date-string time-string "] "
2412 item)))
2c173503
SB
2413 (todos-remove-item)
2414 (save-excursion
2415 (widen)
d04d6b95
SB
2416 (re-search-forward (concat "^" (regexp-quote todos-category-done)) nil t)
2417 (forward-char)
2418 (todos-insert-with-overlays done-item))
2419 (todos-item-counts cat 'done)
2420 (and diary-item (todos-item-counts cat 'nondiary))
2421 (save-excursion (todos-category-select)))))
2c173503 2422
2c173503 2423(defun todos-item-undo ()
ee7412e4 2424 ""
2c173503
SB
2425 (interactive)
2426 (when (todos-done-item-p)
2427 (let* ((buffer-read-only)
ee7412e4
SB
2428 (cat (todos-current-category))
2429 (done-item (todos-item-string))
2430 (opoint (point))
2431 (orig-mrk (progn (todos-item-start) (point-marker)))
2432 (start (search-forward "] ")) ; end of done date string
2433 (item (buffer-substring start (todos-item-end)))
2434 undone)
2c173503 2435 (todos-remove-item)
ee7412e4
SB
2436 (unwind-protect
2437 (progn
ee7412e4 2438 (todos-set-item-priority item cat)
ee7412e4 2439 (setq undone t)
d04d6b95
SB
2440 (todos-item-counts cat 'undo)
2441 (and (todos-diary-item-p) (todos-item-counts cat 'diary)))
ee7412e4
SB
2442 (unless undone
2443 (widen)
2444 (goto-char orig-mrk)
2445 (todos-insert-with-overlays done-item)
ee7412e4
SB
2446 (let ((todos-show-with-done t))
2447 (todos-category-select)
2448 (goto-char opoint)))
2449 (set-marker orig-mrk nil)))))
2c173503 2450
d04d6b95
SB
2451(defun todos-archive-done-items ()
2452 "Archive the done items in the current category."
2453 (interactive)
2454 (let ((cat (todos-current-category)))
2455 (if (zerop (todos-get-count 'done cat))
2456 (message "No done items in this category")
2457 (when (y-or-n-p "Move all done items in this category to the archive? ")
2458 (let* ((afile (concat (file-name-sans-extension (buffer-file-name)) ".toda"))
2459 (archive (find-file-noselect afile t))
2460 beg end
2461 (buffer-read-only nil))
2462 (save-excursion
2463 (save-restriction
2464 (goto-char (point-min))
2465 (widen)
2466 (setq beg (progn
2467 (re-search-forward todos-done-string-start nil t)
2468 (match-beginning 0)))
2469 (setq end (if (re-search-forward
2470 (concat "^" (regexp-quote todos-category-beg)) nil t)
2471 (match-beginning 0)
2472 (point-max)))
2473 (setq done (buffer-substring beg end))
2474 (with-current-buffer archive
2475 (let (buffer-read-only)
2476 (widen)
2477 (goto-char (point-min))
2478 (if (progn
2479 (re-search-forward
2480 (concat "^" (regexp-quote (concat todos-category-beg cat)))
2481 nil t)
2482 (re-search-forward (regexp-quote todos-category-done) nil t))
2483 (forward-char)
2484 (insert todos-category-beg cat "\n\n" todos-category-done "\n"))
2485 (insert done)
2486 (save-buffer)))
2487 (remove-overlays beg end)
2488 (delete-region beg end)
2489 (todos-item-counts cat 'archive)))))
2490 (message "Done items archived."))))
2491
2492(defun todos-unarchive-category ()
2493 "Restore this archived category to done items in Todos file."
2494 (interactive)
2495 (when (y-or-n-p "Restore all items in this category to Todos file as done items? ")
2496 (let ((buffer-read-only nil)
2497 (tbuf (find-file-noselect
2498 (concat (file-name-sans-extension (buffer-file-name)) ".todo")
2499 t))
2500 (cat (todos-current-category))
2501 (items (buffer-substring (point-min) (point-max))))
2502 (with-current-buffer tbuf
2503 (let (buffer-read-only)
2504 (widen)
2505 (goto-char (point-min))
2506 (re-search-forward (concat "^" (regexp-quote
2507 (concat todos-category-beg cat)))
2508 nil t)
2509 (if (re-search-forward (concat "^" (regexp-quote todos-category-beg))
2510 nil t)
2511 (goto-char (match-beginning 0))
2512 (goto-char (point-max)))
2513 (insert items)))
2514 (widen)
2515 (let ((beg (re-search-backward (concat "^"
2516 (regexp-quote todos-category-beg)
2517 cat) nil t))
2518 (end (if (re-search-forward
2519 (concat "^" (regexp-quote todos-category-beg)) nil t 2)
2520 (match-beginning 0)
2521 (point-max))))
2522 (remove-overlays beg end)
2523 (delete-region beg end))
2524 (goto-char (point-min))
2525 (if (re-search-forward
2526 (concat "^" (regexp-quote todos-category-beg)) nil t)
2527 (progn
2528 ;; delete category from archive
2529 (setq todos-categories (delete (assoc cat todos-categories)
2530 todos-categories))
2531 (todos-update-categories-sexp))
2532 ;; no more categories in archive, so delete it
2533 (set-buffer-modified-p nil) ; no questions
2534 (delete-file (buffer-file-name))
2535 (kill-buffer))
2536 (let ((tfile (buffer-file-name tbuf))
2537 (todos-show-with-done t))
2538 (find-file tfile)
2539 (setq todos-current-todos-file tfile
2540 ;; also updates item counts
2541 todos-categories (todos-make-categories-list t)
2542 todos-category-number (todos-category-number cat))
2543 (todos-show)
2544 (message "Items unarchived.")))))
2545
2c173503 2546(defun todos-toggle-item-diary-inclusion ()
ee7412e4 2547 ""
2c173503
SB
2548 (interactive)
2549 (save-excursion
2550 (let* ((buffer-read-only)
2551 (beg (todos-item-start))
2552 (lim (save-excursion (todos-item-end)))
2553 (end (save-excursion
2554 (or (todos-time-string-match lim)
d04d6b95
SB
2555 (todos-date-string-match lim))))
2556 (cat (todos-current-category)))
2557 (if (looking-at (regexp-quote todos-nondiary-start))
2c173503
SB
2558 (progn
2559 (replace-match "")
d04d6b95
SB
2560 (search-forward todos-nondiary-end (1+ end) t)
2561 (replace-match "")
2562 (todos-item-counts cat 'nondiary))
2c173503 2563 (when end
d04d6b95 2564 (insert todos-nondiary-start)
2c173503 2565 (goto-char (1+ end))
d04d6b95
SB
2566 (insert todos-nondiary-end)
2567 (todos-item-counts cat 'diary))))))
2c173503
SB
2568
2569(defun todos-toggle-diary-inclusion (arg)
ee7412e4 2570 ""
2c173503
SB
2571 (interactive "p")
2572 (save-excursion
2573 (save-restriction
2574 (when (eq arg 2) (widen)) ;FIXME: don't toggle done items
2575 (when (or (eq arg 1) (eq arg 2))
2576 (goto-char (point-min))
2577 (when (eq arg 2)
2578 (re-search-forward (concat "^" (regexp-quote todos-category-beg)) nil t)
2579 (forward-line)
d04d6b95 2580 (when (looking-at (regexp-quote todos-category-done)) (forward-line)))
2c173503
SB
2581 (while (not (eobp))
2582 (todos-toggle-item-diary-inclusion)
2583 (todos-forward-item))))))
3f031767 2584
d04d6b95
SB
2585(defun todos-toggle-item-diary-nonmarking ()
2586 ""
2587 (interactive)
2588 (let ((buffer-read-only))
2589 (save-excursion
2590 (todos-item-start)
2591 (unless (looking-at (regexp-quote todos-nondiary-start))
2592 (if (looking-at (regexp-quote diary-nonmarking-symbol))
2593 (replace-match "")
2594 (insert diary-nonmarking-symbol))))))
2595
2596(defun todos-toggle-diary-nonmarking ()
2597 ""
2598 (interactive)
2599 (save-excursion
2600 (goto-char (point-min))
2601 (while (not (eobp))
2602 (todos-toggle-item-diary-nonmarking)
2603 (todos-forward-item))))
2604
2605;; FIXME: save to a file named according to the current todos file
3f031767
SB
2606(defun todos-save-top-priorities (&optional nof-priorities)
2607 "Save top priorities for each category in `todos-file-top'.
2608
2609Number of entries for each category is given by NOF-PRIORITIES which
2610defaults to `todos-show-priorities'."
2611 (interactive "P")
2612 (save-window-excursion
2613 (save-excursion
2614 (save-restriction
2615 (todos-top-priorities nof-priorities)
2616 (set-buffer todos-tmp-buffer-name)
2617 (write-file todos-file-top)
2618 (kill-this-buffer)))))
2619
d04d6b95
SB
2620;; ;;;###autoload
2621;; (defun todos-print (&optional category-pr-page)
2622;; "Print todo summary using `todos-print-function'.
2623;; If CATEGORY-PR-PAGE is non-nil, a page separator `^L' is inserted
2624;; between each category.
3f031767 2625
d04d6b95
SB
2626;; Number of entries for each category is given by `todos-print-priorities'."
2627;; (interactive "P")
2628;; (when (yes-or-no-p "Print Todos list? ")
2629;; (save-window-excursion
2630;; (save-excursion
2631;; (save-restriction
2632;; (todos-top-priorities todos-print-priorities
2633;; category-pr-page)
2634;; (set-buffer todos-tmp-buffer-name)
2635;; (and (funcall todos-print-function)
2636;; (kill-this-buffer))
2637;; (message "Todo printing done."))))))
2638
2639(defun todos-print ()
2640 ""
2641 (interactive)
2642 (let ((buf (cond ((eq major-mode 'todos-mode)
2643 (concat "Category: " (todos-current-category) " ("
2644 (file-name-nondirectory todos-current-todos-file) ") "))
2645 ((eq major-mode 'todos-top-priorities-mode)
2646 "Todos Top Priorities")))
2647 (prefix (propertize (concat todos-prefix " ") 'face 'todos-prefix-string))
2648 (num 0)
2649 (fill-prefix (make-string todos-indent-to-here 32))
2650 (content (buffer-string)))
2651 (with-current-buffer (get-buffer-create buf)
2652 (insert content)
2653 (goto-char (point-min))
2654 (while (not (eobp))
2655 (let ((beg (point))
2656 (end (save-excursion (todos-item-end))))
2657 (when todos-number-prefix
2658 (setq num (1+ num))
2659 (setq prefix (propertize (concat (number-to-string num) " ")
2660 'face 'todos-prefix-string)))
2661 (insert prefix)
2662 (fill-region beg end))
2663 (todos-forward-item))
2664 ;; FIXME: ask user to choose between sending to printer:
2665 ;; (ps-print-buffer-with-faces)
2666 ;; and printing to a file:
2667 (ps-spool-buffer-with-faces)
2668 ;; (write-file )
2669 )
2670 (kill-buffer buf)))
2c173503
SB
2671
2672;; ---------------------------------------------------------------------------
2673
d04d6b95 2674;;; Internals
2c173503 2675
d04d6b95 2676(defvar todos-date-pattern ;FIXME: start with "^" ?
ee7412e4 2677 (let ((dayname (diary-name-pattern calendar-day-name-array nil t)))
d04d6b95 2678 (concat "\\(?:" dayname "\\|"
ee7412e4 2679 (let ((dayname)
d04d6b95 2680 (monthname (format "\\(?:%s\\|\\*\\)"
ee7412e4
SB
2681 (diary-name-pattern calendar-month-name-array
2682 calendar-month-abbrev-array
2683 t)))
d04d6b95
SB
2684 (month "\\(?:[0-9]+\\|\\*\\)")
2685 (day "\\(?:[0-9]+\\|\\*\\)")
2686 (year "-?\\(?:[0-9]+\\|\\*\\)"))
ee7412e4
SB
2687 (mapconcat 'eval calendar-date-display-form ""))
2688 "\\)"))
2c173503
SB
2689 "Regular expression matching a Todos date header.")
2690
d04d6b95
SB
2691(defvar todos-date-string-start
2692 (concat "^\\(" (regexp-quote todos-nondiary-start) "\\|"
2693 (regexp-quote diary-nonmarking-symbol) "\\)?") ;FIXME: matches anything
2694 "Regular expression matching part of item header before the date.")
2695
2696(defvar todos-done-string-start
2697 (concat "^" (regexp-quote todos-nondiary-start) (regexp-quote todos-done-string))
2698 "Regular expression matching start of done item.")
2699
2700;; FIXME: rename these *-matcher
2c173503 2701(defun todos-date-string-match (lim)
d04d6b95
SB
2702 "Search for Todos date strings within LIM for font-locking."
2703 (re-search-forward (concat todos-date-string-start "\\(?1:"
2704 todos-date-pattern "\\)") lim t))
2c173503
SB
2705
2706(defun todos-time-string-match (lim)
d04d6b95
SB
2707 "Search for Todos time strings within LIM for font-locking."
2708 (re-search-forward (concat todos-date-string-start todos-date-pattern
2709 " \\(?1:" diary-time-regexp "\\)") lim t))
2c173503
SB
2710
2711(defun todos-done-string-match (lim)
d04d6b95
SB
2712 "Search for Todos done headers within LIM for font-locking."
2713 (re-search-forward (concat todos-done-string-start
2714 "[^][]+]")
2715 lim t))
2c173503
SB
2716
2717(defun todos-category-string-match (lim)
d04d6b95
SB
2718 "Search for Todos category headers within LIM for font-locking."
2719 (if (eq major-mode 'todos-top-priorities-mode)
2720 (re-search-forward
2721 ;; (concat "^\\(?1:" (regexp-quote todos-category-beg) ".*\\)$")
2722 (concat "\\(?:^\\[?" todos-date-pattern "\\(?: " diary-time-regexp
2723 "\\)?\\]?\\) \\(?1:\\[.+\\]\\)") lim t)))
2c173503
SB
2724
2725(defun todos-check-format ()
2726 "Signal an error if the current Todos file is ill-formatted."
2727 (save-excursion
2728 (save-restriction
2729 (widen)
2730 (goto-char (point-min))
2731 (let ((legit (concat "^\\(" (regexp-quote todos-category-beg) "\\)"
2732 "\\|\\(\\[?" todos-date-pattern "\\)"
2733 "\\|\\([ \t]+[^ \t]*\\)"
2734 "\\|$")))
2735 (while (not (eobp))
2736 (unless (looking-at legit)
2737 (error "Illegitimate Todos file format at line %d"
2738 (line-number-at-pos (point))))
2739 (forward-line)))))
2740 (message "This Todos file is well-formatted."))
2741
2742(defun todos-wrap-and-indent ()
ee7412e4 2743 ""
2c173503
SB
2744 (make-local-variable 'word-wrap)
2745 (setq word-wrap t)
2746 (make-local-variable 'wrap-prefix)
ee7412e4 2747 (setq wrap-prefix (make-string todos-indent-to-here 32))
2c173503
SB
2748 (unless (member '(continuation) fringe-indicator-alist)
2749 (push '(continuation) fringe-indicator-alist)))
2750
ee7412e4
SB
2751(defun todos-indent ()
2752 ""
2753 (indent-to todos-indent-to-here todos-indent-to-here))
2754
2c173503 2755(defun todos-prefix-overlays ()
ee7412e4 2756 ""
2c173503
SB
2757 (when (or todos-number-prefix
2758 (not (string-match "^[[:space:]]*$" todos-prefix)))
2759 (let ((prefix (propertize (concat todos-prefix " ") 'face 'todos-prefix-string))
ee7412e4 2760 (num 0))
2c173503
SB
2761 (save-excursion
2762 (goto-char (point-min))
ee7412e4
SB
2763 (while (not (eobp))
2764 (when (or (todos-date-string-match (line-end-position))
2765 (todos-done-string-match (line-end-position)))
2766 (goto-char (match-beginning 0))
2767 (when todos-number-prefix
2768 (setq num (1+ num))
2769 ;; reset number for done items
f730d273
SB
2770 (when
2771 ;; FIXME: really need this?
ee7412e4
SB
2772 ;; if last not done item is multiline, then
2773 ;; todos-done-string-match skips empty line, so have
2774 ;; to look back.
d04d6b95
SB
2775 (and (looking-at ;; (concat "^\\[" (regexp-quote todos-done-string))
2776 todos-done-string-start)
2777 (looking-back (concat "^" (regexp-quote todos-category-done)
2778 "\n")))
f730d273 2779 (setq num 1))
ee7412e4
SB
2780 (setq prefix (propertize (concat (number-to-string num) " ")
2781 'face 'todos-prefix-string)))
ee7412e4
SB
2782 (let* ((ovs (overlays-in (point) (point)))
2783 (ov-pref (car ovs))
2784 (val (when ov-pref (overlay-get ov-pref 'before-string))))
d04d6b95 2785 ;; FIXME: is this possible?
ee7412e4
SB
2786 (when (and (> (length ovs) 1)
2787 (not (equal val prefix)))
2788 (setq ov-pref (cadr ovs)))
2789 (when (not (equal val prefix))
d04d6b95 2790 ;; (when ov-pref (delete-overlay ov-pref)) ; why doesn't this work ???
f730d273 2791 (remove-overlays (point) (point)); 'before-string val) ; or this ???
ee7412e4
SB
2792 (setq ov-pref (make-overlay (point) (point)))
2793 (overlay-put ov-pref 'before-string prefix))))
2794 (forward-line))))))
2c173503 2795
f730d273
SB
2796(defun todos-reset-prefix (symbol value)
2797 "Set SYMBOL's value to VALUE, and ." ; FIXME
d04d6b95
SB
2798 (let ((oldvalue (symbol-value symbol))
2799 (files (append todos-files todos-archives)))
f730d273
SB
2800 (custom-set-default symbol value)
2801 (when (not (equal value oldvalue))
d04d6b95
SB
2802 (dolist (f files)
2803 (with-current-buffer (find-file-noselect f)
2804 (save-window-excursion
2805 (todos-show)
2806 (save-excursion
2807 (widen)
2808 (goto-char (point-min))
2809 (while (not (eobp))
2810 (remove-overlays (point) (point)); 'before-string prefix)
2811 (forward-line)))
2812 ;; activate the new setting (save-restriction does not help)
2813 (save-excursion (todos-category-select))))))))
f730d273 2814
2c173503
SB
2815(defun todos-reset-separator (symbol value)
2816 "Set SYMBOL's value to VALUE, and ." ; FIXME
d04d6b95
SB
2817 (let ((oldvalue (symbol-value symbol))
2818 (files (append todos-files todos-archives)))
2c173503 2819 (custom-set-default symbol value)
2c173503 2820 (when (not (equal value oldvalue))
d04d6b95
SB
2821 (dolist (f files)
2822 (with-current-buffer (find-file-noselect f)
2823 (save-window-excursion
2824 (todos-show)
2825 (save-excursion
2826 (goto-char (point-min))
2827 (when (re-search-forward
2828 ;; (concat "^\\[" (regexp-quote todos-done-string))
2829 todos-done-string-start nil t)
2830 (remove-overlays (point) (point))))
2831 ;; activate the new setting (save-restriction does not help)
2832 ;; FIXME: need to wrap in save-excursion ?
2833 (todos-category-select)))))))
2834
2835(defun todos-reset-done-string (symbol value)
2836 "Set SYMBOL's value to VALUE, and ." ; FIXME
2837 ;; (let ((oldvalue (symbol-value symbol)))
2838 ;; (custom-set-default symbol value)
2839 ;; (when (not (equal value oldvalue))
2840 ;; (save-window-excursion
2841 ;; (todos-show)
2842 ;; (save-excursion
2843 ;; (goto-char (point-min))
2844 ;; (when (re-search-forward ;; (concat "^\\[" (regexp-quote todos-done-string))
2845 ;; todos-done-string-start nil t)
2846 ;; (remove-overlays (point) (point))))
2847 ;; ;; activate the new setting (save-restriction does not help)
2848 ;; ;; FIXME: need to wrap in save-excursion ?
2849 ;; (todos-category-select))))
2850 )
2851
2852(defun todos-reset-categories (symbol value)
2853 "Set SYMBOL's value to VALUE, and ." ; FIXME
2854 (custom-set-default symbol value)
2855 (save-window-excursion
2856 (todos-show)
2857 (setq todos-categories
2858 (if value
2859 (todos-truncate-categories-list)
2860 ;; FIXME: with-current-buffer Todos
2861 ;; file and update
2862 ;; todos-categories-sexp
2863 (todos-make-categories-list t)))))
2864 ;; (save-excursion
2865 ;; ;; activate the new setting (save-restriction does not help)
2866 ;; ;; FIXME: need to wrap in save-excursion ?
2867 ;; (todos-category-select)))))
2868
2869(defun todos-toggle-switch-todos-file-noninteractively (symbol value)
2870 ""
2871 (custom-set-default symbol value)
2872 (if value
2873 (add-hook 'post-command-hook
2874 'todos-switch-todos-file nil t)
2875 (remove-hook 'post-command-hook
2876 'todos-switch-todos-file t)))
2877
2878(defun todos-switch-todos-file (&optional file) ;FIXME: need FILE?
2879 "Make another Todos file the current Todos file.
2880Called by post-command-hook if `todos-auto-switch-todos-file' is
2881non-nil (and also in `todos-top-priorities'), it makes the
2882current buffer the current Todos file if it is visiting a Todos
2883file."
2884 (let ((file (or file (buffer-file-name)))
2885 (files (if todos-show-done-only ;FIXME: should only hold for
2886 (funcall todos-files-function t) ; todos-archives
2887 (funcall todos-files-function)))
2888 cat)
2889 (when (and (member file files)
2890 (not (equal todos-current-todos-file file)))
2891 ;; (let ((catbuf (get-buffer todos-categories-buffer)))
2892 ;; (if catbuf (not (eq (other-buffer) catbuf)))))
2893 (if todos-ignore-archived-categories
2894 (progn
2895 (setq todos-categories nil)
2896 (setq todos-categories (todos-truncate-categories-list)))
2897 (setq todos-categories (todos-make-categories-list)))
2898 ;; if file is already in a buffer, redisplay the previous current category
2899 (when (< (- (point-max) (point-min)) (buffer-size))
2900 (widen)
2901 (when (re-search-backward (concat "^" (regexp-quote todos-category-beg)
2902 "\\(.+\\)\n") nil t)
2903 (setq cat (match-string-no-properties 1))
2904 (setq todos-category-number (todos-category-number cat))))
2905 (setq todos-current-todos-file file)
2906 ;; (or todos-category-number (setq todos-category-number 1))
2907 ;; (if (zerop todos-category-number) (setq todos-category-number 1))
2908 (todos-show))))
2c173503 2909
ee7412e4
SB
2910(defun todos-category-number (cat)
2911 "Set todos-category-number to index of CAT in todos-categories."
d04d6b95
SB
2912 (let ((categories (mapcar 'car todos-categories)))
2913 (setq todos-category-number
2914 (1+ (- (length categories)
2915 (length (member cat categories)))))))
2916
2c173503
SB
2917(defun todos-current-category ()
2918 "Return the name of the current category."
d04d6b95 2919 (car (nth (1- todos-category-number) todos-categories)))
2c173503 2920
d04d6b95
SB
2921;; FIXME: wrap in save-excursion (or else have to use todos-show in
2922;; e.g. todos-{forward, backward}-category)
2c173503 2923(defun todos-category-select ()
d04d6b95
SB
2924 "Display the current category correctly.
2925
2926With non-nil `todos-show-with-done' display the category's done
2927\(but not archived) items below the unfinished todo items; else
2928display just the todo items."
2929 (let ((name (todos-current-category))
2930 cat-begin cat-end done-start done-sep-start done-end)
2c173503
SB
2931 (widen)
2932 (goto-char (point-min))
d04d6b95
SB
2933 (re-search-forward
2934 (concat "^" (regexp-quote (concat todos-category-beg name)) "$") nil t)
2935 (setq cat-begin (1+ (line-end-position)))
2936 (setq cat-end (if (re-search-forward
2937 (concat "^" (regexp-quote todos-category-beg)) nil t)
2938 (match-beginning 0)
2939 (point-max)))
2940 (setq mode-line-buffer-identification
2941 (concat (format "Category %d: %s" todos-category-number name)))
2942 (narrow-to-region cat-begin cat-end)
2943 (todos-prefix-overlays)
2944 (goto-char (point-min))
2945 (if (re-search-forward (concat "\n\\(" (regexp-quote todos-category-done)
2946 "\\)") nil t)
2947 (progn
2948 (setq done-start (match-beginning 0))
2949 (setq done-sep-start (match-beginning 1))
2950 (setq done-end (match-end 0)))
2951 (error "Category %s is missing todos-category-done string" name))
2952 (if todos-show-done-only
2953 (narrow-to-region (1+ done-end) (point-max))
2954 ;; display or hide done items as per todos-show-with-done
2955 ;; FIXME: use todos-done-string-start ?
f730d273
SB
2956 (when (re-search-forward (concat "\n\\(\\[" (regexp-quote todos-done-string)
2957 "\\)") nil t)
d04d6b95
SB
2958 (let (done-sep prefix ov-pref ov-done)
2959 ;; FIXME: delete overlay when not viewing done items
2960 (when todos-show-with-done
2961 (setq done-sep todos-done-separator)
2962 (setq done-start cat-end)
2963 (setq ov-pref (make-overlay done-sep-start done-end))
2964 (overlay-put ov-pref 'display done-sep))))
2965 (narrow-to-region (point-min) done-start))))
2c173503
SB
2966
2967(defun todos-insert-with-overlays (item)
ee7412e4
SB
2968 ""
2969 (todos-item-start)
2c173503
SB
2970 (insert item "\n")
2971 (todos-backward-item)
f730d273 2972 (todos-prefix-overlays))
3f031767
SB
2973
2974(defun todos-item-string-start ()
2975 "Return the start of this TODO list entry as a string."
2976 ;; Suitable for putting in the minibuffer when asking the user
2977 (let ((item (todos-item-string)))
2978 (if (> (length item) 60)
2979 (setq item (concat (substring item 0 56) "...")))
2980 item))
2981
d04d6b95
SB
2982(defvar todos-item-start ;; (concat "^\\(\\[\\(" (regexp-quote todos-done-string)
2983 ;; "\\)?\\)?" todos-date-pattern)
2984 (concat "\\(" todos-date-string-start "\\|" todos-done-string-start
2985 "\\)" todos-date-pattern)
ee7412e4
SB
2986 "String identifying start of a Todos item.")
2987
3f031767 2988(defun todos-item-start ()
2c173503
SB
2989 "Move to start of current TODO list item and return its position."
2990 (unless (or (looking-at "^$") ; last item or between done and not done
2991 (looking-at (regexp-quote todos-category-beg))) ; for todos-count-items
2992 (goto-char (line-beginning-position))
ee7412e4 2993 (while (not (looking-at todos-item-start))
d04d6b95
SB
2994 (forward-line -1))
2995 (point)))
3f031767
SB
2996
2997(defun todos-item-end ()
2c173503 2998 "Move to end of current TODO list item and return its position."
ee7412e4
SB
2999 (unless (looking-at "^$") ; FIXME:
3000 (let ((done (todos-done-item-p)))
3001 (todos-forward-item)
3002 ;; adjust if item is last unfinished one before displayed done items
3003 (when (and (not done) (todos-done-item-p))
3004 (forward-line -1))
d04d6b95
SB
3005 (backward-char))
3006 (point)))
3f031767
SB
3007
3008(defun todos-remove-item ()
3009 "Delete the current entry from the TODO list."
ee7412e4
SB
3010 (let* ((beg (todos-item-start))
3011 (end (progn (todos-item-end) (1+ (point))))
2c173503
SB
3012 (ov-start (car (overlays-in beg beg))))
3013 (when ov-start
2c173503
SB
3014 (delete-overlay ov-start))
3015 (delete-region beg end)))
3f031767
SB
3016
3017(defun todos-item-string ()
3018 "Return current TODO list entry as a string."
d04d6b95
SB
3019 (let ((opoint (point))
3020 (start (todos-item-start))
3021 (end (todos-item-end)))
3022 (goto-char opoint)
3023 (and start end (buffer-substring-no-properties start end))))
3024
3025(defun todos-diary-item-p ()
3026 ""
3027 (save-excursion
3028 (todos-item-start)
3029 (looking-at todos-date-pattern)))
3f031767 3030
2c173503 3031(defun todos-done-item-p ()
ee7412e4 3032 ""
2c173503
SB
3033 (save-excursion
3034 (todos-item-start)
d04d6b95 3035 (looking-at todos-done-string-start)))
2c173503 3036
d04d6b95
SB
3037;; FIXME: should be defsubst?
3038(defun todos-counts (cat)
3039 "Plist/Vector of item type counts in category CAT.
3040The counted types are all todo items, todo items for diary
3041inclusion, done items and archived items."
3042 (cdr (assoc cat todos-categories)))
3043
3044(defun todos-get-count (type cat)
3045 "Return count of TYPE items in category CAT."
3046 (let (idx)
3047 (cond ((eq type 'todo)
3048 (setq idx 0))
3049 ((eq type 'diary)
3050 (setq idx 1))
3051 ((eq type 'done)
3052 (setq idx 2))
3053 ((eq type 'archived)
3054 (setq idx 3)))
3055 (aref (todos-counts cat) idx)
3056 ;; (plist-get (todos-counts cat) type)
3057 ))
3058
3059(defun todos-set-count (type counts increment)
3060 "Increment count of item TYPE in vector COUNTS by INCREMENT."
3061 (let (idx)
3062 (cond ((eq type 'todo)
3063 (setq idx 0))
3064 ((eq type 'diary)
3065 (setq idx 1))
3066 ((eq type 'done)
3067 (setq idx 2))
3068 ((eq type 'archived)
3069 (setq idx 3)))
3070 (aset counts idx (+ increment (aref counts idx)))
3071 ;; (plist-put counts type (1+ (plist-get counts type)))
3072 ))
3073
3074(defun todos-set-categories ()
3075 "Set todos-categories from the sexp at the top of the file."
3076 (save-excursion
3077 (save-restriction
3078 (widen)
3079 (goto-char (point-min))
3080 (if (looking-at "\(\(\"")
3081 (setq todos-categories (read (buffer-substring-no-properties
3082 (line-beginning-position)
3083 (line-end-position))))
3084 (error "Invalid or missing todos-categories sexp")))))
3085
3086(defun todos-make-categories-list (&optional force)
3087 "Return a list of Todos categories and their item counts.
3088The items counts are contained in a vector specifying the numbers
3089of todo items, done items and archived items in the category, in
3090that order."
3091 (setq todos-categories nil)
3092 (save-excursion
3093 (save-restriction
3094 (widen)
3095 (goto-char (point-min))
3096 (let (counts cat archive)
3097 ;; FIXME: can todos-archives be too old here?
3098 (unless (member buffer-file-name (funcall todos-files-function t))
3099 (setq archive (concat (file-name-sans-extension
3100 todos-current-todos-file) ".toda")))
3101 (while (not (eobp))
3102 (cond ((looking-at (concat (regexp-quote todos-category-beg)
3103 "\\(.*\\)\n"))
3104 (setq cat (match-string-no-properties 1))
3105 ;; counts for each category: [todo diary done archive]
3106 (setq counts (make-vector 4 0))
3107 ;; (setq counts (list 'todo 0 'diary 0 'done 0 'archived 0))
3108 (setq todos-categories
3109 (append todos-categories (list (cons cat counts))))
3110 ;; todos-archives may be too old here (e.g. during
3111 ;; todos-move-category)
3112 (when (member archive (funcall todos-files-function t))
3113 (with-current-buffer (find-file-noselect archive)
3114 (widen)
3115 (goto-char (point-min))
3116 (when (re-search-forward
3117 (concat (regexp-quote todos-category-beg) cat)
3118 (point-max) t)
3119 (forward-line)
3120 (while (not (or (looking-at
3121 (concat (regexp-quote todos-category-beg)
3122 "\\(.*\\)\n"))
3123 (eobp)))
3124 (when (looking-at todos-done-string-start)
3125 (todos-set-count 'archived counts 1))
3126 (forward-line))))))
3127 ((looking-at todos-done-string-start)
3128 (todos-set-count 'done counts 1))
3129 ((looking-at (concat "^\\(" (regexp-quote diary-nonmarking-symbol)
3130 "\\)?" todos-date-pattern))
3131 (todos-set-count 'diary counts 1)
3132 (todos-set-count 'todo counts 1))
3133 ((looking-at (concat todos-date-string-start todos-date-pattern))
3134 (todos-set-count 'todo counts 1))
3135 ;; if first line is todos-categories list, use it and end loop
3136 ;; unless forced by non-nil parameter `force' to scan whole file
3137 ((bobp)
3138 (unless force
3139 (setq todos-categories (read (buffer-substring-no-properties
3140 (line-beginning-position)
3141 (line-end-position))))
3142 (goto-char (1- (point-max))))))
3143 (forward-line)))))
3144 todos-categories)
3145
3146;; FIXME: don't let truncated list get written by todos-update-categories-sexp
3147(defun todos-truncate-categories-list ()
3148 "Return a truncated list of Todos categories plus item counts.
3149Categories containing only archived items are omitted. This list
3150is used in Todos mode when `todos-ignore-archived-categories' is
3151non-nil."
3152 (let (cats)
3153 (unless todos-categories
3154 (setq todos-categories (todos-make-categories-list)))
3155 (dolist (catcons todos-categories cats)
3156 (let ((cat (car catcons)))
3157 (setq cats
3158 (append cats
3159 (unless (and (zerop (todos-get-count 'todo cat))
3160 (zerop (todos-get-count 'done cat))
3161 (not (zerop (todos-get-count 'archived cat))))
3162 (list catcons))))))))
3163
3164(defun todos-update-categories-sexp ()
3165 ""
3166 (let (buffer-read-only)
2c173503
SB
3167 (save-excursion
3168 (save-restriction
3169 (widen)
3170 (goto-char (point-min))
d04d6b95
SB
3171 (if (looking-at (concat "^" (regexp-quote todos-category-beg)))
3172 (progn (newline) (goto-char (point-min)))
3173 (kill-line))
3174 (prin1 todos-categories (current-buffer))))))
3175
3176;; FIXME: should done diary items count as diary?
3177(defun todos-item-counts (cat &optional type)
3178 ""
3179 (let ((counts (todos-counts cat)))
3180 (cond ((eq type 'insert)
3181 (todos-set-count 'todo counts 1))
3182 ((eq type 'diary)
3183 (todos-set-count 'diary counts 1))
3184 ((eq type 'nondiary)
3185 (todos-set-count 'diary counts -1))
3186 ((eq type 'delete)
3187 ;; FIXME: ok if last done item was deleted?
3188 (if (save-excursion
3189 (re-search-backward (concat "^" (regexp-quote
3190 todos-category-done)) nil t))
3191 (todos-set-count 'done counts -1)
3192 (todos-set-count 'todo counts -1)))
3193 ((eq type 'done)
3194 (todos-set-count 'todo counts -1)
3195 (todos-set-count 'done counts 1))
3196 ((eq type 'undo)
3197 (todos-set-count 'todo counts 1)
3198 (todos-set-count 'done counts -1))
3199 ((eq type 'archive)
3200 (todos-set-count 'archived counts (todos-get-count 'done cat)) ;arch+done
3201 (todos-set-count 'done counts (- (todos-get-count 'done cat))))) ; 0
3202 (todos-update-categories-sexp)))
ee7412e4
SB
3203
3204(defun todos-longest-category-name-length (categories)
2c173503
SB
3205 ""
3206 (let ((longest 0))
ee7412e4 3207 (dolist (c categories longest)
f730d273 3208 (setq longest (max longest (length c))))))
2c173503 3209
3f031767
SB
3210(defun todos-string-count-lines (string)
3211 "Return the number of lines STRING spans."
3212 (length (split-string string "\n")))
3213
3214(defun todos-string-multiline-p (string)
3215 "Return non-nil if STRING spans several lines."
3216 (> (todos-string-count-lines string) 1))
3217
d04d6b95
SB
3218(defun todos-read-file-name (prompt &optional archive)
3219 ""
3220 (unless (file-exists-p todos-files-directory)
3221 (make-directory todos-files-directory))
3222 (let* ((completion-ignore-case t)
3223 (files (mapcar 'file-name-sans-extension
3224 (directory-files todos-files-directory nil
3225 (if archive "\.toda$" "\.todo$"))))
3226 (file (concat todos-files-directory
3227 (completing-read prompt files nil t)
3228 (if archive ".toda" ".todo"))))
3229 (expand-file-name file)))
3230
3231(defun todos-read-category (prompt)
3232 "Return a category name from the current Todos file, with completion.
3233Prompt with PROMPT."
db2c5d34
SB
3234 ;; allow SPC to insert spaces, for adding new category names with
3235 ;; todos-move-item
3236 (let ((map minibuffer-local-completion-map))
3237 (define-key map " " nil)
3238 ;; make a copy of todos-categories in case history-delete-duplicates is
3239 ;; non-nil, which makes completing-read alter todos-categories
3240 (let* ((categories (copy-sequence todos-categories))
3241 (history (cons 'todos-categories (1+ todos-category-number)))
d04d6b95 3242 ;; (default (todos-current-category)) ;FIXME: why this default?
db2c5d34 3243 (completion-ignore-case todos-completion-ignore-case)
d04d6b95
SB
3244 (category (completing-read prompt
3245 ;; (concat "Category [" default "]: ")
3246 todos-categories nil nil nil history))); default)))
db2c5d34
SB
3247 ;; restore the original value of todos-categories
3248 (setq todos-categories categories)
3249 category)))
3f031767 3250
d04d6b95
SB
3251(defun todos-validate-category-name (cat)
3252 "Check new category name CAT and when valid return it."
ee7412e4 3253 (let (prompt)
d04d6b95
SB
3254 (while
3255 (and (cond ((string= "" cat)
3256 (if todos-categories
3257 (setq prompt "Enter a non-empty category name: ")
3258 ;; prompt for initial category of a new Todos file
3259 (setq prompt (concat "Initial category name ["
3260 todos-initial-category "]: "))))
3261 ((string-match "\\`\\s-+\\'" cat)
3262 (setq prompt
3263 "Enter a category name that is not only white space: "))
3264 ((assoc cat todos-categories)
3265 (setq prompt "Enter a non-existing category name: ")))
3266 (setq cat (if todos-categories
3267 (read-from-minibuffer prompt)
3268 ;; offer default initial category name
3269 ;; FIXME: if input is just whitespace, raises "End of
3270 ;; file during parsing" error
3271 (prin1-to-string
3272 (read-from-minibuffer prompt nil nil t nil
3273 (list todos-initial-category))))))))
ee7412e4
SB
3274 cat)
3275
d04d6b95 3276;; adapted from calendar-read-date and calendar-date-string
ee7412e4 3277(defun todos-read-date ()
d04d6b95
SB
3278 "Prompt for Gregorian date and return it in the current format.
3279Also accepts `*' as an unspecified month, day, or year."
ee7412e4 3280 (let* ((year (calendar-read
d04d6b95
SB
3281 "Year (>0 or * for any year): "
3282 (lambda (x) (or (eq x '*) (> x 0)))
ee7412e4
SB
3283 (number-to-string (calendar-extract-year
3284 (calendar-current-date)))))
d04d6b95
SB
3285 (month-array (vconcat calendar-month-name-array (vector "*")))
3286 (abbrevs (vconcat calendar-month-abbrev-array (vector "*")))
ee7412e4 3287 (completion-ignore-case t)
d04d6b95
SB
3288 (monthname (completing-read
3289 "Month name (RET for current month, * for any month): "
3290 (mapcar 'list (append month-array nil))
3291 nil t nil nil
3292 (calendar-month-name (calendar-extract-month
3293 (calendar-current-date)) t)))
ee7412e4 3294 (month (cdr (assoc-string
d04d6b95
SB
3295 monthname (calendar-make-alist month-array nil nil abbrevs))))
3296 (last (if (eq month 13)
3297 31 ; FIXME: what about shorter months?
3298 (let ((yr (if (eq year '*)
3299 1999 ; FIXME: no Feb. 29
3300 year)))
3301 (calendar-last-day-of-month month yr))))
3302 day dayname)
3303 (while (if (numberp day) (or (< day 0) (< last day)) (not (eq day '*)))
ee7412e4 3304 (setq day (read-from-minibuffer
d04d6b95
SB
3305 (format "Day (1-%d or RET for today or * for any day): " last)
3306 nil nil t nil
3307 (number-to-string
3308 (calendar-extract-day (calendar-current-date))))))
3309 (setq year (if (eq year '*) (symbol-name '*) (number-to-string year)))
3310 (setq day (if (eq day '*) (symbol-name '*) (number-to-string day)))
3311 ;; FIXME: make abbreviation customizable
3312 (setq monthname
3313 (calendar-month-name (calendar-extract-month (list month day year)) t))
3314 (mapconcat 'eval calendar-date-display-form "")))
ee7412e4
SB
3315
3316(defun todos-read-dayname ()
3317 ""
3318 (let ((completion-ignore-case t))
3319 (completing-read "Enter a day name: "
3320 (append calendar-day-name-array nil)
3321 nil t)))
3322
3323(defun todos-read-time ()
3324 ""
3325 (let (valid answer)
3326 (while (not valid)
3327 (setq answer (read-from-minibuffer
f730d273 3328 "Enter a clock time (or return for none): "))
ee7412e4
SB
3329 (when (or (string= "" answer)
3330 (string-match diary-time-regexp answer))
3331 (setq valid t)))
3332 answer))
3333
ee7412e4
SB
3334(defun todos-padded-string (str)
3335 ""
d04d6b95
SB
3336 (let* ((categories (mapcar 'car todos-categories))
3337 (len (todos-longest-category-name-length categories))
ee7412e4
SB
3338 (strlen (length str))
3339 (strlen-odd (eq (logand strlen 1) 1)) ; oddp from cl.el
d04d6b95
SB
3340 (padding (max 0 (/ (- len strlen) 2)))
3341 (padding-left (cond ((eq todos-categories-align 'left) 0)
3342 ((eq todos-categories-align 'center) padding)
3343 ((eq todos-categories-align 'right)
3344 (if strlen-odd (1+ (* padding 2)) (* padding 2)))))
3345 (padding-right (cond ((eq todos-categories-align 'left)
3346 (if strlen-odd (1+ (* padding 2)) (* padding 2)))
3347 ((eq todos-categories-align 'center)
3348 (if strlen-odd (1+ padding) padding))
3349 ((eq todos-categories-align 'right) 0))))
3350 (concat (make-string padding-left 32) str (make-string padding-right 32))))
3351
3352(defvar todos-descending-counts-store nil
3353 "Alist of current sorted category counts, keyed by sort key.")
3354
3355;; FIXME: rename to todos-insert-category-info ?
3356(defun todos-sort (list &optional key)
3357 "Return a copy of LIST, possibly sorted according to KEY." ;FIXME
3358 (let* ((l (copy-sequence list))
3359 (fn (if (eq key 'alpha)
3360 (lambda (x) (upcase x)) ;alphabetize case insensitively
3361 (lambda (x) (todos-get-count key x))))
3362 (descending (member key todos-descending-counts-store))
3363 (cmp (if (eq key 'alpha)
3364 'string<
3365 (if descending '< '>)))
3366 (pred (lambda (s1 s2) (let ((t1 (funcall fn (car s1)))
3367 (t2 (funcall fn (car s2))))
3368 (funcall cmp t1 t2)))))
3369 (when key
3370 (setq l (sort l pred))
3371 (if descending
3372 (setq todos-descending-counts-store
3373 (delete key todos-descending-counts-store))
3374 (push key todos-descending-counts-store)))
3375 l))
3376
3377(defun todos-display-sorted (type)
3378 "Keep point on the count sorting button just clicked."
3379 (let ((opoint (point)))
3380 (todos-display-categories type)
3381 (goto-char opoint)))
3382
3383(defun todos-label-to-key (label)
3384 "Return symbol for sort key associated with LABEL."
3385 (let (key)
3386 (cond ((string= label todos-categories-category-label)
3387 (setq key 'alpha))
3388 ((string= label todos-categories-todo-label)
3389 (setq key 'todo))
3390 ((string= label todos-categories-diary-label)
3391 (setq key 'diary))
3392 ((string= label todos-categories-done-label)
3393 (setq key 'done))
3394 ((string= label todos-categories-archived-label)
3395 (setq key 'archived)))
3396 key))
3397
3398(defun todos-insert-sort-button (label)
ee7412e4 3399 ""
d04d6b95
SB
3400 (setq str (if (string= label todos-categories-category-label)
3401 (todos-padded-string label)
3402 label))
3403 (setq beg (point))
3404 (setq end (+ beg (length str)))
3405 (insert-button str 'face nil
3406 'action
3407 `(lambda (button)
3408 (let ((key (todos-label-to-key ,label)))
3409 (if (and (member key todos-descending-counts-store)
3410 (eq key 'alpha))
3411 (progn
3412 (todos-display-categories)
3413 (setq todos-descending-counts-store
3414 (delete key todos-descending-counts-store)))
3415 (todos-display-sorted key)))))
3416 (setq ovl (make-overlay beg end))
3417 (overlay-put ovl 'face 'todos-button))
3418
3419(defun todos-insert-category-line (cat &optional nonum)
3420 ""
3421 (let ((archive (member todos-current-todos-file todos-archives))
3422 (str (todos-padded-string cat))
3423 (opoint (point)))
3424 ;; beg end ovl)
ee7412e4
SB
3425 ;; num is declared in caller
3426 (setq num (1+ num))
d04d6b95
SB
3427 ;; (if nonum
3428 ;; (insert (make-string 4 32))
3429 ;; (insert " " (format "%2d" num) " | "))
3430 ;; (setq beg (point))
3431 ;; (setq end (+ beg (length str)))
3432 (insert-button
3433 ;; FIXME: use mapconcat?
3434 (concat (if nonum
3435 (make-string (+ 3 (length todos-categories-number-separator)) 32)
3436 (format " %2d%s" num todos-categories-number-separator))
3437 str
3438 (make-string (+ 2 (/ (length todos-categories-todo-label) 2)) 32)
3439 (unless archive
3440 (concat
3441 (format "%2d" (todos-get-count 'todo cat))
3442 (make-string (+ 2 (/ (length todos-categories-diary-label) 2)) 32)))
3443 (unless archive
3444 (concat
3445 (format "%2d" (todos-get-count 'diary cat))
3446 (make-string (+ 3 (/ (length todos-categories-done-label) 2)) 32)))
3447 (format "%2d" (todos-get-count 'done cat))
3448 (unless archive
3449 (concat
3450 (make-string (+ 2 (/ (length todos-categories-archived-label) 2)) 32)
3451 (format "%2d" (todos-get-count 'archived cat))
3452 (make-string 2 32))))
3453 'face (if (and todos-ignore-archived-categories
3454 (zerop (todos-get-count 'todo cat))
3455 (zerop (todos-get-count 'done cat))
3456 (not (zerop (todos-get-count 'archived cat))))
3457 'todos-archived-only
3458 nil)
3459 'action `(lambda (button) (todos-jump-to-category ,cat)))
3460 ;; (setq ovl (make-overlay beg end))
3461 ;; (overlay-put ovl 'face 'todos-button)
3462 (let* ((beg1 (+ opoint 6 (length str)))
3463 end1 ovl1)
3464 (cond ((eq nonum 'todo)
3465 (setq beg1 (+ beg1 1 (/ (length todos-categories-todo-label) 2))))
3466 ((eq nonum 'diary)
3467 (setq beg1 (+ beg1 1 (length todos-categories-todo-label)
3468 2 (/ (length todos-categories-diary-label) 2))))
3469 ((eq nonum 'done)
3470 (setq beg1 (+ beg1 1 (length todos-categories-todo-label)
3471 2 (length todos-categories-diary-label)
3472 2 (/ (length todos-categories-done-label) 2))))
3473 ((eq nonum 'archived)
3474 (setq beg1 (+ beg1 1 (length todos-categories-todo-label)
3475 2 (length todos-categories-diary-label)
3476 2 (length todos-categories-done-label)
3477 2 (/ (length todos-categories-archived-label) 2)))))
3478 (unless (= beg1 (+ opoint 6 (length str)))
3479 (setq end1 (+ beg1 4))
3480 (setq ovl1 (make-overlay beg1 end1))
3481 (overlay-put ovl1 'face 'todos-sorted-column)))
3482 (insert (concat "\n"))))
3f031767
SB
3483
3484(provide 'todos)
3485
f730d273
SB
3486;;; UI
3487;; - display
3488;; - show todos in cat
3489;; - show done in cat
3490;; - show catlist
3491;; - show top priorities in all cats
3492;; - show archived
3493;; - navigation
3494;; -
3495;; - editing
3496;;
3497;;; Internals
3498;; - cat props: name, number, todos, done, archived
3499;; - item props: priority, date-time, status?
3500;; - file format
3501;; - cat begin
3502;; - todo items 0...n
3503;; - empty line
3504;; - done-separator
3505;; - done item 0...n
3506
3f031767 3507;;; todos.el ends here