Initial Lime window manager

This commit is contained in:
2026-10-02 14:14:47 -05:00
commit ccd691edd0
121 changed files with 29719 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
;;;; Real windows, keyboard/bar navigation, geometry, and restart on OWN Xvfb.
(require :asdf)
(asdf:load-asd (truename (merge-pathnames "../lime.asd" *load-truename*)))
(asdf:load-system "lime/x11")
(lime.config:defconfiguration portable-smoke-config (:navigation-keys))
(defun run-portable-activity-smoke ()
(let ((server nil) (client nil) (backend nil) (checks 0))
(unwind-protect
(let* ((directory (uiop:ensure-directory-pathname
(string-trim '(#\Newline)
(uiop:run-program '("mktemp" "-d" "/tmp/lime-portable-XXXXXX") :output :string))))
(file (merge-pathnames "session.sexp" directory)))
(setf server (uiop:launch-program '("Xvfb" "-displayfd" "1" "-screen" "0" "1200x700x24" "-nolisten" "tcp")
:output :stream :error-output :interactive))
(let* ((name (format nil ":~D" (parse-integer (read-line (uiop:process-info-output server)))))
(heads (list (lime.x11:make-x11-head :head-id "left" :x 0 :y 0 :width 700 :height 700)
(lime.x11:make-x11-head :head-id "right" :x 700 :y 0 :width 500 :height 700))))
(setf client (xlib:open-default-display name))
(labels ((check (truth label) (assert truth () "Portable Activities: ~A" label) (incf checks))
(world () (lime.x11:x11-backend-world backend))
(current (head) (lime.core:current-presentation (world) head))
(start ()
(setf backend (lime.x11:start-lime :display-name name :heads heads :session-file file
:install-default-keybindings-p nil))
(lime.x11:apply-lime-configuration backend portable-smoke-config))
(pump ()
(xlib:display-finish-output client)
(let ((count 0))
(lime.x11:run-event-loop backend :timeout 0.005
:before-event
(lambda (b)
(assert (< (incf count) 600) () "Event queue did not settle.")
(xlib:display-finish-output (lime.x11::x11-backend-display b))
(unless (xlib:event-listen (lime.x11::x11-backend-display b))
(setf (lime.x11::x11-backend-running-p b) nil))))))
(spawn ()
(let ((window (xlib:create-window :parent (xlib:screen-root (xlib:display-default-screen client))
:x 0 :y 0 :width 150 :height 100)))
(xlib:map-window window) (pump)
(lime.x11::%backend-client-id backend window)))
(window (id) (gethash id (lime.x11::x11-backend-client-windows backend)))
(visible (id) (eq :viewable (xlib:window-map-state (window id))))
(tap-activity (head activity)
(lime.x11:backend-select-head backend head)
(let* ((display (lime.x11::x11-backend-display backend))
(super (first (nth-value 6 (xlib:modifier-mapping display))))
(slot (lime.x11:backend-activity-slot backend activity))
(code (xlib:keysym->keycodes display (+ (char-code #\0) slot))))
(xlib/xtest:fake-key-event client super t)
(xlib/xtest:fake-key-event client code t)
(xlib/xtest:fake-key-event client code nil)
(xlib/xtest:fake-key-event client super nil) (pump)))
(click-activity (head activity)
(let* ((bar (gethash head (lime.x11::x11-backend-head-bars backend)))
(entries (lime.x11::%bar-activity-entries backend head))
(index (position activity entries :key (lambda (e) (getf e :id)) :test #'equal))
(region (nth (1+ index) (lime.x11::x11-head-bar-hit-regions bar)))
(bar-window (lime.x11::x11-head-bar-window bar)))
(xlib/xtest:fake-motion-event client
(+ (xlib:drawable-x bar-window)
(floor (+ (lime.x11::x11-head-bar-hit-region-left region)
(lime.x11::x11-head-bar-hit-region-right region)) 2)) 10)
(pump) (xlib/xtest:fake-button-event client 1 t)
(xlib/xtest:fake-button-event client 1 nil) (pump))))
(start)
(lime.x11:backend-start-activity backend "work" "Work" :head-id "left")
(let ((browser (spawn)) (work (current "left")))
(lime.x11:backend-select-head backend "right")
(lime.x11:backend-start-activity backend "chat" "Chat")
(let* ((discord (spawn)) (hidden (spawn)) (chat (current "right")))
(lime.x11:backend-hide-client backend hidden)
(lime.x11:backend-save-named-view backend "Chat")
(lime.x11:backend-toggle-client-floating backend discord)
(let ((lease (lime.core:lease-for-client (world) discord))
(placement (copy-tree (lime.core:placement-data (first (lime.core:presentation-placements chat))))))
(tap-activity "left" "chat")
(check (eq chat (current "left")) "numeric key brings original arrangement")
(check (eq work (current "right")) "numeric key swaps displaced activity")
(check (visible discord) "Discord stays visible")
(check (visible browser) "browser stays visible")
(check (not (visible hidden)) "hidden window stays hidden")
(check (eq lease (lime.core:lease-for-client (world) discord)) "lease identity survives swap")
(check (equal placement (lime.core:placement-data (first (lime.core:presentation-placements chat))))
"normalized floating geometry survives monitor size changes")
(check (< (xlib:drawable-x (window discord)) 700) "Discord is on left in X")
(check (>= (xlib:drawable-x (window browser)) 700) "browser is on right in X")
(check (= (xlib:window-id (window discord)) (xlib:window-id (xlib:input-focus client)))
"keyboard focus is on incoming window")
(click-activity "right" "chat")
(check (eq chat (current "right")) "bar click uses same portable behavior")
(check (eq work (current "left")) "bar click swaps back")
(check (equal "right" (lime.x11:x11-backend-focused-head-id backend)) "clicked monitor retains command focus")
(check (null (lime.x11::x11-backend-ui-session backend)) "no unnecessary collision menu")
(dotimes (i 12)
(tap-activity (if (evenp i) "left" "right") "chat")
(check (lime.core:validate-world (world)) "repeated swaps preserve model")))
(lime.x11:backend-select-head backend "right")
(lime.x11:backend-start-activity backend "other" "Other")
(let ((other (current "right")))
(tap-activity "left" "chat")
(check (eq other (current "right")) "inactive return leaves other monitor alone")
(check (eq chat (current "left")) "inactive arrangement follows target monitor"))
(lime.x11:backend-extend-activity-to-head backend "chat" "right")
(check (eq chat (current "left")) "extension leaves original portion alone")
(check (null (lime.core:presentation-client-ids (current "right"))) "new portion does not gather windows")
(check (eq :extended (lime.x11:backend-activity-navigation-mode backend "chat")) "explicit extended mode")
(tap-activity "right" "other") (tap-activity "right" "chat")
(check (null (lime.core:presentation-client-ids (current "right"))) "extended empty portion intentionally resumes")
(check (eq chat (current "left")) "extended returns do not pull the other portion")
(tap-activity "right" "other")
(lime.x11:backend-select-head backend "left")
(lime.x11:backend-make-activity-portable backend "chat" "left")
(check (eq :portable (lime.x11:backend-activity-navigation-mode backend "chat")) "explicit return to portability")
(check (not (visible hidden)) "fold does not show hidden window")
;; Leave an intentionally empty extended activity for restart.
(lime.x11:backend-extend-activity-to-head backend "other" "right")
(lime.x11:backend-save-session backend)
(let ((before (lime.x11::%session-snapshot backend)))
(check (= 3 (getf before :version)) "versioned footprint persistence")
(check (lime.x11::%validate-session-envelope before) "saved envelope validates"))
(lime.x11:stop-backend backend) (setf backend nil)
(start) (pump)
(check (not (getf (lime.x11:backend-session-status backend) :blocked-p)) "restart restore succeeds")
(check (eq :portable (lime.x11:backend-activity-navigation-mode backend "chat")) "portable state survives restart")
(check (eq :extended (lime.x11:backend-activity-navigation-mode backend "other")) "empty extended intent survives restart")
(check (not (visible hidden)) "hidden window remains hidden after restart")
(tap-activity "right" "chat")
(check (equal "chat" (lime.core:presentation-activity-id (current "right"))) "portable key works after restart")
(check (visible discord) "surviving application reconnected and moved")
(check (null (lime.x11::x11-backend-last-command-error backend)) "no command errors")
(check (lime.core:validate-world (world)) "final invariants")
(format t "~&Portable Activities smoke: ~D checks passed.~%" checks))))))
(when backend (ignore-errors (lime.x11:stop-backend backend)))
(when client (ignore-errors (xlib:close-display client :abort t)))
(when server (uiop:terminate-process server) (uiop:wait-process server)))))
(handler-case (run-portable-activity-smoke)
(error (c) (format *error-output* "~&Portable Activities smoke failed: ~A~%" c) (uiop:quit 1)))