Emacs Themes
Until a few months ago I didn’t use Emacs themes. A custom-set-faces
form in my init file gradually accumulated face specs like a lint-roller.
Then I started to use Solarized. Mostly light, sometimes dark. Switching between them using M-x load-theme worked fine.
Later I liked the look of Material. Although too high-contrast to use full-time, it works well in certain situations.
After I installed it I had two annoyances:
-
I didn’t love the 3D “button” look it gives org-mode headings. Must tweak.
-
Switching between the Solarized and Material themes using
load-theme
definitely did not work well: If the old theme defined a face, but the new theme did not, the old face would remain in effect. So for example I might switch to Material then back to Solarized, and get a weird mix of mostly Solarized but with Material org headings.
Here’s what I’m doing to address both issues.
Multiple themes
What I didn’t understand at first is that Emacs supports many themes enabled simultaneously. The variable custom-enabled-themes
is plural, a list. As a result, load-theme
doesn’t mean “use this one theme” — it means, “layer this theme on top of those already enabled”.
If want just one theme at a time? I must first disable-theme
each currently-enabled theme. A little function/command:1
1 2 3 |
(defun gh/disable-all-themes () (interactive) (mapc #'disable-theme custom-enabled-themes)) |
So fine, but, now I need to issue two commands. Give me convenience or give me death.
One idea would be to make a new gh/load-theme
command that composes gh/disable-all-themes
and load-theme
. But will I remember to
M-x that instead?
Another idea would be to advise load-theme
so I can continue doing
M-x load-theme.
I did that. As a bonus, such advice can also provide a kind of “theme hooks” feature — a way to call a function after a specific theme is loaded.
Emacs init file
Here are some snippets from my ~/.emacs.d/init.el
.
Advice and hooks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
;;; Theme hooks (defvar gh/theme-hooks nil "((theme-id . function) ...)") (defun gh/add-theme-hook (theme-id hook-func) (add-to-list 'gh/theme-hooks (cons theme-id hook-func))) (defun gh/load-theme-advice (f theme-id &optional no-confirm no-enable &rest args) "Enhances `load-theme' in two ways: 1. Disables enabled themes for a clean slate. 2. Calls functions registered using `gh/add-theme-hook'." (unless no-enable (gh/disable-all-themes)) (prog1 (apply f theme-id no-confirm no-enable args) (unless no-enable (pcase (assq theme-id gh/theme-hooks) (`(,_ . ,f) (funcall f)))))) (advice-add 'load-theme :around #'gh/load-theme-advice) |
Packages
Now my configuration of each theme package can add a theme hook to make tweaks.
I manage package install and config in my init file using the wonderful use-package.
My personal preferences for Material:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
(use-package material-theme :ensure t :defer t :init (defun gh/material-theme-hook () (set-face-attribute 'which-key-key-face nil :foreground (face-attribute 'error :foreground)) (loop for n from 1 to 8 do (set-face-attribute (intern-soft (format "org-level-%s" n)) nil :height 'unspecified :background 'unspecified :box 'unspecified))) (gh/add-theme-hook 'material #'gh/material-theme-hook) (gh/add-theme-hook 'material-light #'gh/material-theme-hook)) |
And for Solarized:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
(use-package solarized :ensure solarized-theme :defer t :init (defun gh/solarized-theme-hook () (set-face-attribute 'font-lock-constant-face nil :weight 'normal) (set-face-attribute 'font-lock-function-name-face nil :weight 'bold) (set-face-attribute 'which-key-key-face nil :foreground (face-attribute 'error :foreground))) (gh/add-theme-hook 'solarized-dark #'gh/solarized-theme-hook) (gh/add-theme-hook 'solarized-light #'gh/solarized-theme-hook) :config (setq solarized-use-variable-pitch nil solarized-use-less-bold t solarized-use-more-italic nil solarized-distinct-doc-face t solarized-high-contrast-mode-line t ;; I find different font sizes irritating. solarized-height-minus-1 1.0 solarized-height-plus-1 1.0 solarized-height-plus-2 1.0 solarized-height-plus-3 1.0 solarized-height-plus-4 1.0)) |
Hydra
Finally, let’s use the hydra package …
1 2 3 4 |
(use-package hydra :ensure t :config (setq hydra-lv nil) ;use echo area) |
… to make it easy to switch among themes with a single key press …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
(defhydra gh/themes-hydra (:hint nil :color pink) " Themes ^Solarized^ ^Material^ ^Other^ ---------------------------------------------------- _s_: Dark _m_: Dark _z_: Zenburn _DEL_: none _S_: Light _M_: Light " ("s" (load-theme 'solarized-dark t)) ("S" (load-theme 'solarized-light t)) ("m" (load-theme 'material t)) ("M" (load-theme 'material-light t)) ("z" (load-theme 'zenburn t)) ("DEL" (gh/disable-all-themes)) ("RET" nil "done" :color blue)) (bind-keys ("C-c w t" . gh/themes-hydra/body)) |
Incidentally I bind this hydra to C-c w t following an idea I saw in lunaryorn’s init file:
-
Use
bind-keys
(fromuse-package
) to put commands on C-c prefixes, with an additional prefix key to group related commands. So for example window-related commands have a C-c w prefix. -
Use
which-key
to label prefixes and bindings.
Anyway, the upshot is I can type C-c w t, then keep pressing single hydra keys to switch among themes. Upon finding the optimal mood management, tap RETURN to exit the hydra.
-
In my init file I do the customary thing: Prefix identifiers with my initials and a slash:
gh/
. ↩