#!/bin/bash # Start a dedicated X server for the attached kiosk display. /usr/bin/Xorg :0 vt1 -nolisten tcp -keeptty & XPID=$! export DISPLAY=:0 export HOME=/home/archipelago X_READY=false for _ in $(seq 1 30); do if kill -0 "$XPID" 2>/dev/null && xrandr --query >/tmp/archipelago-kiosk-xrandr.txt 2>/dev/null; then X_READY=true break fi sleep 0.5 done if [ "$X_READY" != "true" ]; then echo 'ERROR: Xorg failed to become ready' kill "$XPID" 2>/dev/null || true exit 1 fi # Settings-managed display overrides (system.kiosk-display.set writes this # file, then restarts the kiosk): may set ARCHIPELAGO_KIOSK_SCALE, # ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH or ARCHIPELAGO_KIOSK_MAX_WIDTH. [ -f /etc/archipelago/kiosk-display.conf ] && . /etc/archipelago/kiosk-display.conf KIOSK_SAFE_AREA_X_PX=${ARCHIPELAGO_KIOSK_SAFE_AREA_X_PX:-} KIOSK_SAFE_AREA_Y_PX=${ARCHIPELAGO_KIOSK_SAFE_AREA_Y_PX:-} # Actual mode of the kiosk output — configure_display overwrites these so # Chromium's window matches the panel instead of assuming 1080p (a 1366x768 # laptop panel otherwise gets a clipped oversized window). KIOSK_MODE_W=1920 KIOSK_MODE_H=1080 configure_display() { command -v xrandr >/dev/null 2>&1 || return 0 local output mode internal width height output=$(awk '/ connected/ && $1 !~ /^eDP|^LVDS/{print $1; exit}' /tmp/archipelago-kiosk-xrandr.txt) [ -n "$output" ] || output=$(awk '/ connected/{print $1; exit}' /tmp/archipelago-kiosk-xrandr.txt) [ -n "$output" ] || return 0 # Pick the EDID-preferred ("+") mode, falling back to the first-listed # mode (EDID lists native first). Deliberately ignore "*" (currently # active) — trusting "active" lets a bad clone/mirror state from a # previous boot perpetuate itself forever instead of self-healing. mode=$(awk -v out="$output" ' $1 == out { active = 1; next } active && /^[[:space:]]+[0-9]+x[0-9]+/ { if ($0 ~ /\+/ && !preferred) { preferred = $1 } if (!first) first = $1 } active && /^[^[:space:]]/ { active = 0 } END { if (preferred) print preferred; else if (first) print first } ' /tmp/archipelago-kiosk-xrandr.txt) [ -n "$mode" ] || mode=1920x1080 # Optional resolution cap for weak hardware: ARCHIPELAGO_KIOSK_MAX_WIDTH=1920 # drops a 4K panel to its best <=1920-wide mode (the TV upscales) so the # software rasterizer paints 1/4 the pixels. Off by default — native mode # is sharp and fits the tile budget now that --window-size is in DIPs. local max_w=${ARCHIPELAGO_KIOSK_MAX_WIDTH:-0} if [ "$max_w" -gt 0 ] 2>/dev/null && [ "${mode%x*}" -gt "$max_w" ] 2>/dev/null; then local capped capped=$(awk -v out="$output" -v maxw="$max_w" ' $1 == out { active = 1; next } active && /^[[:space:]]+[0-9]+x[0-9]+/ { split($1, wh, "x") if (wh[1] + 0 <= maxw && wh[1] + 0 > best_w) { best_w = wh[1] + 0; best = $1 } } active && /^[^[:space:]]/ { active = 0 } END { if (best) print best } ' /tmp/archipelago-kiosk-xrandr.txt) [ -n "$capped" ] && mode=$capped fi # Kiosk should use one native output. A spanning desktop makes Chromium land # on the laptop panel or stretch across both outputs. for internal in $(awk '/ connected/ && $1 ~ /^eDP|^LVDS/{print $1}' /tmp/archipelago-kiosk-xrandr.txt); do [ "$internal" = "$output" ] || xrandr --output "$internal" --off 2>/dev/null || true done xrandr --output "$output" \ --primary \ --mode "$mode" \ --pos 0x0 \ --scale 1x1 \ --panning 0x0 \ --transform none 2>/dev/null || true width=${mode%x*} height=${mode#*x} case "$width:$height" in *[!0-9:]*|:*) width=1920; height=1080 ;; esac KIOSK_MODE_W=$width KIOSK_MODE_H=$height # Browser safe-area fallback for TVs that crop edges. Driver underscan is # preferable, but many Intel HDMI outputs do not expose that property. KIOSK_SAFE_AREA_X_PX=${KIOSK_SAFE_AREA_X_PX:-$((width * 3 / 100))} KIOSK_SAFE_AREA_Y_PX=${KIOSK_SAFE_AREA_Y_PX:-$((height * 3 / 100))} } configure_display # --- Kiosk UI scaling for large / high-res displays ----------------------- # REVERT: set env ARCHIPELAGO_KIOSK_SCALE=1 (per-node, no rebuild), or restore # the hardcoded --force-device-scale-factor=1 below to disable entirely. # # A big TV reports its full native resolution as the CSS viewport, so a 4K # panel becomes a 3840px-wide viewport and the UI renders tiny — and a # keyboard-less kiosk can't zoom. Derive Chromium's device-scale-factor from # the detected panel width so the *effective* CSS viewport lands near a # comfortable target. Panels >=2560 wide get a 1920-wide layout (4K -> scale # 2.0 — user-validated on a 72" TV: spacious desktop layout, 2x sharp); # smaller panels get a 1280-wide layout (1920 -> 1.50, laptops -> 1.0). if [ -z "${ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH:-}" ]; then if [ "$KIOSK_MODE_W" -ge 2560 ] 2>/dev/null; then ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1920 else ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH=1280 fi fi KIOSK_TARGET_CSS_WIDTH=$ARCHIPELAGO_KIOSK_TARGET_CSS_WIDTH if [ -n "${ARCHIPELAGO_KIOSK_SCALE:-}" ]; then KIOSK_SCALE=$ARCHIPELAGO_KIOSK_SCALE else KIOSK_SCALE=$(awk -v w="$KIOSK_MODE_W" -v t="$KIOSK_TARGET_CSS_WIDTH" \ 'BEGIN{if(t<=0)t=1280; s=w/t; s=int(s*4+0.5)/4; if(s<1)s=1; if(s>3)s=3; printf "%.2f", s}') fi # Chromium's --window-size is in DIPs (CSS px), not physical pixels: at scale S # the window paints S x larger. This X session has no window manager, so # --kiosk/--start-fullscreen cannot snap an oversized window back to the panel # — it just hangs off the right/bottom edges (content cropped, background art # offscreen). Size the window in DIPs so DIPs x scale = the panel exactly. KIOSK_WIN_W=$(awk -v w="$KIOSK_MODE_W" -v s="$KIOSK_SCALE" 'BEGIN{printf "%d", w/s}') KIOSK_WIN_H=$(awk -v h="$KIOSK_MODE_H" -v s="$KIOSK_SCALE" 'BEGIN{printf "%d", h/s}') xhost +SI:localuser:archipelago 2>/dev/null || true xsetroot -solid black 2>/dev/null || true xset s off 2>/dev/null || true xset -dpms 2>/dev/null || true xset s noblank 2>/dev/null || true pkill -u archipelago -f 'chromium.*localhost' 2>/dev/null || true sleep 1 # GPU vs headless (#36, choppy-audio incident 2026-06-28). --enable-gpu-rasterization # spins a dedicated GPU process at 55-92% CPU even on real GPU hardware (Intel HD 5500) # because under X11 it falls back to software compositing anyway — that CPU # starvation is what caused choppy HDMI audio. --in-process-gpu avoids the # separate process; GpuRasterization is also disabled via --disable-features below. # On a GPU-less / headless server (no /dev/dri), disable GPU entirely instead. if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then GPU_FLAGS="--in-process-gpu --num-raster-threads=1" else GPU_FLAGS="--disable-gpu --num-raster-threads=1" fi ARCHIPELAGO_UID=$(id -u archipelago) while true; do # A profile lock left by a previous boot encodes -; after a # hostname change (node rename) Chromium reads it as another computer # holding the profile and refuses to start — with --noerrdialogs that is an # invisible failure and the kiosk black-screens forever. Any Chromium that # owned the lock is dead by now (pkill above / previous loop iteration). rm -f /var/lib/archipelago/chromium-kiosk/Singleton{Lock,Cookie,Socket} # XDG_RUNTIME_DIR must be passed explicitly — without it Chromium's audio # backend can't find PipeWire-Pulse's socket at /run/user//pulse/native, # falls back to raw ALSA "default", fails to connect, and produces no audio # at all with no visible error (--noerrdialogs suppresses it). # OverlayScrollbar: thin auto-hiding scrollbars (the Chrome-on-a-remote- # device look) instead of classic X11 scrollbar chrome — a kiosk TV showed # a permanent fat scrollbar on scrollable views (Peers). # Force a DARK color-scheme preference. The main UI hardcodes its dark # theme, but the bundled AIUI app themes via `@media (prefers-color-scheme)` # and defaults to its LIGHT variant (white panels) when the browser reports # no preference — which a minimal kiosk X session does. Two independent dark # signals so this survives a Chromium enum change: GTK_THEME is version- # independent and can never force light (worst case: no effect), and the # blink-settings flag (0 = kDark in modern Chromium) reinforces it. Neither # can regress the always-dark main UI. sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago GTK_THEME=Adwaita:dark XDG_RUNTIME_DIR=/run/user/$ARCHIPELAGO_UID chromium --kiosk \ --app=http://localhost/kiosk?safe_area_x=${KIOSK_SAFE_AREA_X_PX:-0}\&safe_area_y=${KIOSK_SAFE_AREA_Y_PX:-0} \ --blink-settings=preferredColorScheme=0 \ --noerrdialogs \ --disable-infobars \ --disable-translate \ --no-first-run \ --check-for-update-interval=31536000 \ --disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled,GpuRasterization \ --enable-features=OverlayScrollbar \ --disable-session-crashed-bubble \ --disable-save-password-bubble \ --disable-suggestions-service \ --disable-component-update \ $GPU_FLAGS \ --renderer-process-limit=2 \ --window-size=${KIOSK_WIN_W},${KIOSK_WIN_H} \ --window-position=0,0 \ --start-fullscreen \ --force-device-scale-factor=${KIOSK_SCALE} \ --disable-background-networking \ --disable-background-timer-throttling \ --disable-backgrounding-occluded-windows \ --disable-breakpad \ --disable-metrics \ --disable-metrics-reporting \ --disable-domain-reliability \ --js-flags="--max-old-space-size=256" \ --user-data-dir=/var/lib/archipelago/chromium-kiosk sleep 3 done kill "$XPID" 2>/dev/null || true