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" }
]