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

1543 lines
84 KiB
Common Lisp
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(in-package #:lime.x11.test)
(def-suite lime-x11-suite
:description "Adapter-local geometry and placement policy tests.")
(in-suite lime-x11-suite)
(defparameter *configured-command-runs* 0)
(define-condition broken-diagnostic-printer (error) ()
(:report (lambda (condition stream)
(declare (ignore condition stream))
(error "The printer is broken too"))))
(test diagnostic-printing-is-defensive
(is (string= "[Condition could not be printed]"
(lime.x11::%diagnostic-condition-message (make-condition 'broken-diagnostic-printer))))
(is (= 4096 (length (lime.x11::%diagnostic-condition-message
(make-condition 'simple-error :format-control "~A"
:format-arguments (list (make-string 5000 :initial-element #\x))))))))
(test x11-race-policy-does-not-hide-ownership-or-programming-errors
(dolist (major '(2 3 8 10 12 15 18 19 20 25 28 29 38 42))
(is (eq :ignore (lime.x11::%expected-x-error-policy 'xlib:window-error major t t)))
(is (eq :client-gone (lime.x11::%expected-x-error-policy 'xlib:window-error major nil t)))
(is-false (lime.x11::%expected-x-error-policy 'xlib:window-error major t nil)))
(is (eq :ignore (lime.x11::%expected-x-error-policy 'xlib:match-error 42 t nil)))
(is-false (lime.x11::%expected-x-error-policy 'xlib:match-error 42 nil nil))
(is (eq :client-gone (lime.x11::%expected-x-error-policy 'xlib:drawable-error 14 nil t)))
(dolist (major '(2 28 33 12 42))
(is-false (lime.x11::%expected-x-error-policy 'xlib:access-error major t t))
(is-false (lime.x11::%expected-x-error-policy 'xlib:value-error major t t)))
(is-false (lime.x11::%expected-x-error-policy 'xlib:window-error 99 t t))
(is-false (lime.x11::%expected-x-error-policy 'xlib:drawable-error 14 t nil)))
(test diagnostic-history-is-bounded-and-does-not-change-the-world
(let* ((backend (make-head-bar-fixture))
(revision (world-revision (lime.x11:x11-backend-world backend))))
(setf (lime.x11::x11-backend-event-context backend) '(:kind :map-request :window-id 42))
(dotimes (i 50)
(backend-record-diagnostic backend (make-condition 'simple-error
:format-control "Failure ~D"
:format-arguments (list i))))
(let* ((reports (getf (backend-diagnostics backend) :reports))
(latest (first reports)))
(is (= 32 (length reports)))
(is (string= "Failure 49" (getf latest :message)))
(is (equal '(:kind :map-request :window-id 42) (getf latest :event)))
(setf (getf latest :message) "Changed copy")
(is (string= "Failure 49" (getf (first (getf (backend-diagnostics backend) :reports)) :message))))
(is (= revision (world-revision (lime.x11:x11-backend-world backend))))))
(test everyday-layouts-do-not-include-manual
(dolist (direction '(:next :previous))
(is (eq :monocle (lime.x11::%next-cycle-layout :master-stack direction)))
(is (eq :master-stack (lime.x11::%next-cycle-layout :monocle direction))))
(is (eq :master-stack (lime.x11::%next-cycle-layout :manual :next)))
(is (eq :monocle (lime.x11::%next-cycle-layout :manual :previous)))
(let* ((nodes (lime.x11::%ui-key-path (make-head-bar-fixture)))
(layout (find :layout nodes :key #'lime.ui:ui-key-node-id)))
(is (equal '(:master-stack :monocle :cycle)
(mapcar #'lime.ui:ui-key-node-id (lime.ui:ui-key-node-children layout))))))
(test floating-delta-render-matches-full-render-geometry
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend))
(presentation (current-presentation world "main"))
(id (presentation-id presentation)))
(toggle-client-floating world id "terminal"
:data '(:x 1/4 :y 1/3 :width 1/2 :height 1/2))
(setf (gethash "terminal" (lime.x11::x11-backend-icccm-clients backend))
(lime.x11::make-icccm-client-info
:normal-hints (xlib:make-wm-size-hints :min-width 90 :min-height 70
:width-inc 8 :height-inc 16)))
(dolist (mode '(:manual :master-stack :monocle))
(set-presentation-layout-mode world id mode)
(let* ((full (configure-effect-for backend "terminal"))
(delta (lime.x11::%floating-client-configure-effect backend presentation "terminal")))
(is (eq :configure (x11-effect-kind delta)))
(is (equalp (x11-effect-geometry full) (x11-effect-geometry delta)))
(is (= (x11-effect-border-width full) (x11-effect-border-width delta)))
;; A position update must not overwrite current focus colors.
(is-false (x11-effect-border-color delta))))
(is-false (lime.x11::%floating-client-configure-effect backend presentation "editor"))
(setf (gethash "terminal" (lime.x11::x11-backend-fullscreen-clients backend)) t)
(is-false (lime.x11::%floating-client-configure-effect backend presentation "terminal"))
(remhash "terminal" (lime.x11::x11-backend-fullscreen-clients backend))
(hide-client-from-presentation world "terminal" id)
(is-false (lime.x11::%floating-client-configure-effect backend presentation "terminal"))))
(test pointer-deltas-use-work-area-and-remain-bounded
(let* ((area (make-x11-work-area :x 40 :y 52 :width 800 :height 600))
(drag (lime.x11::make-pointer-drag
:mode :move :initial-root-x 200 :initial-root-y 200
:initial-data '(:x 1/4 :y 1/4 :width 1/2 :height 1/2))))
(is (equal '(:x 3/8 :y 5/12 :width 1/2 :height 1/2)
(lime.x11::%drag-geometry drag area 300 300)))
(is (equal '(:x 0 :y 0 :width 1/2 :height 1/2)
(lime.x11::%drag-geometry drag area -9999 -9999)))
(is (equal '(:x 1/2 :y 1/2 :width 1/2 :height 1/2)
(lime.x11::%drag-geometry drag area 9999 9999)))
(setf (lime.x11::pointer-drag-mode drag) :resize)
(is (equal '(:x 1/4 :y 1/4 :width 1/20 :height 1/20)
(lime.x11::%drag-geometry drag area -9999 -9999)))
(is (equal '(:x 1/4 :y 1/4 :width 3/4 :height 3/4)
(lime.x11::%drag-geometry drag area 9999 9999)))))
(lime.config:defconfiguration config-dsl-fixture
(:options :default-keybindings t :default-modifiers '(:mod-4)
:default-layout :monocle
:new-client-focus nil
:new-tiled-client-position :front
:close-focus-direction :next)
;; A layout entry selectively overrides the ordinary fallback policy.
(:layout-flow :monocle
:new-tiled-client-position :back
:close-focus-direction :previous)
(:command terminal
:label "Terminal"
:description "launch a terminal"
:category "Launch"
:key #\t
:action (lambda (backend)
(declare (ignore backend))
(incf *configured-command-runs*)))
(:bind #\t terminal :modifiers '(:mod-4))
;; The specific rule must win before the generic fallback rule.
(:rule :id mpv :class "mpv" :float t :center t)
(:rule :id fallback :predicate (lambda (backend client-id info)
(declare (ignore backend client-id info))
t)
:float t)
(:bar :height 24 :status-providers '() :title-edge-color "#52616b")
(:appearance :border-width 3 :inner-gap 5 :outer-gap 5
:focused-border "#ffac54" :unfocused-border "#121f25"))
(test normal-hints-limit-snap-and-contain-rendered-geometry
(let* ((hints (xlib:make-wm-size-hints
:min-width 100 :min-height 50
:max-width 220 :max-height 170
:base-width 0 :base-height 0
:width-inc 20 :height-inc 25))
(constrained
(lime.x11::constrain-geometry-to-normal-hints
(make-geometry :x 20 :y 30 :width 247 :height 149)
hints :bounds-x 0 :bounds-y 0 :bounds-width 200 :bounds-height 150)))
;; The Head bound wins over a too-large requested size, and coordinates
;; move only enough to keep the client on that Head.
(is (= (geometry-x constrained) 0))
(is (= (geometry-y constrained) 25))
(is (= (geometry-width constrained) 200))
(is (= (geometry-height constrained) 125)))
(let* ((hints (xlib:make-wm-size-hints
:min-width 100 :min-height 50
:max-width 220 :max-height 170
:base-width 0 :base-height 0
:width-inc 20 :height-inc 25))
(constrained
(lime.x11::constrain-geometry-to-normal-hints
(make-geometry :x 8 :y 9 :width 87 :height 31) hints)))
;; The smallest compatible grid cells at or above the advertised minima.
(is (= (geometry-x constrained) 8))
(is (= (geometry-y constrained) 9))
(is (= (geometry-width constrained) 100))
(is (= (geometry-height constrained) 50))))
(test absent-normal-hints-leave-a-geometry-unchanged
(let ((geometry (lime.x11::constrain-geometry-to-normal-hints
(make-geometry :x 12 :y 34 :width 567 :height 89) nil)))
(is (= (geometry-x geometry) 12))
(is (= (geometry-y geometry) 34))
(is (= (geometry-width geometry) 567))
(is (= (geometry-height geometry) 89))))
(test fixed-size-detection-requires-both-explicit-positive-equal-axes
(is-true (lime.x11::%fixed-size-normal-hints-p
(xlib:make-wm-size-hints :min-width 300 :max-width 300
:min-height 200 :max-height 200)))
(is-false (lime.x11::%fixed-size-normal-hints-p nil))
(dolist (arguments '(()
(:min-width 800 :min-height 500)
(:max-width 300 :max-height 200)
(:min-width 300 :max-width 300)
(:min-height 200 :max-height 200)
(:min-width 300 :max-width 300 :min-height 200 :max-height 400)
(:min-width 300 :max-width 600 :min-height 200 :max-height 200)
(:min-width 300 :max-width 200 :min-height 200 :max-height 200)
(:min-width 0 :max-width 0 :min-height 200 :max-height 200)
(:min-width 300 :max-width 300 :min-height 0 :max-height 0)))
(is-false (lime.x11::%fixed-size-normal-hints-p
(apply #'xlib:make-wm-size-hints arguments)))))
(test fixed-size-placement-is-an-overridable-default-not-a-class-rule
(let* ((backend (make-head-bar-fixture))
(world (lime.x11:x11-backend-world backend))
(before (lime.persistence:snapshot-world world))
(info (lime.x11::make-icccm-client-info
:client-id "popup" :resource-class "discord"
:normal-hints (xlib:make-wm-size-hints
:min-width 300 :max-width 300
:min-height 200 :max-height 200)))
(rectangle '(:x 1/4 :y 1/4 :width 1/2 :height 1/2))
(original (symbol-function 'lime.x11::%centered-floating-data-for-window))
(rule-calls 0))
(setf (gethash "popup" (lime.x11::x11-backend-icccm-clients backend)) info)
(unwind-protect
(progn
;; Geometry itself is covered on a real X server in the smoke test.
(setf (symbol-function 'lime.x11::%centered-floating-data-for-window)
(lambda (b id window head)
(declare (ignore b id window head)) rectangle))
(flet ((decision () (lime.x11::new-client-placement-decision backend "popup" nil)))
(let ((result (decision)))
(is (eq :float (lime.x11::x11-placement-decision-kind result)))
(is (equal rectangle (lime.x11::x11-placement-decision-data result)))
(is (equal "main" (lime.x11::x11-placement-decision-head-id result)))
(is (equal "development" (lime.x11::x11-placement-decision-activity-id result)))
;; Do not force focus when the user's new-client policy says no.
(is-false (lime.x11::x11-placement-decision-focus-p result)))
;; The same class with only a minimum size is still tiled.
(let ((fixed-hints (lime.x11::icccm-client-info-normal-hints info)))
(setf (lime.x11::icccm-client-info-normal-hints info)
(xlib:make-wm-size-hints :min-width 800 :min-height 500))
(is (eq :tile (lime.x11::x11-placement-decision-kind (decision))))
(setf (lime.x11::icccm-client-info-normal-hints info) fixed-hints))
;; Only the first matching rule runs; :FLOAT NIL really overrides
;; the default, rather than leaving stale floating geometry.
(setf (lime.x11::x11-backend-configuration backend)
(make-lime-configuration
:rules (list (make-window-rule
:resource-class "discord" :float-p nil
:predicate (lambda (&rest args)
(declare (ignore args))
(incf rule-calls) t))
(make-window-rule :float-p t :center-p t))))
(let ((result (decision)))
(is (eq :tile (lime.x11::x11-placement-decision-kind result)))
(is-false (lime.x11::x11-placement-decision-data result))
(is (= 1 rule-calls)))
;; A nonmatching rule leaves the fixed-size default alone.
(setf (lime.x11::x11-backend-configuration backend)
(make-lime-configuration
:rules (list (make-window-rule :resource-class "unrelated" :float-p nil))))
(is (eq :float (lime.x11::x11-placement-decision-kind (decision))))
(is (equalp before (lime.persistence:snapshot-world world)))))
(setf (symbol-function 'lime.x11::%centered-floating-data-for-window) original))))
(test icccm-wm-state-uses-the-standard-normal-and-iconic-payloads
;; The icon-window XID is deliberately zero: Views, not an icon box, are
;; Lime's user-facing way to recover a hidden task client.
(is (equal (icccm-wm-state-data :withdrawn) '(0 0)))
(is (equal (icccm-wm-state-data :normal) '(1 0)))
(is (equal (icccm-wm-state-data :iconic) '(3 0))))
(test icccm-property-refresh-retains-the-renderers-wm-state-cache
;; An unrelated property change must not turn an already hidden window back
;; into an apparent withdrawal, or queue another pointless unmap.
(is (eq (lime.x11::%refreshed-wm-state
(lime.x11::make-icccm-client-info :wm-state :iconic))
:iconic))
(is (eq (lime.x11::%refreshed-wm-state nil) :withdrawn)))
(test renderer-unmap-tokens-distinguish-one-view-hide-from-a-later-withdrawal
(let ((backend (make-head-bar-fixture)))
;; Numeric XIDs keep this bookkeeping test independent of a live CLX
;; server. Exactly one token is consumed for each renderer unmap.
(lime.x11::%note-pending-unmap backend 4242)
(lime.x11::%note-pending-unmap backend 4242)
(is-true (lime.x11::%consume-pending-unmap-p backend 4242))
(is-true (lime.x11::%consume-pending-unmap-p backend 4242))
(is-false (lime.x11::%consume-pending-unmap-p backend 4242))
(lime.x11::%note-pending-unmap backend 4242)
(lime.x11::%forget-pending-unmaps backend 4242)
(is-false (lime.x11::%consume-pending-unmap-p backend 4242))))
(test withdrawal-cleanup-retires-a-client-from-views-and-backend-caches
(let* ((backend (make-head-bar-fixture))
(world (lime.x11::x11-backend-world backend)))
(setf (gethash "editor" (lime.x11::x11-backend-icccm-clients backend))
(lime.x11::make-icccm-client-info :client-id "editor"))
;; This is the shared DestroyNotify/withdrawal cleanup seam. It changes
;; durable client membership only after the lifecycle layer has decided
;; that the unmap really came from the application.
(is (equal (lime.x11::%retire-managed-client backend "editor") "editor"))
(signals unknown-entity (find-client world "editor"))
(is-false (backend-client-icccm-info backend "editor"))
(is-false (member "editor"
(presentation-client-ids (current-presentation world "main"))
:test #'equal))))
(test named-commands-derive-readable-unique-identifiers
(let ((world (make-world)))
(create-activity world "lime" "Existing Lime")
(is (equal (lime.x11::%name-slug " Lime WM! " "Activity") "lime-wm"))
(is (equal (lime.x11::%fresh-id-from-name world "Lime" "Activity"
#'find-activity)
"lime-2"))
(is (equal (lime.x11::%fresh-id-from-name world "Code / Debug" "View"
#'find-view :prefix "lime")
"lime/code-debug"))))
(test only-client-lease-conflicts-offer-lease-policy-choices
(flet ((report-with (kind)
(lime.core::make-activation-report
:conflicts (list (lime.core::make-activation-conflict :kind kind)))))
(is-true (lime.x11::%ui-lease-conflicts-only-p
(report-with :client-leased)))
(is-false (lime.x11::%ui-lease-conflicts-only-p
(report-with :view-mounted)))))
(test ewmh-state-actions-follow-remove-add-toggle-semantics
(is-false (lime.x11::%ewmh-state-action-result 0 t))
(is-true (lime.x11::%ewmh-state-action-result 1 nil))
(is-true (lime.x11::%ewmh-state-action-result 2 nil))
(is-false (lime.x11::%ewmh-state-action-result 2 t))
;; Unknown action codes must not accidentally alter the client's state.
(is-true (lime.x11::%ewmh-state-action-result 99 t)))
(test ewmh-window-type-policy-decodes-defaults-and-separates-external-surfaces
;; The atom-id decoder is deliberately server-free. Its production caller
;; supplies real atoms interned on the current X display.
(let ((type-atoms '((:dialog . 10) (:dock . 20) (:utility . 30))))
(is (equal (lime.x11::%ewmh-types-from-atom-ids '(10 30) type-atoms)
'(:dialog :utility)))
(is (equal (lime.x11::%ewmh-types-from-atom-ids '(999) type-atoms)
'(:normal)))
(is-true (lime.x11::%ewmh-dialog-like-window-types-p '(:utility)))
(is-false (lime.x11::%ewmh-dialog-like-window-types-p '(:normal)))
(is-true (lime.x11::%ewmh-external-window-types-p '(:dock)))
(is-false (lime.x11::%ewmh-external-window-types-p '(:dialog)))))
(test ewmh-supporting-window-name-is-an-eight-bit-utf8-payload
;; CLX requires this specialized payload for a UTF8_STRING property.
(is (typep lime.x11::+ewmh-wm-name-utf8+
'(array (unsigned-byte 8) (*))))
(is (equalp lime.x11::+ewmh-wm-name-utf8+ #(76 105 109 101))))
(test ewmh-client-order-keeps-first-map-order-and-removes-only-the-gone-window
(let ((order '()))
(setf order (lime.x11::%ewmh-add-client-window-id order 40)
order (lime.x11::%ewmh-add-client-window-id order 20)
order (lime.x11::%ewmh-add-client-window-id order 40))
(is (equal order '(40 20)))
(is (equal (lime.x11::%ewmh-remove-client-window-id order 40)
'(20)))))
(test ewmh-external-configure-masks-admit-only-requested-geometry-components
;; X's ConfigureRequest mask uses the conventional CWX/CWY/CWWidth/etc.
;; bit positions; docks may update only the fields they actually requested.
(let ((x-and-width (logior #b00001 #b00100)))
(is-true (lime.x11::%ewmh-configure-request-includes-p x-and-width :x))
(is-false (lime.x11::%ewmh-configure-request-includes-p x-and-width :y))
(is-true (lime.x11::%ewmh-configure-request-includes-p x-and-width :width))
(is-false (lime.x11::%ewmh-configure-request-includes-p x-and-width :height))
(is-false (lime.x11::%ewmh-configure-request-includes-p x-and-width
:border-width))))
(defun make-fullscreen-render-fixture ()
"Build a server-free adapter fixture for fullscreen render planning."
(let* ((world (make-world))
(heads (make-hash-table :test #'equal))
(backend
(lime.x11::%make-x11-backend
:world world
:head-id "main"
:activity-id "play"
:focused-head-id "main"
:heads heads
:head-ids '("main"))))
(setf (gethash "main" heads)
(make-x11-head :head-id "main" :x 40 :y 30 :width 800 :height 600))
(create-head world "main")
(create-activity world "play" "Play")
(create-client world "game" "play" :name "Game")
(create-view world "play/home" "play" "Home"
(list (make-placement :client-id "game"))
:focus-client-id "game")
(activate-view-strictly world "main" "play/home")
;; No X connection is needed to plan. A truthy placeholder represents a
;; managed client; the intentionally hostile max hints prove fullscreen
;; bypasses regular tiled geometry constraining.
(setf (gethash "game" (lime.x11::x11-backend-client-windows backend)) :game
(gethash "game" (lime.x11::x11-backend-icccm-clients backend))
(lime.x11::make-icccm-client-info
:client-id "game"
:normal-hints (xlib:make-wm-size-hints :max-width 320 :max-height 240)))
backend))
(test fullscreen-rendering-owns-the-current-leased-head
(let* ((backend (make-fullscreen-render-fixture))
(fullscreen-clients (lime.x11::x11-backend-fullscreen-clients backend)))
(setf (gethash "game" fullscreen-clients) t)
(let ((geometry (cdr (assoc "game" (lime.x11::%active-client-geometries backend)
:test #'equal))))
(is (= (geometry-x geometry) 40))
(is (= (geometry-y geometry) 30))
(is (= (geometry-width geometry) 800))
(is (= (geometry-height geometry) 600)))
(let* ((effects (x11-render-plan-effects (build-render-plan backend)))
(focus-effect (car (last effects))))
(is (find :raise effects :key #'x11-effect-kind))
(is (eq (x11-effect-kind focus-effect) :focus))
(is (equal (x11-effect-client-id focus-effect) "game")))))
(defun make-head-bar-fixture ()
"Build a server-free two-client Presentation for Head-bar policy tests."
(let* ((world (make-world))
(heads (make-hash-table :test #'equal))
(backend
(lime.x11::%make-x11-backend
:world world :head-id "main" :activity-id "development"
:focused-head-id "main" :heads heads :head-ids '("main"))))
(setf (gethash "main" heads)
(make-x11-head :head-id "main" :x 40 :y 30 :width 800 :height 600))
(create-head world "main")
(create-activity world "development" "Development")
(create-client world "editor" "development" :name "Editor")
(create-client world "terminal" "development" :name "Terminal")
;; Browser begins owned by the Activity but absent from Code: it is the
;; small server-free stand-in for a parked/hidden task window.
(create-client world "browser" "development" :name "Browser")
(create-view world "development/code" "development" "Code"
(list (make-placement :client-id "editor")
(make-placement :client-id "terminal"))
:focus-client-id "editor")
(activate-view-strictly world "main" "development/code")
backend))
(defun make-appearance-fixture ()
(let ((backend (make-head-bar-fixture)))
(setf (lime.x11::x11-backend-window-appearance backend)
(lime-configuration-appearance config-dsl-fixture))
(dolist (id '("editor" "terminal" "browser"))
(setf (gethash id (lime.x11::x11-backend-client-windows backend)) id))
backend))
(defun configure-effect-for (backend client-id)
(find-if (lambda (effect)
(and (eq :configure (x11-effect-kind effect))
(equal client-id (x11-effect-client-id effect))))
(x11-render-plan-effects (build-render-plan backend))))
(defun frame-right (effect)
(let ((geometry (x11-effect-geometry effect)))
(+ (geometry-x geometry) (geometry-width geometry)
(* 2 (x11-effect-border-width effect)))))
(defun frame-bottom (effect)
(let ((geometry (x11-effect-geometry effect)))
(+ (geometry-y geometry) (geometry-height geometry)
(* 2 (x11-effect-border-width effect)))))
(test borders-and-odd-gaps-fit-within-the-work-area-without-mutating-views
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend))
(revision (world-revision world))
(presentation (current-presentation world "main"))
(editor (configure-effect-for backend "editor"))
(terminal (configure-effect-for backend "terminal")))
(is (= 3 (x11-effect-border-width editor)))
(is (= 45 (geometry-x (x11-effect-geometry editor))))
(is (= 57 (geometry-y (x11-effect-geometry editor))))
(is (= 5 (- (geometry-x (x11-effect-geometry terminal)) (frame-right editor))))
(is (= 835 (frame-right terminal)))
(is (= 625 (frame-bottom terminal)))
(is (string= "#ffac54" (x11-effect-border-color editor)))
(is (string= "#121f25" (x11-effect-border-color terminal)))
(is-false (configure-effect-for backend "browser"))
(is (= revision (world-revision world)))
(is-false (presentation-dirty-p presentation))
;; The same gap math works between weighted rows as between columns.
(show-client-on-presentation world "browser" (presentation-id presentation))
(set-presentation-master-stack-layout world (presentation-id presentation))
(set-client-tile-weight world (presentation-id presentation) "terminal" 2)
(let ((upper (configure-effect-for backend "terminal"))
(lower (configure-effect-for backend "browser")))
(is (= 5 (- (geometry-y (x11-effect-geometry lower)) (frame-bottom upper)))))
(set-presentation-master-stack-layout world (presentation-id presentation)
:orientation :vertical)
(let ((master (configure-effect-for backend "editor"))
(left (configure-effect-for backend "terminal"))
(right (configure-effect-for backend "browser")))
(is (= 5 (- (geometry-y (x11-effect-geometry left)) (frame-bottom master))))
(is (= 5 (- (geometry-x (x11-effect-geometry right)) (frame-right left)))))))
(test border-highlight-follows-global-focus-not-each-heads-local-focus
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend))
(presentation (current-presentation world "main")))
(focus-client-on-presentation world "terminal" (presentation-id presentation))
(is (string= "#121f25" (x11-effect-border-color (configure-effect-for backend "editor"))))
(is (string= "#ffac54" (x11-effect-border-color (configure-effect-for backend "terminal"))))
(setf (lime.x11::x11-backend-focus-root-override-p backend) t)
(is (string= "#121f25" (x11-effect-border-color (configure-effect-for backend "terminal"))))
(setf (lime.x11::x11-backend-focus-root-override-p backend) nil)
(create-head world "other")
(setf (gethash "other" (lime.x11::x11-backend-heads backend))
(make-x11-head :head-id "other" :x 840 :y 30 :width 800 :height 600)
(lime.x11::x11-backend-head-ids backend) '("main" "other"))
(create-view world "development/browser" "development" "Browser"
(list (make-placement :client-id "browser")) :focus-client-id "browser")
(activate-view-strictly world "other" "development/browser")
(is (string= "#121f25" (x11-effect-border-color (configure-effect-for backend "browser"))))
(setf (lime.x11::x11-backend-focused-head-id backend) "other")
(is (string= "#ffac54" (x11-effect-border-color (configure-effect-for backend "browser"))))
(is (string= "#121f25" (x11-effect-border-color (configure-effect-for backend "terminal"))))))
(test tiled-minimum-hints-cannot-overlap-neighbors-or-borders
(let ((backend (make-appearance-fixture)))
(setf (gethash "editor" (lime.x11::x11-backend-icccm-clients backend))
(lime.x11::make-icccm-client-info
:normal-hints (xlib:make-wm-size-hints :min-width 9000 :min-height 9000)))
(let ((editor (configure-effect-for backend "editor"))
(terminal (configure-effect-for backend "terminal")))
(is (= 5 (- (geometry-x (x11-effect-geometry terminal)) (frame-right editor))))
(is (= 625 (frame-bottom editor))))))
(test floating-frames-have-borders-but-no-tile-gaps
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend)))
(toggle-client-floating world (presentation-id (current-presentation world "main"))
"editor" :data '(:x 0 :y 0 :width 1 :height 1))
(let* ((effect (configure-effect-for backend "editor"))
(geometry (x11-effect-geometry effect)))
(is (= 3 (x11-effect-border-width effect)))
(is (= 40 (geometry-x geometry)))
(is (= 52 (geometry-y geometry)))
(is (= 840 (frame-right effect)))
(is (= 630 (frame-bottom effect))))))
(test fullscreen-removes-decoration-and-exiting-restores-it
(let* ((backend (make-appearance-fixture))
(table (lime.x11::x11-backend-fullscreen-clients backend)))
(setf (gethash "editor" table) t)
(let* ((effect (configure-effect-for backend "editor"))
(geometry (x11-effect-geometry effect)))
(is (= 0 (x11-effect-border-width effect)))
(is (= 40 (geometry-x geometry)))
(is (= 30 (geometry-y geometry)))
(is (= 800 (geometry-width geometry)))
(is (= 600 (geometry-height geometry))))
(remhash "editor" table)
(let ((effect (configure-effect-for backend "editor")))
(is (= 3 (x11-effect-border-width effect)))
(is (= 45 (geometry-x (x11-effect-geometry effect)))))))
(test monocle-is-undecorated-without-becoming-fullscreen
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend))
(presentation-id (presentation-id (current-presentation world "main"))))
(set-presentation-monocle-layout world presentation-id)
(let ((editor (configure-effect-for backend "editor")))
(is (= 0 (x11-effect-border-width editor)))
(is (= 40 (geometry-x (x11-effect-geometry editor))))
(is (= 52 (geometry-y (x11-effect-geometry editor))))
(is (= 840 (frame-right editor)))
(is (= 630 (frame-bottom editor)))
(is-false (configure-effect-for backend "terminal")))
;; Cycling changes which tile is displayed, not its decoration policy.
(focus-client-on-presentation world "terminal" presentation-id)
(is (= 0 (x11-effect-border-width (configure-effect-for backend "terminal"))))
(is-false (configure-effect-for backend "editor"))
;; Ordinary hints still apply; monocle is not application fullscreen.
(setf (gethash "terminal" (lime.x11::x11-backend-icccm-clients backend))
(lime.x11::make-icccm-client-info
:normal-hints (xlib:make-wm-size-hints :max-width 320 :max-height 240)))
(let ((geometry (x11-effect-geometry (configure-effect-for backend "terminal"))))
(is (= 320 (geometry-width geometry)))
(is (= 240 (geometry-height geometry))))
(remhash "terminal" (lime.x11::x11-backend-icccm-clients backend))
;; Switching layouts restores the configured decoration, not a cached zero.
(set-presentation-master-stack-layout world presentation-id)
(let ((editor (configure-effect-for backend "editor")))
(is (= 3 (x11-effect-border-width editor)))
(is (= 45 (geometry-x (x11-effect-geometry editor)))))))
(test monocle-does-not-remove-floating-client-borders
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend))
(presentation-id (presentation-id (current-presentation world "main"))))
(toggle-client-floating world presentation-id "terminal"
:data '(:x 1/4 :y 1/4 :width 1/2 :height 1/2))
(set-presentation-monocle-layout world presentation-id)
(focus-client-on-presentation world "terminal" presentation-id)
(is (= 3 (x11-effect-border-width (configure-effect-for backend "terminal"))))
(is (string= "#ffac54"
(x11-effect-border-color (configure-effect-for backend "terminal"))))
(is (= 0 (x11-effect-border-width (configure-effect-for backend "editor"))))))
(test smart-gaps-make-one-tile-undecorated-in-every-layout
(dolist (mode '(:manual :master-stack :monocle))
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend))
(presentation (current-presentation world "main"))
(id (presentation-id presentation)))
(hide-client-from-presentation world "terminal" id)
(set-presentation-layout-mode world id mode)
(let* ((revision (world-revision world))
(dirty-p (presentation-dirty-p presentation))
(effect (configure-effect-for backend "editor"))
(geometry (x11-effect-geometry effect)))
(is (= 0 (x11-effect-border-width effect)))
(is (= 40 (geometry-x geometry)))
(is (= 52 (geometry-y geometry)))
(is (= 800 (geometry-width geometry)))
(is (= 578 (geometry-height geometry)))
(is (= revision (world-revision world)))
(is (eql dirty-p (presentation-dirty-p presentation)))))))
(test smart-gaps-follow-hide-show-float-unfloat-and-close
(dolist (mode '(:manual :master-stack))
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend))
(id (presentation-id (current-presentation world "main"))))
(set-presentation-layout-mode world id mode)
(hide-client-from-presentation world "terminal" id)
(is (= 0 (x11-effect-border-width (configure-effect-for backend "editor"))))
(show-client-on-presentation world "terminal" id)
(let ((editor (configure-effect-for backend "editor")))
(is (= 3 (x11-effect-border-width editor)))
(is (= 45 (geometry-x (x11-effect-geometry editor)))))
(toggle-client-floating world id "terminal")
(is (= 0 (x11-effect-border-width (configure-effect-for backend "editor"))))
(is (= 3 (x11-effect-border-width (configure-effect-for backend "terminal"))))
(toggle-client-floating world id "editor")
;; Zero tiles does not mean floats should lose their borders.
(is (= 3 (x11-effect-border-width (configure-effect-for backend "editor"))))
(is (= 3 (x11-effect-border-width (configure-effect-for backend "terminal"))))
(toggle-client-floating world id "editor")
(toggle-client-floating world id "terminal")
(is (= 3 (x11-effect-border-width (configure-effect-for backend "editor"))))
(retire-client world "terminal")
(let ((editor (configure-effect-for backend "editor")))
(is (= 0 (x11-effect-border-width editor)))
(is (= 40 (geometry-x (x11-effect-geometry editor))))))))
(test smart-gaps-count-only-this-heads-leased-tiles
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend)))
(create-head world "other")
(setf (gethash "other" (lime.x11::x11-backend-heads backend))
(make-x11-head :head-id "other" :x 840 :y 30 :width 800 :height 600)
(lime.x11::x11-backend-head-ids backend) '("main" "other"))
(create-view world "development/browser" "development" "Browser"
(list (make-placement :client-id "browser")))
(activate-view-strictly world "other" "development/browser")
(is (= 3 (x11-effect-border-width (configure-effect-for backend "editor"))))
(is (= 0 (x11-effect-border-width (configure-effect-for backend "browser"))))
;; A saved slot remaining on main must not count once its lease moves.
(create-view world "development/pair" "development" "Pair"
(list (make-placement :client-id "browser")
(make-placement :client-id "terminal")))
(activate-view-taking world "other" "development/pair")
(is (= 0 (x11-effect-border-width (configure-effect-for backend "editor"))))
(is (= 3 (x11-effect-border-width (configure-effect-for backend "browser"))))
(is (= 3 (x11-effect-border-width (configure-effect-for backend "terminal"))))))
(test smart-gaps-preserve-normal-hints-and-configured-style
(let* ((backend (make-appearance-fixture))
(world (lime.x11::x11-backend-world backend))
(id (presentation-id (current-presentation world "main"))))
(hide-client-from-presentation world "terminal" id)
(setf (gethash "editor" (lime.x11::x11-backend-icccm-clients backend))
(lime.x11::make-icccm-client-info
:normal-hints (xlib:make-wm-size-hints :max-width 320 :max-height 240)))
(let* ((effect (configure-effect-for backend "editor"))
(geometry (x11-effect-geometry effect))
(style (lime.x11::x11-backend-window-appearance backend)))
(is (= 0 (x11-effect-border-width effect)))
(is (= 52 (geometry-y geometry)))
(is (= 320 (geometry-width geometry)))
(is (= 240 (geometry-height geometry)))
(is (= 3 (window-appearance-border-width style)))
(is (= 5 (window-appearance-inner-gap style)))
(is (= 5 (window-appearance-outer-gap style))))))
(test oversized-decoration-degrades-to-a-contained-one-pixel-client
(let ((backend (make-appearance-fixture)))
(setf (lime.x11::x11-backend-window-appearance backend)
(make-window-appearance :border-width 65535 :inner-gap 65535 :outer-gap 65535))
(let* ((effect (configure-effect-for backend "editor"))
(geometry (x11-effect-geometry effect)))
(is (= 0 (x11-effect-border-width effect)))
(is (= 1 (geometry-width geometry)))
(is (= 1 (geometry-height geometry)))
(is (<= 40 (geometry-x geometry) 839))
(is (<= 52 (geometry-y geometry) 629)))))
(test appearance-configuration-validates-before-installation
(let ((style (lime-configuration-appearance config-dsl-fixture)))
(is (= 3 (window-appearance-border-width style)))
(is (= 5 (window-appearance-inner-gap style)))
(is (= 5 (window-appearance-outer-gap style)))
(is (string= "#ffac54" (window-appearance-focused-border style))))
(dolist (style (list (make-window-appearance :border-width -1)
(make-window-appearance :inner-gap 1/2)
(make-window-appearance :outer-gap 65536)
(make-window-appearance :focused-border "#gg2233")
(make-window-appearance :unfocused-border "blue")
nil))
(signals invalid-transition
(lime.x11::%validate-lime-configuration (make-lime-configuration :appearance style)))))
(test head-bars-reserve-the-normal-client-work-area
(let* ((backend (make-head-bar-fixture))
(work-area (x11-head-work-area backend "main"))
(presentation (current-presentation (lime.x11::x11-backend-world backend)
"main"))
(geometries (lime.x11::%active-client-geometries backend))
(editor (cdr (assoc "editor" geometries :test #'equal))))
(is (= (x11-work-area-x work-area) 40))
(is (= (x11-work-area-y work-area) 52))
(is (= (x11-work-area-width work-area) 800))
(is (= (x11-work-area-height work-area) 578))
;; The default manual tree receives the work area rather than the physical
;; Head, so even its first tile cannot draw underneath the persistent bar.
(is (= (geometry-y editor) 52))
(is (= (geometry-height editor) 578))
(setf (presentation-layout-mode presentation) :master-stack)
(is (equal (lime.x11::%bar-layout-token backend "main") "[]="))
(setf (presentation-layout-mode presentation) :manual)
(is (equal (lime.x11::%bar-layout-token backend "main") "|M|"))
(setf (presentation-layout-mode presentation) :monocle)
(is (equal (lime.x11::%bar-layout-token backend "main") "[2]"))))
(test monocle-bar-count-includes-visible-tile-but-not-floats-or-hidden-clients
(let* ((backend (make-head-bar-fixture))
(world (lime.x11::x11-backend-world backend))
(presentation-id (presentation-id (current-presentation world "main"))))
(set-presentation-monocle-layout world presentation-id)
;; Editor and terminal participate; the parked browser does not.
(is (equal "[2]" (lime.x11::%bar-layout-token backend "main")))
(focus-client-on-presentation world "terminal" presentation-id)
(is (equal "[2]" (lime.x11::%bar-layout-token backend "main")))
(hide-client-from-presentation world "terminal" presentation-id)
(is (equal "[1]" (lime.x11::%bar-layout-token backend "main")))
(show-client-on-presentation world "terminal" presentation-id)
(is (equal "[2]" (lime.x11::%bar-layout-token backend "main")))
(toggle-client-floating world presentation-id "terminal")
(is (equal "[1]" (lime.x11::%bar-layout-token backend "main")))
(hide-client-from-presentation world "editor" presentation-id)
(is (equal "[0]" (lime.x11::%bar-layout-token backend "main")))
(hide-client-from-presentation world "terminal" presentation-id)
(is (equal "[0]" (lime.x11::%bar-layout-token backend "main")))
(show-client-on-presentation world "editor" presentation-id)
(is (equal "[1]" (lime.x11::%bar-layout-token backend "main")))))
(test monocle-bar-count-excludes-tiles-leased-to-another-head
(let* ((backend (make-head-bar-fixture))
(world (lime.x11::x11-backend-world backend)))
(set-presentation-monocle-layout world
(presentation-id (current-presentation world "main")))
(create-head world "other")
(create-view world "development/other" "development" "Other"
(list (make-placement :client-id "terminal")))
(activate-view-taking world "other" "development/other")
(is (equal "[1]" (lime.x11::%bar-layout-token backend "main")))))
(defun bar-test-text-width (text)
"Deliberately unequal advances to catch character-count approximations."
(loop for character across text
sum (case character (#\W 11) (#\i 3) (#\. 2) (otherwise 6))))
(test bar-truncation-fits-pixels-including-ellipsis
(let ((measure #'bar-test-text-width))
(is (string= "WWiiii" (lime.x11::%bar-short-text "WWiiii" 34 measure)))
(multiple-value-bind (text retained)
(lime.x11::%bar-short-text "WWiiii" 20 measure)
(is (string= "W..." text))
(is (= 1 retained))
(is (<= (funcall measure text) 20)))
(is (string= "..." (lime.x11::%bar-short-text "WWiiii" 6 measure)))
(is (string= "i" (lime.x11::%bar-short-text "iiWW" 5 measure)))
(is (string= "" (lime.x11::%bar-short-text "WWiiii" 1 measure)))
(is (string= "" (lime.x11::%bar-short-text "WWiiii" 0 measure)))
(is (string= "" (lime.x11::%bar-short-text "" 30 measure)))))
(test bar-layout-uses-measured-separators-and-a-left-aligned-title
(let* ((measure #'bar-test-text-width)
(narrow (lime.x11::%bar-text-layout 1000 "iii" "[]=" "Title" "Status" measure))
(wide (lime.x11::%bar-text-layout 1000 "WWW" "[]=" "Title" "Status" measure)))
(is (= 10 (getf narrow :logo-x)))
(is (= (+ 10 (funcall measure "Lime")) (getf narrow :logo-width)))
(is (= (funcall measure " | ")
(- (getf narrow :label-x) (getf narrow :logo-x) (funcall measure "Lime"))))
(is (= (funcall measure " | ") (- (getf narrow :layout-x)
(+ (getf narrow :label-x) (funcall measure "iii")))))
(is (= (- (funcall measure "WWW") (funcall measure "iii"))
(- (getf wide :layout-x) (getf narrow :layout-x))))
(is (= 990 (+ (getf wide :status-x) (funcall measure (getf wide :status)))))
(let* ((title-left (+ (getf wide :layout-x) (funcall measure "[]=") 8))
(title-right (- (getf wide :status-x) 8)))
(is (= title-left (getf wide :title-left)))
(is (= title-right (getf wide :title-right)))
(is (= (+ title-left 11) (getf wide :title-x))))))
(test long-bar-label-is-truncated-by-pixels-before-positioning-layout
(let* ((measure #'bar-test-text-width)
(layout (lime.x11::%bar-text-layout
300 (make-string 100 :initial-element #\W) "|M|" "Title" "" measure))
(label (getf layout :label)))
(is (string= "..." (subseq label (- (length label) 3))))
(is (= (+ (getf layout :label-x) (funcall measure label)
(funcall measure " | "))
(getf layout :layout-x)))
(is (<= (+ (getf layout :layout-x) (funcall measure "|M|") 8)
(getf layout :title-x)))))
(test bar-padding-insets-text-and-remains-part-of-layout-not-hit-test-guesswork
(dolist (padding '(0 6 10 24 128))
(let ((layout (lime.x11::%bar-text-layout 2000 "Work:Code" "[]=" "Title" "Status"
#'bar-test-text-width :padding-x padding)))
(is (= padding (getf layout :logo-x)))
(is (= (+ padding (bar-test-text-width "Lime")) (getf layout :logo-width)))
(is (= (- 2000 padding) (+ (getf layout :status-x) (bar-test-text-width "Status"))))
(is (= (+ (getf layout :title-left) 1 padding) (getf layout :title-x)))
(is (= (- (getf layout :title-right) 1 padding)
(+ (getf layout :title-x) (getf layout :title-width)))))))
(test bar-fields-stay-contained-and-separated-on-narrow-heads
(dolist (width '(0 1 6 12 24 50 100 160 320 1000))
(dolist (padding '(0 10 128))
(let* ((measure #'bar-test-text-width)
(layout (lime.x11::%bar-text-layout
width "Long Activity:Long View*" "[123]"
"WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
"WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
measure :padding-x padding))
(previous-right (getf layout :logo-width)))
(is (<= 0 previous-right width))
(is (<= 0 (getf layout :title-left) (getf layout :title-right) width))
(dolist (span (getf layout :separators))
(is (<= 0 (car span) (+ (car span) (funcall measure (cdr span))) width)))
(dolist (keys '((:label :label-x) (:layout-token :layout-x)
(:title :title-x) (:status :status-x)))
(let ((text (getf layout (first keys)))
(x (getf layout (second keys))))
(when (plusp (length text))
(is (<= previous-right x (+ x (funcall measure text)) width))
(setf previous-right (+ x (funcall measure text))))))))))
(test status-hit-spans-use-visible-measured-text-only
(let* ((action (lambda (&rest arguments) (declare (ignore arguments))))
(first (make-head-bar-segment :text "Wi" :click-action action))
(second (make-head-bar-segment :text "iiW" :click-action action))
(segments (list first second)))
(is (equal (list (list 100 114 first) (list 132 149 second))
(lime.x11::%bar-status-hit-spans segments 8 100 #'bar-test-text-width)))
;; Prefix ends partway through the second segment, before truncation dots.
(is (equal (list (list 100 114 first) (list 132 138 second))
(lime.x11::%bar-status-hit-spans segments 7 100 #'bar-test-text-width)))
;; Prefix ends within the separator: no phantom second click target.
(is (equal (list (list 100 114 first))
(lime.x11::%bar-status-hit-spans segments 3 100 #'bar-test-text-width)))
(is-false (lime.x11::%bar-status-hit-spans segments 0 100 #'bar-test-text-width))))
(test bar-title-anchor-is-stable-when-title-or-status-length-changes
(let* ((measure #'bar-test-text-width)
(one (lime.x11::%bar-text-layout 1000 "WM:Code*" "[]=" "Short" "" measure))
(two (lime.x11::%bar-text-layout 1000 "WM:Code*" "[]=" "A longer title" "Music | 25%" measure)))
(is (= (getf one :title-x) (getf two :title-x)))
(is (= 1000 (getf one :title-right)))
(is (< (getf two :title-right) 1000))
(is (= 2 (length (getf one :separators))))
(dolist (span (getf one :separators))
(is (string= " | " (cdr span))))))
(test bar-colors-and-optional-title-edges-are-validated-config-data
(is (string= "#52616b"
(head-bar-style-title-edge-color (lime-configuration-bar-style config-dsl-fixture))))
(let* ((configuration (make-lime-configuration))
(style (lime-configuration-bar-style configuration)))
(is (= 10 (head-bar-style-padding-x style)))
(dolist (bad '(-1 129 1/2 nil))
(setf (head-bar-style-padding-x style) bad)
(signals invalid-transition (lime.x11::%validate-lime-configuration configuration)))
(setf (head-bar-style-padding-x style) 10)
(is (string= "#40535d" (head-bar-style-title-edge-color style)))
(setf (head-bar-style-title-edge-color style) nil)
(is (eq configuration (lime.x11::%validate-lime-configuration configuration)))
(setf (head-bar-style-title-edge-color style) "blue")
(signals invalid-transition (lime.x11::%validate-lime-configuration configuration))
(setf (head-bar-style-title-edge-color style) nil
(head-bar-style-separator-color style) nil)
(signals invalid-transition (lime.x11::%validate-lime-configuration configuration)))
(let ((configuration
(eval '(progn
(lime.config:defconfiguration edge-free-fixture
(:bar :title-edge-color nil :background "#123456" :padding-x 16))
edge-free-fixture))))
(is-false (head-bar-style-title-edge-color (lime-configuration-bar-style configuration)))
(is (= 16 (head-bar-style-padding-x (lime-configuration-bar-style configuration))))
(is (string= "#123456" (head-bar-style-background (lime-configuration-bar-style configuration))))))
(test logo-color-is-optional-and-independent-of-other-bar-text
(let* ((configuration
(eval '(progn
(lime.config:defconfiguration logo-color-fixture
(:bar :foreground "#dbc9a6" :logo-color "#768565"))
logo-color-fixture)))
(style (lime-configuration-bar-style configuration)))
(is (eq configuration (lime.x11::%validate-lime-configuration configuration)))
(is (string= "#768565" (head-bar-style-logo-color style)))
(is (string= "#dbc9a6" (head-bar-style-foreground style)))
(is (member "#768565" (lime.x11::%head-bar-style-colors style) :test #'equal))
(setf (head-bar-style-logo-color style) nil)
(is (eq configuration (lime.x11::%validate-lime-configuration configuration)))
(is-false (head-bar-style-logo-color (make-head-bar-style)))
(dolist (bad '("green" "#12345" 42))
(setf (head-bar-style-logo-color style) bad)
(signals invalid-transition (lime.x11::%validate-lime-configuration configuration)))))
(test menu-accents-and-labels-are-independent-of-title-and-separators
(let* ((configuration
(eval '(progn
(lime.config:defconfiguration independent-menu-colors-fixture
(:bar :focused-foreground "#dbc9a6" :separator-color "#686868"
:accent-color "#ffb054" :menu-label-color "#dbc9a6"))
independent-menu-colors-fixture)))
(style (lime-configuration-bar-style configuration)))
(is (eq configuration (lime.x11::%validate-lime-configuration configuration)))
(is (string= "#dbc9a6" (head-bar-style-focused-foreground style)))
(is (string= "#ffb054" (lime.x11::%ui-accent-color style)))
(is (string= "#dbc9a6" (lime.x11::%ui-menu-label-color style)))
(setf (head-bar-style-accent-color style) nil
(head-bar-style-menu-label-color style) nil)
(is (string= "#dbc9a6" (lime.x11::%ui-accent-color style)))
(is (string= "#686868" (lime.x11::%ui-menu-label-color style)))
(setf (head-bar-style-accent-color style) "yellow")
(signals invalid-transition (lime.x11::%validate-lime-configuration configuration))
(setf (head-bar-style-accent-color style) nil
(head-bar-style-menu-label-color style) "cream")
(signals invalid-transition (lime.x11::%validate-lime-configuration configuration))))
(test status-separator-coloring-never-mistakes-provider-content-for-a-divider
(let ((segments (list (make-head-bar-segment :text "a|b")
(make-head-bar-segment :text "end"))))
(is (equal '((118 . " | "))
(lime.x11::%bar-status-separator-spans segments 9 100 #'bar-test-text-width)))
(is-false (lime.x11::%bar-status-separator-spans segments 3 100 #'bar-test-text-width))
(is (equal '((118 . " "))
(lime.x11::%bar-status-separator-spans segments 4 100 #'bar-test-text-width)))))
(test bar-title-fallbacks-are-human-readable
(let* ((backend (make-head-bar-fixture))
(info (lime.x11::make-icccm-client-info :title "Editor title"
:resource-class "Editor" :resource-name "editor")))
(setf (gethash "editor" (lime.x11::x11-backend-icccm-clients backend)) info)
(is (string= "Editor title" (lime.x11::%bar-focused-title backend "main")))
(setf (icccm-client-info-title info) "")
(is (string= "Editor" (lime.x11::%bar-focused-title backend "main")))
(setf (icccm-client-info-resource-class info) nil)
(is (string= "editor" (lime.x11::%bar-focused-title backend "main")))
(remhash "editor" (lime.x11::x11-backend-icccm-clients backend))
(is (string= "Untitled window" (lime.x11::%bar-focused-title backend "main")))))
(test single-line-text-preserves-unicode-but-not-control-characters
(is (string= "café — 日本語 🎮 👩‍💻 ‘quoted’"
(lime.x11::%single-line-text "café — 日本語 🎮 👩‍💻 ‘quoted’")))
(is (string= "a b" (lime.x11::%single-line-text (format nil "a~%b"))))
(is (string= "a b" (lime.x11::%single-line-text (format nil "a~Cb" #\Null)))))
(test head-bar-labels-identify-activities-not-checkpoint-state
(let* ((backend (make-head-bar-fixture))
(world (lime.x11::x11-backend-world backend))
(presentation (current-presentation world "main")))
(is (equal (lime.x11::%bar-presentation-label backend "main")
"Development"))
(setf (presentation-dirty-p presentation) t)
(is (equal (lime.x11::%bar-presentation-label backend "main")
"Development"))
(is (presentation-dirty-p presentation))
(is (equal "development/code" (presentation-source-view-id presentation)))
;; A zero reservation cleanly recovers the full physical Head for an
;; intentionally bar-less configuration.
(setf (lime.x11::x11-backend-head-bar-height backend) 0)
(let ((work-area (x11-head-work-area backend "main")))
(is (= (x11-work-area-y work-area) 30))
(is (= (x11-work-area-height work-area) 600)))))
(test configuration-dsl-keeps-commands-bindings-rules-and-bar-policy-separate
(let ((configuration config-dsl-fixture))
;; This is the complete validation step that happens before a real backend
;; releases/regrabs any key or changes its visible bar state.
(is (eq configuration
(lime.x11::%validate-lime-configuration configuration)))
(is-true (lime-configuration-default-keybindings-p configuration))
(is (equal (lime-configuration-default-modifiers configuration) '(:mod-4)))
(is (eq :monocle (lime-configuration-default-layout configuration)))
(is-false (lime-configuration-new-client-focus-p configuration))
(is (eq (lime-configuration-new-tiled-client-position configuration) :front))
(is (eq (lime-configuration-close-focus-direction configuration) :next))
(let ((monocle-policy
(find :monocle
(lime-configuration-layout-client-flow-policies configuration)
:key #'layout-client-flow-policy-layout-mode)))
(is-true monocle-policy)
(is (eq (layout-client-flow-policy-new-tiled-client-position monocle-policy)
:back))
(is (eq (layout-client-flow-policy-close-focus-direction monocle-policy)
:previous)))
(is (= (lime-configuration-bar-height configuration) 24))
(is-true (lime-configuration-bar-height-supplied-p configuration))
(is-true (lime-configuration-status-providers-supplied-p configuration))
(is (equal (mapcar #'lime-command-id
(lime-configuration-commands configuration))
'(terminal)))
(is (equal (mapcar #'configuration-binding-command-id
(lime-configuration-bindings configuration))
'(terminal)))
(is (equal (mapcar #'window-rule-id (lime-configuration-rules configuration))
'(mpv fallback)))))
(test default-layout-is-adapter-policy-and-does-not-replace-existing-work
(let* ((backend (make-head-bar-fixture))
(world (lime.x11::x11-backend-world backend))
(existing (current-presentation world "main")))
(is (eq :master-stack (lime.x11::configured-default-layout backend)))
(let ((draft (lime.x11::%ensure-active-presentation world "other" "scratch")))
(is (eq :master-stack (presentation-layout-mode draft)))
(is-false (presentation-source-view-id draft)))
(is (eq existing (lime.x11::%ensure-active-presentation world "main" "development")))
(is (eq :manual (presentation-layout-mode existing)))
(setf (lime.x11::x11-backend-configuration backend) config-dsl-fixture)
(is (eq :monocle (lime.x11::configured-default-layout backend)))
;; A portable Activity keeps its existing layout when brought here.
(is (eq :master-stack
(activation-report-desired-layout-mode
(backend-preflight-activity-activation backend "scratch"))))
(create-activity world "new-layout" "New")
(is (eq :monocle (activation-report-desired-layout-mode
(backend-preflight-activity-activation backend "new-layout"))))
(dolist (mode '(:manual :master-stack :monocle))
(is (typep (lime.x11::%validate-lime-configuration
(make-lime-configuration :default-layout mode))
'lime-configuration)))
(dolist (mode '(nil :floating :bogus "master-stack"))
(signals invalid-transition
(lime.x11::%validate-lime-configuration
(make-lime-configuration :default-layout mode))))))
(test layout-client-flow-policies-override-the-ordinary-client-flow-fallback
(let ((backend (make-head-bar-fixture)))
;; The built-in policy is useful even with no configuration file at all.
(is (eq (lime.x11::configured-new-tiled-client-position backend :monocle)
:front))
(is (eq (lime.x11::configured-close-focus-direction backend :monocle)
:next))
(setf (lime.x11::x11-backend-configuration backend) config-dsl-fixture)
;; The fixture's ordinary policy applies to master/stack and manual; its
;; explicit monocle entry overrides only monocle.
(is (eq (lime.x11::configured-new-tiled-client-position
backend :master-stack)
:front))
(is (eq (lime.x11::configured-close-focus-direction
backend :master-stack)
:next))
(is (eq (lime.x11::configured-new-tiled-client-position backend :monocle)
:back))
(is (eq (lime.x11::configured-close-focus-direction backend :monocle)
:previous))))
(test configured-commands-power-both-direct-search-and-which-key
(let ((backend (make-head-bar-fixture))
(*configured-command-runs* 0))
(setf (lime.x11::x11-backend-configuration backend) config-dsl-fixture)
(let* ((candidate (first (lime.x11::configuration-command-candidates backend)))
(result (funcall (ui-candidate-action candidate))))
(is (equal (ui-candidate-id candidate) '(:configured-command terminal)))
(is (equal (ui-candidate-label candidate) "Command: Terminal"))
(is (= *configured-command-runs* 1))
(is (eq (ui-result-kind result) :close)))
(let ((session (make-ui-session '()
:key-path (lime.x11::%ui-key-path backend))))
(ui-session-handle-key session :tab)
(let ((user-node (find :user-commands (ui-session-key-nodes session)
:key #'ui-key-node-id)))
(is-true user-node)
(is (equal (mapcar #'ui-key-node-key (ui-key-node-children user-node))
'(#\t)))))))
(test palette-offers-a-dmenu-like-run-prompt-and-starter-launches
(let* ((backend (make-head-bar-fixture))
(candidates (lime.x11::%command-palette-candidates backend)))
(is (find :run-shell-command candidates :key #'ui-candidate-id))
(is (find :launch-terminal candidates :key #'ui-candidate-id))
(is (find :launch-browser candidates :key #'ui-candidate-id))
(let ((session (make-ui-session '()
:key-path (lime.x11::%ui-key-path backend))))
(ui-session-handle-key session :tab)
(ui-session-handle-key session #\r)
(is (find :run-command (ui-session-key-nodes session)
:key #'ui-key-node-id)))))
(test hiding-and-showing-activity-clients-update-only-the-live-draft
(let* ((backend (make-head-bar-fixture))
(world (lime.x11::x11-backend-world backend))
(presentation (current-presentation world "main"))
(apply-symbol 'lime.x11:apply-active-presentation)
(original-apply (symbol-function apply-symbol)))
;; Command tests use the real core/model and command surfaces, but replace
;; the one CLX execution seam. A separate nested-X test covers actual
;; map/unmap; this fixture keeps state semantics deterministic.
(unwind-protect
(progn
(setf (symbol-function apply-symbol)
(lambda (ignored-backend)
(declare (ignore ignored-backend))
backend))
(backend-hide-focused-client backend)
(is (equal (presentation-client-ids presentation) '("terminal")))
(is (equal (getf (backend-status backend) :hidden-client-ids)
'("editor" "browser")))
(is-false (lease-for-client world "editor"))
(is (equal (presentation-focus-client-id presentation) "terminal"))
(let ((show-editor
(find '(:show-hidden-client "editor")
(lime.x11::%ui-hidden-client-candidates backend)
:key #'ui-candidate-id :test #'equal)))
(is-true show-editor)
(is (equal (ui-candidate-label show-editor) "Show: Editor")))
(backend-show-client backend "editor")
(is (equal (presentation-client-ids presentation)
'("terminal" "editor")))
(is (equal (presentation-focus-client-id presentation) "editor"))
(is (equal (getf (backend-status backend) :hidden-client-ids)
'("browser")))
(let* ((key-root (lime.x11::%ui-key-path backend))
(window-node (find :window key-root :key #'ui-key-node-id))
(activity-node (find :activity key-root :key #'ui-key-node-id)))
(is-true (find :hide-focused-client (ui-key-node-children window-node)
:key #'ui-key-node-id))
(is-true (find :show-hidden-client (ui-key-node-children window-node)
:key #'ui-key-node-id))
(is-false (find :show-hidden-client (ui-key-node-children activity-node)
:key #'ui-key-node-id))))
(setf (symbol-function apply-symbol) original-apply))))
(test showing-a-hidden-client-can-explicitly-take-its-lease-from-another-head
(let* ((backend (make-head-bar-fixture))
(world (lime.x11::x11-backend-world backend))
(heads (lime.x11::x11-backend-heads backend))
(source (current-presentation world "main"))
(apply-symbol 'lime.x11:apply-active-presentation)
(original-apply (symbol-function apply-symbol)))
(setf (gethash "other" heads)
(make-x11-head :head-id "other" :x 840 :y 30 :width 800 :height 600)
(lime.x11::x11-backend-head-ids backend) '("main" "other"))
(create-head world "other")
(let ((target (open-activity-draft world "other" "development")))
(unwind-protect
(progn
(setf (symbol-function apply-symbol)
(lambda (ignored-backend)
(declare (ignore ignored-backend))
backend))
;; Editor begins visible on main and absent from the other draft.
;; The taking command must make the cross-Head move explicit,
;; without pretending the same real X window can appear twice.
(backend-show-client-taking backend "editor" :head-id "other")
(is (equal (lease-presentation-id (lease-for-client world "editor"))
(presentation-id target)))
(is (member "editor" (presentation-client-ids target) :test #'equal))
(is (equal (presentation-focus-client-id target) "editor"))
(is-false (member "editor" (presentation-client-ids source) :test #'equal))
(is (equal (presentation-focus-client-id source) "terminal")))
(setf (symbol-function apply-symbol) original-apply)))))
(test path-program-picker-exposes-one-safe-launch-candidate-per-program
(let ((backend (make-head-bar-fixture))
(function-name 'lime.x11::%path-program-names))
(let ((original-function (symbol-function function-name)))
(unwind-protect
(progn
(setf (symbol-function function-name)
(lambda () '("emacs" "st" "waterfox")))
(let ((candidates (lime.x11::%ui-program-launch-candidates backend)))
(is (equal (mapcar #'ui-candidate-id candidates)
'((:program "emacs") (:program "st") (:program "waterfox"))))
(is (equal (mapcar #'ui-candidate-detail candidates)
'("PATH executable" "PATH executable"
"Waterfox (normal profile)")))))
(setf (symbol-function function-name) original-function)))))
(test program-catalog-normalization-is-distinct-and-alphabetical
(is (equal '("emacs" "st" "waterfox")
(lime.x11::%normalize-program-names '("st" nil "waterfox" "" "st" 42 "emacs")))))
(test program-catalog-cache-invalidation-and-empty-results
(let ((lime.x11::*path-program-cache* nil)
(signature '("path-a" "cwd-a" (1 2)))
(programs '("emacs" "st")) (scans 0)
(original-signature (symbol-function 'lime.x11::%path-program-signature))
(original-scan (symbol-function 'lime.x11::%scan-path-program-names)))
(unwind-protect
(progn
(setf (symbol-function 'lime.x11::%path-program-signature) (lambda () signature)
(symbol-function 'lime.x11::%scan-path-program-names) (lambda () (incf scans) programs))
(is (equal programs (lime.x11::%path-program-names)))
(is (equal programs (lime.x11::%path-program-names)))
(is (= 1 scans))
(setf (car (lime.x11::%path-program-names)) "changed by caller")
(is (equal programs (lime.x11::%path-program-names)))
(dolist (new-signature '(("path-b" "cwd-a" (1 2))
("path-b" "cwd-b" (1 2))
("path-b" "cwd-b" (1 3))))
(setf signature new-signature)
(lime.x11::%path-program-names))
(is (= 4 scans))
(lime.x11::%path-program-names :refresh-p t)
(is (= 5 scans))
(decf (getf lime.x11::*path-program-cache* :time) (* 301 internal-time-units-per-second))
(lime.x11::%path-program-names)
(is (= 6 scans))
(setf programs nil)
(is-false (lime.x11::%path-program-names :refresh-p t))
(is-false (lime.x11::%path-program-names))
(is (= 7 scans)))
(setf (symbol-function 'lime.x11::%path-program-signature) original-signature
(symbol-function 'lime.x11::%scan-path-program-names) original-scan))))
(test shortcut-dispatch-always-releases-queued-keyboard-events
(dolist (outcome '(:success :error :missing :nonlocal))
(let* ((backend (make-head-bar-fixture)) (calls nil)
(allow (symbol-function 'xlib:allow-events))
(flush (symbol-function 'xlib:display-force-output)))
(setf (lime.x11::x11-backend-display backend) :fake-display)
(unless (eq outcome :missing)
(setf (gethash (cons 42 64) (lime.x11::x11-backend-keybindings backend))
(lime.x11::make-keybinding
:handler (lambda (b) (declare (ignore b))
(case outcome (:error (error "test failure"))
(:nonlocal (throw 'dispatch-done :left)))))))
(unwind-protect
(progn
(setf (symbol-function 'xlib:allow-events)
(lambda (display mode &optional time) (declare (ignore time))
(push (list display mode) calls))
(symbol-function 'xlib:display-force-output)
(lambda (display) (push (list display :flush) calls)))
(catch 'dispatch-done (lime.x11::%handle-key-press backend 42 64))
(is (equal '((:fake-display :flush) (:fake-display :async-keyboard)) calls))
(is (eq (eq outcome :error) (not (null (backend-last-command-error backend))))))
(setf (symbol-function 'xlib:allow-events) allow
(symbol-function 'xlib:display-force-output) flush)))))
(test browser-launch-defaults-to-normal-profile-and-isolation-is-explicit
(let ((original-launch (symbol-function 'backend-launch-program))
(original-profile (symbol-function 'lime.x11::%lime-browser-profile-directory))
(profile-requests 0)
(launched nil))
(unwind-protect
(progn
;; Do not spawn a browser or create/write an actual profile.
(setf (symbol-function 'backend-launch-program)
(lambda (backend argv) (declare (ignore backend)) (setf launched argv))
(symbol-function 'lime.x11::%lime-browser-profile-directory)
(lambda () (incf profile-requests) "/tmp/lime-test-profile/"))
(backend-launch-browser nil)
(is (equal '("waterfox") launched))
(is (zerop profile-requests))
(backend-launch-browser nil :test-profile-p t)
(is (equal '("waterfox" "--new-instance" "--profile" "/tmp/lime-test-profile/") launched))
(is (= 1 profile-requests)))
(setf (symbol-function 'backend-launch-program) original-launch
(symbol-function 'lime.x11::%lime-browser-profile-directory) original-profile))))
(test launcher-rejects-blank-shell-commands-before-starting-a-process
(signals invalid-transition
(lime.x11::%nonblank-launch-command (format nil " ~C " #\Tab)))
(is (equal (lime.x11::%nonblank-launch-command " st -e htop ")
"st -e htop")))
(test input-registers-lock-insensitive-variants-of-a-binding-state
(let* ((base-state (xlib:make-state-mask :mod-4 :shift))
(variants (lime.x11::%input-state-variants base-state)))
(is (= (length variants) 4))
(is (member base-state variants))
(is (member (logior base-state (xlib:make-state-mask :lock)) variants))
(is (member (logior base-state (xlib:make-state-mask :mod-2)) variants))
(is (member (logior base-state
(xlib:make-state-mask :lock :mod-2))
variants))))
(test graceful-quit-is-a-command-request-and-a-palette-entry
(let ((backend (make-head-bar-fixture)))
;; The command changes only the event-loop flag. Its owner performs the
;; actual CLX/root cleanup after the current X event has finished.
(setf (lime.x11::x11-backend-running-p backend) t)
(is (eq (backend-request-quit backend) backend))
(is-false (lime.x11::x11-backend-running-p backend))
(let ((candidate (find :quit-lime
(lime.x11::%command-palette-candidates backend)
:key #'ui-candidate-id)))
(is-true candidate)
(is (equal (ui-candidate-label candidate) "Quit Lime"))
(is (eq (ui-result-kind (funcall (ui-candidate-action candidate))) :close)))
(let ((session (make-ui-session '()
:key-path (lime.x11::%ui-key-path backend))))
(ui-session-handle-key session :tab)
(ui-session-handle-key session #\q)
(is (find :quit-lime (ui-session-key-nodes session)
:key #'ui-key-node-id)))))
(test window-rules-use-the-first-icccm-match-without-changing-an-activity
(let* ((backend (make-head-bar-fixture))
(info (lime.x11::make-icccm-client-info
:client-id "player" :resource-name "mpv" :resource-class "mpv"
:title "movie")))
(setf (lime.x11::x11-backend-configuration backend) config-dsl-fixture
(gethash "player" (lime.x11::x11-backend-icccm-clients backend)) info)
(let ((rule (lime.x11::%matching-window-rule backend "player" info)))
(is (eq (window-rule-id rule) 'mpv))
(is-true (window-rule-float-p rule))
(is-true (window-rule-center-p rule)))))
(test loading-a-configuration-file-records-one-reloadable-definition
(multiple-value-bind (configuration name)
(load-lime-configuration
(merge-pathnames "test/config-fixture.lisp"
(asdf:system-source-directory "lime/x11")))
(is (eq name 'config-file-fixture))
(is (typep configuration 'lime-configuration))
;; The no-:OPTIONS default is the normal Super modifier list, not quoted
;; source data accidentally left unevaluated by the macro.
(is (equal (lime-configuration-default-modifiers configuration) '(:mod-4)))
(is (eq :master-stack (lime-configuration-default-layout configuration)))
(is-true (lime-configuration-new-client-focus-p configuration))
(is (eq (lime-configuration-new-tiled-client-position configuration) :back))
(is (eq (lime-configuration-close-focus-direction configuration) :previous))
(is (equal (mapcar #'lime-command-id
(lime-configuration-commands configuration))
'(reload)))))
(test randr-groups-cloned-outputs-into-one-logical-head
(flet ((output (id name crtc connection x y width height)
(lime.x11::make-x11-randr-output
:output-id id :name name :crtc-id crtc :connection connection
:x x :y y :width width :height height)))
(let ((heads
(lime.x11::%x11-randr-heads-from-outputs
(list
(output 101 "DP-1" 11 :connected 0 0 1920 1080)
;; HDMI-1 mirrors DP-1 through the same CRTC: one physical
;; rectangle must never become two overlapping Lime Heads.
(output 102 "HDMI-1" 11 :connected 0 0 1920 1080)
(output 201 "DP-3" 12 :connected 1920 0 1920 1080)
(output 301 "DP-4" 0 :disconnected 0 0 0 0))
201)))
(is (equal (mapcar #'x11-head-head-id heads) '("DP-1" "DP-3")))
(let ((left (first heads))
(right (second heads)))
(is (equal (x11-head-output-ids left) '(101 102)))
(is (= (x11-head-width left) 1920))
(is (= (x11-head-x right) 1920))
;; The primary output is retained as its own representative Head.
(is (= (x11-head-output-id right) 201))))))
(test randr-topology-report-keeps-choice-separate-from-application
(let ((world (make-world)))
(create-head world "left")
(create-head world "right")
(create-activity world "lime" "Lime")
(create-client world "emacs" "lime")
(create-client world "firefox" "lime")
(create-view world "code" "lime" "Code"
(list (make-placement :client-id "emacs")))
(create-view world "browser" "lime" "Browser"
(list (make-placement :client-id "firefox")))
(activate-view-strictly world "left" "code")
(let* ((browser (activate-view-strictly world "right" "browser"))
(backend
(lime.x11::%make-x11-backend
:world world
:head-source :randr
:randr-topology
(lime.x11::make-x11-randr-topology
:heads (list (make-x11-head :head-id "left" :x 0 :y 0
:width 1000 :height 700)
(make-x11-head :head-id "dock" :x 1000 :y 0
:width 1000 :height 700)))))
(snapshot (backend-randr-topology-report backend))
(report (getf snapshot :report))
(plan
(backend-build-randr-topology-plan
backend
:moves (list (make-head-topology-move
:from-head-id "right" :to-head-id "dock")))))
(is (typep report 'head-topology-report))
(is (equal (getf snapshot :added-head-ids) '("dock")))
(is (equal (getf snapshot :removed-head-ids) '("right")))
(is (equal (mapcar #'head-topology-source-presentation-id
(getf snapshot :removed-active-sources))
(list (presentation-id browser))))
;; Building the plan is read-only: a palette/IPC caller can still cancel.
(is (current-presentation world "right"))
(is (equal (mapcar #'head-topology-move-to-head-id
(head-topology-plan-moves plan))
'("dock"))))))
(test backend-status-describes-a-deliberately-blank-head
(let* ((world (make-world))
(heads (make-hash-table :test #'equal))
(backend
(lime.x11::%make-x11-backend
:world world :head-id "new" :focused-head-id "new"
:heads heads :head-ids '("new"))))
(create-head world "new")
(setf (gethash "new" heads)
(make-x11-head :head-id "new" :x 0 :y 0 :width 800 :height 600))
(let ((status (backend-status backend)))
(is-true (getf status :blank-p))
(is-false (getf status :activity-id))
(is-false (getf status :leased-client-ids)))))
(defun make-randr-palette-fixture ()
"Build a pending physical-topology change without requiring an X server."
(let* ((world (make-world))
(heads (make-hash-table :test #'equal)))
(create-head world "left")
(create-head world "right")
(create-activity world "lime" "Lime")
(create-client world "emacs" "lime")
(create-client world "firefox" "lime")
(create-view world "code" "lime" "Code"
(list (make-placement :client-id "emacs")))
(create-view world "browser" "lime" "Browser"
(list (make-placement :client-id "firefox")))
(activate-view-strictly world "left" "code")
(activate-view-strictly world "right" "browser")
(setf (gethash "left" heads)
(make-x11-head :head-id "left" :x 0 :y 0 :width 1000 :height 700)
(gethash "right" heads)
(make-x11-head :head-id "right" :x 1000 :y 0 :width 1000 :height 700))
(lime.x11::%make-x11-backend
:world world :head-id "left" :activity-id "lime" :focused-head-id "left"
:heads heads :head-ids '("left" "right") :head-source :randr
:randr-topology-dirty-p t
:randr-topology
(lime.x11::make-x11-randr-topology
:heads (list (make-x11-head :head-id "left" :x 0 :y 0
:width 1000 :height 700)
(make-x11-head :head-id "dock" :x 1000 :y 0
:width 1000 :height 700))))))
(test randr-palette-offers-explicit-move-or-suspend-choices
(let* ((backend (make-randr-palette-fixture))
(direct-candidate
(find :reconcile-randr-topology
(lime.x11::%command-palette-candidates backend)
:key #'ui-candidate-id))
(choice-result (funcall (ui-candidate-action direct-candidate)))
(choice-labels (mapcar #'ui-candidate-label
(ui-result-candidates choice-result))))
(is-true direct-candidate)
(is (eq (ui-result-kind choice-result) :replace))
(is (member "Move to left" choice-labels :test #'equal))
(is (member "Move to dock" choice-labels :test #'equal))
(is (member "Suspend safely" choice-labels :test #'equal))
(let* ((suspend-candidate
(find "Suspend safely" (ui-result-candidates choice-result)
:key #'ui-candidate-label :test #'equal))
(final-result (funcall (ui-candidate-action suspend-candidate))))
(is (eq (ui-result-kind final-result) :replace))
(is (find "Apply display plan" (ui-result-candidates final-result)
:key #'ui-candidate-label :test #'equal)))
(let ((session (make-ui-session '()
:key-path (lime.x11::%ui-key-path backend))))
(ui-session-handle-key session :tab)
(ui-session-handle-key session #\h)
(is (find :reconcile-randr-topology (ui-session-key-nodes session)
:key #'ui-key-node-id))
(is (eq (ui-result-kind (ui-session-handle-key session #\t)) :replace)))))
(test viewsets-appear-in-the-command-and-which-key-surfaces
(let* ((backend (make-randr-palette-fixture))
(viewset (backend-save-viewset backend "work-desk" "Work desk"))
(candidate (find (list :viewset "work-desk")
(lime.x11::%command-palette-candidates backend)
:key #'ui-candidate-id :test #'equal)))
;; Capture is a mapping of durable source Views, never a serialization of
;; the mutable Presentations currently mounted on these Heads.
(is (equal (viewset-name viewset) "Work desk"))
(is (equal (mapcar #'viewset-entry-head-id (viewset-entries viewset))
'("left" "right")))
(is (equal (mapcar #'viewset-entry-view-id (viewset-entries viewset))
'("code" "browser")))
(is-true candidate)
(is (equal (ui-candidate-label candidate) "Viewset: Work desk"))
(is (search "left -> Lime / Code" (ui-candidate-detail candidate)))
(let ((session (make-ui-session '()
:key-path (lime.x11::%ui-key-path backend))))
(ui-session-handle-key session :tab)
(is (find :viewset (ui-session-key-nodes session)
:key #'ui-key-node-id))
(ui-session-handle-key session #\x)
(is (find :open-viewset (ui-session-key-nodes session)
:key #'ui-key-node-id)))))
(test super-y-builds-a-which-key-root-without-changing-the-world
(let* ((backend (make-head-bar-fixture))
(world (lime.x11::x11-backend-world backend))
(revision (world-revision world))
(session (lime.x11::%make-command-menu-session backend))
(entries (lime.x11::%ui-menu-entries session)))
(is (eq :key-path (ui-session-mode session)))
(is-false (ui-session-candidates session))
(is (find "+Windows" entries :key (lambda (entry) (getf entry :label)) :test #'equal))
(is (find "SPC" entries :key (lambda (entry) (getf entry :key)) :test #'equal))
(ui-session-handle-key session #\w)
(is (string= "Lime › w Windows" (lime.x11::%ui-menu-heading session)))
(is (find :hide-focused-client (ui-session-key-nodes session) :key #'ui-key-node-id))
(ui-session-handle-key session :backspace)
(is (string= "Lime" (lime.x11::%ui-menu-heading session)))
(ui-session-handle-key session #\Space)
(is (eq :search (ui-session-mode session)))
(is (= revision (world-revision world)))))
(test which-key-grid-is-compact-paged-and-bounded-by-its-head
(let ((entries (loop for i below 41 collect (list :key "x" :label (format nil "Command ~D" i)))))
(let ((wide (lime.x11::%ui-key-grid-layout (subseq entries 0 9) 1600 500 28 #'bar-test-text-width)))
(is (= 4 (getf wide :columns)))
(is (= 3 (getf wide :rows)))
(is (< (getf wide :height) 200)))
(dolist (width '(1 100 320 1000 1600))
(dolist (height '(1 70 240 500))
(let* ((layout (lime.x11::%ui-key-grid-layout entries width height 28 #'bar-test-text-width))
(shown nil))
(is (<= 1 (getf layout :height) height))
(is (<= 0 (+ (* 2 (getf layout :padding))
(* (getf layout :columns) (getf layout :cell-width))
(* (1- (getf layout :columns)) (getf layout :gap))) width))
(when (plusp (getf layout :capacity))
(dotimes (page (getf layout :page-count))
(let ((part (lime.x11::%ui-key-grid-layout entries width height 28 #'bar-test-text-width page)))
(setf shown (append shown (subseq entries (getf part :start) (getf part :end))))))
(is (equal entries shown)))
(let ((last (lime.x11::%ui-key-grid-layout entries width height 28 #'bar-test-text-width 999)))
(is (= (1- (getf last :page-count)) (getf last :page)))))))))
(defun run-tests ()
(run! 'lime-x11-suite))