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:
2026-09-05 15:43:33 +01:00
parent 44808ffd2c
commit 63a003e4a5
11 changed files with 73 additions and 41 deletions
+1 -1
View File
@@ -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
+20 -11
View File
@@ -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
View File
@@ -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)
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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"
;;
+1 -1
View File
@@ -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
;;
*)
+9 -1
View File
@@ -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
View File
@@ -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", () => {