Capturing Notes with Org
Table of Contents
A literate programming file for configuring org for capturing notes.
Introduction
Capturing (or collecting) notes from files, browsers, and meetings, is a great way to get organized.
I even have external commands that kick-off the capturing process, and without a command this is what gets called:
(setq org-capture-default-template "cc")
Let’s now define my templates.
Templates
To make sure we can execute this code anytime, let’s define the variable that will hold all the templates:
(defvar org-capture-templates (list))
Some templates put the information in front of other information (as opposed to the default of appending), so I define a helper function:
(defun ha-first-header () (goto-char (point-min)) (search-forward-regexp "^\* ") (beginning-of-line 1) (point))
General Notes
Capturing text into the org-default-notes-file
is something I don’t do much:
(add-to-list 'org-capture-templates '("n" "Thought or Note" entry (file org-default-notes-file "General Notes") "* %?\n\n %i\n\n See: %a" :empty-lines 1)) (add-to-list 'org-capture-templates '("t" "Task" entry (file+olp org-default-notes-file "Tasks") "** %?\n\n %i\n\n See: %a" :empty-lines 1)) (add-to-list 'org-capture-templates '("w" "Website Announcement" entry (file+function "~/website/index.org" ha-first-header) (file "~/templates/website-announcement.org") :empty-lines 1))
Before we go too far, we should create a publishing file for the website announcement, and something for the journal.
Clock in Tasks
Org has one task at a time that can be clocked in keeping a timer. I use that as a destination for collecting notes. For instance, capturing with a c
allows me to enter details under that task without switching to it:
(add-to-list 'org-capture-templates '("c" "Currently clocked in task"))
The default is just to type information to the current clocked-in task using c c
:
(add-to-list 'org-capture-templates `("ci" "Item to Current Clocked Task" item (clock) "%?" :empty-lines 1))
We can select a region and copy that using c r
:
(add-to-list 'org-capture-templates `("cc" "Contents to Current Clocked Task" plain (clock) "%i" :immediate-finish t :empty-lines 1))
If we have copied anything into the clipboard, that information can be add to the current task using c k
:
(add-to-list 'org-capture-templates `("ck" "Kill-ring to Current Clocked Task" plain (clock) "%c" :immediate-finish t :empty-lines 1))
Instead, if I am looking at some code, I can copy some code from a region, but use a helper function to create a link to the original source code using c f
:
(add-to-list 'org-capture-templates `("cf" "Code Reference with Comments to Current Task" plain (clock) "%(ha-org-capture-code-snippet \"%F\")\n\n %?" :empty-lines 1))
If I want a reference to the code, without any comments, I call c l
:
(add-to-list 'org-capture-templates `("cl" "Link to Code Reference to Current Task" plain (clock) "%(ha-org-capture-code-snippet \"%F\")" :empty-lines 1 :immediate-finish t))
Capture Helper Functions
To have a capture back-ref to a function and its code, we need to use this:
(require 'which-func)
This helper function given a code type and the function, analyzes the current buffer in order to collects data about the source code file. It then creates a nice-looking template:
(defun ha-org-capture-fileref-snippet (f type headers func-name) (let* ((code-snippet (buffer-substring-no-properties (mark) (- (point) 1))) (file-name (buffer-file-name)) (file-base (file-name-nondirectory file-name)) (line-number (line-number-at-pos (region-beginning))) (initial-txt (if (null func-name) (format "From [[file:%s::%s][%s]]:" file-name line-number file-base) (format "From ~%s~ (in [[file:%s::%s][%s]]):" func-name file-name line-number file-base)))) (format " %s #+begin_%s %s %s #+end_%s" initial-txt type headers code-snippet type)))
For typical code references, we can get the label for Org’s SRC
block by taking the major-mode
and removing the -mode
part. We can then call the formatter defined above:
(defun ha-org-capture-code-snippet (f) "Given a file, F, this captures the currently selected text within an Org SRC block with a language based on the current mode and a backlink to the function and the file." (with-current-buffer (find-buffer-visiting f) (let ((org-src-mode (replace-regexp-in-string "-mode" "" (format "%s" major-mode))) (func-name (which-function))) (ha-org-capture-fileref-snippet f "src" org-src-mode func-name))))
Let’s assume that we want to copy some text from a file, but it isn’t source code, then this function makes an EXAMPLE
of it.
(defun ha-org-capture-clip-snippet (f) "Given a file, F, this captures the currently selected text within an Org EXAMPLE block and a backlink to the file." (with-current-buffer (find-buffer-visiting f) (ha-org-capture-fileref-snippet f "example" "" nil)))
Code Capturing Functions
To call a capture for code, let’s make two interactive functions, one copies the information, and the other pulls up a capturing window for comments:
(defun ha-code-to-clock (&optional start end) "Send the selected code to the current clocked-in org-mode task." (interactive) (org-capture nil "cl")) (defun ha-code-comment-to-clock (&optional start end) "Send the selected code (with comments) to the current clocked-in org-mode task." (interactive) (org-capture nil "cf"))
And a less-disruptive keybinding:
(ha-leader "C" '("capture code" . ha-code-to-clock)) (ha-leader "o C" '("capture code" . ha-code-comment-to-clock))
External Capturing
Using emacsclient
, the operating system or other applications can trigger a call to capture content into Emacs. I started with the functions from this essay, which made a nice approach to opening and closing a frame:
(defun start-capture-frame (&optional template-key) "Create a new frame and run `org-capture'." (interactive) (make-frame '((name . "capture") (top . 300) (left . 700) (width . 80) (height . 25))) (select-frame-by-name "capture") (delete-other-windows) (cl-letf (((symbol-function 'switch-to-buffer-other-window) 'switch-to-buffer)) (org-capture nil template-key)))
When I call org-capture in its own frame, I don’t want any other windows around, so we rebind org-capture
’s call to switch the buffer to another window, to switch to the capture buffer.
Wouldn’t it be grand if when we finished capturing, the frame automatically closed:
(defun org-capture-delete-frame () "Hook for `org-capture-after-finalize-hook' to delete the frame." (message "Finished with the org-capture-after-finalize-hook") (when (equal "capture" (frame-parameter nil 'name)) (delete-frame))) (add-hook 'org-capture-after-finalize-hook 'org-capture-delete-frame)
This external shell script calls the function to kick everything off from applications that aren’t Emacs:
/usr/local/bin/emacsclient -s work -n -e "(start-capture-frame)"
And for even quicker work, we can have special scripts tied to special keybindings:
/usr/local/bin/emacsclient -s work -n -e "(start-capture-frame \"sm\")"
Pull MacOS-Specific Content
The org-mac-link project makes it easy to tell Emacs to retrieve information from other apps, e.g. the URL of the opened tab in Firefox.
(use-package org-mac-link :straight (:host gitlab :repo "aimebertrand/org-mac-link") :config (ha-leader "i" '("insert app info" . org-mac-link-get-link)))
We then call org-mac-link-get-link to select the app, which then get the information from the app, and inserts it at point. While this is nice, it seems to be the wrong order. As we see something we like, say in Firefox, then we go into Emacs and hit SPC i
. What about an approach where we stay in Firefox. In other words, send the information, perhaps using org-capture.
Push MacOS-Specific Content
I’m use ICanHazShortcut to have a keybinding trigger a script (every simple). For instance:
/usr/bin/osascript ~/bin/emacs-capture-clock.scr
But the following Applescript does the work:
tell application "System Events" to set theApp to name of first application process whose frontmost is true -- Macintosh HD:Applications:iTerm.app: if "iTerm" is in theApp then set function to "ha-external-capture-code-to-org" else set function to "ha-external-capture-to-org" end if tell application "System Events" to keystroke "c" using command down set command to "/usr/local/bin/emacsclient -s work -e '(" & function & ")'" do shell script command -- Tell me it worked and what it did, since this runs in the background say "Capture complete"
Now we have some goodies on the clipboard, and the script uses emacsclient
to call these functions to put those contents into clocked in task.
(defun ha-external-capture-to-org () "Calls `org-capture-string' on the contents of the Apple clipboard." (interactive) (org-capture-string "" "ck") (ignore-errors (delete-frame)))
Oh, and it this is from the Terminal program, let’s wrap it in a block:
(defun ha-external-capture-code-to-org () "Calls `org-capture-string' on the contents of the Apple clipboard." (interactive) (seq-let (type data) (ha-get-clipboard) (let* ((code (thread-last data (s-replace "\r" "\n") (s-trim))) (contents (format "#+begin_example\n%s\n#+end_example" code))) (message contents) (org-capture-string contents "cc"))) (ignore-errors (delete-frame)))
[main] config version = 2 shell = /bin/bash -l populate_menu_with_actions = yes show_hotkeys_in_menu = yes check_for_updates = yes start_on_login = yes show_icon_in_statusbar = yes set_workdir_with_cd = no window_x = -988 window_y = 172 window_width = 600 window_height = 361 shortcut_column_enabled = yes action_column_enabled = yes command_column_enabled = yes workdir_column_enabled = no shortcut_column_width = 80 action_column_width = 160 command_column_width = 173 workdir_column_width = 100 [shortcut1] shortcut = ⇧⌃⌥⌘E action = Personal Emacs command = open -a Emacs workdir = enabled = yes [shortcut2] shortcut = ⇧⌃⌥E action = Work Emacs command = FOR_WORK=yes open -a Emacs-Work workdir = enabled = yes [shortcut3] shortcut = ⇧⌃⌥⌘X action = Emacs Capture command = ~/bin/emacs-capture workdir = enabled = yes [shortcut4] shortcut = ⇧⌃⌥X action = Emacs Capture Clipboard command = ~/bin/emacs-capture-clock workdir = enabled = yes [shortcut5] shortcut = ⇧⌃⌥T action = iTerm command = open -a iTerm workdir = enabled = yes [shortcut6] shortcut = ⇧⌃⌥S action = Slack command = open -a Slack workdir = enabled = yes [shortcut7] shortcut = ⇧⌃⌥W action = Spotify command = open -a Spotify workdir = enabled = yes [shortcut8] shortcut = ⇧⌃⌥F action = Firefox command = open -a Firefox workdir = enabled = yes [shortcut9] shortcut = ⇧⌃⌥C action = Chome command = ~/bin/chrome.scr workdir = enabled = yes [shortcut10] shortcut = ⇧⌃⌥Q action = Keepass command = open -a KeepassXC workdir = enabled = yes [shortcut11] shortcut = ⇧⌃⌥Z action = Zoom command = open -a zoom.us workdir = enabled = yes [shortcut12] shortcut = ⌃F1 action = Mute Zoom command = ~/bin/zoom-muter workdir = enabled = yes [shortcut13] shortcut = ⇧⌃⌥⌘M action = Capture Meeting command = ~/bin/emacs-capture-meeting workdir = enabled = yes [shortcut14] shortcut = ⇧⌃⌥⌘B action = Outlook command = open -a "Microsoft Outlook" workdir = enabled = yes [shortcut15] shortcut = ⇧⌃⌥⌘D action = Discord command = open -a Discord workdir = enabled = yes
Configure the ICanHazShortcut shortcuts to call these scripts, as in this screenshot: And here is the configuration file for that:
[main] config version = 2 shell = /bin/bash -l populate_menu_with_actions = yes show_hotkeys_in_menu = yes check_for_updates = yes start_on_login = yes show_icon_in_statusbar = yes set_workdir_with_cd = no window_x = -988 window_y = 172 window_width = 600 window_height = 361 shortcut_column_enabled = yes action_column_enabled = yes command_column_enabled = yes workdir_column_enabled = no shortcut_column_width = 80 action_column_width = 160 command_column_width = 173 workdir_column_width = 100 [shortcut1] shortcut = ⇧⌃⌥⌘E action = Personal Emacs command = open -a Emacs workdir = enabled = yes [shortcut2] shortcut = ⇧⌃⌥E action = Work Emacs command = open -a /usr/local/Cellar/emacs-plus@28/28.1/Emacs.app workdir = enabled = yes [shortcut3] shortcut = ⇧⌃⌥⌘X action = Emacs Capture command = ~/bin/emacs-capture workdir = enabled = yes [shortcut4] shortcut = ⇧⌃⌥X action = Emacs Capture Clipboard command = ~/bin/emacs-capture-clock workdir = enabled = yes [shortcut5] shortcut = ⇧⌃⌥T action = iTerm command = open -a iTerm workdir = enabled = yes [shortcut6] shortcut = ⇧⌃⌥S action = Slack command = open -a Slack workdir = enabled = yes [shortcut7] shortcut = ⇧⌃⌥W action = Spotify command = open -a Spotify workdir = enabled = yes [shortcut8] shortcut = ⇧⌃⌥F action = Firefox command = open -a Firefox workdir = enabled = yes [shortcut9] shortcut = ⇧⌃⌥C action = Chome command = ~/bin/chrome.scr workdir = enabled = yes [shortcut10] shortcut = ⇧⌃⌥Q action = Keepass command = open -a KeepassXC workdir = enabled = yes [shortcut11] shortcut = ⇧⌃⌥Z action = Zoom command = open -a zoom.us workdir = enabled = yes [shortcut12] shortcut = ⌃F1 action = Mute Zoom command = ~/bin/zoom-muter workdir = enabled = yes
Push Terminal Results
I use this en
script to copy command line output into the Emacs-based engineering notebook to the current clocked-in task. I have two use cases.
First, at the end of a pipe sequence. For instance, this example is what I would type and see in the Terminal:
$ openstack server list --format json | jq '.[1].Networks' | en -f js { "cedev13": [ "10.158.12.169" ] }
But the output, along with being displayed, is also copied into my org file as:
#+begin_src js { "cedev13": [ "2.158.12.169" ] } #+end_src
Second, if I want more information about the command, I can begin the command with en
, as in:
$ en -f js -n "The output from server list" openstack server list --format json
Which puts the following in my org file:
The output from server list #+begin_src sh openstack server list --format json #+end_src #+results: #+begin_src js [ { "ID": "36bf4825-fc5b-4414-8758-4f8523136215", "Name": "kolladev.cedev13.d501.eng.pdx.wd", "Status": "ACTIVE", "Networks": { "cedev13": [ "2.158.12.143" ] }, "Image": "fde6ba50-7b14-4821-96fe-f5b549adc6d3", "Flavor": "163" }, { …
Here is the script I tangle to ~/bin/en
:
# Interface to my Engineering Notebook. # # I use this script as the last pipe entry on the command line, to # display the output, and also copy the output into the Emacs-based # engineering notebook to the current clocked-in task. # # Use the script as a 'runner' of a command as this script passes # any extra command line options directly to the shell. function usage { echo "$(basename $0) [ -t header-title ] [ -n notes ] [ -f format ] [ command [ arguments ] ]" exit 1 } while getopts "t:n:f:" o do case "$o" in t) TITLE="$OPTARG";; n) NOTE="$OPTARG";; f) FORMAT="$OPTARG";; [?]) usage;; esac done shift $(expr $OPTIND - 1) COMMAND=$* FILE=$(mktemp) function process_output { cat -v $1 | sed 's/\^\[\[[0-9][0-9]*\(;[0-9][0-9]*\)*m//g' } # The script can either take a command specified as arguments (in # which case, it will run that), or it will assume all data is coming # from standard in... if [ -z "$COMMAND" ] then # All data should be coming from standard in, so capture it: tee $FILE else # Otherwise, we need to run the command: ${COMMAND} | tee $FILE fi # Either way, let's process the results stored in the file: RESULTS=$(process_output $FILE) function output { if [ -n "$TITLE" ] then echo "*** ${TITLE}" fi if [ -n "$NOTE" ] then echo "${NOTE}" fi if [ -n "$COMMAND" ] then echo "#+begin_src sh" echo "${COMMAND}" echo "#+end_src" echo echo "#+results:" fi if [ -n "$FORMAT" ] then echo "#+begin_src ${FORMAT}" echo "${RESULTS}" echo "#+end_src" else echo "#+begin_example" echo "${RESULTS}" echo "#+end_example" fi } if which pbcopy 2>&1 >/dev/null then output | pbcopy else output | xclip fi # Now that the results are on the clipboard, the `c k` capture # sequence calls my "grab from the clipboard" capture template: emacsclient -s work -e '(org-capture-string "" "ck")' >/dev/null rm -f $FILE