Org capture from everywhere in macOS

Intro

Capture lets you quickly store notes with little interruption of your work flow. Org’s method for capturing new items is heavily inspired by John Wiegley’s excellent Remember package.

The Org Manual – 10.1 Capture

Need

I use Emacs for recording almost anything notes, stream of conscience, interesting tidbits from the internet, documentations, To-Dos & more. Which in turn means that I sometimes want to quickly jot down any of the above and then go on with my day.

As we all know frequent context switching is not conducive to productivity. In that spirit I wanted to get a system with which I can quickly bring up an Emacs capture frame – regardless of the focused application – and capture whatever and then let the frame “disappear”.

How I go about it?

First things first

I will not cover how to setup capturing in general, since this is far better and extensively discussed and documented online. But if of interest, you can take a peak at my capture templates.

What I want to focus on in this post is the integration into the OS as a whole.

Default Org capture

Bringing up a capture window/frame in Emacs is as simple as calling the command org-capture or of course using the corresponding keybinding.

The hard(isch) part is accomplishing this outside of Emacs – meaning from everywhere in macOS.

Not to worry, Emacs comes with a client binary, emacsclient, to connect to a running instance of Emacs. This however requires Emacs to be running in server mode, which can simply be achieved with the following lines.

(unless (server-running-p)
  (server-start))

All of the above can be used to run almost all functions in Emacs from the command line. I have the following function and advices after org-capture-finalize and org-capture-destroy in my configuration to popup a capture frame and close it afterwards…

(defun timu-func-make-capture-frame ()
  "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)
  (noflet ((switch-to-buffer-other-window (buf) (switch-to-buffer buf)))
          (org-capture)))

(defadvice org-capture-finalize
    (after delete-capture-frame activate)
  "Advise capture-finalize to close the frame."
  (if (equal "capture" (frame-parameter nil 'name))
      (delete-frame)))

(defadvice org-capture-destroy
    (after delete-capture-frame activate)
  "Advise capture-destroy to close the frame."
  (if (equal "capture" (frame-parameter nil 'name))
      (delete-frame)))

… which I then call from the terminal like so:

/usr/local/bin/emacsclient -ne "(timu-func-make-capture-frame)"

The next step is then to be able to call this not just from the command line but from everywhere on the system.

In comes Automator.app. Using the “Run Shell Script” Action I can create a Quick Action accessible from the everywhere on the system with the shell command above.

In macOS Services a.k.a. Quick Actions can be bound to keyboard shortcuts:

System Preferences -> Keyboard -> Shortcuts -> Services

This article goes into more details on how to create a macOS Service/Quick Action with Automator.app.

Capture an URL from Safari

Simply put this is a much more elegant way of collecting what are essentially bookmarks for later review. I could of course store found treasures on the internet in the bookmarks in Safari (yes, I do not use Chrome), but bookmarks are notoriously awful to parse in my mind.

I have a file, ~/org/files/notes.org, where I store URLs to content that I mostly find on the internet with a datetree.

This uses the following

a) package:

org-mac-link (Shameless plug: I have taken over the maintenance of the package and moved it out of org-contrib. The Package will be removed from the collection with the next release.)

b) Function:

(defun timu-func-url-safari-capture-to-org ()
  "Call `org-capture-string' on the current front most Safari window.
Use `org-mac-link-safari-get-frontmost-url' to capture url from Safari.
Triggered by a custom macOS Quick Action with a keyboard shortcut."
  (interactive)
  (org-capture-string (org-mac-link-safari-get-frontmost-url) "u")
  (ignore-errors)
  (org-capture-finalize))

c) capture template:

(add-to-list 'org-capture-templates
             '("u" "URL capture from Safari" entry
               (file+olp+datetree "~/org/files/notes.org")
               "* %i    :safari:url:\n%U\n\n"))

d) Automator.app Quick Action:

Now to call this outside of Emacs, I use AppleScript:

on run {input, parameters}
    do shell script "/usr/local/bin/emacsclient -n -e '(timu-func-url-safari-capture-to-org)'"
    do shell script "/usr/local/bin/emacsclient -n -e '(find-file \"~/org/files/notes.org\")'"
    return input
end run

Which in turn can be of course made into a “Run AppleScript” Quick Action:

Capture an URL and content from Safari

There some content on the web that I want to store for posterity however. In these cases I don’t want to just capture the URL, but the content as well.

  1. Select the content to be stored
  2. Hit a keyboard shortcut
  3. Content is stored in an org file including proper formatting

This takes a little more effort to work straight from Safari, which I collected from a bunch of people online and adjusted to my workflow.

a) package:

org-mac-link

b) CLI program:

For this I also need Pandoc on my system, which I get using Homebrew:

brew install pandoc

c) Functions:

Bare in mind, that these do need the cl-lib library. So do not forget to require it.

;;;; capture and/or org-yank from macos clipboard
;; credit: http://www.howardism.org/Technical/Emacs/capturing-content.html
;; credit: https://gitlab.com/howardabrams/spacemacs.d/-/tree/master/layers
(defun timu-func-cmd-with-exit-code (program &rest args)
  "Run PROGRAM with ARGS and return the exit code and output in a list."
  (with-temp-buffer
    (list (apply 'call-process program nil (current-buffer) nil args)
          (buffer-string))))

(defun timu-func-convert-applescript-to-html (contents)
  "Return the Applescript's clipboard CONTENTS in a packed array.
Convert and return this encoding into a UTF-8 string."
  (cl-flet ((hex-pack-bytes (tuple)
                            (string-to-number (apply 'string tuple) 16)))
    (let* ((data (-> contents (substring 10 -2) (string-to-list)))
           (byte-seq (->> data (-partition 2) (mapcar #'hex-pack-bytes))))
      (decode-coding-string
       (mapconcat #'byte-to-string byte-seq "") 'utf-8))))

(defun timu-func-get-mac-clipboard ()
  "Return a list where the first entry is the either :html or :text.
The second is the clipboard contents."
  (cl-destructuring-bind (exit-code contents)
      (timu-func-cmd-with-exit-code
       "/usr/bin/osascript" "-e" "the clipboard as \"HTML\"")
    (if (= 0 exit-code)
        (list :html (timu-func-convert-applescript-to-html contents))
      (list :text (shell-command-to-string
                   "/usr/bin/osascript -e 'the clipboard'")))))

(defun timu-func-org-clipboard ()
  "Return the contents of the clipboard in `org-mode' format."
  (cl-destructuring-bind (type contents) (timu-func-get-mac-clipboard)
    (with-temp-buffer
      (insert contents)
      (if (eq :html type)
          (shell-command-on-region
           (point-min) (point-max)
           (concat (executable-find "pandoc") " -f html -t org --wrap=none") t t)
        (shell-command-on-region
         (point-min) (point-max)
         (concat (executable-find "pandoc") " -f markdown -t org --wrap=none") t t))
      (buffer-substring-no-properties (point-min) (point-max)))))

(defun timu-func-org-yank-clipboard ()
  "Yank the contents of the Mac clipboard in an `org-mode' compatible format."
  (interactive)
  (insert (timu-func-org-clipboard)))

(defun timu-func-safari-capture-to-org ()
  "Call `org-capture-string' on the contents of the Apple clipboard.
Use `org-mac-link-safari-get-frontmost-url' to capture content from Safari.
Triggered by a custom macOS Quick Action with keybinding."
  (interactive)
  (org-capture-string (timu-func-org-clipboard) "s")
  (ignore-errors)
  (insert (org-mac-link-safari-get-frontmost-url))
  (org-capture-finalize))

d) capture template:

(add-to-list 'org-capture-templates
             '("s" "macOS Safari clipboard capture" entry
               (file+olp+datetree "~/org/files/notes.org")
               "* %?    :safari:note:\n%U\n\n%i\n"))

e) Automator.app Quick Action:

Following the already mentioned recipe, the next step is a Quick Action to be called with a keyboard shortcut:

on run {input, parameters}
    tell application "System Events" to keystroke "c" using command down
    do shell script "/usr/local/bin/emacsclient -n -e '(timu-func-safari-capture-to-org)'"
    do shell script "/usr/local/bin/emacsclient -n -e '(find-file \"~/org/files/notes.org\")'"
    if application "Emacs" is running then
        tell application "Emacs"
            activate
        end tell
    end if
    return input
end run

Last words

As mentioned before, I “stole” quite a bit of code and made it mine to fit my workflow. I am reasonably happy with the state of things right now.

This prompted me to write this down in the hope, that it might help a few people needing a similar workflow like mine.

I have few other ways I capture stuff, i.e. from the Finder.app, Outlook, etc. Not gonna go into those however since there is not nead to repeat myself a lot. Plus I think that with the Safari.app examples, the concept is quite clear.

Functions and capture templates in Emacs, Automator.app actions to call the functions and then keyboard shortcuts to call those actions.

Check out my dotemacs repo for more or just send me a quick email if you are curious about more capture stuff.

Side note one:
As of a few versions ago macOS introduced the Shortcuts.app on the mac, which can handle automation jobs as well. However I haven’t gotten into those yet. Who knows, might be topic for future post.
Side note two:
One can use a wonderful app called Hammerspoon for a lot of automation, which I did use in the beginning instead of Automator. However for some reason it failed me at some point and I gave up due to my lack of skills with Lua.

Meta

  • Machine: MacBook Pro 13" M1
  • OS: macOS Monterey
  • Emacs Version: GNU Emacs 28.1 (Emacs Mac Port version)
  • Emacs installation using Homebrew:
brew tap railwaycat/emacsmacport
brew install emacs-mac --with-native-comp --with-xwidgets --with-natural-title-bar