Screenshot showing both a light and dark theme applied.

I’m a dark theme nerd. Partially because of the hacker aesthetic, but mostly because I prefer to work in dark environments and light themes turn computer screens into floodlights.

However, in well lit rooms (or when working outdoors) dark themes become completely unreadable. Such is the case in my office during the morning hours. Naturally, I figured out how to change the system theme from the command line, immediately updating running GTK and Qt applications. The next step was getting Emacs to respond to the same event.

Theme Switching and D-Bus

Linux and other Unix-like operating systems use D-Bus for interprocess communication. Applications can use D-Bus to register methods and properties which can be accessed and monitored from any D-Bus client. For example, to find out whether the system is currently using a light or dark theme, call the org.freedesktop.portal.Settings.Read method from the command line using the dbus-send tool:

dbus-send --session --print-reply \
  --dest=org.freedesktop.portal.Desktop \
  /org/freedesktop/portal/desktop \
  org.freedesktop.portal.Settings.Read \
  string:org.freedesktop.appearance string:color-scheme

This command will print 1 when the system is using a dark theme and 2 when using a light theme (plus some other stuff you can ignore).

With that information in hand we need to get Emacs to do two things:

  1. Call this method when starting up and set its theme to match the system, and

  2. Monitor the color-scheme property and change its theme automatically when the system theme changes.

Emacs Groundwork

In order to get Emacs to change its theme we’ll need a way to store our preferred dark and light themes, and a way to change the current theme based on the results of a D-Bus call:

(defvar pjones:dark-theme 'ef-maris-dark
  "Default dark theme.")

(defvar pjones:light-theme 'ef-maris-light
  "Default light theme.")

(defun pjones:theme-from-dbus (value)
  "Change the theme based on a D-Bus property.

VALUE should be an integer or an arbitrarily nested list that
contains an integer.  When VALUE is equal to 2 then a light theme
will be selected, otherwise a dark theme will be selected."
  (load-theme (if (= 2 (car (flatten-list value)))
                  pjones:light-theme
                pjones:dark-theme)
              t))

You might be wondering what flatten-list is for. It turns out that D-Bus gives two different types of answers depending on how you requested a property. When invoking a D-Bus method, the result comes back to Emacs as a list. For example, when requesting the current color scheme using the Read method the response will be (1) or (2) depending on whether the current system theme is dark or light.

However, when the property changes and D-Bus notifies clients that are listening for change events, Emacs sees the property as a nested list. In this case the color scheme value will be ((1)) or ((2)). The call to flatten-list ensures that both of these situations look the same to the callback function (pjones:theme-from-dbus).

The only thing left is to convince Emacs to set its theme using information from D-Bus.

Using D-Bus from within Emacs

It should come as no surprise that Emacs ships with its own D-Bus library. To find out what the current color scheme is we can call the Read method using dbus-call-method-asynchronously. And then to be notified when the system theme changes we can use dbus-register-signal to be notified when the SettingChanged event is emitted. The arguments to these functions are a bit crazy but they mirror those given to the dbus-send tool.

(require 'dbus)

;; Set the current theme based on what the system theme is right now:
(dbus-call-method-asynchronously
   :session "org.freedesktop.portal.Desktop"
   "/org/freedesktop/portal/desktop"
   "org.freedesktop.portal.Settings"
   "Read"
   #'pjones:theme-from-dbus
   "org.freedesktop.appearance"
   "color-scheme")

;; Register to be notified when the system theme changes:
(dbus-register-signal
   :session "org.freedesktop.portal.Desktop"
   "/org/freedesktop/portal/desktop"
   "org.freedesktop.portal.Settings"
   "SettingChanged"
   (lambda (path var value)
     (when (and (string-equal path "org.freedesktop.appearance")
                (string-equal var "color-scheme"))
       (pjones:theme-from-dbus value))))

That’s it. Switching the system color scheme should now cause Emacs to update its theme too. And when Emacs starts it should automatically set its theme to match the system theme.

Command Line Theme Switching

I mentioned earlier that I figured out how to change the system theme using the command line. The most reliable way that I have found so far is to use gsettings, which also emits the SettingChanged event via D-Bus:

gsettings set org.gnome.desktop.interface color-scheme prefer-dark
gsettings set org.gnome.desktop.interface gtk-theme Adwaita-dark

# or

gsettings set org.gnome.desktop.interface color-scheme prefer-light
gsettings set org.gnome.desktop.interface gtk-theme Adwaita

The second invocation of gsettings (changing the GTK theme) isn’t strictly necessary. However, I configured Qt to use the GTK theme and this is the only way I could get Qt applications to also change their color scheme as they don’t appear to respect the color-scheme setting.

I also wrapped these commands up in XDG desktop entries so I could invoke them from tools like Rofi.

Appendix: GTK and Qt Themes

It probably matters how I configured GTK and Qt, although these don’t have anything to do with Emacs. Basically, GTK is set to use the Adwaita-dark theme, and Qt is configured to use its gtk3 engine which should follow the current GTK theme. This works for almost all Qt applications, although some refuse to update to the light theme.

All of my GTK/Qt settings are done using Home Manager in this file.