pragma Singleton // Printers, through CUPS' own API. // // Driverless only: this adds printers that describe their own capabilities over // IPP. Choosing PPDs and downloading vendor drivers is most of what the panel // this replaces does, and getting it wrong yields a printer that accepts jobs // and silently prints nothing -- so the page says a printer needs a driver // rather than guessing one. // // Discovery is separate from the snapshot because it takes seconds of network // waiting, and a settings page should open immediately. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-printers" property var printers: [] property var jobs: [] property var service: ({}) property var discovered: [] property bool scanned: false property bool searching: false property bool searched: false property string lastError: "" // Guards read the Process objects directly. A derived binding is stale // inside the handler that changes it, which silently drops the refresh // after a successful write. See DefaultApps.qml. readonly property bool busy: query.running || mutation.running readonly property var defaultPrinter: { for (const printer of root.printers) { if (printer.isDefault) return printer; } return null; } readonly property bool anyPrinters: root.printers.length > 0 // Printers found on the network that are not already set up here. readonly property var addable: root.discovered.filter(entry => { for (const printer of root.printers) { if (String(printer.uri ?? "") === String(entry.uri ?? "")) return false; } return true; }) function stateSummary(printer: var): string { const message = String(printer?.stateMessage ?? "").trim(); const state = String(printer?.state ?? ""); if (state === "stopped") return message !== "" ? "Paused — " + message : "Paused"; if (state === "printing") return message !== "" ? "Printing — " + message : "Printing"; if (printer?.accepting === false) return "Not accepting jobs"; return message !== "" ? message : "Ready"; } function jobsFor(printerName: string): int { let count = 0; for (const job of root.jobs) { if (job.printer === printerName) count += 1; } return count; } function refresh(): void { if (query.running) return; query.command = [root.helperPath, "snapshot"]; query.running = true; } function search(): void { if (root.searching) return; root.searching = true; discovery.command = [root.helperPath, "discover"]; discovery.running = true; } function absorb(text: string): void { try { const parsed = JSON.parse(text); root.printers = Array.isArray(parsed.printers) ? parsed.printers : []; root.jobs = Array.isArray(parsed.jobs) ? parsed.jobs : []; root.service = parsed.service ?? ({}); root.lastError = String(parsed.error ?? ""); } catch (error) { root.lastError = "Could not read the printing service's answer."; console.warn("Printers: 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 add(uri: string, name: string): void { root.run(["add", uri, name]); } function remove(name: string): void { root.run(["remove", name]); } function setDefault(name: string): void { root.run(["set-default", name]); } function pause(name: string): void { root.run(["pause", name]); } function resume(name: string): void { root.run(["resume", name]); } function cancel(jobId: int): void { root.run(["cancel", String(jobId)]); } function testPage(name: string): void { root.run(["test-page", name]); } // A queue name CUPS will accept, derived from what the printer calls itself. function suggestedName(label: string): string { const cleaned = String(label).replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, ""); return cleaned === "" ? "printer" : cleaned.slice(0, 60); } 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 // Answers with the fresh state, so the page updates from the change // itself rather than asking again afterwards. stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } Process { id: discovery stdout: StdioCollector { onStreamFinished: { try { const parsed = JSON.parse(this.text); root.discovered = Array.isArray(parsed.found) ? parsed.found : []; if (String(parsed.error ?? "") !== "") root.lastError = String(parsed.error); } catch (error) { root.lastError = "Could not read the discovery result."; } root.searched = true; } } onExited: root.searching = false } }