Adding org-protocol support
Introduction
They say that the best way to make yourself do something, is to leave no other option; I agree. That’s why I unapologetically dedicate this article to Mozilla for discontinuing Pocket2. It is not that I relied on it heavily, but it was a staple: a nice extension to have, especially when finding interesting articles I could not exactly read at the moment.
The solution proposed in this article is not a full equivalent. Unless you sync your org-mode files with your mobile device, this one is closer to using your browser’s built-in reading list (sigh: no I did not have a bookmarks folder full of articles to read). But it offers more than just a subset of what pocket boasted.
It allows us to utilize the powerful org-capture-templates to handle articles:
that way, it offers the functionality to not just save articles (so that we
can never eventually read them) but to also instantly keep notes on them!
To implement it we will need to:
- Properly set up
org-protocolinEmacs- A shooort detour to map
org-protocollinks toEmacsviaxdg-openin Linux
- A shooort detour to map
- Create a simple bookmark in our browser
This article will not cover a full installation of Emacs nor org-mode. See
related tags/ for this, or other tutorials.
Emacs
Understanding org-protocol links
If you’ve played with computers, even just a little bit, you’ll be aware that
not all URLs have the same syntax. The easiest example I can think of? In a
website you might see links that do not start with http(s): but with mailto:
(usually in the /contact page).3
This prefix is then parsed, with each link-type being mapped to an application.
That application, in turn, is responsible for handling that link. org-protocol
is an Emacs package that allows us to gracefully handle properly formatted URLs
with the org-protocol prefix!
More specifically, citing the official org-protocol documentation, we can use it to capture links and information as such:
The form of an Org protocol URL request with the capture protocol looks like this: org-protocol://capture?template=TEMPLATE&url=URL&title=TITLE&body=BODY In table form the following keys for the capture protocol are described as follows: Key Description Template Placeholder Notes template Template key Capture template key in org-capture-templates. If omitted, then org-protocol-default-template-key is used. url URL %:link Typically the web page URL to capture. title Title %:description Typically the title of the above web page, but can be arbitrary. body Body Text %i Typically the selected text in the web page, but can be arbitrary.
Understanding org-capture
Without going in too much depth, org-mode is a wonderful tool when it comes to
letting the users automate mundane tasks and that is particularly shown in the
case of GTD Workflows 4. Usually, you use them within Emacs, but the previous
section shows that org-protocol takes advantage of such templates (at least
one).
Once again the official documentation, explains best (this time through examples):
(setq org-capture-templates '(("t" "Todo" entry (file+headline "~/org/gtd.org" "Tasks") "* TODO %?\n %i\n %a") ("j" "Journal" entry (file+olp+datetree "~/org/journal.org") "* %?\nEntered on %U\n %i\n %a")))
Each template has specific elements:
- keys: (usually) a single letter5
- description: sigh
- type such as
entry, item, plain - target: such as
file , ~file+<stuff>, clock, here, idandfunction - template: a
string, afileor afunction, usually containing special characters - properties
For the templates included in this article, one needs to understand that being
able to use a function gives us much power.
Also, org-protocol gives us some more expansions available:
%:link The URL %:description The webpage title %:annotation Equivalent to [[%:link][%:description]] %i The selected text
Configuring
Having explained both the link structure and the org-capture-templates, to use
org-protocol all that is left at the moment is to configure emacs:
- We must configure it to act like a server (so that new Emacs instances
(
emacsclient) are effectively just new frames of that server) - We must load
org-protocol
(server-start) (require 'org-protocol)
To keep a reading list I went with perhaps the simplest template - using just
what is available through org-protocol :
(add-to-list 'org-capture-templates '("x" "Org-Protocol Reading List Capture" entry (file "reading_list.org") ;; relative to org-directory "* TODO %:annotation\n:PROPERTIES:\n:CREATED: %U\n:END:\n%i\n%?\n" :immediate-finish t :jump-to-captured t))
This is perfect to keep a list of articles to read in the future, but it is not exactly what I often need. Often, when I find something that I like, it happens that I want to take some notes (or jot down some ideas I got out of it). For that, I prefer to have multiple small files, instead of a single big one.
To handle that requirement I had to use a function, since
targetno longer is a simple string, static for all URLs. I want it to parse the URL so that it writes to the right file.(defun org-protocol-note-name () "function to set the target file for org-protocol note capture." ;; 1. get the url from the plist (note: check both :url and :link) (let* ((url (or (plist-get org-store-link-plist :url) (plist-get org-store-link-plist :link))) ;; (domain (url-host (url-generic-parse-url url))) ;; one could use just the domain (filename (if url (org-cache-file-namer-default url :category "article") (progn (message "org-protocol: no url found, defaulting to inbox.org") "inbox.org"))) (path (expand-file-name filename "/tmp"))) ;; debugging ;) (org-capture-put :target (list 'file path)) (set-buffer (find-file-noselect path)) (goto-char (point-max)) )) (add-to-list 'org-capture-templates '("n" "org-protocol note capture" entry (function org-protocol-note-name) "* %U\n:PROPERTIES:\n:CREATED: %U\n:SOURCE: %:link\n:END:\n#+begin_quote\n%i\n#+end_quote" :immediate-finish t :jump-to-captured t))
The documentation here should (in my mind) include an example, because I had to
play around a little bit (with org-capture-put and with setting the buffer nicely):
‘(function function-finding-location)’
Most general way: write your own function which both visits the file and moves point to the right location.
System-level integration
This section is somewhat Linux-specific. I guess macOS and Windows will have different
ways to handle this. The org-protocol documentation/post I suggested before has
sections for those systems as well
If you’ve already added the emacs configuration (and executed it), you might want to run the following:
# use your own template key instead of x xdg-open 'org-protocol://capture?template=x&url=site.com&title=test&body=shakeit'
If it works, then please feel free to ignore the following subsection. If you see any template related errors, you’ll need to take a step back and see what you changed. If, however, you are also unlucky to see some strange emacs frames popping up and doing nothing, off we go…
Desktop Files
In Linux the system knows how to handle specific URLs based on .desktop files
(and the mimeapps.list, but I am not sure of the precedence and configurability
of the latter). Finding the available desktop files is relatively easy:
# Check the available emacs desktop files in your setup find / -type f -name '*macs*.desktop' 2>/dev/null # If you've already customized anything: find $HOME/.local/share/applications -type f -name '*.desktop' 2>/dev/null
I was glad to see that in my distro the default desktop files (for emacsclient)
included the MimeType=x-scheme-handler/org-protocol property: meaning that they
advertised that they could handle org-protocol links.
But I wish they did not
Because my first thought was that, necessarily, I had misconfigured something
myself so I ended up losing time for nothing. Taking a better look at those
desktop files I realized the Exec command was rather unsightly and unnecessarily
complicated, given that most of the checks are already handled by emacs
internally, provided you start it as a server. So, I deleted them.
The thing is that you probably do not need to do this too. All you need to do is:
# Source: https://orgmode.org/worg/org-contrib/org-protocol.html#using-org-protocol cat <<'EOF' > $HOME/.local/share/applications/org-protocol.desktop [Desktop Entry] Name=org-protocol Comment=Intercept calls from emacsclient to trigger custom actions Categories=Other; Keywords=org-protocol; Icon=emacs Type=Application Exec=emacsclient -- %u Terminal=false StartupWMClass=Emacs MimeType=x-scheme-handler/org-protocol; 'EOF' # Then update the desktop files database update-desktop-database ~/.local/share/applications/
At this point, you should rerun the xdg-open tests. Everything must be working
before moving on to the next section.
Browser Integration
In this section we automate the creation of proper org-protocol links inside the
browser. There used to be extensions providing this functionality, but my
searches revealed nothing of the sort. Thus the options were:
- Create an extension
- K.I.S.S.
And I went with option (2): Creating a simple bookmark (or 2)!
Make sure to test it with your browser because I saw differences in how each
browser handled these. Chromium handled these perfectly, while for some reason
Firefox overwrites the page (showing the org-protocol URL) and requires a reload
afterwards.
I suggest that you use the following bookmark as found in docs:
javascript:location.href='org-protocol://capture?template=x'+ '&url='+encodeURIComponent(window.location.href)+ '&title='+encodeURIComponent(document.title)+ '&body='+encodeURIComponent(window.getSelection());
If this does not work, I also found these by alphapapa
javascript:location.href = 'org-protocol:///capture-html?template=w&url=' + encodeURIComponent(location.href) + '&title=' + encodeURIComponent(document.title || "[untitled page]") + '&body=' + encodeURIComponent(function () {var html = ""; if (typeof window.getSelection != "undefined") {var sel = window.getSelection(); if (sel.rangeCount) {var container = document.createElement("div"); for (var i = 0, len = sel.rangeCount; i < len; ++i) {container.appendChild(sel.getRangeAt(i).cloneContents());} html = container.innerHTML;}} else if (typeof document.selection != "undefined") {if (document.selection.type == "Text") {html = document.selection.createRange().htmlText;}} var relToAbs = function (href) {var a = document.createElement("a"); a.href = href; var abs = a.protocol + "//" + a.host + a.pathname + a.search + a.hash; a.remove(); return abs;}; var elementTypes = [['a', 'href'], ['img', 'src']]; var div = document.createElement('div'); div.innerHTML = html; elementTypes.map(function(elementType) {var elements = div.getElementsByTagName(elementType[0]); for (var i = 0; i < elements.length; i++) {elements[i].setAttribute(elementType[1], relToAbs(elements[i].getAttribute(elementType[1])));}}); return div.innerHTML;}()); javascript:location.href = 'org-protocol:///capture?template=x&url=' + encodeURIComponent(location.href) + '&title=' + encodeURIComponent(document.title || "[untitled page]");
And fooled around with these as well: I think I once had a problem with URL encoding so that’s why they’re encoded? The first one is way more readable, try to use that one
javascript:location.href='org-protocol:///capture?template=x&url=' + encodeURIComponent(location.href) + '&title=' + encodeURIComponent(document.title || "[untitled page]"); javascript:location.href='org-protocol:///capture?template=x&url=%27+encodeURIComponent(location.href)+%27&title+encodeURIComponent(document.title)+%27&body=%27+encodeURIComponent(window.getSelection().toString()) javascript:location.href='org-protocol://capture?url=%27+encodeURIComponent(location.href)+%27&title+encodeURIComponent(document.title)+%27&body=%27+encodeURIComponent(window.getSelection().toString()) javascript:location.href='org-protocol://capture?template=x&url=%27+encodeURIComponent(location.href)+%27&title+encodeURIComponent(document.title)+%27&body=%27+encodeURIComponent(window.getSelection().toString())
Caveat
If you go for the bookmark solution you might find yourself annoyed by the fact
that you repeatedly have to allow the site you’re visiting to visit org-protocol
URLs. This is a security feature and can under certain circumstances be
bypassed.
Footnotes:
It is a slightly more technical and more complete version of my article. For some reason I had forgotten all about it and found it just before publishing.
I was not aware of the link between Pocket and Mozilla to be frank.
I thought of the file: example as well, but I guess it is not as common
This article might be the reason why I started using emacs (though I am not
entirely sure). What I am absolutely sure of, however, is that this article, was
the reason why I decided to create my blog using emacs, and why (even now) my
css file is called rougier.css.
It is possible to have nested capture templates but that is not our focus