Files
Omarchy-Stream/bin/sunshine-stream-do-sway.sh
Levi Woodard ee1379d5be Add Debian/Ubuntu support via a thin distro dispatch layer
Adds a parallel install path for Debian/Ubuntu hosts alongside the existing
Arch/Omarchy/Hyprland one. The Arch path is untouched at runtime; everything
new is gated on $DISTRO and (for headless) $COMPOSITOR.

Highlights:
- lib/distro.sh: detect_distro + pkg_install/pkg_remove/ca_anchor_path/
  ca_update_trust dispatch helpers
- lib/packages.sh: Ubuntu sunshine install pulls LizardByte's official .deb
  from GitHub releases (override via SUNSHINE_DEB_URL/SUNSHINE_DEB_VERSION);
  GPU encoder packages branch per $DISTRO:$GPU_VENDOR
- bin/sunshine-stream-{do,undo,prestart}-sway.sh + files/sway-headless.*:
  swaymsg-based headless capture path for hosts without Hyprland. sway runs
  under a systemd-user unit that sunshine.service depends on via drop-in.
- lib/preflight.sh: clearer NVIDIA driver guidance on Ubuntu (we don't install
  the driver - too many branch/kernel/Secure-Boot variants); sway-aware
  headless preflight
- lib/certs.sh + lib/verify.sh + uninstall.sh: distro-aware CA trust anchor
  (Arch: /etc/ca-certificates/trust-source/anchors + update-ca-trust;
   Debian: /usr/local/share/ca-certificates + update-ca-certificates)

Verified on Ubuntu 24.04: ./install.sh --doctor --headless loads cleanly,
distro/GPU/compositor detection report the right values, all pre-install
failures correspond to the actual missing pieces.
2026-05-23 01:17:42 +00:00

113 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Sunshine global_prep_cmd `do` hook for the Sway-based headless capture path
# (Debian/Ubuntu installs where Hyprland isn't available).
#
# On client connect:
# - Ensures a HEADLESS-1 output exists on the running sway session
# - Resizes it to the client's negotiated mode (WxH@FPS)
# - Snapshots state for the undo hook
#
# Sunshine env vars set on connect:
# SUNSHINE_CLIENT_WIDTH, SUNSHINE_CLIENT_HEIGHT, SUNSHINE_CLIENT_FPS
#
# This script intentionally mirrors the Hyprland do-hook's shape so debugging
# transfers across the two paths.
set -uo pipefail
WIDTH="${SUNSHINE_CLIENT_WIDTH:-1920}"
HEIGHT="${SUNSHINE_CLIENT_HEIGHT:-1080}"
FPS="${SUNSHINE_CLIENT_FPS:-60}"
HEADLESS_NAME="${OMARCHY_VIRTUAL_OUTPUT:-HEADLESS-1}"
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless"
mkdir -p "$STATE_DIR"
HOOK_LOG="$STATE_DIR/hook.log"
: > "$HOOK_LOG"
log() {
local msg
msg="$(date +%H:%M:%S.%3N) [sunshine-do-sway] $*"
printf '%s\n' "$msg" >&2
printf '%s\n' "$msg" >> "$HOOK_LOG"
}
log "do-hook start: client=${WIDTH}x${HEIGHT}@${FPS} target=${HEADLESS_NAME}"
if ! command -v swaymsg >/dev/null 2>&1; then
log "swaymsg not found; nothing to configure."
exit 0
fi
# Recover SWAYSOCK if the unit env didn't propagate it. sway writes the socket
# path into a predictable /run/user/$UID location, but the env var is the
# clean handle.
if [[ -z "${SWAYSOCK:-}" ]]; then
for sock in "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"/sway-ipc.*.sock; do
[[ -S "$sock" ]] || continue
export SWAYSOCK="$sock"
log "Discovered SWAYSOCK=$SWAYSOCK"
break
done
if [[ -z "${SWAYSOCK:-}" ]]; then
log "sway not running (no IPC socket found); nothing to configure."
exit 0
fi
fi
# True if an output named $HEADLESS_NAME currently exists on the session.
_headless_present() {
swaymsg -t get_outputs -r 2>/dev/null \
| jq -e --arg n "$HEADLESS_NAME" '.[] | select(.name == $n)' >/dev/null
}
# Create the headless output if missing. Sway's `create_output` accepts an
# optional name; without one it auto-assigns HEADLESS-N like Hyprland does.
if ! _headless_present; then
log "Creating headless output $HEADLESS_NAME"
if ! swaymsg create_output "$HEADLESS_NAME" >/dev/null 2>&1; then
# Older sway versions ignore the name argument; fall back and rename via
# output detection after the fact.
swaymsg create_output >/dev/null 2>&1 || true
fi
# Poll briefly for the output to appear.
for _ in 1 2 3 4 5; do
_headless_present && break
sleep 0.1
done
if ! _headless_present; then
# Last-ditch: take the highest-numbered HEADLESS-* that exists and treat
# it as ours. Update HEADLESS_NAME in-memory so the resize below targets it.
found="$(swaymsg -t get_outputs -r 2>/dev/null \
| jq -r '[.[].name | select(startswith("HEADLESS-"))] | sort_by(.) | last // empty')"
if [[ -n "$found" ]]; then
HEADLESS_NAME="$found"
log "Adopted existing headless output: $HEADLESS_NAME"
else
log "Failed to create a headless output; stream will rely on whatever Sunshine selects."
exit 0
fi
fi
fi
# Snapshot state so undo can put things back. We don't move workspaces around
# on a headless-only box (there is no other monitor), but we still record what
# was active in case the user runs sway with a real display attached.
swaymsg -t get_outputs -r > "$STATE_DIR/prev-outputs.json" 2>/dev/null || true
echo "$HEADLESS_NAME" > "$STATE_DIR/headless-name"
# Resize the headless output. Sway accepts mode strings as "WIDTHxHEIGHT@FPSHz".
log "Sizing $HEADLESS_NAME${WIDTH}x${HEIGHT}@${FPS}Hz"
if ! swaymsg output "$HEADLESS_NAME" mode "${WIDTH}x${HEIGHT}@${FPS}Hz" >/dev/null 2>&1; then
log "Mode set with refresh rate failed; retrying without refresh"
swaymsg output "$HEADLESS_NAME" mode "${WIDTH}x${HEIGHT}" >/dev/null 2>&1 || \
log "Mode set failed; sway will keep the previous mode."
fi
# Focus the headless output so window placement lands there.
swaymsg focus output "$HEADLESS_NAME" >/dev/null 2>&1 || true
post="$(swaymsg -t get_outputs -r 2>/dev/null \
| jq -r '.[] | "\(.name) \(.current_mode.width)x\(.current_mode.height)@\(.current_mode.refresh) focused=\(.focused)"' \
| tr '\n' ';' || true)"
log "post-state outputs: $post"
log "Stream ready: ${WIDTH}x${HEIGHT}@${FPS} on $HEADLESS_NAME"