pragma Singleton // Local screen recognition. Capture owns geometry; this service owns the // Tesseract/ZBar process boundary, result state, explicit follow-up actions, // and cleanup of temporary screen images. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/screen-intelligence" // "idle" | "capturing" | "reading" | "ready" | "error" property string phase: "idle" // Do not map the result surface until grim has finished, or the overlay // can appear in the very pixels it is trying to analyze. readonly property bool visible: phase !== "idle" && phase !== "capturing" readonly property bool busy: phase === "capturing" || phase === "reading" property bool ocrReady: false property bool codeReady: false property bool englishReady: false property string text: "" property string codeType: "" property string codeValue: "" property string error: "" property string imagePath: "" property bool ownsImage: false readonly property string primaryUrl: root._firstWebUrl(root.codeValue) || root._firstWebUrl(root.text) function refresh(): void { probeProc.running = false; probeProc.running = true; } function analyzeRegion(geometry: string, outputName: string): void { root._prepare(); root.imagePath = Quickshell.cachePath("screen-intelligence-" + Date.now() + ".png"); root.ownsImage = true; root.phase = "capturing"; const command = ["grim"]; if (geometry !== "") command.push("-g", geometry); else if (outputName !== "") command.push("-o", outputName); command.push(root.imagePath); captureProc.command = command; captureProc.running = false; captureProc.running = true; } function analyzeFile(path: string): void { root._prepare(); root.imagePath = path; root.ownsImage = false; root._startRecognition(); } function copyText(): void { if (root.text !== "") Quickshell.execDetached(["wl-copy", root.text]); } function copyCode(): void { if (root.codeValue !== "") Quickshell.execDetached(["wl-copy", root.codeValue]); } function search(): void { if (root.text === "") return; Quickshell.execDetached(["xdg-open", "https://www.google.com/search?q=" + encodeURIComponent(root.text.slice(0, 4000))]); } function translate(): void { if (root.text === "") return; Quickshell.execDetached(["xdg-open", "https://translate.google.com/?sl=auto&tl=en&text=" + encodeURIComponent(root.text.slice(0, 4000))]); } function openDetected(): void { if (root.primaryUrl !== "") Quickshell.execDetached(["xdg-open", root.primaryUrl]); } function close(): void { captureProc.running = false; recognizeProc.running = false; root._dropOwnedImage(); root.phase = "idle"; root.text = ""; root.codeType = ""; root.codeValue = ""; root.error = ""; root.imagePath = ""; root.ownsImage = false; } function _prepare(): void { captureProc.running = false; recognizeProc.running = false; root._dropOwnedImage(); root.text = ""; root.codeType = ""; root.codeValue = ""; root.error = ""; root.imagePath = ""; root.ownsImage = false; } function _startRecognition(): void { root.phase = "reading"; recognizeProc.sawOutput = false; recognizeProc.command = [root.helperPath, "analyze-file", root.imagePath]; recognizeProc.running = false; recognizeProc.running = true; } function _consume(raw: string): void { try { const result = JSON.parse(raw); if (!result.ok) { root.error = result.error || "Panama could not read that capture."; root.phase = "error"; return; } root.text = (result.text || "").trim(); root.codeType = result.codeType || ""; root.codeValue = result.codeValue || ""; root.error = ""; root.phase = "ready"; } catch (e) { root.error = "The local recognition engine returned an unreadable result."; root.phase = "error"; } } function _dropOwnedImage(): void { if (root.ownsImage && root.imagePath !== "") Quickshell.execDetached(["rm", "-f", root.imagePath]); } function _firstWebUrl(value: string): string { if (!value) return ""; const match = value.match(/https?:\/\/[^\s<>'\"]+/i); if (!match) return ""; return match[0].replace(/[),.;!?]+$/, ""); } Process { id: probeProc command: [root.helperPath, "probe"] stdout: StdioCollector { onStreamFinished: { try { const result = JSON.parse(this.text); root.ocrReady = result.tesseract === true; root.codeReady = result.zbar === true; root.englishReady = result.english === true; } catch (e) { root.ocrReady = false; root.codeReady = false; root.englishReady = false; } } } } Process { id: captureProc onExited: (code, status) => { if (root.phase !== "capturing") return; if (code !== 0) { root.error = "Panama could not capture the selected area."; root.phase = "error"; return; } root._startRecognition(); } } Process { id: recognizeProc property bool sawOutput: false stdout: StdioCollector { onStreamFinished: { recognizeProc.sawOutput = this.text !== ""; root._consume(this.text); } } onExited: (code, status) => { if (!recognizeProc.sawOutput && root.phase === "reading") { root.error = "The local recognition engine did not return a result."; root.phase = "error"; } } } Component.onCompleted: root.refresh() }