Wildcard or list matching for source/sink #13

Merged
lucas merged 1 commit from agent/issue-3-node-pattern-matching into main 2026-11-12 22:41:46 +01:00
Collaborator

Implements the plan in #3.

Closes: #3

What changed

  • NodeMatcher (includes/NodeMatcher.h, src/NodeMatcher.cpp): compiles each configured entry as ^(?:entry)$ (ECMAScript, case-sensitive). Entries stay individually addressable (FirstMatchingEntry) because the representative rule ranks by entry. An invalid entry warns on stderr and is dropped; the rest of the list keeps working.
  • Config: every parameter is now std::vector<std::string>, so a scalar is a one-element list. Parsing moved from glaze reflection to a glz::generic DOM walk, since the accepted shape is polymorphic per key. Both actions and Actions are accepted. FriendlyNames values may be lists, and expansion splices them in place, single-level.
  • AudioService: listeners are subscriptions (matcher + pointer) instead of name-keyed maps; GetMatchingNodes defines the representative order (configured entry index, then ascending PipeWire bound id); the node cache only admits nodes whose media.class contains Audio.
  • Actions: writes fan out to every match, reads come from the representative, commanded state is sticky. connection manages the full cross product and no longer re-enforces default-state on link events, so manually created links survive.

Decisions worth flagging

  1. set-default was not in the plan (it shares IActionBase::m_Node, which became m_Target). There is exactly one default per media class, so the action cannot fan out: it acts on the representative node — the same node whose state the LED reads. Documented in the README.
  2. connection registers one combined node subscription instead of two. The duplicate-registration guard is keyed on the listener pointer, so registering m_Left and then m_Right would have silently dropped the right-hand subscription and starved that side of node events. The action therefore subscribes with a matcher built from the union of both sides' patterns, and keeps m_Left/m_Right separate for the ranked queries.
  3. glz::json_t is deprecated in glaze v9.1.0 (the version CI installs) in favour of glz::generic; the code uses glz::generic, with the accessors verified against that release.
  4. CI now also builds and runs the test target (-DNANOKONTROLLER2_BUILD_TESTS=ON plus a ctest step). The option still defaults to OFF, so the packaging and default build are unchanged.
  5. on_node_params_changed / on_node_ports_changed gained a null-node.name guard; the old code passed a possibly-null const char* into a map lookup.

Verification

  • nanoKontroller2_tests passes locally (built with gcc-14 against glaze v9.1.0 headers): anchoring, the firefox|chromium case that ^a|b$ would get wrong, entry ranking with overlapping patterns, dropped-invalid-entry reindexing, alias splicing order and single-level expansion, GetScalarParam.
  • The config parser was exercised against configs using lists, aliases-to-lists, the legacy Actions key, and malformed values (non-string list entries, non-string parameter, missing type); each warns and degrades as designed. config-example.json parses to the expected lists.
  • The WirePlumber-dependent half (AudioService, the actions) is compiled by CI only — no PipeWire/WirePlumber headers or session are available in this environment, so the live checks of the issue's Verification section (steps 3, 4 and 6: fan-out, representative promotion, sticky volume, cross-product links, video nodes untouched) have not been run and need a real session.
Implements the plan in #3. Closes: #3 ## What changed - **`NodeMatcher`** (`includes/NodeMatcher.h`, `src/NodeMatcher.cpp`): compiles each configured entry as `^(?:entry)$` (ECMAScript, case-sensitive). Entries stay individually addressable (`FirstMatchingEntry`) because the representative rule ranks by entry. An invalid entry warns on stderr and is dropped; the rest of the list keeps working. - **Config**: every parameter is now `std::vector<std::string>`, so a scalar is a one-element list. Parsing moved from glaze reflection to a `glz::generic` DOM walk, since the accepted shape is polymorphic per key. Both `actions` and `Actions` are accepted. `FriendlyNames` values may be lists, and expansion splices them in place, single-level. - **`AudioService`**: listeners are subscriptions (matcher + pointer) instead of name-keyed maps; `GetMatchingNodes` defines the representative order (configured entry index, then ascending PipeWire bound id); the node cache only admits nodes whose `media.class` contains `Audio`. - **Actions**: writes fan out to every match, reads come from the representative, commanded state is sticky. `connection` manages the full cross product and no longer re-enforces `default-state` on link events, so manually created links survive. ## Decisions worth flagging 1. **`set-default` was not in the plan** (it shares `IActionBase::m_Node`, which became `m_Target`). There is exactly one default per media class, so the action cannot fan out: it acts on the representative node — the same node whose state the LED reads. Documented in the README. 2. **`connection` registers one combined node subscription** instead of two. The duplicate-registration guard is keyed on the listener pointer, so registering `m_Left` and then `m_Right` would have silently dropped the right-hand subscription and starved that side of node events. The action therefore subscribes with a matcher built from the union of both sides' patterns, and keeps `m_Left`/`m_Right` separate for the ranked queries. 3. **`glz::json_t` is deprecated in glaze v9.1.0** (the version CI installs) in favour of `glz::generic`; the code uses `glz::generic`, with the accessors verified against that release. 4. **CI now also builds and runs the test target** (`-DNANOKONTROLLER2_BUILD_TESTS=ON` plus a `ctest` step). The option still defaults to `OFF`, so the packaging and default build are unchanged. 5. `on_node_params_changed` / `on_node_ports_changed` gained a null-`node.name` guard; the old code passed a possibly-null `const char*` into a map lookup. ## Verification - `nanoKontroller2_tests` passes locally (built with gcc-14 against glaze v9.1.0 headers): anchoring, the `firefox|chromium` case that `^a|b$` would get wrong, entry ranking with overlapping patterns, dropped-invalid-entry reindexing, alias splicing order and single-level expansion, `GetScalarParam`. - The config parser was exercised against configs using lists, aliases-to-lists, the legacy `Actions` key, and malformed values (non-string list entries, non-string parameter, missing `type`); each warns and degrades as designed. `config-example.json` parses to the expected lists. - The WirePlumber-dependent half (`AudioService`, the actions) is compiled by CI only — no PipeWire/WirePlumber headers or session are available in this environment, so the live checks of the issue's Verification section (steps 3, 4 and 6: fan-out, representative promotion, sticky volume, cross-product links, video nodes untouched) have **not** been run and need a real session.
feat: match nodes by pattern or list instead of one exact name
All checks were successful
Build / build (pull_request) Successful in 1m13s
541f49ea86
Every node reference (target, left, right, FriendlyNames values) now
accepts either a string or a list of strings, and each entry is an
anchored ECMAScript regex, a literal name or an alias. Aliases may
expand to a list, spliced in place, one level deep.

Writes fan out to every match; the state an action reads back, and
therefore its LED, comes from a representative node: the first
configured entry with a live match, oldest PipeWire id within it.
Commanded state is sticky, so a node that starts matching later is
brought to it as it appears. This replaces connection's re-enforcement
of default-state on every node event, which clobbered manual toggles;
link events no longer tear down links created by hand.

The node cache is restricted to nodes whose media.class contains Audio,
so a broad pattern cannot reach video or MIDI nodes. Listener
registration is keyed by subscription rather than by node name, which
also makes unregistering work: the previous loops compared rbegin()
with rbegin() and never ran.

Config parsing moves from glaze reflection to a DOM walk because the
accepted shape is now polymorphic per key; both "actions" and "Actions"
are accepted so neither documented nor previously parsed configs break.

Co-Authored-By: Claude Opus 5 via omp-deck
omp-deck changed title from WIP: Wildcard or list matching for source/sink to Wildcard or list matching for source/sink 2026-11-12 22:35:42 +01:00
lucas approved these changes 2026-11-12 22:41:42 +01:00
lucas merged commit 69ab2de81e into main 2026-11-12 22:41:46 +01:00
lucas deleted branch agent/issue-3-node-pattern-matching 2026-11-12 22:41:47 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/nanoKontroller2!13
No description provided.