fix(docking): make workspace and lid transitions atomic
All checks were successful
Check / eval (push) Successful in 3m19s

Restore the old flake's external-only workspace handoff, protect closed-lid cable removal with a verified low-level logind inhibitor, and use monitor-added as the settled audio reprobe boundary.

Verified: V2 — nix flake check --no-build; docking-ux, option-docs, and template-sot checks; KVM tools/monitor-fallback.nix lifecycle test.

V3 pending: closed-lid BenQ round 4 in agent/HARDWARE-QUEUE.md.
This commit is contained in:
2026-07-13 13:56:54 +01:00
parent cffe432912
commit 2a34c7398b
12 changed files with 654 additions and 292 deletions

View File

@@ -15,14 +15,6 @@ let
px = n: mkLiteral "${toString n}px";
# Dock-class audio sinks (HDMI/DP/USB) — the same regex data the
# WirePlumber priority rules and the dock-audio watcher consume
# (modules/nixos/dock-audio-rules.nix is the single source).
dockSinkRe = lib.concatStringsSep "|"
(map (m: lib.removePrefix "~" m."node.name")
(lib.concatMap (r: r.matches)
(import ../nixos/dock-audio-rules.nix)."monitor.alsa.rules"));
# Per-theme override probe (same convention as waybar.css).
rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi";
hasRasiOverride = builtins.pathExists rasiOverride;
@@ -507,24 +499,16 @@ ${themeRows}
if [ ! -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ]; then
notify-send "Audio" "No PipeWire/PulseAudio socket (nomarchy.audio off?)."; exit 0
fi
# Docked quick row: the first dock-class sink (HDMI/DP/USB
# dockSinkRe, single source with the wireplumber rules) that is
# not already the default. One pick sends output there; the
# dock-audio watcher normally does this on plug, so this row is
# the manual override / catch-up. pactl by store path it is
# not on PATH (PipeWire stack, wpctl only).
sinks=$(${pkgs.pulseaudio}/bin/pactl --format=json list sinks 2>/dev/null || true)
# The helper owns dock-class detection + availability (regexes come
# from dock-audio-rules.nix once). This row is the manual catch-up.
candidate=
command -v nomarchy-dock-audio >/dev/null 2>&1 \
&& candidate=$(nomarchy-dock-audio candidates 2>/dev/null \
| ${pkgs.coreutils}/bin/head -n 1)
dockname=''${candidate%%"$TAB"*}
dockdesc=''${candidate#*"$TAB"}
cursink=$(${pkgs.pulseaudio}/bin/pactl get-default-sink 2>/dev/null || true)
dockname=$(printf '%s' "$sinks" | jq -r --arg cur "$cursink" --arg re '${dockSinkRe}' \
'[.[]
| select((.name | test("^(" + $re + ")$")) and .name != $cur)
| select(
((.ports // []) | length) == 0
or any(.ports[]?; (.availability // "unknown") != "not available")
)][0].name // empty')
dockdesc=
[ -n "$dockname" ] && dockdesc=$(printf '%s' "$sinks" \
| jq -r --arg n "$dockname" '[.[] | select(.name == $n)][0].description // empty')
[ "$dockname" = "$cursink" ] && dockname=
choice=$( {
[ -n "$dockname" ] && row "Send output ''${dockdesc:-$dockname}" video-display
row "Output device" audio-volume-high
@@ -534,8 +518,7 @@ ${themeRows}
case "$choice" in
"$BACK") exec "$0" system ;;
"Send output"*)
${pkgs.pulseaudio}/bin/pactl set-default-sink "$dockname" \
&& notify-send "Audio" "Output ''${dockdesc:-$dockname}" ;;
nomarchy-dock-audio select menu ;;
*Output*) exec rofi-pulse-select sink ;;
*Input*) exec rofi-pulse-select source ;;
esac ;;
@@ -599,12 +582,11 @@ ${themeRows}
if [ -n "$profs" ] || [ "$nActive" -gt 1 ] || [ -n "$offmon" ]; then
name=$( {
[ -n "$profs" ] && printf 'Profiles \n'
# Dock mode deliberately keeps the internal panel logically
# active as the unplug fallback. Soft-disabling it can leave
# Hyprland with zero outputs and kill the whole session before a
# user-space rescue runs. Move its workspaces and focus instead.
# Dock mode performs an atomic workspace handoff and only then
# disables the internal panel. The always-on hotplug watcher
# holds logind's lid-switch inhibitor across the reverse path.
[ "$nActive" -gt 1 ] && [ -n "$internal" ] && [ -n "$external" ] \
&& printf 'Dock mode · everything %s (safe unplug)\n' "$external"
&& printf 'Dock mode · external only %s\n' "$external"
[ -n "$offmon" ] && printf 'Screen on · %s\n' "$offmon"
[ "$nActive" -gt 1 ] && printf 'Move workspace · next monitor\n'
[ "$nActive" -eq 2 ] && printf 'Swap workspaces · %s %s\n' \
@@ -617,20 +599,17 @@ ${themeRows}
[ "$name" = "Profiles " ] && exec "$0" display-profile
case "$name" in
"Dock mode"*)
moved=0
while read -r ws; do
[ -n "$ws" ] || continue
hyprctl dispatch moveworkspacetomonitor "$ws" "$external" >/dev/null 2>&1 \
&& moved=$((moved+1))
done < <(hyprctl workspaces -j | jq -r --arg m "$internal" \
'.[] | select(.monitor == $m and .id > 0) | .id')
hyprctl dispatch focusmonitor "$external" >/dev/null 2>&1
notify-send "Display" "Dock mode $moved workspace(s) moved to $external; laptop panel kept as the unplug fallback."
if nomarchy-display-transition dock "$internal" "$external" \
"$external, preferred, auto, 1"; then
notify-send "Display" "Dock mode every laptop workspace moved to $external; laptop panel off."
else
notify-send "Display" "Dock transition failed; the previous layout was kept where possible."
fi
exit 0 ;;
"Screen on"*)
# preferred/auto/auto is good enough live; the declared
# preferred/auto/1 is the known-good live fallback; the declared
# scale/position come back at the next relogin/switch.
if hyprctl keyword monitor "$offmon,preferred,auto,auto" >/dev/null 2>&1; then
if nomarchy-display-transition enable "$offmon"; then
notify-send "Display" "$offmon is back on."
else
notify-send "Display" "Could not enable $offmon."