| 1 | ;;; todo-mode.el --- major mode for editing TODO list files |
| 2 | |
| 3 | ;; Copyright (C) 1997, 1999, 2001, 2002, 2003, 2004, 2005, 2006, 2007 |
| 4 | ;; Free Software Foundation, Inc. |
| 5 | |
| 6 | ;; Author: Oliver Seidel <os10000@seidel-space.de> |
| 7 | ;; [Not clear the above works, July 2000] |
| 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, or (at your option) |
| 16 | ;; 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; see the file COPYING. If not, write to the |
| 25 | ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| 26 | ;; Boston, MA 02110-1301, USA. |
| 27 | |
| 28 | ;; --------------------------------------------------------------------------- |
| 29 | |
| 30 | ;;; Commentary: |
| 31 | |
| 32 | ;; Mode Description |
| 33 | ;; |
| 34 | ;; TODO is a major mode for EMACS which offers functionality to |
| 35 | ;; treat most lines in one buffer as a list of items one has to |
| 36 | ;; do. There are facilities to add new items, which are |
| 37 | ;; categorised, to edit or even delete items from the buffer. |
| 38 | ;; The buffer contents are currently compatible with the diary, |
| 39 | ;; so that the list of todo-items will show up in the FANCY diary |
| 40 | ;; mode. |
| 41 | ;; |
| 42 | ;; Notice: Besides the major mode, this file also exports the |
| 43 | ;; function `todo-show' which will change to the one specific |
| 44 | ;; TODO file that has been specified in the todo-file-do |
| 45 | ;; variable. If this file does not conform to the TODO mode |
| 46 | ;; conventions, the todo-show function will add the appropriate |
| 47 | ;; header and footer. I don't anticipate this to cause much |
| 48 | ;; grief, but be warned, in case you attempt to read a plain text |
| 49 | ;; file. |
| 50 | ;; |
| 51 | ;; Preface, Quickstart Installation |
| 52 | ;; |
| 53 | ;; To get this to work, make emacs execute the line |
| 54 | ;; |
| 55 | ;; (autoload 'todo-mode "todo-mode" |
| 56 | ;; "Major mode for editing TODO lists." t) |
| 57 | ;; (autoload 'todo-show "todo-mode" |
| 58 | ;; "Show TODO items." t) |
| 59 | ;; (autoload 'todo-insert-item "todo-mode" |
| 60 | ;; "Add TODO item." t) |
| 61 | ;; |
| 62 | ;; You may now enter new items by typing "M-x todo-insert-item", |
| 63 | ;; or enter your TODO list file by typing "M-x todo-show". |
| 64 | ;; |
| 65 | ;; The TODO list file has a special format and some auxiliary |
| 66 | ;; information, which will be added by the todo-show function if |
| 67 | ;; it attempts to visit an un-initialised file. Hence it is |
| 68 | ;; recommended to use the todo-show function for the first time, |
| 69 | ;; in order to initialise the file, but it is not necessary |
| 70 | ;; afterwards. |
| 71 | ;; |
| 72 | ;; As these commands are quite long to type, I would recommend |
| 73 | ;; the addition of two bindings to your to your global keymap. I |
| 74 | ;; personally have the following in my initialisation file: |
| 75 | ;; |
| 76 | ;; (global-set-key "\C-ct" 'todo-show) ;; switch to TODO buffer |
| 77 | ;; (global-set-key "\C-ci" 'todo-insert-item) ;; insert new item |
| 78 | ;; |
| 79 | ;; Note, however, that this recommendation has prompted some |
| 80 | ;; criticism, since the keys C-c LETTER are reserved for user |
| 81 | ;; functions. I believe my recommendation is acceptable, since |
| 82 | ;; the Emacs Lisp Manual *Tips* section also details that the |
| 83 | ;; mode itself should not bind any functions to those keys. The |
| 84 | ;; express aim of the above two bindings is to work outside the |
| 85 | ;; mode, which doesn't need the show function and offers a |
| 86 | ;; different binding for the insert function. They serve as |
| 87 | ;; shortcuts and are not even needed (since the TODO mode will be |
| 88 | ;; entered by visiting the TODO file, and later by switching to |
| 89 | ;; its buffer). |
| 90 | ;; |
| 91 | ;; If you are an advanced user of this package, please consult |
| 92 | ;; the whole source code for autoloads, because there are several |
| 93 | ;; extensions that are not explicitly listed in the above quick |
| 94 | ;; installation. |
| 95 | ;; |
| 96 | ;; Pre-Requisites |
| 97 | ;; |
| 98 | ;; This package will require the following packages to be |
| 99 | ;; available on the load-path: |
| 100 | ;; |
| 101 | ;; time-stamp |
| 102 | ;; easymenu |
| 103 | ;; |
| 104 | ;; Operation |
| 105 | ;; |
| 106 | ;; You will have the following facilities available: |
| 107 | ;; |
| 108 | ;; M-x todo-show will enter the todo list screen, here type |
| 109 | ;; |
| 110 | ;; + to go to next category |
| 111 | ;; - to go to previous category |
| 112 | ;; d to file the current entry, including a |
| 113 | ;; comment and timestamp |
| 114 | ;; e to edit the current entry |
| 115 | ;; E to edit a multi-line entry |
| 116 | ;; f to file the current entry, including a |
| 117 | ;; comment and timestamp |
| 118 | ;; i to insert a new entry, with prefix, omit category |
| 119 | ;; I to insert a new entry at current cursor position |
| 120 | ;; j jump to category |
| 121 | ;; k to kill the current entry |
| 122 | ;; l to lower the current entry's priority |
| 123 | ;; n for the next entry |
| 124 | ;; p for the previous entry |
| 125 | ;; P print |
| 126 | ;; q to save the list and exit the buffer |
| 127 | ;; r to raise the current entry's priority |
| 128 | ;; s to save the list |
| 129 | ;; S to save the list of top priorities |
| 130 | ;; t show top priority items for each category |
| 131 | ;; |
| 132 | ;; When you add a new entry, you are asked for the text and then |
| 133 | ;; for the category. I for example have categories for things |
| 134 | ;; that I want to do in the office (like mail my mum), that I |
| 135 | ;; want to do in town (like buy cornflakes) and things I want to |
| 136 | ;; do at home (move my suitcases). The categories can be |
| 137 | ;; selected with the cursor keys and if you type in the name of a |
| 138 | ;; category which didn't exist before, an empty category of the |
| 139 | ;; desired name will be added and filled with the new entry. |
| 140 | ;; |
| 141 | ;; Configuration |
| 142 | ;; |
| 143 | ;; Variable todo-prefix |
| 144 | ;; |
| 145 | ;; I would like to recommend that you use the prefix "*/*" (by |
| 146 | ;; leaving the variable 'todo-prefix' untouched) so that the |
| 147 | ;; diary displays each entry every day. |
| 148 | ;; |
| 149 | ;; To understand what I mean, please read the documentation that |
| 150 | ;; goes with the calendar since that will tell you how you can |
| 151 | ;; set up the fancy diary display and use the #include command to |
| 152 | ;; include your todo list file as part of your diary. |
| 153 | ;; |
| 154 | ;; If you have the diary package set up to usually display more |
| 155 | ;; than one day's entries at once, consider using |
| 156 | ;; |
| 157 | ;; "&%%(equal (calendar-current-date) date)" |
| 158 | ;; |
| 159 | ;; as the value of `todo-prefix'. Please note that this may slow |
| 160 | ;; down the processing of your diary file some. |
| 161 | ;; |
| 162 | ;; Carsten Dominik <dominik@strw.LeidenUniv.nl> suggested that |
| 163 | ;; |
| 164 | ;; "&%%(todo-cp)" |
| 165 | ;; |
| 166 | ;; might be nicer and to that effect a function has been declared |
| 167 | ;; further down in the code. You may wish to auto-load this. |
| 168 | ;; |
| 169 | ;; Carsten also writes that that *changing* the prefix after the |
| 170 | ;; todo list is already established is not as simple as changing |
| 171 | ;; the variable - the todo files have to be changed by hand. |
| 172 | ;; |
| 173 | ;; Variable todo-file-do |
| 174 | ;; |
| 175 | ;; This variable is fairly self-explanatory. You have to store |
| 176 | ;; your TODO list somewhere. This variable tells the package |
| 177 | ;; where to go and find this file. |
| 178 | ;; |
| 179 | ;; Variable todo-file-done |
| 180 | ;; |
| 181 | ;; Even when you're done, you may wish to retain the entries. |
| 182 | ;; Given that they're timestamped and you are offered to add a |
| 183 | ;; comment, this can make a useful diary of past events. It will |
| 184 | ;; even blend in with the EMACS diary package. So anyway, this |
| 185 | ;; variable holds the name of the file for the filed todo-items. |
| 186 | ;; |
| 187 | ;; Variable todo-file-top |
| 188 | ;; |
| 189 | ;; File storing the top priorities of your TODO list when |
| 190 | ;; todo-save-top-priorities is non-nil. Nice to include in your |
| 191 | ;; diary instead of the complete TODO list. |
| 192 | ;; |
| 193 | ;; Variable todo-mode-hook |
| 194 | ;; |
| 195 | ;; Just like other modes, too, this mode offers to call your |
| 196 | ;; functions before it goes about its business. This variable |
| 197 | ;; will be inspected for any functions you may wish to have |
| 198 | ;; called once the other TODO mode preparations have been |
| 199 | ;; completed. |
| 200 | ;; |
| 201 | ;; Variable todo-insert-threshold |
| 202 | ;; |
| 203 | ;; Another nifty feature is the insertion accuracy. If you have |
| 204 | ;; 8 items in your TODO list, then you may get asked 4 questions |
| 205 | ;; by the binary insertion algorithm. However, you may not |
| 206 | ;; really have a need for such accurate priorities amongst your |
| 207 | ;; TODO items. If you now think about the binary insertion |
| 208 | ;; halving the size of the window each time, then the threshold |
| 209 | ;; is the window size at which it will stop. If you set the |
| 210 | ;; threshold to zero, the upper and lower bound will coincide at |
| 211 | ;; the end of the loop and you will insert your item just before |
| 212 | ;; that point. If you set the threshold to, e.g. 8, it will stop |
| 213 | ;; as soon as the window size drops below that amount and will |
| 214 | ;; insert the item in the approximate centre of that window. I |
| 215 | ;; got the idea for this feature after reading a very helpful |
| 216 | ;; e-mail reply from Trey Jackson <trey@cs.berkeley.edu> who |
| 217 | ;; corrected some of my awful coding and pointed me towards some |
| 218 | ;; good reading. Thanks Trey! |
| 219 | ;; |
| 220 | ;; Things to do |
| 221 | ;; |
| 222 | ;; These originally were my ideas, but now also include all the |
| 223 | ;; suggestions that I included before forgetting them: |
| 224 | ;; |
| 225 | ;; o Fancy fonts for todo/top-priority buffer |
| 226 | ;; o Remove todo-prefix option in todo-top-priorities |
| 227 | ;; o Rename category |
| 228 | ;; o Move entry from one category to another one |
| 229 | ;; o Entries which both have the generic */* prefix and a |
| 230 | ;; "deadline" entry which are understood by diary, indicating |
| 231 | ;; an event (unless marked by &) |
| 232 | ;; o The optional COUNT variable of todo-forward-item should be |
| 233 | ;; applied to the other functions performing similar tasks |
| 234 | ;; o Modularization could be done for repeated elements of |
| 235 | ;; the code, like the completing-read lines of code. |
| 236 | ;; o license / version function |
| 237 | ;; o export to diary file |
| 238 | ;; o todo-report-bug |
| 239 | ;; o GNATS support |
| 240 | ;; o elide multiline (as in bbdb, or, to a lesser degree, in |
| 241 | ;; outline mode) |
| 242 | ;; o rewrite complete package to store data as lisp objects |
| 243 | ;; and have display modes for display, for diary export, |
| 244 | ;; etc. (Richard Stallman pointed out this is a bad idea) |
| 245 | ;; o so base todo-mode.el on generic-mode.el instead |
| 246 | ;; |
| 247 | ;; History and Gossip |
| 248 | ;; |
| 249 | ;; Many thanks to all the ones who have contributed to the |
| 250 | ;; evolution of this package! I hope I have listed all of you |
| 251 | ;; somewhere in the documentation or at least in the RCS history! |
| 252 | ;; |
| 253 | ;; Enjoy this package and express your gratitude by sending nice |
| 254 | ;; things to my parents' address! |
| 255 | ;; |
| 256 | ;; Oliver Seidel |
| 257 | ;; (Lessingstr. 8, 65760 Eschborn, Federal Republic of Germany) |
| 258 | |
| 259 | ;;; Code: |
| 260 | |
| 261 | (require 'time-stamp) |
| 262 | |
| 263 | |
| 264 | ;; User-configurable variables: |
| 265 | |
| 266 | (defgroup todo nil |
| 267 | "Maintain a list of todo items." |
| 268 | :link '(emacs-commentary-link "todo-mode") |
| 269 | :version "21.1" |
| 270 | :group 'calendar) |
| 271 | |
| 272 | (defcustom todo-prefix "*/*" |
| 273 | "*TODO mode prefix for entries. |
| 274 | |
| 275 | This is useful in conjunction with `calendar' and `diary' if you use |
| 276 | |
| 277 | #include \"~/.todo-do\" |
| 278 | |
| 279 | in your diary file to include your todo list file as part of your |
| 280 | diary. With the default value \"*/*\" the diary displays each entry |
| 281 | every day and it may also be marked on every day of the calendar. |
| 282 | Using \"&%%(equal (calendar-current-date) date)\" instead will only |
| 283 | show and mark todo entries for today, but may slow down processing of |
| 284 | the diary file somewhat." |
| 285 | :type 'string |
| 286 | :group 'todo) |
| 287 | (defcustom todo-file-do "~/.todo-do" |
| 288 | "*TODO mode list file." |
| 289 | :type 'file |
| 290 | :group 'todo) |
| 291 | (defcustom todo-file-done "~/.todo-done" |
| 292 | "*TODO mode archive file." |
| 293 | :type 'file |
| 294 | :group 'todo) |
| 295 | (defcustom todo-mode-hook nil |
| 296 | "*TODO mode hooks." |
| 297 | :type 'hook |
| 298 | :group 'todo) |
| 299 | (defcustom todo-edit-mode-hook nil |
| 300 | "*TODO Edit mode hooks." |
| 301 | :type 'hook |
| 302 | :group 'todo) |
| 303 | (defcustom todo-insert-threshold 0 |
| 304 | "*TODO mode insertion accuracy. |
| 305 | |
| 306 | If you have 8 items in your TODO list, then you may get asked 4 |
| 307 | questions by the binary insertion algorithm. However, you may not |
| 308 | really have a need for such accurate priorities amongst your TODO |
| 309 | items. If you now think about the binary insertion halving the size |
| 310 | of the window each time, then the threshold is the window size at |
| 311 | which it will stop. If you set the threshold to zero, the upper and |
| 312 | lower bound will coincide at the end of the loop and you will insert |
| 313 | your item just before that point. If you set the threshold to, |
| 314 | e.g. 8, it will stop as soon as the window size drops below that |
| 315 | amount and will insert the item in the approximate center of that |
| 316 | window." |
| 317 | :type 'integer |
| 318 | :group 'todo) |
| 319 | (defvar todo-edit-buffer " *TODO Edit*" |
| 320 | "TODO Edit buffer name.") |
| 321 | (defcustom todo-file-top "~/.todo-top" |
| 322 | "*TODO mode top priorities file. |
| 323 | |
| 324 | Not in TODO format, but diary compatible. |
| 325 | Automatically generated when `todo-save-top-priorities' is non-nil." |
| 326 | :type 'string |
| 327 | :group 'todo) |
| 328 | |
| 329 | (defcustom todo-print-function 'ps-print-buffer-with-faces |
| 330 | "*Function to print the current buffer." |
| 331 | :type 'symbol |
| 332 | :group 'todo) |
| 333 | (defcustom todo-show-priorities 1 |
| 334 | "*Default number of priorities to show by \\[todo-top-priorities]. |
| 335 | 0 means show all entries." |
| 336 | :type 'integer |
| 337 | :group 'todo) |
| 338 | (defcustom todo-print-priorities 0 |
| 339 | "*Default number of priorities to print by \\[todo-print]. |
| 340 | 0 means print all entries." |
| 341 | :type 'integer |
| 342 | :group 'todo) |
| 343 | (defcustom todo-remove-separator t |
| 344 | "*Non-nil to remove category separators in\ |
| 345 | \\[todo-top-priorities] and \\[todo-print]." |
| 346 | :type 'boolean |
| 347 | :group 'todo) |
| 348 | (defcustom todo-save-top-priorities-too t |
| 349 | "*Non-nil makes `todo-save' automatically save top-priorities in `todo-file-top'." |
| 350 | :type 'boolean |
| 351 | :group 'todo) |
| 352 | |
| 353 | ;; Thanks for the ISO time stamp format go to Karl Eichwalder <ke@suse.de> |
| 354 | ;; My format string for the appt.el package is "%3b %2d, %y, %02I:%02M%p". |
| 355 | ;; |
| 356 | (defcustom todo-time-string-format |
| 357 | "%:y-%02m-%02d %02H:%02M" |
| 358 | "*TODO mode time string format for done entries. |
| 359 | For details see the variable `time-stamp-format'." |
| 360 | :type 'string |
| 361 | :group 'todo) |
| 362 | |
| 363 | (defcustom todo-entry-prefix-function 'todo-entry-timestamp-initials |
| 364 | "*Function producing text to insert at start of todo entry." |
| 365 | :type 'symbol |
| 366 | :group 'todo) |
| 367 | (defcustom todo-initials (or (getenv "INITIALS") (user-login-name)) |
| 368 | "*Initials of todo item author." |
| 369 | :type 'string |
| 370 | :group 'todo) |
| 371 | |
| 372 | (defun todo-entry-timestamp-initials () |
| 373 | "Prepend timestamp and your initials to the head of a TODO entry." |
| 374 | (let ((time-stamp-format todo-time-string-format)) |
| 375 | (concat (time-stamp-string) " " todo-initials ": "))) |
| 376 | |
| 377 | ;; --------------------------------------------------------------------------- |
| 378 | |
| 379 | ;; Set up some helpful context ... |
| 380 | |
| 381 | (defvar todo-categories nil |
| 382 | "TODO categories.") |
| 383 | |
| 384 | (defvar todo-cats nil |
| 385 | "Old variable for holding the TODO categories. |
| 386 | Use `todo-categories' instead.") |
| 387 | |
| 388 | (defvar todo-previous-line 0 |
| 389 | "Previous line asked about.") |
| 390 | |
| 391 | (defvar todo-previous-answer 0 |
| 392 | "Previous answer got.") |
| 393 | |
| 394 | (defvar todo-mode-map |
| 395 | (let ((map (make-keymap))) |
| 396 | (suppress-keymap map t) |
| 397 | (define-key map "+" 'todo-forward-category) |
| 398 | (define-key map "-" 'todo-backward-category) |
| 399 | (define-key map "d" 'todo-file-item) ;done/delete |
| 400 | (define-key map "e" 'todo-edit-item) |
| 401 | (define-key map "E" 'todo-edit-multiline) |
| 402 | (define-key map "f" 'todo-file-item) |
| 403 | (define-key map "i" 'todo-insert-item) |
| 404 | (define-key map "I" 'todo-insert-item-here) |
| 405 | (define-key map "j" 'todo-jump-to-category) |
| 406 | (define-key map "k" 'todo-delete-item) |
| 407 | (define-key map "l" 'todo-lower-item) |
| 408 | (define-key map "n" 'todo-forward-item) |
| 409 | (define-key map "p" 'todo-backward-item) |
| 410 | (define-key map "P" 'todo-print) |
| 411 | (define-key map "q" 'todo-quit) |
| 412 | (define-key map "r" 'todo-raise-item) |
| 413 | (define-key map "s" 'todo-save) |
| 414 | (define-key map "S" 'todo-save-top-priorities) |
| 415 | (define-key map "t" 'todo-top-priorities) |
| 416 | map) |
| 417 | "TODO mode keymap.") |
| 418 | |
| 419 | (defvar todo-category-number 0 "TODO category number.") |
| 420 | |
| 421 | (defvar todo-tmp-buffer-name " *todo tmp*") |
| 422 | |
| 423 | (defvar todo-category-sep (make-string 75 ?-) |
| 424 | "Category separator.") |
| 425 | |
| 426 | (defvar todo-category-beg " --- " |
| 427 | "Category start separator to be prepended onto category name.") |
| 428 | |
| 429 | (defvar todo-category-end "--- End" |
| 430 | "Separator after a category.") |
| 431 | |
| 432 | (defvar todo-header "-*- mode: todo; " |
| 433 | "Header of todo files.") |
| 434 | |
| 435 | ;; --------------------------------------------------------------------------- |
| 436 | |
| 437 | (defun todo-category-select () |
| 438 | "Make TODO mode display the current category correctly." |
| 439 | (let ((name (nth todo-category-number todo-categories))) |
| 440 | (setq mode-line-buffer-identification |
| 441 | ;; (concat "Category: " name)) |
| 442 | (concat "Category: " (format "%18s" name))) |
| 443 | (widen) |
| 444 | (goto-char (point-min)) |
| 445 | (search-forward-regexp |
| 446 | (concat "^" |
| 447 | (regexp-quote (concat todo-prefix todo-category-beg name)) |
| 448 | "$")) |
| 449 | (let ((begin (1+ (line-end-position)))) |
| 450 | (search-forward-regexp (concat "^" todo-category-end)) |
| 451 | (narrow-to-region begin (line-beginning-position)) |
| 452 | (goto-char (point-min))))) |
| 453 | (defalias 'todo-cat-slct 'todo-category-select) |
| 454 | |
| 455 | (defun todo-forward-category () |
| 456 | "Go forward to TODO list of next category." |
| 457 | (interactive) |
| 458 | (setq todo-category-number |
| 459 | (mod (1+ todo-category-number) (length todo-categories))) |
| 460 | (todo-category-select)) |
| 461 | (defalias 'todo-cmd-forw 'todo-forward-category) |
| 462 | |
| 463 | (defun todo-backward-category () |
| 464 | "Go back to TODO list of previous category." |
| 465 | (interactive) |
| 466 | (setq todo-category-number |
| 467 | (mod (1- todo-category-number) (length todo-categories))) |
| 468 | (todo-category-select)) |
| 469 | (defalias 'todo-cmd-back 'todo-backward-category) |
| 470 | |
| 471 | (defun todo-backward-item () |
| 472 | "Select previous entry of TODO list." |
| 473 | (interactive) |
| 474 | (search-backward-regexp (concat "^" (regexp-quote todo-prefix)) nil t) |
| 475 | (message "")) |
| 476 | (defalias 'todo-cmd-prev 'todo-backward-item) |
| 477 | |
| 478 | (defun todo-forward-item (&optional count) |
| 479 | "Select COUNT-th next entry of TODO list." |
| 480 | (interactive "P") |
| 481 | (if (listp count) (setq count (car count))) |
| 482 | (end-of-line) |
| 483 | (search-forward-regexp (concat "^" (regexp-quote todo-prefix)) |
| 484 | nil 'goto-end count) |
| 485 | (beginning-of-line) |
| 486 | (message "")) |
| 487 | (defalias 'todo-cmd-next 'todo-forward-item) |
| 488 | |
| 489 | (defun todo-save () |
| 490 | "Save the TODO list." |
| 491 | (interactive) |
| 492 | (save-excursion |
| 493 | (save-restriction |
| 494 | (save-buffer))) |
| 495 | (if todo-save-top-priorities-too (todo-save-top-priorities))) |
| 496 | (defalias 'todo-cmd-save 'todo-save) |
| 497 | |
| 498 | (defun todo-quit () |
| 499 | "Done with TODO list for now." |
| 500 | (interactive) |
| 501 | (widen) |
| 502 | (todo-save) |
| 503 | (message "") |
| 504 | (bury-buffer)) |
| 505 | (defalias 'todo-cmd-done 'todo-quit) |
| 506 | |
| 507 | (defun todo-edit-item () |
| 508 | "Edit current TODO list entry." |
| 509 | (interactive) |
| 510 | (let ((item (todo-item-string))) |
| 511 | (if (todo-string-multiline-p item) |
| 512 | (todo-edit-multiline) |
| 513 | (let ((new (read-from-minibuffer "Edit: " item))) |
| 514 | (todo-remove-item) |
| 515 | (insert new "\n") |
| 516 | (todo-backward-item) |
| 517 | (message ""))))) |
| 518 | (defalias 'todo-cmd-edit 'todo-edit-item) |
| 519 | |
| 520 | (defun todo-edit-multiline () |
| 521 | "Set up a buffer for editing a multiline TODO list entry." |
| 522 | (interactive) |
| 523 | (let ((buffer-name (generate-new-buffer-name todo-edit-buffer))) |
| 524 | (switch-to-buffer |
| 525 | (make-indirect-buffer |
| 526 | (file-name-nondirectory todo-file-do) buffer-name)) |
| 527 | (message "To exit, simply kill this buffer and return to list.") |
| 528 | (todo-edit-mode) |
| 529 | (narrow-to-region (todo-item-start) (todo-item-end)))) |
| 530 | |
| 531 | ;;;###autoload |
| 532 | (defun todo-add-category (cat) |
| 533 | "Add new category CAT to the TODO list." |
| 534 | (interactive "sCategory: ") |
| 535 | (save-window-excursion |
| 536 | (setq todo-categories (cons cat todo-categories)) |
| 537 | (find-file todo-file-do) |
| 538 | (widen) |
| 539 | (goto-char (point-min)) |
| 540 | (let ((posn (search-forward "-*- mode: todo; " 17 t))) |
| 541 | (if (not (null posn)) (goto-char posn)) |
| 542 | (if (equal posn nil) |
| 543 | (progn |
| 544 | (insert "-*- mode: todo; \n") |
| 545 | (forward-char -1)) |
| 546 | (kill-line))) |
| 547 | (insert (format "todo-categories: %S; -*-" todo-categories)) |
| 548 | (forward-char 1) |
| 549 | (insert (format "%s%s%s\n%s\n%s %s\n" |
| 550 | todo-prefix todo-category-beg cat |
| 551 | todo-category-end |
| 552 | todo-prefix todo-category-sep))) |
| 553 | 0) |
| 554 | |
| 555 | ;;;###autoload |
| 556 | (defun todo-add-item-non-interactively (new-item category) |
| 557 | "Insert NEW-ITEM in TODO list as a new entry in CATEGORY." |
| 558 | (save-excursion |
| 559 | (todo-show)) |
| 560 | (save-excursion |
| 561 | (if (string= "" category) |
| 562 | (setq category (nth todo-category-number todo-categories))) |
| 563 | (let ((cat-exists (member category todo-categories))) |
| 564 | (setq todo-category-number |
| 565 | (if cat-exists |
| 566 | (- (length todo-categories) (length cat-exists)) |
| 567 | (todo-add-category category)))) |
| 568 | (todo-show) |
| 569 | (setq todo-previous-line 0) |
| 570 | (let ((top 1) |
| 571 | (bottom (1+ (count-lines (point-min) (point-max))))) |
| 572 | (while (> (- bottom top) todo-insert-threshold) |
| 573 | (let* ((current (/ (+ top bottom) 2)) |
| 574 | (answer (if (< current bottom) |
| 575 | (todo-more-important-p current) nil))) |
| 576 | (if answer |
| 577 | (setq bottom current) |
| 578 | (setq top (1+ current))))) |
| 579 | (setq top (/ (+ top bottom) 2)) |
| 580 | ;; goto-line doesn't have the desired behavior in a narrowed buffer |
| 581 | (goto-char (point-min)) |
| 582 | (forward-line (1- top))) |
| 583 | (insert new-item "\n") |
| 584 | (todo-backward-item) |
| 585 | (todo-save) |
| 586 | (message ""))) |
| 587 | |
| 588 | ;;;###autoload |
| 589 | (defun todo-insert-item (arg) |
| 590 | "Insert new TODO list entry. |
| 591 | With a prefix argument solicit the category, otherwise use the current |
| 592 | category." |
| 593 | (interactive "P") |
| 594 | (save-excursion |
| 595 | (if (not (string-equal mode-name "TODO")) (todo-show)) |
| 596 | (let* ((new-item (concat todo-prefix " " |
| 597 | (read-from-minibuffer |
| 598 | "New TODO entry: " |
| 599 | (if todo-entry-prefix-function |
| 600 | (funcall todo-entry-prefix-function))))) |
| 601 | (categories todo-categories) |
| 602 | (history (cons 'categories (1+ todo-category-number))) |
| 603 | (current-category (nth todo-category-number todo-categories)) |
| 604 | (category |
| 605 | (if arg |
| 606 | current-category |
| 607 | (completing-read (concat "Category [" current-category "]: ") |
| 608 | (todo-category-alist) nil nil nil |
| 609 | history current-category)))) |
| 610 | (todo-add-item-non-interactively new-item category)))) |
| 611 | |
| 612 | (defalias 'todo-cmd-inst 'todo-insert-item) |
| 613 | |
| 614 | (defun todo-insert-item-here () |
| 615 | "Insert new TODO list entry under the cursor." |
| 616 | (interactive "") |
| 617 | (save-excursion |
| 618 | (if (not (string-equal mode-name "TODO")) (todo-show)) |
| 619 | (let* ((new-item (concat todo-prefix " " |
| 620 | (read-from-minibuffer |
| 621 | "New TODO entry: " |
| 622 | (if todo-entry-prefix-function |
| 623 | (funcall todo-entry-prefix-function)))))) |
| 624 | (insert (concat new-item "\n"))))) |
| 625 | |
| 626 | (defun todo-more-important-p (line) |
| 627 | "Ask whether entry is more important than the one at LINE." |
| 628 | (if (not (equal todo-previous-line line)) |
| 629 | (progn |
| 630 | (setq todo-previous-line line) |
| 631 | (goto-char (point-min)) |
| 632 | (forward-line (1- todo-previous-line)) |
| 633 | (let ((item (todo-item-string-start))) |
| 634 | (setq todo-previous-answer |
| 635 | (y-or-n-p (concat "More important than '" item "'? ")))))) |
| 636 | todo-previous-answer) |
| 637 | (defalias 'todo-ask-p 'todo-more-important-p) |
| 638 | |
| 639 | (defun todo-delete-item () |
| 640 | "Delete current TODO list entry." |
| 641 | (interactive) |
| 642 | (if (> (count-lines (point-min) (point-max)) 0) |
| 643 | (let* ((todo-entry (todo-item-string-start)) |
| 644 | (todo-answer (y-or-n-p (concat "Permanently remove '" |
| 645 | todo-entry "'? ")))) |
| 646 | (if todo-answer |
| 647 | (progn |
| 648 | (todo-remove-item) |
| 649 | (todo-backward-item))) |
| 650 | (message "")) |
| 651 | (error "No TODO list entry to delete"))) |
| 652 | (defalias 'todo-cmd-kill 'todo-delete-item) |
| 653 | |
| 654 | (defun todo-raise-item () |
| 655 | "Raise priority of current entry." |
| 656 | (interactive) |
| 657 | (if (> (count-lines (point-min) (point)) 0) |
| 658 | (let ((item (todo-item-string))) |
| 659 | (todo-remove-item) |
| 660 | (todo-backward-item) |
| 661 | (save-excursion |
| 662 | (insert item "\n")) |
| 663 | (message "")) |
| 664 | (error "No TODO list entry to raise"))) |
| 665 | (defalias 'todo-cmd-rais 'todo-raise-item) |
| 666 | |
| 667 | (defun todo-lower-item () |
| 668 | "Lower priority of current entry." |
| 669 | (interactive) |
| 670 | (if (> (count-lines (point) (point-max)) 1) |
| 671 | ;; Assume there is a final newline |
| 672 | (let ((item (todo-item-string))) |
| 673 | (todo-remove-item) |
| 674 | (todo-forward-item) |
| 675 | (save-excursion |
| 676 | (insert item "\n")) |
| 677 | (message "")) |
| 678 | (error "No TODO list entry to lower"))) |
| 679 | (defalias 'todo-cmd-lowr 'todo-lower-item) |
| 680 | |
| 681 | (defun todo-file-item (&optional comment) |
| 682 | "File the current TODO list entry away, annotated with an optional COMMENT." |
| 683 | (interactive "sComment: ") |
| 684 | (or (> (count-lines (point-min) (point-max)) 0) |
| 685 | (error "No TODO list entry to file away")) |
| 686 | (let ((time-stamp-format todo-time-string-format)) |
| 687 | (if (and comment (> (length comment) 0)) |
| 688 | (progn |
| 689 | (goto-char (todo-item-end)) |
| 690 | (insert |
| 691 | (if (save-excursion (beginning-of-line) |
| 692 | (looking-at (regexp-quote todo-prefix))) |
| 693 | " " |
| 694 | "\n\t") |
| 695 | "(" comment ")"))) |
| 696 | (goto-char (todo-item-end)) |
| 697 | (insert " [" (nth todo-category-number todo-categories) "]") |
| 698 | (goto-char (todo-item-start)) |
| 699 | (let ((temp-point (point))) |
| 700 | (if (looking-at (regexp-quote todo-prefix)) |
| 701 | (replace-match (time-stamp-string)) |
| 702 | ;; Standard prefix -> timestamp |
| 703 | ;; Else prefix non-standard item start with timestamp |
| 704 | (insert (time-stamp-string))) |
| 705 | (append-to-file temp-point (1+ (todo-item-end)) todo-file-done) |
| 706 | (delete-region temp-point (1+ (todo-item-end)))) |
| 707 | (todo-backward-item) |
| 708 | (message ""))) |
| 709 | |
| 710 | ;; --------------------------------------------------------------------------- |
| 711 | |
| 712 | ;; Utility functions: |
| 713 | |
| 714 | |
| 715 | ;;;###autoload |
| 716 | (defun todo-top-priorities (&optional nof-priorities category-pr-page) |
| 717 | "List top priorities for each category. |
| 718 | |
| 719 | Number of entries for each category is given by NOF-PRIORITIES which |
| 720 | defaults to \'todo-show-priorities\'. |
| 721 | |
| 722 | If CATEGORY-PR-PAGE is non-nil, a page separator \'^L\' is inserted |
| 723 | between each category." |
| 724 | |
| 725 | (interactive "P") |
| 726 | (or nof-priorities (setq nof-priorities todo-show-priorities)) |
| 727 | (if (listp nof-priorities) ;universal argument |
| 728 | (setq nof-priorities (car nof-priorities))) |
| 729 | (let ((todo-print-buffer-name todo-tmp-buffer-name) |
| 730 | ;;(todo-print-category-number 0) |
| 731 | (todo-category-break (if category-pr-page "\f" "")) |
| 732 | (cat-end |
| 733 | (concat |
| 734 | (if todo-remove-separator |
| 735 | (concat todo-category-end "\n" |
| 736 | (regexp-quote todo-prefix) " " todo-category-sep "\n") |
| 737 | (concat todo-category-end "\n")))) |
| 738 | beg end) |
| 739 | (todo-show) |
| 740 | (save-excursion |
| 741 | (save-restriction |
| 742 | (widen) |
| 743 | (copy-to-buffer todo-print-buffer-name (point-min) (point-max)) |
| 744 | (set-buffer todo-print-buffer-name) |
| 745 | (goto-char (point-min)) |
| 746 | (when (re-search-forward (regexp-quote todo-header) nil t) |
| 747 | (beginning-of-line 1) |
| 748 | (delete-region (point) (line-end-position))) |
| 749 | (while (re-search-forward ;Find category start |
| 750 | (regexp-quote (concat todo-prefix todo-category-beg)) |
| 751 | nil t) |
| 752 | (setq beg (+ (line-end-position) 1)) ;Start of first entry. |
| 753 | (re-search-forward cat-end nil t) |
| 754 | (setq end (match-beginning 0)) |
| 755 | (replace-match todo-category-break) |
| 756 | (narrow-to-region beg end) ;In case we have too few entries. |
| 757 | (goto-char (point-min)) |
| 758 | (if (= 0 nof-priorities) ;Traverse entries. |
| 759 | (goto-char end) ;All entries |
| 760 | (todo-forward-item nof-priorities)) |
| 761 | (setq beg (point)) |
| 762 | (delete-region beg end) |
| 763 | (widen)) |
| 764 | (and (looking-at "\f") (replace-match "")) ;Remove trailing form-feed. |
| 765 | (goto-char (point-min)) ;Due to display buffer |
| 766 | )) |
| 767 | ;; Could have used switch-to-buffer as it has a norecord argument, |
| 768 | ;; which is nice when we are called from e.g. todo-print. |
| 769 | ;; Else we could have used pop-to-buffer. |
| 770 | (display-buffer todo-print-buffer-name) |
| 771 | (message "Type C-x 1 to remove %s window. M-C-v to scroll the help." |
| 772 | todo-print-buffer-name))) |
| 773 | |
| 774 | (defun todo-save-top-priorities (&optional nof-priorities) |
| 775 | "Save top priorities for each category in `todo-file-top'. |
| 776 | |
| 777 | Number of entries for each category is given by NOF-PRIORITIES which |
| 778 | defaults to `todo-show-priorities'." |
| 779 | (interactive "P") |
| 780 | (save-window-excursion |
| 781 | (save-excursion |
| 782 | (save-restriction |
| 783 | (todo-top-priorities nof-priorities) |
| 784 | (set-buffer todo-tmp-buffer-name) |
| 785 | (write-file todo-file-top) |
| 786 | (kill-this-buffer))))) |
| 787 | |
| 788 | ;;;###autoload |
| 789 | (defun todo-print (&optional category-pr-page) |
| 790 | "Print todo summary using `todo-print-function'. |
| 791 | If CATEGORY-PR-PAGE is non-nil, a page separator `^L' is inserted |
| 792 | between each category. |
| 793 | |
| 794 | Number of entries for each category is given by `todo-print-priorities'." |
| 795 | (interactive "P") |
| 796 | (save-window-excursion |
| 797 | (save-excursion |
| 798 | (save-restriction |
| 799 | (todo-top-priorities todo-print-priorities |
| 800 | category-pr-page) |
| 801 | (set-buffer todo-tmp-buffer-name) |
| 802 | (and (funcall todo-print-function) |
| 803 | (kill-this-buffer)) |
| 804 | (message "Todo printing done."))))) |
| 805 | |
| 806 | (defun todo-jump-to-category () |
| 807 | "Jump to a category. Default is previous category." |
| 808 | (interactive) |
| 809 | (let* ((categories todo-categories) |
| 810 | (history (cons 'categories (1+ todo-category-number))) |
| 811 | (default (nth todo-category-number todo-categories)) |
| 812 | (category (completing-read |
| 813 | (concat "Category [" default "]: ") |
| 814 | (todo-category-alist) nil nil nil history default))) |
| 815 | (if (string= "" category) |
| 816 | (setq category (nth todo-category-number todo-categories))) |
| 817 | (setq todo-category-number |
| 818 | (if (member category todo-categories) |
| 819 | (- (length todo-categories) |
| 820 | (length (member category todo-categories))) |
| 821 | (todo-add-category category))) |
| 822 | (todo-show))) |
| 823 | |
| 824 | (defun todo-line-string () |
| 825 | "Return current line in buffer as a string." |
| 826 | (buffer-substring (line-beginning-position) (line-end-position))) |
| 827 | |
| 828 | (defun todo-item-string-start () |
| 829 | "Return the start of this TODO list entry as a string." |
| 830 | ;; Suitable for putting in the minibuffer when asking the user |
| 831 | (let ((item (todo-item-string))) |
| 832 | (if (> (length item) 60) |
| 833 | (setq item (concat (substring item 0 56) "..."))) |
| 834 | item)) |
| 835 | |
| 836 | (defun todo-item-start () |
| 837 | "Return point at start of current TODO list item." |
| 838 | (save-excursion |
| 839 | (beginning-of-line) |
| 840 | (if (not (looking-at (regexp-quote todo-prefix))) |
| 841 | (search-backward-regexp |
| 842 | (concat "^" (regexp-quote todo-prefix)) nil t)) |
| 843 | (point))) |
| 844 | |
| 845 | (defun todo-item-end () |
| 846 | "Return point at end of current TODO list item." |
| 847 | (save-excursion |
| 848 | (end-of-line) |
| 849 | (search-forward-regexp |
| 850 | (concat "^" (regexp-quote todo-prefix)) nil 'goto-end) |
| 851 | (1- (line-beginning-position)))) |
| 852 | |
| 853 | (defun todo-remove-item () |
| 854 | "Delete the current entry from the TODO list." |
| 855 | (delete-region (todo-item-start) (1+ (todo-item-end)))) |
| 856 | |
| 857 | (defun todo-item-string () |
| 858 | "Return current TODO list entry as a string." |
| 859 | (buffer-substring (todo-item-start) (todo-item-end))) |
| 860 | |
| 861 | (defun todo-string-count-lines (string) |
| 862 | "Return the number of lines STRING spans." |
| 863 | (length (split-string string "\n"))) |
| 864 | |
| 865 | (defun todo-string-multiline-p (string) |
| 866 | "Return non-nil if STRING spans several lines." |
| 867 | (> (todo-string-count-lines string) 1)) |
| 868 | |
| 869 | (defun todo-category-alist () |
| 870 | "Generate an alist for use in `completing-read' from `todo-categories'." |
| 871 | (mapcar #'list todo-categories)) |
| 872 | |
| 873 | ;; --------------------------------------------------------------------------- |
| 874 | |
| 875 | (easy-menu-define todo-menu todo-mode-map "Todo Menu" |
| 876 | '("Todo" |
| 877 | ["Next category" todo-forward-category t] |
| 878 | ["Previous category" todo-backward-category t] |
| 879 | ["Jump to category" todo-jump-to-category t] |
| 880 | ["Show top priority items" todo-top-priorities t] |
| 881 | ["Print categories" todo-print t] |
| 882 | "---" |
| 883 | ["Edit item" todo-edit-item t] |
| 884 | ["File item" todo-file-item t] |
| 885 | ["Insert new item" todo-insert-item t] |
| 886 | ["Insert item here" todo-insert-item-here t] |
| 887 | ["Kill item" todo-delete-item t] |
| 888 | "---" |
| 889 | ["Lower item priority" todo-lower-item t] |
| 890 | ["Raise item priority" todo-raise-item t] |
| 891 | "---" |
| 892 | ["Next item" todo-forward-item t] |
| 893 | ["Previous item" todo-backward-item t] |
| 894 | "---" |
| 895 | ["Save" todo-save t] |
| 896 | ["Save Top Priorities" todo-save-top-priorities t] |
| 897 | "---" |
| 898 | ["Quit" todo-quit t] |
| 899 | )) |
| 900 | |
| 901 | ;; As calendar reads .todo-do before todo-mode is loaded. |
| 902 | ;;;###autoload |
| 903 | (defun todo-mode () |
| 904 | "Major mode for editing TODO lists. |
| 905 | |
| 906 | \\{todo-mode-map}" |
| 907 | (interactive) |
| 908 | (kill-all-local-variables) |
| 909 | (setq major-mode 'todo-mode) |
| 910 | (setq mode-name "TODO") |
| 911 | (use-local-map todo-mode-map) |
| 912 | (easy-menu-add todo-menu) |
| 913 | (run-mode-hooks 'todo-mode-hook)) |
| 914 | |
| 915 | (eval-when-compile |
| 916 | (defvar date) |
| 917 | (defvar entry)) |
| 918 | |
| 919 | ;; Read about this function in the setup instructions above! |
| 920 | ;;;###autoload |
| 921 | (defun todo-cp () |
| 922 | "Make a diary entry appear only in the current date's diary." |
| 923 | (if (equal (calendar-current-date) date) |
| 924 | entry)) |
| 925 | |
| 926 | (define-derived-mode todo-edit-mode text-mode "TODO Edit" |
| 927 | "Major mode for editing items in the TODO list. |
| 928 | |
| 929 | \\{todo-edit-mode-map}") |
| 930 | |
| 931 | ;;;###autoload |
| 932 | (defun todo-show () |
| 933 | "Show TODO list." |
| 934 | (interactive) |
| 935 | (if (file-exists-p todo-file-do) |
| 936 | (find-file todo-file-do) |
| 937 | (todo-initial-setup)) |
| 938 | (if (null todo-categories) |
| 939 | (if (null todo-cats) |
| 940 | (error "Error in %s: No categories in list `todo-categories'" |
| 941 | todo-file-do) |
| 942 | (goto-char (point-min)) |
| 943 | (and (search-forward "todo-cats:" nil t) |
| 944 | (replace-match "todo-categories:")) |
| 945 | (make-local-variable 'todo-categories) |
| 946 | (setq todo-categories todo-cats))) |
| 947 | (beginning-of-line) |
| 948 | (todo-category-select)) |
| 949 | |
| 950 | (defun todo-initial-setup () |
| 951 | "Set up things to work properly in TODO mode." |
| 952 | (find-file todo-file-do) |
| 953 | (erase-buffer) |
| 954 | (todo-mode) |
| 955 | (todo-add-category "Todo")) |
| 956 | |
| 957 | (provide 'todo-mode) |
| 958 | |
| 959 | ;;; arch-tag: 6fd91be5-776e-4464-a109-da4ea0e4e497 |
| 960 | ;;; todo-mode.el ends here |