Last active
April 30, 2025 16:02
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;;; 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