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"
|
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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -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
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
@@ -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
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user