From 6da0c4284725647ac51fea6ee0fde569546724de Mon Sep 17 00:00:00 2001 From: Alan Silva Date: Sat, 5 Sep 2026 22:56:26 +0100 Subject: [PATCH] Open links from QR payloads: Ctrl+O opens the decoded URL (browser) instead of the image; clickable Open-link chips in link and QR previews --- Classify.js | 10 +++++ Clipboard.qml | 1 + PreviewPane.qml | 87 +++++++++++++++++++++++++++++++++++++++++ README.md | 6 ++- open-entry.sh | 11 ++++++ tests/test-classify.mjs | 10 +++++ 6 files changed, 123 insertions(+), 2 deletions(-) diff --git a/Classify.js b/Classify.js index 9863223..0ce6e39 100644 --- a/Classify.js +++ b/Classify.js @@ -213,6 +213,16 @@ function urlDomain(url) { return m ? m[1] : "" } +// First URL inside arbitrary text ("visit https://x.io/a for more", QR +// payloads, …). Returns "" when none. Bare domains are NOT extracted here — +// too noisy for prose; the shell-side open script has its own fallback. +function extractUrl(text) { + var m = /https?:\/\/[^\s\]"'<>]+/i.exec(String(text || "")) + if (m) return m[0].replace(/[.,;:)!]+$/, "") + m = /(?:^|\s)(www\.[^\s\]"<>]+)/i.exec(String(text || "")) + return m ? m[1].replace(/[.,;:)!]+$/, "") : "" +} + // ---------------------------------------------------------------- apps var APP_NAMES = { diff --git a/Clipboard.qml b/Clipboard.qml index fc36041..e39a982 100644 --- a/Clipboard.qml +++ b/Clipboard.qml @@ -794,6 +794,7 @@ Item { anchors.left: parent.left anchors.leftMargin: root.listWidth + Style.space(14) result: root.currentResult + onOpen: root.openResult(root.currentResult) visible: root.currentResult !== null } diff --git a/PreviewPane.qml b/PreviewPane.qml index 773db04..861ce72 100644 --- a/PreviewPane.qml +++ b/PreviewPane.qml @@ -12,6 +12,8 @@ Item { property var result: null property var entry: result ? result.row.entry : null property string derived: result ? result.row.type : "" + // Wired by the picker: opens the current result (browser for links). + property var onOpen: function() {} readonly property string font_: Style.font.menuFamily readonly property color fg: Color.menu.text @@ -51,6 +53,17 @@ Item { return hsl ? "hsl(" + hsl[0] + ", " + hsl[1] + "%, " + hsl[2] + "%)" : "" } + // URL inside the decoded QR payload ("" when the payload is not a link). + function qrUrl() { + if (!entry || !entry.qr) return "" + var url = Classify.extractUrl(entry.qr) + if (url) return url + var trimmed = String(entry.qr).trim() + return root.bareDomainRe.test(trimmed) ? trimmed : "" + } + + readonly property var bareDomainRe: /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}(?:\/[^\s]*)?$/i + function metaChips() { if (!result) return [] var r = result.row @@ -292,6 +305,42 @@ Item { font.family: root.font_ font.pixelSize: Style.font.caption } + + Rectangle { + radius: height / 2 + color: Util.alpha(Color.accent, 0.15) + width: linkOpenLabel.implicitWidth + Style.space(16) + height: Style.space(22) + + Row { + anchors.centerIn: parent + spacing: Style.space(4) + + Text { + anchors.verticalCenter: parent.verticalCenter + text: "󰌹" + color: Color.accent + font.family: root.font_ + font.pixelSize: Style.font.caption + } + + Text { + id: linkOpenLabel + anchors.verticalCenter: parent.verticalCenter + text: "Open link" + color: Color.accent + font.family: root.font_ + font.pixelSize: Style.font.caption + font.bold: true + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.onOpen() + } + } } // image body @@ -359,6 +408,44 @@ Item { wrapMode: TextEdit.WrapAnywhere selectionColor: Util.alpha(Color.accent, 0.4) } + + // QR payloads frequently encode links — offer the open affordance. + Rectangle { + visible: root.qrUrl().length > 0 + radius: height / 2 + color: Util.alpha(Color.accent, 0.15) + width: qrOpenLabel.implicitWidth + Style.space(16) + height: Style.space(20) + + Row { + anchors.centerIn: parent + spacing: Style.space(4) + + Text { + anchors.verticalCenter: parent.verticalCenter + text: "󰌹" + color: Color.accent + font.family: root.font_ + font.pixelSize: Style.font.caption + } + + Text { + id: qrOpenLabel + anchors.verticalCenter: parent.verticalCenter + text: "Open link" + color: Color.accent + font.family: root.font_ + font.pixelSize: Style.font.caption + font.bold: true + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.onOpen() + } + } } } diff --git a/README.md b/README.md index fefd8ac..2b74505 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ full theme integration. domain, JSON is pretty-printed, file lists show paths — every preview with metadata chips (app, date, words/lines, size, paste counts) - **QR codes** — copied QR images are decoded with `zbarimg`; the payload is - shown in the list, preview, and is searchable like any text clip + shown in the list, preview, and is searchable like any text clip — and if it + encodes a link, `Ctrl+O` (or the preview's "Open link" chip) opens it in the + browser directly - **Rich capture** — a `wl-paste --watch` daemon records every clip with mime type, byte size, source app (via `hyprctl`), timestamp, and image dimensions. Text, images, and `file://` URI lists (file-manager copies) are supported; @@ -67,7 +69,7 @@ Updating: `omarchy plugin update tank.clipboard` · Uninstall: `omarchy plugin r | `Ctrl+N` / `Ctrl+P` (or arrows) | navigate results | | `Enter` | copy to clipboard and paste into the focused window | | `Shift+Enter` | copy only | -| `Ctrl+O` | open (link → browser, image → editor, file → xdg-open, text → editor) | +| `Ctrl+O` | open (link → browser, image → editor, file → xdg-open, text → editor). QR images open their **decoded link** in the browser; the preview pane also has clickable "Open link" chips for links and QR payloads | | `Tab` | pin/unpin | | `Ctrl+=` | pause/resume recording | | `Delete` | remove entry · `Shift+Delete` clear all (with confirm) | diff --git a/open-entry.sh b/open-entry.sh index 0d49cf2..bf75039 100755 --- a/open-entry.sh +++ b/open-entry.sh @@ -38,6 +38,17 @@ open_text() { case $(jq -r '.type' <<<"$entry") in image) path=$(jq -r '.path' <<<"$entry") + # A decoded QR payload often IS a link — opening it beats opening the image. + qr=$(jq -r '.qr // empty' <<<"$entry") + if [[ -n $qr ]]; then + url=$(printf '%s' "$qr" | grep -Eom1 'https?://[^[:space:]"<>]+' || true) + if [[ -z $url ]] && [[ $qr =~ ^[[:alnum:]][[:alnum:].-]*\.[[:alpha:]]{2,}(/[^[:space:]]*)?[[:space:]]*$ ]]; then + url="https://${BASH_REMATCH[0]}" + fi + if [[ -n $url ]]; then + exec omarchy-launch-browser "$url" + fi + fi [[ -r $path ]] || exit 0 if command -v tensaku-edit >/dev/null 2>&1; then exec tensaku-edit "$path" diff --git a/tests/test-classify.mjs b/tests/test-classify.mjs index 1d7b582..c162963 100644 --- a/tests/test-classify.mjs +++ b/tests/test-classify.mjs @@ -114,3 +114,13 @@ test("colorToRgb rejects junk", () => { assert.equal(Classify.colorToRgb("#gg"), null) assert.equal(Classify.colorToRgb("hello"), null) }) + +test("extractUrl finds links in text and QR payloads", () => { + assert.equal(Classify.extractUrl("see https://omarchy.org/docs?a=1 for details."), "https://omarchy.org/docs?a=1") + assert.equal(Classify.extractUrl("go to www.example.com now"), "www.example.com") + assert.equal(Classify.extractUrl("https://x.io"), "https://x.io") + assert.equal(Classify.extractUrl("no link here"), "") + assert.equal(Classify.extractUrl(""), "") + // trailing punctuation is not part of the URL + assert.equal(Classify.extractUrl("(https://a.io/x)."), "https://a.io/x") +})