summary refs log tree commit diff
diff options
context:
space:
mode:
authorFranck Cuny <fcuny@twitter.com>2019-08-29 16:56:24 -0700
committerFranck Cuny <fcuny@twitter.com>2019-08-29 16:56:24 -0700
commit244ecae6d5e4780ccf22e9333061a4c45a843647 (patch)
tree721dbef723f1e51d48bb32c4fafdcfe785c2dbfe
parent[org] fix template to create tasks (diff)
downloademacs.d-244ecae6d5e4780ccf22e9333061a4c45a843647.tar.gz
[org] major rewrite of the config
i've used the workflow documented at
http://www.howardism.org/Technical/Emacs/getting-more-boxes-done.html
for the last few days and it's definitely better than anything i've
tried so far.

the rewrite is mostly to replicate the configuration (with some minor
adjustments).
-rw-r--r--emacs.d/custom/fcuny-defuns.el225
-rw-r--r--emacs.d/custom/fcuny-org.el182
2 files changed, 335 insertions, 72 deletions
diff --git a/emacs.d/custom/fcuny-defuns.el b/emacs.d/custom/fcuny-defuns.el
index 40bd4ce..88e73a3 100644
--- a/emacs.d/custom/fcuny-defuns.el
+++ b/emacs.d/custom/fcuny-defuns.el
@@ -74,4 +74,229 @@
                             (decode-coding-string title 'utf-8))
       (concat "[[" url "][" title "]]"))))
 
+(defun fcuny/org-archive-subtree-as-completed ()
+  "Archives the current subtree to today's current journal entry."
+  (interactive)
+  (ignore-errors
+    ;; According to the docs for `org-archive-subtree', the state should be
+    ;; automatically marked as DONE, but I don't notice that:
+    (when (not (equal "DONE" (org-get-todo-state)))
+      (org-todo "DONE")))
+
+  (let* ((org-archive-file (or org-default-completed-file
+                               (fcuny/this-month-archive-entry)))
+         (org-archive-location (format "%s::" org-archive-file)))
+    (org-archive-subtree)))
+
+(defun fcuny/this-month-archive-entry ()
+  "Return the full pathname to the month's archive entry file.
+Granted, this assumes each journal's file entry to be formatted
+with year/month, as in `201901' 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 "%s-archive.org" (format-time-string "%Y-%m")))
+         (file-name    (concat org-archive-dir daily-name)))
+    (expand-file-name file-name)))
+
+(defun fcuny/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 fcuny/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 (fcuny/org-subtree-region))
+           (contents (buffer-substring (first region) (second region))))
+      (apply 'kill-region region)
+      (save-window-excursion (dump-it file-dest contents)))))
+
+(defun fcuny/org-refile-to-task ()
+  "Refile (move) the current Org subtree to `org-default-tasks-file'."
+  (interactive)
+  (fcuny/org-refile-directly org-default-tasks-file))
+
+(defun fcuny/org-refile-to-task ()
+  "Refile (move) the current Org subtree to `org-default-tasks-file'."
+  (interactive)
+  (fcuny/org-refile-directly org-default-tasks-file))
+
+(defun fcuny/org-refile-to-personal-notes ()
+  "Refile (move) the current Org subtree to `org-default-notes-file'."
+  (interactive)
+  (fcuny/org-refile-directly org-default-notes-file))
+
+(defun fcuny/org-refile-subtree-to-file (dir)
+  "Archive the org-mode subtree and create an entry in the
+directory folder specified by DIR. It attempts to move as many of
+the subtree's properties and other features to the new file."
+  (interactive "DDestination: ")
+  (let* ((props      (fcuny/org-subtree-metadata))
+         (head       (plist-get props :header))
+         (body       (plist-get props :body))
+         (tags       (plist-get props :tags))
+         (properties (plist-get props :properties))
+         (area       (plist-get props :region))
+         (filename   (fcuny/org-filename-from-title head))
+         (filepath   (format "%s/%s.org" dir filename)))
+    (apply #'delete-region area)
+    (fcuny/org-create-org-file filepath head body tags properties)))
+
+(defun fcuny/org-set-file-property (key value &optional spot)
+  "Make sure file contains a top-level, file-wide property.
+KEY is something like `TITLE' or `FILETAGS'. This function makes
+sure that the property contains the contents of VALUE, and if the
+file doesn't have the property, it is inserted at either SPOT, or
+if nil,the top of the file."
+  (save-excursion
+    (goto-char (point-min))
+    (let ((case-fold-search t))
+      (if (re-search-forward (format "^#\\+%s:\s*\\(.*\\)" key) nil t)
+          (replace-match value nil nil nil 1)
+
+        (cond
+         ;; if SPOT is a number, go to it:
+         ((numberp spot) (goto-char spot))
+         ;; If SPOT is not given, jump to first blank line:
+         ((null spot) (progn (goto-char (point-min))
+                             (re-search-forward "^\s*$" nil t)))
+         (t (goto-char (point-min))))
+
+        (insert (format "#+%s: %s\n" (upcase key) value))))))
+
+(defun fcuny/org-create-org-file (filepath header body tags properties)
+  "Create a new Org file by FILEPATH. The contents of the file is
+pre-populated with the HEADER, BODY and any associated TAGS."
+  (find-file-other-window filepath)
+  (fcuny/org-set-file-property "TITLE" header t)
+  (when tags
+    (fcuny/org-set-file-property "FILETAGS" (s-join " " tags)))
+
+  ;; Insert any drawer properties as #+PROPERTY entries:
+  (when properties
+    (goto-char (point-min))
+    (or (re-search-forward "^\s*$" nil t) (point-max))
+    (--map (insert (format "#+PROPERTY: %s %s" (first it) (second it))) properties))
+
+  ;; My auto-insert often adds an initial headline for a subtree, and in this
+  ;; case, I don't want that... Yeah, this isn't really globally applicable,
+  ;; but it shouldn't cause a problem for others.
+  (when (re-search-forward "^\\* [0-9]$" nil t)
+    (replace-match ""))
+
+  (delete-blank-lines)
+  (goto-char (point-max))
+  (insert "\n")
+  (insert body))
+
+(defun fcuny/org-subtree-metadata ()
+  "Return a list of key aspects of an org-subtree. Includes the
+following: header text, body contents, list of tags, region list
+of the start and end of the subtree."
+  (save-excursion
+    ;; Jump to the parent header if not already on a header
+    (when (not (org-at-heading-p))
+      (org-previous-visible-heading 1))
+
+    (let* ((context (org-element-context))
+           (attrs   (second context))
+           (props   (org-entry-properties)))
+
+      (list :region     (list (plist-get attrs :begin) (plist-get attrs :end))
+            :header     (plist-get attrs :title)
+            :tags       (fcuny/org-get-subtree-tags props)
+            :properties (fcuny/org-get-subtree-properties attrs)
+            :body       (fcuny/org-get-subtree-content attrs)))))
+
+(defun fcuny/org-filename-from-title (title)
+  "Creates a useful filename based on a header string, TITLE.
+For instance, given the string:    What's all this then?
+     This function will return:    whats-all-this-then"
+  (let* ((no-letters (rx (one-or-more (not alphanumeric))))
+         (init-try (->> title
+                        downcase
+                        (replace-regexp-in-string "'" "")
+                        (replace-regexp-in-string no-letters "-"))))
+    (string-trim init-try "-+" "-+")))
+
+(defun fcuny/org-get-subtree-content (attributes)
+  "Return the contents of the current subtree as a string."
+  (let ((header-components '(clock diary-sexp drawer headline inlinetask
+                             node-property planning property-drawer section)))
+
+      (goto-char (plist-get attributes :contents-begin))
+
+      ;; Walk down past the properties, etc.
+      (while
+          (let* ((cntx (org-element-context))
+                 (elem (first cntx))
+                 (props (second cntx)))
+            (when (member elem header-components)
+              (goto-char (plist-get props :end)))))
+
+      ;; At this point, we are at the beginning of what we consider
+      ;; the contents of the subtree, so we can return part of the buffer:
+      (buffer-substring-no-properties (point) (org-end-of-subtree))))
+
+(defun fcuny/org-get-subtree-properties (attributes)
+  "Return a list of tuples of a subtrees properties where the keys are strings."
+
+  (defun symbol-upcase? (sym)
+    (let ((case-fold-search nil))
+      (string-match-p "^:[A-Z]+$" (symbol-name sym))))
+
+  (defun convert-tuple (tup)
+    (let ((key (first tup))
+          (val (second tup)))
+      (list (substring (symbol-name key) 1) val)))
+
+  (->> attributes
+       (-partition 2)                         ; Convert plist to list of tuples
+       (--filter (symbol-upcase? (first it))) ; Remove lowercase tuples
+       (-map 'convert-tuple)))
+
+(defun fcuny/org-get-subtree-tags (&optional props)
+  "Given the properties, PROPS, from a call to
+`org-entry-properties', return a list of tags."
+  (unless props
+     (setq props (org-entry-properties)))
+  (let ((tag-label "ALLTAGS" ))
+    (-some->> props
+         (assoc tag-label)
+         cdr
+         substring-no-properties
+         (s-split ":")
+         (--filter (not (equalp "" it))))))
+
+(defun fcuny/org-refile-to-projects-dir ()
+  "Move the current subtree to a file in the `projects' directory."
+  (interactive)
+  (fcuny/org-refile-subtree-to-file org-default-projects-dir))
+
+(defun fcuny/org-refile-to-personal-dir ()
+  "Move the current subtree to a file in the `personal' directory."
+  (interactive)
+  (fcuny/org-refile-subtree-to-file org-default-personal-dir))
+
+(defun fcuny/org-refile-to-incubate ()
+  "Refile (move) the current Org subtree to `org-default-incubate-fire'."
+  (interactive)
+  (fcuny/org-refile-directly org-default-incubate-file))
+
+(defun fcuny/org-refile-to-technical-dir ()
+  "Move the current subtree to a file in the `technical' directory."
+  (interactive)
+  (fcuny/org-refile-subtree-to-file org-default-technical-dir))
+
 (provide 'fcuny-defuns)
diff --git a/emacs.d/custom/fcuny-org.el b/emacs.d/custom/fcuny-org.el
index 0bdac7b..9f10720 100644
--- a/emacs.d/custom/fcuny-org.el
+++ b/emacs.d/custom/fcuny-org.el
@@ -1,13 +1,19 @@
 (require 'fcuny-vars)
 
+(use-package dash
+  :ensure t)
+
 (use-package org
+  :after (dash)
   :ensure t
 
-  :hook ((org-mode . visual-line-mode)
-         (org-mode . org-indent-mode))
+  :hook ((org-mode              . visual-line-mode)
+         (org-mode              . org-indent-mode)
+         (org-capture-mode-hook . delete-other-windows))
 
   :bind (("C-c c" . org-capture)
-         ("C-c a" . org-agenda))
+         ("C-c a" . org-agenda)
+         ("C-c o" . hydra-org-menu/body))
 
   :config
   (progn ()
@@ -18,32 +24,49 @@
        (emacs-lisp . t))))
 
   :custom
-  ;; priorities. I use:
-  ;; -1 important + urgent
-  ;; -2 important + non-urgent
-  ;; -3 non-important + urgent
-  ;; -4 non-important + non-urgent
-  (org-highest-priority ?1)
-  (org-default-priority ?4)
-  (org-lowest-priority ?4)
-
+  ;; cosmetic
   (org-pretty-entities t)
-  (org-src-fontify-natively t)
-  ;; prevent the conversion of spaces into tabs (necessary for Python code exports)
-  (org-src-preserve-indentation t)
-  (org-edit-src-content-indentation t)
-
   (org-startup-indented t)
+  (org-tags-column -120)
+
+  ;; files
   (org-directory (expand-file-name "~/Documents/notebooks"))
-  (org-default-inbox-file (concat org-directory "/gtd.org"))
-  (org-default-notes-file (concat org-directory "/gtd.org"))
+  (org-archive-dir (concat org-directory "/archive/"))
+
+  (org-default-projects-dir (concat org-directory "/projects"))
+  (org-default-personal-dir (concat org-directory "/personal"))
+  (org-default-completed-dir (concat org-directory "/completed"))
+  (org-default-technical-dir (concat org-directory "/technical"))
+
+  (org-default-completed-file nil)
+  (org-default-inbox-file (concat org-directory "/inbox.org"))
+  (org-default-tasks-file (concat org-directory "/tasks.org"))
+  (org-default-incubate-file (concat org-directory "/incubate.org"))
+  (org-default-media-file (concat org-directory "/media.org"))
+  (org-default-notes-file (concat org-directory "/notes.org"))
+
+  ;; how to manage tasks
+  (org-todo-keywords '((sequence "TODO(t)" "DOING(g)" "|" "DONE(d)")
+                       (sequence "|" "CANCELED(c)")))
+
+  ;; agenda related
   (org-agenda-start-on-weekday 1)
-  (org-tags-column -120)
+  (org-agenda-files `(,org-default-projects-dir
+                      ,org-default-inbox-file
+                      ,org-default-tasks-file))
+
+  ;; org babel related
+  ;; prevent the conversion of spaces into tabs (necessary for Python code exports)
+  (org-src-fontify-natively t)
+  (org-src-preserve-indentation t)
+  (org-edit-src-content-indentation t)
 
+  ;; behavior
   ;; I want to follow links on RET
   (org-return-follows-link t)
-  (org-blank-before-new-entry (quote ((heading . nil)
-				      (plain-list-item . nil))))
+  (org-enforce-todo-dependencies t)
+  (org-export-with-toc nil)
+  (org-export-with-section-numbers nil)
 
   ;; A few abbreviations I use regularly
   (org-link-abbrev-alist
@@ -52,62 +75,28 @@
      ("ph"   . "https://phabricator.twitter.biz/%s")
      ("go"   . "http://go/%s")))
 
-  ;; The sequence I want to use to navigate tasks
-  (org-todo-keywords
-   '((sequence "TODO(t)" "NEXT(n)" "STARTED(s)" "|" "DONE(d)" "CANCELED(c)")))
-
-  (org-enforce-todo-dependencies t)
-
-  ;; list of files to use for the agenda
-  (org-agenda-files (list (expand-file-name "tasks.org" org-directory)
-                          (expand-file-name "projects.org" org-directory)
-                          (expand-file-name "tw-journal.org" org-directory)))
-
-  (org-export-with-toc nil)
-  (org-export-with-section-numbers nil)
+  ;; entries
+  (org-blank-before-new-entry (quote ((heading . nil)
+				      (plain-list-item . nil))))
 
   ;; see https://github.com/abo-abo/swiper/issues/986
   (org-goto-interface 'outline-path-completion)
-  (org-outline-path-complete-in-steps nil)
+
+  ;; refile and capture
   (org-refile-use-outline-path 'file)
+  (org-outline-path-complete-in-steps nil)
   (org-refile-allow-creating-parent-nodes 'confirm)
-  (org-refile-targets '((org-agenda-files :maxlevel . 4)))
-
-  ;; for the agenda, I want to see tasks in order of priorities.
-  (org-agenda-custom-commands
-   '(("c" "Agenda by priorities"
-      ((tags-todo "PRIORITY=\"1\""
-                  ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
-                   (org-agenda-overriding-header "important and urgent:")))
-       (tags-todo "PRIORITY=\"2\""
-                  ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
-                   (org-agenda-overriding-header "important and non-urgent:")))
-       (tags-todo "PRIORITY=\"3\""
-                  ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
-                   (org-agenda-overriding-header "non-important and urgent:")))
-       (agenda "" ((org-agenda-ndays 1)))
-       (alltodo "" ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done)))))
-      ((org-agenda-compact-blocks t)))))
+  (org-refile-targets (append '((org-default-media-file :level . 1)
+                                (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))
+                               (--map (format "%s/%s" org-default-projects-dir it))
+                               (--map (cons it (cons :level 1))))))
 
   (org-capture-templates
-   `(;; templates for general references, links, etc. They can be relevant for both work and personal learning.
-     ("r" "Reference" entry
-      (file ,(concat org-directory "/kb.org") "Reference")
-      "* %^{TITLE} %^G\n:PROPERTIES:\n:Created: %U\n:END:\n%?")
-     ("e" "Event" entry
-      (file+headline ,(concat org-directory "/notes.org") "Event")
-      "* %^{EVENT}\n:PROPERTIES:\n:Created: %U\n:Location: %^{prompt}\n:END:\n%?")
-     ("b" "Bookmark" entry
-      (file+headline ,(concat org-directory "/notes.org") "Bookmark")
-      "* %(fcuny/get-page-title (current-kill 0)) %^g\n:PROPERTIES:\n:Created: %U\n:ReadLater: %^{read later|Yes|No}\n:Effort: %^{effort|1:00|0:05|0:15|0:30|2:00|4:00|5:00}\n:END:\n%?")
-     ("q" "Quotes" entry
-      (file+headline ,(concat org-directory "/notes.org") "Quote")
-      "* %^{TITLE}\n:PROPERTIES:\n:Created: %U\n:Page: %^{page}\n:END:\n%?")
-     ("d" "Debug" entry
-      (file ,(concat org-directory "/debug.org") "Debug")
-      "* %^{TITLE}\n:PROPERTIES:\n:Created: %U\n:END:\n%?")
-
-     ;; templates for personal things only.
+   `(;; templates for personal things only.
      ("j" "Personal Journal" entry
       (file+olp+datetree ,(concat org-directory "/journal.org"))
       "* %U %?\n"
@@ -133,9 +122,58 @@
 
      ("t" "task entry" entry
       (file ,org-default-inbox-file )
-      "* TODO [#4] %? %^G\n:PROPERTIES:\n:CREATED: %U\n:END:\n\n%i\n\nfrom: %a"))))
+      "* %?\n:PROPERTIES:\n:CREATED: %U\n:END:\n%i\n\nfrom: %a")))
+
+  :config
+  (defhydra hydra-org-menu (:columns 6)
+     "
+  ^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     _p_: personal  _m P_: personal  _S_: schedule   _g x_: inbox    _g P_: personal
+  _c_: archive    _r_: refile    _m T_: technical _D_: deadline   _g n_: notes    _g T_: technical
+  _d_: delete     _i_: incubate                 _R_: rename                   _g C_: completed
+  "
+    ("<up>" org-previous-visible-heading)
+    ("<down>" org-next-visible-heading)
+    ("k" org-previous-visible-heading)
+    ("j" org-next-visible-heading)
+    ("c" fcuny/org-archive-subtree-as-completed)
+    ("d" org-cut-subtree)
+    ("t" fcuny/org-refile-to-task)
+    ("i" fcuny/org-refile-to-incubate)
+    ("p" fcuny/org-refile-to-personal-notes)
+    ("r" org-refile)
+    ("m X" fcuny/org-refile-to-projects-dir)
+    ("m P" fcuny/org-refile-to-personal-dir)
+    ("m T" fcuny/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")
+    ("q" nil "quit")))
 
 (use-package htmlize
   :ensure t)
 
+(use-package org-journal
+  :ensure t
+  :after (org)
+  :custom
+  (org-journal-file-format "%Y-%m.org")
+  (org-journal-dir "~/Documents/notebooks/journal/")
+  (org-journal-date-format "#+TITLE: Monthly Journal Entry - %e %b %Y (%A)")
+  (org-journal-file-type `monthly))
+
 (provide 'fcuny-org)