Migrating to modus-themes v4 and Going Through the Changes

New year marks the day Protesilaos releases modus-themes v4 into the wild; and my package update from MELPA already ingested preliminary changes on Friday Dec 30th (much to my chagrin, because I initially wanted to do something else than fiddling with my Emacs setup) that were absolutely not backwards compatible.

While I migrated my configuration over, I reported some issues, which were promptly fixed, and after waiting for a couple days, this post is what’s left of my v3-to-v4 migration without any temporary workarounds.

A lot of the old configuration options are gone; instead of many defvars (i.e. “state” that affects theme loading) to affect e.g. the color of code blocks, the selection region, or buffer fringes, we’re now interacting with a large option list instead and declare what we want (i.e. one data source for the truth about the theme). That change means we all need to figure out which old setting now are affected by which names color, and then sprinkle some hooks on top to use named colors for our own purposes.

This is not a migration guide by any means, but it could help you understand how the themes now work by following along. The v4 documentation is thorough, but for a first read, you need to keep a lot of stuff in your head. I hope this eases that a bit.

New themes, and how to load them

The modus-themes package comes with 6 themes:

  1. Light
    1. modus-operandi
    2. modus-operandi-deuteranopia
    3. modus-operandi-tinted
  2. Dark
    1. modus-vivendi
    2. modus-vivendi-deuteranopia
    3. modus-vivendi-tinted

I used to have the deuteranopia flag enabled, so I’m now picking the base theme modus-operandi-deuteranopia. Same if you were >50% using tinted variants: use modus-operandi-tinted. If in doubt, start with the default.

To declare that I want to use the deuteranopia-friendly dark and light variant e.g. when I use F5 to toggle themes, I start with:

(setq modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia))

Before loading the theme (via built-in load-theme, which replaces the old modus-themes-load-theme that did some extra work), you need to set all your configuration options so they are available during the loading process. We’ll check these out in a minute, but this here is ultimately the last line in my theme config:

;; Replaces a call to (modus-themes-load-themes)
(load-theme (car modus-themes-to-toggle) t t)

Note that I could just as well write (load-theme 'modus-operandi-deuteranopia), but this way I can treat the modus-themes-to-toggle variable as the source of truth. Hashtag DRY.

Heads up: While the load-theme call above works anytime after I’ve confirmed the theme, it refuses to load the theme for the first time. I noticed that it wouldn’t load after a fresh start of Emacs. So I ran (load-theme 'modus-operandi-deuteranopia) once and gave my approval; same for the vivendi variant. Then all is good. (I also had no luck with :no-confirm parameter, by the way.)

So that’s the basic wireframe for how I load and configure the theme. With my actual use-package call, this is the structrue:

(use-package modus-themes
  :init
  (setq modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia))
  ;; ... setting all variable that need to
  ;; be present before loading the theme ...
  :config
  (load-theme (car modus-themes-to-toggle) t t)
  :bind ("<f5>" . modus-themes-toggle))

Next, we’ll fill out the placeholder I put in the :init section’s comments.

Configuration options

The simple configuration options for me that I kept and still like:

(setq modus-themes-italic-constructs t
      modus-themes-bold-constructs t
      modus-themes-variable-pitch-ui t
      modus-themes-mixed-fonts t)

Most of the work is done by palettes and their overrides, that we’ll look at for the rest of this post. These affect all colors; but to affect bold constructs and font sizes, there are still a couple more variables that inform theme loading. These are listed as 5. Customization options in the docs. The ones I use are:

;; Color customizations
(setq modus-themes-prompts '(bold))
(setq modus-themes-completions nil)
(setq modus-themes-org-blocks 'gray-background)

;; Font sizes for titles and headings, including org
(setq modus-themes-headings '((1 . (light variable-pitch 1.5))
                              (agenda-date . (1.3))
                              (agenda-structure . (variable-pitch light 1.8))
						      (t . (medium))))

Only these simple variable settings did survive; the rest is trickier, because we need to understand the new (and very flexible and quite amazing) palettes.

Palette overrides

The “more power to the user” release comes with some cost: you need to understand how the new palettes work. Theres a whole section in the docs, 5.11 Palette Overrides: for each of the 6 themes, theres a -palette-overrides variable you can use to inject your own styles that, well, override the default settings of the respective theme. The actual code that merges overrides with the defaults is this:

;; modus-operandi-deuteranopia-theme.el
(modus-themes-theme modus-operandi-deuteranopia
                    modus-operandi-deuteranopia-palette
                    modus-operandi-deuteranopia-palette-overrides)

Think of palettes are lists of color names and colors. If you peek into the modus-operandi-deuteranopia-theme.el code, you’ll see it boils down to a very long list like this:

;; modus-operandi-deuteranopia-theme.el
(defconst modus-operandi-deuteranopia-palette
  '((bg-main          "#ffffff")
    (bg-dim           "#f0f0f0")
    ;; ...
    ))

The good news is that there are still many named colors, like bg-main. These semantic colors can be used to style other pieces of Emacs to have the modus colors.

The bad news is getting to them is a bit more cumbersome if you need their values, but it works well and consistently. I’ll show some applications of this below.

Another upside is that some “settings” are part of this list, too, usually further down, and they can reference the more basic color definitions. One example is (fg-link blue-warmer) which declared the link foreground color by referencing the blue-warmer color setting from elsewhere in the file. This replaces old settings like (setq modus-themes-links '(neutral underline)).

I didn’t use many of these, but I noticed the deprecation warnings to guide me through the process.

It’s hard to say in retrospect (as e.g. Tony Zorman pointed out) what e.g. the ‘neutrality’ setting used to do. You need to look up the hex colors and find close matches, is my guess. (After Tony’s email exchange, the v4 release candidate improved a lot, so your personal migration might go much smoother.)

For example, if you don’t like the theme/palette’s default link color and want something with less contrast, override fg-link with blue-faint instead of blue-warmer, maybe.

So how do you do that?

If you want global overrides like the old modus-themes-links setting, you use the shared palette overrides, called modus-themes-common-palette-overrides. These should not use absolute hex colors but reference named colors instead. That way, you get the proper “faint blue” for the dark and light theme.

(setq modus-themes-common-palette-overrides
      '(;; Globally use faint instead of warm blue for links
        (fg-link blue-faint)
        ;; ... more overrides here ...
        ))

If you want to tweak just one particular palette, you choose one from the 6 palette override options like I mentioned above.

Here’s an example to change the inactive background color of the light deuteranopia theme:

(customize-set-variable 'modus-operandi-deuteranopia-palette-overrides
                        '(
                          ;; More subtle gray for the inactive window and modeline
                          (bg-inactive "#efefef")
                          ))

Setting a light gray like "#efefef" wouldn’t work for the modus-vivendi variants.

Notice how I also actually use customize-set-variable, not setq: the variable modus-themes-custom-auto-reload is enabled by default, which means when I change the palette via customize-set-variable, the theme auto-reloads and shows the changes quickly. This is great to experiment with colors, so I recommend you use that, too.

At this point, we know how to

  • pick the toggle-able themes, i.e. which dark and light one F5 should switch between;
  • override color “pointers” (like fg-link) by referencing other named colors in the “common” palette;
  • override colors with absolute hex codes.

You can also use override presets (5.11.1 Palette override presets) to get back e.g. an overall “faint” look and feel. The documentation explains how to use that and even how to inherit from it while setting your own colors. Armed with that knowledge, my actual common palette settings were the following (“were”, because I removed inheriting from -faint eventually to give the new colors a real chance):

(customize-set-variable
 'modus-themes-common-palette-overrides
 `(
   ;; Make the mode-line borderless and stand out less
   (bg-mode-line-active bg-inactive)
   (fg-mode-line-active fg-main)
   (bg-mode-line-inactive bg-inactive)
   (fg-mode-line-active fg-dim)
   (border-mode-line-active bg-main)
   (border-mode-line-inactive bg-inactive)

   ;; Inherit rest from faint style
   ,@modus-themes-preset-overrides-faint))

I’ll explain my choice for the mode-line color overrides in the next section.

Depending on when you read this, this will just work – on 2022-12-30 I reported an issue that the overrides wouldn’t merge properly. See there for a quick fix; I didn’t bother to put it here again because it was already fixed by Prot within the hour, but YMMV when you try to do crazy things with the themes.

Working with modus-themes color values

There’s no convenient way to get a single named color from the themes at the moment. The modus-themes-with-colors macro is clever, but at the time of writing, the documentation is sparse and only shows how you can use it to produce a list, i.e. use the macro for reading the variable names. On the mailing list, Prot shared more examples and eventually included mode-line color overrides in the docs based on our email exchange, which I believe serves as a better example.

To understand the modus-themes-with-colors macro, you need to feel somewhat comfortable with Elisp macros because you’ll be using lists eventually and need to escape variable names for the colors inside these.

Check out these links if you need a refresher for the backquoted `(...) list form and how the comma splicing works within macro expressions:

With that, you can use theme color variables like so:

(modus-themes-with-colors
  (set-face-attribute 'tab-bar nil :background bg-main))

The macro makes all theme variables like bg-main available as locally bound variables.

The actual use within backquoted lists is a bit different, as you’ll see next.

Overriding the mode-line even more

Above, I shared my mode line color settings: commonly, I use the background color in the inactive window (to blend it in), and a light contrast in the active window. I tweaked the bg-inactive color of the light theme to be even less dark, so it’s a more subtle tonal contrast. (And IMHO less ugly.)

These changes aren’t all, though. I also apply a box to the modeline to make it appear larger (and add whitespace around the text). The box should have the appropriate color to appear like an extension of the background, so I need to reference the mode-line background colors of the theme when adding a box.

The inactive window and the mode line have the same light gray color to blend in. The mlscroll indicator is a bit stark, still, but this is WIP.

Update 2023-12-13: I’ve changed the mlscroll.el settings in the meantime and thanks to updates to the package later in 2023, I can use the scroll-bar face directly.

With an improved implementation suggested by Prot, I’m now using this:

(defun ct/modus-themes-customize-mode-line ()
  "Apply padding to mode-line via box that has the background color"
  (modus-themes-with-colors
    (custom-set-faces
     `(mode-line ((,c :box (:line-width 10 :color ,bg-mode-line-active))))
     `(mode-line-inactive ((,c :box (:line-width 10 :color ,bg-mode-line-inactive)))))))
(add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-customize-mode-line)

You might wonder what ,c means: it inlines the variable c’s value. What is c, then? Well, it’s locally bound in the modus-themes-theme function, and its value is '((class color) (min-colors 256)). This is actually scoping when the theme’s colors are applied: in a color environment with at least 256 colors available. There are no constructs in the modus-themes for e.g. terminals that only support 16 colors, but Emacs’s faces do support settings that are scoped to different environments (with t as the fallback). So this tells Emacs to only use these faces if it makes sense, and as users we need to remember to prepend this scoping variable.

Overriding tab bar faces and colors

With that tool as a good stand-in for the old modus-themes-color function, I could rewrite my tab-bar overrides to this form:

(defun ct/modus-themes-tab-bar-colors ()
  "Override tab faces to have even less variety"
  (modus-themes-with-colors
    (custom-set-faces
     `(tab-bar ((,c
                 :height 0.8
                 :background ,bg-main
                 :box nil)))
     `(tab-bar-tab ((,c
                     :background ,bg-main
                     :underline (:color ,blue-intense :style line)
                     :box (:line-width 2 :style flat-button))))
     `(tab-bar-tab-inactive ((,c
                              :background ,bg-main
                              :box (:line-width 2 :style flat-button)))))))
(add-hook 'modus-themes-after-load-theme-hook #'ct/modus-themes-tab-bar-colors)

If you recall my previous tab-bar related posts, you will notice I am now referencing the faces 'tab-bar etc. directly instead of changing modus-* tab bar colors.

There used to be custom modus-themes faces like modus-themes-tab-backdrop. These are now gone in v4. In effect, they’re “inlined” when the merged palette specificiations are applied by the modus-themes-theme call which I already showed above. It maps over a list of face names and applies their style; for tabs, this is what’s going on:

;; modus-themes.el
(defconst modus-themes-faces
  '(
    ;; ...
    `(tab-bar ((,c :inherit modus-themes-ui-variable-pitch :background ,bg-tab-bar)))
    `(tab-bar-tab-group-current ((,c :inherit bold :background ,bg-tab-current :box (:line-width -2 :color ,bg-tab-current) :foreground ,fg-alt)))
    `(tab-bar-tab-group-inactive ((,c :background ,bg-tab-bar :box (:line-width -2 :color ,bg-tab-bar) :foreground ,fg-alt)))
    ;; ...
   )
  "Face specs for use with `modus-themes-theme'.")

A downside of v4 in its current form is that I couldn’t find a way to change the :box. So directly overriding the actual faces via set-face-attribute was my best idea; and Prot’s suggestion to use custom-set-faces made this even better. (The default application of boxes to the tab bar is “opt-out”, one could say, and requires more cumbersome overrides.)

So while I could use the palette variables to change the color of tabs, I couldn’t remove the box, and I personally opted for doing all overrides in my custom function instead of setting the palette colors for bg-tab-bar and bg-tab-curent first, and then still override the box declaration later. This way, it’s all in one place and easy to revert. But that’ just my opinion.

In summary

Printed below is my current configuration of the theme itself in full. On top, there are the hook-based overrides for a couple of things: the mode-line, the tab-bar, the mlscroll indicator, and neotree fonts. I’m also using the latest HEAD revision from Prot’s git repository instead of the latest MELPA release because things are moving so fast, but commented-out the :load-path option to share it here.

(use-package modus-themes
  ;; :load-path "~/.emacs.d/src/modus-themes"
  :ensure
  :demand
  :init
  (require 'modus-themes)

  (setq modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia))
  (setq modus-themes-italic-constructs t
	    modus-themes-bold-constructs t
	    modus-themes-variable-pitch-ui t
	    modus-themes-mixed-fonts t)

  ;; Color customizations
  (setq modus-themes-prompts '(bold))
  (setq modus-themes-completions nil)
  (setq modus-themes-org-blocks 'tinted-background) ;'gray-background)

  ;; Font sizes for titles and headings, including org
  (setq modus-themes-headings '((1 . (light variable-pitch 1.5))
                                (agenda-date . (1.3))
                                (agenda-structure . (variable-pitch light 1.8))
						        (t . (medium))))
  ;; Theme overrides
  (customize-set-variable 'modus-themes-common-palette-overrides
                          `(
                            ;; Make the mode-line borderless
                            (bg-mode-line-active bg-inactive)
                            (fg-mode-line-active fg-main)
                            (bg-mode-line-inactive bg-inactive)
                            (fg-mode-line-active fg-dim)
                            (border-mode-line-active bg-inactive)
                            (border-mode-line-inactive bg-main)

                            ;; macOS Selection colors
                            (bg-region "mac:selectedTextBackgroundColor")
                            (fg-region "mac:selectedTextColor")
                            ))
  (customize-set-variable 'modus-operandi-deuteranopia-palette-overrides
                          `(
                            ;; More subtle gray for the inactive window and modeline
                            (bg-inactive "#efefef")))
  (customize-set-variable 'modus-vivendi-deuteranopia-palette-overrides
                          `(
                            ;; More subtle gray for the inactive window and modeline
                            (bg-inactive "#202020")))

  ;; Quick fix: Don't load immediately, but during after-init-hook so all other modifications further down can be prepared
  (defun ct/modus-themes-init ()
    (load-theme (car modus-themes-to-toggle)))

  :bind ("<f5>" . modus-themes-toggle)
  :hook (after-init . ct/modus-themes-init))