hamacs/elisp/boxes.el

239 lines
9.8 KiB
EmacsLisp
Raw Normal View History

;;; BOXES --- An opinionated approach to a GTD workflow
;;
;; Author: Howard Abrams <howard@howardabrams.com>
;; Copyright © 2019, Howard Abrams, all rights reserved.
;; Created: 7 January 2019
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;; Commentary:
;;
;; WARNING: This file is tangled from its original essay.
;;
;; For a thorough explanation of this code, see the online essay:
;; http://www.howardism.org/Technical/Emacs/getting-more-boxes-done.html
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; This program 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 3, or
;; (at your option) any later version.
;;
;; This program 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 this program; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
;; Floor, Boston, MA 02110-1301, USA.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;; Code:
(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")
(defvar org-capture-templates (list))
(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))
(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"))
(setq org-refile-use-outline-path 'file
org-refile-allow-creating-parent-nodes t
org-outline-path-complete-in-steps nil)
(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)
(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)))))
(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.")
(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))))
(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))
(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)))
(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)))
;; Attempt to load the extra library functions tangled from a different essay:
(condition-case nil
(load-library "boxes-extras")
(error
(defun org-refile-to-projects-dir ()
(interactive)
(message "Need to load the 'boxes-extra project first."))
(defun org-refile-to-personal-dir ()
(interactive)
(message "Need to load the 'boxes-extra project first."))
(defun org-refile-to-technical-dir ()
(interactive)
(message "Need to load the 'boxes-extra project first."))))
(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)))
(defun ha/org-agenda ()
"Displays my favorite agenda perspective."
(interactive)
(org-agenda nil "a")
(get-buffer "*Org Agenda*")
(execute-kbd-macro (kbd "A t")))
(provide 'boxes)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; boxes.el ends here