Skip to content

Instantly share code, notes, and snippets.

@wroyca
Last active April 30, 2025 16:02
Show Gist options
  • Save wroyca/4120cac8838b73b216681f06835ee054 to your computer and use it in GitHub Desktop.
Save wroyca/4120cac8838b73b216681f06835ee054 to your computer and use it in GitHub Desktop.
Automatic synchronization between Emacs themes and system appearance settings (light/dark mode) via D-Bus.
;;; appearance.el --- System theme integration via D-Bus -*- lexical-binding: t -*-
;;; Commentary:
;;
;; This module provides automatic synchronization between Emacs themes
;; and the system appearance settings (light/dark mode) via D-Bus.
;;
;; NOTE: This *only* works with desktop environments that support the
;; org.freedesktop.appearance portal interface.
;;; Code:
(require 'dbus)
(defgroup appearance nil
"System appearance synchronization for Emacs."
:group 'faces
:prefix "appearance-")
(defcustom appearance-light-theme 'modus-operandi-tinted
"Theme to use when system is using light mode."
:type 'symbol
:group 'appearance)
(defcustom appearance-dark-theme 'modus-vivendi-tinted
"Theme to use when system is using dark mode."
:type 'symbol
:group 'appearance)
(defconst appearance--color-scheme-light 2
"Value representing light color scheme from freedesktop portal.")
(defconst appearance--color-scheme-dark 1
"Value representing dark color scheme from freedesktop portal.")
(defconst appearance--color-scheme-default 0
"Value representing default (usually light) color scheme from freedesktop portal.")
(defvar appearance--current-theme nil
"Currently active theme set by appearance module.")
;;;###autoload
(defun appearance-parse-color-scheme (value)
"Apply appropriate theme based on system color scheme VALUE.
VALUE is an integer as defined by the freedesktop.org appearance portal:
- 0: Default (typically light)
- 1: Dark preference
- 2: Light preference"
(let ((theme (cond
((or (= value appearance--color-scheme-light)
(= value appearance--color-scheme-default))
(prog1 appearance-light-theme
(set-frame-parameter nil 'background-mode 'light)))
(t
(prog1 appearance-dark-theme
(set-frame-parameter nil 'background-mode 'dark))))))
;; Clean transition: unload previous theme only if different from target
(when (and appearance--current-theme
(not (eq appearance--current-theme theme))
(custom-theme-enabled-p appearance--current-theme))
(disable-theme appearance--current-theme))
(unless (custom-theme-enabled-p theme)
(load-theme theme t))
(setq appearance--current-theme theme)
;; Ensure background-mode propagates to all frames
(frame-set-background-mode nil)))
;;;###autoload
(defun appearance-setup-sync ()
"Establish D-Bus connections for system appearance monitoring.
Creates two communication channels with the desktop portal:
1. Initial value retrieval to sync current system state
2. Signal registration for change notifications"
(if (not (featurep 'dbusbind))
(message "D-Bus support not available, cannot sync system appearance")
(condition-case err
(progn
;; Asynchronously retrieve current system preference to establish initial state
(dbus-call-method-asynchronously
:session
"org.freedesktop.portal.Desktop"
"/org/freedesktop/portal/desktop"
"org.freedesktop.portal.Settings"
"Read"
(lambda (value)
(when value
(appearance-parse-color-scheme (car (car value)))))
"org.freedesktop.appearance"
"color-scheme")
;; Monitor for system preference changes in real-time
(dbus-register-signal
:session
"org.freedesktop.portal.Desktop"
"/org/freedesktop/portal/desktop"
"org.freedesktop.portal.Settings"
"SettingChanged"
(lambda (namespace key variant)
(when (and (string-equal namespace "org.freedesktop.appearance")
(string-equal key "color-scheme"))
(let ((color-scheme (car variant)))
(when color-scheme
(appearance-parse-color-scheme color-scheme)))))))
(error
(message "Failed to set up appearance synchronization: %s" (error-message-string err))))))
;;;###autoload
(defun appearance-toggle-theme ()
"Toggle between light and dark themes manually."
(interactive)
(if (eq appearance--current-theme appearance-light-theme)
(appearance-parse-color-scheme appearance--color-scheme-dark)
(appearance-parse-color-scheme appearance--color-scheme-light)))
;;;###autoload
(define-minor-mode appearance-sync-mode
"Toggle automatic synchronization with system appearance.
When enabled, Emacs will switch between light and dark themes
based on the system appearance settings."
:global t
:init-value nil
:lighter " AppSync"
(if appearance-sync-mode
(appearance-setup-sync)
(when appearance--current-theme
(message "Appearance sync disabled, theme remains %s" appearance--current-theme))))
(appearance-sync-mode 1)
(provide 'appearance)
;;; appearance.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment