Floating card window, retention settings (maxAgeDays/historyLimit/maxRows), pause/resume (Ctrl+= + IPC), LICENSE, README with screenshots, screenshot tooling

This commit is contained in:
2026-09-05 20:28:03 +01:00
parent 1aac582e90
commit 66ba5bd3db
10 changed files with 428 additions and 73 deletions
+120 -19
View File
@@ -28,6 +28,9 @@ Item {
property string statePath: Quickshell.env("HOME") + "/.local/state/omarchy/clipboard-history-rich.json"
property int historyLimit: 1500
property int displayLimit: 200
property int maxAgeDays: 0 // 0 = keep forever
property bool qrDecode: true
property bool paused: false
property var typeCache: ({}) // id → derived type, memoized
// Theme surface tokens (menu) — tracks the active Omarchy theme.
@@ -46,8 +49,8 @@ Item {
readonly property string fontFamily: Style.font.menuFamily
readonly property int contentMargin: Style.spacing.panelPadding
readonly property int headerHeight: Math.max(Style.space(40), Style.font.heading + Style.spacing.controlPaddingY * 2)
readonly property int cardWidth: Math.min(Style.space(980), panel.width - Style.gapsOut * 2)
readonly property int cardHeight: Math.min(Style.space(680), panel.height - Style.gapsOut * 2)
readonly property int cardWidth: Style.space(980)
readonly property int cardHeight: Style.space(680)
readonly property int rowHeight: Style.space(52)
readonly property int listWidth: Math.round(card.width * 0.46)
@@ -76,19 +79,65 @@ Item {
else root.open()
}
// ------------------------------------------------------------ pause
// Toggle via IPC: omarchy-shell shell call tank.clipboard pause '{"paused":true}'
// (or {"paused":false}, or {"paused":"toggle"}), and Ctrl+Space in the picker.
function setPaused(next) {
root.paused = !!next
pausedFile.setText(JSON.stringify({ paused: root.paused }) + "\n")
}
function pause(payloadJson) {
var p = null
try { p = JSON.parse(String(payloadJson || "{}")) } catch (e) { p = null }
var next = p && typeof p.paused !== "undefined"
? (p.paused === "toggle" ? !root.paused : !!p.paused)
: !root.paused
root.setPaused(next)
Quickshell.execDetached(["notify-send", "-a", "Clipboard History",
next ? "Clipboard history paused" : "Clipboard history resumed",
next ? "New copies are not being recorded." : "Recording new copies again."])
return JSON.stringify({ paused: root.paused })
}
function isPaused() { return JSON.stringify({ paused: root.paused }) }
// Screenshot helper: omarchy-shell shell call tank.clipboard debugSetFilter '{"text":"…"}'
// Opens the picker with a preset query so scripts can capture it keyboard-free.
function debugSetFilter(payloadJson) {
var p = {}
try { p = JSON.parse(String(payloadJson || "{}")) } catch (e) { p = {} }
root.open()
root.setFilter(String(p.text || ""))
return "ok"
}
// ------------------------------------------------------------ store
function loadHistory(raw) {
root.history = Store.parseHistory(raw, Math.floor(Date.now() / 1000))
root.typeCache = {}
root.applyRetentionPolicy()
if (root.opened) root.rebuild()
}
function saveHistory() {
var pruned = Store.prune(root.history, root.historyLimit)
root.history = pruned.entries
var aged = Store.pruneByAge(pruned.entries, root.maxAgeDays > 0 ? root.maxAgeDays * 86400 : -1, Math.floor(Date.now() / 1000))
root.history = aged.entries
historyFile.setText(JSON.stringify(root.history, null, 1) + "\n")
if (pruned.droppedImagePaths.length > 0) queueGc(pruned.droppedImagePaths)
var paths = pruned.droppedImagePaths.concat(aged.droppedImagePaths)
if (paths.length > 0) queueGc(paths)
}
// Full retention pass, run when history loads and when settings change.
function applyRetentionPolicy() {
if (root.maxAgeDays <= 0) return
var aged = Store.pruneByAge(root.history, root.maxAgeDays * 86400, Math.floor(Date.now() / 1000))
if (aged.entries.length === root.history.length) return
root.history = aged.entries
if (aged.droppedImagePaths.length > 0) queueGc(aged.droppedImagePaths)
root.saveHistory()
}
// Serialize GC batches: a single reusable Process would silently drop
@@ -105,6 +154,7 @@ Item {
}
function addClipboardJson(line) {
if (root.paused) return
var entry = null
try { entry = JSON.parse(String(line || "")) } catch (e) { return }
if (!entry) return
@@ -266,6 +316,41 @@ Item {
PointerMoveGate { id: pointerGate; referenceItem: card }
// User settings live on this plugin's entry in shell.json (hot-reloads):
// { "id": "tank.clipboard", "historyLimit": 1500, "maxAgeDays": 30, "maxRows": 200 }
FileView {
id: shellConfigFile
path: Quickshell.env("HOME") + "/.config/omarchy/shell.json"
watchChanges: true
printErrors: false
onLoaded: root.applySettings(text())
onLoadFailed: root.applySettings("{}")
onFileChanged: reload()
}
function applySettings(raw) {
var s = Store.parseSettings(raw, "tank.clipboard")
root.historyLimit = s.historyLimit
root.maxAgeDays = s.maxAgeDays
root.displayLimit = s.maxRows
root.qrDecode = s.qrDecode
root.applyRetentionPolicy()
if (root.opened) root.rebuild()
}
// Pause state survives shell restarts.
FileView {
id: pausedFile
path: Quickshell.env("HOME") + "/.local/state/omarchy/clipboard-paused.json"
watchChanges: true
atomicWrites: true
printErrors: false
onLoaded: {
try { root.paused = !!JSON.parse(text()).paused } catch (e) { root.paused = false }
}
onLoadFailed: root.paused = false
}
FileView {
id: historyFile
path: root.statePath
@@ -304,6 +389,7 @@ Item {
Process {
id: watchProc
command: ["setpriv", "--pdeathsig", "TERM", "wl-paste", "--watch", "python3", root.pluginDir + "/capture.py", "watch"]
environment: ({ "CLIPBOARD_QR": root.qrDecode ? "1" : "0" })
onExited: watchRestartTimer.restart()
stdout: SplitParser {
onRead: function(data) { root.addClipboardJson(data) }
@@ -327,32 +413,23 @@ Item {
// ------------------------------------------------------------ window
// Floating card: an unanchored layer surface is centered by the compositor,
// so the picker is just the card — no fullscreen scrim underneath.
PanelWindow {
id: panel
visible: root.opened
anchors { top: true; bottom: true; left: true; right: true }
width: root.cardWidth
height: root.cardHeight
color: "transparent"
WlrLayershell.namespace: "tank-clipboard"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
Rectangle {
anchors.fill: parent
color: root.scrim
}
MouseArea {
anchors.fill: parent
onClicked: root.close()
}
BorderSurface {
id: card
width: root.cardWidth
height: root.cardHeight
anchors.fill: parent
radius: root.cornerRadius
anchors.centerIn: parent
color: root.background
borderSpec: root.borderSpec
padding: root.contentMargin
@@ -405,6 +482,9 @@ Item {
} else if (event.key === Qt.Key_Tab) {
root.togglePinIndex(root.selectedIndex)
event.accepted = true
} else if (event.key === Qt.Key_Equal && (event.modifiers & Qt.ControlModifier)) {
root.pause(JSON.stringify({ paused: "toggle" }))
event.accepted = true
} else if (event.key === Qt.Key_O && (event.modifiers & Qt.ControlModifier)) {
root.openResult(root.currentResult)
event.accepted = true
@@ -497,6 +577,7 @@ Item {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: {
if (root.paused) return "⏸ paused"
var shown = root.results.length
var total = root.history.length
if (shown === total) return total + " items"
@@ -556,10 +637,29 @@ Item {
}
}
// ---- paused banner
Rectangle {
width: parent.width
height: root.paused ? Style.space(24) : 0
visible: root.paused
radius: Style.cornerRadius
color: Util.alpha(Color.urgent, 0.15)
Text {
anchors.centerIn: parent
text: "⏸ Paused — new copies are not being recorded · Ctrl+= to resume"
color: Color.urgent
font.family: root.fontFamily
font.pixelSize: Style.font.caption
}
}
// ---- list + preview
Item {
width: parent.width
height: parent.height - root.headerHeight - chipsRow.height - footer.height - Style.space(30)
height: parent.height - root.headerHeight - chipsRow.height
- (root.paused ? Style.space(24) + Style.space(10) : 0)
- footer.height - Style.space(30)
ListView {
id: resultList
@@ -740,6 +840,7 @@ Item {
{ keys: "shift+enter", hint: "copy" },
{ keys: "ctrl+o", hint: "open" },
{ keys: "tab", hint: "pin" },
{ keys: "ctrl+=", hint: "pause" },
{ keys: "del", hint: "remove" },
{ keys: "esc", hint: "close" }
]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 alanfortlink
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+115 -53
View File
@@ -1,35 +1,64 @@
# clipboard-history
A Raycast/vicinae-style clipboard history manager for Omarchy — implemented as
an Omarchy shell plugin (`tank.clipboard`, a clone of `omarchy.clipboard`) so it
rides on the system theme, fonts, and layer-shell infrastructure.
A Raycast/vicinae-style clipboard history manager for [Omarchy](https://omarchy.org),
built as an Omarchy shell plugin. Fuzzy search over everything (content, type,
source app, date — even text inside QR codes), rich per-type previews, and
full theme integration.
## Features
<p align="center">
<img src="docs/screenshots/picker.png" alt="clipboard-history picker showing a QR code with its decoded content" width="720">
</p>
- **Rich capture** — a `wl-paste --watch` daemon records every clip with mime
type, byte size, source app (via `hyprctl`), timestamp, and image dimensions.
Text, images, and `file://` URI lists (file-manager copies) are supported;
password-manager clips and binary payloads are skipped.
- **QR codes** — copied QR images are decoded with `zbarimg`; the payload shows
in the list title, the preview pane (copy-selectable), and a metadata chip,
and is fuzzy-searchable like any text clip.
- **Fuzzy search over everything** — fzf-style scoring over content, source
app, and type, plus recency, pin, and usage boosts. Query tokens:
## Highlights
- **Fuzzy search over everything** — fzf-style scoring across content, source
app, and type, boosted by recency, pins, and usage. Query tokens:
- `type:image|link|text|files|code|json|color|email|html|number` (prefix match)
- `app:firefox` — fuzzy match on the source app
- `is:pinned`, `today`, `yesterday`, `week`, `<2h`, `>30s`, `<3d`
- **Raycast-style UI** — search bar with blinking cursor, type filter chips,
two-line result rows with type icons / image thumbnails and metadata
(type · app · age · size), and a right-hand preview pane:
- images rendered inline (with dimensions + size)
- colors shown as a swatch with hex/rgb/hsl values
- links show the domain headline
- JSON is pretty-printed; code/html shown appropriately
- file lists with paths; every preview shows metadata chips (app, date,
words/lines, bytes, pin/paste counts)
- **Fully theme-integrated** — colors, spacing, corner radius, borders, and the
monospace font all come from the Omarchy shell's `Color`/`Style` singletons;
it re-themes itself on `omarchy theme set`.
- **Rich previews, per type** — images rendered inline, QR codes show their
decoded content, colors show a swatch with hex/rgb/hsl, links show the
domain, JSON is pretty-printed, file lists show paths — every preview with
metadata chips (app, date, words/lines, size, paste counts)
- **QR codes** — copied QR images are decoded with `zbarimg`; the payload is
shown in the list, preview, and is searchable like any text clip
- **Rich capture** — a `wl-paste --watch` daemon records every clip with mime
type, byte size, source app (via `hyprctl`), timestamp, and image dimensions.
Text, images, and `file://` URI lists (file-manager copies) are supported;
password-manager clips and binary payloads are skipped
- **Retention control** — configure how much and how long history is kept
- **Pause/resume** — stop recording new copies without losing your history
- **Fully theme-integrated** — colors, spacing, corner radius, borders, and
the monospace font all come from the Omarchy shell's theme singletons; it
re-themes itself on `omarchy theme set`
<p align="center">
<img src="docs/screenshots/search.png" alt="fuzzy search for “omarchy” with highlighted matches" width="720">
</p>
## Install
```bash
omarchy plugin add https://github.com/alanfortlink/clipboard-history.git --enable
```
Optional QR support (usually already installed):
```bash
omarchy pkg add zbar
```
That's the whole install — `omarchy plugin add` clones the repo, validates the
manifest, and enables it. It replaces the built-in `omarchy.clipboard`
(restore it later with `omarchy plugin disable tank.clipboard`), and you bind
it wherever you like:
```bash
# ~/.config/hypr/bindings.conf
bindd = ALT SHIFT, V, Clipboard manager (clipboard-history), exec, omarchy-shell shell toggle tank.clipboard
```
Updating: `omarchy plugin update tank.clipboard` · Uninstall: `omarchy plugin remove tank.clipboard`
## Keys
@@ -40,56 +69,89 @@ rides on the system theme, fonts, and layer-shell infrastructure.
| `Shift+Enter` | copy only |
| `Ctrl+O` | open (link → browser, image → editor, file → xdg-open, text → editor) |
| `Tab` | pin/unpin |
| `Ctrl+=` | pause/resume recording |
| `Delete` | remove entry · `Shift+Delete` clear all (with confirm) |
| `Esc` | clear filter, then close |
## Install
Pause/resume is also scriptable — useful for automation or a custom binding:
```bash
./install.sh
omarchy-shell shell call tank.clipboard pause '{"paused":"toggle"}'
omarchy-shell shell call tank.clipboard isPaused
```
This symlinks the repo (the plugin root) into
`~/.config/omarchy/plugins/tank.clipboard`, rescans + enables the plugin (the
built-in `omarchy.clipboard` is replaced — revert with
`omarchy plugin disable tank.clipboard`), and rebinds `ALT+SHIFT+V` from
vicinae to this picker (backing up `bindings.conf`).
## Configuration
On other machines, install straight from git — no clone/step needed:
Settings live on the plugin's entry in the `plugins` array of
`~/.config/omarchy/shell.json` and hot-reload on save:
```bash
omarchy plugin add <this-repo-git-url> --enable
```json
{
"version": 1,
"plugins": [
{
"id": "tank.clipboard",
"historyLimit": 1500,
"maxAgeDays": 30,
"maxRows": 200
}
]
}
```
Note: the local dev symlink trips `omarchy plugin validate` (it refuses
symlinked plugin folders); a git-cloned copy validates clean.
| Key | Default | Meaning |
|-----|---------|---------|
| `historyLimit` | `1500` | max entries kept in history |
| `maxAgeDays` | `0` (forever) | drop entries older than N days (images get garbage-collected; pinned entries are exempt) |
| `maxRows` | `200` | max rows the picker shows per search |
| `qrDecode` | `true` | decode QR codes with `zbarimg` on captured images |
## Layout
History lives in `~/.local/state/omarchy/clipboard-history-rich.json`; image
blobs are content-addressed under `~/.local/state/omarchy/clipboard-images/`.
The repo root *is* the plugin folder (so `omarchy plugin add <git-url>` works
directly — it validates and clones a repo whose root holds `manifest.json`).
## How it compares
| | built-in `omarchy.clipboard` | [sspaeti's OCR fork](https://github.com/sspaeti/omarchy-clipboard-plugin) | [Clipbasket](https://github.com/clipbasket/clipbasket-omarchy) | vicinae | **clipboard-history** |
|---|---|---|---|---|---|
| Fuzzy search | substring | + OCR text | ✓ | ✓ | content + app + type + date tokens (`type:`, `app:`, `<2h`, `today`) |
| Previews | text/image | — | — | ✓ | images, **QR payloads**, color swatches, JSON pretty-print, links, files |
| Metadata | basic | basic | SQLite | ✓ | size, source app, dims, word/line counts, pins, paste counts |
| Retention config | 300 cap | limit | ✓ | ✓ | `historyLimit` + `maxAgeDays` + GC |
| Pause recording | ✗ | ✗ | ? | ✓ | ✓ (in-picker + IPC) |
| Theme-integrated shell overlay | ✓ | ✓ | ✓ | own | ✓ (uses Omarchy's theme singletons) |
## Roadmap / ideas
- OCR search: tesseract on captured images so text inside screenshots is
searchable ([reference implementation](https://github.com/sspaeti/omarchy-clipboard-plugin))
- `autoPaste` mode swap: Enter = copy-only, Shift+Enter = paste (walker-style)
- Small bar widget: last-copied item + paused indicator
- Snippet editing: edit a pinned entry's content in place
- Multi-select delete, export/import of history
- Optional per-app capture exclusions beyond the password-manager hint
## Development
```
├── manifest.json # plugin manifest (clonedFrom omarchy.clipboard)
├── manifest.json # plugin manifest (replaces omarchy.clipboard via clonedFrom)
├── Clipboard.qml # picker overlay: search, chips, list, keys, capture watchers
├── PreviewPane.qml # per-type preview + metadata chips
├── Store.js # history model: dedup, pins, pruning, ids
├── Store.js # history model: dedup, pins, retention, settings parsing
├── Fuzzy.js # query parser, fuzzy matcher, scoring, highlighting
├── Classify.js # type detection, app names, formatting, color math
├── capture.py # clipboard watcher → one JSON line per clip (incl. QR decode)
├── paste-entry.sh # copy + shift-insert paste into the focused window
── open-entry.sh # open with the right app
tests/ # node --test suites for the JS logic
── open-entry.sh # open with the right app
├── tests/ # node --test suites for the JS logic
└── scripts/ # screenshot regeneration helpers
```
History is stored in `~/.local/state/omarchy/clipboard-history-rich.json`
(image blobs content-addressed under `~/.local/state/omarchy/clipboard-images/`).
- Run logic tests: `tests/run.sh`
- Validate the manifest: `omarchy plugin validate <checkout-dir>`
- Regenerate screenshots: `scripts/take-screenshots.sh` (stages demo content
and captures the picker keyboard-free over IPC)
- Hot-reload after edits: `omarchy-shell shell rescanPlugins`
## Development
## License
- Run logic tests: `tests/run.sh` (49 tests).
- After editing files in the repo, reload with `omarchy-shell shell rescanPlugins`
(the plugin dir is a symlink, so the shell's inotify does not watch repo edits).
- Data lives in `~/.local/state/omarchy/`; the picker state is independent of
the built-in clipboard plugin's `clipboard-history.json`.
[MIT](LICENSE)
+50
View File
@@ -174,6 +174,56 @@ function prune(history, limit) {
return { entries: kept, droppedImagePaths: droppedImagePaths }
}
// Retention: drop entries older than maxAgeSeconds (-1 = keep forever).
// Pinned entries are exempt — pins are favorites; delete them explicitly.
// Returns { entries, droppedImagePaths } like prune().
function pruneByAge(history, maxAgeSeconds, now) {
var values = Array.isArray(history) ? history : []
if (maxAgeSeconds === undefined || maxAgeSeconds === null || maxAgeSeconds < 0)
return { entries: values, droppedImagePaths: [] }
var cutoff = now - maxAgeSeconds
var kept = []
var droppedImagePaths = []
for (var i = 0; i < values.length; i++) {
var e = values[i]
if (!e) continue
if ((Number(e.ts) || 0) < cutoff && !e.pinned) {
if (e.type === "image" && e.path) droppedImagePaths.push(e.path)
continue
}
kept.push(e)
}
return { entries: kept, droppedImagePaths: droppedImagePaths }
}
// Parse this plugin's settings from the shell.json contents. Every key is
// 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 config = null
try { config = JSON.parse(String(raw || "{}")) } catch (e) { return out }
if (!config || !Array.isArray(config.plugins)) return out
for (var i = 0; i < config.plugins.length; i++) {
var entry = config.plugins[i]
if (!entry || entry.id !== pluginId) continue
var n = Number(entry.historyLimit)
if (isFinite(n) && n >= 1) out.historyLimit = Math.floor(n)
n = Number(entry.maxAgeDays)
if (isFinite(n) && n >= 0) out.maxAgeDays = Math.floor(n)
n = Number(entry.maxRows)
if (isFinite(n) && n >= 1) out.maxRows = Math.floor(n)
if (typeof entry.qrDecode === "boolean") out.qrDecode = entry.qrDecode
break
}
return out
}
// Build the search-row context Fuzzy.searchRows expects.
function buildRow(entry, derivedType, now) {
var content = ""
+1 -1
View File
@@ -115,7 +115,7 @@ def capture_image(types, app):
pass
return
qr = decode_qr(path)
qr = decode_qr(path) if os.environ.get("CLIPBOARD_QR", "1") != "0" else None
if qr:
entry["qr"] = qr
Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Print the PHYSICAL-pixel crop geometry (WxH+X+Y) of the tank-clipboard
layer surface on grim's combined framebuffer. Monitors report logical .x/.y
with .scale; layers report logical xywh; grim captures physical pixels."""
import json
import re
import subprocess
layers = subprocess.run(["hyprctl", "layers"], capture_output=True, text=True).stdout
m = re.search(r"xywh:\s*(\d+) (\d+) (\d+) (\d+).*namespace: tank-clipboard", layers)
if not m:
raise SystemExit(1)
lx, ly, lw, lh = map(int, m.groups())
mons = json.loads(subprocess.run(["hyprctl", "monitors", "-j"], capture_output=True, text=True).stdout)
# Physical framebuffer layout: monitors sorted by (y, x); each occupies its
# physical width/height; offsets accumulate.
mons.sort(key=lambda d: (d["y"], d["x"]))
for i, d in enumerate(mons):
logical_w = d["width"] / d["scale"]
if d["x"] <= lx < d["x"] + logical_w and d["y"] <= ly < d["y"] + d["height"] / d["scale"]:
phys_x = sum(o["width"] for o in mons[:i])
px = phys_x + round((lx - d["x"]) * d["scale"])
py = round((ly - d["y"]) * d["scale"])
pw = round(lw * d["scale"])
ph = round(lh * d["scale"])
print(f"{pw}x{ph}+{px}+{py}")
break
else:
raise SystemExit(1)
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Regenerate README screenshots (docs/screenshots/*.png).
# Stages demo clipboard entries, opens the picker keyboard-free (IPC), and
# crops the card from a full-screen capture. Run in a graphical session.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT="$REPO_DIR/docs/screenshots"
mkdir -p "$OUT"
crop_shot() { # <output.png> — open via IPC first; caller decides what's on screen
local out=$1
omarchy-shell shell hide tank.clipboard
sleep 0.4
local geo
case $out in
"$OUT/search.png")
omarchy-shell shell call tank.clipboard debugSetFilter '{"text":"omarchy"}'
;;
*)
omarchy-shell shell toggle tank.clipboard
;;
esac
sleep 1.5
local geo; geo=$(python3 "$REPO_DIR/scripts/physical-crop.py")
grim /tmp/clipshot-full.png
omarchy-shell shell hide tank.clipboard
sleep 0.4
magick /tmp/clipshot-full.png -crop "$geo" +repage "$out"
}
# ---- stage demo content ----------------------------------------------------
qrencode -s 10 -o /tmp/clipshot-qr-repo.png "https://github.com/alanfortlink/clipboard-history"
qrencode -s 10 -o /tmp/clipshot-qr-omarchy.png "https://omarchy.org"
# stage newest-last so the QR pointing at this repo is selected on open
printf 'The quick brown fox jumps over the lazy dog.' | wl-copy; sleep 1.5
printf 'function pasteEntry(id) {\n const entry = store.findById(id)\n return paste(entry)\n}' | wl-copy; sleep 1.5
printf '#7aa2f7' | wl-copy; sleep 1.5
printf 'https://omarchy.org/manual/shell-plugins/' | wl-copy; sleep 1.5
wl-copy --type image/png < /tmp/clipshot-qr-omarchy.png; sleep 1.5
wl-copy --type image/png < /tmp/clipshot-qr-repo.png; sleep 1.5
# ---- shots -----------------------------------------------------------------
crop_shot "$OUT/picker.png"
crop_shot "$OUT/search.png"
echo "Wrote $OUT/picker.png and $OUT/search.png"
+44
View File
@@ -126,3 +126,47 @@ test("buildRow makes QR payload searchable", () => {
const row = Store.buildRow({ type: "image", path: "/x/qr.png", mime: "image/png", qr: "secret-payload-xyz", ts: 1, bytes: 10, app: "", uses: 0 }, "image", 1)
assert.ok(row.content.includes("secret-payload-xyz"))
})
test("pruneByAge drops old entries, keeps pins, reports images", () => {
const now = 1000000
const h = [
{ id: "new", type: "text", text: "fresh", ts: now - 10, bytes: 1, app: "", uses: 0 },
{ id: "old", type: "text", text: "old", ts: now - 100 * 86400, bytes: 1, app: "", uses: 0 },
{ id: "oldimg", type: "image", path: "/tmp/old.png", ts: now - 90 * 86400, bytes: 1, app: "", uses: 0 },
{ id: "oldpin", type: "text", text: "pinned old", ts: now - 200 * 86400, pinned: true, bytes: 1, app: "", uses: 0 }
]
const r = Store.pruneByAge(h, 30 * 86400, now)
assert.deepEqual(r.entries.map(e => e.id).sort().join(","), ["new", "oldpin"].sort().join(","))
assert.equal(r.droppedImagePaths.length, 1)
})
test("pruneByAge no-op when forever", () => {
const h = [{ id: "a", type: "text", text: "x", ts: 1, bytes: 1, app: "", uses: 0 }]
const r = Store.pruneByAge(h, -1, 1000000)
assert.equal(r.entries.length, 1)
assert.equal(r.droppedImagePaths.length, 0)
})
test("parseSettings from shell.json", () => {
const raw = JSON.stringify({
plugins: [
{ id: "other.plugin" },
{ id: "tank.clipboard", historyLimit: 500, maxAgeDays: 14, maxRows: 50, qrDecode: false, customKey: 1 }
]
})
const s = Store.parseSettings(raw, "tank.clipboard")
assert.equal(s.historyLimit, 500)
assert.equal(s.maxAgeDays, 14)
assert.equal(s.maxRows, 50)
assert.equal(s.qrDecode, false)
const d = Store.parseSettings("{}", "tank.clipboard")
assert.equal(d.historyLimit, 1500)
assert.equal(d.maxAgeDays, 0)
assert.equal(d.maxRows, 200)
assert.equal(d.qrDecode, true)
})
test("parseSettings ignores garbage", () => {
const s = Store.parseSettings("not json", "tank.clipboard")
assert.equal(s.historyLimit, 1500)
})