Files
lime/src/x11/config.lisp
2026-10-02 16:35:17 -05:00

892 lines
44 KiB
Common Lisp

(in-package #:lime.x11)
;;;; User configuration: a small declarative layer over public commands
;;;
;;; Configuration stays outside LIME.CORE. It is adapter policy which says
;;; which input invokes a command, how a freshly mapped X11 client is styled,
;;; and which optional bar providers should run. Activities, Views, leases,
;;; and saved layouts are deliberately never serialized into or reconstructed
;;; from a configuration file.
(defstruct lime-command
"One user-named imperative command shared by bindings and the palette.
ACTION is a function of one X11-BACKEND. KEY is optional and only controls
the command's entry under the palette's `User` which-key branch; it does not
create a passive X11 grab by itself."
id
label
description
category
key
action)
(defstruct configuration-binding
"A declarative passive-key binding to a LIME-COMMAND id."
key
modifiers
command-id
description)
(defstruct window-rule
"An ordered, initial-placement rule for ordinary newly managed clients.
RESOURCE-NAME, RESOURCE-CLASS, and TITLE are NIL (wildcard), a string
(case-insensitive exact match), or a predicate of one actual string-or-NIL.
PREDICATE, when supplied, receives BACKEND, CLIENT-ID, and ICCCM-CLIENT-INFO.
The first matching rule wins. The intentionally small initial action set is
FLOAT-P and CENTER-P: it never mounts a View, changes an Activity, or takes a
lease as an incidental result of a WM_CLASS match."
id
resource-name
resource-class
title
predicate
(float-p nil)
(center-p nil))
(defstruct layout-client-flow-policy
"Per-layout placement and removal policy for ordinary tiled clients.
LAYOUT-MODE is one of Lime's built-in layout modes. A configuration may
override only the modes it cares about; its ordinary global client-flow
settings remain the fallback for the others."
layout-mode
(new-tiled-client-position :back)
(close-focus-direction :previous))
(defstruct lime-configuration
"A fully evaluated user configuration ready to install on a backend."
(default-keybindings-p nil)
(default-modifiers '(:mod-4))
(default-layout :master-stack)
;; Client-flow policy is adapter configuration, not a property of an
;; Activity or saved View. The defaults deliberately match Lime's starter
;; workflow: new clients are focused, append at the visual bottom of a
;; master/stack layout, and focus moves back toward master after removal.
(new-client-focus-p t)
(new-tiled-client-position :back)
(close-focus-direction :previous)
;; Monocle's meaningful order is its focus cycle rather than its geometry:
;; a new client becomes the visible one and closing it advances to the next
;; older client. Further entries selectively override this built-in policy.
(layout-client-flow-policies
(list (make-layout-client-flow-policy
:layout-mode :monocle
:new-tiled-client-position :front
:close-focus-direction :next)))
(commands '())
(bindings '())
(rules '())
(appearance (make-window-appearance))
(bar-style (make-head-bar-style))
(font (make-ui-font-style))
(cursor (make-ui-cursor-style))
bar-height
(bar-height-supplied-p nil)
status-providers
(status-providers-supplied-p nil)
(startup-hooks '()))
(defvar *lime-configuration-definitions* (make-hash-table :test #'eq)
"Configuration name symbol -> latest object defined by DEFCONFIGURATION.")
(defvar *lime-configuration-load-names* nil
"Dynamically collects definitions evaluated by LOAD-LIME-CONFIGURATION.")
(defun register-lime-configuration (name configuration)
"Register NAME as CONFIGURATION and return CONFIGURATION.
DEFCONFIGURATION calls this while a user file is loaded. It is public mostly
for people constructing a configuration entirely from ordinary Lisp instead
of the small macro surface."
(unless (symbolp name)
(error 'invalid-transition
:description "A Lime configuration name must be a symbol."))
(unless (typep configuration 'lime-configuration)
(error 'invalid-transition
:description "A registered Lime configuration must be a LIME-CONFIGURATION."))
(setf (gethash name *lime-configuration-definitions*) configuration)
(when (boundp '*lime-configuration-load-names*)
(pushnew name *lime-configuration-load-names* :test #'eq))
configuration)
(defun find-lime-configuration (name)
"Return the configuration registered as NAME, or signal a useful error."
(or (gethash name *lime-configuration-definitions*)
(error 'invalid-transition
:description (format nil "No Lime configuration named ~S is loaded." name))))
(defun %configuration-name-from-load (names requested-name)
(cond
(requested-name
(unless (member requested-name names :test #'eq)
(error 'invalid-transition
:description
(format nil "Configuration file did not define the requested ~S configuration."
requested-name)))
requested-name)
((null names)
(error 'invalid-transition
:description "The configuration file did not evaluate DEFCONFIGURATION."))
((null (rest names))
(first names))
(t
(error 'invalid-transition
:description
(format nil "The configuration file defined ~{~S~^, ~}; choose one with :NAME."
names)))))
(defun load-lime-configuration (path &key name)
"Load PATH and return its one newly defined LIME-CONFIGURATION.
When a file deliberately defines several configurations, NAME selects one of
the symbols it defined during this load. The second return value is that
symbol, retained by a backend so `reload-configuration` can faithfully reload
the same choice later. Loading evaluates normal Common Lisp; keep the file's
top-level effects limited to configuration definitions and helper functions."
(let ((*lime-configuration-load-names* '()))
(load path)
(let* ((names (nreverse *lime-configuration-load-names*))
(selected-name (%configuration-name-from-load names name)))
(values (find-lime-configuration selected-name) selected-name))))
(defun %configuration-command-id-valid-p (id)
(or (symbolp id) (stringp id) (integerp id)))
(defun %configuration-modifiers-valid-p (modifiers)
(or (integerp modifiers) (listp modifiers)))
(defun %configuration-duplicate-p (items key-function &key (test #'equal))
;; The configuration lists are intentionally tiny, and MEMBER lets the
;; which-key alphabet use CHAR-EQUAL (which MAKE-HASH-TABLE cannot accept
;; as an equivalence relation) without a separate normalization path.
(let ((seen '()))
(loop for item in items
for key = (funcall key-function item)
thereis (if (member key seen :test test)
t
(progn (push key seen) nil)))))
(defun %validate-window-rule (rule)
(unless (typep rule 'window-rule)
(error 'invalid-transition
:description "Configuration rules must be WINDOW-RULE objects."))
(dolist (value (list (window-rule-resource-name rule)
(window-rule-resource-class rule)
(window-rule-title rule)))
(unless (or (null value) (stringp value) (functionp value))
(error 'invalid-transition
:description "A window-rule matcher must be NIL, a string, or a function.")))
(when (and (window-rule-predicate rule)
(not (functionp (window-rule-predicate rule))))
(error 'invalid-transition
:description "A window-rule :PREDICATE must be a function."))
(when (and (window-rule-center-p rule) (not (window-rule-float-p rule)))
(error 'invalid-transition
:description "A centered window rule must also specify :FLOAT T."))
rule)
(defun %validate-lime-configuration (configuration)
"Check all reloadable policy before changing any installed backend state."
(unless (typep configuration 'lime-configuration)
(error 'invalid-transition
:description "Expected a LIME-CONFIGURATION."))
(unless (%configuration-modifiers-valid-p
(lime-configuration-default-modifiers configuration))
(error 'invalid-transition
:description "Configuration default modifiers must be a list or integer mask."))
(unless (member (lime-configuration-default-layout configuration)
'(:manual :master-stack :monocle))
(error 'invalid-transition
:description "Configuration :DEFAULT-LAYOUT must be :MANUAL, :MASTER-STACK, or :MONOCLE."))
(unless (member (lime-configuration-new-client-focus-p configuration) '(t nil))
(error 'invalid-transition
:description "Configuration :NEW-CLIENT-FOCUS must be T or NIL."))
(unless (member (lime-configuration-new-tiled-client-position configuration)
'(:front :back))
(error 'invalid-transition
:description "Configuration :NEW-TILED-CLIENT-POSITION must be :FRONT or :BACK."))
(unless (member (lime-configuration-close-focus-direction configuration)
'(:previous :next))
(error 'invalid-transition
:description "Configuration :CLOSE-FOCUS-DIRECTION must be :PREVIOUS or :NEXT."))
(let ((policies (lime-configuration-layout-client-flow-policies configuration)))
(unless (and (listp policies)
(every (lambda (policy)
(and (typep policy 'layout-client-flow-policy)
(member (layout-client-flow-policy-layout-mode policy)
'(:manual :master-stack :monocle))
(member (layout-client-flow-policy-new-tiled-client-position policy)
'(:front :back))
(member (layout-client-flow-policy-close-focus-direction policy)
'(:previous :next))))
policies))
(error 'invalid-transition
:description "Each layout client-flow policy needs a known layout mode, :FRONT/:BACK insertion, and :PREVIOUS/:NEXT close direction."))
(when (%configuration-duplicate-p policies
#'layout-client-flow-policy-layout-mode)
(error 'invalid-transition
:description "A configuration may define at most one client-flow policy per layout mode.")))
(let ((commands (lime-configuration-commands configuration))
(bindings (lime-configuration-bindings configuration)))
(unless (every (lambda (command)
(and (typep command 'lime-command)
(%configuration-command-id-valid-p (lime-command-id command))
(stringp (lime-command-label command))
(functionp (lime-command-action command))
(or (null (lime-command-key command))
(characterp (lime-command-key command)))))
commands)
(error 'invalid-transition
:description "Every configuration command needs an id, string label, function action, and optional character key."))
(when (%configuration-duplicate-p commands #'lime-command-id)
(error 'invalid-transition
:description "Configuration command ids must be unique."))
(when (%configuration-duplicate-p (remove nil (mapcar #'lime-command-key commands)) #'identity
:test #'char-equal)
(error 'invalid-transition
:description "Configuration which-key command letters must be unique."))
(unless (every (lambda (binding)
(and (typep binding 'configuration-binding)
(or (characterp (configuration-binding-key binding))
(and (integerp (configuration-binding-key binding))
(not (minusp (configuration-binding-key binding)))))
(%configuration-modifiers-valid-p
(configuration-binding-modifiers binding))
(find (configuration-binding-command-id binding) commands
:key #'lime-command-id :test #'equal)))
bindings)
(error 'invalid-transition
:description "Each configuration binding needs a key, valid modifiers, and a configured command id."))
(when (%configuration-duplicate-p
bindings
(lambda (binding)
(list (configuration-binding-key binding)
(configuration-binding-modifiers binding))))
(error 'invalid-transition
:description "Configuration key/modifier pairs must be unique.")))
(dolist (rule (lime-configuration-rules configuration))
(%validate-window-rule rule))
(%validate-window-appearance (lime-configuration-appearance configuration))
(%validate-head-bar-style (lime-configuration-bar-style configuration))
(%validate-ui-font-style (lime-configuration-font configuration))
(%validate-ui-cursor-style (lime-configuration-cursor configuration))
(when (lime-configuration-bar-height-supplied-p configuration)
(unless (and (integerp (lime-configuration-bar-height configuration))
(not (minusp (lime-configuration-bar-height configuration))))
(error 'invalid-transition
:description "A configured bar height must be a non-negative integer.")))
(when (lime-configuration-status-providers-supplied-p configuration)
(%validate-status-providers (lime-configuration-status-providers configuration)))
(unless (every #'functionp (lime-configuration-startup-hooks configuration))
(error 'invalid-transition
:description "Configuration startup hooks must be functions."))
configuration)
(defun configured-new-client-focus-p (backend)
"Return BACKEND's ordinary newly-managed-client focus policy."
(let ((configuration (x11-backend-configuration backend)))
(if configuration
(lime-configuration-new-client-focus-p configuration)
t)))
(defun configured-default-layout (backend)
"Initial layout for a new Head-local draft; never reapplied on resume/reload."
(let ((configuration (x11-backend-configuration backend)))
(if configuration
(lime-configuration-default-layout configuration)
(x11-backend-default-layout backend))))
(defun %configured-layout-client-flow-policy (backend layout-mode)
"Return the optional explicit policy for LAYOUT-MODE on BACKEND."
(let ((configuration (x11-backend-configuration backend)))
(and configuration
(find layout-mode
(lime-configuration-layout-client-flow-policies configuration)
:key #'layout-client-flow-policy-layout-mode))))
(defun configured-new-tiled-client-position (backend layout-mode)
"Return where new tiled clients enter LAYOUT-MODE's placement ordering."
(let ((policy (%configured-layout-client-flow-policy backend layout-mode))
(configuration (x11-backend-configuration backend)))
(cond
(policy (layout-client-flow-policy-new-tiled-client-position policy))
((eq layout-mode :monocle) :front)
(configuration (lime-configuration-new-tiled-client-position configuration))
(t :back))))
(defun configured-close-focus-direction (backend layout-mode)
"Return the removal focus direction preferred by LAYOUT-MODE."
(let ((policy (%configured-layout-client-flow-policy backend layout-mode))
(configuration (x11-backend-configuration backend)))
(cond
(policy (layout-client-flow-policy-close-focus-direction policy))
((eq layout-mode :monocle) :next)
(configuration (lime-configuration-close-focus-direction configuration))
(t :previous))))
(defun list-configured-commands (backend)
"Return BACKEND's user-defined commands in configuration order."
(copy-list
(and (x11-backend-configuration backend)
(lime-configuration-commands (x11-backend-configuration backend)))))
(defun find-configured-command (backend command-id)
"Find a configured command by id, without inventing a fallback behavior."
(or (find command-id (list-configured-commands backend)
:key #'lime-command-id :test #'equal)
(error 'invalid-transition
:description (format nil "No configured command named ~S exists." command-id))))
(defun run-configured-command (backend command-id)
"Run COMMAND-ID against BACKEND and return its ordinary Lisp result."
(funcall (lime-command-action (find-configured-command backend command-id))
backend))
(defun %restore-keybindings (backend bindings)
"Restore a captured binding list after a failed configuration install."
(dolist (binding bindings)
(bind-key backend (keybinding-key binding) (keybinding-modifiers binding)
(keybinding-handler binding)
:description (keybinding-description binding))))
(defun %install-configuration-bindings (backend configuration)
(clear-keybindings backend)
(when (lime-configuration-default-keybindings-p configuration)
(install-default-keybindings
backend :modifiers (lime-configuration-default-modifiers configuration)))
(dolist (binding (lime-configuration-bindings configuration))
(let ((command-id (configuration-binding-command-id binding)))
(bind-key
backend
(configuration-binding-key binding)
(configuration-binding-modifiers binding)
(lambda (bound-backend)
(run-configured-command bound-backend command-id))
:description
(or (configuration-binding-description binding)
(lime-command-description
(find-configured-command backend command-id)))))))
(defun apply-lime-configuration (backend configuration
&key source name (run-startup-hooks-p nil))
"Install CONFIGURATION while preserving the running Lime world.
All type, rule, command, and binding-reference checks happen before touching
the X server. A later passive-grab/render error restores the previous binding
set, appearance, and bar settings before the error is re-signalled. Reloading normally
leaves startup hooks alone; START-LIME opts into them once after initial
installation so a reload can never unexpectedly launch another program."
(%validate-lime-configuration configuration)
(let ((old-configuration (x11-backend-configuration backend))
(old-source (x11-backend-configuration-source backend))
(old-name (x11-backend-configuration-name backend))
(old-appearance (x11-backend-window-appearance backend))
(old-bindings (list-keybindings backend))
(old-bar-height (x11-backend-head-bar-height backend))
(old-cursors (x11-backend-cursor-set backend))
(session-settings-touched-p nil)
(new-cursors nil)
(old-status-providers
(copy-list (x11-backend-head-bar-status-providers backend))))
(handler-case
(progn
;; Keep the old XIDs alive until every other config operation commits.
(setf new-cursors (%prepare-cursor-set backend (lime-configuration-cursor configuration)))
(%install-cursor-set backend new-cursors)
;; Give bound handlers the complete new command registry before
;; they can ever be dispatched by X after the next output flush.
(setf (x11-backend-configuration backend) configuration
(x11-backend-configuration-source backend) source
(x11-backend-configuration-name backend) name
(x11-backend-window-appearance backend)
(lime-configuration-appearance configuration))
;; Resolve both colors before touching window geometry. Allocation
;; errors still take the same configuration rollback path.
(%border-pixel backend (window-appearance-focused-border
(lime-configuration-appearance configuration)))
(%border-pixel backend (window-appearance-unfocused-border
(lime-configuration-appearance configuration)))
(%install-configuration-bindings backend configuration)
(when (lime-configuration-bar-height-supplied-p configuration)
(backend-set-head-bar-height backend
(lime-configuration-bar-height configuration)))
(when (lime-configuration-status-providers-supplied-p configuration)
(backend-set-head-bar-status-providers
backend (lime-configuration-status-providers configuration)))
(apply-active-presentation backend)
(%render-command-palette backend)
;; Unlike ordinary frames, installation needs a server roundtrip:
;; catch asynchronous grab/configure errors inside this rollback.
(xlib:display-finish-output (x11-backend-display backend))
(setf session-settings-touched-p t)
(%apply-cursor-session-settings backend (lime-configuration-cursor configuration))
(when run-startup-hooks-p
(dolist (hook (lime-configuration-startup-hooks configuration))
(funcall hook backend)))
(%prune-border-colors backend)
(%free-cursor-set old-cursors)
configuration)
(error (condition)
;; This path is specifically about reconfiguration plumbing, never
;; core world state. Activities, Presentations, Views, and leases
;; were untouched and thus need no rollback.
(ignore-errors (clear-keybindings backend))
(ignore-errors (%restore-keybindings backend old-bindings))
(setf (x11-backend-configuration backend) old-configuration
(x11-backend-configuration-source backend) old-source
(x11-backend-configuration-name backend) old-name
(x11-backend-window-appearance backend) old-appearance)
(ignore-errors (%install-cursor-set backend old-cursors))
(when session-settings-touched-p
(ignore-errors
(%apply-cursor-session-settings
backend (if old-configuration (lime-configuration-cursor old-configuration)
(make-ui-cursor-style)))))
(%free-cursor-set new-cursors)
(ignore-errors (backend-set-head-bar-height backend old-bar-height))
(ignore-errors (backend-set-head-bar-status-providers backend old-status-providers))
(ignore-errors (apply-active-presentation backend))
(ignore-errors (%render-command-palette backend))
(ignore-errors (xlib:display-finish-output (x11-backend-display backend)))
(ignore-errors (%prune-border-colors backend))
(error condition)))))
(defun configure-backend-from-file (backend path &key name (run-startup-hooks-p nil))
"Load PATH then install its selected configuration on BACKEND."
(multiple-value-bind (configuration selected-name)
(load-lime-configuration path :name name)
(apply-lime-configuration backend configuration
:source (namestring (truename path))
:name selected-name
:run-startup-hooks-p run-startup-hooks-p)))
(defun reload-backend-configuration (backend)
"Reload BACKEND's original configuration file without rerunning startup hooks."
(let ((source (x11-backend-configuration-source backend)))
(unless source
(error 'invalid-transition
:description "This backend was not started from a configuration file."))
(configure-backend-from-file backend source
:name (x11-backend-configuration-name backend))))
(defun %configuration-argument-plist-without (arguments removed-keys)
(loop for (key value) on arguments by #'cddr
unless (member key removed-keys)
append (list key value)))
(defun start-lime (&rest start-arguments
&key config-file configuration-name &allow-other-keys)
"Start Lime and, when CONFIG-FILE is given, install its user configuration.
CONFIG-FILE configurations replace the starter keymap as one unit; request it
again from `(:OPTIONS :DEFAULT-KEYBINDINGS T)` when desired. With no config
file this simply forwards to START-BACKEND and retains its normal defaults.
The file's DEFAULT-LAYOUT takes precedence over the startup keyword."
(if (null config-file)
(apply #'start-backend
(%configuration-argument-plist-without
start-arguments '(:config-file :configuration-name)))
(multiple-value-bind (configuration selected-name)
(load-lime-configuration config-file :name configuration-name)
;; Validate before claiming X or creating bootstrap drafts. The rest of
;; the configuration is installed once the backend resources exist.
(%validate-lime-configuration configuration)
(let ((backend
(apply #'start-backend
(append
(%configuration-argument-plist-without
start-arguments
'(:config-file :configuration-name
:install-default-keybindings-p :default-layout))
(list :install-default-keybindings-p nil
:default-layout
(lime-configuration-default-layout configuration))))))
(handler-case
(progn
(apply-lime-configuration backend configuration
:source (namestring (truename config-file))
:name selected-name
:run-startup-hooks-p t)
backend)
(error (condition)
(stop-backend backend :save-session-p nil)
(error condition)))))))
(defun %rule-text-matches-p (expected actual)
(cond
((null expected) t)
((stringp expected) (and actual (string-equal expected actual)))
((functionp expected) (not (null (funcall expected actual))))
(t nil)))
(defun %matching-window-rule (backend client-id info)
(let ((configuration (x11-backend-configuration backend)))
(when configuration
(find-if
(lambda (rule)
(and (%rule-text-matches-p (window-rule-resource-name rule)
(icccm-client-info-resource-name info))
(%rule-text-matches-p (window-rule-resource-class rule)
(icccm-client-info-resource-class info))
(%rule-text-matches-p (window-rule-title rule)
(icccm-client-info-title info))
(or (null (window-rule-predicate rule))
(funcall (window-rule-predicate rule) backend client-id info))))
(lime-configuration-rules configuration)))))
(defun apply-configured-window-rules (backend client-id window decision)
"Return DECISION styled by the first matching ordinary-client window rule.
Rules are evaluated after ICCCM facts have been cached but before the new
client joins any Presentation. They do not run for transients (whose parent
placement is mandatory) or dialog-like EWMH clients, and they do not route
between Activities or Heads. A matching non-floating rule overrides the
ordinary fixed-size floating default."
(let* ((info (backend-client-icccm-info backend client-id))
(rule (and info (%matching-window-rule backend client-id info))))
(when rule
(if (window-rule-float-p rule)
(let ((head (%head-for-id backend
(x11-placement-decision-head-id decision))))
(setf (x11-placement-decision-kind decision) :float
(x11-placement-decision-data decision)
(if (window-rule-center-p rule)
(%centered-floating-data-for-window backend client-id window head)
(%floating-data-for-window backend client-id window head nil))))
(setf (x11-placement-decision-kind decision) :tile
(x11-placement-decision-data decision) nil)))
decision))
(defun configuration-command-candidates (backend)
"Make direct-search candidates for every first-class configured command."
(mapcar
(lambda (command)
(let ((command-id (lime-command-id command)))
(make-ui-candidate
:id (list :configured-command command-id)
:label (format nil "Command: ~A" (lime-command-label command))
:detail (or (lime-command-description command)
(format nil "~A command" (or (lime-command-category command)
"User")))
:key-hint "Enter"
:action
(lambda ()
(let ((result (run-configured-command backend command-id)))
(if (typep result 'ui-result)
result
(ui-close-result
(or (lime-command-description command)
(format nil "Ran ~A." (lime-command-label command))))))))))
(list-configured-commands backend)))
(defun configuration-command-key-nodes (backend)
"Return optional `User` which-key children declared by configuration."
(loop for command in (list-configured-commands backend)
when (lime-command-key command)
collect
(let ((command-id (lime-command-id command)))
(make-ui-key-node
:id (list :configured-command command-id)
:key (lime-command-key command)
:label (lime-command-label command)
:detail (or (lime-command-description command)
(format nil "~A command" (or (lime-command-category command)
"User")))
:action
(lambda ()
(let ((result (run-configured-command backend command-id)))
(if (typep result 'ui-result)
result
(ui-close-result
(or (lime-command-description command)
(format nil "Ran ~A." (lime-command-label command)))))))))))
(in-package #:lime.config)
;;;; Small, readable source-file DSL
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun %configuration-option (options key &optional default)
(if (member key options)
(getf options key)
default))
(defun %require-configuration-options (options allowed context)
(unless (evenp (length options))
(error "~A needs keyword/value pairs, got ~S." context options))
(loop for key in options by #'cddr
do (unless (member key allowed)
(error "~A does not understand ~S." context key)))))
(defun %configuration-command-label (id)
(etypecase id
(symbol (string-capitalize (symbol-name id)))
(string id)
(integer (princ-to-string id))))
(defmacro defconfiguration (name &body clauses)
"Define and register one reloadable Lime user configuration.
The compact clauses are deliberately data-first Common Lisp:
(:OPTIONS :DEFAULT-KEYBINDINGS T :DEFAULT-MODIFIERS '(:MOD-4)
:DEFAULT-LAYOUT :MASTER-STACK
:NEW-CLIENT-FOCUS T :NEW-TILED-CLIENT-POSITION :BACK
:CLOSE-FOCUS-DIRECTION :PREVIOUS)
(:LAYOUT-FLOW :MONOCLE :NEW-TILED-CLIENT-POSITION :FRONT
:CLOSE-FOCUS-DIRECTION :NEXT)
(:COMMAND terminal :LABEL 'Terminal' :ACTION (lambda (backend) ...))
(:BIND #\Return terminal :MODIFIERS '(:MOD-4))
(:RULE :CLASS 'mpv' :FLOAT T :CENTER T)
(:FONT :FAMILY 'monospace' :SIZE 14 :WEIGHT :NORMAL)
(:BAR :HEIGHT 22 :STATUS-PROVIDERS (list #'my-status))
(:APPEARANCE :BORDER-WIDTH 3 :INNER-GAP 5 :OUTER-GAP 5)
(:STARTUP (lambda (backend) ...))
Commands become searchable palette entries automatically. Give a command
`:KEY #\t` to also place it under the palette's `User` which-key branch;
bind it separately when it needs a physical X key."
(unless (symbolp name)
(error "DEFCONFIGURATION needs a symbol name, got ~S." name))
(let ((options '())
(layout-flow-options '())
(commands '())
(bindings '())
(rules '())
(bar-options nil)
(font-options nil)
(font-supplied-p nil)
(cursor-options nil)
(cursor-supplied-p nil)
(appearance-options nil)
(appearance-supplied-p nil)
(startup-hooks '())
(rule-number 0))
(dolist (clause clauses)
(unless (consp clause)
(error "A configuration clause must be a list, got ~S." clause))
(case (first clause)
(:options
(when options
(error "DEFCONFIGURATION accepts only one :OPTIONS clause."))
(setf options (rest clause))
(%require-configuration-options
options '(:default-keybindings :default-modifiers :default-layout
:new-client-focus :new-tiled-client-position
:close-focus-direction)
":OPTIONS"))
(:layout-flow
(unless (second clause)
(error ":LAYOUT-FLOW needs a layout mode."))
(let ((flow-options (cddr clause)))
(%require-configuration-options
flow-options '(:new-tiled-client-position :close-focus-direction)
":LAYOUT-FLOW")
(push (list (second clause) flow-options) layout-flow-options)))
(:command
(unless (second clause)
(error ":COMMAND needs an id."))
(let ((command-options (cddr clause)))
(%require-configuration-options
command-options '(:label :description :category :key :action) ":COMMAND")
(unless (member :action command-options)
(error ":COMMAND ~S needs an :ACTION." (second clause)))
(push (list (second clause) command-options) commands)))
(:bind
(unless (and (second clause) (third clause))
(error ":BIND needs a key and a command id."))
(let ((binding-options (cdddr clause)))
(%require-configuration-options
binding-options '(:modifiers :description) ":BIND")
(push (list (second clause) (third clause) binding-options) bindings)))
(:navigation-keys
(%require-configuration-options (rest clause) '(:modifiers) ":NAVIGATION-KEYS")
;; Expand into ordinary named commands/bindings, so validation,
;; configuration reload, and custom overrides share the usual path.
(let ((modifiers (%configuration-option
(rest clause) :modifiers
(%configuration-option (cdr (assoc :options clauses))
:default-modifiers ''(:mod-4)))))
(loop for slot from 1 to 10 for key across "1234567890" do
(dolist (kind '(:activity :view))
(let* ((view-p (eq kind :view))
(id (intern (format nil "NAVIGATE-~A-~D" kind slot) :keyword))
(label (format nil "Select ~A slot ~D" (if view-p "local View" "Activity") slot)))
(push (list id (list :label label :category "Navigation"
:action `(lambda (backend)
(,(if view-p 'lime.x11:backend-select-view-slot
'lime.x11:backend-select-activity-slot) backend ,slot)))) commands)
(push (list key id (list :modifiers (if view-p
`(lime.x11::%default-modifiers ,modifiers '(:shift))
modifiers))) bindings))))))
(:rule
(let ((rule-options (rest clause)))
(%require-configuration-options
rule-options '(:id :name :class :title :predicate :float :center) ":RULE")
(incf rule-number)
(push (list rule-number rule-options) rules)))
(:bar
(when bar-options
(error "DEFCONFIGURATION accepts only one :BAR clause."))
(setf bar-options (rest clause))
(%require-configuration-options
bar-options '(:height :status-providers :padding-x :background :foreground
:focused-background :focused-foreground
:unfocused-background :unfocused-foreground
:accent-color :menu-label-color :logo-color
:separator-color :title-edge-color :attention-color
:attention-marker :attention-indicator-p) ":BAR"))
(:font
(when font-supplied-p (error "DEFCONFIGURATION accepts only one :FONT clause."))
(setf font-options (rest clause) font-supplied-p t)
(%require-configuration-options font-options '(:family :size :weight) ":FONT"))
(:cursor
(when cursor-supplied-p (error "DEFCONFIGURATION accepts only one :CURSOR clause."))
(setf cursor-options (rest clause) cursor-supplied-p t)
(%require-configuration-options cursor-options '(:theme :size) ":CURSOR"))
(:appearance
(when appearance-supplied-p
(error "DEFCONFIGURATION accepts only one :APPEARANCE clause."))
(setf appearance-options (rest clause) appearance-supplied-p t)
(%require-configuration-options
appearance-options '(:border-width :focused-border :unfocused-border
:inner-gap :outer-gap) ":APPEARANCE"))
(:startup
(unless (= (length (rest clause)) 1)
(error ":STARTUP needs exactly one function expression."))
(push (second clause) startup-hooks))
(otherwise
(error "DEFCONFIGURATION does not understand ~S." (first clause)))))
(let ((configuration (gensym "CONFIGURATION")))
`(progn
(defparameter ,name
(let ((,configuration
(lime.x11:make-lime-configuration
:appearance (lime.x11:make-window-appearance ,@appearance-options)
:font (lime.x11:make-ui-font-style ,@font-options)
:cursor (lime.x11:make-ui-cursor-style ,@cursor-options)
:default-keybindings-p
,(%configuration-option options :default-keybindings nil)
:default-modifiers
,(%configuration-option options :default-modifiers ''(:mod-4))
:default-layout
,(%configuration-option options :default-layout :master-stack)
:new-client-focus-p
,(%configuration-option options :new-client-focus t)
:new-tiled-client-position
,(%configuration-option options :new-tiled-client-position :back)
:close-focus-direction
,(%configuration-option options :close-focus-direction :previous)
:layout-client-flow-policies
(list
,@(mapcar
(lambda (entry)
(destructuring-bind (layout-mode flow-options) entry
`(lime.x11:make-layout-client-flow-policy
:layout-mode ',layout-mode
:new-tiled-client-position
,(%configuration-option
flow-options :new-tiled-client-position :back)
:close-focus-direction
,(%configuration-option
flow-options :close-focus-direction :previous))))
(nreverse layout-flow-options))))))
,@(mapcar
(lambda (entry)
(destructuring-bind (id command-options) entry
`(push
(lime.x11:make-lime-command
:id ',id
:label ,(%configuration-option command-options :label
(%configuration-command-label id))
:description ,(%configuration-option command-options :description nil)
:category ,(%configuration-option command-options :category "User")
:key ,(%configuration-option command-options :key nil)
:action ,(%configuration-option command-options :action))
(lime.x11:lime-configuration-commands ,configuration))))
(nreverse commands))
(setf (lime.x11:lime-configuration-commands ,configuration)
(nreverse (lime.x11:lime-configuration-commands ,configuration)))
,@(mapcar
(lambda (entry)
(destructuring-bind (key command-id binding-options) entry
`(push
(lime.x11:make-configuration-binding
:key ,key
:command-id ',command-id
:modifiers
,(%configuration-option
binding-options :modifiers
`(lime.x11:lime-configuration-default-modifiers ,configuration))
:description ,(%configuration-option binding-options :description nil))
(lime.x11:lime-configuration-bindings ,configuration))))
(nreverse bindings))
(setf (lime.x11:lime-configuration-bindings ,configuration)
(nreverse (lime.x11:lime-configuration-bindings ,configuration)))
,@(mapcar
(lambda (entry)
(destructuring-bind (number rule-options) entry
`(push
(lime.x11:make-window-rule
:id ',(%configuration-option rule-options :id
(intern (format nil "RULE-~D" number)
:keyword))
:resource-name ,(%configuration-option rule-options :name nil)
:resource-class ,(%configuration-option rule-options :class nil)
:title ,(%configuration-option rule-options :title nil)
:predicate ,(%configuration-option rule-options :predicate nil)
:float-p ,(%configuration-option rule-options :float nil)
:center-p ,(%configuration-option rule-options :center nil))
(lime.x11:lime-configuration-rules ,configuration))))
(nreverse rules))
(setf (lime.x11:lime-configuration-rules ,configuration)
(nreverse (lime.x11:lime-configuration-rules ,configuration)))
,@(when (member :height bar-options)
`((setf (lime.x11:lime-configuration-bar-height ,configuration)
,(%configuration-option bar-options :height)
(lime.x11:lime-configuration-bar-height-supplied-p ,configuration) t)))
(setf (lime.x11:lime-configuration-bar-style ,configuration)
(lime.x11:make-head-bar-style
,@(loop for (key value) on bar-options by #'cddr
unless (member key '(:height :status-providers))
append (list key value))))
,@(when (member :status-providers bar-options)
`((setf (lime.x11:lime-configuration-status-providers ,configuration)
,(%configuration-option bar-options :status-providers)
(lime.x11:lime-configuration-status-providers-supplied-p ,configuration) t)))
(setf (lime.x11:lime-configuration-startup-hooks ,configuration)
(list ,@(nreverse startup-hooks)))
,configuration))
(lime.x11:register-lime-configuration ',name ,name)
',name))))
(defun load-configuration (path &key name)
"Load PATH through the DEFCONFIGURATION registry."
(lime.x11:load-lime-configuration path :name name))
(defun apply-configuration (backend configuration &key source name)
"Install a prebuilt CONFIGURATION on BACKEND without running startup hooks."
(lime.x11:apply-lime-configuration backend configuration :source source :name name))
(defun reload-configuration (backend)
"Reload BACKEND's original configuration file safely."
(lime.x11:reload-backend-configuration backend))
(defun start-lime (&rest arguments)
"Start the X11 backend, optionally using :CONFIG-FILE and :CONFIGURATION-NAME."
(apply #'lime.x11:start-lime arguments))
(defun run-command (backend command-id)
"Run a named command from BACKEND's installed user configuration."
(lime.x11:run-configured-command backend command-id))
(defun list-commands (backend)
"List first-class user commands available on BACKEND."
(lime.x11:list-configured-commands backend))
(defun find-command (backend command-id)
"Find one installed user command by id."
(lime.x11:find-configured-command backend command-id))