#!/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