install.sh: rebind ALT+SHIFT+V in bindings.lua (the live Quattro config), not just legacy bindings.conf
This commit is contained in:
+17
-1
@@ -30,6 +30,8 @@ Item {
|
||||
property int displayLimit: 200
|
||||
property int maxAgeDays: 0 // 0 = keep forever
|
||||
property bool qrDecode: true
|
||||
property bool ocr: true
|
||||
property string ocrLang: "eng"
|
||||
property bool paused: false
|
||||
property var typeCache: ({}) // id → derived type, memoized
|
||||
|
||||
@@ -330,10 +332,20 @@ Item {
|
||||
|
||||
function applySettings(raw) {
|
||||
var s = Store.parseSettings(raw, "tank.clipboard")
|
||||
var needsWatchRestart = s.qrDecode !== root.qrDecode || s.ocr !== root.ocr
|
||||
|| s.ocrLang !== root.ocrLang
|
||||
root.historyLimit = s.historyLimit
|
||||
root.maxAgeDays = s.maxAgeDays
|
||||
root.displayLimit = s.maxRows
|
||||
root.qrDecode = s.qrDecode
|
||||
root.ocr = s.ocr
|
||||
root.ocrLang = s.ocrLang
|
||||
// The watchers read qr/ocr settings from the environment — restart them
|
||||
// so changes take effect without a shell reload.
|
||||
if (needsWatchRestart && watchProc.running) {
|
||||
watchProc.running = false
|
||||
watchRestartTimer.restart()
|
||||
}
|
||||
root.applyRetentionPolicy()
|
||||
if (root.opened) root.rebuild()
|
||||
}
|
||||
@@ -389,7 +401,11 @@ Item {
|
||||
Process {
|
||||
id: watchProc
|
||||
command: ["setpriv", "--pdeathsig", "TERM", "wl-paste", "--watch", "python3", root.pluginDir + "/capture.py", "watch"]
|
||||
environment: ({ "CLIPBOARD_QR": root.qrDecode ? "1" : "0" })
|
||||
environment: ({
|
||||
"CLIPBOARD_QR": root.qrDecode ? "1" : "0",
|
||||
"CLIPBOARD_OCR": root.ocr ? "1" : "0",
|
||||
"CLIPBOARD_OCR_LANG": root.ocrLang
|
||||
})
|
||||
onExited: watchRestartTimer.restart()
|
||||
stdout: SplitParser {
|
||||
onRead: function(data) { root.addClipboardJson(data) }
|
||||
|
||||
@@ -71,6 +71,10 @@ Item {
|
||||
var chips = []
|
||||
if (r.app) chips.push(Classify.prettyApp(r.app))
|
||||
if (e.qr) chips.push("QR: " + Classify.firstLine(e.qr, 40))
|
||||
if (e.ocr) {
|
||||
var ow = Classify.textStats(e.ocr).words
|
||||
chips.push("OCR: " + Classify.plural(ow, "word"))
|
||||
}
|
||||
if (e.type === "text" && e.text) {
|
||||
var st = Classify.textStats(e.text)
|
||||
chips.push(Classify.plural(st.words, "word"))
|
||||
@@ -449,10 +453,62 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// OCR text recovered from the image (searchable like any text clip).
|
||||
Rectangle {
|
||||
id: ocrPanel
|
||||
visible: root.entry && root.entry.ocr
|
||||
width: parent.width
|
||||
height: Math.min(Style.space(150), ocrContent.height + Style.space(12))
|
||||
radius: Style.cornerRadius
|
||||
color: root.chipBg
|
||||
|
||||
Column {
|
||||
id: ocrContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Style.space(6)
|
||||
anchors.leftMargin: Style.space(6)
|
||||
anchors.rightMargin: Style.space(6)
|
||||
spacing: Style.space(4)
|
||||
|
||||
Text {
|
||||
text: " Recognized text (OCR)"
|
||||
color: Color.accent
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: ocrText.implicitHeight
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: ocrText.implicitHeight
|
||||
|
||||
TextEdit {
|
||||
id: ocrText
|
||||
width: parent.width
|
||||
readOnly: true
|
||||
activeFocusOnPress: false
|
||||
text: root.entry && root.entry.ocr ? root.entry.ocr : ""
|
||||
textFormat: TextEdit.PlainText
|
||||
color: root.fg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
wrapMode: TextEdit.Wrap
|
||||
selectionColor: Util.alpha(Color.accent, 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(0, parent.height
|
||||
- (root.entry && root.entry.qr ? qrContent.height + Style.space(12) + Style.space(8) : 0)
|
||||
- (root.entry && root.entry.ocr ? ocrPanel.height + Style.space(8) : 0)
|
||||
- infoLabel.height - Style.space(12))
|
||||
|
||||
Image {
|
||||
|
||||
@@ -51,6 +51,7 @@ function normalize(value, now) {
|
||||
if (value.w) out.w = Number(value.w)
|
||||
if (value.h) out.h = Number(value.h)
|
||||
if (value.qr) out.qr = String(value.qr)
|
||||
if (value.ocr) out.ocr = String(value.ocr)
|
||||
} else if (type === "files") {
|
||||
var paths = Array.isArray(value.paths) ? value.paths.filter(function(p) { return !!p }) : []
|
||||
if (paths.length === 0) return null
|
||||
@@ -104,8 +105,9 @@ function addEntry(history, entry, now) {
|
||||
// Re-copy of existing content: keep its pin state and usage count.
|
||||
if (existing.pinned) normalized.pinned = true
|
||||
if (existing.uses > 0) normalized.uses = existing.uses
|
||||
// Keep expensive derived data (e.g. decoded QR) across re-captures.
|
||||
// Keep expensive derived data (decoded QR, OCR) across re-captures.
|
||||
if (existing.qr && !normalized.qr) normalized.qr = existing.qr
|
||||
if (existing.ocr && !normalized.ocr) normalized.ocr = existing.ocr
|
||||
continue
|
||||
}
|
||||
next.push(existing)
|
||||
@@ -200,7 +202,14 @@ function pruneByAge(history, maxAgeSeconds, now) {
|
||||
// optional; unknown keys are ignored. Reads only the plugins[] entry whose
|
||||
// id matches.
|
||||
function parseSettings(raw, pluginId) {
|
||||
var out = { historyLimit: DEFAULT_LIMIT, maxAgeDays: 0, maxRows: 200, qrDecode: true }
|
||||
var out = {
|
||||
historyLimit: DEFAULT_LIMIT,
|
||||
maxAgeDays: 0,
|
||||
maxRows: 200,
|
||||
qrDecode: true,
|
||||
ocr: true,
|
||||
ocrLang: "eng"
|
||||
}
|
||||
var config = null
|
||||
try { config = JSON.parse(String(raw || "{}")) } catch (e) { return out }
|
||||
if (!config || !Array.isArray(config.plugins)) return out
|
||||
@@ -218,6 +227,8 @@ function parseSettings(raw, pluginId) {
|
||||
if (isFinite(n) && n >= 1) out.maxRows = Math.floor(n)
|
||||
|
||||
if (typeof entry.qrDecode === "boolean") out.qrDecode = entry.qrDecode
|
||||
if (typeof entry.ocr === "boolean") out.ocr = entry.ocr
|
||||
if (typeof entry.ocrLang === "string" && entry.ocrLang) out.ocrLang = entry.ocrLang
|
||||
|
||||
break
|
||||
}
|
||||
@@ -230,6 +241,8 @@ function buildRow(entry, derivedType, now) {
|
||||
if (entry.type === "image") {
|
||||
content = fileLabel(entry.path) + " " + String(entry.mime || "")
|
||||
if (entry.qr) content += " " + String(entry.qr).slice(0, 500)
|
||||
// OCR text makes screenshots searchable like any text clip.
|
||||
if (entry.ocr) content += " " + String(entry.ocr).slice(0, 4000)
|
||||
} else if (entry.type === "files") {
|
||||
content = (entry.paths || []).join(" ")
|
||||
} else {
|
||||
|
||||
+29
@@ -16,6 +16,7 @@ binary payloads are skipped silently.
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -119,6 +120,10 @@ def capture_image(types, app):
|
||||
if qr:
|
||||
entry["qr"] = qr
|
||||
|
||||
ocr = decode_ocr(path, os.environ.get("CLIPBOARD_OCR_LANG", "eng"))
|
||||
if ocr:
|
||||
entry["ocr"] = ocr
|
||||
|
||||
emit(entry)
|
||||
return True
|
||||
|
||||
@@ -143,6 +148,30 @@ def decode_text(data):
|
||||
return text
|
||||
|
||||
|
||||
def decode_ocr(path, lang):
|
||||
"""Recognize text in an image with tesseract; None when unavailable/binary."""
|
||||
if os.environ.get("CLIPBOARD_OCR", "1") == "0":
|
||||
return None
|
||||
# Tesseract is optional — degrade to no-OCR when missing.
|
||||
if shutil.which("tesseract") is None:
|
||||
return None
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["tesseract", path, "stdout", "-l", lang, "--quiet"],
|
||||
capture_output=True, timeout=30
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return None
|
||||
text = r.stdout.decode("utf-8", "replace")
|
||||
# Collapse tesseract's whitespace so the stored haystack stays dense.
|
||||
text = "\n".join(line.strip() for line in text.splitlines()
|
||||
if line.strip())
|
||||
text = text.strip()
|
||||
return text[:4000] or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def capture_uri_list(types, app):
|
||||
if "text/uri-list" not in types:
|
||||
return False
|
||||
|
||||
+22
-6
@@ -39,14 +39,30 @@ echo "==> Enabling $PLUGIN_ID (replaces built-in omarchy.clipboard)"
|
||||
omarchy plugin enable "$PLUGIN_ID"
|
||||
|
||||
echo "==> Rebinding ALT+SHIFT+V"
|
||||
if [[ -f $BINDINGS ]] && grep -q "^bindd = ALT SHIFT, V, " "$BINDINGS"; then
|
||||
cp "$BINDINGS" "$BINDINGS.bak.$(date +%s)"
|
||||
sed -i 's|^bindd = ALT SHIFT, V, .*|bindd = ALT SHIFT, V, Clipboard manager (clipboard-history), exec, omarchy-shell shell toggle tank.clipboard|' "$BINDINGS"
|
||||
# Omarchy's live config may be bindings.lua (Quattro) or legacy bindings.conf —
|
||||
# edit whichever exists and let the other alone.
|
||||
rebound=0
|
||||
if [[ -f $HOME/.config/hypr/bindings.lua ]]; then
|
||||
LUA="$HOME/.config/hypr/bindings.lua"
|
||||
if grep -q 'ALT + SHIFT + V' "$LUA"; then
|
||||
cp "$LUA" "$LUA.bak.$(date +%s)"
|
||||
sed -i 's|^o.bind("ALT + SHIFT + V", "Clipboard manager (vicinae)", "vicinae deeplink vicinae://launch/clipboard/history")|o.bind("ALT + SHIFT + V", "Clipboard manager (clipboard-history)", "omarchy-shell shell toggle tank.clipboard")|' "$LUA"
|
||||
echo " rebound in bindings.lua (vicinae binding replaced)"
|
||||
rebound=1
|
||||
fi
|
||||
fi
|
||||
if [[ $rebound != 1 && -f $HOME/.config/hypr/bindings.conf ]] && grep -q "^bindd = ALT SHIFT, V, " "$HOME/.config/hypr/bindings.conf"; then
|
||||
CONF="$HOME/.config/hypr/bindings.conf"
|
||||
cp "$CONF" "$CONF.bak.$(date +%s)"
|
||||
sed -i 's|^bindd = ALT SHIFT, V, .*|bindd = ALT SHIFT, V, Clipboard manager (clipboard-history), exec, omarchy-shell shell toggle tank.clipboard|' "$CONF"
|
||||
echo " rebound in bindings.conf"
|
||||
rebound=1
|
||||
fi
|
||||
if (( rebound )); then
|
||||
hyprctl reload >/dev/null
|
||||
echo " rebound; previous bindings backed up next to $BINDINGS"
|
||||
else
|
||||
echo " no ALT SHIFT V binding found in $BINDINGS — add manually:"
|
||||
echo ' bindd = ALT SHIFT, V, Clipboard manager (clipboard-history), exec, omarchy-shell shell toggle tank.clipboard'
|
||||
echo " no ALT+SHIFT+V binding found — add manually to ~/.config/hypr/bindings.lua:"
|
||||
echo ' o.bind("ALT + SHIFT + V", "Clipboard manager (clipboard-history)", "omarchy-shell shell toggle tank.clipboard")'
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
@@ -170,3 +170,20 @@ test("parseSettings ignores garbage", () => {
|
||||
const s = Store.parseSettings("not json", "tank.clipboard")
|
||||
assert.equal(s.historyLimit, 1500)
|
||||
})
|
||||
|
||||
test("ocr passes through normalize, dedup, and search", () => {
|
||||
const e = Store.normalize({ type: "image", path: "/x/s.png", mime: "image/png", ocr: "deploy staging" })
|
||||
assert.equal(e.ocr, "deploy staging")
|
||||
let h = Store.addEntry([], { type: "image", path: "/x/s.png", ocr: "deploy staging", ts: 1 })
|
||||
h = Store.addEntry(h, { type: "image", path: "/x/s.png", ts: 2 })
|
||||
assert.equal(h[0].ocr, "deploy staging")
|
||||
const row = Store.buildRow(h[0], "image", 2)
|
||||
assert.ok(row.content.includes("deploy staging"))
|
||||
})
|
||||
|
||||
test("parseSettings ocr keys", () => {
|
||||
const raw = JSON.stringify({ plugins: [{ id: "tank.clipboard", ocr: false, ocrLang: "deu" }] })
|
||||
const s = Store.parseSettings(raw, "tank.clipboard")
|
||||
assert.equal(s.ocr, false)
|
||||
assert.equal(s.ocrLang, "deu")
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user