Fix review findings: dual-entry image capture, image blob GC at limit, stdin drain, fuzzy query edge cases, uri encoding, GC queue, temp-file pruning, theme border widths
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@ echo "==> Enabling $PLUGIN_ID (replaces built-in omarchy.clipboard)"
|
||||
omarchy plugin enable "$PLUGIN_ID"
|
||||
|
||||
echo "==> Rebinding ALT+SHIFT+V"
|
||||
if [[ -f $BINDINGS ]] && grep -q "ALT SHIFT, V" "$BINDINGS"; then
|
||||
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"
|
||||
hyprctl reload >/dev/null
|
||||
|
||||
+21
-12
@@ -88,18 +88,27 @@ Item {
|
||||
var pruned = Store.prune(root.history, root.historyLimit)
|
||||
root.history = pruned.entries
|
||||
historyFile.setText(JSON.stringify(root.history, null, 1) + "\n")
|
||||
if (pruned.droppedImagePaths.length > 0) {
|
||||
var cmd = ["rm", "-f"].concat(pruned.droppedImagePaths)
|
||||
gcProc.command = cmd
|
||||
gcProc.running = true
|
||||
}
|
||||
if (pruned.droppedImagePaths.length > 0) queueGc(pruned.droppedImagePaths)
|
||||
}
|
||||
|
||||
// Serialize GC batches: a single reusable Process would silently drop
|
||||
// overlapping runs, so pending paths queue until the current rm exits.
|
||||
property var gcQueue: []
|
||||
function queueGc(paths) {
|
||||
root.gcQueue.push(paths)
|
||||
if (!gcProc.running) runNextGc()
|
||||
}
|
||||
function runNextGc() {
|
||||
if (root.gcQueue.length === 0) return
|
||||
gcProc.command = ["rm", "-f"].concat(root.gcQueue.shift())
|
||||
gcProc.running = true
|
||||
}
|
||||
|
||||
function addClipboardJson(line) {
|
||||
var entry = null
|
||||
try { entry = JSON.parse(String(line || "")) } catch (e) { return }
|
||||
if (!entry) return
|
||||
root.history = Store.addEntry(root.history, entry, root.historyLimit, Math.floor(Date.now() / 1000))
|
||||
root.history = Store.addEntry(root.history, entry, Math.floor(Date.now() / 1000))
|
||||
root.saveHistory()
|
||||
if (root.opened) root.rebuild()
|
||||
}
|
||||
@@ -242,10 +251,7 @@ Item {
|
||||
root.history = []
|
||||
root.typeCache = {}
|
||||
root.saveHistory()
|
||||
if (dropped.length > 0) {
|
||||
gcProc.command = ["rm", "-f"].concat(dropped)
|
||||
gcProc.running = true
|
||||
}
|
||||
if (dropped.length > 0) queueGc(dropped)
|
||||
root.selectedIndex = 0
|
||||
root.clearConfirmOpen = false
|
||||
root.rebuild()
|
||||
@@ -271,7 +277,10 @@ Item {
|
||||
onFileChanged: reload()
|
||||
}
|
||||
|
||||
Process { id: gcProc }
|
||||
Process {
|
||||
id: gcProc
|
||||
onExited: runNextGc()
|
||||
}
|
||||
|
||||
// Reap watchers left behind by a previous shell instance, then start our
|
||||
// own. pdeathsig kills them whenever the shell exits.
|
||||
@@ -674,7 +683,7 @@ Item {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.listWidth + Style.space(6)
|
||||
width: 1
|
||||
width: Style.normalBorderWidth
|
||||
color: root.lineColor
|
||||
}
|
||||
|
||||
|
||||
+4
-11
@@ -65,10 +65,11 @@ function parseQuery(q) {
|
||||
var canonical = TYPE_ALIASES[wanted]
|
||||
if (canonical) { out.type = canonical; continue }
|
||||
// prefix match: "type:im" → image
|
||||
var matchedType = ""
|
||||
for (var key in TYPE_ALIASES) {
|
||||
if (key.indexOf(wanted) === 0) { out.type = TYPE_ALIASES[key]; break }
|
||||
if (key.indexOf(wanted) === 0) { matchedType = TYPE_ALIASES[key]; break }
|
||||
}
|
||||
if (out.type) continue
|
||||
if (matchedType) { out.type = matchedType; continue }
|
||||
// unknown type — treat the whole token as a term
|
||||
out.terms.push(tok)
|
||||
continue
|
||||
@@ -91,7 +92,7 @@ function parseQuery(q) {
|
||||
|
||||
if (AGE_WORDS[lower] !== undefined) {
|
||||
out.maxAge = AGE_WORDS[lower]
|
||||
if (lower === "yesterday") out.minAge = 0 // refined below relative to now; caller passes now
|
||||
if (lower === "yesterday") out.minAge = 86400
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -219,14 +220,6 @@ function ageFilterOk(parsed, ageSeconds) {
|
||||
return true
|
||||
}
|
||||
|
||||
// "yesterday" means older than 24h but within 48h.
|
||||
function refineYesterday(parsed, now) {
|
||||
// Only applies when the query contained the bare word "yesterday" and no
|
||||
// explicit duration bounds; approximated by caller passing minAge via the
|
||||
// maxAge=172800 already set. We require age > 86400 - slack handled here.
|
||||
return parsed
|
||||
}
|
||||
|
||||
function recencyBonus(ts, now) {
|
||||
if (!ts) return 0
|
||||
var ageDays = Math.max(0, (now - ts) / 86400)
|
||||
|
||||
@@ -163,7 +163,7 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Style.space(10)
|
||||
height: 1
|
||||
height: Style.normalBorderWidth
|
||||
color: root.lineColor
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ Item {
|
||||
height: Style.space(120)
|
||||
radius: Style.cornerRadius
|
||||
color: root.hexRe.test(root.rawSafe()) ? root.rawSafe() : "transparent"
|
||||
border.width: 1
|
||||
border.width: Style.normalBorderWidth
|
||||
border.color: root.lineColor
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ Item {
|
||||
Rectangle {
|
||||
anchors.fill: img
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.width: Style.normalBorderWidth
|
||||
border.color: root.lineColor
|
||||
radius: Style.space(4)
|
||||
visible: img.status === Image.Ready
|
||||
|
||||
+6
-6
@@ -86,17 +86,17 @@ function parseHistory(raw, now) {
|
||||
}
|
||||
|
||||
// Add (or bump) an entry; newest first. Keeps pin state and usage count of
|
||||
// the existing copy when the same content is copied again.
|
||||
function addEntry(history, entry, limit, now) {
|
||||
// the existing copy when the same content is copied again. Does NOT truncate
|
||||
// to the limit: the caller prunes via prune() so evicted image blobs can be
|
||||
// garbage-collected (truncating here would leak them).
|
||||
function addEntry(history, entry, now) {
|
||||
var normalized = normalize(entry, now)
|
||||
var max = limit === undefined || limit === null ? DEFAULT_LIMIT : Number(limit)
|
||||
if (isNaN(max) || max < 1) max = DEFAULT_LIMIT
|
||||
if (!normalized) return Array.isArray(history) ? history.slice(0, max) : []
|
||||
if (!normalized) return Array.isArray(history) ? history.slice() : []
|
||||
|
||||
var key = entryKey(normalized)
|
||||
var next = [normalized]
|
||||
var values = Array.isArray(history) ? history : []
|
||||
for (var i = 0; i < values.length && next.length < max; i++) {
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var existing = normalize(values[i], now)
|
||||
if (!existing) continue
|
||||
if (entryKey(existing) === key) {
|
||||
|
||||
Binary file not shown.
+8
-1
@@ -99,6 +99,7 @@ def capture_image(types, app):
|
||||
pass
|
||||
return
|
||||
emit(entry)
|
||||
return True
|
||||
|
||||
|
||||
def decode_text(data):
|
||||
@@ -159,7 +160,13 @@ def capture_text(types, app):
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdin.close() # watcher payload unused; we probe ourselves
|
||||
# The watcher pipes the clipboard payload to us; wl-clipboard blocks on
|
||||
# writes if we close the pipe early, so drain it instead (we still probe
|
||||
# types ourselves below).
|
||||
try:
|
||||
sys.stdin.buffer.read()
|
||||
except Exception:
|
||||
pass
|
||||
types = list_types()
|
||||
if not types:
|
||||
return
|
||||
|
||||
@@ -28,6 +28,8 @@ open_text() {
|
||||
local dir file
|
||||
dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/clipboard-open"
|
||||
mkdir -p "$dir"
|
||||
# Prune temp copies older than a week; nothing else reclaims them.
|
||||
find "$dir" -type f -name 'clipboard.*.txt' -mtime +7 -delete 2>/dev/null || true
|
||||
file=$(mktemp --tmpdir="$dir" clipboard.XXXXXX.txt)
|
||||
printf '%s' "$text" >"$file"
|
||||
exec omarchy-launch-editor "$file"
|
||||
@@ -43,7 +45,7 @@ case $(jq -r '.type' <<<"$entry") in
|
||||
exec xdg-open "$path"
|
||||
;;
|
||||
files)
|
||||
first=$(jq -r '.paths[0]' <<<"$entry")
|
||||
first=$(jq -r '.paths[0] // empty' <<<"$entry")
|
||||
[[ -n $first ]] || exit 0
|
||||
exec xdg-open "$first"
|
||||
;;
|
||||
|
||||
@@ -23,7 +23,7 @@ case "$type" in
|
||||
;;
|
||||
files)
|
||||
while IFS= read -r p; do
|
||||
printf 'file://%s\n' "$p"
|
||||
printf 'file://%s\n' "$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$p")"
|
||||
done < <(jq -r '.paths[]' <<<"$entry") | wl-copy --type text/uri-list
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -156,7 +156,15 @@ test("highlightFirstLine multiline only first line", () => {
|
||||
assert.ok(!html.includes("\n"))
|
||||
})
|
||||
|
||||
test("parseQuery yesterday bounds", () => {
|
||||
test("parseQuery yesterday excludes today", () => {
|
||||
const q = Fuzzy.parseQuery("yesterday")
|
||||
assert.equal(q.maxAge, 172800)
|
||||
assert.equal(q.minAge, 86400)
|
||||
})
|
||||
|
||||
test("regression: unknown type: token becomes a term even after a known one", () => {
|
||||
const q = Fuzzy.parseQuery("type:image type:foo")
|
||||
assert.equal(q.type, "image")
|
||||
assert.equal(q.terms.length, 1)
|
||||
assert.equal(q.terms[0], "type:foo")
|
||||
})
|
||||
|
||||
+17
-4
@@ -46,11 +46,24 @@ test("addEntry dedupes and bumps to front, keeps pin/uses", () => {
|
||||
assert.equal(h[0].uses, 3)
|
||||
})
|
||||
|
||||
test("addEntry respects limit", () => {
|
||||
test("addEntry keeps all entries; prune truncates", () => {
|
||||
let h = []
|
||||
for (let i = 0; i < 20; i++) h = Store.addEntry(h, { type: "text", text: "t" + i, ts: i }, 5)
|
||||
assert.equal(h.length, 5)
|
||||
assert.equal(h[0].text, "t19")
|
||||
for (let i = 0; i < 20; i++) h = Store.addEntry(h, { type: "text", text: "t" + i, ts: i })
|
||||
assert.equal(h.length, 20)
|
||||
const r = Store.prune(h, 5)
|
||||
assert.equal(r.entries.length, 5)
|
||||
assert.equal(r.entries[0].text, "t19")
|
||||
})
|
||||
|
||||
test("regression: addEntry at limit must not leak evicted images (prune reports them)", () => {
|
||||
// Images beyond the limit are only reclaimable if prune() sees them,
|
||||
// which is why addEntry must not truncate by itself.
|
||||
let h = []
|
||||
for (let i = 0; i < 10; i++)
|
||||
h = Store.addEntry(h, { type: i < 2 ? "image" : "text", path: "/tmp/x" + i + ".png", text: "t" + i, ts: i })
|
||||
const r = Store.prune(h, 5)
|
||||
assert.ok(r.droppedImagePaths.includes("/tmp/x0.png"))
|
||||
assert.ok(r.droppedImagePaths.includes("/tmp/x1.png"))
|
||||
})
|
||||
|
||||
test("removeById / findById / togglePin / touch", () => {
|
||||
|
||||
Reference in New Issue
Block a user