pragma Singleton // Which applications may use the camera and microphone. // // Backed by xdg-desktop-portal's permission store, which records the answer an // application got when it asked through the portal. That is the whole of what // this controls, and the limit belongs on the page rather than in a comment: a // native binary opens /dev/video0 directly and no desktop setting stands in its // way. What this covers is Flatpaks and anything else going through the portal. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-permissions" property bool available: false property var devices: [] property bool scanned: false property string lastError: "" // Guards read the Processes directly rather than a derived binding, which // returns its cached value inside the handler that changes its dependency. readonly property bool busy: query.running || mutation.running // Devices something has actually asked for. A device nothing has asked for // is still reported, so the page can say so rather than omit it. readonly property var recorded: root.devices.filter( device => (device.applications ?? []).length > 0) readonly property int grantedCount: root.devices.reduce( (total, device) => total + (device.applications ?? []).filter(app => app.allowed).length, 0) function refresh(): void { if (query.running) return; query.command = [root.helperPath, "snapshot"]; query.running = true; } function absorb(text: string): void { try { const parsed = JSON.parse(text); root.available = parsed.available === true; root.devices = Array.isArray(parsed.devices) ? parsed.devices : []; root.lastError = String(parsed.error ?? ""); } catch (error) { root.lastError = "Could not read the portal's permissions."; console.warn("Permissions: could not parse helper output:", error); } root.scanned = true; } function run(arguments: var): void { if (mutation.running) return; root.lastError = ""; mutation.command = [root.helperPath].concat(arguments); mutation.running = true; } function setAllowed(device: string, app: string, allowed: bool): void { root.run(["set", device, app, allowed ? "allow" : "deny"]); } // Drops the recorded answer entirely, so the application is asked again the // next time it wants the device. function forget(device: string, app: string): void { root.run(["forget", device, app]); } Component.onCompleted: root.refresh() Process { id: query stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } Process { id: mutation stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } }