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 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:14:44 +01:00
parent 4a57e9902f
commit 19d6936aa6
9 changed files with 131 additions and 1 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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

5
dotfiles/.config/sunshine/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Machine-local Sunshine state lives next to the stowed config; never commit it
credentials/
*.log
*.log.*
sunshine_state.json

View File

@@ -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"
}
]
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 })'

View File

@@ -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"}]