Switching Emacs Themes

I've been playing around with Emacs themes recently; I can't seem to find one that's just right. While experimenting, I would load one theme after another using load-theme & I noticed that after the first few, successive themes would look decidedly odd; as if the prior theme wasn't completely gone.

It turns out that that's exactly right. Unsurprisingly, had I just read the docs, I would have known that. The docstring for enable-theme (which is called by default when loading a theme) says "…After this function completes, THEME will have the highest precedence (after `user') among enabled themes. Note that any already-enabled themes remain enabled after this function runs. To disable other themes, use `disable-theme'."

I only found that later; what I started with was a nice post by Greg Hendershott on exactly this topic. He wanted to do what I want to do: switch cleanly between themes, and maybe apply a few tweaks to each when they're loaded. He notes (as echoed by the docstring) that the first thing we have to do is call disable-theme on every currently enabled theme. Then load the new theme, then finally apply any customizations you want to make to the new theme– sounds like advice.

I'm going to post my code, but it's almost completely copied from his (on which more below). Let's start with some scaffolding:

(defvar sp1ff/theme-hooks nil
  "An alist mapping theme symbol to user-defined customization
  function.")

(defun sp1ff/add-theme-hook (theme hook)
  "Add HOOK for theme THEME. The hook will be invoked whenever
THEME is enabled."
  (add-to-list 'sp1ff/theme-hooks (cons theme hook)))

(defun sp1ff/disable-all-themes ()
  "Disable all currently enabled themes, as defined by `custom-enabled-themes'."
  (interactive)
  (mapc #'disable-theme custom-enabled-themes))

Now I can define my advice:

(defun sp1ff/enable-theme-advice (f theme)
  "Around advice for `enable-theme'.

Before the invocation of `enable-theme' disable all
currently-enabled themes. After, invoke any user-supplied hook to
carry out final customization of the new theme."
  (if (eq theme 'user)
      (apply f theme nil)
    (sp1ff/disable-all-themes)
    (prog1 ;; return the result of calling `f'
        (apply f theme nil)
      (pcase (assq theme sp1ff/theme-hooks)
        (`((,_ . ,f) (funcall f))))))

  (advice-add 'enable-theme
              :around
              #'sp1ff/enable-theme-advice))

Greg advised load-theme, which I chose not to do. I read the code, and all load-theme does is load the file & delegate to enable-theme where all the customization of faces & variables happens. Furthermore, once I've loaded a theme, I could see myself just calling enable-theme to switch to it in the future (assuming I'd loaded another in the meantime). If I advised load-theme none of my customizations would be run.

Feeling quite clever, I proceeded to advise enable-theme instead & tried loading up a custom theme– the screen flashed & I found myself right back where I'd started. It took some poking around, but I found the problem– the last thing enable-theme does on the way out is to call itself recursively on the 'user theme (to give 'user the highest priority) so long as it's not being called on 'user in the first place. My advice would get run again, disabling the theme I'd just loaded. That's the reason for the initial if clause in my advice.

07/12/20 08:26

Updated 07/24/20 10:23 to correct a few typos.

Updated 04/29/21 07:01 to correct a few more typos I noticed after Sacha kindly mentioned this post in her weekly Emacs News post & brought it to more people's attention– thanks Sacha!

Updated 05/29/22 17:14 to correct yet another typo sigh