The React UI is sluggish on the kiosk and the reasons are browser-shaped: a compositor deciding what gets its own layer, a requestAnimationFrame loop repainting the viewport, and a JPEG decode per album card per cold start. The last three commits chased that through the service worker and the cover pipeline. This is the other direction - the same backend, the same layout, the same German copy, without a browser. Browse and play only. The typing game, the room-lights page, parent mode and the IR-remote assignment stay web-only, and nothing here touches python-backend. Four things carry the performance claim, in rough order of how much they should matter: Covers are downscaled once, ever. The backend serves one size - 640px on the long edge, no ?size= - and a grid card is 180. src/covers.rs fetches each cover once on a worker thread, resizes it, and writes a JPEG thumbnail to ~/.cache/musicmouse-slint/covers/. Two tiers and only two, which is what makes the cache worth keeping: a per-widget pixel size would give each album a dozen near-identical files and a fresh decode for each. The cache is dropped when the websocket announces a rescan, because that is the one moment the backend rewrites the art behind an unchanged cover URL - the trap sw.js had to learn about the hard way. The grid is virtualized. Rust hands the UI the album list pre-chunked into rows and the view puts those in a ListView, which instantiates only what is on screen. A flat list of 660 cards gives it no rows to skip, hence the chunking. Same purpose as content-visibility: auto on .grid > .card. The progress bar animates between the 2 Hz pushes rather than running a clock, so interpolation costs a property evaluation per frame on the render side and there is no equivalent of usePlaybackClock. It is suppressed for the frame a track changes on, so a new track jumps instead of sliding across two unrelated positions. Nothing on screen animates by itself. The ambient canvas is not ported, for the reason lib/lowPower.ts already gives: a loop repainting the viewport is a floor you cannot get under while it runs at all. The device must not use FemtoVG. On Mesa V3D it draws every runtime-loaded Image as solid black (slint-ui/slint#11785, open), which here means every album cover; Skia and the software renderer are unaffected. So `kiosk` is linuxkms + Skia and FemtoVG stays the default only for desktop development. Both profiles are verified to build; the kiosk binary links libinput/libgbm/libdrm and no X11 or Wayland at all. lib/search.ts, lib/keyboard.ts and lib/format.ts were already pure functions with their own tests, so they port across as pure Rust with theirs: 43 tests, no window required. The .slint files are layout only - nothing in them formats a number or picks a word. Verified against the real 660-album library rather than a fixture. tools/headless-shots.sh renders the UI inside a nested headless compositor and grabs a frame per screen, which is how that was checked on a machine whose session was locked; a Slint window needs a real compositor, and under bare Xvfb nothing maps at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 lines
2.6 KiB
Bash
Executable File
57 lines
2.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Render the UI and screenshot it without touching your session.
|
|
#
|
|
# A Slint window needs a compositor: under bare Xvfb nothing maps, and on a locked or
|
|
# remote machine there is no session to draw into. So this starts a nested headless
|
|
# sway, runs the app inside it, drives it with synthetic keys and grabs a frame per
|
|
# screen. It is how the shots in README.md were made, and how to check a layout change
|
|
# on a machine with no display.
|
|
#
|
|
# nix-shell -E '(import ./shell.nix {}).overrideAttrs (o: {
|
|
# nativeBuildInputs = o.nativeBuildInputs ++ (with (import <nixpkgs> {}); [ sway grim wtype ]);
|
|
# })' --run 'tools/headless-shots.sh out/ http://127.0.0.1:8080'
|
|
#
|
|
set -euo pipefail
|
|
OUTDIR="${1:?usage: headless-shots.sh <outdir> [server-url]}"
|
|
SERVER="${2:-http://127.0.0.1:8080}"
|
|
BIN="${BIN:-./target/release/musicmouse-slint}"
|
|
mkdir -p "$OUTDIR"
|
|
|
|
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
|
unset WAYLAND_DISPLAY DISPLAY
|
|
export WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_RENDERER=pixman
|
|
trap 'kill %1 2>/dev/null || true; rm -rf "$XDG_RUNTIME_DIR"' EXIT
|
|
|
|
launcher="$XDG_RUNTIME_DIR/launch.sh"
|
|
printf '#!/usr/bin/env bash\nexec %q --server %q >%q/app.log 2>&1\n' \
|
|
"$(realpath "$BIN")" "$SERVER" "$XDG_RUNTIME_DIR" > "$launcher"
|
|
chmod +x "$launcher"
|
|
printf 'output HEADLESS-1 mode 1920x1080\ndefault_border none\nexec %s\n' "$launcher" \
|
|
> "$XDG_RUNTIME_DIR/sway.cfg"
|
|
|
|
sway -c "$XDG_RUNTIME_DIR/sway.cfg" >"$XDG_RUNTIME_DIR/sway.log" 2>&1 &
|
|
for _ in $(seq 30); do [ -S "$XDG_RUNTIME_DIR/wayland-1" ] && break; sleep 1; done
|
|
export WAYLAND_DISPLAY=wayland-1
|
|
sleep 16 # library fetch plus the first covers
|
|
|
|
# Each step is ONE wtype invocation, and starts with a throwaway keypress.
|
|
#
|
|
# wtype creates a virtual keyboard, sends, and exits. The client needs a moment to
|
|
# notice the seat gained a keyboard, so the first key or two of any invocation is
|
|
# dropped - which makes a per-key loop lose every key it sends. Batching a step into
|
|
# one invocation and opening it with "x" + BackSpace spends that warm-up on a keypress
|
|
# that cancels itself out.
|
|
WARM=(-s 260 "x" -k BackSpace)
|
|
shot() { sleep 2.5; grim "$OUTDIR/$1.png"; echo " $1"; }
|
|
|
|
shot 01-root
|
|
wtype "${WARM[@]}" -k Down -k Return; shot 02-categories
|
|
wtype "${WARM[@]}" -k Return; shot 03-grid
|
|
wtype "${WARM[@]}" -k Escape -k Escape "maja"; shot 04-search
|
|
wtype "${WARM[@]}" -k Escape -k question "brumm"; shot 05-tracksearch
|
|
wtype "${WARM[@]}" -k Return; sleep 4; shot 06-play
|
|
wtype "${WARM[@]}" -k Escape; shot 07-bar
|
|
|
|
echo "wrote 7 frames to $OUTDIR"
|