From 0fe38e97974e10e0d58b6904c3eb7f77f3089d5c Mon Sep 17 00:00:00 2001 From: Levi Woodard Date: Thu, 30 Jul 2026 16:10:29 -0600 Subject: [PATCH] headless-primary: relocate workspaces stranded on a mirrored output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On swap-back Hyprland brings DP-1 up normal, binds a workspace to it, then apply mirrors DP-1 — trapping that workspace on a layout-excluded mirror (unreachable; symptom: 'can't make workspace 1 show'). apply now moves any workspace off a mirrored output onto the headless primary. Verified the heal operation (move-off-mirror) manually. Co-Authored-By: Claude Opus 4.8 (1M context) --- bin/sunshine-headless-primary.sh | 22 ++++++++++++++++++---- docs/HEADLESS-PRIMARY.md | 7 ++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/bin/sunshine-headless-primary.sh b/bin/sunshine-headless-primary.sh index 06efd7d..b7c06d7 100755 --- a/bin/sunshine-headless-primary.sh +++ b/bin/sunshine-headless-primary.sh @@ -113,18 +113,32 @@ apply() { log "DP-1 mirroring $head" fi - # 4. Rescue orphaned workspaces (monitorID == -1) onto the headless primary. + # 4. Relocate any workspace that lives on a MIRRORED output onto the headless + # primary. A mirror is excluded from the layout, so a workspace bound to it + # is trapped and unreachable — exactly what happens on swap-back when + # Hyprland brings DP-1 up normal, binds a workspace to it, then we mirror + # it. (tostring guards mirrorOf being null | "None" | a numeric id.) + local mon ws + for mon in $(hyprctl monitors all -j \ + | jq -r '.[] | select(((.mirrorOf // "None") | tostring | ascii_downcase) != "none") | .name'); do + [[ "$mon" == "$head" ]] && continue + for ws in $(hyprctl workspaces -j | jq -r --arg m "$mon" '.[] | select(.monitor==$m) | .id'); do + log "relocating workspace $ws off mirror $mon -> $head" + hyprctl dispatch moveworkspacetomonitor "$ws $head" >/dev/null 2>&1 || true + done + done + + # 5. 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. + # 6. 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. + # 7. 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 diff --git a/docs/HEADLESS-PRIMARY.md b/docs/HEADLESS-PRIMARY.md index 854be3d..815a41c 100644 --- a/docs/HEADLESS-PRIMARY.md +++ b/docs/HEADLESS-PRIMARY.md @@ -62,7 +62,12 @@ clients. Inherent to cloning an ultrawide. `hyprctl output remove` is unreliable for these, returns "output not found"). 2. Sizes it `5120x1440@60` at `0x0`. 3. If `DP-1` is present → `hyprctl keyword monitor "DP-1,…,mirror,HEADLESS-1"`. -4. Rescues any **orphaned** workspace (`monitorID == -1`) onto the headless +4. **Relocates any workspace stranded on a mirrored output** onto the primary. + On swap-back Hyprland brings `DP-1` up *normal*, binds a workspace to it, and + then we mirror it — leaving that workspace trapped on a layout-excluded + mirror (unreachable). This moves it off. (You can move a workspace *off* a + mirror but not *onto* one, which is why this only bites via hotplug.) +5. Rescues any **orphaned** workspace (`monitorID == -1`) onto the headless primary — deliberately narrow, so a healthy second monitor (DP-2) is left alone. 5. **Rescues off-screen floating windows** — recenters any floating, mapped