#!/usr/bin/env bash # Invoked by Sunshine as a stream-start hook (global_prep_cmd `do`). # Creates/resizes a Hyprland headless output to match the connecting # Moonlight client's resolution, and moves the active workspace onto it # so the user's existing windows are visible on the stream. # # Sunshine env vars set on connect: # SUNSHINE_CLIENT_WIDTH, SUNSHINE_CLIENT_HEIGHT, SUNSHINE_CLIENT_FPS set -euo pipefail log() { printf '[sunshine-do] %s\n' "$*" >&2; } WIDTH="${SUNSHINE_CLIENT_WIDTH:-1920}" HEIGHT="${SUNSHINE_CLIENT_HEIGHT:-1080}" FPS="${SUNSHINE_CLIENT_FPS:-60}" STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless" mkdir -p "$STATE_DIR" if ! command -v hyprctl >/dev/null 2>&1; then log "hyprctl not found; cannot configure headless. Stream will use whatever output Sunshine selects." exit 0 fi # Recover Hyprland signature if it wasn't inherited (defensive — UWSM exports it, # but a stray service environment could miss it). if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then for sig_dir in "${XDG_RUNTIME_DIR:-/tmp}"/hypr/*/; do [[ -d "$sig_dir" ]] || continue export HYPRLAND_INSTANCE_SIGNATURE="$(basename "$sig_dir")" log "Discovered HYPRLAND_INSTANCE_SIGNATURE=$HYPRLAND_INSTANCE_SIGNATURE" break done if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then log "Hyprland not running; nothing to configure." exit 0 fi fi # Snapshot prior state so undo can restore. hyprctl monitors -j > "$STATE_DIR/prev-monitors.json" 2>/dev/null || true PREV_WS="$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id // 1' || echo 1)" echo "$PREV_WS" > "$STATE_DIR/prev-workspace-id" # Discover whatever headless output already exists. sunshine-prestart.sh is # responsible for ensuring one exists and aligning sunshine.conf's output_name # to its actual name (Hyprland's HEADLESS-N counter drifts across restarts). MON="$(hyprctl monitors -j 2>/dev/null \ | jq -r '.[] | select(.name | startswith("HEADLESS")) | .name' | head -1)" if [[ -z "$MON" ]]; then log "No headless output found; creating one" hyprctl output create headless >/dev/null for _ in 1 2 3 4 5; do MON="$(hyprctl monitors -j 2>/dev/null \ | jq -r '.[] | select(.name | startswith("HEADLESS")) | .name' | head -1)" [[ -n "$MON" ]] && break sleep 0.1 done fi if [[ -z "$MON" ]]; then log "Failed to obtain a headless output; bailing." exit 0 fi echo "$MON" > "$STATE_DIR/headless-name" # Resize headless to the client's resolution / framerate. log "Sizing $MON → ${WIDTH}x${HEIGHT}@${FPS}" hyprctl keyword monitor "$MON,${WIDTH}x${HEIGHT}@${FPS},auto,1" >/dev/null # Move the active workspace onto the headless so existing windows appear in the stream. log "Moving workspace $PREV_WS → $MON, focusing it" hyprctl dispatch moveworkspacetomonitor "$PREV_WS $MON" >/dev/null || true hyprctl dispatch focusmonitor "$MON" >/dev/null || true log "Stream ready: ${WIDTH}x${HEIGHT}@${FPS} on $MON"