fix(docking): don't lose the hotplug event; one prompt per keyboard; follow the jack
All checks were successful
Check / eval (push) Successful in 3m31s

Four failures from Bernardo's round-4 dock pass on the AMD dev box, plus a
headphone regression, tracked to three causes.

The display watcher ran socat with `-T 1`, which closes Hyprland's IPC
socket after one second of *inactivity*. A dock is normally plugged into an
idle desktop, so the socket was almost always mid-reconnect at that moment
and `monitoradded` was lost outright — and the wallpaper repaint, the
docking profile, and the dock-audio reprobe all hung off that single event.
The reconnect path recovered only the lid inhibitor, so the dock stayed
half-configured until something else poked the watcher.

Make the connection long-lived and take the periodic tick from a 1s `read`
timeout instead, then let the *output set* — not the event — decide: a new
`reconcile` diffs it and is called on both ticks and monitor events, so an
event now only makes the reaction instant rather than necessary. A newly
present external output is as unambiguous a physical plug as the event was,
so it may still override a manual sink choice. Wallpaper paints are retried
over the first few seconds because awww registers an output on its own
schedule, not the compositor's.

Hyprland calls every key-capable evdev node a "keyboard": the lid and power
buttons, a monitor's control channel, and each extra HID collection of a
real keyboard. One dock with one keyboard asked for a layout four times.
udev already draws the distinction properly, so take ID_INPUT_KEYBOARD from
it, map the names into Hyprland's spelling, and ask once per physical
keyboard — applying the answer to every node it brought with it.

Finally, a sink WirePlumber has been told to prefer is stored as the
configured default and outranks every priority rule. On UCM cards the
headphones are a sink of their own rather than a route, so once anything
pinned a sink (this feature on a dock plug, or the user in the Audio menu),
plugging headphones in could no longer take the audio back. The `watch`
loop already subscribed to exactly the right events with an empty handler;
finish it. Unplug needs no undo — the pin goes stale and WirePlumber falls
back by priority on its own, to the dock when docked and the speakers
otherwise.

Verified V2: checks.docking-ux and checks.dock-audio pass with new guards
against each regression; the keyboard filter and both sink selectors run
against the dev box's real devices and sink names. The hotplug paths need
hardware — queued as docking round 5.

Also queues #107, renaming theme.json now that it holds the system state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 10:24:48 +01:00
parent 9550d2be5e
commit e0da1ea40f
5 changed files with 263 additions and 87 deletions

View File

@@ -894,13 +894,15 @@
set -euo pipefail
menu=$(find ${gen}/home-path/bin -name nomarchy-menu | head -1)
keyboard=$(find ${gen}/home-path/bin -name nomarchy-keyboard-layout | head -1)
kbwatch=$(find ${gen}/home-path/bin -name nomarchy-keyboard-watch | head -1)
watcher=$(find ${gen}/home-path/bin -name nomarchy-display-profile-watch | head -1)
transition=$(find ${gen}/home-path/bin -name nomarchy-display-transition | head -1)
audio=$(find ${gen}/home-path/bin -name nomarchy-dock-audio | head -1)
test -n "$menu" -a -n "$keyboard" -a -n "$watcher" \
-a -n "$transition" -a -n "$audio"
-a -n "$transition" -a -n "$audio" -a -n "$kbwatch"
bash -n "$menu"
bash -n "$keyboard"
bash -n "$kbwatch"
bash -n "$watcher"
bash -n "$transition"
bash -n "$audio"
@@ -923,9 +925,22 @@
grep -q 'nomarchy-dock-lid-inhibitor' "$watcher"
! grep -q 'exec -a nomarchy-dock-lid-inhibitor' "$watcher"
grep -Fq 'kill -TERM -"$pid"' "$watcher"
grep -q 'done < <(' "$watcher"
grep -q 'exec 3< <(' "$watcher"
grep -q '"$TRANSITION" undock' "$watcher"
grep -q 'nomarchy-dock-audio reprobe monitoradded' "$watcher"
# A dock plugged while the desktop sits idle must still be
# seen: `socat -T` closed the IPC socket after a second of
# quiet and lost the event in the reconnect gap, so the
# output set not the event decides, on a read timeout.
! grep -q 'socat -T' "$watcher"
grep -Fq 'read -r -t 1 line <&3' "$watcher"
grep -q 'reconcile "$hint"' "$watcher"
grep -q 'outputs-changed added=' "$watcher"
# One physical keyboard exposes several key-capable evdev
# nodes; only udev's own classification may raise a prompt.
grep -q 'ID_INPUT_KEYBOARD=1' "$kbwatch"
grep -q 'typing_keyboards' "$kbwatch"
grep -q 'base_name' "$kbwatch"
grep -q 'hyprctl --batch' "$transition"
grep -q 'keyword monitor.*disable' "$transition"
grep -q 'result=no-lid-inhibitor' "$transition"
@@ -936,7 +951,12 @@
grep -q 'restart.*pipewire\|pipewire.service' "$audio"
grep -q 'set-default-sink' "$audio"
grep -q 'subscription-closed' "$audio"
grep -q 'result=no-available-dock-sink' "$audio"
grep -q 'result=no-available-sink' "$audio"
# A pinned default sink outranks every priority rule, so on
# UCM cards (headphones = their own sink) the jack has to be
# followed explicitly or plugging in stops switching output.
grep -q 'headphone_sinks' "$audio"
grep -Fq '"$0" headphones jack' "$audio"
! grep -q 'power_save' "$audio"
echo "docking-ux: atomic display, lid inhibitor, and monitor-triggered audio reprobe present"
touch $out