From 19d6936aa67af5137118a5f91757cf4407e2e912 Mon Sep 17 00:00:00 2001 From: thomas Date: Sun, 11 Oct 2026 18:14:44 +0100 Subject: [PATCH] Feature: Enable hypridle without locking during Sunshine streams Sunshine's global_prep_cmd now runs hypr/scripts/SunshineIdle.sh, which holds a systemd idle inhibitor for the length of a stream (hypridle respects it) and unlocks hyprlock if the session was already locked. With that in place hypridle starts at login again. Sunshine's config (sunshine.conf, apps.json, prep scripts) moves into the dotfiles; credentials, logs and state stay local and are gitignored. Co-Authored-By: Claude Opus 5.5 --- bootstrap.sh | 2 + dotfiles/.config/hypr/core/startup.lua | 3 +- dotfiles/.config/hypr/scripts/SunshineIdle.sh | 29 ++++++++++++ dotfiles/.config/sunshine/.gitignore | 5 ++ dotfiles/.config/sunshine/apps.json | 47 +++++++++++++++++++ .../sunshine/scripts/steam-bigpicture-prep.sh | 17 +++++++ .../scripts/steam-bigpicture-tv-prep.sh | 18 +++++++ .../scripts/steam-bigpicture-tv-undo.sh | 6 +++ dotfiles/.config/sunshine/sunshine.conf | 5 ++ 9 files changed, 131 insertions(+), 1 deletion(-) create mode 100755 dotfiles/.config/hypr/scripts/SunshineIdle.sh create mode 100644 dotfiles/.config/sunshine/.gitignore create mode 100644 dotfiles/.config/sunshine/apps.json create mode 100755 dotfiles/.config/sunshine/scripts/steam-bigpicture-prep.sh create mode 100755 dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh create mode 100755 dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh create mode 100644 dotfiles/.config/sunshine/sunshine.conf diff --git a/bootstrap.sh b/bootstrap.sh index 516ed28..013fc0f 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -152,6 +152,8 @@ step "Linking dotfiles with stow" # Create real parent dirs first, otherwise stow symlinks the whole directory # (e.g. ~/.config) into the repo and every app's files end up in git mkdir -p "$HOME/.config" "$HOME/.local/bin" "$HOME/.local/share" "$HOME/Pictures" +# Sunshine keeps credentials and logs beside its config; those must stay out of git +mkdir -p "$HOME/.config/sunshine" mkdir -p -m 700 "$HOME/.ssh" if ! stow -d "$repo" -t "$HOME" --restow dotfiles; then echo "stow hit conflicts (listed above). Move those files aside, then run:" >&2 diff --git a/dotfiles/.config/hypr/core/startup.lua b/dotfiles/.config/hypr/core/startup.lua index de50aca..0d648c5 100644 --- a/dotfiles/.config/hypr/core/startup.lua +++ b/dotfiles/.config/hypr/core/startup.lua @@ -34,7 +34,8 @@ hl.on("hyprland.start", function() hl.exec_cmd("wl-paste --type image --watch cliphist store") -- Starting hypridle to start hyprlock - -- hl.exec_cmd("hypridle") + -- Sunshine streams hold an idle inhibitor (scripts/SunshineIdle.sh) so this never locks mid-stream + hl.exec_cmd("hypridle") -- Sunshine for streaming media hl.exec_cmd("sunshine") diff --git a/dotfiles/.config/hypr/scripts/SunshineIdle.sh b/dotfiles/.config/hypr/scripts/SunshineIdle.sh new file mode 100755 index 0000000..61f5489 --- /dev/null +++ b/dotfiles/.config/hypr/scripts/SunshineIdle.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Keeps hypridle from locking the screen while Sunshine is streaming. +# Hooked up as Sunshine's global_prep_cmd, so it runs for every app: +# start -> hold a systemd idle inhibitor (hypridle respects these) and +# unlock hyprlock if the session was already locked +# stop -> release the inhibitor so normal idle locking resumes + +pidfile="${XDG_RUNTIME_DIR:-/tmp}/sunshine-idle-inhibit.pid" + +case "$1" in + start) + if ! { [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; }; then + setsid systemd-inhibit --what=idle --who=Sunshine --why="Streaming" \ + sleep infinity >/dev/null 2>&1 & + echo $! > "$pidfile" + fi + # SIGUSR1 tells hyprlock to unlock + pkill -USR1 -x hyprlock + ;; + stop) + [ -f "$pidfile" ] && kill "$(cat "$pidfile")" 2>/dev/null + rm -f "$pidfile" + ;; + *) + echo "Usage: $0 start|stop" >&2 + exit 1 + ;; +esac +exit 0 diff --git a/dotfiles/.config/sunshine/.gitignore b/dotfiles/.config/sunshine/.gitignore new file mode 100644 index 0000000..ec3a6de --- /dev/null +++ b/dotfiles/.config/sunshine/.gitignore @@ -0,0 +1,5 @@ +# Machine-local Sunshine state lives next to the stowed config; never commit it +credentials/ +*.log +*.log.* +sunshine_state.json diff --git a/dotfiles/.config/sunshine/apps.json b/dotfiles/.config/sunshine/apps.json new file mode 100644 index 0000000..55544be --- /dev/null +++ b/dotfiles/.config/sunshine/apps.json @@ -0,0 +1,47 @@ +{ + "env": { + "PATH": "$(PATH):$(HOME)/.local/bin" + }, + "apps": [ + { + "name": "Desktop", + "image-path": "desktop.png" + }, + { + "name": "Low Res Desktop", + "image-path": "desktop.png", + "prep-cmd": [ + { + "do": "xrandr --output HDMI-1 --mode 1920x1080", + "undo": "xrandr --output HDMI-1 --mode 1920x1200" + } + ] + }, + { + "name": "Steam Big Picture", + "detached": [ + "setsid steam steam://open/bigpicture" + ], + "prep-cmd": [ + { + "do": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-prep.sh", + "undo": "setsid steam steam://close/bigpicture" + } + ], + "image-path": "steam.png" + }, + { + "name": "Steam Big Picture (TV)", + "detached": [ + "setsid steam steam://open/bigpicture" + ], + "prep-cmd": [ + { + "do": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh", + "undo": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh" + } + ], + "image-path": "steam.png" + } + ] +} diff --git a/dotfiles/.config/sunshine/scripts/steam-bigpicture-prep.sh b/dotfiles/.config/sunshine/scripts/steam-bigpicture-prep.sh new file mode 100755 index 0000000..312065a --- /dev/null +++ b/dotfiles/.config/sunshine/scripts/steam-bigpicture-prep.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Run before Sunshine launches Steam Big Picture: focus the headless virtual +# monitor and park the cursor on it, so Big Picture (which spawns on whatever +# monitor currently has focus/the mouse) opens there instead of wherever the +# mouse happened to be. +set -e +hyprctl dispatch 'hl.dsp.focus({monitor = "HEADLESS-1"})' +hyprctl dispatch 'hl.dsp.cursor.move({x = 5344, y = 540})' + +# Sunshine only flips the system default audio sink to sink-sunshine-stereo +# after the streaming session/client handshake completes (~500ms after this +# script runs and Steam is spawned) — see sunshine.log timestamps. If Steam's +# own audio (SDL) initializes in that window, it binds to whatever was +# default at the time (the real HDMI output) and never re-homes even after +# Sunshine's swap. Set it here first so it's already correct before Steam +# ever launches; Sunshine's own (redundant) swap right after is harmless. +pactl set-default-sink sink-sunshine-stereo || true diff --git a/dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh b/dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh new file mode 100755 index 0000000..a800fff --- /dev/null +++ b/dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Run before Sunshine launches Steam Big Picture for the 4K TV target. +# Switches the headless virtual monitor from its default 1728x1080@60 +# (used for the Steam Deck target) up to 3840x2160@60, then focuses it +# and parks the cursor there so Big Picture opens on it. Reverted by +# steam-bigpicture-tv-undo.sh once the stream ends. +set -e +# NOTE: this Hyprland build uses the Lua config layer, which rejects the +# usual `hyprctl keyword monitor ...` syntax ("keyword can't work with +# non-legacy parsers. Use eval.") — hl.monitor() via `hyprctl eval` is the +# runtime equivalent of the hl.monitor() call in layout/monitors.lua. +hyprctl eval 'hl.monitor({ output = "HEADLESS-1", mode = "3840x2160@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })' +hyprctl dispatch 'hl.dsp.focus({monitor = "HEADLESS-1"})' +hyprctl dispatch 'hl.dsp.cursor.move({x = 6400, y = 1080})' + +# See steam-bigpicture-prep.sh for why this runs here instead of relying on +# Sunshine's own post-handshake audio-sink swap. +pactl set-default-sink sink-sunshine-stereo || true diff --git a/dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh b/dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh new file mode 100755 index 0000000..1e85b58 --- /dev/null +++ b/dotfiles/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Undo for steam-bigpicture-tv-prep.sh: close Big Picture and restore +# HEADLESS-1 to its default mode (the one the Steam Deck target expects). +set -e +setsid steam steam://close/bigpicture +hyprctl eval 'hl.monitor({ output = "HEADLESS-1", mode = "1728x1080@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })' diff --git a/dotfiles/.config/sunshine/sunshine.conf b/dotfiles/.config/sunshine/sunshine.conf new file mode 100644 index 0000000..8305557 --- /dev/null +++ b/dotfiles/.config/sunshine/sunshine.conf @@ -0,0 +1,5 @@ +audio_sink = sink-sunshine-stereo +csrf_allowed_origins = https://xol.twhomelab.local:47990 +nvenc_preset = 4 +output_name = HEADLESS-1 +global_prep_cmd = [{"do":"/home/thomas/.config/hypr/scripts/SunshineIdle.sh start","undo":"/home/thomas/.config/hypr/scripts/SunshineIdle.sh stop"}]