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

This commit is contained in:
2026-09-05 22:56:26 +01:00
parent 66ba5bd3db
commit 6da0c42847
6 changed files with 123 additions and 2 deletions
+10
View File
@@ -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 = {
+1
View File
@@ -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
}
+87
View File
@@ -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()
}
}
}
}
+4 -2
View File
@@ -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) |
+11
View File
@@ -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"
+10
View File
@@ -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")
})