1543 lines
84 KiB
Common Lisp
1543 lines
84 KiB
Common Lisp
(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))
|