The `route.set <app> <route>` overlay used to emit a single
`RouteRule` keyed on `process_binary`. The matcher ANDs across
non-empty fields, so a stream that didn't advertise
`application.process.binary` would miss the rule even though
its `application.name` was a perfect match. pw-cat is the
canonical hit — it sets `application.name = "pw-cat"` and
`node.name = "pw-cat"` but leaves `process.binary` unset
entirely. The same goes for several Electron and Flatpak
wrappers where the wrapping process eats the binary name.
`apply_route_overrides` now emits TWO rules per override, one
keyed on each identity field, with the same route. PipeWire
iterates rules in order and returns on first match, so the
effect is an OR across `process_binary` and `application_name`
for the single override — exactly the "match by whatever name
the stream advertises" intent of the CLI verb.
Why two rules and not "loosen the matcher to OR these two
fields": the matcher's AND-across-fields is load-bearing for
profile-author rules like `{process_binary: ["firefox"],
media_role: ["voice"]}` (match firefox-with-voice-role only).
Loosening the matcher would silently break those. Two
single-field rules with the same route preserve the original
semantics and add zero risk.
`is_single_app_rule_for_any` (the retain pre-pass that drops
old override rules before re-emitting) extends to recognise
the application_name-only variant too, so re-setting or
unsetting an override leaves no residual rules.
Tests
- `profile_store::tests::set_route_emits_both_process_binary_and_application_name_rules`
asserts both variants exist after `set_route`.
- `profile_store::tests::set_route_then_unset_leaves_no_residual_rules`
catches the matching retain-pre-pass regression that would
have leaked rules on unset.
- `routing::tests::application_name_only_rule_matches_stream_with_no_process_binary`
proves a stream with `application.name = "pw-cat"` and no
`process.binary` actually matches the application_name-keyed
rule path. 194 tests pass (was 191; +3 for the new
coverage); clippy clean.
Live verification
Daemon up, pw-cat → headroom-processed (default rule).
`headroom route set pw-cat bypass`: pw-cat's link snaps to
`Mbox:playback_FL` within one drain tick (~50 ms); status
reports `route: bypass`. Layer A tap survives the transition
intact. `headroom route unset pw-cat`: snaps back to
`headroom-processed:playback_FL`. Both transitions are
audibly clean against the F2 audio-gap mitigation from
`
|
||
|---|---|---|
| .. | ||
| headroom-cli | ||
| headroom-client | ||
| headroom-core | ||
| headroom-dsp | ||
| headroom-ipc | ||