Getting Boxes Done, the Code

Benjamin Franklin once wrote,

“For every minute spent in organizing, an hour is earned.”

Due to Emacs, I may have accrued nearly a lifetime by now.

As I mentioned in the first part of this essay, my shiny idea syndrome filled my task list with half-baked ideas, shopping lists, books to read, and solutions to all the world’s problems.

Assuming you like the idea of storing your project ideas in Org, the rest of essay describes the details and the code I use. Since the code for this project is tangled from this essay, ala literate programming, this page should stay up-to-date. ;-)

Workflow with an Example

As I mentioned before, I pretend ideas are on a virtual index card, and process them as I move them around organizational boxes. After creating a flowchart, I simplified it into more of a workflow:

getting-more-boxes-done-01.jpg

What are the red letters, you ask? Those are the key bindings I use to move the virtual index cards from my inbox to other boxes (files or directories). You’ll see where I specify these and their implementation later.

Perhaps an example of using this workflow would be helpful.

I may notice that I’m running low on personal supplies, and quickly jot a note in Orgzly:

getting-more-boxes-done-05.png

This puts a line in my Inbox:

* Buy toothpaste and eye drops

The next morning, as I’m reviewing the entries, I hit t to refile (move) the entry into my Task List. After I’ve been shopping, and later review the task list, I hit a to archive it, or c to archive it the completed file, which for me, is that day’s journal entry (however, you could specify a file like, accomplishments-yay-me.org). Completed tasks in org are typically archived and I discuss the details of this later on.

How about another example? The other day while noticing the large number of uncommitted text files (why yes, my org directories are under git control), I thought of an idea. I quickly kick off an Org capture and put another entry in my Inbox:

* Can I do a git commit automatically (at least, more regularly)?

When reviewing my Inbox the next day, I realize, while a good idea, I have no clue how to accomplish it. Since it isn’t ready, I refile it to my incubate file (by typing i). Returning to my incubating entries I hatch a plan (which in this example involves a little web searching). With a plan, the wish becomes a project, I update the entry:

* TODO Git commit on Sleep
  SCHEDULED: <2018-12-28 Fri>

  Certain directories under git-control often go weeks or months between
  commits, as "projects" containing text notes don't really have a *change* to
  commit. What about a checkpoint? Perhaps closing laptop or system sleeps, we
  commit all files.

  * On a Mac, checkout [[https://www.bernhard-baehr.de/][SleepWatcher]]
  * On Linux, checkout [[https://launchpad.net/ubuntu/+source/pm-utils][pm-utils]]

  Goal should be to have a script that both programs can run, e.g.

  #+BEGIN_SRC shell
      ~/bin/sleepwatcher -d -s ~/bin/on-sleep
  #+END_SRC

Note the TODO label and scheduled date, as these make it show on my agenda.

As a real project, I refile it from incubate to tasks using t, or if it’s a larger, involved project, to its own file in projects using m P. Completion of a project moves it again, but since this project contains notes I may have to reference later, I shuffle the entire file to my technical directory.

Why so many steps and so many files? In a large project management system, tasks are typically associated with states in a database where views present tasks in different states. I’m working with text files, so tasks in different states live in different files. Normally, this would be painful, but not with org-mode. Single key sequences shuffle the text to different files, allowing me to view state by simply viewing a file.

If this concept is clear, the remaining sections include my procedures (and Emacs functions) for processing ideas from inception to completion using the examples described above.

See all those destinations in the workflow illustration above? Let’s create constant variables, where the functions can use them later:

(defvar org-default-projects-dir   "~/projects"                     "Primary GTD directory")
(defvar org-default-technical-dir  "~/technical"                    "Directory of shareable notes")
(defvar org-default-personal-dir   "~/personal"                     "Directory of un-shareable, personal notes")
(defvar org-default-completed-dir  "~/projects/trophies"            "Directory of completed project files")
(defvar org-default-inbox-file     "~/projects/breathe.org"         "New stuff collects in this file")
(defvar org-default-tasks-file     "~/projects/tasks.org"           "Tasks, TODOs and little projects")
(defvar org-default-incubate-file  "~/projects/incubate.org"        "Ideas simmering on back burner")
(defvar org-default-completed-file nil                              "Ideas simmering on back burner")
(defvar org-default-notes-file     "~/personal/general-notes.org"   "Non-actionable, personal notes")
(defvar org-default-media-file     "~/projects/media.org"           "White papers and links to other things to check out")

Why yes, if you steal my code, you’ll want to change those values.

Oh, and since some of these are pretty cool and could have items that would show up on an agenda, we may want to add them to org-agenda-files automatically:

(add-to-list 'org-agenda-files org-default-inbox-file)
(add-to-list 'org-agenda-files org-default-tasks-file)

Or, you can use code like this to add all files in the projects directory as potential agenda files:

(setq org-agenda-files (list org-default-projects-dir))
(setq org-agenda-file-regexp "^[a-z0-9-_]+.org")

However, let’s begin by keeping it simple:

(dolist (agenda-file (list org-default-inbox-file
                          org-default-tasks-file
                          org-default-incubate-file
                          org-default-notes-file))
  (add-to-list 'org-agenda-files (expand-file-name agenda-file)))

The Inbox, breathe.org

Everything begins as an entry in the Inbox (org-default-inbox-file) that I call breathe.org (seemed like a good filename to avoid feeling overwhelmed), so I need a flexible capturing entry to make it easy to create entries in that file:

(add-to-list 'org-capture-templates
             `("t" "Task Entry"        entry
               (file ,org-default-inbox-file)
               "* %?\n:PROPERTIES:\n:CREATED:%U\n:END:\n\n%i\n\nFrom: %a"
               :empty-lines 1))

Calling org-capture (which most people bind to C-c c, but Spacemacs has as SPC C c) might look like the following screenshot:

getting-more-boxes-done-02.png

Note: With Orgzly (or other mobile org-specific application) configured to use this file as its default destination for all new ideas or todos, your mobile device will match your system.

Tidying by Refiling

As a temporary holding spot for all incoming tasks, notes and ideas, the goal is to keep the Inbox empty. Every day, I review the collected entries, and adjust and move them to appropriate locations.

I have created a little helper system for processing these ideas through the workflow shown in the flowchart above. Here is a screenshot of it in action where I am currently determining what to do with an entry captured earlier. The menu at the bottom of the screen matches my workflow shown earlier:

getting-more-boxes-done-04.png

Note: The screenshot above may not match the code you’ll see below. Since this essay is really just a literate programming approach to generating the code I actually use, I’ll be changing the code, but may not re-render the images. Sorry.

While Emacs has many options for calling functions, I’ve chosen to use the Hydra project to present a focused collection of keybindings to functions. Originally I made a minor mode, but found Hydra’s hint screen and repetitive key calls easier (once I start the hydra, I can simply type i multiple times to refile multiple entries to the incubation chamber).

Here is the definition for the Hydra, and you’ll notice that after the hint display, it is just a list of keybindings to functions:

(defhydra hydra-org-refiler (org-mode-map "C-c s" :hint nil)
    "
  ^Navigate^      ^Refile^       ^Move^           ^Update^        ^Go To^        ^Dired^
  ^^^^^^^^^^---------------------------------------------------------------------------------------
  _k_: ↑ previous _t_:   tasks     _m X_: projects  _T_: todo task  _g t_: tasks    _g X_: projects
  _j_: ↓ next     _i_/_I_: incubate  _m P_: personal  _S_: schedule   _g i_: incubate _g P_: personal
  _c_: archive    _p_:   personal  _m T_: technical _D_: deadline   _g x_: inbox    _g T_: technical
  _d_: delete     _r_:   refile                   _R_: rename     _g n_: notes    _g C_: completed
  "
    ("<up>" org-previous-visible-heading)
    ("<down>" org-next-visible-heading)
    ("k" org-previous-visible-heading)
    ("j" org-next-visible-heading)
    ("c" org-archive-subtree-as-completed)
    ("d" org-cut-subtree)
    ("t" org-refile-to-task)
    ("i" org-refile-to-incubate)
    ("I" org-refile-to-another-incubator)
    ("p" org-refile-to-personal-notes)
    ("r" org-refile)
    ("m X" org-refile-to-projects-dir)
    ("m P" org-refile-to-personal-dir)
    ("m T" org-refile-to-technical-dir)
    ("T" org-todo)
    ("S" org-schedule)
    ("D" org-deadline)
    ("R" org-rename-header)
    ("g t" (find-file-other-window org-default-tasks-file))
    ("g i" (find-file-other-window org-default-incubate-file))
    ("g x" (find-file-other-window org-default-inbox-file))
    ("g c" (find-file-other-window org-default-completed-file))
    ("g n" (find-file-other-window org-default-notes-file))
    ("g X" (dired org-default-projects-dir))
    ("g P" (dired org-default-personal-dir))
    ("g T" (dired org-default-technical-dir))
    ("g C" (dired org-default-completed-dir))
    ("[\t]" (org-cycle))
    ("s" (org-save-all-org-buffers) "save")
    ("<tab>" (org-cycle) "toggle")
    ("q" nil "quit"))

We just need to bind something like C-c s to call hydra-org-refiler/body. Or, under Spacemacs with a leader key:

(spacemacs/set-leader-keys-for-major-mode 'org-mode "r" 'hydra-org-refiler/body)

Or Doom’s leader key:

(map! :map org-mode-map :localleader
      :desc "boxes" "B" 'hydra-org-refiler/body)

Now I have a system for calling all these mover and shaker functions. Some functions, like org-schedule, come with Org, while others, I’ll need to write.

Ideas in Incubation: incubate.org

Things and ideas that need fleshing before tackling go into an incubation file. Org has a nice feature for moving a subtree (an Org header entry and its contents) to another file, org-refile (see the Org Manual for details).

Calling the org-refile function presents a list of target files, which you need to program using an odd cons entry. I found specifying these easier to learn by example. Along with the target file, you need to specify the heading levels presented as a destination. Since I want top-level entries from Inbox to go to top-level entries in the Incubation file, I specify a level of 0:

(setq org-refile-targets `((,org-default-incubate-file :level . 0)))

Not familiar with that back-tick/comma syntax? You can define a list of symbols with '(a b c), where a is not a variable, but just a Lisp symbol. But what if you wanted the b to actually refer to the value of a variable? For this, you replace the normal tick ' (apostophe or single quote) with a back-tick, ` (or back quote), which acts the same, except that anything that follows a comma is a variable, and Emacs replaces it with its value. I guess I could have written that code as:

(setq org-refile-targets (list (list org-default-incubate-file :level . 0)))

But now I am getting off topic… sorry for that interlude.

Anyway, to allow refiling entries to incubate.org as a top-level destination, we need to make two variable changes:

(setq org-refile-use-outline-path 'file
      org-refile-allow-creating-parent-nodes t
      org-outline-path-complete-in-steps nil)

Now, move to an org-mode section, and call org-refile (with either C-c C-w or , s r if you are using Spacemacs or SPC m r r in Doom … wait, I shouldn’t bother with all the keystrokes, as you, gentle reader, can look that up with C-h f or SPC h f if you are using Spacemacs or Doom… and here I did it again).

Notice that incubate.org shows this as a destination.

Since I last updated this essay, I realize that I incubate a lot of ideas, and I figured I might want to expand the incubating refiling to any file that starts with incubate-

(defun boxes--get-incubators ()
  "Calls `completing-read' with a multiple choice of incubating files.
That is, any files located in the `org-default-projects-dir' directory
that begin with `incubate-'."
  (let ((choices (cons (cons "incubator" org-default-incubate-file)
                       (--map (cons (replace-regexp-in-string ".*incubate-\\(.*\\).org" "\\1" it)  it)
                              (directory-files org-default-projects-dir t "incubate-.*.org")))))
    (assoc (completing-read "Refile to Incubator:" choices) choices 'equal)))

(defun org-refile-to-another-incubator (incubator)
  "Refile the current org subtree to an incubate file.
A list of available `incubate-*.org' files are presented as a choice."
  (interactive (list #'boxes--get-incubators))
  (org-refile nil nil (list (car incubator) (cdr incubator) nil 0)))

tasks.org

Well-developed tasks go into the tasks.org file (which will show on my agenda):

(setq org-refile-targets `((,org-default-incubate-file :level . 0)
                           (,org-default-tasks-file :level . 0)))

Books to read and music to check out go into appropriate sub-sections in my media.org file, so I set :level to 1 for this target destination:

(setq org-refile-targets `((,org-default-incubate-file :level . 0)
                           (,org-default-media-file :level . 1)
                           (,org-default-tasks-file :level . 0)))

Now, refiling should look something like:

Refile subtree to:
incubate.org/
media.org/Articles
media.org/Books
media.org/Music
media.org/Movies
media.org/Series
media.org/Games
tasks.org/

However, every file in my projects directory could be a potential destination, and with the hydra making typical destinations easy, I could specify them all:

(setq org-refile-targets
      (append `((,(expand-file-name org-default-media-file) :level . 1)
                (,(expand-file-name org-default-notes-file) :level . 0))
              (->>
               (directory-files org-default-projects-dir nil ".org")
               (-remove-item (file-name-base org-default-media-file))
               (--remove (s-starts-with? "." it))
               (--remove (s-ends-with? "_archive" it))
               (--map (format "%s/%s" (expand-file-name org-default-projects-dir) it))
               (--map `(,it :level . 0)))))

(setq org-refile-target-table nil)

I am taking advantage of a threading macro from Magmar’s lovely dash library. In case you are unfamiliar with the ->> macro, this code gets a list of all the Org files, then removes the media file and any hidden files. It then takes that list of files and creates a new list of fully-qualified file names and appends the :level of 0. Once you get the idea of the threading macro, the code seems much easier to read.

If you haven’t installed dash, require the subr-x library, and call the more verbose thread-last macro, as it does the same thing.

getting-more-boxes-done-03.png

Refiling Programmatically

I can’t call org-refile programmatically with a file destination (as needed in the hydra shown above), as it is only interactive. More problematic, org-refile is a monolithic function, so I can’t call any helper functions it might use. I currently see no other approach but to implement my own simpler re-filer.

Copying regions is what Emacs does well…so, let’s define a region of a subtree:

(defun org-subtree-region ()
  "Return a list of the start and end of a subtree."
  (save-excursion
    (list (progn (org-back-to-heading) (point))
          (progn (org-end-of-subtree)  (point)))))

Now we can use that function to kill the region of a subtree, open a file (the target destination), and insert the previous contents:

(defun org-refile-directly (file-dest)
  "Move the current subtree to the end of FILE-DEST.
If SHOW-AFTER is non-nil, show the destination window,
otherwise, this destination buffer is not shown."
  (interactive "fDestination: ")

  (defun dump-it (file contents)
    (find-file-other-window file-dest)
    (goto-char (point-max))
    (insert "\n" contents))

  (save-excursion
    (let* ((region (org-subtree-region))
           (contents (buffer-substring (first region) (second region))))
      (apply 'kill-region region)
      (if org-refile-directly-show-after
          (save-current-buffer (dump-it file-dest contents))
        (save-window-excursion (dump-it file-dest contents))))))

(defvar org-refile-directly-show-after nil
  "When refiling directly (using the `org-refile-directly'
function), show the destination buffer afterwards if this is set
to `t', otherwise, just do everything in the background.")

After moving a subtree, do I want to see the resulting buffer in a window? If so, I use save-current-buffer (shown above), otherwise, this function calls save-window-execursion. I haven’t decided which I like best, so I have a customization variable that I can change.

Now, let’s create functions for the most-used refile destinations used by the Hydra (notice that the Hydra can also call org-refile directly in order to get access to all targets):

(defun org-refile-to-incubate ()
  "Refile (move) the current Org subtree to `org-default-incubate-fire'."
  (interactive)
  (org-refile-directly org-default-incubate-file))

(defun org-refile-to-task ()
  "Refile (move) the current Org subtree to `org-default-tasks-file'."
  (interactive)
  (org-refile-directly org-default-tasks-file))

(defun org-refile-to-personal-notes ()
  "Refile (move) the current Org subtree to `org-default-notes-file'."
  (interactive)
  (org-refile-directly org-default-notes-file))

(defun org-refile-to-completed ()
  "Refile (move) the current Org subtree to `org-default-completed-file',
unless it doesn't exist, in which case, refile to today's journal entry."
  (interactive)
  (if (and org-default-completed-file (file-exists-p org-default-completed-file))
      (org-refile-directly org-default-completed-file)
    (org-refile-directly (get-journal-file-today))))

Scheduling and Planning

While reviewing the collected ideas in my Inbox, I often need to tidy them before moving them around. Add a TODO label to each task with T in my hydra, as well as schedule a date with an S (as a task without due date is just a wish). Before I move the subtree, I may need to change the header’s text (which I added to the hydra with an R key):

(defun org-rename-header (label)
  "Rename the current section's header to LABEL, and moves the
point to the end of the line."
  (interactive (list
                (read-string "Header: "
                             (substring-no-properties (org-get-heading t t t t)))))
  (org-back-to-heading)
  (replace-string (org-get-heading t t t t) label))

Completing Tasks

Completed tasks are typically archived, using org-archive-subtree. This is a brilliant feature for keeping your notes relevant (as long as this feature doesn’t surprise you by an accidental fat-fingering of the keyboard):

getting-more-boxes-done-06.png

To archive to a separate file like this, set the variable, org-archive-location to "%s_archive".

However, in my world tasks marked as completed originate from multiple file locations; I would rather move these finished tasks to my journal. I use org-journal with a file for each day; for instance, 20180113 (without the .org extension…see my journal helper functions if you are curious).

This function archives a subtree to today’s journal entry (marking the task completed in the process):

(defun org-archive-subtree-as-completed ()
  "Archives the current subtree to today's current journal entry."
  (interactive)
  ;; According to the docs for `org-archive-subtree', the state should be
  ;; automatically marked as DONE, but I don't notice it, so let's force:
  (when (not (equal "DONE" (org-get-todo-state)))
    (org-todo "DONE"))

  (let* ((org-archive-file (or org-default-completed-file
                               (todays-journal-entry)))
         (org-archive-location (format "%s::" org-archive-file)))
     (org-archive-subtree)))

The following function returns the filename of today’s journal entry (which I can use as a refile destination):

(defun todays-journal-entry ()
  "Return the full pathname to the day's journal entry file.
Granted, this assumes each journal's file entry to be formatted
with year/month/day, as in `20190104' for January 4th.

Note: `org-journal-dir' variable must be set to the directory
where all good journal entries live, e.g. ~/journal."
  (let* ((daily-name   (format-time-string "%Y%m%d"))
         (file-name    (concat org-journal-dir daily-name)))
    (expand-file-name file-name)))

Refiling to Org Files

Archiving completed org entries (subtrees) is common, but sometimes a project entry grows and deserves its own file. While Org can refile or archive a subtree to a new file, these functions simply create a new file and copy the subtree. Personally, I really want a subtree to become a proper org file.

Writing that code became large and perhaps a bit of a white rabbit hole for most readers of this essay. Interested? Please, check out Part 3 of this essay.

Summary

We now have a Hydra that easily kicks off functions that help keep my task inbox manageable. Each morning, I would like to start an environment when my Inbox is loaded and ready for refiling. The following function does that by calling functions I would normally do manually:

(defun org-boxes-workflow ()
  "Load the default tasks file and start our hydra on the first task shown."
  (interactive)
  (let ((org-startup-folded nil))
    (find-file org-default-inbox-file)
    (delete-other-windows)
    (ignore-errors
      (ha/org-agenda))
    (delete-other-windows)
    (split-window-right-and-focus)
    (pop-to-buffer (get-file-buffer org-default-inbox-file))
    (goto-char (point-min))
    (org-next-visible-heading 1)
    (hydra-org-refiler/body)))

This assumes that I have a special agenda display:

(defun ha/org-agenda ()
  "Displays my favorite agenda perspective."
  (interactive)
  (org-agenda nil "a")
  (get-buffer "*Org Agenda*")
  (execute-kbd-macro (kbd "A t")))

I hope you can grab and modify this code to suit your own needs and whims. I may attempt to make the code more general and useful to others, but at the moment, I’d like to see if this code has any legs.

Let me know if you have any improvements you’d make.