feat(audio,menu): dock audio auto-follow watcher + docked Display/Audio menu rows
Some checks failed
Check / eval (push) Failing after 1m41s
Some checks failed
Check / eval (push) Failing after 1m41s
Bernardo's dock test (the #87 V3): audio stayed on EasyEffects→speakers. Root cause, established live: WirePlumber's stored default (default-nodes state, written by any past explicit pick) outranks the priority.session rules, so #87 alone can never move the default on a real machine; EasyEffects 8 follows the default device (kcfg default true, verified by watching its output links). Fix: nomarchy-dock-audio (modules/home/dock-audio.nix, nomarchy.dockAudio.enable, default on) — a pactl-subscribe watcher that set-default-sinks new dock-class sinks (regex data imported from modules/nixos/dock-audio-rules.nix, single source) with a toast, plus a startup sweep for login-while-docked that never overrides a manual in-dock pick. Unplug falls back by priority. Menus: Display gains docked rows — "Laptop screen off · everything → <ext>" (live-only disable; a persisted one could black-screen an undocked boot), "Screen on · <name>", "Move workspace · → next monitor", "Swap workspaces" (exactly-2 gate); Audio gains a self-gated "Send output → <dock sink>" quick row. All self-gate on real outputs. Verified: V2-equivalent on live hardware (no dock available) — watcher followed a fake dock-named null sink and fell back on unload, control run without the watcher stayed pinned (proving the stored-default diagnosis), EE output links moved mid-playback; generated menu bash -n green, dock-row jq exercised on real pactl JSON, dispatchers probed on live Hyprland; flake check + option-docs + template-sot green. V3 pending: real-dock retest ×2 in HARDWARE-QUEUE (watcher keys on new-sink events; pre-existing nodes that only flip route availability would need the queued diagnosis data). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,14 @@ 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;
|
||||
@@ -498,13 +506,30 @@ ${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)
|
||||
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)][0].name // empty')
|
||||
dockdesc=
|
||||
[ -n "$dockname" ] && dockdesc=$(printf '%s' "$sinks" \
|
||||
| jq -r --arg n "$dockname" '[.[] | select(.name == $n)][0].description // empty')
|
||||
choice=$( {
|
||||
[ -n "$dockname" ] && row "Send output → ''${dockdesc:-$dockname}" video-display
|
||||
row "Output device" audio-volume-high
|
||||
row "Input device" audio-input-microphone
|
||||
back
|
||||
} | rofi_menu -show-icons -p Audio) || exit 0
|
||||
case "$choice" in
|
||||
"$BACK") exec "$0" system ;;
|
||||
"Send output"*)
|
||||
${pkgs.pulseaudio}/bin/pactl set-default-sink "$dockname" \
|
||||
&& notify-send "Audio" "Output → ''${dockdesc:-$dockname}" ;;
|
||||
*Output*) exec rofi-pulse-select sink ;;
|
||||
*Input*) exec rofi-pulse-select source ;;
|
||||
esac ;;
|
||||
@@ -522,6 +547,14 @@ ${themeRows}
|
||||
|| { notify-send "Display" "Hyprland is not running."; exit 0; }
|
||||
mons=$(hyprctl monitors -j)
|
||||
|
||||
# Docked context: which outputs are active, which is the built-in
|
||||
# panel, is anything currently soft-disabled (monitors all shows
|
||||
# disabled outputs; plain monitors does not).
|
||||
nActive=$(printf '%s' "$mons" | jq -r '.[].name' | grep -c .)
|
||||
internal=$(printf '%s' "$mons" | jq -r '[.[].name | select(test("^(eDP|LVDS|DSI)"))][0] // empty')
|
||||
external=$(printf '%s' "$mons" | jq -r '[.[].name | select(test("^(eDP|LVDS|DSI)") | not)][0] // empty')
|
||||
offmon=$(printf '%s' "$(hyprctl monitors all -j)" | jq -r '[.[] | select(.disabled)][0].name // empty')
|
||||
|
||||
# Choose the output (skip the chooser when only one is connected and
|
||||
# there are no profiles — the Profiles row needs the chooser to live in).
|
||||
# Where the mode picker's Back returns to: the output chooser when it
|
||||
@@ -530,14 +563,54 @@ ${themeRows}
|
||||
# `display` would just re-open this same mode picker (item 40).
|
||||
modeBack=system
|
||||
profs=; command -v nomarchy-display-profile >/dev/null 2>&1 && profs=1
|
||||
if [ -n "$profs" ] || [ "$(printf '%s' "$mons" | jq -r '.[].name' | grep -c .)" -gt 1 ]; then
|
||||
if [ -n "$profs" ] || [ "$nActive" -gt 1 ] || [ -n "$offmon" ]; then
|
||||
name=$( {
|
||||
[ -n "$profs" ] && printf 'Profiles ›\n'
|
||||
# Docked rows — self-gated: never offer to disable the only
|
||||
# active display; the re-enable row appears whenever an
|
||||
# output is soft-off (so an undocked laptop can recover too).
|
||||
[ "$nActive" -gt 1 ] && [ -n "$internal" ] && [ -n "$external" ] \
|
||||
&& printf 'Laptop screen off · everything → %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' \
|
||||
"$(printf '%s' "$mons" | jq -r '.[0].name')" \
|
||||
"$(printf '%s' "$mons" | jq -r '.[1].name')"
|
||||
printf '%s' "$mons" | jq -r '.[] | "\(.name) · \(.width)x\(.height)@\(.refreshRate|round)Hz"'
|
||||
printf '%s\n' "$BACK"
|
||||
} | rofi_menu -p Display ) || exit 0
|
||||
[ "$name" = "$BACK" ] && exec "$0" system
|
||||
[ "$name" = "Profiles ›" ] && exec "$0" display-profile
|
||||
case "$name" in
|
||||
"Laptop screen off"*)
|
||||
# Live-only on purpose: Hyprland moves the panel's
|
||||
# workspaces to the remaining output on disable; persisting
|
||||
# a disable could black-screen an undocked boot, so the
|
||||
# declared rule returns at the next relogin/switch.
|
||||
if hyprctl keyword monitor "$internal,disable" >/dev/null 2>&1; then
|
||||
notify-send "Display" "Laptop screen off — workspaces moved to $external (this session; turn it back on from this menu)."
|
||||
else
|
||||
notify-send "Display" "Could not disable $internal."
|
||||
fi
|
||||
exit 0 ;;
|
||||
"Screen on"*)
|
||||
# preferred/auto/auto is good enough live; the declared
|
||||
# scale/position come back at the next relogin/switch.
|
||||
if hyprctl keyword monitor "$offmon,preferred,auto,auto" >/dev/null 2>&1; then
|
||||
notify-send "Display" "$offmon is back on."
|
||||
else
|
||||
notify-send "Display" "Could not enable $offmon."
|
||||
fi
|
||||
exit 0 ;;
|
||||
"Move workspace"*)
|
||||
hyprctl dispatch movecurrentworkspacetomonitor +1 >/dev/null 2>&1
|
||||
exit 0 ;;
|
||||
"Swap workspaces"*)
|
||||
hyprctl dispatch swapactiveworkspaces \
|
||||
"$(printf '%s' "$mons" | jq -r '.[0].name')" \
|
||||
"$(printf '%s' "$mons" | jq -r '.[1].name')" >/dev/null 2>&1
|
||||
exit 0 ;;
|
||||
esac
|
||||
name=''${name%% ·*} # strip the " · WxH@R" hint
|
||||
modeBack=display
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user