feat(volume): Focused window volume in Hyprland - #163
Merged
Merged
Conversation
Focus volume and every other focused-app feature needed KDE Plasma (kdotool) or X11 (xdotool). Hyprland serves neither, so on it the features were dead and the app warned once per launch that the focused window could not be resolved. Hyprland does expose the focused window, over its own IPC socket rather than a Wayland protocol: `hyprctl -j activewindow` returns the pid, class and title in one call -- the same three identifiers the chained xdotool subcommands produce, including the class/title pair that Steam and Proton streams are matched on. Add it as a third tool, tried first and gated on the session so it costs nothing elsewhere, and reach it through the same flatpak-spawn shim as pactl. This says nothing about wlroots compositors in general; the API is Hyprland's own. GNOME still has none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5365SdDpEexv8TQyTYc3w
initSessions fired its ADDED/REMOVED/CHANGED events from inside
`synchronized (sessions)`. An observer is foreign code, and
MuteColorService takes its own monitor and then calls back into
getAllSessions, which wants `sessions` -- so the two acquire the same
pair in opposite orders and deadlock. The JVM names it outright:
"PulseAudio change listener": holds the sessions HashSet,
waits for the MuteColorService monitor (recomputeAll)
"executor-thread-6": holds the MuteColorService monitor,
waits for the sessions HashSet (getAllSessions)
Every dial then stops responding, not just the focused-app ones: the
command handler blocks on `sessions` behind the same cycle.
The MuteColorService side is reached from LinuxWindowFocusPoller, which
fires only when the focused application *changes*. Where the focused
window never resolves, getFocusApplication() is always null, the poller
never fires, and the cycle is unreachable -- which is why this stayed
hidden on any desktop without focus support.
Collect the events under the lock and fire them after releasing it,
which is what the initial-state notification a few lines above already
does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5365SdDpEexv8TQyTYc3w
setProcessVolume/setFocusVolume call setVolumeNoTrigger before writing, so the cached session matches what pactl is about to report and the CHANGED diff sees nothing. That holds only while each write's event arrives before the next write goes out. Turning a dial breaks it: writes leave every few milliseconds and their events land tens of milliseconds later, so the diff compares an already-superseded cached value against a newer one, calls it a change, and force-volume re-asserts the position of whichever App-volume dial also targets that process. With one app on both an App-volume dial and the focus dial the two then alternate for the length of the sweep, which is plainly audible. Track the volumes we wrote per session index for a short window and treat a CHANGED whose value is one of them as ours. A window rather than a single expected value because a sweep has many writes in flight and their events can arrive out of order; keyed on the value so a genuine external change to some other volume is still reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5365SdDpEexv8TQyTYc3w
Conflicts, all small: - LinuxProcessHelper: upstream's Tool.available now takes the ProcessHelper (a9f6d70); kept that signature with the Hyprland session gate in front. - LinuxProcessHelperTest: both sides appended a test; kept both. - CHANGELOG: both sides added to the unreleased section; kept both, and added the two entries the bug fixes were missing. initSessions needed care rather than a textual resolution: upstream moved it onto an Optional from getSessionsFromCmd with an early return, inside the same synchronized block the deadlock fix empties. The merged method keeps both -- the early return, and collecting the events under the lock to fire them after it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5365SdDpEexv8TQyTYc3w
hyprctl answers `{}` whenever focus is on no window at all - an empty
workspace, or a layer surface such as a launcher, a notification or the
lock screen. That empty result went down the same path as a tool that
could not answer, so it raised warnFocusUnavailable() and, with it, the
one-shot "focused-app control unavailable" desktop notification. One
knob tick at such a moment left a Hyprland user permanently told that
focus volume cannot work here, while it works fine.
Read hyprctl's reply as the three-valued answer it is (a window /
nothing focused / unreadable) and warn only on the last. The Settings
"detect focused app" toast says so plainly too, instead of quoting
hyprctl as if it had failed. Only hyprctl gets this: a silent
kdotool/xdotool may equally mean the desktop refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
|
Merged, thanks. Tested on WSL (Linux on Windows), seemed to work. Moved the changelog bullets — they were under an I'll trigger a snapshot build for the 2.1 once the changes land. |
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.
Add support for focused window volume using Hyprland's
hyprctl. While not a full Wayland support, some users who also use this compositor might benefit.Hyprland exposes the focused window over its own IPC socket, so
hyprctl -j activewindowgives the pid, class and title in one call which are the same three identifiers the chained xdotool subcommands produce. Adds it as a thirdToolinLinuxProcessHelper, tried first and gated on the session so it costs nothing on KDE/X11/GNOME. Hyprctl ships with the compositor.Additional bugs that I encoundered and Claude investigated/fixed (have to be fixed for this feature to work properly):
initSessionsfired CDI events insidesynchronized (sessions).MuteColorServicetakes its own monitor then callsgetAllSessions, so the two lock in opposite orders and deadlock; the command handler queues behind the same lock, freezing every control. Collect under the lock, fire after.setVolumeNoTriggeronly suppresses self-triggering while each write event arrives before the next write goes out. A dial sweep breaks that, so force-volume re-asserts another dial's position mid-turn. Track the volumes we wrote per session for 750ms and treat those CHANGED events as ours.The feature and fixes were planned out and implemented by Claude Code. All tests ran and passed. I did the manual code review and end user testing on multiple apps.