Files
Omarchy-Stream/bin/sunshine-headless-primary.sh
Levi Woodard 0d6da8c24a Add headless-primary streaming mode + off-screen window self-heal
Desktop lives on a persistent HEADLESS output that Sunshine captures; DP-1
mirrors it when present, so streaming works whether or not the physical monitor
is on. Fixes workspace/window orphaning when the monitor powers off (the old
'HEADLESS mirrors DP-1' design collapsed when DP-1 disappeared).

- bin/sunshine-headless-primary.sh: apply (idempotent topology + rescue),
  rescue (recenter off-screen floating windows), watch (self-heal on monitor
  add/remove and on windows that open off-screen).
- bin/sunshine-prestart.sh: delegates to the manager's apply.
- docs/HEADLESS-PRIMARY.md: full rationale + the ufw stale-subnet gotcha.

Not yet wired into install.sh; configured directly on JARVIS via monitors.conf
and autostart.conf (tracked in the dotfiles repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 16:47:22 -06:00

175 lines
6.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# omarchy-moonlight — HEADLESS-PRIMARY manager.
#
# Model: the desktop permanently lives on a virtual HEADLESS output that always
# exists, independent of the physical monitor. Sunshine captures it. The
# physical DP-1 MIRRORS the headless primary when it's present, so the at-desk
# view == the stream. When DP-1 is off (remote, monitor asleep/unplugged),
# nothing is orphaned — the headless output keeps the whole desktop and the
# stream keeps working.
#
# Why this replaces the old "HEADLESS mirrors DP-1" design: when the physical
# monitor fully powers off, DP-1 disappears from Hyprland, the mirror collapses,
# and every workspace bound to DP-1 is stranded off-screen. Inverting the mirror
# (headless is the source of truth) removes that failure mode entirely.
#
# SAFETY: DP-1 is a NORMAL monitor in monitors.conf. This script only ever ADDS
# a mirror on top; if it never runs, DP-1 still displays normally. The physical
# screen is never left blank by this machinery.
#
# Subcommands:
# apply (default) establish/repair state — idempotent (topology + rescues)
# rescue just pull off-screen floating windows back on-screen
# watch long-running: re-apply on monitor hotplug AND rescue windows that
# open off-screen (this is what makes stray windows self-heal)
set -uo pipefail
log() { printf '[headless-primary] %s\n' "$*" >&2; }
WIDTH=5120
HEIGHT=1440
RATE=60
POS="0x0"
CONF="$HOME/.config/sunshine/sunshine.conf"
ensure_hypr_sig() {
[[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]] && return 0
for sig in "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"/hypr/*/; do
[[ -d "$sig" ]] || continue
export HYPRLAND_INSTANCE_SIGNATURE="$(basename "$sig")"
return 0
done
return 1
}
have_tools() { command -v hyprctl >/dev/null && command -v jq >/dev/null; }
# NOTE: always `hyprctl monitors all` — a MIRRORED output is excluded from plain
# `hyprctl monitors`, so the plain form is blind to exactly the outputs we manage.
headless_names() {
hyprctl monitors all -j 2>/dev/null \
| jq -r '.[] | select(.name | startswith("HEADLESS")) | .name' | sort -V
}
# Recenter any FLOATING, mapped window whose rectangle does not intersect the
# monitor its workspace lives on (i.e. it's stranded off-screen after a monitor
# topology change, or it opened off-screen). Pass a single window address to
# check just that window (used on the openwindow event); no arg = sweep all.
rescue_offscreen_windows() {
local only="${1:-}" mons clients addrs a
have_tools || return 0
mons="$(hyprctl monitors all -j 2>/dev/null)" || return 0
clients="$(hyprctl clients -j 2>/dev/null)" || return 0
addrs="$(printf '%s' "$clients" | jq -r --argjson mons "$mons" --arg only "$only" '
($mons | map({key:(.id|tostring), value:{x:.x,y:.y,w:.width,h:.height}}) | from_entries) as $M
| .[]
| select(.mapped == true and .floating == true)
| select($only == "" or .address == $only)
| . as $c | ($M[$c.monitor|tostring]) as $m
| select($m != null)
| select( ($c.at[0]+$c.size[0]) <= $m.x
or $c.at[0] >= ($m.x+$m.w)
or ($c.at[1]+$c.size[1]) <= $m.y
or $c.at[1] >= ($m.y+$m.h) )
| .address' 2>/dev/null)"
for a in $addrs; do
hyprctl dispatch focuswindow "address:$a" >/dev/null 2>&1
hyprctl dispatch centerwindow >/dev/null 2>&1
log "rescued off-screen window $a"
done
}
apply() {
ensure_hypr_sig || { log "Hyprland not running; skip."; return 0; }
have_tools || { log "hyprctl/jq missing."; return 0; }
# 1. Exactly one headless output. Create if none; disable extras (remove is
# unreliable for mirrored/persistent headless — it returns "output not
# found" and exits 0). Keep the lowest-numbered.
mapfile -t hs < <(headless_names)
local head="${hs[0]:-}"
if [[ -z "$head" ]]; then
log "no headless output; creating one"
hyprctl output create headless >/dev/null
for _ in 1 2 3 4 5; do
head="$(headless_names | head -1)"
[[ -n "$head" ]] && break
sleep 0.2
done
else
for extra in "${hs[@]:1}"; do
hyprctl keyword monitor "$extra,disable" >/dev/null 2>&1 || true
done
fi
[[ -z "$head" ]] && { log "failed to obtain a headless output"; return 0; }
# 2. Size/position the headless primary (top-left origin, full res, scale 1).
hyprctl keyword monitor "$head,${WIDTH}x${HEIGHT}@${RATE},${POS},1" >/dev/null
# 3. If DP-1 is present, mirror the headless primary onto it (at-desk == stream).
if hyprctl monitors all -j | jq -e '.[] | select(.name=="DP-1")' >/dev/null 2>&1; then
hyprctl keyword monitor "DP-1,${WIDTH}x${HEIGHT}@${RATE},${POS},1,mirror,$head" >/dev/null
log "DP-1 mirroring $head"
fi
# 4. Rescue orphaned workspaces (monitorID == -1) onto the headless primary.
# Narrow by design — a healthy second monitor (DP-2) is left alone.
local ws
for ws in $(hyprctl workspaces -j | jq -r '.[] | select(.monitorID == -1) | .id'); do
log "rescuing orphaned workspace $ws -> $head"
hyprctl dispatch moveworkspacetomonitor "$ws $head" >/dev/null 2>&1 || true
done
# 5. Rescue any floating windows stranded off-screen by the topology change.
rescue_offscreen_windows
# 6. Keep Sunshine's output_name pointed at the live headless name.
if [[ -f "$CONF" ]] && grep -qF '# managed-by: omarchy-moonlight' "$CONF"; then
local cur; cur="$(awk '/^output_name = / {print $3; exit}' "$CONF" 2>/dev/null || true)"
if [[ "$cur" != "$head" ]]; then
log "sunshine.conf output_name: ${cur:-(unset)} -> $head"
sed -i "s|^output_name = .*|output_name = $head|" "$CONF" 2>/dev/null || true
fi
fi
log "headless-primary established on $head"
}
watch() {
ensure_hypr_sig || { log "Hyprland not running; watcher exiting."; return 0; }
have_tools || { log "hyprctl/jq missing; watcher exiting."; return 0; }
command -v socat >/dev/null || { log "socat missing; no watcher."; return 0; }
local sock="${XDG_RUNTIME_DIR}/hypr/${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock"
log "watching Hyprland events on $sock"
# monitoradded/removed -> full re-apply (re-impose mirror + rescue everything)
# openwindow -> targeted rescue if the NEW window opened off-screen
socat -U - "UNIX-CONNECT:$sock" 2>/dev/null | {
local last=0 now addr
while read -r event; do
case "$event" in
monitoradded*|monitorremoved*)
now=$(date +%s)
(( now - last < 2 )) && continue # debounce event bursts (v1+v2)
last=$now
log "monitor event (${event%%>*}) -> apply"
apply
;;
openwindow*)
addr="0x${event#openwindow>>}"; addr="${addr%%,*}"
sleep 0.3 # let the new window's geometry settle
rescue_offscreen_windows "$addr"
;;
esac
done
}
}
case "${1:-apply}" in
apply) apply ;;
rescue) rescue_offscreen_windows "${2:-}" ;;
watch) watch ;;
*) echo "Usage: $(basename "$0") {apply|rescue|watch}" >&2; exit 1 ;;
esac