// Read-only contract harness for the Sound page. It instantiates every device // row against the real PipeWire graph but exposes no mutating IPC methods. // // The two services that read the machine rather than the graph -- SoundDefaults // and SoundCards -- are fed through their own fixture seams by the contract, so // the ghost row and the profile card can be measured against canned state // instead of against whatever hardware is plugged in today. import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import QtQuick import qs.services import qs.modules.settings ShellRoot { id: root QtObject { id: fixtureAudio property real volume: 0.42 property bool muted: false } QtObject { id: fixtureNode property QtObject audio: fixtureAudio } readonly property var fixtureApplication: ({ key: "fixture.player", label: "Fixture Player", icon: "audio-x-generic-symbolic", nodes: [fixtureNode] }) SoundPage { width: 760 height: 900 } // A device list of its own, so the ghost row can be read without reaching // into the page's internals. SoundDeviceList { id: outputList width: 620 visible: false output: true } SoundCaptureRow { id: captureProbe width: 620 } ApplicationMixer { id: populatedMixer width: 620 visible: false applications: [root.fixtureApplication] pipewireReady: true } ApplicationMixer { id: emptyMixer width: 620 visible: false applications: [] pipewireReady: true } ApplicationMixer { id: unavailableMixer width: 620 visible: false applications: [] pipewireReady: false } PwObjectTracker { objects: AudioDevices.outputs.concat(AudioDevices.inputs) } IpcHandler { target: "sound-page-test" function status(): string { return JSON.stringify({ ready: Pipewire.ready, outputs: AudioDevices.outputs.length, inputs: AudioDevices.inputs.length, applications: AudioDevices.applications.length, defaultOutput: AudioDevices.label(AudioDevices.current(true)), defaultInput: AudioDevices.label(AudioDevices.current(false)), populatedRows: populatedMixer.rowCount, populatedStatus: populatedMixer.statusText, emptyRows: emptyMixer.rowCount, emptyStatus: emptyMixer.statusText, unavailableRows: unavailableMixer.rowCount, unavailableStatus: unavailableMixer.statusText, captureApplications: AudioDevices.captureApplications.length, captureTypesValid: AudioDevices.captureApplications.every(application => application.nodes.every(node => (node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream)), captureRowVisible: captureProbe.visible, ghostVisible: !!outputList.absent, ghostLabel: outputList.absent ? String(outputList.absent.label) : "" }); } } }