Nicolas Martyanoff – Brain dump About

Controlling link opening in Emacs

Multiple Emacs modes recognize URIs so that you can follow them in a web browser, usually with a mouse left click. In modes that do not support it, one can still call browse-url-at-point interactively with the cursor positioned on a URI.

I wanted to make link handling more convenient for some time; this is what I ended up with.

Key bindings

There is no global Emacs concept of following links, so modes deal with them differently. I wanted to use the same key binding everywhere; I went for C-o (“o” for “open”) since I never use open-line. I also wanted the ability to use Eww —the Emacs builtin web browser— when it is convenient. So I added a function to select the secondary browser when the prefix is set. This way C-o opens Firefox, and C-u C-o opens Eww.

(setq browse-url-secondary-browser-function 'eww-browse-url)

(defun g-browse-url-at-point (arg)
  (interactive "P")
  (let ((browse-url-browser-function
         (if arg
             browse-url-secondary-browser-function
           browse-url-browser-function)))
    (browse-url-at-point)))

(keymap-global-set "C-o" 'g-browse-url-at-point)

Ultimately the function calls browse-url-at-point which identifies the URI under the cursor (i.e. at the current point) and calls browse-url.

Selecting a web browser

The browse-url function is fully configurable; two mechanisms are used to select a web browser for each link:

  • the browse-url-handlers and browse-url-default-handlers association lists, used to match specific URIs using predicates or regular expression;
  • the browse-url-browser-function variable (by default browser-url-default-browser) referencing a function which looks for available web browsers on the current machine.

Since I use multiple Firefox profiles, I needed the ability to open URIs in the right profile depending of the context. For example links in a professional email read in Gnus should be open in my work context.

The following function returns a handler that can be used in browse-url-handlers or as value for browse-url-browser-function:

(defun g-browse-url-firefox-function (profile)
  (lambda (uri &rest args)
    (let ((process-environment (browse-url-process-environment))
          (process-name (concat "firefox " uri))
          (process-args `("--new-tab" ,uri
                          ,@(if profile (list "-P" profile)))))
      (apply #'start-process process-name nil "firefox" process-args))))

For example, (g-browse-url-firefox-function "test") returns a browse function that will open the link in a new tab of a Firefox window associated with the “test” profile.

To change the logic deciding how to handle links in a buffer with a specific major mode, all you have to do is to add a hook to set browse-url-browser-function in a way that only affects the current buffer. Since this variable is not buffer-local by default, you have to take care to use setq-local when changing it.

Gnus messages are displayed using the gnus-article-mode major mode.

We first write a function to identify the account associated with the current group (assuming you are using nnimap for your email accounts as I do):

(defun g-current-gnus-account ()
  (let ((group gnus-newsgroup-name))
    (when (string-match "^nnimap\\+\\([^:]+\\):" group)
      (match-string 1 group))))

Then we define the actual browse function:

(defun g-gnus-browse-url (url &rest args)
  (let ((profile
         (pcase (g-current-gnus-account)
           ("my-company"
            "my-company-profile")
           ("a-client"
            "the-client-profile")
           (_
            "default"))))
    (apply (g-browse-url-firefox-function profile) url args)))

And the hook function:

(defun g-gnus-set-browser-function ()
  (setq-local browse-url-browser-function 'g-gnus-browse-url))

Finally we update the hook:

(use-package gnus-art
  :hook
  ((gnus-article-mode-hook . g-gnus-set-browser-function)))

Links in emails are now open in the right Firefox profile!

Same idea for Circe, select the right Firefox profile based on the network and channel of the current channel buffer.

We define the function to be called during the initialization of each channel buffer:

(defun g-circe-set-browser-function ()
  (let* ((network (with-circe-server-buffer
                    circe-network))
         (channel circe-chat-target)
         (profile (cond
                   ((and (equal network "my-network")
                         (equal channel "#my-channel"))
                    "the-right-profile")
                   (t
                    "default"))))
    (setq-local browse-url-browser-function
                (g-browse-url-firefox-function profile))))

And add it as a hook:

(use-package circe
  :hook
  ((circe-channel-mode-hook . g-circe-set-browser-function))

Share the word!

Liked my article? Follow me on Twitter or on Mastodon to see what I'm up to.