Add permanent, cross-instance-synced hide/show for list items (Ctrl+t) - #72
Open
HoneyHazard wants to merge 11 commits into
Open
Add permanent, cross-instance-synced hide/show for list items (Ctrl+t)#72HoneyHazard wants to merge 11 commits into
HoneyHazard wants to merge 11 commits into
Conversation
Lets a user hide individual nodes/devices from their own running instance with the t key (toggle). Hidden state is purely local: never persisted to disk, never synced to other running instances - that's covered by a planned follow-up perma-hide tier (Ctrl+t). - Hidden items sink to the bottom of their list, in a stable sort that otherwise preserves the existing object_serial order - both the visible group and the hidden group keep their relative order. - Excluded from peak capture: start_capture() gates on hidden_instance the same way it already gates on lazy_capture's visible_objects, and toggling hidden explicitly stops/resumes an in-flight capture immediately rather than waiting for the next eligibility event. - New row_hidden theme key, patched onto a hidden row's text spans (title, target, volume%, and Configuration tab's device/profile) using the same Style::patch() no-op-by-default mechanism as the rest of the theme system. - New hidden_instance char_set key (default "[hide] ") prepended to a hidden item's title, so hidden items are identifiable even with row_hidden left unset - built-in char_sets and wiremix.toml's docs suggest an emoji alternative (e.g. "🙈 ").
Two fixes to the per-instance hide feature:
- row_hidden defaulted to an empty style in every built-in theme, so the
only visual difference for a hidden row was its char_set prefix
("[hide] "/emoji) - easy to miss at a glance. default now dims it with
DarkGray, nocolor with the DIM modifier (matching that theme's existing
"dim = inactive" convention elsewhere). plain is left alone on purpose -
it's styled nowhere else either, relying entirely on the prefix text.
- Hiding the selected item left the selection pinned to it. Since hidden
items sink to the bottom of the list, this meant the cursor stayed on a
now-relocated, visually de-emphasized row instead of following what the
user was actually looking at. ObjectList::release_hidden_selection()
moves the selection to whatever comes right after the hidden item in
the list's current (pre-sink) order, falling back to whatever comes
before it if it was last, or to no selection at all if it was the only
item. Unhiding intentionally keeps the selection where it is - the user
is still looking at that item, there's nothing to release.
HoneyHazard
force-pushed
the
hide-items-permanent
branch
from
August 8, 2026 10:35
9e58eb8 to
e8946d4
Compare
HoneyHazard
force-pushed
the
hide-items-permanent
branch
from
August 9, 2026 09:41
5e88716 to
a4fd80c
Compare
Hiding an item for this instance currently always stops its peak capture unconditionally, with no way to opt out. That's a hardcoded behavior, not a configurable one - hiding is about decluttering the visible list, and some users may want hidden items to keep reporting levels exactly like any other item rather than having that tied to visibility. Adds capture_hidden: bool (default true), also available as --capture-hidden/--no-capture-hidden on the command line. It's a filter layered on top of the existing capture-eligibility rules (lazy_capture, filters, etc.), never a replacement for them - when true (the default), hidden items are captured under exactly the same rules as regular ones, reproducing today's non-hidden-item behavior for hidden items too. When false, hidden items are excluded from capture in addition to whatever else already excludes non-hidden items, matching the previous hardcoded-exclusion behavior exactly. - start_capture()'s existing hidden_instance check is now gated behind !capture_hidden instead of always applying. - ToggleHiddenInstance's explicit stop_capture() call on hide (and the resuming start_capture() call on unhide) are similarly gated - important not just for correctness under the new default, but to avoid double-starting an already-running capture stream when capture_hidden is true and hiding never stopped it in the first place. - When monitoring actually is suspended for a hidden item (hidden + capture_hidden = false), NodeWidget now leaves the peak meter completely blank instead of drawing the usual inactive-looking placeholder over a stream that will never receive samples, which otherwise reads as broken rather than intentionally off.
Extends the per-instance hide from the previous branch with a second, independent tier: Ctrl+t hides/shows the selected item durably - saved to disk, and rediscovered against live nodes on every future launch (including other already-running instances that reload the same file, though nothing here makes that reload happen live yet - see the follow-up branch for cross-instance sync via PipeWire Metadata). - Items are identified by node.name via a new MatchCondition::from_node_name() constructor, reusing the existing [[filters]] matching engine rather than raw object IDs, which don't survive a restart. - New src/hidden_state.rs module handles load/save of a small $XDG_STATE_HOME/wiremix/hidden.toml file (mirroring the existing $XDG_CONFIG_HOME resolution for the main config file), with an atomic temp-file-then-rename write. - Adds Serialize support to MatchCondition/MatchValue/PropertyKey (previously deserialize-only, since nothing needed to write a matcher back out before this), implemented as the exact inverse of their existing FromStr parsing. - Sort order becomes three-tier: visible, then instance-hidden, then permanently-hidden at the very end - both hidden groups keep their own relative order. - Reuses the row_hidden theme key from the previous branch (same faint-text treatment for both hidden tiers) and adds a second char_set key, hidden_permanent (default "[perm-hide] "), so the two tiers stay visually distinguishable by their title prefix even when styled identically. - Capture gating gate is evaluated directly against the durable matcher list, not a cached per-object-ID set - the initial flood of capture-eligibility events on startup is handled before the first opportunity to recompute that cache, so relying on the cache alone let an already-permanently-hidden node start capturing again on every fresh launch (caught via live pw-dump verification, not the unit tests, which mock past the real startup ordering).
Watches the permanent-hide state file's directory for changes made by other wiremix instances (e.g. another instance's own Ctrl+t), so the change takes effect immediately everywhere instead of only on each instance's next restart. Uses inotify on the directory rather than the file itself, since HiddenState::save() writes via temp file + rename and a direct file watch would go stale after the first rename. Linux-only, gated behind target_os = "linux" with a no-op fallback elsewhere, matching wiremix's existing de facto Linux-only scope (unconditional nix dependency, no PipeWire port to other platforms). This replaces an earlier PipeWire-Metadata-based approach for live sync that was abandoned after a production incident; this design has no PipeWire object creation/lifecycle involved at all, only local filesystem event watching reusing the same directory HiddenState::save() already writes to.
Same fix as the per-instance hide/show branch, applied to Ctrl+t: hiding the selected item now moves the selection to whatever's next in line (falling back to the previous item, or no selection at all if it was the only one) instead of leaving it pinned to an item that's about to sink to the bottom of the list. Reuses release_hidden_selection(), already added for the instance-hide case. Un-hiding still keeps the selection where it is.
Shorter and reads more clearly at a glance than "perm-hide" - "[hide]" (instance) vs "[HIDE]" (permanent) distinguishes the two by case alone rather than needing an extra word.
NodeWidget's hidden field was split into hidden_instance/ hidden_permanent by the permanent-hide work, but the capture_hidden meter-blanking check from the earlier commit still referenced the old single hidden field. Combine both tiers, matching the pattern already used elsewhere in this widget for hidden-item styling.
The capture_hidden filter introduced alongside per-instance hide only covered hidden_instance. Extend the same !capture_hidden gating to: - start_capture()'s hidden_permanent_matchers check - ToggleHiddenPermanent's stop_capture()/start_capture() calls, with the same double-start guard as the instance case - apply_file_hidden_state_change()'s stop sweep, so another instance's hidden-state file change respects the setting too By default (capture_hidden = true), permanently-hidden items keep being monitored exactly like regular ones, matching the per-instance behavior from the previous commit.
HoneyHazard
force-pushed
the
hide-items-permanent
branch
from
August 9, 2026 11:03
a4fd80c to
c426ba7
Compare
HoneyHazard
marked this pull request as ready for review
August 10, 2026 05:19
HoneyHazard
added a commit
to HoneyHazard/wiremix
that referenced
this pull request
Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
That being said, I hope these can be helpful and useful additions that users could appreciate.
Depends on the per-instance hide PR - this branch is stacked on top of it, so the diff shown here includes its changes too until that one merges. Please review/merge that one first if possible; happy to rebase this once it lands.
The previous PR added
t("toggle hide") to hide an item for the current session only - useful, but it's forgotten the moment wiremix restarts. This adds a second, independent tier onCtrl+t- the same "toggle hide" mnemonic, just permanent: a permanent hide that's saved to disk and rediscovered on every future launch, including live-syncing to any other wiremix instances that are already running against the same state file - no restart needed on either side.node.name(via a newMatchCondition::from_node_name()constructor), reusing the exact same matching engine[[filters]]already uses - not raw object IDs, which never survive a restart.$XDG_STATE_HOME/wiremix/hidden.tomlfile holds the list of hidden matchers, written atomically (temp file + rename) so a crash - or another instance saving at the same moment - can't leave a half-written file behind.save()'s temp-file-then-rename replaces the underlying inode). Linux-only, gated behindtarget_os = "linux"with a no-op fallback elsewhere (a no-op is a safe fallback here since wiremix already depends unconditionally on Linux-only things likenixand has no PipeWire port to other platforms); everywhere else, sync still happens on next restart via the existing load-at-startup path.row_hiddentheme key from the previous PR for both tiers' text styling (now defaulting to a visibleDarkGray/DIMtint rather than empty - see that PR for details), and adds one new char_set key,hidden_permanent(default"[perm-hide] "), so the two tiers stay visually distinguishable by their title prefix even when styled identically.capture_hiddenoption from the per-instance hide PR automatically (this branch stacks on top of it) and extends the same filtering to this tier: permanently-hidden items are captured under the same rules as regular ones by default, or excluded - with a blank peak meter - via--no-capture-hidden. The exclusion applies to the cross-instance file-sync path too, so another instance's hidden-state change stops (or resumes) capture here exactly the way togglingCtrl+tlocally would.Default theme with just
row_hiddenset.[hide] pw-catis instance-hidden (t);[perm-hide] defconis permanently hidden (Ctrl+t) and ranks below it.Both instances show the same
[HIDE] SW_MICS_PREitem, permanently hidden withCtrl+t. Left: defaultcapture_hidden = true- the meter keeps reporting real levels. Right:--no-capture-hidden- the meter is blank because the capture stream itself is stopped.A real bug this surfaced, worth calling out explicitly: the capture-eligibility gate can't just check a cached "is this object currently permanently hidden" set - on startup, PipeWire delivers an initial flood of eligibility events before there's ever been a chance to compute that cache from the freshly-loaded state file, so a node that's genuinely supposed to stay permanently hidden would start being captured again on every fresh launch. Fixed by evaluating the capture gate directly against the durable matcher list instead of the cache (the cache is still used for the cheap, non-critical stuff - sort order and styling). Found this via live
pw-dumpverification, not the unit tests - the mocked test setup doesn't reproduce the real startup event ordering that exposed it.On the live-sync design: an earlier version of this used a PipeWire Metadata object to broadcast changes between instances, but that approach was abandoned after causing a real, live production issue, so it's not part of this PR's history. The inotify-based approach here has no PipeWire object creation or lifecycle involved at all - it's purely local filesystem event watching, reusing the same directory
HiddenState::save()already writes to for restart-persistence. That keeps the blast radius of a bug in the sync path limited to "this instance's hidden list doesn't update live," never "this instance corrupts or confuses the shared PipeWire graph."Tested:
cargo test --release: 181/181 passing (includes the same selection-release edge-case tests as the per-instance hide PR, applied toCtrl+t, pluscapture_hiddencoverage for the permanent tier: eligibility checks, explicit toggling, and the file-sync path, in both directions)cargo fmt --check/cargo clippy --release --all-targets -- -D warnings/cargo doc --no-deps(matching this repo's CI): all clean$XDG_STATE_HOME: permanently hiding a node stops its capture stream immediately; a full process restart rediscovers it as hidden from the saved file and it stays uncaptured (this is what caught the bug above - an earlier version of this branch showed the item as hidden in the UI on restart, but was still silently capturing it); un-hiding resumes the capture and clears the file entry$XDG_STATE_HOME, hiding a node in one instance stops capture in both within about a second, with no restart of either; un-hiding does the same in reverse. Verified viapw-dump, checking both instances' PipeWire client nodes directly, since simultaneous real background audio activity made the on-screen list too noisy to eyeball reliablyCtrl+tunused (andhidden_permanentunset in config) never touches the state file or changes any existing behaviorcapture_hidden's default (permanently-hidden items keep reporting levels) and--no-capture-hidden(blank meter, immediately on toggle and on file-sync from another instance) both work for this tier the same way they do for the per-instance one