From f47903a6a60007b7334df388ec9405fb4085ad40 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sat, 11 Jul 2026 10:36:07 +0100 Subject: [PATCH] feat(audio): prefer dock/HDMI/USB sinks in WirePlumber (#87) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raise priority.session for HDMI/DisplayPort (1100) and USB (1050) ALSA outputs so the default sink follows dock plug/unplug; built-in analog wins again when externals disappear. Rules live in modules/nixos/dock-audio-rules.nix; checks.dock-audio covers the contract. Verified: V2 — flake check --no-build; checks.dock-audio green. V3 pending: HARDWARE-QUEUE #87 real HDMI/USB-dock hotplug. --- README.md | 2 +- agent/BACKLOG.md | 8 ++--- agent/HARDWARE-QUEUE.md | 7 ++++ agent/JOURNAL.md | 11 ++++++ docs/ROADMAP.md | 6 ++++ docs/VISION.md | 2 +- flake.nix | 55 ++++++++++++++++++++++++++++++ modules/nixos/default.nix | 4 +++ modules/nixos/dock-audio-rules.nix | 38 +++++++++++++++++++++ 9 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 modules/nixos/dock-audio-rules.nix diff --git a/README.md b/README.md index c14c7f7..3507ee2 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ option, e.g. `services.easyeffects.enable = lib.mkForce false;` — see | `nomarchy.system.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) | | `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet | | `nomarchy.system.greeter.autoLogin` | `null` | Auto-login this user into Hyprland (installer sets it on LUKS machines) | -| `nomarchy.system.audio.enable` | `true` | Pipewire stack | +| `nomarchy.system.audio.enable` | `true` | Pipewire stack; WirePlumber prefers HDMI/USB (dock) sinks when present so default output follows dock/undock | | `nomarchy.system.bluetooth.enable` | `true` | Bluetooth + blueman | | `nomarchy.system.autoTimezone.enable` | `false` | Automatic timezone (geoclue + automatic-timezoned) — the clock follows your location; toggle from System › Auto timezone (a menu enable rebuilds: it has to unset the static `time.timeZone`) | | `nomarchy.system.snapper.enable` | `false` | Hourly/daily BTRFS timeline snapshots + `nixos-rebuild-snap` (installer enables it; no-op unless root is BTRFS) | diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 0bb3b22..32d2301 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -55,10 +55,6 @@ runtime-computed `resume_offset` into the boot cmdline, and the dev box runs exactly this layout (`/proc/cmdline` verified) but isn't Nomarchy. Steps in HARDWARE-QUEUE. Close #76 on PASS. -### 87. Dock audio sink follows plug/unplug (`VISION § C`) -Default audio sink: dock sink on plug, internal on unplug (WirePlumber -policy or udev hook). Complements #86; can ship independently. - ## LATER - **Wallpapers artifact split** (ROADMAP § Faster switches — decided, @@ -111,8 +107,8 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.* 2026-07-10). Keep #59 commented install guidance; no `nomarchy.hardware.nvidia.*` until a hybrid maintainer + queue. -_(#80–#83 + #85–#86 shipped 2026-07-11. Remaining dock slice: **#87** - dock audio. #86 V3 clamshell on real external display → HARDWARE-QUEUE.)_ +_(#80–#83 + #85–#87 shipped 2026-07-11. Dock V3 residuals: #86 + clamshell + #87 audio on real hardware → HARDWARE-QUEUE.)_ ### v1.0 pointer diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index 43eacd9..a1b3317 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -19,6 +19,13 @@ the **T14s** (webcam case). — should suspend as usual. Optional: with a docked display-profile (`eDP-1` disable), confirm clamshell layout still looks right after lid close. Pass = no surprise suspend when docked. +- [ ] **#87 dock audio sink follow** — V2 asserts WirePlumber + `90-nomarchy-dock-audio` rules (HDMI/USB priority). On a laptop + with HDMI or USB-dock audio: play sound on speakers, plug external + audio → default sink should move to HDMI/dock (`wpctl status` shows + `*` on the external sink); unplug → `*` returns to built-in. + Manual `wpctl set-default` still works; Bluetooth may outrank dock + (by design). Pass = auto switch without menu intervention. - [ ] **#55 fingerprint enroll on real reader** — with `nomarchy.hardware.fingerprint.enable` and a physical reader: System › Fingerprint › Enroll a finger; List shows it; Verify succeeds; optional diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 482c1a6..73d0b24 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -19,6 +19,17 @@ Template: --- +## 2026-07-11 — #87 dock audio sink priority +- **Task:** NEXT #87 — default audio follows dock plug/unplug. +- **Did:** WirePlumber `90-nomarchy-dock-audio` (HDMI/DP session prio + 1100, USB 1050) via `modules/nixos/dock-audio-rules.nix` + default.nix + pipewire. `checks.dock-audio` pure rule asserts + conf-file VM. V3 + hotplug steps in HARDWARE-QUEUE. Closed #87. +- **Verified:** **V2** — `nix flake check --no-build`; `checks.dock-audio` + green (pure rule asserts + conf installed under wireplumber.conf.d). +- **Pending:** V3 real HDMI/USB-dock hotplug (HARDWARE-QUEUE). +- **Next suggestion:** QA sweep or LATER wallpapers split if promoted. + ## 2026-07-11 — #86 clamshell HandleLidSwitchDocked - **Task:** NEXT #86 — lid closed + external monitor must not suspend. - **Did:** Explicit `HandleLidSwitchDocked=ignore` in diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e98ca99..233af29 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -394,6 +394,12 @@ Design/decision records and a running log of shipped work (items marked power management is on (systemd docked = external display present). Undocked lid suspend left to upstream. `checks.clamshell-logind` asserts logind.conf + active logind. Real lid+external V3 in HARDWARE-QUEUE. + ✓ **Dock audio sink priority (#87, `VISION § C`):** WirePlumber + `90-nomarchy-dock-audio` raises `priority.session` for HDMI/DP and USB + ALSA sinks so they become default when present; unplug falls back to + built-in. Rules in `modules/nixos/dock-audio-rules.nix`; + `checks.dock-audio` pure + conf-file VM. Real hotplug V3 in + HARDWARE-QUEUE. - ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS passphrase already gates the machine, but locking hyprlock before sleep meant a second password. Fixed by *not locking* before an encrypted diff --git a/docs/VISION.md b/docs/VISION.md index f82c23d..33f0a65 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -80,7 +80,7 @@ Reference: [HARDWARE.md](HARDWARE.md) §4–§5, §11 (doctor). | Idea | Intent | Notes | |------|--------|-------| -| **Dock life** | Lid closed + external only, wake, default audio sink on undock | ✓ #86 clamshell logind ignore when docked; #87 audio still open | +| **Dock life** ✓ | Lid closed + external only, wake, default audio sink on undock | ✓ #86 clamshell logind; ✓ #87 WirePlumber HDMI/USB priority (V3 hotplug QA) | | **Hibernate/sleep confidence** | Doctor: resume device, swap size, clean suspend journal | Trust > new power UI | | **Battery health readout** ✓ | Cycles + design capacity % in doctor where sysfs allows | ✓ shipped #80 (report-only; charge limit is a separate row) | | **Charge-limit instant apply** | Already PROPOSED `[blocked:hw]` | Keep privilege tradeoff explicit | diff --git a/flake.nix b/flake.nix index 940ecbe..1e59e6d 100644 --- a/flake.nix +++ b/flake.nix @@ -510,6 +510,61 @@ ''; }; + # Dock audio sink priority (#87): WirePlumber rules prefer + # HDMI/DP/USB sinks over built-in analog. Pure unit test of the + # rule set + a minimal VM that the conf file is installed where + # WirePlumber reads it (XDG_DATA_DIRS / share/wireplumber). + dock-audio = + let + inherit (nixpkgs) lib; + rules = import ./modules/nixos/dock-audio-rules.nix; + alsa = rules."monitor.alsa.rules" or [ ]; + expect = cond: msg: lib.assertMsg cond "dock-audio: ${msg}"; + json = builtins.toJSON alsa; + pureOk = + expect (builtins.isList alsa && builtins.length alsa >= 2) + "expected ≥2 alsa rule groups" + && expect (lib.hasInfix "hdmi" json) + "HDMI match missing from rules JSON" + && expect (lib.hasInfix "usb-" json || lib.hasInfix "usb" json) + "USB match missing from rules JSON" + && expect (lib.hasInfix "priority.session" json) + "priority.session missing" + && expect (lib.hasInfix "1100" json) + "HDMI session priority 1100 missing"; + in + assert pureOk; + pkgs.testers.runNixOSTest { + name = "nomarchy-dock-audio"; + nodes.machine = { ... }: { + services.pipewire = { + enable = true; + alsa.enable = true; + pulse.enable = true; + wireplumber.enable = true; + wireplumber.extraConfig."90-nomarchy-dock-audio" = + import ./modules/nixos/dock-audio-rules.nix; + }; + # WirePlumber is a user service; enable linger-less check via + # the system data dir the package puts conf into. + environment.systemPackages = [ pkgs.wireplumber ]; + }; + testScript = '' + machine.wait_for_unit("multi-user.target") + conf = machine.succeed( + "find /nix/store -path " + "'*wireplumber.conf.d/90-nomarchy-dock-audio.conf' " + "2>/dev/null | head -1" + ).strip() + assert conf, "90-nomarchy-dock-audio.conf not in the store" + body = machine.succeed(f"cat '{conf}'") + assert "monitor.alsa.rules" in body, body + assert "priority.session" in body, body + assert "hdmi" in body.lower() or "HDMI" in body, body + assert "usb" in body.lower(), body + ''; + }; + # Clamshell / dock lid policy (#86): logind must ignore lid close # when docked (external display present). Real multi-monitor lid # events stay V3; this asserts the baked logind.conf + that diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index f42a5a3..97eb267 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -125,6 +125,10 @@ in alsa.support32Bit = true; pulse.enable = true; wireplumber.enable = true; + # Prefer dock/HDMI/USB sinks when present; fall back to built-in when + # they disappear (BACKLOG #87). Rules: ./dock-audio-rules.nix. + wireplumber.extraConfig."90-nomarchy-dock-audio" = + import ./dock-audio-rules.nix; }; # ── Desktop services ───────────────────────────────────────────── diff --git a/modules/nixos/dock-audio-rules.nix b/modules/nixos/dock-audio-rules.nix new file mode 100644 index 0000000..27e6087 --- /dev/null +++ b/modules/nixos/dock-audio-rules.nix @@ -0,0 +1,38 @@ +# WirePlumber 0.5 ALSA rules: prefer dock / external sinks over built-in +# analog when they appear (BACKLOG #87). Higher priority.session wins as +# the default node; when HDMI/USB goes away, the next-highest (usually +# the laptop speakers/headphones) becomes default again. +# +# Pure attrset so checks.dock-audio can unit-test the contract without a +# full PipeWire stack. Consumed by modules/nixos/default.nix. +# +# Priorities stay ≤1500 (WirePlumber docs: sinks default ~600–1000; going +# much higher can outrank streams / BT in surprising ways). +{ + "monitor.alsa.rules" = [ + { + # HDMI / DisplayPort monitor audio (names vary: .hdmi-stereo, + # .hdmi-surround, HiFi__HDMI1__sink, …). + matches = [ + { "node.name" = "~alsa_output\\..*\\.hdmi-.*"; } + { "node.name" = "~alsa_output\\..*HDMI.*"; } + { "node.name" = "~alsa_output\\..*\\.DisplayPort.*"; } + { "node.name" = "~alsa_output\\..*\\.dp-.*"; } + ]; + actions."update-props" = { + "priority.driver" = 1100; + "priority.session" = 1100; + }; + } + { + # USB dock / dongle audio (Thunderbolt dock, USB-C audio, …). + matches = [ + { "node.name" = "~alsa_output\\.usb-.*"; } + ]; + actions."update-props" = { + "priority.driver" = 1050; + "priority.session" = 1050; + }; + } + ]; +}