Give Sound the whole story, and keep the buttons inside the card
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user