Firefox Sends Commands To Emacs

We create an endpoint in emacs that listens to commands from firefox (the same technique works in chrome or other browsers1). For this article, we focus on org-protocol only2 on linux.

Here's a demo of of me writing the current url to a file using a custom org protocol (not capture, not store-link, not something built in!):

A video of myself clicking a button in firefox, on the left. Emacs on the right shows that I'm receiveing the url it auto adds to a buffer.

But of course, you could make emacs do anything, like take a screenshot through a shell command, create a todo item, send out an email, and more!

Head to Setup if you want to get setup right away!

Breakdown:

Just note, that

  • I click a javascript bookmarklet, which attempts to GET a resource.
    javascript:(function(){document.location.href="org-protocol://my-protocol?url=" + encodeURIComponent(location.href) + "&file-name=.txt";})();
    
  • Firefox recognizes org-protocol://my-protocol?url=https://17070415.xyz/support&file-name=delete-me.txt

    and lets the system handle the request.

  • My linux system looks table for handling the org-protocol protocol (the bit before the first :)

    Inside either /usr/share/applications/mimeinfo.cache or ~/.config/mimeapps.list, I have

    x-scheme-handler/org-protocol=emacsclient.desktop;
    
  • emacsclient.desktop handles the call

    The relevant part is the Exec key. The main focus is emacsclient %U:

    Exec=sh -c "if [ -n \\"\\$*\\" ]; then exec /usr/bin/emacsclient --alternate-editor= --display=\\"\\$DISPLAY\\" \\"\\$@\\"; else exec emacsclient %U; fi" sh %F
    

    The %U allows emacsclient to be called with an arg interpreted as a url, and not a file3.

  • emacsclient receives arguments

    and is called like so, as if I was executing from a terminal:

    emacsclient "org-protocol://my-protocol?url=https://17070415.xyz/support&file-name=delete-me.txt"
    
  • the custom handler in emacs runs!

And of course, you can use the same technique to have emacs call anything on your system! Further, you can use arbitrary javascript to get a lot data from the webpages (currently selected text, author of an article, etc.).

Requirements

  • You have emacsclient installed (ships with emacs)
  • org mode (ships with emacs)
  • firefox, but this also works on most major browsers

Setup

Setup in three parts:

  1. Load some code so emacs can listen for a request
  2. Configure firefox to open a org-protocol externally.
  3. Ensure your system recognizes the org-protocol to open emacsclient, allowing us to trigger code from 1.

1. In emacs, run this:

(require 'org-protocol)
(server-start)

(defun my-protocol-handler (url)
  (message "%s" url)
  ;; (:url https:/duckduckgo.com :title TITLE)
  (let* ((splitparts (org-protocol-parse-parameters url nil '(:url :file-name)))
         (uri (org-protocol-sanitize-uri (plist-get splitparts :url)))
         (file-name (plist-get splitparts :file-name))
         (kill-p (plist-get splitparts :kill-p))
         (buffer (find-file-noselect (file-name-concat "/tmp/delete-me.txt" file-name))))

    (with-current-buffer buffer
      (goto-char (point-max))
      (insert "\n")
      (insert uri)
      (save-buffer)
      (when kill-p (kill-current-buffer))))
  nil)

(with-eval-after-load 'org-protocol
  (setq org-protocol-protocol-alist
        '(("my-protocol"
           :protocol "my-protocol"
           :function my-protocol-handler))))

2. Configure firefox

Create a bookmark in firefox:

javascript:(function(){document.location.href="org-protocol://my-protocol?url=" + encodeURIComponent(location.href) + "&file-name=.txt";})();
Creating a new bookmark with the bookmark pane open (Ctrl + b) for the custom org protocol.

And in firefox, goto about:config and make sure you have these items set:

  • network.protocol-handler.expose.org-protocol is set to true
  • network.protocol-handler.external.org-protocol is set to true
    (or network.protocol-handler.external-default is set to true, but this applies more broadly than just org-protocol)
about-config-settings.png

3. Mime types

cp /usr/share/applications/emacsclient.desktop ~/.local/share/applications/emacsclient.desktop

and change the Exec in the .local copy to:

Exec=sh -c "if [ -n \\"\\$*\\" ]; then exec /usr/bin/emacsclient --alternate-editor= --display=\\"\\$DISPLAY\\" \\"\\$@\\"; else exec emacsclient %U; fi" sh %F

Or change the Exec to just

emacsclient %U

Sometimes you need to ensure that this setting takes effect. On my system it was instant.

Run this to be sure:

update-desktop-database ~/.local/share/applications/

4. Try it!

In firefox, click the bookmark. You should have similar results as the demo.

Troubleshooting

Here are useful debugging steps to figure out what's not working

The handler works, but the webpage gets rewritten

For whatever reason, on my firefox version 133.0.3 javascript of this form

javascript:document.location.href="org-protocol://my-protocol?url=" + encodeURIComponent(location.href) + "&file-name=delete-me.txt";

will overwrite the current tab's state. What ended up working for me was wrapping the assignment in a function:

javascript:(function(){document.location.href="org-protocol://my-protocol?url=" + encodeURIComponent(location.href) + "&file-name=delete-me.txt";})();

Does emacsclient recognize the protocol?

Try calling in your terminal:

emacsclient "org-protocol://my-protocol?url=https://17070415.xyz&file-name=delete-me.txt"

If your handler doesn't fire, do step 1 again. Inspect org-protocol-protocol-alist. It should look like

(("my-protocol" :protocol "my-protocol" :function my-protocol-handler))

If this variable doesn't exist, you forgot to (require 'org-protocol).

Firefox doesn't recognize the protocol

Your console (bring it up with F12 in your browser) will show

Prevented navigation to “org-protocol://my-protocol?url=https%3A%2F%2Fspecifications.freedesktop.org%2Fdesktop-entry-spec%2Flatest%2Fexec-variables.html&file-name=delete-me.txt” due to an unknown protocol.

This error shows up usually because you are missing some firefox config flags. And different config flags seem to change depending on your FF version or perhaps even how you installed FF.

  • visit about:config in the URL bar.
  • ensure network.protocol-handler.expose.org-protocol is set to true
  • ensure network.protocol-handler.external.org-protocol is set to true
    or network.protocol-handler.external-default is set to true

You'll know if this is working when you don't see the error message anymore when you run

(function(){document.location.href="org-protocol://my-protocol?url=" + encodeURIComponent(location.href) + "&file-name=.txt";})();

Emacs tries to open a file

Most likely in your emacsclient.desktop, emacsclient is called with %F, not %U.

%U A list of URLs. Each URL is passed as a separate argument to the executable program. Local files may either be passed as file: URLs or as file path.

You can verify correct behavior from steps in Does emacsclient recognize the protocol?

Doesn't work in some FF browsers?

If you use FF based browsers, like Tor Browser, I found that

~/.local/share/applications/emacsclient.desktop

wasn't used. Instead it uses explicitly,

/usr/share/applications/emacsclient.desktop

In this case, modify the emacsclient.desktop file at the system level to call emacsclient %U.

Something else?

See footnotes for sources for this article4. One of those links might be useful.

Wrap up

We've seen how to register our own "subprotocol" under org-protocol, (in our example, the subprotocol is "my-protocol"). What if you wanted even more power?

  • Emacs can open ports and listen to them allowing you to have a full request/response cycle. You can have your browser javascript make a GET/POST request on a local server served by emacs.
  • This is how browser extensions like Atomic Chrome, and Edit With Emacs work.

Lastly if this article helped you please consider supporting my content and visit support. Thanks so much!


1

You'll want to make sure the browser opens an org-protocol link externally.

2

firefox also supports DBUS, but its interface is limited. I doubt you can send to dbus on firefox with javascript. If you're interested in using emacs with DBUS, check out my other article on using dbus in emacs.

3

See https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html. If your emacsclient is opening a new frame, emacsclient is likely treating the argument as a file.