(in-package #:lime.x11.test) (in-suite lime-x11-suite) (test status-formatters (is (= 60 (lime.x11::%next-minute-delay 120))) (is (= 1 (lime.x11::%next-minute-delay 179))) (is (< (abs (- 0.25d0 (lime.x11::%next-minute-delay 179.75d0))) 0.00001)) (let ((data (lime.x11::%clock-snapshot (encode-universal-time 59 7 15 30 9 2026)))) (is (= 15 (getf data :hour))) (is (= 2026 (getf data :year))) (is (string= " Wed Sep 30, 03:07" (lime.x11:format-clock-status data))) (is (string= " Wed Sep 30, 15:07" (lime.x11:format-clock-status data :twenty-four-hour-p t)))) (is (string= " Wed Sep 30, 12:00" (lime.x11:format-clock-status (lime.x11::%clock-snapshot (encode-universal-time 0 0 0 30 9 2026))))) (is (= 25 (lime.x11::%volume-percent "Volume: front-left: 16384 / 25% / -36.12 dB, front-right: 16384 / 25%"))) (is (= 150 (lime.x11::%volume-percent "mono: 98304 / 150% / 1.23 dB"))) (signals error (lime.x11::%volume-percent "no sink")) (is (lime.x11::%volume-event-p "Event 'change' on sink #42")) (is (lime.x11::%volume-event-p "Event 'change' on server #0")) (is (lime.x11::%volume-event-p "Event 'remove' on card #1")) (is (not (lime.x11::%volume-event-p "Event 'new' on client #9"))) (is (not (lime.x11::%volume-event-p "Event 'change' on sink-input #9"))) (is (equal '(:state :playing :title "スパイダー" :source :mpd) (lime.x11::%music-snapshot "play" "スパイダー" :mpd))) (is (string= " スパイダー" (lime.x11:format-music-status (lime.x11::%music-snapshot "play" "スパイダー" :mpd)))) (let ((data (lime.x11::%mpris-line-snapshot (format nil "Paused~Chello" #\Tab) "spotify"))) (is (eq :paused (getf data :state))) (is (equal "spotify" (getf data :source))) (is (string= " hello" (lime.x11:format-music-status data)))) (is (null (lime.x11::%mpris-line-snapshot "" "spotify"))) (let ((data (lime.x11::%music-snapshot "stop" "old track" :mpd))) (is (equal "old track" (getf data :title))) (is (eq :stopped (getf data :state))) (is (null (lime.x11:format-music-status data)))) (is (string= " Untitled track" (lime.x11:format-music-status (lime.x11::%music-snapshot "play" "" :mpd)))) (is (string= " 25%" (lime.x11:format-volume-status '(:volume 25 :muted-p nil)))) (is (string= " Muted" (lime.x11:format-volume-status '(:volume 25 :muted-p t)))) (is (string= "\"a\\\"b\\\\c\"" (lime.x11::%mpd-quote "a\"b\\c"))) (signals error (lime.x11::%mpd-quote (format nil "foo~%bar")))) (test status-icons-and-gaps-are-presentation-options (let ((font (lime.x11::%open-text-font (lime.x11:make-ui-font-style :family "Dina, Fira Code Nerd Font, Noto Color Emoji" :size 12 :weight :bold)))) (unwind-protect (dolist (pair '(("" "Song") ("" "スパイダー") ("" "25%") ("" "Muted") ("" "Wed Sep 30, 03:07"))) (destructuring-bind (icon label) pair (let ((text (lime.x11::%status-icon-label icon label " "))) (is (string= text (concatenate 'string icon " " label))) (is (> (lime.x11::%text-width font text) (lime.x11::%text-width font (concatenate 'string icon " " label))))))) (lime.x11::%free-text-font font))) (is (string= "PLAY · My track" (lime.x11:format-music-status '(:state :playing :title "My track") :playing-icon "PLAY" :icon-gap " · "))) (is (string= "Quiet" (lime.x11:format-volume-status '(:muted-p t :volume 30) :muted-icon nil :muted-label "Quiet")))) (test status-icon-colors-preserve-text-and-provider-behavior (let* ((click (lambda (&rest ignored) (declare (ignore ignored)))) (provider (make-volume-status-provider :foreground "#dbc9a6" :maximum-width 100 :click-action click :formatter (lambda (data) (format-volume-status data :icon-foreground "#76929f")))) (segment (lime.x11::%format-status-segment provider '(:volume 25)))) (is (string= " 25%" (head-bar-segment-text segment))) (is (string= "#dbc9a6" (head-bar-segment-foreground segment))) (is (string= "#76929f" (head-bar-segment-prefix-foreground segment))) (is (= 1 (head-bar-segment-prefix-length segment))) (is (= 100 (head-bar-segment-maximum-width segment))) (is (eq click (head-bar-segment-click-action segment)))) (dolist (segment (list (format-music-status '(:state :paused :title "スパイダー") :paused-icon "PAUSE" :icon-foreground "#76929f") (format-volume-status '(:muted-p t) :icon-foreground "#76929f") (format-clock-status (lime.x11::%clock-snapshot (get-universal-time)) :icon-foreground "#76929f"))) (is (typep segment 'head-bar-segment)) (is (string= "#76929f" (head-bar-segment-prefix-foreground segment))) (is (null (head-bar-segment-foreground segment)))) (is (string= "25%" (format-volume-status '(:volume 25) :icon nil :icon-foreground "#76929f"))) (is (null (format-music-status '(:state :stopped) :icon-foreground "#76929f"))) (signals invalid-transition (format-volume-status '(:volume 25) :icon-foreground "blue")) (dolist (bad '(-1 99 1/2 nil)) (signals invalid-transition (lime.x11::%validate-head-bar-segment (make-head-bar-segment :text "test" :prefix-length bad))))) (test status-provider-validation (let ((provider (lime.x11:make-clock-status-provider))) (is (equal (list provider) (lime.x11::%validate-status-providers (list provider)))) (is (lime.x11::%validate-status-providers (list provider (lambda (backend head) (declare (ignore backend head)))))) (signals lime.core:invalid-transition (lime.x11::%validate-status-providers (list provider (lime.x11:make-clock-status-provider))))) (signals lime.core:invalid-transition (lime.x11::%validate-status-providers (list (lime.x11:make-status-provider :id :broken)))) (signals error (lime.x11:make-music-status-provider :source :unknown)) (signals error (lime.x11:make-music-status-provider :maximum-width 0)) (signals invalid-transition (lime.x11::%validate-status-providers (list (lime.x11:make-clock-status-provider :formatter nil)))) (signals invalid-transition (lime.x11::%validate-status-providers (list (lime.x11:make-volume-status-provider :foreground "red")))) (is (typep (lime.x11:make-music-status-provider :source "brave" :sources '("brave")) 'lime.x11:status-provider))) (test status-data-is-snapshotted-and-formatted-only-by-the-consumer (let* ((calls 0) (runtime (lime.x11::make-status-runtime :closed-p t)) (input (list :state :playing :title (copy-seq "Original"))) (provider (lime.x11:make-music-status-provider :formatter (lambda (data) (incf calls) ;; Even a mutating formatter cannot damage the cache. (setf (char (getf data :title) 0) #\X) (getf data :title)))) (worker (lime.x11::make-status-worker :runtime runtime :provider provider))) (let ((returned (lime.x11:status-publish worker input))) (setf (char (getf returned :title) 0) #\Y)) (setf (getf input :state) :paused (char (getf input :title) 0) #\Z) (is (zerop calls)) (is (equal '(:state :playing :title "Original") (lime.x11::%status-worker-snapshot worker))) (let ((segment (lime.x11::%format-status-segment provider (lime.x11::%status-worker-snapshot worker)))) (is (= 1 calls)) (is (string= "Xriginal" (head-bar-segment-text segment))) (is (equal '(:state :playing :title "Original") (lime.x11::%status-worker-snapshot worker)))) ;; NIL means unavailable, not a formatting callback with incomplete data. (is (null (lime.x11::%format-status-segment provider nil))) (is (= 1 calls)))) (test status-formatters-can-return-colored-segments-or-hide-data (let* ((click (lambda (backend head) (declare (ignore backend head)))) (provider (lime.x11:make-volume-status-provider :foreground "#aabbcc" :maximum-width 90 :click-action click)) (value '(:volume 25 :muted-p nil :sink "fixture"))) (let ((segment (lime.x11::%format-status-segment provider value))) (is (eq :volume (head-bar-segment-id segment))) (is (equal "#aabbcc" (lime.x11:head-bar-segment-foreground segment))) (is (= 90 (head-bar-segment-maximum-width segment))) (is (eq click (head-bar-segment-click-action segment)))) (setf (lime.x11:status-provider-formatter provider) (lambda (data) (make-head-bar-segment :text (format nil "Volume ~D" (getf data :volume)) :foreground "#112233" :maximum-width 40))) (let ((segment (lime.x11::%format-status-segment provider value))) (is (string= "Volume 25" (head-bar-segment-text segment))) (is (equal "#112233" (lime.x11:head-bar-segment-foreground segment))) (is (= 40 (head-bar-segment-maximum-width segment))) (is (eq click (head-bar-segment-click-action segment)))) (setf (lime.x11:status-provider-formatter provider) (constantly nil)) (is (null (lime.x11::%format-status-segment provider value))) (setf (lime.x11:status-provider-formatter provider) (lambda (data) (declare (ignore data)) (make-head-bar-segment :text "bad" :foreground "not a color"))) (signals invalid-transition (lime.x11::%format-status-segment provider value)))) (test custom-status-formatting-can-show-stopped-music-and-differing-date-styles (let* ((music (lime.x11:make-music-status-provider :formatter (lambda (data) (format nil "~(~A~): ~A" (getf data :state) (getf data :title))))) (clock (lime.x11:make-clock-status-provider :formatter (lambda (data) (format nil "~D-~2,'0D-~2,'0D" (getf data :year) (getf data :month) (getf data :day)))))) (is (string= "stopped: Track" (head-bar-segment-text (lime.x11::%format-status-segment music (lime.x11::%music-snapshot "stop" "Track" :mpd))))) (is (string= "2026-09-30" (head-bar-segment-text (lime.x11::%format-status-segment clock (lime.x11::%clock-snapshot (encode-universal-time 0 0 0 30 9 2026)))))))) (test status-publication-and-cancellation (let* ((runtime (lime.x11::make-status-runtime :closed-p t)) (worker (lime.x11::make-status-worker :runtime runtime))) (lime.x11:status-publish worker "first") (is (string= "first" (lime.x11::status-worker-value worker))) (lime.x11:status-publish worker nil) (is (null (lime.x11::status-worker-value worker))) (lime.x11::%stop-status-worker worker) (lime.x11:status-publish worker "late result") (is (null (lime.x11::status-worker-value worker))) (is (not (lime.x11:status-wait worker 60))) (let ((cancelled nil) (ran nil)) (lime.x11::%call-with-status-resource worker (lambda () (setf cancelled t)) (lambda () (setf ran t))) (is (eq t cancelled)) (is (not ran))))) (test volume-subscription-coalesces-bursts-without-polling (let* ((worker (lime.x11::make-status-worker :runtime (lime.x11::make-status-runtime :closed-p t))) (snapshots 0) (events (format nil "Event 'new' on client #1~%Event 'change' on sink #2~%Event 'change' on sink #2~%Event 'change' on server #0~%"))) (signals error ; finite fixture EOF is a normal source disconnect (lime.x11::%run-volume-status worker :subscribe (lambda (ignored argv action) (declare (ignore ignored)) (is (equal '("pactl" "subscribe") argv)) (with-input-from-string (stream events) (funcall action stream nil))) :snapshot (lambda (ignored) (declare (ignore ignored)) (format nil "snapshot ~D" (incf snapshots))))) (is (= 2 snapshots)) ; initial + one burst, not 3 events or an interval (is (string= "snapshot 2" (lime.x11::status-worker-value worker))))) (test widget-width-cap-keeps-following-widgets-and-graphemes (let ((font (lime.x11::%open-text-font (lime.x11:make-ui-font-style)))) (unwind-protect (let* ((music (lime.x11:make-head-bar-segment :id :music :text "👩‍💻👩‍💻👩‍💻 a long track" :maximum-width 65)) (clock (lime.x11:make-head-bar-segment :id :clock :text "12:34")) (runs (nth-value 3 (lime.x11::%fit-bar-status font (list music clock) 500))) (first (first runs))) (is (eq music (getf first :segment))) (is (<= (getf first :width) 65)) (is (find (getf first :prefix) (lime.x11::text-metrics-boundaries (lime.x11::%text-metrics font (lime.x11:head-bar-segment-text music))))) (is (eq clock (getf (car (last runs)) :segment)))) (lime.x11::%free-text-font font)))) (test music-source-switching-and-command-routing (let* ((provider (lime.x11:make-music-status-provider :mpd-host "localhost" :mpd-port 6601)) (runtime (lime.x11::make-status-runtime :closed-p t)) (backend (lime.x11::%make-x11-backend :status-runtime runtime :head-bar-status-providers (list provider))) (old-worker nil)) ;; Exercise the real lifecycle with a test source, never actual playback. (setf (lime.x11::status-provider-run provider) (lambda (worker) (lime.x11:status-wait worker 60)) old-worker (lime.x11::%start-status-worker runtime provider) (lime.x11::status-runtime-workers runtime) (list old-worker)) (unwind-protect (progn (is (equal '("mpc" "--host" "localhost" "--port" "6601" "prev") (lime.x11::%music-command-argv provider :previous))) (is (equal "spotify" (lime.x11:backend-cycle-music-source backend))) (is (not (bt:thread-alive-p (lime.x11::status-worker-thread old-worker)))) (is (= 1 (length (lime.x11::status-runtime-workers runtime)))) (is (equal '("playerctl" "--player" "spotify" "play-pause") (lime.x11::%music-command-argv provider :toggle))) (is (equal '("playerctl" "--player" "spotify" "previous") (lime.x11::%music-command-argv provider :previous))) (is (eq :mpd (lime.x11:backend-cycle-music-source backend))) (signals error (lime.x11::%music-command-argv provider :invalid))) (mapc #'lime.x11::%stop-status-worker (lime.x11::status-runtime-workers runtime)))))