;; Two on-screen widgets for the thin client: now-playing and the idle-gallery ;; weather/clock overlay. Installed to ;; /home//.config/eww/eww.yuck by build-thin-client-iso.sh. ;; ;; Data sources are separate `deflisten`/`defpoll` processes, NOT thinclient_agent — ;; now-playing uses /usr/local/bin/now-playing-json (`playerctl --follow`), the ;; weather overlay uses /usr/local/bin/weather-json (MQTT). The agent is a *system* ;; service that deliberately outlives the compositor, whereas both widgets are ;; session-scoped and die with sway; wiring either to the agent would mean either ;; restarting the agent whenever a widget restarts, or opening a second inbound ;; control channel into it, which the security boundary in mqtt_discovery.py rules ;; out. The cost is one extra subscription each, which is negligible. ;; ;; The :onclick strings below are fixed constants in this file. Nothing from MQTT, HA, ;; or player metadata is ever interpolated into them. (defvar players "mpv,spotifyd,%any") (deflisten nowplaying :initial '{"visible":false,"title":"","artist":"","art":"","controls":false}' "/usr/local/bin/now-playing-json") (defwidget np-meta [] (box :orientation "vertical" :space-evenly false :halign "start" :valign "center" :hexpand true (label :class "np-title" :text nowplaying.title :halign "start" :limit-width 34 :truncate true) (label :class "np-artist" :text nowplaying.artist :halign "start" :limit-width 34 :truncate true))) ;; Hidden rather than absent when the player exposes no transport control (some ;; sources only report metadata) — the widget then degrades to art + text. (defwidget np-controls [] (box :class "np-controls" :orientation "horizontal" :space-evenly true :spacing 4 :visible {nowplaying.controls} :valign "center" (button :class "np-btn" :onclick "playerctl -p ${players} previous" "⏮") (button :class "np-btn" :onclick "playerctl -p ${players} play-pause" "⏯") (button :class "np-btn" :onclick "playerctl -p ${players} next" "⏭"))) (defwidget np-root [] (box :class "np-root" :orientation "horizontal" :space-evenly false :spacing 12 :visible {nowplaying.visible} (image :class "np-art" :path nowplaying.art :image-width 72 :image-height 72) (np-meta) (np-controls))) ;; :exclusive false — the widget floats over whatever is on screen and must never ;; reserve layer-shell space, or it would shrink the fullscreen media surfaces this ;; whole image exists to show. Visibility is owned by fullscreen-watcher.sh, which ;; opens and closes this window; the :visible binding above only handles "nothing is ;; playing at all". (defwindow now-playing :monitor 0 :geometry (geometry :x "24px" :y "24px" :anchor "bottom left" :width "420px" :height "96px") :stacking "overlay" :exclusive false :focusable false (np-root)) ;; --- Idle-gallery weather/clock overlay -------------------------------------- ;; ;; Time/date/weekday are computed locally (no Home Assistant round-trip needed for ;; those — the box already has a clock) via `date`(1); only temperature/condition/ ;; location come from weather-json's MQTT subscription, see that script's own ;; docstring for the smarthome/weather/current data contract. A missing/never- ;; published weather reading degrades to just hiding the weather line — the clock ;; and date keep showing regardless, same "never blank the whole thing over one ;; missing part" rule this repo applies everywhere (digest render.js, admin-canvas ;; render.js, idle-gallery.sh's own fallback chain). (defpoll clock_time :interval "20s" :initial "--:--" "date +'%H:%M'") (defpoll clock_weekday :interval "60s" :initial "" "date +'%A'") (defpoll clock_date :interval "60s" :initial "" "date +'%-d %B'") (deflisten weatherinfo :initial '{"available":false,"temperature":"","condition":"","location":""}' "/usr/local/bin/weather-json") (defwidget weather-overlay-root [] (box :class "wo-root" :orientation "vertical" :space-evenly false :halign "end" (label :class "wo-time" :text clock_time) (label :class "wo-date" :text "${clock_weekday}, ${clock_date}") (box :class "wo-weather" :orientation "horizontal" :space-evenly false :halign "end" :visible {weatherinfo.available} (label :class "wo-location" :text weatherinfo.location) (label :class "wo-sep" :text " · " :visible {weatherinfo.location != ""}) (label :class "wo-temp" :text weatherinfo.temperature) (label :class "wo-sep" :text " · ") (label :class "wo-condition" :text weatherinfo.condition)))) ;; Same :exclusive false/:stacking "overlay" shape as now-playing, and the same ;; ownership split: visibility is entirely owned by idle-gallery.sh, which is the ;; one thing that actually knows when its own slideshow is the thing on screen — ;; opened right before the slideshow's mpv is launched, closed in stop_slideshow(). ;; No :visible gate at the window level the way now-playing has one, because there ;; is no "open but should still hide" sub-case here: once idle-gallery owns the ;; screen, showing at least the clock is always correct. (defwindow weather-overlay :monitor 0 :geometry (geometry :x "32px" :y "32px" :anchor "top right" :width "440px" :height "140px") :stacking "overlay" :exclusive false :focusable false (weather-overlay-root))