#!/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}" MON="HEADLESS-1" 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" # Ensure headless exists. if ! hyprctl monitors all -j 2>/dev/null | jq -e --arg m "$MON" '.[] | select(.name == $m)' >/dev/null; then log "Creating headless output $MON" hyprctl output create headless >/dev/null # Brief settle so Hyprland registers the new output before we configure it. for _ in 1 2 3 4 5; do hyprctl monitors all -j | jq -e --arg m "$MON" '.[] | select(.name == $m)' >/dev/null 2>&1 && break sleep 0.1 done fi # 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"