;;; planner.el --- The Emacs Planner ;; Copyright (C) 2001 John Wiegley ;; Emacs Lisp Archive Entry ;; Filename: planner.el ;; Version: $Id: planner.el,v 1.89 2003/04/14 02:40:30 sacha Exp sacha $ ;; Keywords: hypermedia ;; Author: John Wiegley ;; Maintainer: Sacha Chua ;; Description: Use Emacs for life planning ;; URL: http://richip.dhs.org/~sachac/notebook/emacs/planner.el ;; ChangeLog: http://richip.dhs.org/~sachac/notebook/emacs/planner.changelog ;; Compatibility: Emacs20, Emacs21 ;; This file is not part of GNU Emacs. ;; This 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 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. ;;; Usage: ;; Place planner.el in your load path and add this to your .emacs: ;; ;; (require 'planner) ;; ;; If you want to change `planner-directory' and some other variables, ;; either use Customize or use `planner-option-customized'. For ;; example: ;; ;; (planner-option-customized 'planner-directory "~/Plans") ;; (add-to-list 'planner-custom-variables '(emacs-wiki-publishing-directory . "~/public_html/plans")) ;; (planner-option-customized 'planner-custom-variables planner-custom-variables) ;; ;; Planner can work with other Emacs packages. Try ;; ;; (planner-insinuate-calendar) ;; (planner-insinuate-gnus) ;; ;; You can customize Planner. M-x customize-group RET planner RET ;; or see the Options section. ;;; Note: ;; This package extends emacs-wiki.el to act as a day planner, roughly ;; equivalent to the one used by Franklin-Covey. If they have patents ;; and trademarks and copyrights to prevent me even thinking in terms ;; of their methodology, then I can't believe they care at all about ;; productivity. ;;; Summary: ;; * Make a planning file ;; Open a wiki file within your planning directory. By default, ;; planner-directory is set to "~/Plans". You may have to use C-x C-f ;; to open the file. ;; A plan file generally describes a long-term plan. For example, you ;; could make a plan file for your ThesisProject or your ;; ContinuousLearning. Planner.el can help you organize related ideas, ;; tasks and resources into a coherent plan. ;; * Break your plan into stages ;; Start the file with your "vision", or the goal you intend to ;; accomplish. Break this up into parts, and create a Wiki file for ;; each part, with defined milestones which constitute the "goal" for ;; that part. ;; * Write out the tasks for each stage ;; In each sub-plan, list out the tasks necessary to accomplish the ;; milestone. Write them into the file like this: ;; #A _ 1h Call so and so to make a reservation ;; * Decide on a priority for each task ;; The A is the priority of the task. The _ means it isn't done yet, ;; and the 1h is a quick estimate on how long it will task. The time ;; estimates are optional. ;; The priorities break down like this: ;; ;; A: if you don't do it, your plan will be compromised, and you ;; will have to either abort, backtrack, or make profuse apologies ;; to someone ;; ;; B: if you don't do it, your plan will be delayed ;; ;; C: the plan won't be complete until it's done, but there's no ;; pressure to do it now ;; * Schedule the tasks ;; Put your cursor on a line containing a task, and type C-c C-c. ;; This will copy the task to a specific day, which you will be ;; prompted for. The Emacs Calendar pops up, so you can pick a free ;; day (if you use the Emacs diary and appointment system, the ;; Calendar is even more useful). ;; You will now see your new task, with a link back to your planning ;; page. Selecting this link will take you back to that task on the ;; planning page, where you will see that the planning page's task now ;; has a link to the particular day you scheduled the task for. ;; The two tasks (the one on the planning page, and the one on the ;; daily task list) are linked. Changing the status of one (using C-c ;; C-x, or C-c C-s, for example) will change the status of the other. ;; If you forward the task to another day (using C-c C-c on the daily ;; task page), the planning page's link will be updated to refer to ;; the new day. This is so that you can focus on your daily task list ;; during the day, but see an overview of your plan's progress at any ;; time. ;; * Do the work ;; That's it, as far as what planner.el can do. As you complete tasks ;; each day, they will disappear from view. This only happens for ;; today's completed and forwarded tasks. ;; Planning is an art, just as estimating time is an art. It happens ;; with practice, and by thinking about these things. The Commentary ;; below provides a few of my own thoughts on the matter, although I ;; will say that this an art I have yet to truly develop. ;;; Commentary: ;; What is planning? It can be a nebulous thing to define. In its ;; essence, however, it is very simple: it's how we achieve our dreams. ;; Our days are filled with time, and hence with actions, whether they ;; be of a mental or physical sort. But there are two kinds of ;; action: reactive and creative. Reactive action is a response to ;; the environment, a reaction to stimulus. Had we enough instincts ;; to ensure survival, we could live according to this kind of action ;; alone. It is a mode of behavior we share with every living ;; species. ;; The opposite to reactivity is creativity, when we decide upon a ;; course of action that is a wholly a product of personal choice. We ;; then make decisions as to the steps needed to make this wish a ;; reality. This is planning. Planning is essentially a creative ;; endeavor at every step. ;; First, create the idea, what you want to achieve. Very short-term ;; ideas do not need much more than thinking about how to do it. But ;; long-term ideas require planning, since the mind cannot contain all ;; of the details. ;; Second, decide how the idea maps into the circumstances you find ;; yourself in. Some environments will assist your plan, others ;; hinder it. But step by step, identify every barrier to the ;; realization of your idea, and devise a countermeasure to overcome ;; it. Once you've mapped things out from beginning to end, ;; accounting for unknowables as best you can, you now have your plan. ;; Third is to break the stages of the plan into parts that are not ;; overwhelming in their complexity. It is at during this phase that ;; a plan is turned into task items, each to be accomplished within ;; the span of one day's time. If a task requires several days, break ;; it up further. The smaller it is, the less your mind will recoil ;; from attempting it. ;; Fourth is to monitor your progress, identifying problems and ;; correcting for them as you go. Some plans start out unachievable, ;; and remain that way indefinitely, due to a simple lack of ;; observation. If nothing is working for you, change it. Otherwise, ;; your plan is merely a well-crafted wish. ;; Fifth is just to do the work, and be patient. All good plans take ;; a great deal of time, and *cannot* happen immediately. The ;; groundwork must be laid for each step, or else it will rest on an ;; unsecure foundation. If you follow your plan doggedly, applying ;; some time to it each day or week, it _will_ happen. Remember the ;; story of the tortoise and the hare. I've even written a short ;; essay on the necessity of gradual accomplishment, which can be ;; found at http://www.gci-net.com/~johnw/AdvancingTheProcess.html. ;; How can this software help? Computers are ideal for manipulating ;; information, since they allow you to change things without erasing ;; or rewriting. And since all plans change quite a bit during their ;; implementation, a planning program can be very helpful. ;; Start by adding the following to your .emacs file: ;; (load "planner") ;; Now, conceive your idea. I can't believe there's nothing you want ;; from life. More peace, time to enjoy the world, an end to war? ;; Everyone wants something. Search deeply, and you will find ;; countless unhoped wishes lurking therein. Choose one for now, and ;; think on it for a while. ;; Then open a file (using C-x C-f) within the directory named by ;; `planner-directory'. Emacs will automatically recognize this file ;; as a planner file. Name the file after your plan, such as ;; "BetterHealth". ;; Choose an idea you really want to accomplish. Struggle to ;; differentiate between the things you want because others want them, ;; and the things you want for yourself. It takes quite an effort, ;; and may require a long time before you notice the difference. Many ;; people want to be more healthy to be more attractive, which is an ;; externally driven goal. Unless _you_ really want to accomplish ;; what you envision, the odds are you will fail. Only our own wishes ;; and dreams possess enough personal energy to see themselves to ;; fruition. What happens to many of us is simply that we never ;; become conscious of these dreams: what we love, what we desire ;; most. When I talk to friends, so much of what I hear is things ;; they want because they feel they should want them. There's just ;; not enough energy there to pursue a good plan, because nearly all ;; of it is negative energy. ;; Do you know what you really want? Don't worry, many people don't. ;; It's not a question anyone really wants us to pursue, because often ;; we don't want what others do; it doesn't contribute to the social ;; welfare, and all that nonsense. Somehow we always forget that ;; what's good for the social welfare now, was someone else's crazy ;; dream a hundred years ago. The human aversion to fundamental ;; change is always one's greatest enemy, so don't waste any time ;; getting bitter about it. ;; For the sake of argument I assume you really do want to be ;; healthier, because you've fallen in love with the ideal of purity, ;; or you understand the connection between your physical self and the ;; world around you, and how this can open up your spirit to desiring ;; more. I assume. :) ;; So you're in a Wiki file called BetterHealth. Start typing. Type ;; anything related to your idea: what you think about it, your ideas ;; on it, *and especially what the end will look like*. If you can't ;; visualize the end, you can't plan, since planning is about drawing ;; a line between now and then. ;; When you've typed enough to gain a vision of your goal, start ;; drafting what the possible intermediate steps might be. Then stop, ;; get up, walk around, enjoy life, and come back to it. Taking a ;; long time at the beginning is not a bad idea at all, as long as ;; it's not forever. ;; As you chew on your idea, it will begin to become more and more ;; concrete. You'll have ideas about the smallest pieces, and ideas ;; about the biggest pieces. Keep going until it starts to take shape ;; before you, and you can see yourself in your mind's eye moving from ;; the present into the future. Write down this progression, and the ;; sorts of things you might encounter along the way. ;; As you continue, you'll naturally discover discrete phases, or ;; "milestones" as managers love to call them. These are very ;; important, because they let you know you're making progress. I ;; recommend having a big party with friends every time you achieve a ;; milestone. A typical plan might have between three and ten. ;; Between the milestones are the bigger pieces of your plan. Name ;; these pieces using MixedCase words, and you'll notice that Emacs ;; colors and underlines them for you. Like, FindGoodGym. Hit return ;; on this highlighted word, and you'll find yourself in another, ;; blank file. In this file, start drafting your sub-plan, just as ;; you did with the larger plan. You should find it easier now, since ;; the scope is smaller. ;; As you break down further, you'll notice simple little things that ;; need to get done. These are your tasks. Every plan is a ;; succession of tasks. The difference from reactivity is that each ;; task is part of the larger plan. This is what it means to be ;; systematic: that everything you do helps further your plan. If you ;; have tasks in your day that contribute to no plan, they are ;; reactive. Of course, life is full of these, but don't let them ;; take up more than 20% of your day. If you allow yourself to be ;; dominated by reactive tasks, you'll regret it at the end of your ;; life. I don't know this personally, but I do know that striving ;; for one's dreams -- and seeing them come to fruition -- is the ;; greatest joy a man can possess. It is the essence of freedom, of ;; living, of creation. Reactivity is the opposite of this, and ;; serves only to drain our energy and slacken our spirits. ;; Now that you've thought of a simple task, type C-c C-t. This will ;; ask for a brief description of the task, and when you plan to do ;; it. If you hit RETURN at the question 'When', it assumes you mean ;; today. The Planner will also pop up a three-month calendar at this ;; question, so you can see where your free days are. Make sure you ;; set the variable `mark-diary-entries-in-calendar' to t in your ;; .emacs file. This way, you can see which days your appointments ;; fall on. (Read about the Emacs Calendar and Diary in the Emacs ;; info manual). ;; (setq mark-diary-entries-in-calendar t) ;; Once your task is in there, go back to your plan and keep ;; generating more tasks. Generate them all! Fully describe -- as ;; tasks -- everything necessary to bring your sub-plan to completion. ;; Don't create tasks for the other sub-plans. You may have good idea ;; of what they'll look like, but don't bother rendering them into ;; tasks just yet. Things will change too much between now and then, ;; for that to be a good use of your time. ;; Is your sub-plan now rendered into all of the tasks necessary to ;; reach your first milestone? Great! That is the purpose of ;; planner.el. The rest is really up to you. If you find that you ;; keep putting things off, and never do them, that's the surest sign ;; you're planning for someone else's dream, and not your own. ;; Here are some of the things planner.el can do, to help you manage ;; and track your tasks: ;; At the beginning of every day, type M-x plan. This will jump you ;; to the top of the most recent task list before today. If you ;; skipped a bunch of days, you'll have to open up those files on your ;; own. ;; Probably some of the tasks that day won't be finished -- that's OK. ;; Learning to properly estimate time is a magical, mystical art that ;; few have mastered. Put your cursor on those undone tasks, and type ;; C-c C-c. This will move them into today's task page. You can jump ;; to today's task page at any time by typing C-c C-n (from a Wiki or ;; planning page). I heartily recommend binding C-c n, to jump you to ;; this page from anywhere: ;; (define-key mode-specific-map [?n] 'planner-goto-today) ;; As you look at your task sheet each day, the first thing to do is ;; to "clock in" to one of them. This isn't necessary, and is only ;; helpful if you're around your computer a lot. But by typing C-c ;; C-i (assuming you have my timeclock.el on your load-path), it will ;; log the time you spend working on your sub-plan. This is helpful ;; for viewing your progress. Type C-c C-o to clock out. ;; C-c C-u and C-c C-d will move a task up and down in priority. The ;; priority scheme has two components: a letter A through C, and a ;; number from 1 onwards. 'A' tasks mean they must be done that day, ;; or else your plan is compromised and you will have to replan. 'B' ;; means they should be done that day, to further the plan, otherwise ;; things will be delayed. 'C' means you can put off the task if you ;; need to, although ultimately it will have to be done. ;; For reactive tasks, the letters mean something different: 'A' means ;; you must do it today, or somebody will roast your chestnuts over an ;; open fire. 'B' means you should do it today, or else someone will ;; be practicing patience at the day's end. 'C' means no one will ;; notice if you don't do it. ;; Again, reactive tasks are ENEMIES OF PLANNING. Really, until you ;; see them that way, circumstances will push you around and steal ;; your life away. We have only so many years to use, and everyone is ;; greedy to take them. It's insidious, almost invisible. A healthy ;; dislike of reactivity will do wonders for organizing your affairs ;; according to their true priority. ;; The last word that needs to be said concerns "roles". Every person ;; stands in several positions in his life: husband, employee, ;; manager, etc. These roles will tend to generate tasks not ;; associated with any immediate plan, but necessary to maintain the ;; health and functioning of the role. My suggestion is to keep this ;; the smallest possible number, and fulfill those that remain well. ;; How you decide to apportion your time between pursuing grand ;; designs, and fostering deep relationships, is a personal matter. ;; If you choose well, each will feed the other. ;; I mention this to point that reactivity is something not ;; exclusively associated with tasks that have no master plan, because ;; being a father, for example, is something that rarely proceeds ;; according to orderly plans. But the role of father itself is its ;; own plan, whose goal is "to be the best one can", and whose ;; component tasks are spending time on whatever comes up. It is, in ;; a sense, an implicit plan. But reactive tasks follow no plan at ;; all; they are parasites of time that suck the spirit away, whereas ;; properly chose roles actually help fulfill one's own inner needs. ;; At least, this is what I believe. And now back to technical ;; matters: ;; In order to refresh and renumber all of your tasks according to their ;; actual order in the buffer, simply save the file or call ;; M-x planner-renumber-tasks . ;; Here is a summary of the keystrokes available, including a few I ;; did not mention: ;; ;; M-x plan Begin your planning session. This goes to the last ;; day for which there is any planning info (or today if ;; none), allowing you to review, and create/move tasks ;; from that day. ;; ;; C-c C-u Raise a task's priority ;; C-c C-d Lower a task's priority ;; ;; C-c C-s Mark the task as in progress or delegated ;; C-c C-x Mark the task as finished ;; ;; C-c C-t Create a task associated with the current Wiki page ;; If you are on the opening line of a Note entry, it is ;; assume that the note itself is the origin of the task. ;; C-c C-c Move or copy the current task to another date ;; If the current task is an original (meaning you are in ;; the buffer where's defined, hopefully a planning page) ;; then it will be copied, and the original task will also ;; now point to the copy. If the current task is a copy, ;; it will just be moved to the new day, and the original ;; tasks link will be updated. ;; ;; C-c C-n Jump to today's task page ;; ;; Also, in the Emacs Calendar, typing 'n' will jump to today's task ;; page. ;; * Planning and schedules ;; Sometimes you will have appointments during the day to schedule, ;; which "block out" time that might otherwise be spent on tasks. ;; Users are encouraged to use the Emacs Calendar for this, along with ;; Diary Mode (see the Emacs manual) ;;. ;; However, there is a way to do scheduling directly in planner-mode. ;; It requires the external tool "remind" (Debian users type "apt-get ;; install remind". All others go to ;; http://www.roaringpenguin.com/remind.html). ;; Once you have remind installed, you will need three scripts in your ;; local bin directory (/usr/local/bin, $HOME/bin, wherever). These ;; scripts can be downloaded from my web site: ;; ;; http://www.gci-net.com/~johnw/bin/rem ;; http://www.gci-net.com/~johnw/bin/allrems ;; http://www.gci-net.com/~johnw/bin/remconv ;; ;; "rem" is a replacement for the rem command that comes with remind. ;; It has the additional ability of scanning for reminders in planning ;; files. NOTE: You should edit the last lines in this file to ;; correspond with your own environment! ;; ;; "allrems" is a Perl script that rips through planning files, ;; extracting scheduling information into remind's scripting format. ;; You shouldn't call this directly. It is called by the new "rem". ;; ;; "remconv" is also a Perl script, used to convert the output of ;; "remind -s" into a format compatible with Emacs' calendaring ;; system. ;; Once these scripts are installed, add the following lines to your ;; .emacs file: ;; ;; (defadvice mark-diary-entries (before remind-generate-diary activate) ;; "Generate a diary file from a .reminders file." ;; (with-current-buffer (find-file-noselect diary-file) ;; (erase-buffer) ;; (call-process "remconv" nil t nil) ;; (save-buffer))) ;; ;; This function OVERWRITES your diary file with remind's appointments ;; every time you load the Emacs calendar. Thus, you can view your ;; current appointments either from the command-line (using "rem"), or ;; from Emacs. Just remember to add new entries to your planning ;; files, not the diary file! The diary file is overwritten every ;; time you visit the Emacs calendar. ;; Lastly, here is another snippet for your .emacs file. It creates a ;; keybinding in planner-mode, C-c C-w, which jumps you to the ;; Schedule section of that file. ;; (defun planner-goto-schedule () ;; (interactive) ;; (goto-char (point-min)) ;; (unless (re-search-forward "^\\* Schedule\n\n" nil t) ;; (re-search-forward "^\\* Notes") ;; (beginning-of-line) ;; (insert "* Schedule\n\n\n\n") ;; (forward-line -2))) ;; ;; (eval-after-load "planner" ;; '(progn ;; (define-key planner-mode-map [(control ?c) (control ?w)] ;; 'planner-goto-schedule))) ;; The contents of a scheduling section look like this, which is ;; rendered in HTML as a table: ;; ;; * Schedule ;; ;; 8:00 | Wake up ;; 14:00 | Go to the dentist (2:00) ;; 18:00 | Watch TV ;; ;; The start time is given in 24-hour time, with an optional duration ;; occuring in parentheses at the end of the description (in ;; HOURS:MINUTES). And off you go! ;; * Example planning file ;; The format of a planning file is given below. You are responsible ;; for keeping it looking like this. I intentionally did not make ;; planner.el heavy on the UI side of things, too keep it more ;; free-form and open. This lets you adapt it to whatever your ;; particular preferences might be. ;;---------------------------------------------------------------------- ;; * Tasks ;; ;; #A1 _ An open task, very important! ;; #A2 X A closed task (MyPlan) ;; #A3 o A task that's delayed, or delegated (MyPlan) ;; #B1 _ Open task again, reacting to [[bbdb://John Wiegley]] ;; #B2 _ Hey, this task came from [[gnus://nnml:mail.personal/100]] ;; ;; Use `A n' in the Gnus summary buffer to create tasks based on an ;; e-mail request, complete with the correct cross-reference. Hitting ;; enter on the above [[gnus link]] will bring up that e-mail. E-mail ;; generated tasks are nearly also reactive, though. Watch out! ;; ;; Clicking on a `bbdb' URL will perform a search for that name/text ;; in your BBDB. ;; ;; * Notes ;; ;; .#1 This is note number one ;; ;; Notes on note number one! Here's a task reference to a task within ;; this file: (#A1). An old task reference is done with ;; (2000.10.20#A2) or (10.20#A3) or even (20#A3). Whatever you leave ;; out, it will assume it's the same as the current file's year/month. ;; You can even reference other notes in this file or in other files: ;; (#2), (20#2), etc. ;; ;; .#2 This wierd ".#2" syntax is used because it's what allout.el ;; likes for enumerated lists, and it makes using ;; outline-minor-mode (with allout) very handy. You can omit the ;; leading period if you like, though. It's optional. ;; ;; * Diary ;; ;; If you desire, here is a good place to note observations and other ;; thoughts about your day. The Diary section and any other sections ;; are entirely optional, however, as planner does not require them. ;; ---------------------------------------------------------------------- ;;; Thanks: ;; John Wiegley (johnw AT gnu DOT org) ;; ubercoder and author of planner.el ;; Eric Belpaire (Eric DOT Belpaire AT equant DOT com) ;; lots of bug reports and feature suggestions ;; Damien Elmes (resolve AT repose DOT cx) ;; better planner-read-date handling ;; Jody Klymak (jklymak AT coas DOT oregonstate DOT edu) ;; note font-locking, emacswiki filename completion, remember.el suggestions ;; Thomas Gehrlein (Thomas DOT Gehrlein AT t DASH online DOT de) ;; Lots of bugfixes and new features, date navigation functions ;; David Forrest (drf5n AT mug DOT sys DOT virginia DOT edu) ;; note about obsolete documentation strings ;; Quasi (quasi AT kc4 DOT so DASH net DOT ne DOT jp) ;; inspiration from planner-browser.el ;; irc.freenode.net#emacs ;;; Code: (require 'emacs-wiki) (require 'sort) (defvar planner-loaded nil) ;;; Options: (defgroup planner nil "An extension of Emacs-Wiki for doing time planning in Emacs." :prefix "planner-" :group 'applications) (defcustom planner-use-other-window t "If non-nil, Planner will open planner files in another window." :type 'boolean :group 'planner) (defcustom planner-mode-hook nil "A hook that is run when planner mode is entered." :type 'hook :group 'planner) (defun planner-option-customized (sym val) "Set SYM to VAL and update the WikiPlanner project." (set sym val) (if planner-loaded (planner-update-wiki-project))) (defcustom planner-use-bbdb t "If non-nil, use BBDB to determine people's real names." :type 'boolean :group 'planner) (defcustom planner-carry-tasks-forward nil "If non-nil, always carry undone tasks forward automatically." :type 'boolean :group 'planner) (defcustom planner-day-page-template "* Tasks\n\n\n* Schedule\n\n\n* Notes" "Template to be inserted into blank daily pages." :type 'string :group 'planner) (defcustom planner-plan-page-template "* Tasks\n\n\n* Notes" "Template to be inserted into blank plan pages." :type 'string :group 'planner) (defcustom planner-show-only-existing t "If non-nil, `planner-show' only shows existing files." :type 'boolean :group 'planner) (defcustom planner-calendar-show-planner-files t "If non-nil, shows a plan file every time a day is selected in Calendar." :type 'boolean :group 'planner) (defcustom planner-directory "~/Plans" "The directory that contains your planning files." :require 'planner :type 'directory :set 'planner-option-customized :group 'planner) (defcustom planner-custom-variables nil "A list of planner-specific Emacs-Wiki variable settings. You can customize any emacs-wiki variable to be used specially within planner mode buffers, except for the following, whose values are derived from the other planner mode customized variables: `emacs-wiki-directories' `emacs-wiki-major-mode' `emacs-wiki-markup-tags' `emacs-wiki-publishing-markup' `emacs-wiki-url-regexp' `emacs-wiki-name-regexp' `emacs-wiki-url-or-name-regexp' `emacs-wiki-highlight-regexp' `emacs-wiki-browse-url-function' If you want to customize the derived variables, you can set them from `planner-mode-hook'." :type `(repeat (choice (cons :tag "emacs-wiki-predicate" (const emacs-wiki-predicate) function) (cons :tag "emacs-wiki-project-server-prefix" (const emacs-wiki-project-server-prefix) string) ,@(mapcar (function (lambda (sym) (list 'cons :tag (symbol-name sym) (list 'const sym) (get sym 'custom-type)))) (apropos-internal "\\`emacs-wiki-" (function (lambda (sym) (get sym 'custom-type))))))) :set 'planner-option-customized :group 'planner) (defcustom planner-name-regexp (concat "\\([1-9][0-9][0-9][0-9]\\.\\)?\\([0-9][0-9]\\.\\)?" "[0-9][0-9]#[A-C][1-9][0-9]*\\|" "[0-9]\\{4\\}\\.[0-9]\\{2\\}\\.[0-9]\\{2\\}") "A regexp used to match planner references in a planning buffer." :type 'regexp :set 'planner-option-customized :group 'planner) (defcustom planner-url-regexp (concat "\\<\\(https?:/?/?\\|ftp:/?/?\\|gopher://\\|" "telnet://\\|wais://\\|file:/\\|s?news:\\|" "bbdb:\\|gnus:\\|mailto:\\)" "[^] \n \"'()<>[^`{}]*[^] \n \"'()<>[^`{}.,;]+") "A regexp used to match URLs within a Wiki buffer." :type 'regexp :set 'planner-option-customized :group 'planner) (defcustom planner-publishing-markup '(["^#\\([A-C]\\)" 0 "- **\\1** "] ["^#\\([A-C][0-9]+\\)" 0 "- **\\1** "] ["^\\.#\\([0-9]+\\)" 0 "** "]) "List of additional markup rules to apply when publishing planner pages. These rules are performed first, before any emacs-wiki rules. See the docs for `emacs-wiki-publishing-markup' for more info." :type '(repeat (vector :tag "Markup rule" (choice regexp symbol) integer (choice string function symbol))) :group 'emacs-planner) (defcustom planner-markup-tags '(("timeclock-report" nil nil t planner-timeclock-report-tag) ("past-notes" nil t nil planner-past-notes-tag)) "A list of tag specifications used for marking up planner pages. See the documentation for `emacs-wiki-markup-tags'." :type '(repeat (list (string :tag "Markup tag") (boolean :tag "Expect closing tag" :value t) (boolean :tag "Parse attributes" :value nil) (boolean :tag "Highlight tag" :value nil) function)) :set 'planner-option-customized :group 'planner) (defcustom planner-seek-section-function 'planner-seek-create-at-top "Called when jumping to the argument SECTION (Tasks by default). Should create a section in a planner file that doesn't have it yet. Some functions you can use are `planner-seek-create-at-top' and `planner-seek-create-at-bottom'." :type 'function :group 'planner) (defcustom planner-marks-regexp "[_oX>]" "Regexp that matches status character for a task." :type 'regexp :group 'planner) (defcustom planner-template-fuzz-factor 5 "Controls the fuzziness of `planner-page-default-p'. Right now, this is the number of additional characters over `planner-day-page-template' allowed in a buffer before `planner-page-default-p' assumes it has been modified." :type 'integer :group 'planner) (defvar planner-mode-map (let ((map (copy-keymap emacs-wiki-mode-map))) (define-key map [(control ?c) (control ?n)] 'planner-goto-today) (define-key map [(control ?c) (control ?j)] 'planner-goto) (define-key map [(control ?c) (control ?i)] 'planner-clock-in) (define-key map [(control ?c) (control ?o)] 'timeclock-out) (define-key map [(control ?c) (control ?t)] 'planner-create-task) (define-key map [(control ?c) (control ?c)] 'planner-copy-or-move-task) (define-key map [(control ?c) (control ?u)] 'planner-raise-task) (define-key map [(control ?c) (control ?d)] 'planner-lower-task) (define-key map [(meta ?p)] 'planner-raise-task) (define-key map [(meta ?n)] 'planner-lower-task) (define-key map [(control ?c) (control ?z)] 'planner-task-in-progress) (define-key map [(control ?c) (control ?x)] 'planner-task-done) (define-key map [(control ?c) return] 'planner-show-end-project) (define-key map [(control ?c) (control ?m)] 'planner-show-end-project) map) "Keymap used by Planner mode.") ;; Code: (defvar planner-date-regexp "\\`\\([1-9][0-9][0-9][0-9]\\)\\.\\([0-9]+\\)\\.\\([0-9]+\\)\\'") (defun planner-current-task-info () "Return a list of the form (LINK CATEGORY PRIORITY STATUS DESCRIPTION LINK LINK-TEXT)." (save-excursion (beginning-of-line) (when (looking-at (concat "^#?\\([A-C]\\)\\([0-9]*\\) \\(" planner-marks-regexp "\\) \\(.+\\)")) (let ((category (match-string-no-properties 1)) (priority (match-string-no-properties 2)) (status (match-string-no-properties 3)) (description (match-string-no-properties 4)) link-text link) (if (= (length priority) 0) (setq priority nil)) (when (string-match "(\\[\\[\\(.+\\)\\]\\])" description) (setq description (replace-match "(\\1)" t nil description))) (when (string-match "\\s-+\\((\\([^#)]*\\)\\(#[^)]+\\)?)\\|\\[\\[\\(.+\\)\\]\\]\\)" description) (setq link-text (match-string 1 description) link (or (match-string 2 description) (emacs-wiki-wiki-base link-text))) (setq description (replace-match "" t t description))) (list (emacs-wiki-page-name) category priority status description link link-text))))) (defsubst planner-task-page (info) "Return the page of a task given INFO." (nth 0 info)) (defsubst planner-task-category (info) "Return the category of a task given INFO." (nth 1 info)) (defsubst planner-task-priority (info) "Return the priority of a task given INFO." (nth 2 info)) (defsubst planner-task-status (info) "Return the status of a task given INFO." (nth 3 info)) (defsubst planner-task-description (info) "Return the description of a task given INFO." (nth 4 info)) (defsubst planner-task-link (info) "Return the page linked to by a task given INFO." (nth 5 info)) (defsubst planner-task-link-text (info) "Return the link text of a task given INFO." (nth 6 info)) (defsubst planner-task-estimate (info) "Return a time estimate for how much time a task (INFO) would take to complete, in seconds." (require 'schedule) (if (string-match "\\`\\s-*\\([0-9]+[smhdw]\\)" (planner-task-description info)) (schedule-duration-to-seconds (match-string 1 (planner-task-description info))))) (defun planner-end-projection () "Show when today's task load will be finished, according to estimates." (require 'schedule) (schedule-initialize) (save-excursion (let ((now (schedule-completion-time (current-time) 0)) spent remaining slippage finish) (goto-char (point-min)) (while (re-search-forward "^#[A-C]" nil t) (let* ((task (planner-current-task-info)) (estimate (planner-task-estimate task))) (if estimate (setq now (schedule-completion-time now estimate))))) now))) (defun planner-show-end-project () "Display the estimated project completion time." (interactive) (message (format-time-string "%c" (planner-end-projection)))) ;;;###autoload (defun plan (&optional force) "Start your planning for the day, beginning with the last day's tasks. If a planner page for today exists, go to that instead. If `planner-carry-tasks-forward' is non-nil, then reschedule the last day's tasks to today. If FORCE is non-nil and `planner-carry-tasks-forward' is non-nil, then examine all date files for unfinished tasks." (interactive "P") (planner-goto-today) (let ((names (sort (mapcar (function (lambda (pair) (if (string-match planner-date-regexp (car pair)) (car pair)))) (emacs-wiki-file-alist)) (function (lambda (l r) (string-lessp r l))))) (today (buffer-name)) (today-buffer (current-buffer)) (tasks "") (planner-use-other-window nil) other-buffer) (while names (if (and (car names) (string-lessp (car names) today)) (progn (emacs-wiki-find-file (car names)) (let ((buffer (current-buffer))) (planner-seek-to-first) (when (and planner-carry-tasks-forward (not (equal today (buffer-name)))) (planner-copy-or-move-region 1 (1+ (buffer-size)) (planner-today) t) (when planner-carry-tasks-forward (set-buffer buffer) (save-buffer) (planner-maybe-remove-file)))) (unless (or force (looking-at "\n\\*")) (setq names nil)))) (setq names (cdr names))) (when planner-carry-tasks-forward (switch-to-buffer today-buffer) (when (> (length tasks) 0) (insert tasks) (save-buffer))) (if (emacs-wiki-page-file today) (planner-goto-today)))) (defun planner-jump-to-linked-task () "Display the linked task page." (interactive) (let* ((task-info (planner-current-task-info)) (link (and task-info (planner-task-link task-info)))) (when (and link (assoc link (emacs-wiki-file-alist))) (emacs-wiki-find-file (planner-task-link task-info)) (goto-char (point-min)) (if (search-forward (planner-task-description task-info) nil t) (beginning-of-line)) t))) (defun planner-renumber-tasks () "Update task numbering." (interactive) (save-excursion (if font-lock-mode (font-lock-unfontify-region (point-min) (point-max))) (goto-char (point-min)) (let ((counters (list (cons "A" 1) (cons "B" 1) (cons "C" 1)))) (while (re-search-forward "^#\\([A-C]\\)\\([0-9]+\\)" nil t) (let ((counter (assoc (match-string 1) counters))) (replace-match (number-to-string (cdr counter)) t t nil 2) (setcdr counter (1+ (cdr counter)))))) (goto-char (point-min)) (when (re-search-forward "^#\\([A-C]\\)\\([0-9]+\\)" nil t) (goto-char (match-beginning 0)) (let ((here (point))) (let ((paragraph-start (concat paragraph-start "\\|\\*.+"))) (forward-paragraph)) (sort-fields-1 1 here (point) (lambda () (skip-chars-forward "#ABC") (let ((case-fold-search t) (ch (char-before)) status) (skip-chars-forward "0123456789 ") (setq status (char-after)) (skip-chars-backward "0123456789 ") (+ (read (current-buffer)) (cond ((eq status ?X) 1000) (t 0)) (cond ((eq ch ?A) 100) ((eq ch ?B) 200) ((eq ch ?C) 300))))) nil))) ;; Fix numbering after completed tasks are moved to the bottom. (goto-char (point-min)) (let ((counters (list (cons "A" 1) (cons "B" 1) (cons "C" 1)))) (while (re-search-forward "^#\\([A-C]\\)\\([0-9]+\\)" nil t) (let ((counter (assoc (match-string 1) counters))) (replace-match (number-to-string (cdr counter)) t t nil 2) (setcdr counter (1+ (cdr counter)))))) (if font-lock-mode (font-lock-fontify-region (point-min) (point-max))))) (defun planner-markup-tasks (beg end &optional verbose) "Highlight tasks from BEG to END. VERBOSE is ignored." (goto-char beg) (while (re-search-forward (concat "^#\\([A-C]\\)\\([0-9]+\\) \\(" planner-marks-regexp "\\) ") end t) (let ((mark (match-string 3))) (add-text-properties (match-beginning 0) (1+ (line-end-position)) (list 'face (cond ((string= mark "X") 'italic) ((string= mark "o") 'bold) ((string= mark ">") 'bold-italic))))))) (defun planner-markup-notes (beg end &optional verbose) "Highlight notes as second-level headers from BEG to END. VERBOSE is ignored." (goto-char beg) (while (re-search-forward (concat "^.#\\([0-9]+\\) ") end t) (let ((mark (match-string 3))) (add-text-properties (match-beginning 0) (1+ (line-end-position)) '(face emacs-wiki-header-2))))) (defun planner-raise-task (&optional arg) "Change the priority of the current task. Negative ARG raises the priority by -ARG steps. Positive ARG lowers the priority by ARG steps. By default, ARG is -1." (interactive) (beginning-of-line) (kill-region (point) (1+ (line-end-position))) (forward-line (or arg -1)) (save-excursion (yank))) (defun planner-lower-task () "Lower the priority of the current task." (interactive) (planner-raise-task 1)) (defun planner-mark-task (mark &optional this-only) "Change task status to MARK. If THIS-ONLY is non-nil, the linked planner page is not updated." (let ((case-fold-search nil)) (save-excursion (beginning-of-line) (re-search-forward planner-marks-regexp) (replace-match mark) (unless this-only (save-window-excursion (if (planner-jump-to-linked-task) (planner-mark-task mark t))))))) (defun planner-task-in-progress () "Mark the current task as in progress." (interactive) (planner-mark-task "o")) (defun planner-task-done () "Mark the current task as done." (interactive) (planner-mark-task "X")) (defun planner-task-delegated () "Mark the current task as delegated." (interactive) (planner-mark-task ">")) (defun planner-task-pending () "Mark the current task as pending." (interactive) (planner-mark-task "_")) (defsubst planner-today () "Return the filename of the current date." (planner-date-to-filename (decode-time (current-time)))) (defun planner-clock-in () "Start working on a task." (interactive) (let ((task-info (planner-current-task-info))) (timeclock-in nil (if (planner-task-link task-info) (concat (planner-task-link task-info) ": " (planner-task-description task-info)) (planner-task-description task-info))) (planner-task-in-progress))) (defun planner-read-date () "Prompt for a date string in the minibuffer." (save-window-excursion (calendar) (read-string (format-time-string "When (%Y.%m.%d, %m.%d, %d): ")))) (defun planner-prepare-file () "Insert some standard sections into an empty planner file." (when (= (buffer-size) 0) (insert (if (string-match planner-date-regexp (buffer-name)) planner-day-page-template planner-plan-page-template)) (set-buffer-modified-p nil))) (defun planner-seek-to-first (&optional section) "Positions the point at the specified SECTION, or Tasks if not specified." (interactive) (unless section (setq section "Tasks")) (goto-char (point-min)) (planner-prepare-file) (funcall planner-seek-section-function section) (if (looking-at "\\*") (forward-line -1)) (when (looking-at (concat "\\* " section)) (forward-line 1))) (defun planner-seek-create-at-top (section) "Jumps to the specified SECTION. If not found, create at top of file." (goto-char (point-min)) (unless (re-search-forward (concat "^\\* " section "\n+") nil t) (let ((buffer-status (buffer-modified-p))) (insert "* " section "\n\n") (set-buffer-modified-p buffer-status)))) (defun planner-seek-create-at-bottom (section) "Jumps to the specified SECTION. If not found, create at bottom of file." (goto-char (point-max)) (if (re-search-backward (concat "^\\* " section "\n+") nil t) (goto-char (match-end 0)) (let ((buffer-status (buffer-modified-p))) (insert "\n* " section "\n\n") (set-buffer-modified-p buffer-status)))) (defun planner-page-exists-p (page) "Return t if PAGE exists." (let ((file (expand-file-name (planner-expand-name page) planner-directory))) (or (get-file-buffer file) (file-exists-p file)))) (defvar planner-goto-hook '(planner-seek-to-first) "Functions called after a planner page is opened.") ;;;###autoload (defun planner-goto (date &optional just-show) "Jump to the planning page for DATE. If no page for DATE exists and JUST-SHOW is non-nil, don't create a new page - simply return nil." (interactive (list (planner-read-date))) (if (or (not just-show) (planner-page-exists-p date)) (let ((file (expand-file-name (planner-expand-name date) planner-directory))) (if planner-use-other-window (find-file-other-window file) (find-file file)) (goto-char (point-min)) (run-hooks 'planner-goto-hook)) (message "No planner file for %s." date))) ;;;###autoload (defun planner-show (date) "Show the plan page for DATE in another window, but don't select it." (interactive (list (planner-read-date))) (save-selected-window (let ((planner-use-other-window t)) (planner-goto date planner-show-only-existing)))) ;;;###autoload (defun planner-goto-today () "Jump to the planning page for today." (interactive) (planner-goto (planner-today))) ;;;###autoload (defun planner-goto-most-recent () "Go to the most recent day with planning info." (interactive) (planner-goto-today) (if (looking-at "\n\\*") (plan))) (defvar planner-task-annotation nil "If set, use as the annotation for the current task.") (defun planner-create-task (title date) "Create a new task named TITLE based on the current Wiki page. If DATE is non-nil, makes a daily entry on DATE, else makes an entry in today's planner page. It's assumed that the current Wiki page is the page you're using to plan an activity. Any time accrued to this task will be applied to that page's name in the timelog file, assuming you use timeclock. With a prefix, creates a task for today and stores it in the TaskPool." (interactive (list (read-string "Describe task: ") (unless current-prefix-arg (planner-read-date)))) (let* ((origin (if (and (null planner-task-annotation) (memq major-mode '(planner-mode emacs-wiki-mode))) (if (looking-at "^\\.#\\([0-9]+\\)") (concat (emacs-wiki-page-name) "#" (match-string 1)) (emacs-wiki-page-name)))) (origin-buffer (and origin (current-buffer)))) (save-window-excursion (save-excursion (when current-prefix-arg (emacs-wiki-find-file "TaskPool" nil planner-directory) (setq origin "TaskPool")) (when (and origin (memq major-mode '(planner-mode emacs-wiki-mode)) (not (string-match planner-date-regexp origin))) (planner-seek-to-first) (insert "#A0 _ " title) (if planner-task-annotation (insert " " planner-task-annotation) (unless (string= date (planner-today)) (insert " (" (planner-expand-name date) ")"))) (insert "\n")) (if (and origin (not (string-match emacs-wiki-name-regexp origin))) (setq origin (concat "[[" origin "]]"))) (planner-goto (planner-expand-name date)) (insert "#A0 _ " title) (if planner-task-annotation (insert " " planner-task-annotation) (unless (or (not origin) (string-match planner-date-regexp origin)) (insert " (" origin ")"))) (insert "\n"))))) (defun planner-create-note (&optional page) "Create a note to be remembered in PAGE (today if PAGE is nil). If the note should generate a task, use \\[planner-copy-or-move-task] on the numbered line. Returns the index number of the created note." (interactive) (if page (planner-goto page) (planner-goto-today)) (let ((index 0)) (while (re-search-forward "^\\.#\\([0-9]+\\)" nil t) (setq index (string-to-int (match-string 1)))) (goto-char (point-max)) (unless (bolp) (insert ?\n)) (insert ?\n) (insert ".#" (int-to-string (1+ index)) " ") (1+ index))) (defsubst planner-strip-whitespace (string) "Remove all whitespace from STRING. Return the modified string." (replace-regexp-in-string "[\r\n\t ]+" "" string t t)) (defun planner-page-default-p (&optional buffer) "Return t if this plan page can be safely deleted. If the contents of this plan page are the same as the value of `planner-day-page-template' or the plan page is empty, then no information has been added and the page can safely be removed. If BUFFER is given, considers the planner page in BUFFER instead." (with-current-buffer (or buffer (current-buffer)) (if (not (> (buffer-size) (+ (length planner-day-page-template) planner-template-fuzz-factor))) (let ((body (planner-strip-whitespace (buffer-string)))) (or (= (length body) 0) (string= body (planner-strip-whitespace planner-day-page-template))))))) (defvar planner-delete-file-function 'delete-file "Function called to remove a planner file from the current wiki.") (defun planner-maybe-remove-file () "Delete the planner file if it does not contain new information." (if (planner-page-default-p (current-buffer)) (let ((filename buffer-file-name)) (set-buffer-modified-p nil) (kill-buffer (current-buffer)) (funcall planner-delete-file-function filename)) (kill-buffer (current-buffer)))) (defun planner-delete-task () "Deletes this task from the current page and the linked page." (interactive) (save-excursion (save-window-excursion (beginning-of-line) (let* ((task-info (planner-current-task-info)) (task-link (and task-info (planner-task-link task-info)))) (unless task-info (error "There is no task on the current line")) (beginning-of-line) (delete-region (point) (min (point-max) (1+ (line-end-position)))) (when task-link (emacs-wiki-find-file task-link) (planner-seek-to-first "Tasks") (when (search-forward (planner-task-description task-info) nil t) (beginning-of-line) (delete-region (point) (min (point-max) (1+ (line-end-position)))))))))) (defun planner-update-task () "Update the current task's priority and status on the linked page." (interactive) (save-excursion (save-window-excursion (beginning-of-line) (let* ((task-info (planner-current-task-info)) (task-link (and task-info (planner-task-link task-info))) (original (emacs-wiki-page-name))) (unless task-info (error "There is no task on the current line")) (unless task-link (error "There is no link for the current task")) (if (not (string-match emacs-wiki-name-regexp original)) (setq original (concat "[[" original "]]"))) (emacs-wiki-find-file task-link) (planner-seek-to-first "Tasks") (when (search-forward (planner-task-description task-info) nil t) (beginning-of-line) (delete-region (point) (min (point-max) (1+ (line-end-position))))) (insert "#" (planner-task-category task-info) "0 " (planner-task-status task-info) " " (planner-task-description task-info) " " "(" original ")" "\n"))))) (defun planner-copy-or-move-task (&optional date) "Move the current task to DATE. If this is the original task, it copies it instead of moving. Most of the time, the original should be kept in a planning file, but this is not required. It also works for creating tasks from a Note. This function is the most complex aspect of planner.el." (interactive (list (planner-expand-name (planner-read-date)))) (if (equal date (emacs-wiki-page-name)) (error "Cannot move a task back to the same day!")) (save-excursion (save-window-excursion (beginning-of-line) (let* ((task-info (planner-current-task-info)) (task-link (and task-info (planner-task-link task-info))) plan-page date-page) (unless task-info (error "There is no task on the current line")) (if (equal task-link date) (error "Cannot move a task back to the same day!")) (if (equal (planner-task-status task-info) "X") (error "Cannot reschedule a completed task")) (if (string-match planner-date-regexp (emacs-wiki-page-name)) (progn (setq plan-page task-link) (setq date-page (emacs-wiki-page-name))) (setq plan-page (emacs-wiki-page-name)) (setq date-page task-link)) ;; Delete it from the old date page (when date-page (planner-goto date-page) (goto-char (point-min)) (when (search-forward (planner-task-description task-info) nil t) (beginning-of-line) (delete-region (point) (min (point-max) (1+ (line-end-position)))))) ;; Update planner page (when (and plan-page (assoc plan-page (emacs-wiki-file-alist))) (emacs-wiki-find-file plan-page) (goto-char (point-min)) (when (search-forward (planner-task-description task-info) nil t) (beginning-of-line) (delete-region (point) (min (point-max) (1+ (line-end-position)))) (insert "#" (planner-task-category task-info) "0 " (planner-task-status task-info) " " (planner-task-description task-info) " " "(" date ")" "\n"))) (planner-goto date) (if (and plan-page (not (string-match emacs-wiki-name-regexp plan-page))) (setq plan-page (concat "[[" plan-page "]]"))) (insert "#" (planner-task-category task-info) "0 " (planner-task-status task-info) " " (planner-task-description task-info) " " (if plan-page (concat "(" plan-page ")\n") "\n")))))) (defun planner-copy-or-move-region (beg end &optional date muffle-errors) "Move all tasks from BEG to END to DATE. If this is the original task, it copies it instead of moving. Most of the time, the original should be kept in a planning file, but this is not required. `planner-copy-or-move-region' will copy or move all tasks from the line containing BEG to the line just before END. If MUFFLE-ERRORS is non-nil, no errors will be reported." (interactive "r") (unless date (setq date (planner-expand-name (planner-read-date)))) (save-excursion (let ((start (if (< beg end) beg end)) (finish (if (< beg end) end beg)) (buffer (current-buffer)) (error-count 0) done) ;; Invoke planner-copy-or-move-task on each line in reverse (goto-char (1- finish)) (while (not done) (goto-char (line-beginning-position)) (when (looking-at (concat "^#?\\([A-C]\\)\\([0-9]*\\) \\(" planner-marks-regexp "\\) \\(.+\\)")) (condition-case err (planner-copy-or-move-task date) (error (unless muffle-errors (princ (format "Error with %s: %s" (elt (planner-current-task-info) 4) err)) (setq error-count (1+ error-count))) nil)) (set-buffer buffer)) (if (<= (point) start) (setq done t)) (forward-line -1)) (if (and (not muffle-errors) (not error-count) (> error-count 0)) (message (if (> error-count 1) "%d errors." "%d error.") error-count)) (save-buffer)))) (defun planner-get-message-id () "Return the message-id of the current message." (save-excursion (set-buffer (get-buffer gnus-article-buffer)) (set-buffer gnus-original-article-buffer) (goto-char (point-min)) (let ((case-fold-search t)) (if (re-search-forward "^Message-ID:\\s-*\\(<.+>\\)" (point-max) t) (match-string 1))))) (defun planner-get-from () "Return the address of the sender of the current message." (save-excursion (set-buffer (get-buffer gnus-article-buffer)) (set-buffer gnus-original-article-buffer) (goto-char (point-min)) (let ((case-fold-search t)) (if (re-search-forward "^From:\\s-*\\(.+\\)" (point-max) t) (let ((addr (mail-extract-address-components (match-string 1)))) (if planner-use-bbdb (let ((rec (apply 'bbdb-search-simple addr))) (if rec (bbdb-record-name rec) (or (car addr) (cadr addr)))) (or (car addr) (cadr addr)))))))) (defun planner-task-from-gnus () "Generate a new task, based on a mail message." (interactive) (let* ((newsgroup gnus-newsgroup-name) (planner-task-annotation (concat "[[gnus://" newsgroup "/" (planner-get-message-id) "][(E-Mail from " (planner-get-from) ")]]"))) (call-interactively 'planner-create-task))) (defun planner-browse-url (url) "Jump to a planner URL." (cond ((string-match "^gnus://\\(.+\\)/\\(.+\\)" url) (let ((group (match-string 1 url)) (article (match-string 2 url))) (unless (and (fboundp 'gnus-alive-p) (gnus-alive-p)) (gnus-unplugged)) (switch-to-buffer "*Group*") (gnus-group-jump-to-group group) (gnus-group-select-group) (gnus-summary-goto-article article))) ((string-match "^bbdb://\\(.+\\)" url) (bbdb (match-string 1 url) nil)) (t (browse-url url)))) (defun planner-expand-name (name) "Expand the given NAME to its fullest form. This typically means that dates like 3.31 will become 2001.03.31." (let ((bufname (and buffer-file-name (file-name-nondirectory buffer-file-name))) year month) (if (and bufname (string-match planner-date-regexp bufname)) (setq year (string-to-number (match-string 1 bufname)) month (string-to-number (match-string 2 bufname))) (let ((now (decode-time (current-time)))) (setq year (nth 5 now) month (nth 4 now)))) (setq year (format "%04d." year) month (format "%02d." month)) (if (= (length name) 0) (planner-today) (if (string-match (concat "\\([1-9][0-9][0-9][0-9]\\.\\)?" "\\(\\([0-9]+\\)\\.\\)?" "\\([0-9]+\\)\\(#.*\\)?") name) (concat (or (match-string 1 name) year) (if (match-string 2 name) (format "%02d." (string-to-int (match-string 3 name))) month) (format "%02d" (string-to-int (match-string 4 name))) (match-string 5 name)) name)))) (defun planner-extract-tasks (task-type) "Return a list of the tasks in the current buffer. If TASK-TYPE is non-nil, only tasks with the mark TASK-TYPE will be listed." (let (tasks) (save-excursion (goto-char (point-min)) (while (re-search-forward (concat "^#?[A-C][0-9]* [" task-type "]") nil t) (add-to-list 'tasks (buffer-substring-no-properties (match-beginning 0) (line-end-position)) t))) tasks)) (defun planner-select-task (task-type task-action) "Jump to a particular task (of TASK-TYPE, if non-nil) and perform TASK-ACTION." (interactive) (let (tasks selected-task) (planner-goto-today) (setq tasks (planner-extract-tasks task-type))) (setq selected-task (read-string "Task: " nil 'tasks)) (when selected-task (goto-char (point-min)) (if (member selected-task tasks) (search-forward selected-task) (planner-create-task selected_task (planner-today))) (funcall task-action))) (defun planner-select-task-to-work-on () "Prompts for a task to mark in progress." (interactive) (planner-select-task "_>" 'planner-task-in-progress)) (defun planner-select-task-done () "Prompts for a task to mark finished." (interactive) (planner-select-task "_o>" 'planner-task-done)) (defun planner-select-task-delegated () "Prompts for a task to mark delegated." (interactive) (planner-select-task "_o" 'planner-task-delegated)) ;;;###autoload (define-derived-mode planner-mode emacs-wiki-mode "Planner" "An extension to Emacs Wiki that supports a planning system." (cond ((boundp 'write-file-functions) (add-hook 'write-file-functions 'planner-renumber-tasks nil t)) ((boundp 'local-write-file-hooks) (add-hook 'local-write-file-hooks 'planner-renumber-tasks nil t)) (t (add-hook 'local-write-file-hooks 'planner-renumber-tasks nil t))) (add-hook 'emacs-wiki-highlight-buffer-hook 'planner-markup-tasks nil t) (add-hook 'emacs-wiki-highlight-buffer-hook 'planner-markup-notes nil t)) (defun planner-update-wiki-project () "Update the \"planner\" project in emacs-wiki-projects." (setq emacs-wiki-projects (delq (assoc "WikiPlanner" emacs-wiki-projects) emacs-wiki-projects)) (add-to-list 'emacs-wiki-projects `("WikiPlanner" . ((emacs-wiki-directories . (,planner-directory)) (emacs-wiki-major-mode . planner-mode) (emacs-wiki-markup-tags . ,(append planner-markup-tags emacs-wiki-markup-tags)) (emacs-wiki-publishing-markup . ,(append planner-publishing-markup emacs-wiki-publishing-markup)) (emacs-wiki-url-or-name-regexp . nil) (emacs-wiki-url-regexp . ,planner-url-regexp) (emacs-wiki-name-regexp . ,(concat "\\(" emacs-wiki-name-regexp "\\|" planner-name-regexp "\\)")) (emacs-wiki-browse-url-function . planner-browse-url) ;; this is here just so the right url-or-name-regexp value is ;; used (emacs-wiki-highlight-regexp . nil) (emacs-wiki-highlight-vector . nil) (emacs-wiki-highlight-markup . ,emacs-wiki-highlight-markup) ,@planner-custom-variables))) (emacs-wiki-update-project-interwikis)) (setq planner-loaded t) (planner-update-wiki-project) (add-hook 'emacs-wiki-update-project-hook 'planner-update-wiki-project) (put 'planner-mode 'flyspell-mode-predicate 'emacs-wiki-mode-flyspell-verify) ;;; Date code (defun planner-get-current-date-filename () "Return the date of the daily page currently being viewed. If no daily page is being viewed, return today's date." (if (string-match planner-date-regexp (buffer-name)) (buffer-name) (planner-today))) (defun planner-filename-to-calendar-date (filename) "Return the date of the planning file FILENAME. Date is a list (month day year)." (list (string-to-number (substring filename 5 7)) ; month (string-to-number (substring filename 8 10)) ; day (string-to-number (substring filename 0 4)))) ; year (defun planner-date-to-filename (date) "Return the planner filename corresponding to DATE. DATE is a list (month day year) or an internal date representation." (if (= (length date) 3) (format "%04d.%02d.%02d" (elt date 2) (elt date 0) (elt date 1)) (if (= (length date) 2) (setq date (decode-time date))) (format "%04d.%02d.%02d" (elt date 5) ; year (elt date 4) ; month (elt date 3)))) ; day (defun planner-calculate-date-from-day-offset (origin offset) "From ORIGIN, calculate the date OFFSET days into the past or future. ORIGIN can be a buffer name, a list of the form (MONTH DAY YEAR), or an internal date representation. If OFFSET is positive, returns a date in the future. If OFFSET is negative, returns the date -OFFSET days in the past. Return an object that is the same type as ORIGIN." (cond ((stringp origin) (let ((date (planner-filename-to-calendar-date origin))) (planner-date-to-filename (encode-time 0 0 0 (+ (elt date 1) offset) (elt date 0) (elt date 2))))) ((= (length origin) 2) (encode-time 0 0 0 (+ (elt origin 1) offset) (elt origin 0) (elt origin 2))) ((= (length origin) 3) (let ((result (decode-time (encode-time 0 0 0 (+ (elt origin 1) offset) (elt origin 0) (elt origin 2))))) (list (elt result 4) (elt result 3) (elt result 5)))))) (defun planner-get-previous-existing-day (date) "Return the planner file immediately before DATE. DATE is a filename or a list (month day year). When called from a planner file, DATE defaults to the date of this file, otherwise it defaults to today. Returns an object of the same type as DATE." (interactive (list (planner-get-current-date-filename))) (let ((newdate (if (listp date) (planner-date-to-filename date) date)) (result nil)) ;; beginning of hackish part (mapcar (lambda (elt) (if (and (or (not result) (string> elt result)) (string< elt newdate)) (setq result elt))) (planner-list-daily-files)) (if result (if (listp date) (planner-filename-to-calendar-date result) result) (error "No previous planner file")))) (defun planner-get-next-existing-day (date) "Return the existing planner file immediately after DATE. DATE is a filename or a list (month day year). When called from a planner file, DATE defaults to the date of this file, otherwise it defaults to today. Returns an object of the same type as DATE." (interactive (list (planner-get-current-date-filename))) (let ((newdate (if (listp date) (planner-date-to-filename date) date)) (result nil)) ;; beginning of hackish part (mapcar (lambda (elt) (if (and (or (not result) (string< elt result)) (string> elt newdate)) (setq result elt))) (planner-list-daily-files)) (if result (if (listp date) (planner-filename-to-calendar-date result) result) (error "No next planner file")))) (defun planner-goto-yesterday (&optional days) "Goto the planner page DAYS before the currently displayed date. If DAYS is nil, goes to the day immediately before the currently displayed date. If the current buffer is not a daily planner page, calculates date based on today." (interactive "P") (if (listp days) (setq days (car days))) (planner-goto (planner-calculate-date-from-day-offset (planner-get-current-date-filename) (if days (- days) -1)))) (defun planner-goto-tomorrow (&optional days) "Goto the planner page DAYS after the currently displayed date. If DAYS is nil, goes to the day immediately after the currently displayed date. If the current buffer is not a daily planner page, calculates date based on today." (interactive "P") (if (listp days) (setq days (car days))) (planner-goto (planner-calculate-date-from-day-offset (planner-get-current-date-filename) (or days 1)))) (defun planner-goto-previous-daily-page () "Goto the planner page immediately after the currently displayed date. Does not create pages if they do not yet exist." (interactive) (planner-goto (planner-get-previous-existing-day (planner-get-current-date-filename)))) (defun planner-goto-next-daily-page () "Goto the planner page immediately after the currently displayed date. Does not create pages if they do not yet exist." (interactive) (planner-goto (planner-get-next-existing-day (planner-get-current-date-filename)))) (defun planner-list-daily-files () "Return an unsorted list of daily files in `planner-directory'." ;; get a list of all files (save-some-buffers t (lambda () (equal 'planner-mode major-mode))) (directory-files planner-directory nil ; no full name planner-date-regexp 'no-sort)) ;;; Code to generate a report from timeclock (defun planner-timeclock-report-tag (beg end highlight-p) "Replace the region BEG to END with a timeclock report. If HIGHLIGHT-P is non-nil, the output is also displayed." (require 'timeclock) (if highlight-p (add-text-properties beg end (list 'display (with-temp-buffer (timeclock-generate-report emacs-wiki-publishing-p) (buffer-string)))) (delete-region beg end) (timeclock-generate-report emacs-wiki-publishing-p) (add-text-properties beg (point) '(read-only t)))) (defun planner-past-notes-tag (beg end &optional attrs) "Replace the region BEG to END with an index of past notes. If ATTRS is non-nil, it is an alist containing values for DIRECTORY and START." (let ((files (nreverse (directory-files (or (cdr (assoc "directory" attrs)) planner-directory) t planner-date-regexp))) (earliest (cdr (assoc "start" attrs)))) (while files (if (or (null earliest) (not (string-lessp (car files) earliest))) (let ((title-lines (list t))) (with-temp-buffer (insert-file-contents-literally (car files) t) (while (re-search-forward "^\\.#\\([0-9]+\\)\\s-+\\(.+\\)" nil t) (nconc title-lines (list (cons (match-string 1) (match-string 2)))))) (setq title-lines (cdr title-lines)) (when title-lines (insert "[[" (emacs-wiki-page-name (car files)) "]] ::\n") (insert "
\n") (while title-lines (insert (format "
[[%s#%s][%s]]
\n" (emacs-wiki-page-name (car files)) (caar title-lines) (cdar title-lines))) (setq title-lines (cdr title-lines))) (insert "
\n\n")))) (setq files (cdr files))))) ;;; planner.el and other modules ;;; Calendar ;;;###autoload (defun planner-insinuate-calendar () "Hook Planner into Calendar. Adds special planner key bindings to `calendar-mode-map'. After this function is evaluated, you can use the following planner-related keybindings in `calendar-mode-map': n jump to the planner page for the current day. N display the planner page for the current day." (interactive) (require 'calendar) (add-hook 'calendar-move-hook (lambda () (when planner-calendar-show-planner-files (planner-calendar-show)))) (define-key calendar-mode-map "n" 'planner-calendar-goto) (define-key calendar-mode-map "N" 'planner-calendar-show)) (defvar planner-calendar-buffer-list nil "List of buffers opened by calendar.") (defun planner-kill-calendar-files () "Remove planner files shown from Calendar." (interactive) (message "Called!") (while planner-calendar-buffer-list (if (buffer-live-p (car planner-calendar-buffer-list)) (with-current-buffer (car planner-calendar-buffer-list) (save-buffer) (planner-maybe-remove-file))) (setq planner-calendar-buffer-list (cdr planner-calendar-buffer-list)))) ;;;###autoload (defun planner-calendar-goto () "Goto the plan page corresponding to the calendar date." (interactive) (let ((planner-use-other-window t)) (planner-goto (planner-date-to-filename (calendar-cursor-to-date))))) ;;;###autoload (defun planner-calendar-show () "Show the plan page for DATE in another window." (interactive) (save-selected-window (let ((planner-use-other-window t)) (planner-goto (planner-date-to-filename (calendar-cursor-to-date)) planner-show-only-existing) (add-to-list 'planner-calendar-buffer-list (current-buffer))))) (defadvice exit-calendar (after planner activate protect compile) "Call `planner-kill-calendar-files'." (planner-kill-calendar-files)) ;;; Gnus ;;;#autoload (defun planner-insinuate-gnus () "Hook Planner into Gnus. Adds special planner keybindings to the variable `gnus-summary-article-map'. From the Gnus summary article view, you can type: \\\\[gnus-summary-article-map] n planner-task-fom-gnus" (eval-after-load "gnus-sum" (define-key gnus-summary-article-map "n" 'planner-task-from-gnus))) ;;; Diary (defun planner-get-diary-entries (&optional date) "Return a string containing the diary entries for DATE. Return nil if none found. DATE is a list (month day year) or a filename. If DATE is nil, this returns a string containing the diary entries for the current date. You can use this function in your planner files or in `planner-day-page-template' by including the following line: (`planner-get-diary-entries') Note that the results displayed are dynamically generated from M-x diary and any changes made to the planner file will not be reflected in your diary file." (require 'diary-lib) (unless date (setq date (emacs-wiki-page-name))) (if (stringp date) (setq date (planner-filename-to-calendar-date date))) (with-temp-buffer (let ((fancy-diary-buffer (buffer-name)) (diary-display-hook (lambda () (kill-buffer (current-buffer)))) result) (setq result (car (cdr (car (list-diary-entries date 1))))) (set-buffer-modified-p nil) result))) (provide 'planner) ;;; planner.el ends here