Retarget overlay: temporarily narrow a strip's target to the active node #16

Open
opened 2026-11-12 23:17:45 +01:00 by omp-deck · 2 comments
Collaborator

Why

A strip's target is a NodeMatcher pattern list, so one strip can cover a whole class of nodes — "all music players", say — and act on every match at once. That is the right default: the fader moves everything in the class.

But sometimes you want the same strip to act on just the one that currently matters, without editing the config and without permanently giving up the broad target. "All music" should be able to become "only the music that is actually playing right now" for as long as you hold or latch a button, then go back.

This is also the cheapest answer to having more interesting nodes than physical strips: a strip can be pointed at something on demand instead of being statically assigned.

Scope

A button-driven overlay that temporarily replaces the set of nodes a strip acts on. While the overlay is active, the strip's other controls (fader, mute, …) operate on the overlaid set; when it is cleared, they return to the configured matcher.

Two modes of operation:

  1. Retarget to the currently active node — ignores the strip's configured matcher and binds to whatever is active system-wide.
  2. Narrow to the active member of the strip's own list — keeps the configured matcher as the candidate set and picks the active one out of it. This is the "all music → only the playing one" case.

The second mode is the more clearly useful of the two and is strictly safer, since it can never bind the strip to something unrelated.

Surface area

  • NodeMatcher and AudioService::GetMatchingNodes already produce the ranked match list; the overlay is a filter or replacement applied on top of that result (includes/NodeMatcher.h, includes/AudioService.h).
  • IActionBase::MatchedNodes() / RepresentativeNode() are the single choke point every action reads its targets through (src/Actions/IAction.cpp), which makes them the natural place for an overlay to take effect.
  • The main obstacle, same as in #14: actions are independent instances keyed by one control each. An overlay button must change what other actions on the same strip operate on, and there is no mechanism for that today. This is the second independent motivation for a strip/group concept in the config, and the two issues should probably be designed together.

Open questions

  • What does "active" mean? Candidates: the node is in a running state rather than idle; it has live links carrying signal; it belongs to the focused window. The first two are answerable from PipeWire, the third needs a window-manager query and would leave the PipeWire-only domain. The modes above may well want different definitions.
  • How is active state observed? Whether node state transitions are available through the existing node listener path, or whether AudioService needs to expose and watch something new, is unverified.
  • Ties. If several nodes qualify as active, does the overlay take all of them, or the representative only?
  • Momentary or latched? Hold-to-narrow auto-restores and cannot strand state; a latched toggle is more comfortable but needs a defined answer for "the overlaid node disappeared while the overlay was active".
  • Nothing active. Does the strip fall back to the full set, or act on nothing? Acting on nothing is safer but looks broken.
  • What does the LED show? "Overlay engaged" is the obvious choice, but it does not tell you what the strip is now pointed at, and the button has no other way to say so.
  • Scope of the overlay: the whole strip, or only actions that opt in?

Acceptance

  • A button activates an overlay that narrows or replaces the strip's target set, and clearing it restores the configured behaviour exactly.
  • Both modes are selectable from the config.
  • The definition of "active" is documented and observable.
  • Node appearance/disappearance while the overlay is active has defined behaviour.
  • README documents the action, its modes, and its interaction with the strip's other controls.

Out of scope

  • Permanently rebinding a strip or persisting the overlay across restarts.
  • Selecting a target by scrolling through candidates; this is about automatic selection of the active one.
## Why A strip's target is a `NodeMatcher` pattern list, so one strip can cover a whole class of nodes — "all music players", say — and act on every match at once. That is the right default: the fader moves everything in the class. But sometimes you want the same strip to act on just the one that currently matters, without editing the config and without permanently giving up the broad target. "All music" should be able to become "only the music that is actually playing right now" for as long as you hold or latch a button, then go back. This is also the cheapest answer to having more interesting nodes than physical strips: a strip can be pointed at something on demand instead of being statically assigned. ## Scope A button-driven **overlay** that temporarily replaces the set of nodes a strip acts on. While the overlay is active, the strip's other controls (fader, mute, …) operate on the overlaid set; when it is cleared, they return to the configured matcher. Two modes of operation: 1. **Retarget to the currently active node** — ignores the strip's configured matcher and binds to whatever is active system-wide. 2. **Narrow to the active member of the strip's own list** — keeps the configured matcher as the candidate set and picks the active one out of it. This is the "all music → only the playing one" case. The second mode is the more clearly useful of the two and is strictly safer, since it can never bind the strip to something unrelated. ## Surface area - `NodeMatcher` and `AudioService::GetMatchingNodes` already produce the ranked match list; the overlay is a filter or replacement applied on top of that result (`includes/NodeMatcher.h`, `includes/AudioService.h`). - `IActionBase::MatchedNodes()` / `RepresentativeNode()` are the single choke point every action reads its targets through (`src/Actions/IAction.cpp`), which makes them the natural place for an overlay to take effect. - **The main obstacle**, same as in #14: actions are independent instances keyed by one control each. An overlay button must change what *other* actions on the same strip operate on, and there is no mechanism for that today. This is the second independent motivation for a strip/group concept in the config, and the two issues should probably be designed together. ## Open questions - **What does "active" mean?** Candidates: the node is in a running state rather than idle; it has live links carrying signal; it belongs to the focused window. The first two are answerable from PipeWire, the third needs a window-manager query and would leave the PipeWire-only domain. The modes above may well want different definitions. - **How is active state observed?** Whether node state transitions are available through the existing node listener path, or whether `AudioService` needs to expose and watch something new, is unverified. - **Ties.** If several nodes qualify as active, does the overlay take all of them, or the representative only? - **Momentary or latched?** Hold-to-narrow auto-restores and cannot strand state; a latched toggle is more comfortable but needs a defined answer for "the overlaid node disappeared while the overlay was active". - **Nothing active.** Does the strip fall back to the full set, or act on nothing? Acting on nothing is safer but looks broken. - **What does the LED show?** "Overlay engaged" is the obvious choice, but it does not tell you *what* the strip is now pointed at, and the button has no other way to say so. - **Scope of the overlay**: the whole strip, or only actions that opt in? ## Acceptance - A button activates an overlay that narrows or replaces the strip's target set, and clearing it restores the configured behaviour exactly. - Both modes are selectable from the config. - The definition of "active" is documented and observable. - Node appearance/disappearance while the overlay is active has defined behaviour. - README documents the action, its modes, and its interaction with the strip's other controls. ## Out of scope - Permanently rebinding a strip or persisting the overlay across restarts. - Selecting a target by scrolling through candidates; this is about automatic selection of the active one.
Owner

I think this might need thinking about a broader architectural scope, because the "it" in "retarget it" can mean different things in different use cases.

Currently, the entire config is effectively independent per control. E.g. "Mute1" is completely independent of "Fader1" and could target completely different applications. So in order for "retarget this strip" to work, we are lacking the ability to assign a single target to an entire strip.

Then another use case is "target the active one out of this group", where "this group" would likely be an already existing matcher. And it could be meant either to be scoped to a single control, e.g. "make Fader2 only target the active entity in its set", but could also mean "every control that uses the 'all music' set, target only the active one in that set".

But I think both can be accomplished reasonably well if the "FriendlyNames" feature was reworked conceptually. Rather than just making it aliases that are replaced in the config before evaluation, make them node groups as a living entity.

Then, if "retarget" was pointed at a node group, it would alter the group's state, and every control that uses the group would behave accordingly. E.g. retarget the 'all music' group to only the active one, then any control using that group would automatically also only target the active one.

I think this might need thinking about a broader architectural scope, because the "it" in "retarget it" can mean different things in different use cases. Currently, the entire config is effectively independent per control. E.g. "Mute1" is completely independent of "Fader1" and could target completely different applications. So in order for "retarget this strip" to work, we are lacking the ability to assign a single target to an entire strip. Then another use case is "target the active one out of this group", where "this group" would likely be an already existing matcher. And it could be meant either to be scoped to a single control, e.g. "make Fader2 only target the active entity in its set", but could also mean "every control that uses the 'all music' set, target only the active one in that set". But I think both can be accomplished reasonably well if the "FriendlyNames" feature was reworked conceptually. Rather than just making it aliases that are replaced in the config before evaluation, make them node groups as a living entity. Then, if "retarget" was pointed at a node group, it would alter the group's state, and every control that uses the group would behave accordingly. E.g. retarget the 'all music' group to only the active one, then any control using that group would automatically also only target the active one.
Author
Collaborator

Agreed, and I think the group reframe is stronger than the "strip" direction I wrote into the issue — it solves the coupling problem without inventing a strip concept at all.

The part that convinces me: one mechanism covers both scopes you describe. If every node-valued parameter resolves to a group, and an inline pattern list is just an anonymous group private to that action, then the scope of a retarget is decided purely by which group the button names:

  • retarget pointed at "all music" → every control referencing that group narrows together;
  • retarget pointed at Fader2's own (named, but used once) group → only Fader2 narrows.

No scope: control | group parameter, no strip block. "Which controls move together" becomes a config-authoring decision — share a group name or don't — rather than a feature with two modes. That also makes the two modes in the issue collapse into one: "retarget to the system-wide active node" is just the same filter applied to a group whose candidate set is .*.

What it means in the code

Today the alias is gone by the time anything runs: ExpandFriendlyNames splices alias entries into each action's parameter lists in-place, and IActionBase::Init then builds a NodeMatcher copy from them (src/AppSettings.cpp, src/Actions/IAction.cpp:14-16). Making groups live means roughly:

  1. A runtime registry of groups, each holding its candidate NodeMatcher plus mutable state (All / ActiveOnly / pinned ids). It has to outlive the actions, so it sits next to AudioService, not in the parsed AppSettings; actions hold a non-owning handle resolved at Init.
  2. MatchedNodes() becomes "ask AudioService::GetMatchingNodes for the candidate set, then apply the group's current filter". Ranking and the representative stay where they are — the filter is a post-filter over the ranked id list, so front() keeps its meaning.
  3. Listener registration can stay as it is: RegisterNodeListener subscribes against the candidate matcher, which a state change does not alter. So no re-subscription churn — but a group state change does need a new notification edge (group → its subscriber actions) so every affected control re-reads and refreshes its LED, parallel to the existing on_node_available / on_node_removed path.
  4. It has to apply to all node-valued parameters, not just target: Action_Connection takes left/right, and ExpandFriendlyNames already expands every parameter indiscriminately.

Mixed lists like "target": ["all music", "firefox.*"] work today by splicing. Under groups the natural reading is "union of the resolved groups and the inline entries, each group contributing its filtered members, with expansion keeping its position so configured order still ranks" — worth confirming that's what you want, because it means one half of a target can be narrowed while the other isn't.

This decouples #16 from #14

The issue text claims #16 and #14 should be designed together because both are blocked on actions being independent. With groups, that's only half true: they share a symptom, not a mechanism. #16's shared state is a target set, which groups own cleanly. #14's shared state is a continuous value (knob trim feeding the fader's mapping) — a group has nothing to say about it. So they want different solutions and can proceed independently.

What the reframe does not answer

Everything under "Open questions" that's about semantics rather than plumbing survives unchanged: what "active" means, whether node state transitions are observable through the current listener path, ties, momentary vs latched, and the nothing-active fallback.

It also adds some:

  • Group state is global. Two buttons pointing at the same group with different filters — last writer wins, or a stack of overlays that unwinds in order? Momentary buttons make the stack question concrete.
  • LED feedback matters more, since a latched group overlay can now visibly change controls on strips the user isn't looking at.
  • Nesting. Aliases are deliberately single-level today, which is why they can't cycle. Groups referencing groups is tempting; keeping the restriction is probably right.

Proposal

Split it: the FriendlyNames → live node groups rework is a prerequisite with its own value (it's a config-semantics change with its own data model and compatibility story), and #16 becomes the retarget action on top of it. Want me to open that as a separate issue with the data model sketched out, and reduce #16's scope to the action plus the "active" semantics?

Agreed, and I think the group reframe is stronger than the "strip" direction I wrote into the issue — it solves the coupling problem without inventing a strip concept at all. The part that convinces me: **one mechanism covers both scopes you describe.** If every node-valued parameter resolves to a group, and an inline pattern list is just an *anonymous group private to that action*, then the scope of a retarget is decided purely by which group the button names: - `retarget` pointed at `"all music"` → every control referencing that group narrows together; - `retarget` pointed at Fader2's own (named, but used once) group → only Fader2 narrows. No `scope: control | group` parameter, no strip block. "Which controls move together" becomes a config-authoring decision — share a group name or don't — rather than a feature with two modes. That also makes the two modes in the issue collapse into one: "retarget to the system-wide active node" is just the same filter applied to a group whose candidate set is `.*`. ## What it means in the code Today the alias is gone by the time anything runs: `ExpandFriendlyNames` splices alias entries into each action's parameter lists in-place, and `IActionBase::Init` then builds a `NodeMatcher` copy from them (`src/AppSettings.cpp`, `src/Actions/IAction.cpp:14-16`). Making groups live means roughly: 1. A runtime registry of groups, each holding its candidate `NodeMatcher` plus mutable state (`All` / `ActiveOnly` / pinned ids). It has to outlive the actions, so it sits next to `AudioService`, not in the parsed `AppSettings`; actions hold a non-owning handle resolved at `Init`. 2. `MatchedNodes()` becomes "ask `AudioService::GetMatchingNodes` for the candidate set, then apply the group's current filter". Ranking and the representative stay where they are — the filter is a post-filter over the ranked id list, so `front()` keeps its meaning. 3. Listener registration can stay as it is: `RegisterNodeListener` subscribes against the *candidate* matcher, which a state change does not alter. So no re-subscription churn — but a group state change does need a new notification edge (group → its subscriber actions) so every affected control re-reads and refreshes its LED, parallel to the existing `on_node_available` / `on_node_removed` path. 4. It has to apply to all node-valued parameters, not just `target`: `Action_Connection` takes `left`/`right`, and `ExpandFriendlyNames` already expands every parameter indiscriminately. Mixed lists like `"target": ["all music", "firefox.*"]` work today by splicing. Under groups the natural reading is "union of the resolved groups and the inline entries, each group contributing its *filtered* members, with expansion keeping its position so configured order still ranks" — worth confirming that's what you want, because it means one half of a target can be narrowed while the other isn't. ## This decouples #16 from #14 The issue text claims #16 and #14 should be designed together because both are blocked on actions being independent. With groups, that's only half true: they share a *symptom*, not a mechanism. #16's shared state is a target set, which groups own cleanly. #14's shared state is a continuous value (knob trim feeding the fader's mapping) — a group has nothing to say about it. So they want different solutions and can proceed independently. ## What the reframe does *not* answer Everything under "Open questions" that's about semantics rather than plumbing survives unchanged: what "active" means, whether node state transitions are observable through the current listener path, ties, momentary vs latched, and the nothing-active fallback. It also adds some: - **Group state is global.** Two buttons pointing at the same group with different filters — last writer wins, or a stack of overlays that unwinds in order? Momentary buttons make the stack question concrete. - **LED feedback matters more**, since a latched group overlay can now visibly change controls on strips the user isn't looking at. - **Nesting.** Aliases are deliberately single-level today, which is why they can't cycle. Groups referencing groups is tempting; keeping the restriction is probably right. ## Proposal Split it: the FriendlyNames → live node groups rework is a prerequisite with its own value (it's a config-semantics change with its own data model and compatibility story), and #16 becomes the retarget action on top of it. Want me to open that as a separate issue with the data model sketched out, and reduce #16's scope to the action plus the "active" semantics?
Sign in to join this conversation.
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#16
No description provided.