gnu-emacs-sources
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

todo-mode revisions and extensions


From: Stephen Berman
Subject: todo-mode revisions and extensions
Date: Wed, 27 Nov 2002 00:39:37 +0100
User-agent: Gnus/5.090007 (Oort Gnus v0.07) Emacs/21.2 (i586-suse-linux)

The other day on gnu.emacs.help Mark Mysted asked about functionality
for manipulating completed todo items in todo-mode.  Last Spring I
posted a large patch for todo-mode.el to gnu.emacs.bug, which fixed a
number of small bugs in the todo-mode.el included with Emacs 21.2.1
and also added quite a bit of new functionality, some of which seems
to address concerns expressed by Mysted.  Prompted by his question, I
take this opportunity to post the latest version of my revisions and
extensions to todo-mode.el, now just as the source file (which I call
todo-mode-srb.el for development purposes, but it should be installed
and loaded as todo-mode.el), instead of the large diff I posted to
gnu.emacs.bug.  If you want to see just what I changed and added, run
ediff on this file and the todo-mode.el included with Emacs.  I hope
you find this useful and welcome any feedback.

--Steve Berman


;;; todo-mode.el --- major mode for editing TODO list files

;; Copyright (C) 1997, 1999, 2001 Free Software Foundation, Inc.

;; Author: Oliver Seidel <address@hidden>
;;   [Not clear the above works, July 2000]
;; Created: 2 Aug 1997
;; Partly revised and extended by Steve Berman <address@hidden>
;; Version: $Id: todo-mode-srb.el,v 1.21 2002/11/26 21:04:04 steve Exp $
;; Keywords: calendar, todo

;; This file [with revisions and extensions] is [not] part of GNU Emacs.

;; GNU Emacs is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;; ---------------------------------------------------------------------------

;;; Commentary:

;;  Mode Description
;;
;;      TODO is a major mode for EMACS which offers functionality to
;;      treat most lines in one buffer as a list of items one has to
;;      do.  There are facilities to add new items, which are
;;      categorised, to edit or even delete items from the buffer.
;;      The buffer contents are currently compatible with the diary,
;;      so that the list of todo-items will show up in the FANCY diary
;;      mode.
;;
;;      Notice: Besides the major mode, this file also exports the
;;      function `todo-show' which will change to the one specific
;;      TODO file that has been specified in the todo-file-do
;;      variable.  If this file does not conform to the TODO mode
;;      conventions, the todo-show function will add the appropriate
;;      header and footer.  I don't anticipate this to cause much
;;      grief, but be warned, in case you attempt to read a plain text
;;      file.
;;
;;  Preface, Quickstart Installation
;;
;;      To get this to work, make emacs execute the line
;;
;;          (autoload 'todo-mode "todo-mode"
;;                    "Major mode for editing TODO lists." t)
;;          (autoload 'todo-show "todo-mode"
;;                    "Show TODO items." t)
;;          (autoload 'todo-insert-item "todo-mode"
;;                    "Add TODO item." t)
;;
;;      You may now enter new items by typing "M-x todo-insert-item",
;;      or enter your TODO list file by typing "M-x todo-show".
;;
;;      The TODO list file has a special format and some auxiliary
;;      information, which will be added by the todo-show function if
;;      it attempts to visit an un-initialised file.  Hence it is
;;      recommended to use the todo-show function for the first time,
;;      in order to initialise the file, but it is not necessary
;;      afterwards.
;;
;;      As these commands are quite long to type, I would recommend
;;      the addition of two bindings to your to your global keymap.  I
;;      personally have the following in my initialisation file:
;;
;;          (global-set-key "\C-ct" 'todo-show) ;; switch to TODO buffer
;;          (global-set-key "\C-ci" 'todo-insert-item) ;; insert new item
;;
;;      Note, however, that this recommendation has prompted some
;;      criticism, since the keys C-c LETTER are reserved for user
;;      functions.  I believe my recommendation is acceptable, since
;;      the Emacs Lisp Manual *Tips* section also details that the
;;      mode itself should not bind any functions to those keys.  The
;;      express aim of the above two bindings is to work outside the
;;      mode, which doesn't need the show function and offers a
;;      different binding for the insert function.  They serve as
;;      shortcuts and are not even needed (since the TODO mode will be
;;      entered by visiting the TODO file, and later by switching to
;;      its buffer).
;;
;;      If you are an advanced user of this package, please consult
;;      the whole source code for autoloads, because there are several
;;      extensions that are not explicitly listed in the above quick
;;      installation.
;;
;;  Version
;;
;;      Which version of todo-mode.el does this documentation refer to?
;;
;;      $Id: todo-mode-srb.el,v 1.21 2002/11/26 21:04:04 steve Exp $
;;
;;      Commentary marked with `[srb]' was added by Steve Berman. [srb]
;;
;;  Pre-Requisites
;;
;;      This package will require the following packages to be
;;      available on the load-path:
;;
;;          time-stamp
;;          easymenu
;;          cl (unless Emacs 21.1 or later) [srb]
;;
;;  Operation
;;
;;      You will have the following facilities available:
;;
;;          M-x todo-show   will enter todo-mode and display the todo 
;;                          list screen.  The functions of this mode 
;;                          can be invoked by clicking on an item from
;;                          the "Todo" menu in the menubar, or by typing:
;;
;;          +  to go to next category
;;          -  to go to previous category
;;          A  to add a new todo category [srb]
;;          c  to display a clickable list of all todo categories [srb]
;;          C  to change the string separating entries in 
;;                                  the current category [srb]
;;          d  to display the filed entries of the current category [srb]
;;          D  to delete the current category [srb]
;;          e  to edit the current entry
;;          E  to edit a multi-line entry
;;          f  to file the current entry, including a
;;                                  comment and timestamp
;;          F  to file the current entry under its category, 
;;                                  including a comment and timestamp [srb]
;;          h  to toggle display of item prefix or timestamp and initials [srb]
;;          i  to insert a new entry, with prefix, solicit category (with 
;;                                  case-insensitive completion [srb])
;;          I  to insert a new entry at current cursor position
;;          j  jump to category (with case-insensitive completion [srb])
;;          k  to kill the current entry
;;          l  to lower the current entry's priority
;;          m  to move the current entry to another category (with 
;;                                  case-insensitive completion) [srb]
;;          n  for the next entry
;;          p  for the previous entry
;;          P  print
;;          q  to save the list and exit the buffer
;;          r  to raise the current entry's priority
;;          R  to rename the current category [srb]
;;          s  to save the list
;;          S  to save the list of top priorities
;;          t  show top priority items for each category
;;          X  to change the todo prefix string [srb]
;;
;;      When you add a new entry, you are asked for the text and
;;      optionally for the category.  I for example have categories for 
;;      things that I want to do in the office (like mail my mum), that
;;      I want to do in town (like buy cornflakes) and things I want to
;;      do at home (move my suitcases).  The categories can be
;;      selected with the cursor keys and if you type in the name of a
;;      category which didn't exist before, an empty category of the
;;      desired name will be added and filled with the new entry.
;;      
;;      Todo list items can be edited as normal text, and they can be
;;      reprioritized by moving them up and down within their category.
;;      Items can also be moved from one category to another.  If you
;;      want to remove a todo list entry, you can either permanently
;;      delete it or file it.  You can file it either in a separate
;;      file for done items (see below) or within its category.  In the
;;      latter case, it is displayed in separate window below the
;;      unfiled items in that category.  The buffer for filed items has
;;      its own mode, for which a restricted subset of todo-mode
;;      commands is available. [srb]
;;
;;      Todo categories can be displayed in a buffer as an
;;      alphabetized list, and clicking or pressing the return key on
;;      a category name in the list pops up the buffer of that
;;      category.  Categories can be renamed and, as long as they are
;;      empty (i.e., contain neither unfiled nor filed items), can
;;      also be deleted. [srb]
;;
;;  Configuration [srb for the following three sentences]
;;
;;      Many of todo-mode's options can be customized by the user;
;;      to do so click on "Customize" in the "Todo" menu.  If you save
;;      your customizations for future sessions, they will be loaded
;;      the next time you invoke todo-mode.  The customizable variables
;;      include:
;;
;;  Variable todo-prefix
;;
;;      I would like to recommend that you use the prefix "*/*" (by
;;      leaving the variable 'todo-prefix' untouched) so that the
;;      diary displays each entry every day.
;;
;;      To understand what I mean, please read the documentation that
;;      goes with the calendar since that will tell you how you can
;;      set up the fancy diary display and use the #include command to
;;      include your todo list file as part of your diary.
;;
;;      If you have the diary package set up to usually display more
;;      than one day's entries at once, consider using
;;
;;          "&%%(equal (calendar-current-date) date)"
;;
;;      as the value of `todo-prefix'.  Please note that this may slow
;;      down the processing of your diary file some.
;;
;;      Carsten Dominik <address@hidden> suggested that
;;
;;          "&%%(todo-cp)"
;;
;;      might be nicer and to that effect a function has been declared
;;      further down in the code.  You may wish to auto-load this.
;;
;;      Carsten also writes that that *changing* the prefix after the
;;      todo list is already established is not as simple as changing
;;      the variable - the todo files have to be changed by hand.
;;      (The command `todo-change-todo-prefix' has now been added to 
;;      do this. [srb])
;;
;;  Variable todo-file-do
;;
;;      This variable is fairly self-explanatory.  You have to store
;;      your TODO list somewhere.  This variable tells the package
;;      where to go and find this file.
;;
;;  Variable todo-file-done
;;
;;      Even when you're done, you may wish to retain the entries.
;;      Given that they're timestamped and you are offered to add a
;;      comment, this can make a useful diary of past events.  It will
;;      even blend in with the EMACS diary package.  So anyway, this
;;      variable holds the name of the file for the filed todo-items.
;;
;;  Variable todo-file-top
;;
;;      File storing the top priorities of your TODO list when
;;      todo-save-top-priorities is non-nil.  Nice to include in your
;;      diary instead of the complete TODO list.
;;
;;  Variable todo-mode-hook
;;
;;      Just like other modes, too, this mode offers to call your
;;      functions before it goes about its business.  This variable
;;      will be inspected for any functions you may wish to have
;;      called once the other TODO mode preparations have been
;;      completed.
;;
;;  Variable todo-insert-threshold
;;
;;      Another nifty feature is the insertion accuracy.  If you have
;;      8 items in your TODO list, then you may get asked 4 questions
;;      by the binary insertion algorithm.  However, you may not
;;      really have a need for such accurate priorities amongst your
;;      TODO items.  If you now think about the binary insertion
;;      halfing the size of the window each time, then the threshhold
;;      is the window size at which it will stop.  If you set the
;;      threshhold to zero, the upper and lower bound will coincide at
;;      the end of the loop and you will insert your item just before
;;      that point.  If you set the threshhold to, e.g. 8, it will stop
;;      as soon as the window size drops below that amount and will
;;      insert the item in the approximate centre of that window.  I
;;      got the idea for this feature after reading a very helpful
;;      e-mail reply from Trey Jackson <address@hidden> who
;;      corrected some of my awful coding and pointed me towards some
;;      good reading.  Thanks Trey!
;;
;;  Variable todo-item-separator [srb]
;;
;;      This variable stores the string used to separate entries in
;;      the todo list.  It may improve the readability of a crowded 
;;      category, and may vary between categories to visually
;;      distinguish them.  To insure proper behavior it must begin and 
;;      end with a newline (hence minimally "\n", the default value)
;;      and not contain alphanumeric characters.  Its value can also 
;;      be changed within a todo-category buffer by 
;;      `M-x todo-change-item-sep-in-cat'.
;;
;;  Things to do
;;
;;      These originally were my ideas, but now also include all the
;;      suggestions that I included before forgetting them:
;;
;;      o   Fancy fonts for todo/top-priority buffer
;;      o   Remove todo-prefix option in todo-top-priorities
;;      o   Rename category [Done [srb]]
;;      o   Move entry from one category to another one [Done [srb]]
;;      o   Entries which both have the generic */* prefix and a
;;          "deadline" entry which are understood by diary, indicating
;;          an event (unless marked by &)
;;      o   The optional COUNT variable of todo-forward-item should be
;;          applied to the other functions performing similar tasks
;;      o   Modularization could be done for repeaded elements of
;;          the code, like the completing-read lines of code.
;;      o   license / version function
;;      o   export to diary file
;;      o   todo-report-bug
;;      o   GNATS support
;;      o   elide multiline (as in bbdb, or, to a lesser degree, in
;;          outline mode)
;;      o   rewrite complete package to store data as lisp objects
;;          and have display modes for display, for diary export,
;;          etc. (Richard Stallman pointed out this is a bad idea)
;;      o   so base todo-mode.el on generic-mode.el instead
;;
;;  History and Gossip
;;
;;      Many thanks to all the ones who have contributed to the
;;      evolution of this package!  I hope I have listed all of you
;;      somewhere in the documentation or at least in the RCS history!
;;
;;      Enjoy this package and express your gratitude by sending nice
;;      things to my parents' address!
;;
;;      Oliver Seidel
;;      (Lessingstr. 8, 65760 Eschborn, Federal Republic of Germany)
;;

;; ---------------------------------------------------------------------------

;;; Code:

(require 'time-stamp)

;; User-configurable variables:

(defgroup todo nil
  "Maintain a list of todo items."
  :link '(emacs-commentary-link "todo-mode")
  :version "21.1"
  :group 'calendar)

(defcustom todo-prefix     "*/*"
  "*TODO mode prefix for entries.

This is useful in conjunction with `calendar' and `diary' if you use

#include \"~/.todo-do\"

in your diary file to include your todo list file as part of your
diary.  With the default value \"*/*\" the diary displays each entry
every day and it may also be marked on every day of the calendar.
Using \"&%%(equal (calendar-current-date) date)\" instead will only
show and mark todo entreis for today, but may slow down processing of
the diary file somewhat."
  :type 'string
  :group 'todo)
(defcustom todo-file-do    "~/.todo-do"
  "*TODO mode list file."
  :type 'file
  :group 'todo)
(defcustom todo-file-done  "~/.todo-done"
  "*TODO mode archive file."
  :type 'file
  :group 'todo)
(defcustom todo-mode-hook  nil
  "*TODO mode hooks."
  :type 'hook
  :group 'todo)
(defcustom todo-edit-mode-hook nil
  "*TODO Edit mode hooks."
  :type 'hook
  :group 'todo)
(defcustom todo-categories-mode-hook nil
  "*TODO Categories mode hooks."
  :type 'hook
  :group 'todo)
(defcustom todo-filed-mode-hook nil
  "*TODO Filed mode hooks."
  :type 'hook
  :group 'todo)
(defcustom todo-item-separator "\n"
  "*String for separating todo entries within a category.
To insure proper behavior must begin and end with a newline and not contain 
alphanumeric characters.  Can also be changed within a todo-category buffer
by \\[todo-change-item-sep-in-cat]."
  :type 'string
  :group 'todo)
(defcustom todo-insert-threshold 0
  "*TODO mode insertion accuracy.

If you have 8 items in your TODO list, then you may get asked 4
questions by the binary insertion algorithm.  However, you may not
really have a need for such accurate priorities amongst your TODO
items.  If you now think about the binary insertion halfing the size
of the window each time, then the threshhold is the window size at
which it will stop.  If you set the threshhold to zero, the upper and
lower bound will coincide at the end of the loop and you will insert
your item just before that point.  If you set the threshhold to,
e.g. 8, it will stop as soon as the window size drops below that
amount and will insert the item in the approximate centre of that
window."
  :type 'integer
  :group 'todo)
(defcustom todo-file-top "~/.todo-top"
  "*TODO mode top priorities file.

Not in TODO format, but diary compatible.
Automatically generated when `todo-save-top-priorities' is non-nil."
  :type 'string
  :group 'todo)

(defcustom todo-print-function 'ps-print-buffer-with-faces
  "*Function to print the current buffer."
  :type 'symbol
  :group 'todo)
(defcustom todo-show-priorities 1
  "*Default number of priorities to show by \\[todo-top-priorities].
0 means show all entries."
  :type 'integer
  :group 'todo)
(defcustom todo-print-priorities 0
  "*Default number of priorities to print by \\[todo-print].
0 means print all entries."
  :type 'integer
  :group 'todo)
(defcustom todo-remove-separator t
  "*Non-nil to remove category separators in output of \\[todo-top-priorities] 
and \\[todo-print]."
  :type 'boolean
  :group 'todo)
(defcustom todo-save-top-priorities-too t
  "*Non-nil makes `todo-save' automatically save top-priorities in 
`todo-file-top'."
  :type 'boolean
  :group 'todo)

;; Thanks for the ISO time stamp format go to Karl Eichwalder <address@hidden>
;; My format string for the appt.el package is "%3b %2d, %y, %02I:%02M%p".
;;
(defcustom todo-time-string-format
  "%:y-%02m-%02d %02H:%02M"
  "*TODO mode time string format for done entries.
For details see the variable `time-stamp-format'."
  :type 'string
  :group 'todo)

(defcustom todo-entry-prefix-function 'todo-entry-timestamp-initials
  "*Function producing text to insert at start of todo entry."
  :type 'symbol
  :group 'todo)
(defcustom todo-initials (or (getenv "INITIALS") (user-login-name))
  "*Initials of todo item author."
  :type 'string
  :group 'todo)

(defun todo-entry-timestamp-initials ()
  "Prepend timestamp and your initials to the head of a TODO entry."
  (let ((time-stamp-format todo-time-string-format))
    (concat (time-stamp-string) " " todo-initials ": ")))

;; ---------------------------------------------------------------------------

;; Set up some helpful context ...

(defvar todo-categories nil
  "TODO categories.")

(defvar todo-cats nil
  "Old variable for holding the TODO categories.
Use `todo-categories' instead.")

(defvar todo-filed-categories todo-categories
  "Variable for TODO categories in todo-filed-mode.")

(defvar todo-previous-line 0
  "Previous line asked about.")

(defvar todo-previous-answer 0
  "Previous answer got.")

(defvar todo-mode-map
  (let ((map (make-keymap)))
    (suppress-keymap map t)
    (define-key map "+" 'todo-forward-category)
    (define-key map "-" 'todo-backward-category)
    (define-key map "A" 'todo-add-category)
    (define-key map "c" 'todo-display-categories)
    (define-key map "C" 'todo-change-item-sep-in-cat)
    (define-key map "d" 'todo-display-filed-items-under-cat)
    (define-key map "D" 'todo-delete-category)
    (define-key map "e" 'todo-edit-item)
    (define-key map "E" 'todo-edit-multiline)
    (define-key map "f" 'todo-file-item)
    (define-key map "F" 'todo-file-item-under-cat)
    (define-key map "h" 'todo-toggle-item-header)
    (define-key map "i" 'todo-insert-item)
    (define-key map "I" 'todo-insert-item-here)
    (define-key map "j" 'todo-jump-to-category)
    (define-key map "k" 'todo-delete-item)
    (define-key map "l" 'todo-lower-item)
    (define-key map "m" 'todo-move-item)
    (define-key map "n" 'todo-forward-item)
    (define-key map "p" 'todo-backward-item)
    (define-key map "P" 'todo-print)
    (define-key map "q" 'todo-quit)
    (define-key map "r" 'todo-raise-item)
    (define-key map "R" 'todo-rename-category)
    (define-key map "s" 'todo-save)
    (define-key map "S" 'todo-save-top-priorities)
    (define-key map "t" 'todo-top-priorities)
    (define-key map "X" 'todo-change-todo-prefix)
    map)
  "TODO mode keymap.")

(defvar todo-filed-mode-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map todo-mode-map)
    (suppress-keymap map t)
    (define-key map "C" 'todo-change-item-sep-in-cat)
    (define-key map "e" 'todo-edit-item)
    (define-key map "E" 'todo-edit-multiline)
    (define-key map "f" 'todo-file-item)
    (define-key map "h" 'todo-toggle-item-header)
    (define-key map "k" 'todo-delete-item)
    (define-key map "n" 'todo-forward-item)
    (define-key map "p" 'todo-backward-item)
    (define-key map "q" 'todo-kill-buffer)
    (define-key map "s" 'todo-save)
    map)
  "TODO filed mode keymap.")

(defvar todo-categories-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map "q" 'todo-kill-buffer)
    (define-key map [mouse-2] 'todo-category-click)
    (define-key map "\r" 'todo-category-return)
    map)
  "TODO categories mode keymap.")

(defvar todo-category-number 0 "TODO category number.")

(defvar todo-tmp-buffer-name " *todo tmp*")

(defvar todo-edit-buffer " *TODO Edit*"
  "TODO Edit buffer name.")

(defvar todo-categories-buffer "Todo Categories"
  "TODO categories buffer name.")

(defvar todo-filed-buffer "Filed items:"
  "Name of buffer displaying filed items in the current category.")

(defvar todo-category-sep (make-string 75 ?-)
  "Category separator.")

(defvar todo-category-beg " --- "
  "Category start separator to be prepended onto category name.")

(defvar todo-category-filed "--- Filed"
  "Separator for filed category items, inserted before todo-category-end.")

(defvar todo-category-end "--- End"
  "Separator after a category.")

(defvar todo-header "-*- mode: todo; "
  "Header of todo files.")

(defvar todo-buffer-read-only t ; set to nil to make todo-mode buffer writable
  "For setting read-only status of todo-mode.  Default is read-only.")

;; ---------------------------------------------------------------------------

(defun todo-category-select ()
  "Make TODO mode display the current category correctly."
  (let ((name (nth todo-category-number todo-categories)))
    (setq mode-line-buffer-identification
;;          (concat "Category: " name))
          (concat "Category: " (format "%18s" name)))
    (widen)
    (goto-char (point-min))
    (search-forward-regexp
     (concat "^"
             (regexp-quote (concat todo-prefix todo-category-beg name))
             "$"))
    ;; display only unfiled items 
    (let* ((begin (1+ (line-end-position)))
           (end0 (progn
                   (re-search-forward (concat "^" todo-category-end))
                   (match-beginning 0)))
           (filed-beg (re-search-backward 
                       (concat "^" todo-category-filed "$") begin t))
           (end (or filed-beg end0)))
      (narrow-to-region begin end)
      (goto-char (point-min)))
    ;; set the value of todo-item-separator to the actual string used in
    ;; this category (needed e.g. for todo-change-item-sep-in-cat)
    (setq todo-item-separator (save-excursion 
                                (goto-char (point-min))
                                (if (re-search-forward 
                                     (concat "\\(\n[^A-Ba-z0-9]*\\)" 
                                             (regexp-quote todo-prefix)) nil t)
                                    (match-string 1)
                                  "\n")))))
(defalias 'todo-cat-slct 'todo-category-select)

(defun todo-forward-category ()
  "Go forward to TODO list of next category."
  (interactive)
  (setq todo-category-number
        (mod (1+ todo-category-number) (length todo-categories)))
  (todo-category-select))
(defalias 'todo-cmd-forw 'todo-forward-category)

(defun todo-backward-category ()
  "Go back to TODO list of previous category."
  (interactive)
  (setq todo-category-number
        (mod (1- todo-category-number) (length todo-categories)))
  (todo-category-select))
(defalias 'todo-cmd-back 'todo-backward-category)

(defun todo-backward-item ()
  "Select previous entry of TODO list."
  (interactive)
  (if (or (looking-at (concat "^" (regexp-quote todo-prefix)))
          (todo-item-separator-p)
          (eobp))
      (search-backward-regexp (concat "^" (regexp-quote todo-prefix)) nil t)
    ;; go up to previous item also from anywhere within the current item
    (goto-char (todo-item-start))
    (search-backward-regexp (concat "^" (regexp-quote todo-prefix)) nil t))
  (message ""))
(defalias 'todo-cmd-prev 'todo-backward-item)

(defun todo-forward-item (&optional count)
  "Select COUNT-th next entry of TODO list."
  (interactive "P")
  (if (listp count) (setq count (car count)))
  (end-of-line)
  (search-forward-regexp (concat "^" (regexp-quote todo-prefix))
                         nil 'goto-end count)
  ;; empty last line is not an item
  (if (and (eobp) (looking-at "^$")) (backward-char 1))
  (beginning-of-line)
  (message ""))
(defalias 'todo-cmd-next 'todo-forward-item)

(defun todo-save ()
  "Save the TODO list."
  (interactive)
  (save-excursion
    (save-restriction
      (save-buffer)
      (if todo-save-top-priorities-too (todo-save-top-priorities)))))
(defalias 'todo-cmd-save 'todo-save)

(defun todo-quit ()
  "Done with TODO list for now."
  (interactive)
  (widen)
  (todo-save)
  (message "")
  (bury-buffer))
(defalias 'todo-cmd-done 'todo-quit)

(defun todo-edit-item ()
  "Edit current unfiled or filed TODO list entry."
  (interactive)
  (if (todo-item-string-p)
      (let ((item (todo-item-string)))
        (if (todo-string-multiline-p item)
            (todo-edit-multiline)
          (let* ((buffer-read-only nil)
                 ;; cursor position within item prior to editing
                 (here (- (point) (todo-item-start) -1))
                 (new (read-from-minibuffer 
                       "Edit: " (cons item here)))) ; start editing at point
            (todo-remove-item)
            (insert new todo-item-separator)
            (todo-backward-item)
            ;; make cursor position after editing the same as before editing
            (forward-char (1- here)) 
            (message ""))))
    (error "No TODO list entry to edit")))
(defalias 'todo-cmd-edit 'todo-edit-item)

(defun todo-edit-multiline ()
  "Set up a buffer for editing an unfiled or filed multiline TODO list entry."
  (interactive)
  (if (todo-item-string-p)
      (let ((buffer-name (generate-new-buffer-name todo-edit-buffer))
            (here (point))) ; to enable editing filed multiline items
        (switch-to-buffer
         (make-indirect-buffer
          (file-name-nondirectory todo-file-do) buffer-name))
        (todo-edit-mode)
        (message "To finish editing and return to TODO item list, type C-c 
C-c.")
        (widen) ; in case the item is filed
        (goto-char here)
        ;; This bit of code insures that, if the last unfiled or filed item
        ;; in the category is inserted into the editing buffer, the 
        ;; todo-category-filed or todo-category-end string, respectively, 
        ;; is not displayed with it.
        (let* ((begin (save-excursion
                        (search-backward-regexp
                         (concat "^" (regexp-quote 
                                      (concat todo-prefix todo-category-beg))))
                        (1+ (line-end-position))))
               (end (save-excursion
                      (re-search-forward (concat "^" todo-category-end))
                      (match-beginning 0)))
               (filed-beg (save-excursion
                            (if (re-search-forward
                                 (concat "^" todo-category-filed "$") end t)
                                (match-beginning 0))))
               (filed-end (save-excursion
                            (if (re-search-backward 
                                 (concat "^" todo-category-filed "$") begin t)
                                (match-end 0)))))
          (cond (filed-end (narrow-to-region filed-end end))
                (filed-beg (narrow-to-region begin filed-beg))
                (t (narrow-to-region begin end))))
        (narrow-to-region (todo-item-start) (todo-item-end)))
    (error "No TODO list entry to edit")))

;;;###autoload
(defun todo-add-category (cat)
  "Add new category CAT to the TODO list."
  (interactive "sCategory: ")
  (if (string= "" cat) (error "A category name must be supplied"))
  (if (member cat todo-categories)
      (error "Category already exists -- choose another name"))
  (save-window-excursion
    (setq todo-categories (cons cat todo-categories))
    (find-file todo-file-do)
    (let ((buffer-read-only nil))
      (widen)
      (goto-char (point-min))
      (let ((posn (search-forward "-*- mode: todo; " 17 t)))
        (if (not (null posn)) (goto-char posn))
        (if (equal posn nil)
            (progn
              (insert "-*- mode: todo; \n")
              (forward-char -1))
          (kill-line)))
      (insert (format "todo-categories: %S; -*-" todo-categories))
      (forward-char 1)
      (insert (format "%s%s%s\n%s\n%s %s\n"
                      todo-prefix todo-category-beg cat
                      todo-category-end
                      todo-prefix todo-category-sep))))
  ;; show the newly added category
  (setq todo-category-number 0)
  (todo-category-select))

(defun todo-rename-category (new)
  "Rename current todo category."
  (interactive "sNew category name: ")
  (let ((cat (nth todo-category-number todo-categories))
        (vec (vconcat todo-categories))
        (buffer-read-only nil))
    (if (string= "" new) (error "A category name must be supplied"))
    (aset vec todo-category-number new)
    (setq todo-categories (append vec nil))
    (save-excursion
      (widen)
      (search-backward (concat todo-prefix todo-category-beg))
      (goto-char (match-end 0))
      (when (looking-at (regexp-quote cat))
        (replace-match new t))
      (goto-char (point-min))
      (when (looking-at (regexp-quote todo-header))
        (goto-char (match-end 0))
        (kill-line)
        (insert (format "todo-categories: %S; -*-" todo-categories)))
      (setq mode-line-buffer-identification
            (concat "Category: " (format "%18s" new)))))
  (todo-category-select))

(defun todo-delete-category ()
  "Delete current TODO category if it contains no filed or unfiled items."
  (interactive)
  (widen)
  (let* ((cat (nth todo-category-number todo-categories))
         (begin0 (search-backward (concat todo-prefix todo-category-beg cat)))
         (begin (1+ (match-end 0)))
         (end (progn
                (re-search-forward (concat "^" todo-category-end "$"))
                (match-beginning 0)))
         (cat-end (1+ (search-forward 
                       (concat todo-prefix " " todo-category-sep))))
         (filed-beg (re-search-backward 
                     (concat "^" todo-category-filed "$") begin t))
         (filed-end (if (not (null filed-beg)) (1+ (match-end 0))))
         (buffer-read-only nil))
          ;; A deletable category must contain neither unfiled items...
    (cond ((or (and (not (null filed-beg)) (not (equal begin filed-beg)))
               (and (null filed-beg) (not (equal begin end))))
           (todo-category-select)
           (error "Cannot delete nonempty category"))
          ;; ...nor filed items.
          ((and (not (null filed-beg)) (not (equal filed-end end)))
           (todo-category-select)
           (todo-display-filed-items-under-cat)
           (error "Cannot delete category with filed items"))
          ;; category is empty, so request deletion confirmation
          (t (if (equal filed-end end) (setq begin filed-end))
             (todo-category-select)
             (if (y-or-n-p (concat "Permanently remove '" cat "'? "))
                 (progn
                   (widen)
                   (kill-region begin0 cat-end)
                   (goto-char (point-min))
                   (when (looking-at (regexp-quote todo-header))
                     (setq todo-categories 
                           (delete cat todo-categories))
                     (goto-char (match-end 0))
                     (kill-line)
                     (insert (format "todo-categories: %S; -*-" 
                                     todo-categories))
                     (todo-category-select)
                     (message "Deleted category \"%s\"" cat)))
               (todo-category-select))))))

(defun todo-display-categories ()
  "Display clickable alphabetical list of TODO categories.
Click or type RET on a category name to go to it."
  (interactive)
  (let ((cat-list todo-categories)
        (buffer-name (concat "*" todo-categories-buffer "*")))
    (with-current-buffer (get-buffer-create buffer-name)
      (pop-to-buffer (current-buffer))
      (let ((buffer-read-only nil))
        (erase-buffer)
        (kill-all-local-variables)
        ;; insert the category names as clickable text
        (while cat-list
          (setq cat (car cat-list))
          (insert cat)
          (add-text-properties (line-beginning-position) (line-end-position)
                               (list 'mouse-face 'highlight))
          (insert "\n")
          (setq cat-list (cdr cat-list)))
        ;; display categories in case-insensitive alphabetical order
        (make-local-variable 'sort-fold-case)
        (let ((begin (point-min))
              (end (point-max))
              (sort-fold-case t))
          (sort-lines nil begin end))
        (goto-char (point-min)))
      (todo-categories-mode)
      ;; to enable todo-category-click under tty (but useless if the tty
      ;; doesn't emulate an xterm with mouse support, e.g. Linux console
      ;; without gpm enabled)
      (if (null window-system) (funcall 'xterm-mouse-mode 1))
      (message "Click or type RET on a category to go to it.  Type \"q\" to 
quit."))))

;;;###autoload
(defun todo-add-item-non-interactively (new-item category)
  "Insert NEW-ITEM in TODO list as a new entry in CATEGORY."
  (save-excursion
    (todo-show))
  (if (string= "" category)
      (setq category (nth todo-category-number todo-categories)))
  (let ((cat-exists (member category todo-categories))
        (buffer-read-only nil))
    (setq todo-category-number
          (if cat-exists
              (- (length todo-categories) (length cat-exists))
            (todo-add-category category)))
    ;; (todo-show) is needed here for todo-item-move, but with a new
    ;; category produces a `let: Wrong type argument: integerp, "^J"' 
    ;; error, the source of which currently escapes me.
    (unless (eq (point-min) (point-max))
      (todo-show))
    ;; need to make separator "\n" to calculate priority, 
    ;; so save current separator for later reinsertion
    (let ((old-sep (save-excursion 
                     (goto-char (point-min))
                     (if (re-search-forward 
                          (concat "\\(\n[^A-Ba-z0-9]*\\)" 
                                  (regexp-quote todo-prefix)) nil t)
                         (match-string 1)
                       "\n"))))
      (unless (eq (point-min) (point-max))
        (funcall 'todo-change-item-sep-in-cat "\n"))
      ;; If the category is still empty, just insert the new item...
      (if (eq 0 (todo-count-items))
          (progn
            (insert new-item todo-item-separator)
            ;; Without the following, `Wrong type argument' errors result,
            ;; for reasons I currently don't understand.  (However, it
            ;; also causes todo-item-move to fail if it is used to create
            ;; a new category, so for now I've had to exclude that
            ;; possibility from todo-item-move.)
            (todo-jump-to-category-noninteractively category))
        ;; ...otherwise, calculate the new item's priority and then insert it.
        (setq todo-previous-line 0)
        (let ((top 1)
              (bottom (1+ (todo-count-items)))
              (last 1))
          (while (> (- bottom top) todo-insert-threshold)
            (let* ((current (/ (+ top bottom) 2))
                   (answer (if (< current bottom)
                               (todo-more-important-p current) nil)))
              (if answer
                  (setq bottom current)
                (setq top (1+ current)))
              (setq last current)))
          (if (todo-more-important-p last)
              (insert new-item todo-item-separator)
            (goto-char (1+ (todo-item-end)))
            (insert new-item todo-item-separator))))
      (todo-backward-item)
      (funcall 'todo-change-item-sep-in-cat old-sep)
;       (todo-save)
      (message ""))))
;; todo-show puts cursor at (point-min) rather than leaving it at the
;; beginning of the new item
;       (todo-show))))

;;;###autoload
(defun todo-insert-item (arg)
  "Insert new TODO list entry.
With a prefix argument solicit the category, otherwise use the current
category."
  (interactive "P")
  (if (not (string-equal mode-name "TODO")) (todo-show))
  (let* ((new-item (concat todo-prefix " "
                           (read-from-minibuffer
                            "New TODO entry: "
                            (if todo-entry-prefix-function
                                (funcall todo-entry-prefix-function)))))
         (completion-ignore-case t)
         (categories todo-categories)
         (history (cons 'categories (1+ todo-category-number)))
         (current-category (nth todo-category-number todo-categories))
         (category
          (if arg
              (completing-read (concat "Category [" current-category "]: ")
                               (todo-category-alist) nil nil nil
                               history current-category)
            current-category)))
    (todo-add-item-non-interactively new-item category)))
(defalias 'todo-cmd-inst 'todo-insert-item)

(defun todo-insert-item-here ()
  "Insert new TODO list entry under the cursor."
  (interactive)
  (if (not (string-equal mode-name "TODO")) (todo-show))
  (let* ((new-item (concat todo-prefix " "
                           (read-from-minibuffer
                            "New TODO entry: "
                            (if todo-entry-prefix-function
                                (funcall todo-entry-prefix-function)))))
         (buffer-read-only nil))
    ;; If the cursor is on the first item in the category, insert this
    ;; item immediately above, making it the new first item, and add
    ;; an item separator below it; if the cursor is on any other item, 
    ;; insert this item directly below, putting a separator in between; 
    ;; if the cursor is between two items (i.e. on a separator string), 
    ;; insert this item in between.  Puts the cursor at the beginning of
    ;; the new item.
    (cond ((eq (point-min) (point-max)) ; category has no unfiled items
           (insert (concat new-item todo-item-separator)))
          ((todo-first-in-category-p)
           (goto-char (point-min))
           (insert (concat new-item todo-item-separator))
           (goto-char (point-min)))
          ((and (todo-item-string-p) (not (todo-last-in-category-p)))
           (goto-char (todo-item-end))
           (insert (concat todo-item-separator new-item)))
          ((and (todo-item-separator-p) (not (eobp)))
           (re-search-forward (regexp-quote todo-prefix) nil 1 nil)
           (goto-char (1- (match-beginning 0)))
           (insert (concat "\n" new-item todo-item-separator))
           (delete-backward-char 1))
          ((or (todo-last-in-category-p) (eobp))
           (goto-char (point-max))
           (if (looking-at "^$") (delete-backward-char 1))
           (forward-line)
           (insert (concat todo-item-separator new-item "\n")))))
  (goto-char (todo-item-start)))

(defun todo-more-important-p (line)
  "Ask whether entry is more important than the one at LINE."
  (if (not (equal todo-previous-line line))
      (progn
        (setq todo-previous-line line)
        (goto-char (point-min))
        (todo-forward-item (1- todo-previous-line))
        (let ((item (todo-item-string-start)))
          (setq todo-previous-answer
                (y-or-n-p (concat "More important than '" item "'? "))))))
  todo-previous-answer)
(defalias 'todo-ask-p 'todo-more-important-p)

(defun todo-delete-item ()
  "Delete current TODO list entry."
  (interactive)
  (if (and (todo-item-string-p)
           (> (count-lines (point-min) (point-max)) 0))
      (let* ((todo-entry (todo-item-string-start))
             (todo-answer (y-or-n-p (concat "Permanently remove '"
                                            todo-entry "'? ")))
             (buffer-read-only nil))
        (if todo-answer
            (if (todo-last-in-category-p)
                (progn
                  (todo-remove-item)
                  (if (search-backward todo-item-separator (point-min) t)
                      (progn
                        (delete-region (point) (point-max))
                        (newline)
                        (todo-backward-item))))
              (todo-remove-item)))
        ;; uncomment to make the cursor move to the item immediately
        ;; preceding the deleted item
;       (todo-backward-item)))
        (message ""))
    (error "No TODO list entry to delete")))
(defalias 'todo-cmd-kill 'todo-delete-item)

(defun todo-raise-item ()
  "Raise priority of current entry."
  (interactive)
  (if (and (> (count-lines (point-min) (point)) 0)
           (and (todo-item-string-p) (not (eobp)))
           (not (todo-first-in-category-p)))
      (let ((item (todo-item-string))
            (buffer-read-only nil))
        (todo-remove-item)
        (todo-backward-item)
        (if (todo-last-in-category-p)
            (save-excursion
              (goto-char (todo-item-end))
              (newline)
              ;; don't leave item separator dangling after the last item
              (let ((bound (save-excursion (search-backward todo-prefix)
                                           (match-end 0))))
                (if (search-backward todo-item-separator bound t)
                    nil)
                (delete-region (point) (point-max))
                (newline))))
          (save-excursion (insert item todo-item-separator))
          (message ""))
      (error "No TODO list entry to raise")))
(defalias 'todo-cmd-rais 'todo-raise-item)

(defun todo-lower-item ()
  "Lower priority of current entry."
  (interactive)
  (if (and (> (count-lines (point) (point-max)) 1)
           (todo-item-string-p) (not (todo-last-in-category-p)))
      ;; Assume there is a final newline
      (let ((item (todo-item-string))
            (buffer-read-only nil))
        (todo-remove-item)
        (if (todo-last-in-category-p) 
            (goto-char (point-max)) 
          (todo-forward-item))
        (if (eobp) (progn (delete-backward-char 1)
                          (insert todo-item-separator item))
          (insert item todo-item-separator)
          (re-search-backward (regexp-quote todo-prefix)))
        (if (equal (point) (point-max))
            (progn (auto-fill-mode nil) (newline) (forward-line -1)
                   (auto-fill-mode t)))
        (message ""))
    (error "No TODO list entry to lower")))
(defalias 'todo-cmd-lowr 'todo-lower-item)

(defun todo-file-item ()
  "File the current TODO list entry away, annotated with an optional comment."
  (interactive)
  (or (> (count-lines (point-min) (point-max)) 0)
      (error "No TODO list entry to file away"))
  (if (todo-item-string-p)
      (if (y-or-n-p "Put this item in the done items file? ")
          (let ((time-stamp-format todo-time-string-format)
                (cat (nth todo-category-number todo-categories))
                (bare-item (buffer-substring
                            (progn
                              (goto-char (todo-item-start))
                              (search-forward todo-initials))
                            (todo-item-end)))
                (old-timestamp (buffer-substring
                                (1+ (progn (goto-char (todo-item-start))
                                           (search-forward todo-prefix)))
                                (search-forward todo-initials)))
                (fill-prefix " ")
                (buffer-read-only nil))
            ;; If item is already filed under its category...
            (if (eq major-mode 'todo-filed-mode)
                (save-excursion
                  (save-restriction
                    (widen)
                    ;; ...first find the category name
                    (let ((fcat (save-excursion
                                  (re-search-backward 
                                   (concat "^" (regexp-quote todo-prefix)
                                           (regexp-quote todo-category-beg)
                                           "\\(.+\\)" "$"))
                                  (match-string 1)))
                          ;; ...then update the filed item's timestamp but
                          ;; don't added a (new) comment before filing away.
                          (fitem (buffer-substring 
                                  (progn
                                    (goto-char (todo-item-start))
                                    (re-search-forward 
                                     (concat (regexp-quote todo-prefix) 
                                             "[^\\[]*\\["))
                                    (point))
                                  (progn
                                    (goto-char (todo-item-end))
                                    (beginning-of-line)
                                    ;; last filed item should not grab
                                    ;; todo-category-end
                                    (if (looking-at todo-category-end)  
                                        (1- (point))
                                      (todo-item-end))))))
                      (write-region (concat (time-stamp-string) 
                                            " [" fcat ": " fitem "\n")
                                    'end todo-file-done t -1))))
              ;; If item is unfiled, solicit comment before filing.
              (let ((comment (read-from-minibuffer "Comment: ")))
                (write-region (concat (time-stamp-string) " [" cat
                                      ": " old-timestamp "]" bare-item
                                      " [Comment: " comment "]\n")
                              'end todo-file-done t -1)))
            (todo-remove-item)
            (message "Item filed in %s." todo-file-done))
        (message ""))
    (error "Not a TODO list entry")))


(defun todo-file-item-under-cat ()
   "File the current TODO list entry under its category, comment optional.

The filed items are kept in a separate region of the category, and can be
displayed by typing \\[todo-display-filed-items-under-cat]."
   (interactive)
   (or (> (count-lines (point-min) (point-max)) 0)
       (error "No TODO list entry to file away"))
   (if (todo-item-string-p)
       (if (y-or-n-p "File this item under its category? ")
           (let ((time-stamp-format todo-time-string-format)
                 (cat (nth todo-category-number todo-categories))
                 (bare-item (buffer-substring
                             (progn
                               (goto-char (todo-item-start))
                               (search-forward todo-initials))
                             (progn
                               (goto-char (todo-item-end))
                               (goto-char (line-beginning-position))
                               ;; exclude todo-category-end from bare-item
                               (if (looking-at 
                                    (regexp-quote todo-category-end))
                                   (progn
                                     (backward-char)
                                     (point))
                                 (todo-item-end)))))
                 (old-timestamp (buffer-substring
                                 (1+ (progn (goto-char (todo-item-start))
                                            (search-forward todo-prefix)))
                                 (search-forward todo-initials)))
                 (comment (read-from-minibuffer "Comment: "))
                 (fill-prefix " ")
                 (buffer-read-only nil))
             (save-excursion
               (save-restriction
                 (widen)
                 ;; make sure category is set up to file items
                 (let ((end (save-excursion
                              (re-search-forward 
                               (concat "^" todo-category-end "$"))
                              (match-beginning 0))))
                   (if (re-search-forward 
                        (concat "^" todo-category-filed "$") end t)
                       (newline)
                     (goto-char end)
                     (insert todo-category-filed "\n")))
                 (insert todo-prefix " " (time-stamp-string)
                         " [" old-timestamp "]" bare-item)
                 (if (and comment (> (length comment) 0))
                     (insert " [Comment: " comment "]"))
                 (if (looking-at todo-category-end) (insert "\n"))
                 (fill-region (todo-item-start) (point))))
             ;; make sure todo-category-filed string remains in place
             (if (string-match (concat "^" todo-category-filed "$")
                               (todo-item-string))
                 (progn
                   (todo-remove-item)
                   (insert todo-category-filed "\n"))
               (todo-remove-item))
             (todo-category-select)
             (message "Item filed.  Press `d' to view filed items in this 
category."))
         (message ""))
     (error "Not a TODO list entry")))

(defun todo-display-filed-items-under-cat ()
  "Display a buffer of the filed TODO items in the current category.

The filed items are displayed in a window below the unfiled items
of that category.  This display uses a special major mode, which contains
a proper subset of the todo-mode key bindings."
  (interactive)
  (goto-char (point-min))
  (widen)
  (let* ((cat (nth todo-category-number todo-categories))
         (end (save-excursion 
                (re-search-forward (concat "^" todo-category-end "$"))
                (match-beginning 0)))
         (begin (re-search-forward 
                 (concat "^" todo-category-filed "$") end t))
         (name (generate-new-buffer-name todo-filed-buffer))
         (buffer (make-indirect-buffer
                  (file-name-nondirectory todo-file-do) name)))
    (if (or (null begin) (equal (1+ begin) end))
        (progn
          (todo-category-select)
          (delete-other-windows)
          (error "No filed items in this category"))
      (todo-category-select)
      (set-buffer buffer)
      (todo-filed-mode)
      (narrow-to-region (1+ begin) end)
      (setq mode-line-buffer-identification
            (concat todo-filed-buffer (format "%18s" cat)))
      (display-buffer buffer)
      (message "Type C-x o C-h m to list the functions available in 
todo-filed-mode."))))
;       (message "Type C-x 1 to remove filed items or M-C-v to scroll them."))))

(defun todo-move-item ()
  "Move the current todo item to another category."
  (interactive)
  (if (todo-item-string-p)
      (let* ((item (todo-item-string))
             (categories todo-categories)
             (completion-ignore-case t)
             (history (cons 'categories (1+ todo-category-number)))
             (category (completing-read (concat "Move to category: ")
                                        (todo-category-alist) nil nil nil
                                        history))
             (buffer-read-only nil))
        (if (todo-last-in-category-p)
            (progn
              (todo-remove-item)
              ;; don't leave item separator dangling at the bottom
              (if (search-backward todo-item-separator (point-min) t)
                  (progn
                    (delete-region (point) (point-max))
                    (newline)
                    (todo-backward-item))))
          (todo-remove-item))
        ;; currently can't create a category by moving into it (fails
        ;; when todo-add-item-non-interactively is called, but the
        ;; source of the problem currently escapes me)
        (if (member category todo-categories)
            (todo-jump-to-category-noninteractively category)
          (save-excursion (insert item todo-item-separator))
          (error "Cannot move item to a nonexisting category"))
        (todo-add-item-non-interactively item category)
        (message ""))
    (error "Cursor must be on a TODO entry to move it")))

;; This function was inspired by a feature request from Daniel Ortmann
;; on gnu.emacs.bug [srb]
(defun todo-change-item-sep-in-cat (&optional new)
  "Change the todo item separator in the current category."
  (interactive)
  (save-excursion 
    ;; the current separator string in this category (assuming the separator 
    ;; string does not contain any alphanumeric character and the last line 
    ;; of a todo item does contain at least one alphanumeric character)
    (let ((sep (save-excursion 
                 (goto-char (point-min))
                 (if (re-search-forward 
                      (concat "\\(\n[^A-Ba-z0-9]*\\)" 
                              (regexp-quote todo-prefix)) nil t)
                     (match-string 1)
                   "\n"))))
      (unless (string-equal todo-item-separator sep)
        (setq todo-item-separator sep)))
    (save-excursion 
      (goto-char (point-min))
      (when (interactive-p)
        (unless (search-forward todo-prefix nil t 2)
          (error "There must be at least two items to change the separator"))))
    ;; the new separator string
    (unless new
      (setq new (read-from-minibuffer 
                 (concat "Current item separator string is " 
                         todo-item-separator ".  Replace with: "))))
    (let ((buffer-read-only nil))
      (unless (string-match "\\(^\n$\\|^\n.*\n$\\)" new)
        (error "Separator string must begin and end with a newline"))
      (goto-char (point-min))
      (while (not (todo-last-in-category-p)) ; no separator below last item
        (goto-char (todo-item-end))
        (if (re-search-forward todo-item-separator) 
            (replace-match new)
          (re-search-forward (concat "\\(\n.*\\)" 
                                     (regexp-quote todo-prefix)))
          (goto-char (match-beginning 0))
          (replace-match new nil nil nil 1)
          (forward-char)))
      (setq todo-item-separator new))
    (message "New separator string: %s" todo-item-separator)))

(defun todo-change-todo-prefix ()
  "Change the TODO prefix to a new string.

Afterwards, a customization buffer for Todo Prefix is visited, so you
can save the change for future sessions.  If you quit Emacs without doing
this, problems will result in the next TODO session."
  (interactive)
  (let* ((new (read-from-minibuffer 
               (concat "Current item prefix string is " 
                       todo-prefix ".  Replace with: ")))
         (buffer-read-only nil))
    (save-excursion 
      (save-restriction
        (widen)
        (goto-char (point-min))
        (while (search-forward todo-prefix nil t)
          (replace-match new nil nil))))
    (setq todo-prefix new)
    (todo-category-select))
  (customize-apropos "todo-prefix")
  (search-forward "Todo Prefix")
  (goto-char (match-beginning 0))
  (message "Save for future sessions to make available in next TODO session."))

;; This function was inspired by a question posted by Kai Großjohann
;; on gnu.emacs.help [srb]
(defun todo-toggle-item-header (arg)
  "Toggle display of TODO item timestamp and initials.
With prefix argument toggle display of TODO item prefix."
  (interactive "P")
  (goto-char (point-min))
  (while (not (eobp))
    (let ((begin (if arg 
                     (progn (or (search-forward todo-prefix nil t) 
                                (error "Done"))
                            (match-beginning 0))
                   (or (re-search-forward 
                        (concat (regexp-quote todo-prefix) 
                                "\\(.*[0-9]+\\) " 
                                (regexp-quote todo-initials) 
                                "\\(]?: \\)") nil t)
                       (error "Done"))
                   (match-beginning 1)))
          (end (if arg 
                   (1+ (match-end 0))
                 (1- (match-end 2))))
          (buffer-read-only nil))
      (or (unless (not (next-single-property-change (point) '(invisible t) 
                                                    nil end))
            (add-text-properties begin end '(invisible t)))
          (remove-text-properties begin end '(invisible t))))
    (goto-char (todo-item-end))
    (save-buffer 0)))

;; ---------------------------------------------------------------------------

;; Utility functions:


;;;###autoload
(defun todo-top-priorities (&optional nof-priorities category-pr-page)
  "List top priorities for each category.

Number of entries for each category is given by NOF-PRIORITIES which
defaults to \'todo-show-priorities\'.

If CATEGORY-PR-PAGE is non-nil, a page separator \'^L\' is inserted
between each category."

  (interactive "P")
  (or nof-priorities (setq nof-priorities todo-show-priorities))
  (if (listp nof-priorities)            ;universal argument
      (setq nof-priorities (car nof-priorities)))
  (let ((todo-print-buffer-name todo-tmp-buffer-name)
        ;;(todo-print-category-number 0)
        (todo-category-break (if category-pr-page "" ""))
        (cat-end
         (concat
          (if todo-remove-separator
              (concat todo-category-end "\n"
                      (regexp-quote todo-prefix) " " todo-category-sep "\n")
            (concat todo-category-end "\n"))))
        beg end)
    (todo-show)
    (save-excursion
      (save-restriction
        (widen)
        (copy-to-buffer todo-print-buffer-name (point-min) (point-max))
        (set-buffer todo-print-buffer-name)
        (goto-char (point-min))
        (when (re-search-forward (regexp-quote todo-header) nil t)
          (beginning-of-line 1)
          (delete-region (point) (line-end-position)))
        (while (re-search-forward       ;Find category start
                (regexp-quote (concat todo-prefix todo-category-beg))
                nil t)
          (setq beg (+ (line-end-position) 1)) ;Start of first entry.
          (re-search-forward cat-end nil t)
          (setq end (match-beginning 0))
          (replace-match todo-category-break)
          (narrow-to-region beg end)    ;In case we have too few entries.
          (goto-char (point-min))
          (if (= 0 nof-priorities)      ;Traverse entries.
              (goto-char end)            ;All entries
            (todo-forward-item nof-priorities))
          (setq beg (point))
          (delete-region beg end)
          (widen))
        (and (looking-at "") (replace-match "")) ;Remove trailing form-feed.
        (goto-char (point-min))         ;Due to display buffer
        ))
    ;; Could have used switch-to-buffer as it has a norecord argument,
    ;; which is nice when we are called from e.g. todo-print.
    ;; Else we could have used pop-to-buffer.
    (display-buffer todo-print-buffer-name)
    (message "Type C-x 1 to remove %s window.  M-C-v to scroll the help."
             todo-print-buffer-name)))

(defun todo-save-top-priorities (&optional nof-priorities)
  "Save top priorities for each category in `todo-file-top'.

Number of entries for each category is given by NOF-PRIORITIES which
defaults to `todo-show-priorities'."
  (interactive "P")
  (save-window-excursion
    (save-excursion
      (save-restriction
        (todo-top-priorities nof-priorities)
        (set-buffer todo-tmp-buffer-name)
        (write-file todo-file-top)
        (kill-this-buffer)))))

;;;###autoload
(defun todo-print (&optional category-pr-page)
  "Print todo summary using `todo-print-function'.
If CATEGORY-PR-PAGE is non-nil, a page separator `^L' is inserted
between each category.

Number of entries for each category is given by `todo-print-priorities'."
  (interactive "P")
  (save-window-excursion
    (save-excursion
      (save-restriction
        (todo-top-priorities todo-print-priorities
                             category-pr-page)
        (set-buffer todo-tmp-buffer-name)
        (and (funcall todo-print-function)
             (kill-this-buffer))
        (message "Todo printing done.")))))

(defun todo-jump-to-category ()
  "Jump to a category.  Default is previous category."
  (interactive)
  (let* ((categories todo-categories)
         (completion-ignore-case t)
         (history (cons 'categories (1+ todo-category-number)))
         (default (nth todo-category-number todo-categories))
         (category (completing-read
                    (concat "Category [" default "]: ")
                    (todo-category-alist) nil nil nil history default)))
    (if (string= "" category)
        (setq category (nth todo-category-number todo-categories)))
    (setq todo-category-number
          (if (member category todo-categories)
              (- (length todo-categories)
                 (length (member category todo-categories)))
            (todo-add-category category)))
    (todo-show)))

;; called by todo-category-click and todo-category-return, and also 
;; needed (to avoid mysterious errors I currently don't understand) by 
;; todo-add-item-non-interactively and todo-move-item
(defun todo-jump-to-category-noninteractively (cat)
  (let ((name (concat "*" todo-categories-buffer "*")))
    (if (string= (buffer-name) name)
        (kill-buffer name)))
  (switch-to-buffer (file-name-nondirectory todo-file-do))
  (delete-other-windows)
  (widen)
  (goto-char (point-min))
  (setq todo-category-number (- (length todo-categories)
                                (length (member cat todo-categories))))
  (todo-category-select))

(defun todo-category-click (event)
  (interactive "e")
  (let (cat)
    (save-excursion
      (set-buffer (window-buffer (posn-window (event-end event))))
      (save-excursion
        (goto-char (posn-point (event-end event)))
        (setq cat (buffer-substring (line-beginning-position) 
                                    (line-end-position))))
      (select-window (posn-window (event-end event)))
      (todo-jump-to-category-noninteractively cat))))

(defun todo-category-return ()
  (interactive)
  (let ((cat (buffer-substring (line-beginning-position) (line-end-position))))
    (save-excursion                     ; alternatively:
      (beginning-of-line)               ; (require 'thingatpt)
      (if (looking-at cat)              ; (if (thing-at-point-looking-at cat)
        (todo-jump-to-category-noninteractively cat)))))

(defun todo-line-string ()
  "Return current line in buffer as a string."
  (buffer-substring (line-beginning-position) (line-end-position)))

(defun todo-item-string-start ()
  "Return the start of this TODO list entry as a string."
  ;; Suitable for putting in the minibuffer when asking the user
  (let ((item (todo-item-string)))
    (if (> (length item) 60)
        (setq item (concat (substring item 0 56) "...")))
    item))

(defun todo-item-start ()
  "Return point at start of current TODO list item."
  (save-excursion
    (beginning-of-line)
    (if (not (looking-at (regexp-quote todo-prefix)))
        (search-backward-regexp
         (concat "^" (regexp-quote todo-prefix)) nil t))
    (point)))

(defun todo-item-end ()
  "Return point at end of current TODO list item."
  (save-excursion
    (end-of-line)
    (or (if (re-search-forward 
             (concat (regexp-quote todo-item-separator)
;                      "^"  ; see Elisp manual, Regexp Special
                     (regexp-quote todo-prefix)) nil t)
            (goto-char (match-beginning 0))) 
        (goto-char (1- (point-max)))))) ; no separator after last item

(defun todo-remove-item ()
  "Delete the current entry from the TODO list."
  (if (string-equal todo-item-separator "\n")
      (delete-region (todo-item-start) (1+ (todo-item-end)))
    (if (todo-last-in-category-p)
        (delete-region (todo-item-start) (point-max))
      (delete-region (todo-item-start) 
                     (re-search-forward (regexp-quote todo-item-separator))))))

(defun todo-item-string ()
  "Return current TODO list entry as a string."
  (buffer-substring (todo-item-start) (todo-item-end)))

(defun todo-string-count-lines (string)
  "Return the number of lines STRING spans."
  (length (split-string string "\n")))

(defun todo-string-multiline-p (string)
  "Return non-nil if STRING spans several lines."
  (> (todo-string-count-lines string) 1))

(defun todo-category-alist ()
  "Generate an alist for use in `completing-read' from `todo-categories'."
  (mapcar #'list todo-categories))

(defun todo-item-separator-p ()
  "Return non-nil if point is within a TODO item separator string."
  (unless (string-equal todo-item-separator "\n")
    (string-match todo-item-separator (todo-item-string) 0)))

(defun todo-item-string-p ()
  "Return non-nil if point is on a TODO item."
  (unless (or (todo-item-separator-p) (eobp))
    (or (looking-at (regexp-quote todo-prefix))
        (save-excursion
          (goto-char (todo-item-end))
          (looking-at (regexp-quote todo-item-separator)))
        (todo-last-in-category-p))))

(defun todo-first-in-category-p ()
  "Return non-nil if point is on the current first item in the category."
  (and (todo-item-string-p)
       (save-excursion
         (end-of-line)
         (and (re-search-backward (regexp-quote todo-prefix) nil t)
              (not (re-search-backward (regexp-quote todo-prefix) nil t))))))

(defun todo-last-in-category-p ()
  "Return non-nil if point is on the current last item in the category."
  (unless (eobp)
    (save-excursion
      (end-of-line)
      (re-search-backward (regexp-quote todo-prefix) nil t)
      (end-of-line)
      (not (re-search-forward (regexp-quote todo-prefix) nil t)))))

(defun todo-count-items ()
  "Return number of unfiled items in the current category."
  (goto-char (point-min))
  (let ((count 0))
    (save-excursion
      (while (not (eobp))
        (re-search-forward (concat "^" todo-prefix))
        (setq count (1+ count))
        (goto-char (1+ (todo-item-end))))
    count)))

;; ---------------------------------------------------------------------------

(easy-menu-define todo-menu todo-mode-map "Todo Menu"
                  '("Todo"
                    ["Show Categories"      todo-display-categories t]
                    ["Next Category"        todo-forward-category t]
                    ["Previous Category"    todo-backward-category t]
                    ["Jump to Category"     todo-jump-to-category t]
                    ["Rename Category"      todo-rename-category t]
                    ["Add New Category"     todo-add-category t]
                    ["Delete Category"      todo-delete-category t]
                    ["Show Top Priority Items" todo-top-priorities t]
                    ["Print Categories"     todo-print t]
                    "---"
                    ["Edit Item"            todo-edit-item t]
                    ["File Item"            todo-file-item t]
                    ["File Item under Category" todo-file-item-under-cat t]
                    ["Display Filed Items in Category" 
todo-display-filed-items-under-cat t]
                    ["Insert New Item"      todo-insert-item t]
                    ["Insert New Item Here" todo-insert-item-here t]
                    ["Move Item to Category" todo-move-item t]
                    ["Change Item Separator" todo-change-item-sep-in-cat t]
                    ["Change Item Prefix"   todo-change-todo-prefix t]
                    ["Toggle Item Header"   todo-toggle-item-header t]
                    ["Kill Item"            todo-delete-item t]
                    "---"
                    ["Lower Item Priority"  todo-lower-item t]
                    ["Raise Item Priority"  todo-raise-item t]
                    "---"
                    ["Next Item"            todo-forward-item t]
                    ["Previous Item"        todo-backward-item t]
                    "---"
                    ["Save"                 todo-save t]
                    ["Save Top Priorities"  todo-save-top-priorities t]
                    "---"
                    ["Customize" (customize-group "todo") t]
                    "---"
                    ["Quit"                 todo-quit t]
                    ))

(easy-menu-define todo-filed-menu todo-filed-mode-map "Todo Filed Menu"
                  '("Todo"
                    ["Edit Item"            todo-edit-item t]
                    ["File Item"            todo-file-item t]
                    ["Change Item Separator" todo-change-item-sep-in-cat t]
                    ["Toggle Item Header"   todo-toggle-item-header t]
                    ["Kill Item"            todo-delete-item t]
                    ["Next Item"            todo-forward-item t]
                    ["Previous Item"        todo-backward-item t]
                    ["Save"                 todo-save t]
                    ["Quit"                 todo-kill-buffer t]
                    ))

;; As calendar reads .todo-do before todo-mode is loaded.
;;;###autoload
(defun todo-mode ()
  "Major mode for editing TODO lists.

\\{todo-mode-map}"
  (interactive)
  (kill-all-local-variables)
  (setq major-mode 'todo-mode)
  (setq mode-name "TODO")
  (make-local-variable 'next-line-add-newlines)
  (setq next-line-add-newlines t)
  (make-local-variable 'buffer-read-only)
  (setq buffer-read-only todo-buffer-read-only)
  (use-local-map todo-mode-map)
  (easy-menu-add todo-menu)
  (todo-advice)
  (run-hooks 'todo-mode-hook))

(defvar todo-disabled-functions '(todo-add-category todo-backward-category 
todo-delete-category todo-display-filed-items-under-cat todo-display-categories 
todo-file-item-under-cat todo-forward-category todo-insert-item 
todo-insert-item-here todo-jump-to-category todo-lower-item todo-move-item 
todo-raise-item todo-rename-category)
  "List of TODO functions to be disabled outside of todo-mode.")

;; As of Emacs 21.1 `dolist' is defined in subr.el instead of cl.el
(if (< emacs-major-version 21) (eval-when-compile (require 'cl)))

;; Thanks to Barry Margolin <address@hidden> and Kevin Rodgers
;; <address@hidden>, respectively, for help with the formulation of
;; this function and the advice it contains. [srb]
;;;###autoload
(defun todo-advice ()
  "Disable specified functions when called outside todo-mode."
  (dolist (fn todo-disabled-functions)
    (eval `(defadvice ,fn (around todo-disable first (&rest args) activate)
             (interactive)
             (cond ((not (eq major-mode 'todo-mode))
                    (error "Disabled in %s mode" mode-name))
                   ((interactive-p)
                    (let ((prefix-arg current-prefix-arg))
                      (call-interactively (ad-make-origname (quote ,fn)))))
                   (t ad-do-it))))))

(eval-when-compile
  (defvar date)
  (defvar entry))

;; Read about this function in the setup instructions above!
;;;###autoload
(defun todo-cp ()
  "Make a diary entry appear only in the current date's diary."
  (if (equal (calendar-current-date) date)
      entry))

(define-derived-mode todo-edit-mode text-mode "TODO Edit"
  "Major mode for editing items in the TODO list.

\\{todo-edit-mode-map}"
  (local-set-key (kbd "C-c C-c") 'todo-kill-buffer)
  (add-hook 'todo-edit-mode-hook 'turn-on-auto-fill))

(defun todo-categories-mode ()
  "Major mode for displaying TODO categories and choosing one to visit.

\\{todo-categories-mode-map}"
  (interactive)
  (kill-all-local-variables)
  (use-local-map todo-categories-mode-map)
  (setq major-mode 'todo-categories-mode)
  (setq mode-name "TODO categories")
  (setq buffer-read-only todo-buffer-read-only))

(defun todo-filed-mode ()
  "Major mode for handling filed TODO items.

\\{todo-filed-mode-map}"
  (kill-all-local-variables)
  (setq major-mode 'todo-filed-mode)
  (setq mode-name "TODO Filed")
  (setq buffer-read-only todo-buffer-read-only)
  (use-local-map todo-filed-mode-map)
  (easy-menu-add todo-filed-menu))

(defun todo-kill-buffer (&rest ignore)
  "Kill the current buffer and return to the TODO item list.
Used by `todo-categories-mode', `todo-edit-mode', and `todo-filed-mode'.
IGNORE arguments."
  (interactive)
  (if (or (eq major-mode 'todo-categories-mode)
          (eq major-mode 'todo-edit-mode)
          (eq major-mode 'todo-filed-mode))
      (progn
        (kill-buffer (current-buffer))
        (switch-to-buffer (file-name-nondirectory todo-file-do))
        (delete-other-windows)
        (message ""))
    (error "%s cannot be executed in %s" this-command major-mode)))
 
;;;###autoload
(defun todo-show ()
  "Show TODO list."
  (interactive)
  (if (file-exists-p todo-file-do)
      (find-file todo-file-do)
    (todo-initial-setup))
  (if (null todo-categories)
      (if (null todo-cats)
          (error "Error in %s: No categories in list `todo-categories'"
                 todo-file-do)
        (goto-char (point-min))
        (and (search-forward "todo-cats:" nil t)
             (replace-match "todo-categories:"))
        (make-local-variable 'todo-categories)
        (setq todo-categories todo-cats)))
  (beginning-of-line)
  (todo-category-select))

(defun todo-initial-setup ()
  "Set up things to work properly in TODO mode."
  (find-file todo-file-do)
  (erase-buffer)
  (let ((first (read-from-minibuffer "Enter a new category: ")))
    (todo-add-category first))
  (write-file todo-file-do)
  (todo-mode))

;; make sure user's todo-mode customizations are used
(unless (null custom-file) (load-file custom-file))

(provide 'todo-mode)

;;; todo-mode.el ends here




reply via email to

[Prev in Thread] Current Thread [Next in Thread]