#!/usr/bin/env bash # Sunshine global_prep_cmd `do` hook for the Sway-based headless capture path # (Debian/Ubuntu installs where Hyprland isn't available). # # On client connect: # - Ensures a HEADLESS-1 output exists on the running sway session # - Resizes it to the client's negotiated mode (WxH@FPS) # - Snapshots state for the undo hook # # Sunshine env vars set on connect: # SUNSHINE_CLIENT_WIDTH, SUNSHINE_CLIENT_HEIGHT, SUNSHINE_CLIENT_FPS # # This script intentionally mirrors the Hyprland do-hook's shape so debugging # transfers across the two paths. set -uo pipefail WIDTH="${SUNSHINE_CLIENT_WIDTH:-1920}" HEIGHT="${SUNSHINE_CLIENT_HEIGHT:-1080}" FPS="${SUNSHINE_CLIENT_FPS:-60}" HEADLESS_NAME="${OMARCHY_VIRTUAL_OUTPUT:-HEADLESS-1}" STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless" mkdir -p "$STATE_DIR" HOOK_LOG="$STATE_DIR/hook.log" : > "$HOOK_LOG" log() { local msg msg="$(date +%H:%M:%S.%3N) [sunshine-do-sway] $*" printf '%s\n' "$msg" >&2 printf '%s\n' "$msg" >> "$HOOK_LOG" } log "do-hook start: client=${WIDTH}x${HEIGHT}@${FPS} target=${HEADLESS_NAME}" if ! command -v swaymsg >/dev/null 2>&1; then log "swaymsg not found; nothing to configure." exit 0 fi # Recover SWAYSOCK if the unit env didn't propagate it. sway writes the socket # path into a predictable /run/user/$UID location, but the env var is the # clean handle. if [[ -z "${SWAYSOCK:-}" ]]; then for sock in "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"/sway-ipc.*.sock; do [[ -S "$sock" ]] || continue export SWAYSOCK="$sock" log "Discovered SWAYSOCK=$SWAYSOCK" break done if [[ -z "${SWAYSOCK:-}" ]]; then log "sway not running (no IPC socket found); nothing to configure." exit 0 fi fi # True if an output named $HEADLESS_NAME currently exists on the session. _headless_present() { swaymsg -t get_outputs -r 2>/dev/null \ | jq -e --arg n "$HEADLESS_NAME" '.[] | select(.name == $n)' >/dev/null } # Create the headless output if missing. Sway's `create_output` accepts an # optional name; without one it auto-assigns HEADLESS-N like Hyprland does. if ! _headless_present; then log "Creating headless output $HEADLESS_NAME" if ! swaymsg create_output "$HEADLESS_NAME" >/dev/null 2>&1; then # Older sway versions ignore the name argument; fall back and rename via # output detection after the fact. swaymsg create_output >/dev/null 2>&1 || true fi # Poll briefly for the output to appear. for _ in 1 2 3 4 5; do _headless_present && break sleep 0.1 done if ! _headless_present; then # Last-ditch: take the highest-numbered HEADLESS-* that exists and treat # it as ours. Update HEADLESS_NAME in-memory so the resize below targets it. found="$(swaymsg -t get_outputs -r 2>/dev/null \ | jq -r '[.[].name | select(startswith("HEADLESS-"))] | sort_by(.) | last // empty')" if [[ -n "$found" ]]; then HEADLESS_NAME="$found" log "Adopted existing headless output: $HEADLESS_NAME" else log "Failed to create a headless output; stream will rely on whatever Sunshine selects." exit 0 fi fi fi # Snapshot state so undo can put things back. We don't move workspaces around # on a headless-only box (there is no other monitor), but we still record what # was active in case the user runs sway with a real display attached. swaymsg -t get_outputs -r > "$STATE_DIR/prev-outputs.json" 2>/dev/null || true echo "$HEADLESS_NAME" > "$STATE_DIR/headless-name" # Resize the headless output. Sway accepts mode strings as "WIDTHxHEIGHT@FPSHz". log "Sizing $HEADLESS_NAME → ${WIDTH}x${HEIGHT}@${FPS}Hz" if ! swaymsg output "$HEADLESS_NAME" mode "${WIDTH}x${HEIGHT}@${FPS}Hz" >/dev/null 2>&1; then log "Mode set with refresh rate failed; retrying without refresh" swaymsg output "$HEADLESS_NAME" mode "${WIDTH}x${HEIGHT}" >/dev/null 2>&1 || \ log "Mode set failed; sway will keep the previous mode." fi # Focus the headless output so window placement lands there. swaymsg focus output "$HEADLESS_NAME" >/dev/null 2>&1 || true post="$(swaymsg -t get_outputs -r 2>/dev/null \ | jq -r '.[] | "\(.name) \(.current_mode.width)x\(.current_mode.height)@\(.current_mode.refresh) focused=\(.focused)"' \ | tr '\n' ';' || true)" log "post-state outputs: $post" log "Stream ready: ${WIDTH}x${HEIGHT}@${FPS} on $HEADLESS_NAME"