fix(docking): dock automatically; repair cards parked on pro-audio
All checks were successful
Check / eval (push) Successful in 3m9s
All checks were successful
Check / eval (push) Successful in 3m9s
Bernardo's retest on the AMD dev box. Two of the four reports were never running the new code — both watchers are Hyprland `exec-once`, so a `nomarchy-home` switch leaves the old processes alive until relogin; only `nomarchy-dock-audio`, a user service, had picked the fix up. The journal showed the old watcher's `event=monitoradded` next to the new audio tool's `result=no-available-sink`, which is what gave it away. The other two were real. Automatic dock mode did not exist. The watcher auto-*undocked* when the last external left, but on plug it only ever matched a display profile — and with no profile saved and displayProfileAuto unset it did nothing at all, while `Dock mode` was reachable only from the menu. Half the pair being automatic is what made the dock feel broken: the session came home by itself but never left. Mirror it, letting a matched profile win as the more specific layout. The BenQ's audio was unreachable by any means because WirePlumber had the GPU's DisplayPort card pinned to `pro-audio`, whose raw `pro-output-N` sinks carry no ports, no routing and no volume — so no sink matched, nothing was selectable, and the profile is stored per card and so survived forever. The monitor's own USB card is input-only (every profile has sinks: 0), leaving DisplayPort as the only path to its speakers. Repair the card when a plug finds nothing routable, which is both the symptom and the only moment worth touching profiles: narrow to cards carrying an *available* HDMI/DP port — the monitor asking for audio over the cable — so the internal analog card is never touched and a considered pro-audio setup elsewhere survives. Verified V2: checks.docking-ux and checks.dock-audio pass with guards for both; the repair filter picks exactly the GPU card → HiFi against the dev box's live graph, leaving the BenQ USB, internal analog and Bluetooth cards alone. Hotplug itself still needs hardware — HARDWARE-QUEUE round 6, which now leads with the relogin requirement that invalidated round 5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -83,7 +83,54 @@ let
|
||||
log "trigger=$trigger result=set-default-failed candidate=$name"
|
||||
return 1
|
||||
}
|
||||
select_first_dock() { select_first dock_sinks "$1"; }
|
||||
# A card parked on `pro-audio` (or `off`) publishes no sink we can route:
|
||||
# pro-audio exposes raw `pro-output-N` nodes with no ports, no routing and
|
||||
# no volume, so a monitor's audio is unreachable however it is chosen —
|
||||
# and WirePlumber stores that profile per card, so it survives forever
|
||||
# once set. When a dock plug finds nothing to select, repair the cards
|
||||
# that carry an available HDMI/DisplayPort output by moving them to their
|
||||
# best real profile. Deliberately narrow: an internal analog card is
|
||||
# never touched, and a card already on a routable profile is left alone,
|
||||
# so a considered pro-audio setup on anything else survives.
|
||||
repair_dock_cards() { # $1 = observable trigger
|
||||
trigger="$1"
|
||||
cards=$("$PACTL" --format=json list cards 2>/dev/null \
|
||||
| "$JQ" -r '
|
||||
.[]
|
||||
| select(.active_profile == "pro-audio" or .active_profile == "off")
|
||||
# An available HDMI/DP port is the monitor itself asking for
|
||||
# audio over the cable — that is what makes this card a dock.
|
||||
| select([.ports[]? | select((.type // "") == "HDMI")
|
||||
| select((.availability // "unknown") == "available")]
|
||||
| length > 0)
|
||||
| . as $card
|
||||
| [ $card.profiles // {} | to_entries[]
|
||||
| select(.key != "pro-audio" and .key != "off")
|
||||
| select((.value.sinks // 0) > 0)
|
||||
| select((.value.available // true) != false) ]
|
||||
| sort_by(-(.value.priority // 0))
|
||||
| if length == 0 then empty else "\($card.name)\t\(.[0].key)" end') || cards=
|
||||
[ -n "$cards" ] || return 1
|
||||
printf '%s\n' "$cards" | while IFS= read -r row; do
|
||||
card=''${row%%"$TAB"*}
|
||||
prof=''${row#*"$TAB"}
|
||||
if "$PACTL" set-card-profile "$card" "$prof" 2>/dev/null; then
|
||||
log "trigger=$trigger repaired-card=$card profile=$prof"
|
||||
else
|
||||
log "trigger=$trigger repair-failed=$card profile=$prof"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
select_first_dock() { # $1 = observable trigger
|
||||
select_first dock_sinks "$1" && return 0
|
||||
# Nothing routable to select is the symptom a parked card produces, so
|
||||
# it is also the only moment worth touching profiles. Retry once the
|
||||
# repaired card has had time to publish its sinks.
|
||||
repair_dock_cards "$1" || return 1
|
||||
sleep 1
|
||||
select_first dock_sinks "$1"
|
||||
}
|
||||
|
||||
case "''${1:-watch}" in
|
||||
candidates)
|
||||
|
||||
@@ -454,6 +454,31 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
||||
fi
|
||||
}
|
||||
|
||||
# Entering dock mode is the mirror of the automatic undock below: the
|
||||
# session already comes back to the panel by itself when the last
|
||||
# external leaves, so an arriving external has to take it the other way
|
||||
# or the pair is only half-automatic and the menu has to finish the job.
|
||||
# A display profile is a deliberate, more specific layout, so it wins —
|
||||
# the caller only reaches here when none is in effect.
|
||||
auto_dock() {
|
||||
internal=$(internal_output)
|
||||
target=$(external_outputs | ${pkgs.coreutils}/bin/head -n 1)
|
||||
[ -n "$internal" ] && [ -n "$target" ] || return
|
||||
# `monitors` (unlike `monitors all`) lists only enabled outputs, so the
|
||||
# panel still being there is what "not docked yet" means. Re-docking
|
||||
# an already-docked session would move nothing and disable nothing.
|
||||
hyprctl monitors -j 2>/dev/null \
|
||||
| jq -e --arg m "$internal" 'any(.[]; .name == $m)' >/dev/null || return
|
||||
if "$TRANSITION" dock "$internal" "$target" "$target, preferred, auto, 1"; then
|
||||
log "auto-dock internal=$internal target=$target result=ok"
|
||||
${pkgs.libnotify}/bin/notify-send -a Nomarchy "Display" \
|
||||
"Docked — every laptop workspace moved to $target; laptop panel off." \
|
||||
2>/dev/null || true
|
||||
else
|
||||
log "auto-dock internal=$internal target=$target result=failed"
|
||||
fi
|
||||
}
|
||||
|
||||
# The set of connected outputs — not the IPC event stream — is the source
|
||||
# of truth. Hyprland's socket can miss or drop events (see the loop
|
||||
# below), and a lost monitoradded used to mean no docking profile, no
|
||||
@@ -506,6 +531,10 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
||||
log "outputs-changed removed=$gone transition=undock"
|
||||
fi
|
||||
check_monitors
|
||||
if [ -n "$added_ext" ] \
|
||||
&& [ "$(nomarchy-display-profile active 2>/dev/null || echo none)" = "none" ]; then
|
||||
auto_dock
|
||||
fi
|
||||
fi
|
||||
|
||||
# Safety invariants, re-checked regardless of whether the set moved.
|
||||
|
||||
Reference in New Issue
Block a user