Dump Your Currently Set Faces in Emacs

Posted on 26th of May 2024 | 639 words

Every once in a while I like to play around with Emacs themes. Of course, mainly due to yak-shaving and procrastination reasons and nothing actually valid. But it’s fun nonetheless, although occasionally very tedious. Tedious in a sense that Emacs tend to come with a lot of different faces, especially if you happen to use more than a few third-party packages.

Defining a custom theme in Emacs

Emacs comes with couple relatively straight forward way to set custom themes in the function custom-theme-set where you define the name of the theme and then the faces you want to modify:

(deftheme fancy-theme)

(custom-set-faces
 'fancy-theme
 '(default ((t (:foreground "black" :background "white"))))

 ;; [... and so on]
 )

You’re also able to control on what sort of displays your custom theme should support. For example, if you want to use theme in only terminals that support minimum number of 16 different colors, you’re able to define that like:

'(default ((((class color) (min-colors 16)) (:foreground "black" :background "white"))))

Of course that can be slightly verbose, so often you might see custom themes to be set like:

(deftheme fancy-theme)

(let ((class '((class color) (min-colors 16))))
  (custom-set-faces
   'fancy-theme
   `(default ((,class (:foreground "black" :background "white"))))

   ;; [... and so on]
   ))

But it still tedious since there is a lot of faces and there can be lot of variance between the faces so often if you want to write your own full fledged theme, it’s better to explicitly set all the faces that you plan to use. Mainly since, if the face is not set in the theme it’ll use that lastly set face for that face in question, so if you session has used multiple different faces, it can lead into some funky color combinations.

Currently in my Emacs session – which is relatively light package-wise – amount of currently set faces equals to 869.

Simple tool written for aspiring theme developers

This is why, I decided to write a simple Emacs Lisp helper function to dump all the currently set faces to a buffer where you can copy them to your own theme at will. I also made it so you can simply just dump the faces from a give package if you only care, e.g. about Magit’s faces.

(require 'cl-lib)

(defconst theme-dump--buffer-name "*theme-dump*")
(defconst theme-dump--default-format-string "%s %s ")
(defconst theme-dump--string-format-string "%s \"%s\" ")

(defun theme-dump-current-faces (pkg)
  (interactive (list (read-string "Dump faces from package (leave empty for all packages): ")))
  (let ((faces '()))
    (dolist (f (theme-dump--filter-faces-by-package pkg))
      (let ((attr-str ""))
        (dolist (a (face-all-attributes f))
          (setq attr-str
                (concat attr-str (theme-dump--format-attr-str f a))))
        (push (if (not (string-equal attr-str ""))
                  (format "`(%s ((,class (%s))))\n" f (string-trim-right attr-str))
                (format "`(%s (( )))\n" f))
              faces)))
    (let ((buf (get-buffer-create theme-dump--buffer-name)))
      (with-current-buffer buf
        (erase-buffer)
        (insert (mapconcat 'identity faces))
        (local-set-key (kbd "q") 'kill-buffer-and-window))
      (split-window-right)
      (other-window 1)
      (switch-to-buffer buf))))

(defun theme-dump--filter-faces-by-package (pkg)
  (cl-flet ((pkg-face-p (face)
              (cl-search pkg (symbol-name face))))
    (if (not (string-equal pkg ""))
        (cl-remove-if-not #'pkg-face-p (face-list))
      (face-list))))

(defun theme-dump--format-attr-str (face attr)
  (unless (equal (face-attribute face (car attr))
                 'unspecified)
    (cl-typecase (face-attribute face (car attr))
      (string (format theme-dump--string-format-string
                      (symbol-name (car attr))
                      (face-attribute face (car attr))))
      (t (format theme-dump--default-format-string
                 (symbol-name (car attr))
                 (face-attribute face (car attr)))))))

Essentially, this just gets all the faces with face-list and either returns all of the or filters them by package name if you want. Function prints the faces into a new buffer in a format that is already suitable for new custom themes:

`(magit-section-highlight ((,class (:extend t :background "grey95"))))
`(magit-section-heading ((,class (:weight bold :extend t))))
`(magit-section-secondary-heading ((,class (:weight bold :extend t))))
`(magit-section-heading-selection ((,class (:extend t :foreground "salmon4"))))
`(magit-section-child-count (( )))

;; and so on...

The printed faces can then easily be copied to custom-set-faces for your possible new theme. Faces are also backticked by default, in case you want to change faces to use variables instead of hardcoded names.

Code is in public domain and also available in sourcehut.