;;; mst-planner.el --- Experimentation with Emacs Planner ;; Author: Mark Triggs ;; This file 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. ;; This file 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., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Commentary: ;; ;; For a fair while I've been keeping track of my todo list using my trusty ;; ~/.plan file and a whole bunch of ticked messages in Gnus. My usual method ;; for deciding what to do next is to pluck the next cryptic note from ;; ~/.plan--which must have made sense when I wrote it--then search through my ;; ticked articles to decipher it. ;; ;; The code I've written here is sort of in the same spirit as that. I define ;; tasks I want to complete, each with its own page of notes, links to emails ;; and miscellaneous rambling. I can prioritise my tasks, mark them as "in ;; progress" or "completed". I've never really bothered aiming for deadlines, ;; so I've excluded that whole business--I keep track of things with M-x diary, ;; and that suits me pretty well. Maybe I'll want to integrate that later. ;; ;;; Code: (require 'planner) (require 'planner-publish) (require 'planner-bbdb) (require 'planner-w3m) (require 'emacs-wiki) (defvar *task-index-page* "TaskPool") (defvar *planner-last-window-config* nil) (setq planner-plan-page-template "* Notes\n\n") (setq planner-use-day-pages nil) (defun mst-planner-save-windows () (setq *planner-last-window-config* (current-window-configuration))) (defun mst-planner-create-task (title priority &optional noselect) (interactive (list (read-from-minibuffer "Title for this task: ") (mst-planner-read-priority))) (let ((page (loop for i from (length (directory-files planner-directory)) do (unless (planner-page-exists-p (number-to-string i)) (return (number-to-string i)))))) (with-current-buffer (planner-goto-plan-page page) (erase-buffer) (insert (format "* %s\n\n" title)) (save-buffer) (kill-buffer nil)) (planner-create-task-from-info nil priority "0" planner-default-task-status (format "[[%s.muse][%s]] (%s)" page title (format-time-string "%Y-%m-%d")) "" nil "TaskPool") (unless noselect (mst-planner-summary) (split-window) (other-window 1) (planner-goto-plan-page page) (goto-char (point-max))))) (defun mst-planner-move-to-bottom () (save-excursion (let ((line (buffer-substring (line-beginning-position) (line-end-position)))) (delete-region (line-beginning-position) (line-end-position)) (delete-char 1) (search-forward-regexp "* Completed$") (skip-chars-forward "\n") (insert (concat line "\n")) (backward-char 1) (when (= (char-before (point)) (string-to-char ")")) (backward-char 1) (insert (format "--%s" (format-time-string "%Y-%m-%d"))))))) (defun mst-planner-task-done () (interactive) (when (planner-mark-task "X") (mst-planner-move-to-bottom))) (defun mst-planner-task-cancelled () (interactive) (when (planner-mark-task "C") (mst-planner-move-to-bottom))) (defun mst-planner-create-task-from-buffer (title &optional noselect) (interactive "sTitle for this task: ") (let ((xref (mst-planner-xref-as-kill))) (mst-planner-create-task title (mst-planner-read-priority) noselect) (insert (format "Reference: %s\n" xref)))) (defun mst-planner-xref-as-kill () (interactive) (let ((text (run-hook-with-args-until-success 'planner-annotation-functions))) (with-temp-buffer (insert text) (kill-region (point-min) (point-max))) text)) (defun mst-planner-replace-regexps-in-string (replacements string) (reduce (lambda (string replacement) (replace-regexp-in-string (car replacement) (cadr replacement) string)) replacements :initial-value string)) (defun mst-planner-read-priority () (completing-read "Priority? " '(("A") ("B") ("C")) nil t planner-default-task-priority)) (defun mst-planner-create-task-from-message () "Create a new task using the current message's subject for its title." (interactive) (save-window-excursion (gnus-summary-select-article nil t) (with-current-buffer gnus-article-buffer (let ((body (save-restriction (gnus-narrow-to-body) (buffer-string)))) (mst-planner-create-task (mst-planner-replace-regexps-in-string '(("\\]" ")") ("\\[" "()") ("^Planner: " "")) (message-fetch-field "subject")) (mst-planner-read-priority) nil) (insert body) (when (search-backward-regexp "^-- ") (delete-region (point) (point-max))) (mst-unplanner))))) (define-key global-map (kbd "C-c p a") 'mst-planner-create-task) (define-key global-map (kbd "C-c p b") 'mst-planner-create-task-from-buffer) (define-key global-map (kbd "C-c p m") 'mst-planner-create-task-from-message) (define-key global-map (kbd "C-c p y") 'mst-planner-xref-as-kill) (define-key global-map (kbd "C-c p p") 'mst-planner-summary) (defun mst-planner-summary () (interactive) (mst-planner-save-windows) (delete-other-windows) (planner-goto-plan-page "TaskPool")) (defun mst-unplanner () (interactive) (planner-save-buffers) (dolist (buf (buffer-list)) (with-current-buffer buf (when (eq major-mode 'planner-mode) (kill-buffer nil)))) (when *planner-last-window-config* (set-window-configuration *planner-last-window-config*) (setq *planner-last-window-config* nil))) (defun mst-planner-garbage-collect () (interactive) (save-window-excursion (mst-planner-summary) (let ((files '())) (goto-char (point-min)) (while (search-forward "[[" nil t) (push (remove-properties-from-string (muse-link-at-point)) files)) (dolist (file (muse-project-file-alist planner-project)) (let ((filename (file-name-nondirectory (cdr file)))) (unless (or (member filename files) (not (string-match "^[0-9]+\.muse$" filename))) (delete-file (cdr file)))))))) (define-key planner-mode-map (kbd "C-c C-q") 'mst-unplanner) (define-key planner-mode-map (kbd "C-c C-x") 'mst-planner-task-done) (define-key planner-mode-map (kbd "C-c C-C") 'planner-task-cancelled) ;; Allow links to emacs-wiki pages. (defun planner-ewiki-annotation () (when (eq major-mode 'emacs-wiki-mode) (format "[[ewiki://%s#%s][%s]]" emacs-wiki-current-project (emacs-wiki-page-name) (emacs-wiki-page-title)))) (defun planner-ewiki-browse-url (url) (when (string-match "\\`ewiki://\\(.+\\)#\\(.+\\)" url) (let ((project (match-string 1 url)) (page (match-string 2 url))) (let ((emacs-wiki-current-project project)) (find-file (emacs-wiki-page-file page)))))) (add-hook 'planner-annotation-functions 'planner-ewiki-annotation) (planner-add-protocol "ewiki://" 'planner-ewiki-browse-url nil) ;; Create tasks from IRC, saving an extract from the buffer as reference. (defun planner-irclog-annotation () (when (eq major-mode 'erc-mode) (let* ((min (save-excursion (beginning-of-line) (forward-line -40) (point))) (max (save-excursion (beginning-of-line) (forward-line 10) (point))) (text (buffer-substring-no-properties min max))) (let ((page (loop for i from 0 do (let ((page (concat "irc" (number-to-string i)))) (unless (planner-page-exists-p page) (return page)))))) (with-current-buffer (planner-goto-plan-page page) (let ((inhibit-read-only t)) (erase-buffer) (insert "\n") (insert text) (insert "\n\n") (delete-matching-lines erc-prompt (point-min) (point-max)) (delete-matching-lines "^$" (point-min) (point-max))) (save-buffer) (kill-buffer nil)) (format "irclog://%s" page))))) (defun planner-irclog-browse-url (url) (when (string-match "\\`irclog://\\(.+\\)$" url) (let ((page (match-string 1 url))) (planner-goto-plan-page page)))) (add-hook 'planner-annotation-functions 'planner-irclog-annotation) (planner-add-protocol "irclog://" 'planner-irclog-browse-url nil) (defun mst-planner-unhighlight-dates (beg end) (dolist (o (overlays-in beg end)) (when (eq (overlay-get o 'type) 'mst-planner-date) (delete-overlay o)))) (defun mst-planner-parse-date (date-string) (multiple-value-bind (year month day) (mapcar 'string-to-number (split-string date-string "-")) (time-to-seconds (encode-time 1 1 1 day month year)))) (defun mst-planner-highlight-dates (beg end) (let ((beg (point-min)) (end (point-max))) (save-excursion (mst-planner-unhighlight-dates beg end) (goto-char beg) ;; Mark all dates with overlays (while (search-forward-regexp "(\\([0-9]*-[0-9]*-[0-9]*\\))" nil t) (let* ((date-string (match-string 1)) (o (make-overlay (match-beginning 1) (match-end 1)))) (overlay-put o 'type 'mst-planner-date) (overlay-put o 'date (mst-planner-parse-date date-string)))) ;; Set the overlay faces (let ((dates (sort (remove-if-not (lambda (o) (eq (overlay-get o 'type) 'mst-planner-date)) (overlays-in beg end)) (lambda (o1 o2) (> (overlay-get o1 'date) (overlay-get o2 'date)))))) (when dates (let* ((youngest (overlay-get (car dates) 'date)) (oldest (overlay-get (car (last dates)) 'date)) (mapper (mst-planner-make-mapper oldest youngest 0 50))) (dolist (o dates) (let ((date (overlay-get o 'date))) (overlay-put o 'face `(:foreground ,(format "gray%d" (- 100 (funcall mapper date))))))))))))) (defun mst-planner-make-mapper (old-range-lower old-range-upper new-range-lower new-range-upper) "Return a function that converts integers in the range `old-range-lower'...`old-range-upper' to the range `new-range-lower`...`new-range-upper'" `(lambda (n) (if (= ,old-range-lower ,old-range-upper) ,new-range-lower (+ (* (/ (- n ,old-range-lower) (float (- ,old-range-upper ,old-range-lower))) (- ,new-range-upper ,new-range-lower)) ,new-range-lower)))) ;; Register the new-fangled highlighting (add-hook 'planner-mode-hook (lambda () (mst-planner-highlight-dates (point-min) (point-max)) (jit-lock-register 'mst-planner-highlight-dates))) (defun mst-planner-sort-tasks () "Sort by priority and move 'in progress' tasks up" (skip-chars-forward "#ABC") (let ((case-fold-search t) (ch (char-before)) status) (skip-chars-forward "0123456789 ") (setq status (char-after)) (+ (cond ((or (= status ?X) (= status ?C)) 0) ((= ch ?A) 10000) ((= ch ?B) 20000) ((= ch ?C) 30000) (t 0))))) (setq planner-sort-tasks-key-function 'mst-planner-sort-tasks) (provide 'mst-planner) ;;; mst-planner.el ends here