ergopractice / org-catch

An alternative to `org-capture'
33 stars 2 forks source link
ergonomics org-capture org-mode productivity

+title: ~org-catch~ - An Alternative to ~org-capture~

+html:

~org-catch~ is an Emacs package for creating any kinds of records (e.g., journal entries, tasks, calendar events, links between notes, etc.), specifically in ~org-mode~ but not only. It can be seen as a concise and more flexible alternative to ~org-capture~.

~org-catch~ template is a plist that specifies how to get the data for the record. The [[#keywords][keywords]] mostly correspond special org properties and are defined and documented in ~org-catch-keywords~ variable that can be modified and extended. The values are evaluated one by one so the template specifies the [[#input-order][input order]] precisely.

For example, in comparison to ~org-capture~, ~org-catch~ can ask user to choose a filing target before asking for a title of new org entry, whereas ~org-capture~ only allows to refile an entry to a specific target only after the entry is fully defined and it requires an extra command to do that.

~org-catch~ also provides a handful of [[#helpers][helper functions]] that can be used as shortcuts within a template. This makes ~org-catch~ template neat and concise. See [[#examples][examples]].

The package is not yet on melpa so you can get it with straight.el, for example, by adding the following to your ~init.el~:

+begin_src emacs-lisp

(use-package org-catch :straight (:host github :repo "ergopractice/org-catch"))

+end_src

Otherwise, download it manually (change the YOUR-ELISP-DIR to the directory path you would like to store ~org-catch~ on your computer):

+begin_src shell

cd YOUR-ELISP-DIR git clone https://github.com/ergopractice/org-catch.git

+end_src

Then add the following to your ~init.el~:

+begin_src emacs-lisp

(load-path YOUR-ELISP-DIR/org-catch) (require 'org-catch)

+end_src

If you're on a newer version of Emacs, check if you got ~package-vc~ installed (~M-x describe-function RET use-package RET~, and look for the ~:vc~ keyword). If it is installed, you can install this package like this:

+begin_src emacs-lisp

(use-package org-catch :vc (:url "https://github.com/ergopractice/org-catch"))

+end_src

As of now the package is under ongoing development, so, please, pull it regularly (e.g., whith ~straight-pull-package~ or ~package-vc-upgrade~). Your [[https://github.com/ergopractice/org-catch/issues][comments and suggestions]] are very welcome and helpful.

The key feature of ~org-catch~ is that the order in which user inputs the information required for new record follows the order of the template. For example:

+begin_src emacs-lisp

;; insert a new TODO entry for some general errands at point

;; first ask for tags and then for title (org-catch '(:tags (completing-read-multiple "Tags: " '("@home" "@office" "@city")) :item (read-string "Title: ") :todo "TODO"))

;; first ask for title and then for tags (org-catch '(:item (read-string "Title: ") :tags (completing-read-multiple "Tags: " '("@home" "@office" "@city")) :todo "TODO"))

+end_src

~org-catch~ also provides handful of helper functions for user interaction and content creation. By default helpers functions names are prefixed with ~org-catch--helper-*~ and they can be used with templates as shortcuts, i.e. without the prefix. Helpers shortcuts can be used even without function call wrapping, i.e., without enclosing brackets.

The example below demonstrates shortcuts usage for the following helpers: ~org-catch--helper-read-ol~ (reads outline path), ~org-catch--helper-read-multi~ (reads multiple strings), ~org-catch--helper-list-item~ (if point at org list gets the list item as string), ~org-catch--helper-read~ (reads the string from user). The three ~org-catch~ templates are equivalent:

+begin_src emacs-lisp

;; helpers are not enabled by default (require 'org-catch-helpers)

;; The calls do the following: ;; - read a filing target marker by completing org outline for org-agenda-files ;; - read tags for new entry ;; - if point is on org list item use it as a heading otherwise read a string

;; Helper functions as shortcut variables (the most concise varian) (org-catch '(:target read-ol :tags (read-multi nil ":" "@home" "@office" "@city") :item (or list-item read) :todo "TODO"))

;; Helper functions just as function shortcuts (org-catch '(:target (read-ol) :tags (read-multi nil ":" "@home" "@office" "@city") :item (or (list-item) (read)) :todo "TODO"))

;; Direct usage of helper functions (same as for any other elisp) (org-catch '(:target (org-catch--helper-read-ol) :tags (org-catch--helper-read-multi nil ":" "@home" "@office" "@city") :item (or (org-catch--helper-list-item) (org-catch--helper-read)) :todo "TODO"))

+end_src

All ~org-catch~ keywords used in templates definitions are specified and documented in ~org-catch-keywords~ variable. User can change any keyword to own liking and extend ~org-catch~ with more keywords adding some extra functionalities.

It is possible to specify keywords with glob patterns. Like so: ~:something-~ that will match both ~:something-this~ and ~:something-that~. By default, ~org-catch-keywords~ defines ~:~ to match any other keywords that was not specified in ~org-catch-keywords~ and use it as org properties.

Each keyword is associated with one or more ~org-catch~ methods (~eval-init~, ~target~, ~target-datetree~, ~eval-before~, ~target-item~, ~target-body~, ~set-properties~, ~eval-after~, ~eval-final~). The methods are executed in the following order and it reflexes ~org-catch~ workflow:

  1. Evaluate at initial context
    • ~eval-init~
  2. Get the filing target's file and marker and move the point there
    • ~target~
    • ~target-datetree~
  3. Evaluate at target before inserting new org entry
    • ~eval-before~ (binds results from ~eval-init~)
  4. Insert new org entry, its body and set org properties
    • ~target-item~
    • ~target-body~
    • ~set-properties~
  5. Evaluate at target after creating a new org entry
    • ~eval-after~ (binds results from ~eval-init~ and ~eval-before~)
  6. Evaluate after returning point back to the initial context
    • ~eval-final~ (binds results from ~eval-init~, ~eval-before~ and ~eval-after~)

Below are some examples of selfdocumented user commands. The examples are included in the package. To try them out add ~(require 'org-catch-examples)~ to your ~init.el~. (Note that you might also want to set ~org-catch-default-journal~ variable beforehand.)

+begin_src emacs-lisp

;; first define some common properties for new entry (defvar org-catch-created-properties-tempate '(:created (org-current-time-as-inactive-timestamp-string) :created-on-system (system-name) :created-by-user (user-login-name) :created-while-at (org-with-wide-buffer ;; if filing the subtree then store link to context (and (org-at-heading-p 'invisible-not-ok) (stringp _item) (equal (org-get-heading t t t t) _item) (org-up-heading-or-point-min)) (when-let ((link (org-store-link nil))) (substring-no-properties link)))) "Default properties that logs context for newly created org entries.")

;; catch things to journal (defvar org-catch-default-journal "~/org/journal.org")

(defun org-catch-journal () "Creates a new item in the org-catch-default-journal' under datetree (seeorg-datetree.el').

With interactive ARG first ask for a date for datetree where the new journal entry should be filed. Otherwise file the entry for current date.

Then it asks user for org tags unless it is called when the point is at heading in which case the current heading's tags will be used.

Then

At the end it will delete the text that was refiled and insert the back reference link.

The new journal entry will also have properties to log some context. See org-catch-created-properties-tempate'." (interactive) (org-catch (:target org-catch-default-journal :datetree ((1 . (or region-time t)) (4 . read-time)) :tags (or at-header-tags (read-multi nil ":" "note" "idea" "meeting")) :item (or (and (not region) at-header) read) :body (or region at-header-body paragraph read) :final '(or delete-region delete-at-header-subtree delete-paragraph) :insert-ref t ,@org-catch-created-properties-tempate)))

;; catch todos (defun org-catch-todo () "Catch a new TODO entry with. When called with interactive ARG prefix consider `org-agenda-files' for filing targets. Otherwise seek targets in current buffer.

First asks user for filing target. Consider as targets only entries that does not have a todo keyword or has 'PROJ' as todo keyword to avoid nested TODOs.

Then asks for tags.

Then asks for title for the new TODO entry unless:

At the end delete used text and insert back reference at point." (interactive) (org-catch `(:target ((1 . (read-ol :targets 'buffer :filter (todo-p nil "PROJ"))) (4 . (read-ol :filter (todo-p nil "PROJ")))) :tags (or header-at-tags (read-multi nil ":" "@home" "@office" "@city")) :item (or region header-at list-item read) :body (or header-at-body list-body) :todo "TODO" :final '(or delete-region delete-at-header-subtree (and list-body delete-list) delete-list-item) :insert-ref t ,@org-catch-created-properties-tempate)))

;; org util (require 'cl-macs) ; provides cl-letf (defun org-todo-done (&optional arg) "Set TODO entry as done. With ARG ask when it was done and record it accordingly." (interactive "P") (let ((todo-fun (if (derived-mode-p 'org-agenda-mode) 'org-agenda-todo 'org-todo))) (if arg (cl-letf ((time (org-read-date 'with-time 'to-time nil "When this was done? ")) ((symbol-function 'org-current-effective-time) #'(lambda () time)) ((symbol-function 'org-today) #'(lambda () (time-to-days time)))) (print (org-current-effective-time)) (print (org-today)) (funcall todo-fun 'done)) (funcall todo-fun 'done))))

;; set todo as done (defun org-catch-done () "Ask for a target which is any todo entries in current `org-agenda-files' and set this entry as done. With interactive prefix ARG also ask when it was done.

At the end insert the back reference wrapped as +[[org-id][item]]+, i.e., wrapped in strike-through org markup." (interactive) (org-catch `(:target (read-ol :filter todo-p) :before ((1 . '(org-todo-done)) (4 . '(org-todo-done arg))) :insert-ref '(:wrap "+"))))

+end_src