< expLog

Making Poet, an Emacs theme

Making OfReleaseMonochromeMetapoetDark Poet

The sections stand by themselves for the most part, feel free to jump around because this post became much longer than I'd intended it to.

I describe why I made the theme and the experience of having it out there in the first two sections; Monochrome includes elisp to desaturate any emacs-theme; metapoet and dark-poet cover workflows that might be valuable for anyone building their own themes.

poet-light.png

Making Of

I released my first (and only) Emacs theme in February this year, including publishing to Melpa. Since then I’ve released some automatically generated monochrome variants, and I’ve been iterating on a dark variant today (now in beta).

I started working on Poet because I spend a significant amount of time working in Emacs: programming in C, Python, PHP, Rust (I end up going to IntelliJ for Java & Android); maintaining notes, lists, plans, tasks, time-tracking, etc. And – just about as much time as the rest – tweaking Emacs to make it perfect. 1

Part of that meant I wanted to have a much better experience writing prose 2. And of course, I particularly wanted to be able to use Org and Evil with a great UI.

Certain Markdown editors – particularly Typora – had a much better writing experience; I also particularly enjoyed reading Edward Tufte’s books and was very fond of the typography and design behind Tufte CSS. White on yellow for accents was a design choice that had been running through my head ever since I saw some art using black on yellow with white accents.

Mixing in some inspiration from legendary color schemes like Leuven – which is a great example of just how well a theme can support org-mode, and Jazz – which does a great job with mode-lines helped me make Poet.

Finally, I realized that it’s possible to mix and match different types of fonts in Emacs, which is what adds the most value to the theme. I basically manually identified faces that should be fixed-pitch and those that can be variable-pitch and went ahead and explicitly listed them as such in the theme.

Later on, I found out about Mixed Pitch Mode which would have probably been better to base my work off of; that said I really wanted to be able to completely own the behaviour (for example, poet also slightly increases the line-height).

Release

It didn't take much time to get approval to open-source from my employer.

Adding Poet to Melpa was fairly painless and rewarding – Steve Purcell took the time to look through the time and help improve it, the guidelines around using package-lint and checkdoc helped clean it up ever further.

It’s been incredibly fun and satisfying to observe reactions to Poet along the way; posting about it to Reddit was good, but it’s so much more fun to see other people bring it up – including a blogpost on Irreal. I tend to find out about these from Github’s traffic insights, with a sudden spike resolving into links the next day when Github aggregates traffic data.

(It also helps that I like using Poet in my day to day).

Monochrome

Being lazy, my initial attempt at a dark version of Poet was to write a little bit of elisp:

(defun desaturate-color (color-hex)
  "Converts a color string to its desaturated equivalent hex string"
  (require 'color)
  (apply
   'color-rgb-to-hex
   (append (apply
            'color-hsl-to-rgb
            (apply
             'color-desaturate-hsl
             `(,@(apply 'color-rgb-to-hsl (color-name-to-rgb color-hex)) 100)))
           '(2))))

(defun transform-theme-colors (fn)
  "Apply FN to the colors on every active face.

   FN should accept the face symbol and the current color,
   and return the new color to be applied."
  (interactive)
  (mapc
   (lambda (face)
     (mapc
      (lambda (attr)
        (let ((current (face-attribute face attr)))
          (unless (or (not current)
                      (listp current)
                      (string= current "unspecified")
                      (string= current "t"))
            (set-face-attribute face nil attr (funcall fn face current)))))
      '(:foreground :background :underline :overline :box :strike-through
                    :distant-foreground))
     (mapc
      (lambda (complex-attr)
        (let* ((full (copy-tree (face-attribute face complex-attr)))
               (current (if (listp full) (member :color full))))
          (unless (or (not current)
                      (not (listp full)))
            (setcar (cdr current) (funcall fn face (cadr current)))
            (set-face-attribute face nil complex-attr full))))
      '(:underline :overline :box)))
   (face-list)))

(defun desaturate-theme ()
  "As title: desaturate all currently active face colorsj."
  (interactive)
  (transform-theme-colors
   (lambda (face color)
     (desaturate-color color))))

(defun invert-theme ()
  "Take the complement of all currently active colors."
  (interactive)
  (require 'color)
  (transform-theme-colors
   (lambda (face color)
     (apply
      'color-rgb-to-hex
      (color-complement color))))
  (let ((current-ns-appearance (assoc 'ns-appearance default-frame-alist)))
    (cond ((eq (cdr current-ns-appearance) 'light)
           (setf (cdr current-ns-appearance) 'dark))
          ((eq (cdr current-ns-appearance) 'dark)
           (setf (cdr current-ns-appearance) 'light)))))

desaturate-theme and invert-theme basically work on any theme, in case you want to recolour some other favourites.

Metapoet

After receiving a couple of requests to create a dark theme, I decided to explicitly publish the monochrome themes – both dark and light variants. I explicitly didn't want to maintain the structure, nor pick up a significant additional burden for updating themes.

Which meant generating these automatically : and of course, org-mode, babel and tangling felt like the most suitable options. Tangling allows me to literally3 export a table of colours to generate the theme!

It takes me some time to get all the metadata around a theme perfectly right; only having to do it once makes me much happier.

Perhaps my favourite part is a tight editing loop: at the end of the org file I have a small snippet that reloads the current theme (presumably the variant of poet I'm iterating on) every time I save the file – which means iterating is simply updating a colour in the table and saving the file to reload with new colours.

(I’ve found this pattern extremely valuable for any iteration within emacs.)

(defun poet-refresh-theme ()
  (interactive)
  (org-babel-tangle)
  (load-theme
    (car custom-enabled-themes)
    t))
(add-hook
   'after-save-hook
   'poet-refresh-theme
    nil
    t)

Dark Poet

Finally, I spent a large part of today iterating on poet-dark: my current workflow is to use Rainbow mode to enable colour highlighting within an emacs buffer, with a vertical split showing a test file with poet applied.

poet.png

Figure 1: Iterating

I used Paletton a lot, Coolors a little, as well as lots of websites describing color-schemes as I edited my way towards something that works. At some point I’d love to run my theme through a color contrast checker, and build some better tools within emacs to adjust a colour’s luminance and saturation in small steps. Other sources included staring at anything brown, including Number 28 and Number 13a.

Within emacs itself, C-u C-x = is invaluable in figuring out the font-face behind a given UI element, as well as list-faces-display.

With all that said, today I’m pushing a beta version of Poet-dark (which means it’ll be available to use if you know about it, but I won’t advertise it on Github with screenshots just yet).

darkpoet.png

Figure 2: poet-dark (beta)

I’ll make it official after spending a few weeks using it as my daily driver.

Please reach out if you have any feedback about Poet or this blog!

Footnotes:

1

I had planned to start working on poet first thing in the morning; instead I spent the first hour looking into mechanisms to add a better color picker to Emacs.

2

I was procrastinating from writing peer reviews to work on poet instead.

3

Pun intended.

Discuss this post on Reddit.