210 lines
6.4 KiB
QML
210 lines
6.4 KiB
QML
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()
|
|
}
|