130 lines
4.6 KiB
QML
130 lines
4.6 KiB
QML
// Fixture-fed harness for the three sound services that shell out:
|
|
// SoundCards (pactl list cards / set-card-profile), SoundRouting (pactl
|
|
// move-sink-input) and SoundDefaults (pw-metadata -n default).
|
|
//
|
|
// Nothing here touches the real audio graph. The contracts that drive this file
|
|
// replace pactl and pw-metadata with recording stubs, so what is under test is
|
|
// the parsing and the argv -- not PipeWire, which has its own opinions and a
|
|
// different set on every machine.
|
|
//
|
|
// Application groups arrive as plain objects with the same shape AudioStreams.js
|
|
// produces, because that is what the services are handed at runtime and it is
|
|
// the part a refactor is most likely to break silently.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
import qs.services
|
|
|
|
ShellRoot {
|
|
id: root
|
|
|
|
// Two streams of one application, the way PipeWire numbers them: an
|
|
// `object.serial` that pactl accepts, distinct from the node id.
|
|
readonly property var twoStreamGroup: ({
|
|
key: "org.chromium.Chromium",
|
|
label: "Chromium",
|
|
icon: "chromium",
|
|
nodes: [
|
|
{ id: 61, properties: { "object.serial": "412" } },
|
|
{ id: 62, properties: { "object.serial": "418" } }
|
|
]
|
|
})
|
|
|
|
// A stream PipeWire never gave a serial. The node id is the fallback, and
|
|
// it has to be one: an application that cannot be moved is worse than one
|
|
// moved by a less stable handle.
|
|
readonly property var seriallessGroup: ({
|
|
key: "mpv",
|
|
label: "mpv",
|
|
icon: "mpv",
|
|
nodes: [{ id: 77, properties: {} }]
|
|
})
|
|
|
|
readonly property var emptyGroup: ({
|
|
key: "gone",
|
|
label: "Gone",
|
|
icon: "",
|
|
nodes: []
|
|
})
|
|
|
|
function groupNamed(name: string): var {
|
|
if (name === "twoStream")
|
|
return root.twoStreamGroup;
|
|
if (name === "serialless")
|
|
return root.seriallessGroup;
|
|
return root.emptyGroup;
|
|
}
|
|
|
|
IpcHandler {
|
|
target: "sound-services-test"
|
|
|
|
// ── SoundCards ──────────────────────────────────────────────────────
|
|
function cards(): string {
|
|
return JSON.stringify({
|
|
busy: SoundCards.busy,
|
|
lastError: SoundCards.lastError,
|
|
cards: SoundCards.cards
|
|
});
|
|
}
|
|
|
|
function refreshCards(): bool {
|
|
SoundCards.refresh();
|
|
return true;
|
|
}
|
|
|
|
function setProfile(card: string, profile: string): bool {
|
|
return SoundCards.setProfile(card, profile) !== false;
|
|
}
|
|
|
|
// ── SoundRouting ────────────────────────────────────────────────────
|
|
function routing(): string {
|
|
return JSON.stringify({
|
|
busy: SoundRouting.busy,
|
|
lastError: SoundRouting.lastError
|
|
});
|
|
}
|
|
|
|
function moveApplication(group: string, sink: string): bool {
|
|
return SoundRouting.moveApplication(root.groupNamed(group), sink) !== false;
|
|
}
|
|
|
|
function routeToDefault(group: string): bool {
|
|
return SoundRouting.routeToDefault(root.groupNamed(group)) !== false;
|
|
}
|
|
|
|
function currentSinkFor(group: string): string {
|
|
return String(SoundRouting.currentSinkFor(root.groupNamed(group)) ?? "");
|
|
}
|
|
|
|
// ── SoundDefaults ───────────────────────────────────────────────────
|
|
function defaults(): string {
|
|
return JSON.stringify({
|
|
sink: SoundDefaults.configuredSinkName,
|
|
source: SoundDefaults.configuredSourceName
|
|
});
|
|
}
|
|
|
|
function refreshDefaults(): bool {
|
|
SoundDefaults.refresh();
|
|
return true;
|
|
}
|
|
|
|
// What the ghost row would render, and what it would call the device.
|
|
// The name is all there is to go on: the node is gone, so there is no
|
|
// description to borrow.
|
|
function labelFor(name: string): string {
|
|
return SoundDefaults.label(name);
|
|
}
|
|
|
|
function absent(output: string): string {
|
|
const record = SoundDefaults.absent(output === "output");
|
|
return JSON.stringify(record === null ? { present: true } : {
|
|
present: false,
|
|
name: record.name,
|
|
label: record.label
|
|
});
|
|
}
|
|
}
|
|
}
|