From 0f3bddc452c08efea28a6abb35b45957b6b0a84a Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Tue, 18 Aug 2026 13:58:33 -0400 Subject: [PATCH] Expose live application audio groups --- .../dot/quickshell/audio-streams-harness.qml | 20 +++++++++++++++ .../dot/quickshell/services/AudioDevices.qml | 25 +++++++++++++++++++ .../quickshell/application-volume-contract.sh | 8 ++++++ 3 files changed, 53 insertions(+) diff --git a/config/dot/quickshell/audio-streams-harness.qml b/config/dot/quickshell/audio-streams-harness.qml index 21e0827..a7bdc1d 100644 --- a/config/dot/quickshell/audio-streams-harness.qml +++ b/config/dot/quickshell/audio-streams-harness.qml @@ -1,8 +1,10 @@ import Quickshell import Quickshell.Io +import Quickshell.Services.Pipewire import QtQuick import "services/AudioStreams.js" as AudioStreams +import qs.services ShellRoot { readonly property int audioOutStreamFlag: 4 @@ -112,5 +114,23 @@ ShellRoot { muted: chromium.nodes.map(node => node.audio.muted) }); } + + function serviceSummary(): string { + const applications = AudioDevices.applications; + return JSON.stringify({ + count: applications.length, + validTypes: applications.every(application => + application.nodes.every(node => + (node.type & PwNodeType.AudioOutStream) + === PwNodeType.AudioOutStream)) + }); + } + + function invalidMutations(): string { + return JSON.stringify({ + nullVolume: AudioDevices.setApplicationVolume(null, 0.5), + emptyMute: AudioDevices.setApplicationMuted({ nodes: [] }, true) + }); + } } } diff --git a/config/dot/quickshell/services/AudioDevices.qml b/config/dot/quickshell/services/AudioDevices.qml index 8c7851e..4128151 100644 --- a/config/dot/quickshell/services/AudioDevices.qml +++ b/config/dot/quickshell/services/AudioDevices.qml @@ -8,6 +8,8 @@ import Quickshell import Quickshell.Services.Pipewire import QtQuick +import "AudioStreams.js" as AudioStreams + Singleton { id: root @@ -18,6 +20,13 @@ Singleton { !node.isStream && (node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource) + readonly property var playbackStreams: Pipewire.nodes.values.filter(node => + node.ready && node.audio + && (node.type & PwNodeType.AudioOutStream) === PwNodeType.AudioOutStream) + + readonly property var applications: AudioStreams.group( + root.playbackStreams, PwNodeType.AudioOutStream) + function nodes(output: bool): var { return output ? root.outputs : root.inputs; } @@ -40,4 +49,20 @@ Singleton { return "Unknown device"; return node.description || node.nickname || node.name || "Unknown device"; } + + function applicationVolume(application: var): real { + return AudioStreams.volume(application); + } + + function applicationMuted(application: var): bool { + return AudioStreams.muted(application); + } + + function setApplicationVolume(application: var, value: real): bool { + return AudioStreams.setVolume(application, value); + } + + function setApplicationMuted(application: var, muted: bool): bool { + return AudioStreams.setMuted(application, muted); + } } diff --git a/tests/quickshell/application-volume-contract.sh b/tests/quickshell/application-volume-contract.sh index 0cd4eb6..c7c6fc8 100755 --- a/tests/quickshell/application-volume-contract.sh +++ b/tests/quickshell/application-volume-contract.sh @@ -77,6 +77,14 @@ mute_result="$(qs_for_harness ipc call application-volume-test mutateMute)" jq -e '.changed == true and .muted == [true, true]' <<<"$mute_result" >/dev/null \ || fail "mute mutation did not reach every stream: $mute_result" +service_summary="$(qs_for_harness ipc call application-volume-test serviceSummary)" +jq -e '.count >= 0 and .validTypes == true' <<<"$service_summary" >/dev/null \ + || fail "live service exposed invalid playback groups: $service_summary" + +invalid_result="$(qs_for_harness ipc call application-volume-test invalidMutations)" +jq -e '.nullVolume == false and .emptyMute == false' <<<"$invalid_result" >/dev/null \ + || fail "service mutators accepted missing audio nodes: $invalid_result" + if rg -n 'ReferenceError|TypeError|Binding loop|Cannot assign|PwObjectTracker' "$shell_log"; then fail 'isolated harness emitted a QML runtime error' fi