
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:
-
Call this method when starting up and set its theme to match the system, and
-
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.