Add Control Center integration services
This commit is contained in:
@@ -16,7 +16,8 @@ Singleton {
|
|||||||
property bool kdeInitialized: false
|
property bool kdeInitialized: false
|
||||||
property string previousOutput: ""
|
property string previousOutput: ""
|
||||||
property string previousBluetooth: ""
|
property string previousBluetooth: ""
|
||||||
property string previousKde: ""
|
property bool previousKdeReachable: false
|
||||||
|
property string previousKdeName: ""
|
||||||
|
|
||||||
PwObjectTracker { objects: [Pipewire.defaultAudioSink] }
|
PwObjectTracker { objects: [Pipewire.defaultAudioSink] }
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ Singleton {
|
|||||||
.join("\u001f");
|
.join("\u001f");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string kdeName: KdeConnect.preferredPhone?.name ?? ""
|
||||||
|
|
||||||
onOutputNameChanged: {
|
onOutputNameChanged: {
|
||||||
if (!root.outputInitialized) {
|
if (!root.outputInitialized) {
|
||||||
root.previousOutput = root.outputName;
|
root.previousOutput = root.outputName;
|
||||||
@@ -78,55 +81,41 @@ Singleton {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
root.previousOutput = root.outputName;
|
root.previousOutput = root.outputName;
|
||||||
root.previousBluetooth = root.bluetoothNames;
|
root.previousBluetooth = root.bluetoothNames;
|
||||||
|
root.previousKdeReachable = KdeConnect.phoneReachable;
|
||||||
|
root.previousKdeName = root.kdeName;
|
||||||
root.outputInitialized = true;
|
root.outputInitialized = true;
|
||||||
root.bluetoothInitialized = true;
|
root.bluetoothInitialized = true;
|
||||||
|
root.kdeInitialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Connections {
|
||||||
interval: 15000
|
target: KdeConnect
|
||||||
repeat: true
|
|
||||||
running: true
|
|
||||||
triggeredOnStart: true
|
|
||||||
onTriggered: {
|
|
||||||
if (!kdeProbe.running)
|
|
||||||
kdeProbe.running = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
function onPhoneReachableChanged(): void {
|
||||||
id: kdeProbe
|
if (!root.kdeInitialized)
|
||||||
command: ["kdeconnect-cli", "-a", "--name-only"]
|
return;
|
||||||
stdout: StdioCollector {
|
const reachable = KdeConnect.phoneReachable;
|
||||||
onStreamFinished: {
|
if (reachable === root.previousKdeReachable)
|
||||||
const names = this.text.split("\n")
|
return;
|
||||||
.map(line => line.trim())
|
StatusEvents.publish({
|
||||||
.filter(line => line && !/^\d+ devices? found$/.test(line))
|
key: "device-kdeconnect",
|
||||||
.sort()
|
glyph: "\u{F03F2}",
|
||||||
.join("\u001f");
|
title: root.kdeName || root.previousKdeName || "Phone",
|
||||||
if (root.kdeInitialized && names !== root.previousKde) {
|
detail: reachable ? "KDE Connect available" : "KDE Connect disconnected",
|
||||||
const current = names ? names.split("\u001f") : [];
|
tone: reachable ? "accent" : "warn",
|
||||||
const previous = root.previousKde ? root.previousKde.split("\u001f") : [];
|
priority: StatusEvents.ambientPriority
|
||||||
const connected = current.find(name => !previous.includes(name));
|
});
|
||||||
const disconnected = previous.find(name => !current.includes(name));
|
root.previousKdeReachable = reachable;
|
||||||
StatusEvents.publish({
|
root.previousKdeName = root.kdeName || root.previousKdeName;
|
||||||
key: "device-kdeconnect",
|
|
||||||
glyph: "\u{F03F2}",
|
|
||||||
title: connected || disconnected || "Phone",
|
|
||||||
detail: connected ? "KDE Connect available" : "KDE Connect disconnected",
|
|
||||||
tone: connected ? "accent" : "warn",
|
|
||||||
priority: StatusEvents.ambientPriority
|
|
||||||
});
|
|
||||||
}
|
|
||||||
root.previousKde = names;
|
|
||||||
root.kdeInitialized = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
root.previousOutput = root.outputName;
|
root.previousOutput = root.outputName;
|
||||||
root.previousBluetooth = root.bluetoothNames;
|
root.previousBluetooth = root.bluetoothNames;
|
||||||
|
root.previousKdeReachable = KdeConnect.phoneReachable;
|
||||||
|
root.previousKdeName = root.kdeName;
|
||||||
discoverySettle.start();
|
discoverySettle.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
// Home Assistant state for the Control Center. Credentials and REST details
|
||||||
|
// remain behind the helper; QML receives configured favourites only.
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-home-assistant"
|
||||||
|
|
||||||
|
// "loading" | "ready" | "degraded" | "unavailable"
|
||||||
|
property string phase: "loading"
|
||||||
|
property var entities: []
|
||||||
|
property bool stale: false
|
||||||
|
property string busyEntityId: ""
|
||||||
|
property string lastError: ""
|
||||||
|
property bool fixtureMode: false
|
||||||
|
|
||||||
|
readonly property var visibleEntities: root.entities.slice(0, 4)
|
||||||
|
readonly property int configuredCount: root.entities.length
|
||||||
|
|
||||||
|
function refresh(): void {
|
||||||
|
if (root.fixtureMode || refreshProc.running)
|
||||||
|
return;
|
||||||
|
if (root.entities.length === 0)
|
||||||
|
root.phase = "loading";
|
||||||
|
refreshProc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumeSnapshot(text: string): void {
|
||||||
|
if (root.fixtureMode)
|
||||||
|
return;
|
||||||
|
let result = null;
|
||||||
|
try {
|
||||||
|
result = JSON.parse(text);
|
||||||
|
} catch (error) {
|
||||||
|
result = { ok: false, error: "invalid-response" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.ok === true) {
|
||||||
|
root.entities = Array.isArray(result.entities) ? result.entities : [];
|
||||||
|
root.phase = "ready";
|
||||||
|
root.stale = false;
|
||||||
|
root.lastError = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.lastError = String(result.error || "unreachable");
|
||||||
|
if (root.entities.length > 0) {
|
||||||
|
root.phase = "degraded";
|
||||||
|
root.stale = true;
|
||||||
|
} else {
|
||||||
|
root.phase = "unavailable";
|
||||||
|
root.stale = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEntity(entityId: string): void {
|
||||||
|
if (entityId === "" || root.busyEntityId !== "")
|
||||||
|
return;
|
||||||
|
if (root.fixtureMode) {
|
||||||
|
root.entities = root.entities.map(entity => {
|
||||||
|
if (entity.id !== entityId)
|
||||||
|
return entity;
|
||||||
|
return Object.assign({}, entity, {
|
||||||
|
active: !entity.active,
|
||||||
|
state: entity.active ? "off" : "on"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!root.entities.some(entity => entity.id === entityId))
|
||||||
|
return;
|
||||||
|
root.busyEntityId = entityId;
|
||||||
|
actionProc.command = [root.helperPath, "toggle", entityId];
|
||||||
|
actionProc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumeAction(text: string): void {
|
||||||
|
if (root.busyEntityId === "")
|
||||||
|
return;
|
||||||
|
let ok = false;
|
||||||
|
let errorCode = "action-failed";
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(text);
|
||||||
|
ok = result.ok === true;
|
||||||
|
errorCode = String(result.error || errorCode);
|
||||||
|
} catch (error) {
|
||||||
|
errorCode = "invalid-response";
|
||||||
|
}
|
||||||
|
root.busyEntityId = "";
|
||||||
|
if (ok) {
|
||||||
|
root.lastError = "";
|
||||||
|
refreshDelay.restart();
|
||||||
|
} else {
|
||||||
|
root.lastError = errorCode;
|
||||||
|
if (root.entities.length > 0) {
|
||||||
|
root.phase = "degraded";
|
||||||
|
root.stale = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(): void {
|
||||||
|
Quickshell.execDetached([root.helperPath, "open"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixtureEntities(): var {
|
||||||
|
return [
|
||||||
|
{ id: "light.fixture_all", name: "All lights", domain: "light", state: "on", available: true, active: true },
|
||||||
|
{ id: "light.fixture_kitchen", name: "Kitchen", domain: "light", state: "on", available: true, active: true },
|
||||||
|
{ id: "light.fixture_living", name: "Living room", domain: "light", state: "off", available: true, active: false },
|
||||||
|
{ id: "light.fixture_bedroom", name: "Bedroom", domain: "light", state: "on", available: true, active: true },
|
||||||
|
{ id: "light.fixture_hall", name: "Hall", domain: "light", state: "off", available: true, active: false },
|
||||||
|
{ id: "light.fixture_desk", name: "Desk", domain: "light", state: "off", available: true, active: false },
|
||||||
|
{ id: "light.fixture_corner", name: "Corner lamp", domain: "light", state: "unavailable", available: false, active: false }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFixture(name: string): void {
|
||||||
|
if (["ready", "stale", "unavailable"].indexOf(name) < 0)
|
||||||
|
return;
|
||||||
|
root.fixtureMode = true;
|
||||||
|
root.busyEntityId = "";
|
||||||
|
if (name === "unavailable") {
|
||||||
|
root.entities = [];
|
||||||
|
root.phase = "unavailable";
|
||||||
|
root.stale = false;
|
||||||
|
root.lastError = "not-configured";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.entities = root.fixtureEntities();
|
||||||
|
root.phase = name === "stale" ? "degraded" : "ready";
|
||||||
|
root.stale = name === "stale";
|
||||||
|
root.lastError = name === "stale" ? "unreachable" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFixture(): void {
|
||||||
|
root.fixtureMode = false;
|
||||||
|
root.phase = "loading";
|
||||||
|
root.entities = [];
|
||||||
|
root.stale = false;
|
||||||
|
root.busyEntityId = "";
|
||||||
|
root.lastError = "";
|
||||||
|
root.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: refreshProc
|
||||||
|
command: [root.helperPath, "snapshot"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.consumeSnapshot(this.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: actionProc
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.consumeAction(this.text)
|
||||||
|
}
|
||||||
|
onExited: (code, status) => {
|
||||||
|
if (root.busyEntityId !== "")
|
||||||
|
root.consumeAction("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 60000
|
||||||
|
repeat: true
|
||||||
|
running: ShellState.quickSettingsOpen && !root.fixtureMode
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: refreshDelay
|
||||||
|
interval: 350
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: root.refresh()
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
// Shared KDE Connect state. The Python helper owns all CLI and D-Bus parsing;
|
||||||
|
// this singleton exposes only normalized devices and user-initiated actions.
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-kdeconnect"
|
||||||
|
|
||||||
|
property bool fixtureMode: false
|
||||||
|
property bool available: false
|
||||||
|
property var devices: []
|
||||||
|
property string lastError: ""
|
||||||
|
|
||||||
|
property bool transferActive: false
|
||||||
|
property string transferFileName: ""
|
||||||
|
property string transferDeviceName: ""
|
||||||
|
property var recentExchange: null
|
||||||
|
|
||||||
|
property string actionKind: ""
|
||||||
|
property string actionPath: ""
|
||||||
|
|
||||||
|
readonly property var preferredPhone: {
|
||||||
|
const phones = root.devices.filter(device => device.type === "phone" && device.paired);
|
||||||
|
return phones.find(device => device.reachable) ?? phones[0] ?? null;
|
||||||
|
}
|
||||||
|
readonly property bool phoneReachable: root.preferredPhone?.reachable === true
|
||||||
|
readonly property int pairedCount: root.devices.filter(device => device.paired).length
|
||||||
|
readonly property var phoneActions: root.preferredPhone?.actions ?? []
|
||||||
|
|
||||||
|
function supports(action: string): bool {
|
||||||
|
return root.phoneActions.indexOf(action) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh(): void {
|
||||||
|
if (root.fixtureMode || statusProc.running)
|
||||||
|
return;
|
||||||
|
statusProc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function consumeStatus(text: string): void {
|
||||||
|
if (root.fixtureMode)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(text);
|
||||||
|
root.available = result.available === true;
|
||||||
|
root.devices = Array.isArray(result.devices) ? result.devices : [];
|
||||||
|
root.lastError = String(result.error ?? "");
|
||||||
|
} catch (error) {
|
||||||
|
root.available = false;
|
||||||
|
root.devices = [];
|
||||||
|
root.lastError = "invalid-response";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendFile(path: string): void {
|
||||||
|
if (!root.phoneReachable || !root.supports("share") || path === "" || actionProc.running)
|
||||||
|
return;
|
||||||
|
root.actionKind = "share";
|
||||||
|
root.actionPath = path;
|
||||||
|
root.transferActive = true;
|
||||||
|
root.transferFileName = path.split("/").pop();
|
||||||
|
root.transferDeviceName = root.preferredPhone.name;
|
||||||
|
actionProc.command = [root.helperPath, "send-file", root.preferredPhone.id, path];
|
||||||
|
actionProc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendClipboard(): void {
|
||||||
|
root.runSimpleAction("clipboard", "send-clipboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ring(): void {
|
||||||
|
root.runSimpleAction("ring", "ring");
|
||||||
|
}
|
||||||
|
|
||||||
|
function runSimpleAction(kind: string, command: string): void {
|
||||||
|
if (!root.phoneReachable || !root.supports(kind) || actionProc.running)
|
||||||
|
return;
|
||||||
|
root.actionKind = kind;
|
||||||
|
root.actionPath = "";
|
||||||
|
actionProc.command = [root.helperPath, command, root.preferredPhone.id];
|
||||||
|
actionProc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelTransfer(): void {
|
||||||
|
if (actionProc.running && root.actionKind === "share")
|
||||||
|
actionProc.signal(2);
|
||||||
|
root.actionKind = "";
|
||||||
|
root.actionPath = "";
|
||||||
|
root.transferActive = false;
|
||||||
|
root.transferFileName = "";
|
||||||
|
root.transferDeviceName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishAction(text: string): void {
|
||||||
|
if (root.actionKind === "")
|
||||||
|
return;
|
||||||
|
|
||||||
|
const kind = root.actionKind;
|
||||||
|
const localPath = root.actionPath;
|
||||||
|
const fileName = root.transferFileName;
|
||||||
|
const deviceName = root.transferDeviceName || root.preferredPhone?.name || "Phone";
|
||||||
|
let result = null;
|
||||||
|
try {
|
||||||
|
result = JSON.parse(text);
|
||||||
|
} catch (error) {
|
||||||
|
result = { ok: false, error: "invalid-response" };
|
||||||
|
}
|
||||||
|
|
||||||
|
root.actionKind = "";
|
||||||
|
root.actionPath = "";
|
||||||
|
if (kind === "share") {
|
||||||
|
root.transferActive = false;
|
||||||
|
root.transferFileName = "";
|
||||||
|
root.transferDeviceName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.ok === true && kind === "share") {
|
||||||
|
root.recentExchange = {
|
||||||
|
direction: "sent",
|
||||||
|
fileName: String(result.fileName || fileName),
|
||||||
|
deviceName,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
StatusEvents.publish({
|
||||||
|
key: "phone-transfer",
|
||||||
|
glyph: "\u{F03F2}",
|
||||||
|
title: "Sent to " + deviceName,
|
||||||
|
detail: String(result.fileName || fileName),
|
||||||
|
tone: "ok",
|
||||||
|
priority: StatusEvents.importantPriority,
|
||||||
|
actionId: localPath !== "" ? "open-path" : "",
|
||||||
|
actionData: localPath
|
||||||
|
});
|
||||||
|
} else if (result.ok !== true) {
|
||||||
|
root.lastError = String(result.error || "action-failed");
|
||||||
|
StatusEvents.publish({
|
||||||
|
key: "phone-action",
|
||||||
|
glyph: "\u{F03F2}",
|
||||||
|
title: kind === "share" ? "File was not sent" : "Phone action failed",
|
||||||
|
detail: "Check that the phone is nearby and try again",
|
||||||
|
tone: "warn",
|
||||||
|
priority: StatusEvents.importantPriority
|
||||||
|
});
|
||||||
|
}
|
||||||
|
refreshDelay.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFixture(name: string): void {
|
||||||
|
if (["reachable", "offline", "transfer"].indexOf(name) < 0)
|
||||||
|
return;
|
||||||
|
root.fixtureMode = true;
|
||||||
|
root.available = true;
|
||||||
|
root.lastError = "";
|
||||||
|
root.recentExchange = null;
|
||||||
|
root.devices = [{
|
||||||
|
id: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
name: "Fixture iPhone",
|
||||||
|
type: "phone",
|
||||||
|
paired: true,
|
||||||
|
reachable: name !== "offline",
|
||||||
|
actions: ["clipboard", "ping", "ring", "share"]
|
||||||
|
}];
|
||||||
|
root.transferActive = name === "transfer";
|
||||||
|
root.transferFileName = name === "transfer" ? "Fixture document.pdf" : "";
|
||||||
|
root.transferDeviceName = name === "transfer" ? "Fixture iPhone" : "";
|
||||||
|
root.actionKind = name === "transfer" ? "share" : "";
|
||||||
|
root.actionPath = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFixture(): void {
|
||||||
|
root.fixtureMode = false;
|
||||||
|
root.available = false;
|
||||||
|
root.devices = [];
|
||||||
|
root.lastError = "";
|
||||||
|
root.transferActive = false;
|
||||||
|
root.transferFileName = "";
|
||||||
|
root.transferDeviceName = "";
|
||||||
|
root.actionKind = "";
|
||||||
|
root.actionPath = "";
|
||||||
|
root.recentExchange = null;
|
||||||
|
root.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: statusProc
|
||||||
|
command: [root.helperPath, "status"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.consumeStatus(this.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: actionProc
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.finishAction(this.text)
|
||||||
|
}
|
||||||
|
onExited: (code, status) => {
|
||||||
|
if (root.actionKind !== "")
|
||||||
|
root.finishAction("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 30000
|
||||||
|
repeat: true
|
||||||
|
running: !root.fixtureMode
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: refreshDelay
|
||||||
|
interval: 500
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,18 @@ Singleton {
|
|||||||
if (PrivacyState.microphoneActive)
|
if (PrivacyState.microphoneActive)
|
||||||
result.push(root.privacyActivity("microphone", "\u{F036C}", "Microphone", PrivacyState.microphoneApp));
|
result.push(root.privacyActivity("microphone", "\u{F036C}", "Microphone", PrivacyState.microphoneApp));
|
||||||
|
|
||||||
|
if (KdeConnect.transferActive) {
|
||||||
|
result.push({
|
||||||
|
kind: "phone-transfer",
|
||||||
|
glyph: "\u{F03F2}",
|
||||||
|
label: "Sending to " + (KdeConnect.transferDeviceName || "Phone"),
|
||||||
|
detail: KdeConnect.transferFileName || "Preparing file",
|
||||||
|
state: "Sending",
|
||||||
|
tone: "accent",
|
||||||
|
action: "Cancel"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +95,8 @@ Singleton {
|
|||||||
function activate(kind: string): void {
|
function activate(kind: string): void {
|
||||||
if (kind === "focus")
|
if (kind === "focus")
|
||||||
FocusSession.activateWorkspace();
|
FocusSession.activateWorkspace();
|
||||||
|
else if (kind === "phone-transfer")
|
||||||
|
ShellState.open("quicksettings");
|
||||||
else if (["recording", "screen", "camera", "microphone"].indexOf(kind) >= 0)
|
else if (["recording", "screen", "camera", "microphone"].indexOf(kind) >= 0)
|
||||||
ShellState.open("activity");
|
ShellState.open("activity");
|
||||||
}
|
}
|
||||||
@@ -94,5 +108,7 @@ Singleton {
|
|||||||
Caffeine.toggle();
|
Caffeine.toggle();
|
||||||
else if (kind === "recording")
|
else if (kind === "recording")
|
||||||
Capture.stopRecording();
|
Capture.stopRecording();
|
||||||
|
else if (kind === "phone-transfer")
|
||||||
|
KdeConnect.cancelTransfer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,6 +221,50 @@ ShellRoot {
|
|||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "quicksettings"
|
target: "quicksettings"
|
||||||
function toggle(): void { ShellState.toggle("quicksettings"); }
|
function toggle(): void { ShellState.toggle("quicksettings"); }
|
||||||
|
function open(): void { ShellState.open("quicksettings"); }
|
||||||
|
function close(): void { ShellState.close(); }
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({ open: ShellState.quickSettingsOpen });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "kdeconnect"
|
||||||
|
function fixture(name: string): void { KdeConnect.applyFixture(name); }
|
||||||
|
function reset(): void { KdeConnect.clearFixture(); }
|
||||||
|
function refresh(): void { KdeConnect.refresh(); }
|
||||||
|
function cancel(): void { KdeConnect.cancelTransfer(); }
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
fixture: KdeConnect.fixtureMode,
|
||||||
|
available: KdeConnect.available,
|
||||||
|
reachable: KdeConnect.phoneReachable,
|
||||||
|
pairedCount: KdeConnect.pairedCount,
|
||||||
|
actionCount: KdeConnect.phoneActions.length,
|
||||||
|
transferActive: KdeConnect.transferActive,
|
||||||
|
transferFileName: KdeConnect.transferFileName,
|
||||||
|
ongoingCount: Ongoing.activities.filter(item => item.kind === "phone-transfer").length,
|
||||||
|
lastError: KdeConnect.lastError
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "home-assistant"
|
||||||
|
function fixture(name: string): void { HomeAssistant.applyFixture(name); }
|
||||||
|
function reset(): void { HomeAssistant.clearFixture(); }
|
||||||
|
function refresh(): void { HomeAssistant.refresh(); }
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
fixture: HomeAssistant.fixtureMode,
|
||||||
|
phase: HomeAssistant.phase,
|
||||||
|
configuredCount: HomeAssistant.configuredCount,
|
||||||
|
visibleCount: HomeAssistant.visibleEntities.length,
|
||||||
|
stale: HomeAssistant.stale,
|
||||||
|
busy: HomeAssistant.busyEntityId !== "",
|
||||||
|
lastError: HomeAssistant.lastError
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
|
|||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'Control Center services contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
qs ipc call kdeconnect reset >/dev/null 2>&1 || true
|
||||||
|
qs ipc call home-assistant reset >/dev/null 2>&1 || true
|
||||||
|
qs ipc call status-events reset >/dev/null 2>&1 || true
|
||||||
|
qs ipc call quicksettings close >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
qs ipc show | rg -q '^target kdeconnect$' \
|
||||||
|
|| fail 'KDE Connect IPC target is missing'
|
||||||
|
qs ipc show | rg -q '^target home-assistant$' \
|
||||||
|
|| fail 'Home Assistant IPC target is missing'
|
||||||
|
|
||||||
|
qs ipc call kdeconnect fixture reachable >/dev/null
|
||||||
|
jq -e '
|
||||||
|
.fixture == true and
|
||||||
|
.available == true and
|
||||||
|
.reachable == true and
|
||||||
|
.actionCount == 4 and
|
||||||
|
.transferActive == false and
|
||||||
|
.ongoingCount == 0
|
||||||
|
' <<<"$(qs ipc call kdeconnect status)" >/dev/null \
|
||||||
|
|| fail 'reachable phone fixture is malformed'
|
||||||
|
|
||||||
|
qs ipc call kdeconnect fixture offline >/dev/null
|
||||||
|
jq -e '
|
||||||
|
.fixture == true and
|
||||||
|
.available == true and
|
||||||
|
.reachable == false and
|
||||||
|
.pairedCount == 1 and
|
||||||
|
.ongoingCount == 0
|
||||||
|
' <<<"$(qs ipc call kdeconnect status)" >/dev/null \
|
||||||
|
|| fail 'offline phone fixture is malformed'
|
||||||
|
|
||||||
|
qs ipc call kdeconnect fixture transfer >/dev/null
|
||||||
|
jq -e '
|
||||||
|
.fixture == true and
|
||||||
|
.transferActive == true and
|
||||||
|
.transferFileName == "Fixture document.pdf" and
|
||||||
|
.ongoingCount == 1
|
||||||
|
' <<<"$(qs ipc call kdeconnect status)" >/dev/null \
|
||||||
|
|| fail 'phone transfer did not enter Ongoing'
|
||||||
|
|
||||||
|
qs ipc call kdeconnect cancel >/dev/null
|
||||||
|
jq -e '.transferActive == false and .ongoingCount == 0' \
|
||||||
|
<<<"$(qs ipc call kdeconnect status)" >/dev/null \
|
||||||
|
|| fail 'phone transfer did not leave Ongoing'
|
||||||
|
|
||||||
|
qs ipc call home-assistant fixture ready >/dev/null
|
||||||
|
jq -e '
|
||||||
|
.fixture == true and
|
||||||
|
.phase == "ready" and
|
||||||
|
.configuredCount == 7 and
|
||||||
|
.visibleCount == 4 and
|
||||||
|
.stale == false and
|
||||||
|
.lastError == ""
|
||||||
|
' <<<"$(qs ipc call home-assistant status)" >/dev/null \
|
||||||
|
|| fail 'ready Home fixture is malformed'
|
||||||
|
|
||||||
|
qs ipc call home-assistant fixture stale >/dev/null
|
||||||
|
jq -e '
|
||||||
|
.fixture == true and
|
||||||
|
.phase == "degraded" and
|
||||||
|
.configuredCount == 7 and
|
||||||
|
.visibleCount == 4 and
|
||||||
|
.stale == true and
|
||||||
|
.lastError == "unreachable"
|
||||||
|
' <<<"$(qs ipc call home-assistant status)" >/dev/null \
|
||||||
|
|| fail 'stale Home fixture did not retain entities'
|
||||||
|
|
||||||
|
qs ipc call home-assistant fixture unavailable >/dev/null
|
||||||
|
jq -e '
|
||||||
|
.fixture == true and
|
||||||
|
.phase == "unavailable" and
|
||||||
|
.configuredCount == 0 and
|
||||||
|
.visibleCount == 0 and
|
||||||
|
.lastError == "not-configured"
|
||||||
|
' <<<"$(qs ipc call home-assistant status)" >/dev/null \
|
||||||
|
|| fail 'unavailable Home fixture is malformed'
|
||||||
|
|
||||||
|
trap - EXIT
|
||||||
|
cleanup
|
||||||
|
printf 'Control Center services contract: PASS\n'
|
||||||
Reference in New Issue
Block a user