Emacs: building on top of the Modus themes

I have been doing a lot of work these days on my themes. The immediate goal is two-fold: (i) make the modus-themes more flexible so they can be used as a the basis for other theme packages and (ii) make the ef-themes the first project to benefit from this development. Having the Modus themes as a foundation gives us all of their customisability and extensive face coverage for little extra work. The themes are well tested and are also shipped with core Emacs. It all fits together.

In this article, I give you the big picture view of how this is supposed to work. Remember that the only source of truth for my packages is their corresponding manual. Any blog post is useful the time it is written but will eventually go out of date.

Symbol properties for themes

When we define a theme, we essentially add properties to a symbol. In its simplest form, this is how:

(put 'my-test-symbol 'my-test-proerty "Hello world test value")

Evaluate the above and then try the following:

(get 'my-test-symbol 'my-test-proerty)
;; => "Hello world test value"

The function custom-declare-theme does the heavy lifting, while the deftheme macro streamlines most of that work. Still, the point is that we have symbols whose properties we can access and, thus, we can filter by any given property. To make things even better, we can add arbitrary properties to a theme. Here is a real scenario of _in-development code that might change:

(get 'modus-operandi 'theme-properties)
;; => (:kind color-scheme :background-mode light :family modus-themes :modus-core-palette modus-themes-operandi-palette :modus-user-palette modus-operandi-palette-user :modus-overrides-palette modus-operandi-palette-overrides)

The theme-properties has as a plist value. Its Modus-specific properties are references to variables that we can use to do our work, such as to put together a theme palette that combines the relevant overrides with the core entries.

Getting a list of themes based on their properties

When we declare a theme with custom-declare-theme, we make it known to Emacs by adding it to the custom-known-themes. When we eventually load a theme, its symbol gets stored in the custom-enabled-themes. Knowing that themes have properties, we can filter those lists accordingly. With my current development code, I can do this, for example:

(defun my-demo-is-modus-p (theme)
  "Return non-nil if THEME has `modus-themes' :family property."
  (when-let* ((properties (get theme 'theme-properties))
              (family (plist-get properties :family)))
    (eq family 'modus-themes)))

(seq-filter #'my-demo-is-modus-p custom-known-themes)
;; => (modus-vivendi-tritanopia modus-vivendi-tinted modus-vivendi modus-vivendi-deuteranopia modus-operandi-tritanopia modus-operandi-tinted modus-operandi modus-operandi-deuteranopia)

The next step from here is to make all the Modus infrastructure rely on generic functions and methods for working with themes. Then any package can provides its own method for returning a list of desired themes.

Generic function and methods for getting a list of themes

Emacs Lisp has a concept of generic functions, which it borrows from Common Lisp. The general idea is to have a single symbol, like modus-themes-get-themes whose implementation details are instantiated via specialised methods. For example, when a minor mode is active, a given method takes effect, thus changing what modus-themes-get-themes actually does.

The default implementation is this:

(cl-defgeneric modus-themes-get-themes ()
  "Return a list of all themes with `modus-themes' :family property."
  (modus-themes-get-all-known-themes 'modus-themes))

The function modus-themes-get-all-known-themes has a filter like the one I demonstrated in the code block further above. By default, this is what I get when I run the aforementioned generic function:

(modus-themes-get-themes)
;; => (modus-operandi modus-operandi-tinted modus-operandi-deuteranopia modus-operandi-tritanopia modus-vivendi modus-vivendi-tinted modus-vivendi-deuteranopia modus-vivendi-tritanopia)

The beauty of this design is that another package can define a method to make the same code return something else. This is how I do it in the current development target of the ef-themes (again, the actual code might change):

(cl-defmethod modus-themes-get-themes (&context (ef-themes-take-over-modus-themes-mode (eql t)))
  (modus-themes-get-all-known-themes 'ef-themes))

Notice that this method has a &context, which is the scenario in which it is relevant. In this case, we have a minor mode that activates the method when it is enabled:

(define-minor-mode ef-themes-take-over-modus-themes-mode
  "When enabled, all Modus themes commands consider only Ef themes."
  :global t
  :init-value nil)

This minor mode does not have anything in its body. It does not need to, because the define-minor-mode macro already instantiates the parts we care about. Namely, when we call the function defined by the minor mode (i.e. ef-themes-take-over-modus-themes-mode), it toggles the value of the variable ef-themes-take-over-modus-themes-mode (functions and variables have separate namespaces in Emacs Lisp and thus the same symbol can be in both places). Our method then becomes relevant when the user enables the minor mode:

(ef-themes-take-over-modus-themes-mode 1)

And now the generic function modus-themes-get-themes does something else:

(modus-themes-get-themes)
;; => (ef-winter ef-tritanopia-light ef-tritanopia-dark ef-trio-light ef-trio-dark ef-symbiosis ef-summer ef-spring ef-rosa ef-reverie ef-owl ef-night ef-melissa-light ef-melissa-dark ef-maris-light ef-maris-dark ef-light ef-kassio ef-frost ef-elea-light ef-elea-dark ef-eagle ef-duo-light ef-duo-dark ef-dream ef-deuteranopia-light ef-deuteranopia-dark ef-day ef-dark ef-cyprus ef-cherie ef-bio ef-autumn ef-arbutus)

Since all the Modus functions are redesigned to work with this generic function, we can now use commands like modus-themes-select or even modus-themes-list-colors for any of those themes.

As a bonus, we can now seamlessly blend Modus themes with their derivatives. Imagine a user who wants to invoke the command modus-themes-load-random (or its variants for light and dark themes) and have it consider the likes of modus-operandi and ef-dream. Users can opt in to this feature via the minor mode that the Modus themes provide called modus-themes-include-derivatives-mode. It is the same ideas as the minor mode for the Ef themes, mentioned above:

(define-minor-mode modus-themes-include-derivatives-mode
  "When enabled, all Modus themes commands cover derivatives as well.
Otherwise, they only consider the `modus-themes-items'.

Derivative theme projects can implement the equivalent of this minor
mode plus a method for `modus-themes-get-themes' to filter themes
accordingly."
  :global t
  :init-value nil)

(cl-defmethod modus-themes-get-themes (&context (modus-themes-include-derivatives-mode (eql t)))
  (modus-themes-get-all-known-themes nil))

This is what happens when I load both the modus-themes and the ef-themes and enable this “all good ones fit” minor mode:

(modus-themes-include-derivatives-mode 1)

(modus-themes-get-themes)
;; => (modus-operandi modus-operandi-tinted modus-operandi-deuteranopia modus-operandi-tritanopia modus-vivendi modus-vivendi-tinted modus-vivendi-deuteranopia modus-vivendi-tritanopia ef-winter ef-tritanopia-light ef-tritanopia-dark ef-trio-light ef-trio-dark ef-symbiosis ef-summer ef-spring ef-rosa ef-reverie ef-owl ef-night ef-melissa-light ef-melissa-dark ef-maris-light ef-maris-dark ef-light ef-kassio ef-frost ef-elea-light ef-elea-dark ef-eagle ef-duo-light ef-duo-dark ef-dream ef-deuteranopia-light ef-deuteranopia-dark ef-day ef-dark ef-cyprus ef-cherie ef-bio ef-autumn ef-arbutus)

And when I no longer want to include everything, I just disable the minor mode:

(modus-themes-include-derivatives-mode -1)

(modus-themes-get-themes)
;; => (modus-operandi modus-operandi-tinted modus-operandi-deuteranopia modus-operandi-tritanopia modus-vivendi modus-vivendi-tinted modus-vivendi-deuteranopia modus-vivendi-tritanopia)

It is a thing of beauty!

Finalising the implementation details

I am still experimenting with some of the technicalities involved. In principle, derivative themes will (i) depend on the modus-themes, (ii) define each of their themes using the modus-themes-theme macro, and (iii) specify how/when they affect the behaviour of the generic function modus-themes-get-themes.

The code I am working on will soon be available in the respective main branch of modus-themes.git and ef-themes.git. I think this gives us the tools to realise the full potential of the Modus themes.

Finally, it is not just package authors that can benefit from this development. Users may also curate their themes with something as basic as this:

(cl-defmethod modus-themes-get-themes ()
  '(modus-operandi ef-eagle modus-vivendi-tinted ef-melissa-dark))

(modus-themes-get-themes)
;; => (modus-operandi ef-eagle modus-vivendi-tinted ef-melissa-dark)

In this method, there is no function involved for returning a list of themes nor an opt-in clause. It simply hardcodes a list of themes. The point is that it works! The approach with the minor mode will usually be better and is easy enough. It is all a matter of empowering personal preference, which is the Emacs-y outlook, after all. I expect users to define their own collections, as they see fit.

Have fun!

About the Modus themes

Highly accessible themes, conforming with the highest standard for colour contrast between background and foreground values (WCAG AAA). They also are optimised for users with red-green or blue-yellow colour deficiency.

The themes are very customisable and provide support for a wide range of packages. Their manual is detailed so that new users can get started, while it also provides custom code for all sorts of more advanced customisations.

Since August 2020, the original Modus themes (modus-operandi, modus-vivendi) are built into Emacs version 28 or higher. Emacs 28 ships with modus-themes version 1.6.0. Emacs 29 includes version 3.0.0. Emacs 30 provides version 4.4.0. Version 4 is a major refactoring of how the themes are implemented and customised. Such major versions are not backward-compatible due to the limited resources at my disposal to support multiple versions of Emacs and of the themes across the years.

About the Ef themes

The ef-themes are a collection of light and dark themes for GNU Emacs that provide colourful (“pretty”) yet legible options for users who want something with a bit more flair than the modus-themes (also designed by me).