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() } }