103 lines
5.3 KiB
Plaintext
103 lines
5.3 KiB
Plaintext
;; Two on-screen widgets for the thin client: now-playing and the idle-gallery
|
|
;; weather/clock overlay. Installed to
|
|
;; /home/<KIOSK_USERNAME>/.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))
|