ff4edaab89
Users can use this to configure system-wide themes, icons, fonts, etc. * exwm-xsettings.el: Implement the XSETTINGS protocol (fixes https://github.com/ch11ng/exwm/issues/876)
400 lines
16 KiB
EmacsLisp
400 lines
16 KiB
EmacsLisp
;;; exwm-xsettings.el --- XSETTINGS Module for EXWM -*- lexical-binding: t -*-
|
|
|
|
;; Copyright (C) 2022-2024 Free Software Foundation, Inc.
|
|
|
|
;; Author: Steven Allen <steven@stebalien.com>
|
|
|
|
;; This file is part of GNU Emacs.
|
|
|
|
;; GNU Emacs is free software: you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
|
|
;; GNU Emacs is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; Implements the XSETTINGS protocol, allowing Emacs to manage the system theme,
|
|
;; fonts, icons, etc.
|
|
;;
|
|
;; This package can be configured as follows:
|
|
;;
|
|
;; (require 'exwm-xsettings)
|
|
;; (setq exwm-xsettings-theme '("Adwaita" . "Adwaita-dark") ;; light/dark
|
|
;; exwm-xsettings `(("Xft/HintStyle" . "hintslight")
|
|
;; ("Xft/RGBA" . "rgb")
|
|
;; ("Xft/lcdfilter" . "lcddefault")
|
|
;; ("Xft/Antialias" . 1)
|
|
;; ;; DPI is in 1024ths of an inch, so this is a DPI of
|
|
;; ;; 144, equivalent to ;; a scaling factor of 1.5
|
|
;; ;; (144 = 1.5 * 96).
|
|
;; ("Xft/DPI" . ,(* 144 1024))
|
|
;; ("Xft/Hinting" . 1)))
|
|
;; (exwm-xsettings-enable)
|
|
;;
|
|
;; To modify these settings at runtime, customize them with
|
|
;; `custom-set-variables' or `setopt' (Emacs 29+). E.g., the following will
|
|
;; immediately change the icon theme to "Papirus" at runtime, even in running
|
|
;; applications:
|
|
;;
|
|
;; (setopt exwm-xsettings-icon-theme "Papirus")
|
|
|
|
;;; Code:
|
|
|
|
(require 'xcb-ewmh)
|
|
(require 'xcb-icccm)
|
|
|
|
(require 'exwm-core)
|
|
|
|
(defvar exwm-xsettings--connection nil)
|
|
(defvar exwm-xsettings--XSETTINGS_SETTINGS-atom nil)
|
|
(defvar exwm-xsettings--XSETTINGS_S0-atom nil)
|
|
(defvar exwm-xsettings--selection-owner-window nil)
|
|
|
|
(defun exwm-xsettings--rgba-match (_widget value)
|
|
"Return t if VALUE is a valid RGBA color."
|
|
(and (numberp value) (<= 0 value 1)))
|
|
|
|
(defun exwm-xsettings--custom-set (symbol value)
|
|
"Setter used by `exwm-xsettings' customization options.
|
|
|
|
SYMBOL is the setting being updated and VALUE is the new value."
|
|
(set-default-toplevel-value symbol value)
|
|
(exwm-xsettings--update-settings))
|
|
|
|
(defcustom exwm-xsettings nil
|
|
"Custom XSETTINGS.
|
|
These settings take precedence over `exwm-xsettings-theme' and
|
|
`exwm-xsettings-icon-theme'."
|
|
:group 'exwm
|
|
:type '(alist :key-type (string :tag "Name")
|
|
:value-type (choice :tag "Value"
|
|
(string :tag "String")
|
|
(integer :tag "Integer")
|
|
(list :tag "Color"
|
|
(number :tag "Red"
|
|
:type-error
|
|
"This field should contain a number between 0 and 1."
|
|
:match exwm-xsettings--rgba-match)
|
|
(number :tag "Green"
|
|
:type-error
|
|
"This field should contain a number between 0 and 1."
|
|
:match exwm-xsettings--rgba-match)
|
|
(number :tag "Blue"
|
|
:type-error
|
|
"This field should contain a number between 0 and 1."
|
|
:match exwm-xsettings--rgba-match)
|
|
(number :tag "Alpha"
|
|
:type-error
|
|
"This field should contain a number between 0 and 1."
|
|
:match exwm-xsettings--rgba-match
|
|
:value 1.0))))
|
|
:initialize #'custom-initialize-default
|
|
:set #'exwm-xsettings--custom-set)
|
|
|
|
(defcustom exwm-xsettings-theme nil
|
|
"The system-wide theme."
|
|
:group 'exwm
|
|
:type '(choice (string :tag "Theme")
|
|
(cons (string :tag "Light Theme")
|
|
(string :tag "Dark Theme")))
|
|
:initialize #'custom-initialize-default
|
|
:set #'exwm-xsettings--custom-set)
|
|
|
|
(defcustom exwm-xsettings-icon-theme nil
|
|
"The system-wide icon theme."
|
|
:group 'exwm
|
|
:type '(choice (string :tag "Icon Theme")
|
|
(cons (string :tag "Light Icon Theme")
|
|
(string :tag "Dark Icon Theme")))
|
|
:initialize #'custom-initialize-default
|
|
:set #'exwm-xsettings--custom-set)
|
|
|
|
(defvar exwm-xsettings--serial 0)
|
|
|
|
(defconst xcb:xsettings:-Type:Integer 0)
|
|
(defconst xcb:xsettings:-Type:String 1)
|
|
(defconst xcb:xsettings:-Type:Color 2)
|
|
|
|
(defclass xcb:xsettings:-Settings
|
|
(xcb:-struct)
|
|
((byte-order :initarg :byte-order :type xcb:CARD8)
|
|
(pad~0 :initform 3 :type xcb:-pad)
|
|
(serial :initarg :serial :type xcb:CARD32)
|
|
(settings-len :initarg :settings-len :type xcb:CARD32)
|
|
(settings~ :initform
|
|
'(name settings type xcb:xsettings:-SETTING size
|
|
(xcb:-fieldref 'settings-len))
|
|
:type xcb:-list)
|
|
(settings :initarg :settings :type xcb:-ignore)))
|
|
|
|
(defclass xcb:xsettings:-SETTING
|
|
(xcb:-struct)
|
|
((type :initarg :type :type xcb:CARD8)
|
|
(pad~0 :initform 1 :type xcb:-pad)
|
|
(name-len :initarg :name-len :type xcb:CARD16)
|
|
(name~ :initform
|
|
'(name name type xcb:char size
|
|
(xcb:-fieldref 'name-len))
|
|
:type xcb:-list)
|
|
(name :initarg :name :type xcb:-ignore)
|
|
(pad~1 :initform 4 :type xcb:-pad-align)
|
|
(last-change-serial :initarg :last-change-serial :type xcb:CARD32)))
|
|
|
|
(defclass xcb:xsettings:-SETTING_INTEGER
|
|
(xcb:xsettings:-SETTING)
|
|
((type :initform 'xcb:xsettings:-Type:Integer)
|
|
(value :initarg :value :type xcb:INT32)))
|
|
|
|
(defclass xcb:xsettings:-SETTING_STRING
|
|
(xcb:xsettings:-SETTING)
|
|
((type :initform 'xcb:xsettings:-Type:String)
|
|
(value-len :initarg :value-len :type xcb:CARD32)
|
|
(value~ :initform
|
|
'(name value type xcb:char size
|
|
(xcb:-fieldref 'value-len))
|
|
:type xcb:-list)
|
|
(value :initarg :value :type xcb:-ignore)
|
|
(pad~2 :initform 4 :type xcb:-pad-align)))
|
|
|
|
(defclass xcb:xsettings:-SETTING_COLOR
|
|
(xcb:xsettings:-SETTING)
|
|
((type :initform 'xcb:xsettings:-Type:Color)
|
|
(red :initarg :red :type xcb:CARD16)
|
|
(green :initarg :green :type xcb:CARD16)
|
|
(blue :initarg :blue :type xcb:CARD16)
|
|
(alpha :initarg :alpha :initform #xffff :type xcb:CARD16)))
|
|
|
|
(defclass xcb:xsettings:-ClientMessage
|
|
(xcb:icccm:--ClientMessage xcb:ClientMessage)
|
|
((format :initform 32)
|
|
(type :initform 'xcb:Atom:MANAGER)
|
|
(time :initarg :time :type xcb:TIMESTAMP) ;new slot
|
|
(selection :initarg :selection :type xcb:ATOM) ;new slot
|
|
(owner :initarg :owner :type xcb:WINDOW)) ;new slot
|
|
:documentation "An XSETTINGS client message.")
|
|
|
|
(defalias 'exwm-xsettings--color-dark-p
|
|
(if (eval-when-compile (< emacs-major-version 29))
|
|
;; Borrowed from Emacs 29.
|
|
(lambda (rgb)
|
|
"Whether RGB is more readable against white than black."
|
|
(unless (<= 0 (apply #'min rgb) (apply #'max rgb) 1)
|
|
(error "RGB components %S not in [0,1]" rgb))
|
|
(let* ((r (expt (nth 0 rgb) 2.2))
|
|
(g (expt (nth 1 rgb) 2.2))
|
|
(b (expt (nth 2 rgb) 2.2))
|
|
(y (+ (* r 0.2126) (* g 0.7152) (* b 0.0722))))
|
|
(< y 0.325)))
|
|
'color-dark-p))
|
|
|
|
(defun exwm-xsettings--pick-theme (theme)
|
|
"Pick a light or dark theme from the given THEME.
|
|
If THEME is a string, it's returned directly.
|
|
If THEME is a cons of (LIGHT . DARK), the appropriate theme is picked based on
|
|
the default face's background color."
|
|
(pcase theme
|
|
((cl-type string) theme)
|
|
(`(,(cl-type string) . ,(cl-type string))
|
|
(if (exwm-xsettings--color-dark-p (color-name-to-rgb (face-background 'default)))
|
|
(cdr theme) (car theme)))
|
|
(_ (error "Expected theme to be a string or a pair of strings"))))
|
|
|
|
(defun exwm-xsettings--get-settings ()
|
|
"Get the current settings.
|
|
Combines `exwm-xsettings', `exwm-xsettings-theme' (if set), and
|
|
`exwm-xsettings-icon-theme' (if set)."
|
|
(cl-remove-duplicates
|
|
(append
|
|
exwm-xsettings
|
|
(when exwm-xsettings-theme
|
|
(list (cons "Net/ThemeName" (exwm-xsettings--pick-theme exwm-xsettings-theme))))
|
|
(when exwm-xsettings-icon-theme
|
|
(list (cons "Net/IconThemeName" (exwm-xsettings--pick-theme exwm-xsettings-icon-theme)))))
|
|
:key 'car
|
|
:test 'string=))
|
|
|
|
(defun exwm-xsettings--make-settings (settings serial)
|
|
"Construct a new settings object.
|
|
SETTINGS is an alist of key/value pairs.
|
|
SERIAL is a sequence number."
|
|
(make-instance 'xcb:xsettings:-Settings
|
|
:byte-order (if xcb:lsb 0 1)
|
|
:serial serial
|
|
:settings-len (length settings)
|
|
:settings
|
|
(mapcar
|
|
(lambda (prop)
|
|
(let* ((name (car prop))
|
|
(value (cdr prop))
|
|
(common (list :name name
|
|
:name-len (length name)
|
|
:last-change-serial serial)))
|
|
(pcase value
|
|
((cl-type string)
|
|
(apply #'make-instance 'xcb:xsettings:-SETTING_STRING
|
|
:value-len (length value)
|
|
:value value
|
|
common))
|
|
((cl-type integer)
|
|
(apply #'make-instance 'xcb:xsettings:-SETTING_INTEGER
|
|
:value value common))
|
|
((and (cl-type list) (app length (or 3 4)))
|
|
;; Convert from RGB(A) to 16bit integers.
|
|
(setq value (mapcar (lambda (x) (round (* x #xffff))) value))
|
|
(apply #'make-instance 'xcb:xsettings:-SETTING_COLOR
|
|
:red (pop value)
|
|
:green (pop value)
|
|
:blue (pop value)
|
|
:alpha (or (pop value) #xffff)))
|
|
(_ (error "Setting value must be a string, integer, or length 3-4 list")))))
|
|
settings)))
|
|
|
|
(defun exwm-xsettings--update-settings ()
|
|
"Update the xsettings."
|
|
(when exwm-xsettings--connection
|
|
(setq exwm-xsettings--serial (1+ exwm-xsettings--serial))
|
|
(let* ((settings (exwm-xsettings--get-settings))
|
|
(bytes (xcb:marshal (exwm-xsettings--make-settings settings exwm-xsettings--serial))))
|
|
(xcb:+request exwm-xsettings--connection
|
|
(make-instance 'xcb:ChangeProperty
|
|
:mode xcb:PropMode:Replace
|
|
:window exwm-xsettings--selection-owner-window
|
|
:property exwm-xsettings--XSETTINGS_SETTINGS-atom
|
|
:type exwm-xsettings--XSETTINGS_SETTINGS-atom
|
|
:format 8
|
|
:data-len (length bytes)
|
|
:data bytes)))
|
|
(xcb:flush exwm-xsettings--connection)))
|
|
|
|
(defun exwm-xsettings--on-theme-change (&rest _)
|
|
"Called when the Emacs theme is changed."
|
|
;; We only bother updating the xsettings if changing the theme could effect
|
|
;; the settings.
|
|
(when (or (consp exwm-xsettings-theme) (consp exwm-xsettings-icon-theme))
|
|
(exwm-xsettings--update-settings)))
|
|
|
|
(defun exwm-xsettings--on-SelectionClear (_data _synthetic)
|
|
"Called when another xsettings daemon takes over."
|
|
(exwm--log "XSETTINGS manager has been replaced.")
|
|
(exwm-xsettings--exit))
|
|
|
|
(cl-defun exwm-xsettings--init ()
|
|
"Initialize the XSETTINGS module."
|
|
(exwm--log)
|
|
|
|
(cl-assert (not exwm-xsettings--connection))
|
|
|
|
;; Connect
|
|
(setq exwm-xsettings--connection (xcb:connect))
|
|
(set-process-query-on-exit-flag (slot-value exwm-xsettings--connection
|
|
'process)
|
|
nil)
|
|
|
|
;; Intern the atoms.
|
|
(setq exwm-xsettings--XSETTINGS_SETTINGS-atom
|
|
(exwm--intern-atom "_XSETTINGS_SETTINGS" exwm-xsettings--connection)
|
|
|
|
exwm-xsettings--XSETTINGS_S0-atom
|
|
(exwm--intern-atom "_XSETTINGS_S0" exwm-xsettings--connection))
|
|
|
|
;; Detect running XSETTINGS managers.
|
|
(with-slots (owner)
|
|
(xcb:+request-unchecked+reply exwm-xsettings--connection
|
|
(make-instance 'xcb:GetSelectionOwner
|
|
:selection exwm-xsettings--XSETTINGS_S0-atom))
|
|
(when (/= owner xcb:Window:None)
|
|
(xcb:disconnect exwm-xsettings--connection)
|
|
(setq exwm-xsettings--connection nil)
|
|
(warn "[EXWM] Other XSETTINGS manager detected")
|
|
(cl-return-from exwm-xsettings--init)))
|
|
|
|
|
|
(let ((id(xcb:generate-id exwm-xsettings--connection)))
|
|
(setq exwm-xsettings--selection-owner-window id)
|
|
|
|
;; Create a settings window.
|
|
(xcb:+request exwm-xsettings--connection
|
|
(make-instance 'xcb:CreateWindow
|
|
:wid id
|
|
:parent exwm--root
|
|
:class xcb:WindowClass:InputOnly
|
|
:x 0
|
|
:y 0
|
|
:width 1
|
|
:height 1
|
|
:border-width 0
|
|
:depth 0
|
|
:visual 0
|
|
:value-mask xcb:CW:OverrideRedirect
|
|
:override-redirect 1))
|
|
|
|
;; Set _NET_WM_NAME.
|
|
(xcb:+request exwm-xsettings--connection
|
|
(make-instance 'xcb:ewmh:set-_NET_WM_NAME
|
|
:window id
|
|
:data "EXWM: exwm-xsettings--selection-owner-window"))
|
|
|
|
;; Apply the XSETTINGS properties.
|
|
(exwm-xsettings--update-settings)
|
|
|
|
;; Take ownership and notify.
|
|
(xcb:+request exwm-xsettings--connection
|
|
(make-instance 'xcb:SetSelectionOwner
|
|
:owner id
|
|
:selection exwm-xsettings--XSETTINGS_S0-atom
|
|
:time xcb:Time:CurrentTime))
|
|
(xcb:+request exwm-xsettings--connection
|
|
(make-instance 'xcb:SendEvent
|
|
:propagate 0
|
|
:destination exwm--root
|
|
:event-mask xcb:EventMask:StructureNotify
|
|
:event (xcb:marshal
|
|
(make-instance 'xcb:xsettings:-ClientMessage
|
|
:window exwm--root
|
|
:time xcb:Time:CurrentTime
|
|
:selection exwm-xsettings--XSETTINGS_S0-atom
|
|
:owner id)
|
|
exwm-xsettings--connection)))
|
|
|
|
;; Detect loss of XSETTINGS ownership.
|
|
(xcb:+event exwm-xsettings--connection 'xcb:SelectionClear
|
|
#'exwm-xsettings--on-SelectionClear)
|
|
|
|
(xcb:flush exwm-xsettings--connection))
|
|
|
|
;; Update the xsettings if/when the theme changes.
|
|
(add-hook 'enable-theme-functions #'exwm-xsettings--on-theme-change)
|
|
(add-hook 'disable-theme-functions #'exwm-xsettings--on-theme-change))
|
|
|
|
(defun exwm-xsettings--exit ()
|
|
"Exit the XSETTINGS module."
|
|
(exwm--log)
|
|
|
|
(when exwm-xsettings--connection
|
|
(remove-hook 'enable-theme-functions #'exwm-xsettings--on-theme-change)
|
|
(remove-hook 'disable-theme-functions #'exwm-xsettings--on-theme-change)
|
|
|
|
(xcb:disconnect exwm-xsettings--connection)
|
|
|
|
(setq exwm-xsettings--connection nil
|
|
exwm-xsettings--XSETTINGS_SETTINGS-atom nil
|
|
exwm-xsettings--XSETTINGS_S0-atom nil
|
|
exwm-xsettings--selection-owner-window nil)))
|
|
|
|
(defun exwm-xsettings-enable ()
|
|
"Enable xsettings support for EXWM."
|
|
(exwm--log)
|
|
(add-hook 'exwm-init-hook #'exwm-xsettings--init)
|
|
(add-hook 'exwm-exit-hook #'exwm-xsettings--exit))
|
|
|
|
(provide 'exwm-xsettings)
|
|
|
|
;;; exwm-xsettings.el ends here
|