Implement the XSETTINGS protocol
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)
This commit is contained in:
parent
c573ff143a
commit
ff4edaab89
1 changed files with 400 additions and 0 deletions
400
exwm-xsettings.el
Normal file
400
exwm-xsettings.el
Normal file
|
@ -0,0 +1,400 @@
|
|||
;;; 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
|
Loading…
Reference in a new issue