pragma Singleton // Redacted Home Assistant configuration state. The helper is the only object // that touches the private env file; QML never receives the stored token. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-home-assistant-config" property string url: "" property var entities: [] property bool tokenConfigured: false property bool configured: false property string lastError: "" property string pendingPayload: "" readonly property bool busy: statusProc.running || writeProc.running signal configurationSaved function errorMessage(code: string): string { switch (code) { case "invalid-url": return "Enter an HTTP or HTTPS Home Assistant URL."; case "invalid-token": return "The access token contains unsupported characters."; case "invalid-entities": return "Entity IDs must look like light.living_room."; case "read-failed": return "The private configuration file could not be read."; case "write-failed": return "The private configuration file could not be saved."; case "invalid-payload": return "The configuration could not be validated."; default: return code ? "Home Assistant configuration is unavailable." : ""; } } function applyResult(text: string, saved: bool): void { let result = null; try { result = JSON.parse(text); } catch (error) { result = { ok: false, error: "invalid-response" }; } if (result.ok !== true) { root.lastError = root.errorMessage(String(result.error || "invalid-response")); return; } root.url = String(result.url || ""); root.entities = Array.isArray(result.entities) ? result.entities : []; root.tokenConfigured = result.tokenConfigured === true; root.configured = result.configured === true; root.lastError = ""; if (saved) { root.configurationSaved(); HomeAssistant.refresh(); } } function refresh(): void { if (!statusProc.running && !writeProc.running) statusProc.running = true; } // An empty token means "keep the stored token". Clearing is an explicit // separate action so editing the URL can never erase a secret by accident. function save(url: string, entitiesText: string, token: string): bool { if (root.busy) return false; const payload = { url: url, entities: entitiesText }; if (token.trim() !== "") payload.token = token; return root.startWrite(payload); } function clearToken(): bool { if (root.busy || !root.tokenConfigured) return false; return root.startWrite({ token: "" }); } function startWrite(payload: var): bool { root.lastError = ""; root.pendingPayload = JSON.stringify(payload); writeProc.running = true; return true; } Process { id: statusProc command: [root.helperPath, "status"] stdout: StdioCollector { onStreamFinished: root.applyResult(this.text, false) } onExited: (code, status) => { if (code !== 0 && root.lastError === "") root.lastError = "Home Assistant configuration could not be loaded."; } } Process { id: writeProc command: [root.helperPath, "write"] stdinEnabled: true stdout: StdioCollector { id: writeOutput onStreamFinished: root.applyResult(this.text, true) } onStarted: { writeProc.write(root.pendingPayload + "\n"); root.pendingPayload = ""; } onExited: (code, status) => { root.pendingPayload = ""; if (code !== 0 && root.lastError === "") root.lastError = "Home Assistant configuration could not be saved."; } } Component.onCompleted: root.refresh() }