Let Emacs Follow System Theme

Posted on Feb 7, 2024

Lately, I've started using Emacs again. This time, however, I intend to use it for a longer time. Previously I used Doom Emacs and Spacemacs and I even dabbled a bit in using Vanilla Emacs, but they never quite stuck. What changed it for me is the excellent Mastering Emacs book by Mickey Petersen. That got me enthusiastic again and, for better or worse, I started using the text editor again.

Most of my config is not that interesting, but I am quite proud of the way in which I enabled synchronization between Emacs's colorscheme and the system color scheme (I'm using Gnome at the moment).

Fetching the colorscheme

Fetching the system's colorscheme turns out to be not that difficult, although you have to know where to look. In this case, we can use the builtin dbus package to get the information that we want. To get the current color scheme we need this elisp code:

  (require 'dbus)
  (let ((current-theme (caar (dbus-ignore-errors
			       (dbus-call-method
				:session
				"org.freedesktop.portal.Desktop"
				"/org/freedesktop/portal/desktop"
				"org.freedesktop.portal.Settings" "Read"
				"org.freedesktop.appearance" "color-scheme")))))
    (cond
     ((eq 0 current-theme)
      (setq default-frame-alist '((fullscreen . maximized)
				  (background-color . "#ffffff"))))
     ((eq 1 current-theme)
      (setq default-frame-alist '((fullscreen . maximized)
				  (ns-appearance .dark)
				  (background-color . "#292b2e"))))))

We first require the dbus package and then we use a let expression to use the variable current-theme locally. Then we call dbus-ignore-errors with the appropriate bus (:session), service ("org.freedesktop.portal.Desktop"), path ("org.freedesktop.portal.Desktop"), interface ("org.freedesktop.portal.Settings") and method ("Read").

The final two strings are the arguments to the Read method. We take that output, and extract it twice with the caar expression to get an integer. If the integer is 0, the light theme is enabled, and the dark theme if it is 1. I wrote a cond expression to set the background color accordingly and put this code in my early-init.el file. That sets the theme to relatively dark, before the actual (Spacemacs) theme is loaded.

Changing the colorscheme on the fly

Now that we have set the color in the good direction, we also want to change the colorscheme whenever the system's theme changes. For that, we use the following code:

  (defun theme--handle-dbus-event (_ setting values)
    "Handler for FreeDesktop theme changes."
    (when (string= setting "color-scheme")
      (let ((scheme (car values)))
	(cond
	 ((not (stringp scheme))
	  ())
	 ((string-match-p "prefer-dark" scheme)
	  (load-theme 'spacemacs-dark t))
	 ((string-match-p "default" scheme)
	  (load-theme 'spacemacs-light t))))))

  (require 'dbus)

  (dbus-register-signal :session
			"org.freedesktop.portal"
			"/org/freedesktop/portal/desktop"
			"org.freedesktop.impl.portal.Settings"
			"SettingChanged"
			#'theme--handle-dbus-event)

  (let ((current-theme (caar (dbus-ignore-errors
			       (dbus-call-method
				:session
				"org.freedesktop.portal.Desktop"
				"/org/freedesktop/portal/desktop"
				"org.freedesktop.portal.Settings" "Read"
				"org.freedesktop.appearance" "color-scheme")))))
    (cond
     ((eq 0 current-theme)
      (load-theme 'spacemacs-light t))
     ((eq 1 current-theme)
      (load-theme 'spacemacs-dark t))))

First, we have the function theme--handle-dbus-event, which takes three values. We ignore the first one. Then we check if the setting is equal to "color-scheme". If that is the case, we check if the value of scheme is "prefer-dark" or "default" and change the color scheme accordingly.

We register this function with the dbus-register-signal, where we now take "SettingChanged" (instead of "Read" from the previous code block) and we register our nice color scheme function to this dbus signal.

Then finally, we have a bit of a copy of the previous code block to set our theme to the actual Spacemacs themes. Here is it working in action!