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" omarchy plugin enable "$PLUGIN_ID"
echo "==> Rebinding ALT+SHIFT+V" 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)" 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" 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 hyprctl reload >/dev/null
+21 -12
View File
@@ -88,18 +88,27 @@ Item {
var pruned = Store.prune(root.history, root.historyLimit) var pruned = Store.prune(root.history, root.historyLimit)
root.history = pruned.entries root.history = pruned.entries
historyFile.setText(JSON.stringify(root.history, null, 1) + "\n") historyFile.setText(JSON.stringify(root.history, null, 1) + "\n")
if (pruned.droppedImagePaths.length > 0) { if (pruned.droppedImagePaths.length > 0) queueGc(pruned.droppedImagePaths)
var cmd = ["rm", "-f"].concat(pruned.droppedImagePaths) }
gcProc.command = cmd
gcProc.running = true // 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) { function addClipboardJson(line) {
var entry = null var entry = null
try { entry = JSON.parse(String(line || "")) } catch (e) { return } try { entry = JSON.parse(String(line || "")) } catch (e) { return }
if (!entry) 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() root.saveHistory()
if (root.opened) root.rebuild() if (root.opened) root.rebuild()
} }
@@ -242,10 +251,7 @@ Item {
root.history = [] root.history = []
root.typeCache = {} root.typeCache = {}
root.saveHistory() root.saveHistory()
if (dropped.length > 0) { if (dropped.length > 0) queueGc(dropped)
gcProc.command = ["rm", "-f"].concat(dropped)
gcProc.running = true
}
root.selectedIndex = 0 root.selectedIndex = 0
root.clearConfirmOpen = false root.clearConfirmOpen = false
root.rebuild() root.rebuild()
@@ -271,7 +277,10 @@ Item {
onFileChanged: reload() onFileChanged: reload()
} }
Process { id: gcProc } Process {
id: gcProc
onExited: runNextGc()
}
// Reap watchers left behind by a previous shell instance, then start our // Reap watchers left behind by a previous shell instance, then start our
// own. pdeathsig kills them whenever the shell exits. // own. pdeathsig kills them whenever the shell exits.
@@ -674,7 +683,7 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: root.listWidth + Style.space(6) anchors.leftMargin: root.listWidth + Style.space(6)
width: 1 width: Style.normalBorderWidth
color: root.lineColor color: root.lineColor
} }
+4 -11
View File
@@ -65,10 +65,11 @@ function parseQuery(q) {
var canonical = TYPE_ALIASES[wanted] var canonical = TYPE_ALIASES[wanted]
if (canonical) { out.type = canonical; continue } if (canonical) { out.type = canonical; continue }
// prefix match: "type:im" → image // prefix match: "type:im" → image
var matchedType = ""
for (var key in TYPE_ALIASES) { 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 // unknown type — treat the whole token as a term
out.terms.push(tok) out.terms.push(tok)
continue continue
@@ -91,7 +92,7 @@ function parseQuery(q) {
if (AGE_WORDS[lower] !== undefined) { if (AGE_WORDS[lower] !== undefined) {
out.maxAge = AGE_WORDS[lower] 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 continue
} }
@@ -219,14 +220,6 @@ function ageFilterOk(parsed, ageSeconds) {
return true 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) { function recencyBonus(ts, now) {
if (!ts) return 0 if (!ts) return 0
var ageDays = Math.max(0, (now - ts) / 86400) var ageDays = Math.max(0, (now - ts) / 86400)
+3 -3
View File
@@ -163,7 +163,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: Style.space(10) anchors.topMargin: Style.space(10)
height: 1 height: Style.normalBorderWidth
color: root.lineColor color: root.lineColor
} }
@@ -224,7 +224,7 @@ Item {
height: Style.space(120) height: Style.space(120)
radius: Style.cornerRadius radius: Style.cornerRadius
color: root.hexRe.test(root.rawSafe()) ? root.rawSafe() : "transparent" color: root.hexRe.test(root.rawSafe()) ? root.rawSafe() : "transparent"
border.width: 1 border.width: Style.normalBorderWidth
border.color: root.lineColor border.color: root.lineColor
} }
@@ -322,7 +322,7 @@ Item {
Rectangle { Rectangle {
anchors.fill: img anchors.fill: img
color: "transparent" color: "transparent"
border.width: 1 border.width: Style.normalBorderWidth
border.color: root.lineColor border.color: root.lineColor
radius: Style.space(4) radius: Style.space(4)
visible: img.status === Image.Ready 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 // Add (or bump) an entry; newest first. Keeps pin state and usage count of
// the existing copy when the same content is copied again. // the existing copy when the same content is copied again. Does NOT truncate
function addEntry(history, entry, limit, now) { // 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 normalized = normalize(entry, now)
var max = limit === undefined || limit === null ? DEFAULT_LIMIT : Number(limit) if (!normalized) return Array.isArray(history) ? history.slice() : []
if (isNaN(max) || max < 1) max = DEFAULT_LIMIT
if (!normalized) return Array.isArray(history) ? history.slice(0, max) : []
var key = entryKey(normalized) var key = entryKey(normalized)
var next = [normalized] var next = [normalized]
var values = Array.isArray(history) ? history : [] 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) var existing = normalize(values[i], now)
if (!existing) continue if (!existing) continue
if (entryKey(existing) === key) { if (entryKey(existing) === key) {
Binary file not shown.
+8 -1
View File
@@ -99,6 +99,7 @@ def capture_image(types, app):
pass pass
return return
emit(entry) emit(entry)
return True
def decode_text(data): def decode_text(data):
@@ -159,7 +160,13 @@ def capture_text(types, app):
def main(): 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() types = list_types()
if not types: if not types:
return return
+3 -1
View File
@@ -28,6 +28,8 @@ open_text() {
local dir file local dir file
dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/clipboard-open" dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/clipboard-open"
mkdir -p "$dir" 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) file=$(mktemp --tmpdir="$dir" clipboard.XXXXXX.txt)
printf '%s' "$text" >"$file" printf '%s' "$text" >"$file"
exec omarchy-launch-editor "$file" exec omarchy-launch-editor "$file"
@@ -43,7 +45,7 @@ case $(jq -r '.type' <<<"$entry") in
exec xdg-open "$path" exec xdg-open "$path"
;; ;;
files) files)
first=$(jq -r '.paths[0]' <<<"$entry") first=$(jq -r '.paths[0] // empty' <<<"$entry")
[[ -n $first ]] || exit 0 [[ -n $first ]] || exit 0
exec xdg-open "$first" exec xdg-open "$first"
;; ;;
+1 -1
View File
@@ -23,7 +23,7 @@ case "$type" in
;; ;;
files) files)
while IFS= read -r p; do 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 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")) assert.ok(!html.includes("\n"))
}) })
test("parseQuery yesterday bounds", () => { test("parseQuery yesterday excludes today", () => {
const q = Fuzzy.parseQuery("yesterday") const q = Fuzzy.parseQuery("yesterday")
assert.equal(q.maxAge, 172800) 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) assert.equal(h[0].uses, 3)
}) })
test("addEntry respects limit", () => { test("addEntry keeps all entries; prune truncates", () => {
let h = [] let h = []
for (let i = 0; i < 20; i++) h = Store.addEntry(h, { type: "text", text: "t" + i, ts: i }, 5) for (let i = 0; i < 20; i++) h = Store.addEntry(h, { type: "text", text: "t" + i, ts: i })
assert.equal(h.length, 5) assert.equal(h.length, 20)
assert.equal(h[0].text, "t19") 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", () => { test("removeById / findById / togglePin / touch", () => {