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