battle-plan.el
is a personal to do/checklist manager for GNU Emacs. It is designed to be non-intrusive and not interfere with editing. Of course, it uses a plain text format that plays nicely with other tools, such as Git and grep.
Each item is a single line. There are three types of items. Lines are prefixed with -
for pending items, +
for completed items, and /
for "won't do" (when you change your mind, but still wish to document your history). Items are color-coded, providing a quick and visually satisfying overview.
M-c
toggles the current item between pending/completed, with automatic sorting. Completed items drop to the bottom, while pending items (perhaps you needed to re-open a bug?) pop to the top. Use empty lines to group items logically; toggling will only relocate an item within its local group.
There's also a shorthand for adding stand-out :: Headers
, which has no programmatic meaning but helps arrange items in a meaningful way.
Items can have any number of #tags
which can be used to track progress. Found a bug that needs to be fixed for version 1.3? Tag it #bug #1.3
.
C-c ;
generates a report listing all tags and how many pending/completed items there are for each. Selecting a line in the report with RET
or mouse-2
will open an occur
buffer listing all items with the selected tag. Selecting a line in the occur buffer (as per standard occur behaviour) will jump to that specific line in the to do-list. DEL
(that is; backspace) in the occur buffer will return to the report buffer.
Installation is simple:
battle-plan.el
(or copy it from below)~/.emacs.d/
)(require 'battle-plan)
to your ~/.emacs
configOnce installed, battle-plan will automatically detect filenames containing the phrase TODO
. It can also be manually activated with M-x battle-plan-mode
.
I strongly recommend using move-lines.el
(or something that provides the same functionality). I personally have S-up
and S-down
bound to move the current line or region up and down, which lets me quickly rearrange to do-items (transpose-lines
is kinda dodgy). However, this is purely in the realm of text editing and outside the scope for battle-plan
, so no such routines are included here.
I also recommend using volatile-highlights-mode
, as it will provide visual feedback where items end up as they are toggled and relocated.
;; ;; Happy Pony Land presents ;; ;; ▛▀▖ ▞▀▖ ▀▛▘ ▀▛▘ ▌ ▛▀▘ ▛▀▖ ▌ ▞▀▖ ▛▖▐ ▐▀▀ ▐ ;; ▛▀▖ ▛▀▌ ▌ ▌ ▌ ▛▘ ▝▀ ▛▀ ▌ ▛▀▌ ▌▝▟ ▐▀ ▐ ;; ▀▀ ▘ ▘ ▘ ▘ ▀▀▘ ▀▀▘ ▘ ▀▀▘ ▘ ▘ ▘ ▝ ▝ ▝▀▀ ▝▀▀ ;; ;; battle-plan.el is a personal to do manager for GNU Emacs. ;; ;; See http://www.happyponyland.net/battle-plan for more information. ;; ;; Version: 2018-09-08 ;; ;; Author: ulf.astrom@gmail.com ;; ;; License: By using, modifying or distributing this Software, ;; you pledge your Soul to be consumed by the Dark One. ;; ;;;###autoload (defvar battle-plan-mode-map nil "Keymap for battle-plan-mode.") (setq battle-plan-mode-map (make-sparse-keymap)) (define-key battle-plan-mode-map [M-c] 'bplan-dwim) (define-key battle-plan-mode-map (kbd "C-c ;") 'bplan-report) ;; Face definitions: the colors used for items, tags and headers. (defface bplan-pending '((t :foreground "light goldenrod")) "Face for battle-plan pending tasks." :group 'battle-plan-mode) (defface bplan-done '((t :foreground "spring green")) "Face for battle-plan completed tasks." :group 'battle-plan-mode) (defface bplan-wont-do '((t :foreground "coral4")) "Face for battle-plan tasks that won't be done." :group 'battle-plan-mode) (defface bplan-tag '((t :foreground "cyan3" :weight extra-bold)) "Face for battle-plan #tags in to do tasks." :group 'battle-plan-mode) (defface bplan-header '((t :foreground "light salmon" :underline t :weight extra-bold )) "Face for battle-plan headers." :group 'battle-plan-mode) (defvar bplan-pending 'bplan-pending "") (defvar bplan-done 'bplan-done "") (defvar bplan-wont-do 'bplan-wont-do "") (defvar bplan-tag 'bplan-tag "") (defvar bplan-header 'bplan-header "") ;; Syntax highlighting: used by font lock to set the faces above. (setq battle-plan-format '(("^- .*$" . bplan-pending) ("^\+ .*$" . bplan-done) ("^\/ .*$" . bplan-wont-do) ("^:: .*$" . bplan-header) ("#[[:graph:]]*" . (0 'bplan-tag t)) ) ) (define-derived-mode battle-plan-mode nil "Battle-Planner" "Major mode for managing personal to do-lists." (setq bplan-report-buf-name "*battle-plan-report*" bplan-occur-buf-name "*battle-plan-occur*" font-lock-keywords-only t font-lock-defaults '(battle-plan-format)) ) (defun bplan-rewind-markers () "Determine where -+/ lines should end up relative to the current line. Returns a list with three markers." (beginning-of-line) ;; At this point we don't know what the caller wants to use the ;; markers for, but to determine proper placement we need to ;; calculate all of them anyway, so we'll just return all three. ;; Default: start of current line (setq bplan-pending-insert (point-marker) bplan-done-insert (point-marker) bplan-wont-insert (point-marker)) ;; Move forward so we can match prefixes backwards on this line. (forward-char 2) ;; Decide where to insert a - line (ideally at the start of the ;; current - line block). This can match several different patterns: ;; ;; (note: "whitespace" here refers to space and tab) ;; ;; - an empty line (w/ or w/o whitespace) followed by "/ ", "+ " or "- " ;; - a line with a header (:: + any text) and a newline ;; - two empty lines (to break free from a non-to do block) ;; - an empty line at the start of buffer, followed by /, + or - ;; - a header at the start of buffer ;; - the start of buffer ;; ;; The value returned by cond is how many lines we must skip forward ;; to get to the proper position, e.g. if we match to the start of ;; buffer, there is only one newline between point and desired ;; insertion position. If we match _only_ start of buffer, we don't ;; need to move at all. ;; ;; We match "/ " and "+ " before "- " since if these are preceeded ;; by empty lines, they are considered the start of the group, even ;; if there are "- " blocks further up in the buffer. (forward-line (cond ((search-backward-regexp "\n[ \t]*\n/ " nil t) 2) ((search-backward-regexp "\n[ \t]*\n\\+ " nil t) 2) ((search-backward-regexp "\n[ \t]*\n- " nil t) 2) ((search-backward-regexp "\n:: .*\n" nil t) 2) ((search-backward-regexp "\n[ \t]*\n" nil t) 1) ((search-backward-regexp "\\`[ \t]*\n/ " nil t) 1) ((search-backward-regexp "\\`[ \t]*\n\\+ " nil t) 1) ((search-backward-regexp "\\`[ \t]*\n- " nil t) 1) ((search-backward-regexp "\\`:: .*\n" nil t) 1) ((search-backward-regexp "\\`" nil t) 0) )) ;; This is where we want to insert - lines (setq bplan-pending-insert (point-marker)) ;; Start searching forward for the first line that doesn't start ;; with "- ", or the end of buffer. (if (or (re-search-forward "\n[^-]" nil t) (re-search-forward "\\'" nil t)) (progn ;; If we matched the end of buffer, make sure it ends with a ;; newline. Otherwise, back up a char. (if (eq (point) (point-max)) (unless (eq (point) (line-beginning-position)) (insert "\n")) (forward-char -1)) ;; This is where we want to insert + lines. It is ALSO where ;; we want to insert / lines, UNLESS we find a better place ;; for them below. (setq bplan-done-insert (point-marker) bplan-wont-insert (point-marker)) ;; Look for a line that doesn't start with + (if (re-search-forward "\n[^\\+]" nil t) (progn (forward-char -1) (setq bplan-wont-insert (point-marker)))) ) ;; else; couldn't find any - lines ((setq bplan-done-insert (point-marker) bplan-wont-insert (point-marker))) ) (list bplan-pending-insert bplan-done-insert bplan-wont-insert) ) (defun bplan-modify-item (prefix) "Change the -+/ prefix (any kind) of the current line to PREFIX." ;; Get positions to insert -+/ lines. ;; bplan-rewind-markers moves point, so save start-pos for later. (setq start-pos (point-marker) markers (bplan-rewind-markers) pending-insert (nth 0 markers) done-insert (nth 1 markers) wont-insert (nth 2 markers)) ;; Go to the start of the current line, remove any existing prefix, ;; add the desired prefix and a space. (goto-char start-pos) (beginning-of-line) (if (bplan-is-todo-item) (delete-char 2)) (insert prefix) (insert " ") ;; Sort line (beginning-of-line) (kill-line) ;; Kill the remaining newline, but not if we're at the end of buffer (cond ((eq (point) (point-min)) (delete-char 1)) ((not (eq (point) (point-max))) (delete-char 1)) ) ;; Go to the insert point for this prefix (cond ((eq prefix ?\-) (goto-char pending-insert)) ((eq prefix ?\+) (goto-char done-insert)) ((eq prefix ?\/) (goto-char wont-insert)) ) (yank) (insert "\n") ) (defun bplan-is-todo-item () "Returns t if point is at a -+/ prefix." (and (eq ?\s (char-after (+ (point) 1))) (or (bplan-following ?\+) (bplan-following ?\-) (bplan-following ?\/))) ) (defun bplan-following (ch) "Returns t if the character following point equals CH." (eq (following-char) ch) ) (defun bplan-mark-done () "Marks the current line/item as done." (interactive) (bplan-modify-todo-line ?\+) ) (defun bplan-mark-pending () "Marks the current line/item as pending." (interactive) (bplan-modify-pending-line ?\-) ) (defun bplan-dwim () "Toggles the current line/item prefix (- to +, + to -, / to -)." (interactive) (save-excursion (beginning-of-line) (if (bplan-is-todo-item) (cond ((bplan-following ?\-) (bplan-modify-item ?\+)) ((bplan-following ?\+) (bplan-modify-item ?\-)) ((bplan-following ?\/) (bplan-modify-item ?\-)) ) ) ) ) (defun bplan-incr-todo-counter (status alist) "Increments the key STATUS in association list ALIST and returns ALIST." (incf (alist-get status alist)) alist ) (defun bplan-report () "Generates a report of all #tags present in the current buffer." (interactive) (let ((lines (split-string (buffer-string) "\n" t)) (tags (make-hash-table :test 'equal))) (while lines (setq line (substring-no-properties (car lines))) (if (setq status (cond ( (eq (string-to-char line) ?\+) 'done ) ( (eq (string-to-char line) ?\-) 'pending ) ( (eq (string-to-char line) ?\/) 'wont ) ( t nil ))) (progn (setq start-pos 0) (while (setq pos (string-match "#[[:alnum:]]+" line start-pos)) (let ((tag (match-string 0 line))) (puthash tag (bplan-incr-todo-counter status (gethash tag tags (list 'list (cons 'done 0) (cons 'pending 0) (cons 'wont 0)))) tags) ) (setq start-pos (+ 1 pos)) ) ) ) (setq lines (cdr lines)) ) (setq generated-from (current-buffer)) ;; Keep track of where we came from (with-current-buffer-window bplan-report-buf-name nil nil (make-local-variable 'tab-stop-list) ;; Set up the tabs to align against (setq tab-stop-list '(15 30 50 100)) (maphash 'bplan-list-todo-tag tags) ;; Make a keymap to open an occur buffer; apply it to all lines (let ((map (make-sparse-keymap)) (begin (point-min)) (end (point-max))) (define-key map (kbd "RET") 'bplan-occur) (define-key map [mouse-2] 'bplan-occur) (put-text-property begin end 'keymap map) (add-text-properties begin end '(mouse-face highlight help-echo "mouse-2: find occurences of this tag")) ) ) ) ) (defun bplan-switch-to-report () "Switches the current window (back) to the battle-plan report. This is intended to be mapped in the occur buffer." (interactive) (switch-to-buffer bplan-report-buf-name) ) (defun bplan-occur () "In the report buffer, opens an occur buffer for the currently selected tag." (interactive) (goto-char (line-beginning-position)) (if (re-search-forward "#[[:alnum:]]+" (line-end-position) t) (let ((tag (match-string 0 nil))) (goto-char (line-beginning-position)) (switch-to-buffer bplan-occur-buf-name) (occur-1 tag 0 (list generated-from) bplan-occur-buf-name) ;; Bind *backspace* to return to report (local-set-key (kbd "DEL") 'bplan-switch-to-report) ) ) ) (defun bplan-list-todo-tag (key value) "Format and display a line for #tag KEY, with done/pending/wont count retrieved from VALUE." (let ((done (alist-get 'done value)) (pending (alist-get 'pending value)) (wont (alist-get 'wont value))) (bplan-insert-face-tab key bplan-tag t) (bplan-insert-face-tab (format "%3d" done) bplan-done nil) (bplan-insert-face-tab " / " nil nil) (bplan-insert-face-tab (number-to-string pending) bplan-pending t) (bplan-insert-face-tab (format "(total: %3s) " (number-to-string (+ done pending))) nil nil) (bplan-insert-face-tab (format "(won't do: %s)" (number-to-string wont)) bplan-wont-do nil) (let ((begin (line-beginning-position)) (end (line-end-position))) (add-face-text-property begin end '(:underline "gray20"))) (insert "\n")) ) (defun bplan-insert-face-tab (text face tab) "Inserts TEXT in the current buffer and apply FACE to it. If TAG is t, skip to the next tabstop." (let ((begin (point-marker))) (insert text) (let ((end (point-marker))) (if tab (tab-to-tab-stop)) (add-face-text-property begin end face) ) ) ) ;; Associate buffers called TODO with battle-plan (add-to-list 'auto-mode-alist '("TODO" . battle-plan-mode)) (provide 'battle-plan)
Note: This was my first substantial elisp project. The code might not be optimal.
org-mode
is too complicated and I don't like it.
battle-plan.php was last modified on 2019-09-07 and should be valid XHTML 1.0 Strict.