#+TITLE: 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.

../static/images/poet-light.png

* Making Of
:PROPERTIES:
:CUSTOM_ID: making
:END:
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. [fn:palette]

Part of that meant I wanted to have a much better experience writing
prose [fn:reviews].  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
:PROPERTIES:
:CUSTOM_ID: release
:END:
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
:PROPERTIES:
:CUSTOM_ID: monochrome
:END:
Being lazy, my initial attempt at a dark version of Poet was to write
a little bit of elisp:

#+BEGIN_SRC emacs-lisp
(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)))))
#+END_SRC

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

* Metapoet
:PROPERTIES:
:CUSTOM_ID: metapoet
:END:

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 literally[fn:pun] 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.)

#+BEGIN_SRC emacs-lisp
(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)
#+END_SRC

* Dark Poet
:PROPERTIES:
:CUSTOM_ID: dark
:END:

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.

#+CAPTION: Iterating
../static/images/poet.png

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).

#+CAPTION: poet-dark (beta)
../static/images/darkpoet.png


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!

[fn:palette] 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.

[fn:reviews] I was procrastinating from writing peer reviews to work
on poet instead.

[fn:pun] Pun intended.

Discuss this post on Reddit.