diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 4c44ad6..7f57b53 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -255,8 +255,8 @@ NPanel { // When this delegate is assigned a new image item, trigger the decode. onCurrentClipboardIdChanged: { // Check if it's a valid ID and if the data isn't already cached. - if (currentClipboardId && !CliphistService.getImageData(currentClipboardId)) { - CliphistService.decodeToDataUrl(currentClipboardId, modelData.mime, null) + if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) { + ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null) } } @@ -293,14 +293,14 @@ NPanel { visible: modelData.isImage // This property creates a dependency on the service's revision counter - readonly property int _rev: CliphistService.revision + readonly property int _rev: ClipboardService.revision // Fetches from the service's cache. // The dependency on `_rev` ensures this binding is re-evaluated // when the cache is updated by the service. source: { _rev - return CliphistService.getImageData(modelData.clipboardId) || "" + return ClipboardService.getImageData(modelData.clipboardId) || "" } fillMode: Image.PreserveAspectFit diff --git a/Modules/Launcher/Plugins/CalculatorPlugin.qml b/Modules/Launcher/Plugins/CalculatorPlugin.qml index a9578dd..ee13c70 100644 --- a/Modules/Launcher/Plugins/CalculatorPlugin.qml +++ b/Modules/Launcher/Plugins/CalculatorPlugin.qml @@ -54,10 +54,7 @@ QtObject { "icon": "accessories-calculator", "isImage": false, "onActivate": function () { - // Copy result to clipboard if service available - // if (typeof ClipboardService !== 'undefined') { - // ClipboardService.copy(result.toString()) - // } + // TODO: copy entry to clipboard via ClipHist launcher.close() } }] diff --git a/Modules/Launcher/Plugins/ClipboardPlugin.qml b/Modules/Launcher/Plugins/ClipboardPlugin.qml index 8d51124..586473b 100644 --- a/Modules/Launcher/Plugins/ClipboardPlugin.qml +++ b/Modules/Launcher/Plugins/ClipboardPlugin.qml @@ -13,9 +13,9 @@ QtObject { // Plugin capabilities property bool handleSearch: false // Don't handle regular search + // Listen for clipboard data updates // Connections { - // target: CliphistService - // // Use the function syntax for on + // target: ClipboardService // function onListCompleted() { // // Only refresh if the clipboard plugin is active // if (launcher && launcher.activePlugin === root) { @@ -32,7 +32,7 @@ QtObject { // Called when launcher opens function onOpened() { // Refresh clipboard history when launcher opens - CliphistService.list(100) + ClipboardService.list(100) } // Check if this plugin handles the command @@ -56,7 +56,7 @@ QtObject { "icon": "delete_sweep", "isImage": false, "onActivate": function () { - CliphistService.wipeAll() + ClipboardService.wipeAll() launcher.close() } }] @@ -78,18 +78,29 @@ QtObject { "description": "Remove all items from clipboard history", "icon": "delete_sweep", "onActivate": function () { - CliphistService.wipeAll() + ClipboardService.wipeAll() launcher.close() } }] } + // Show loading state if data isn't ready yet + if (ClipboardService.loading) { + return [{ + "name": "Loading clipboard history...", + "description": "Please wait", + "icon": "view-refresh", + "isImage": false, + "onActivate": function () {} + }] + } + // Search clipboard items const searchTerm = query.toLowerCase() // Force dependency update - const _rev = CliphistService.revision - const items = CliphistService.items || [] + const _rev = ClipboardService.revision + const items = ClipboardService.items || [] // Filter and format results items.forEach(function (item) { @@ -110,7 +121,7 @@ QtObject { // Add activation handler entry.onActivate = function () { - CliphistService.copyToClipboard(item.id) + ClipboardService.copyToClipboard(item.id) launcher.close() } @@ -122,13 +133,14 @@ QtObject { results.push({ "name": searchTerm ? "No matching clipboard items" : "Clipboard is empty", "description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here", - "icon": "content_paste_off", + "icon": "text-x-generic", "isImage": false, "onActivate": function () {// Do nothing } }) } + Logger.log("ClipboardPlugin", `Returning ${results.length} results`) return results } @@ -145,8 +157,7 @@ QtObject { "isImage": true, "imageWidth": meta ? meta.w : 0, "imageHeight": meta ? meta.h : 0, - "clipboardId"// Provide the ID and mime type for the delegate to make an async request - : item.id, + "clipboardId": item.id, "mime": item.mime } } @@ -203,6 +214,6 @@ QtObject { // Public method to get image data for a clipboard item // This can be called by the launcher when rendering function getImageForItem(clipboardId) { - return CliphistService.getImageData ? CliphistService.getImageData(clipboardId) : null + return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null } -} +} \ No newline at end of file diff --git a/Services/ClipboardService.qml b/Services/ClipboardService.qml index 899ec77..e521369 100644 --- a/Services/ClipboardService.qml +++ b/Services/ClipboardService.qml @@ -4,225 +4,335 @@ import QtQuick import Quickshell import Quickshell.Io import qs.Commons -import qs.Services +// Thin wrapper around the cliphist CLI Singleton { id: root - property var history: [] - property bool initialized: false - property int maxHistory: 50 // Limit clipboard history entries + // Public API + property var items: [] // [{id, preview, mime, isImage}] + property bool loading: false + // Active only when feature is enabled, settings have finished initial load, and cliphist is available + property bool active: Settings.data.appLauncher.enableClipboardHistory && Settings.isLoaded && cliphistAvailable - // Internal state - property bool _enabled: true + // Check if cliphist is available on the system + property bool cliphistAvailable: false + property bool dependencyChecked: false - // Cached history file path - property string historyFile: Quickshell.env("NOCTALIA_CLIPBOARD_HISTORY_FILE") - || (Settings.cacheDir + "clipboard.json") + // Optional automatic watchers to feed cliphist DB + property bool autoWatch: true + property bool watchersStarted: false - // Persisted storage for clipboard history - property FileView historyFileView: FileView { - id: historyFileView - objectName: "clipboardHistoryFileView" - path: historyFile - watchChanges: false // We don't need to watch changes for clipboard - onAdapterUpdated: writeAdapter() - Component.onCompleted: reload() - onLoaded: loadFromHistory() - onLoadFailed: function (error) { - // Create file on first use - if (error.toString().includes("No such file") || error === 2) { - writeAdapter() - } - } + // Expose decoded thumbnails by id and a revision to notify bindings + property var imageDataById: ({}) + property int revision: 0 - JsonAdapter { - id: historyAdapter - property var history: [] - property double timestamp: 0 - } + // Approximate first-seen timestamps for entries this session (seconds) + property var firstSeenById: ({}) + + // Internal: store callback for decode + property var _decodeCallback: null + + // Queue for base64 decodes + property var _b64Queue: [] + property var _b64CurrentCb: null + property string _b64CurrentMime: "" + property string _b64CurrentId: "" + + signal listCompleted() + + // Check if cliphist is available + Component.onCompleted: { + checkCliphistAvailability() } - Timer { - interval: 2000 - repeat: true - running: root._enabled - onTriggered: root.refresh() + // Check dependency availability + function checkCliphistAvailability() { + if (dependencyChecked) + return + + dependencyCheckProcess.command = ["which", "cliphist"] + dependencyCheckProcess.running = true } - // Detect current clipboard types (text/image) + // Process to check if cliphist is available Process { - id: typeProcess - property bool isLoading: false - property var currentTypes: [] - + id: dependencyCheckProcess + stdout: StdioCollector {} onExited: (exitCode, exitStatus) => { + root.dependencyChecked = true if (exitCode === 0) { - currentTypes = String(stdout.text).trim().split('\n').filter(t => t) - - // Always check for text first - textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] - textProcess.isLoading = true - textProcess.running = true - - // Also check for images if available - const imageType = currentTypes.find(t => t.startsWith('image/')) - if (imageType) { - imageProcess.mimeType = imageType - imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] - imageProcess.running = true + root.cliphistAvailable = true + // Start watchers if feature is enabled + if (root.active) { + startWatchers() } } else { - typeProcess.isLoading = false - } - } - - stdout: StdioCollector {} - } - - // Read image data - Process { - id: imageProcess - property string mimeType: "" - - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const base64 = stdout.text.trim() - if (base64) { - const entry = { - "type": 'image', - "mimeType": mimeType, - "data": `data:${mimeType};base64,${base64}`, - "timestamp": new Date().getTime() - } - - // Check if this exact image already exists - const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) - if (!exists) { - // Normalize existing history and add the new image - const normalizedHistory = root.history.map(item => { - if (typeof item === 'string') { - return { - "type": 'text', - "content": item, - "timestamp": new Date().getTime( - ) - 1000 // Make it slightly older - } - } - return item - }) - root.history = [entry, ...normalizedHistory].slice(0, maxHistory) - saveHistory() - } + root.cliphistAvailable = false + // Show toast notification if feature is enabled but cliphist is missing + if (Settings.data.appLauncher.enableClipboardHistory) { + ToastService.showWarning( + "Clipboard History Unavailable", + "The 'cliphist' application is not installed. Please install it to use clipboard history features.", + false, 6000) } } - - // Always mark as initialized when done - if (!textProcess.isLoading) { - root.initialized = true - typeProcess.isLoading = false - } } - - stdout: StdioCollector {} } - // Read text data + // Start/stop watchers when enabled changes + onActiveChanged: { + if (root.active && root.cliphistAvailable) { + startWatchers() + } else { + stopWatchers() + loading = false + // Optional: clear items to avoid stale UI + items = [] + } + } + + // Fallback: periodically refresh list so UI updates even if not in clip mode + Timer { + interval: 5000 + repeat: true + running: root.active && root.cliphistAvailable + onTriggered: list() + } + + // Internal process objects Process { - id: textProcess - property bool isLoading: false - + id: listProc + stdout: StdioCollector {} onExited: (exitCode, exitStatus) => { - textProcess.isLoading = false + const out = String(stdout.text) + const lines = out.split('\n').filter(l => l.length > 0) + // cliphist list default format: " " or "\t" + const parsed = lines.map(l => { + let id = "" + let preview = "" + const m = l.match(/^(\d+)\s+(.+)$/) + if (m) { + id = m[1] + preview = m[2] + } else { + const tab = l.indexOf('\t') + id = tab > -1 ? l.slice(0, tab) : l + preview = tab > -1 ? l.slice(tab + 1) : "" + } + const lower = preview.toLowerCase() + const isImage = lower.startsWith("[image]") || lower.includes(" binary data ") + // Best-effort mime guess from preview + var mime = "text/plain" + if (isImage) { + if (lower.includes(" png")) + mime = "image/png" + else if (lower.includes(" jpg") || lower.includes(" jpeg")) + mime = "image/jpeg" + else if (lower.includes(" webp")) + mime = "image/webp" + else if (lower.includes(" gif")) + mime = "image/gif" + else + mime = "image/*" + } + // Record first seen time for new ids (approximate copy time) + if (!root.firstSeenById[id]) { + root.firstSeenById[id] = Time.timestamp + } + return { + "id": id, + "preview": preview, + "isImage": isImage, + "mime": mime + } + }) + items = parsed + loading = false - if (exitCode === 0) { - const content = String(stdout.text).trim() - if (content && content.length > 0) { - const entry = { - "type": 'text', - "content": content, - "timestamp": new Date().getTime() - } + // Emit the signal for subscribers + root.listCompleted() + } + } - // Check if this exact text content already exists - const exists = root.history.find(item => { - if (item.type === 'text') { - return item.content === content - } - return item === content - }) - - if (!exists) { - // Normalize existing history entries - const normalizedHistory = root.history.map(item => { - if (typeof item === 'string') { - return { - "type": 'text', - "content": item, - "timestamp": new Date().getTime( - ) - 1000 // Make it slightly older - } - } - return item - }) - - root.history = [entry, ...normalizedHistory].slice(0, maxHistory) - saveHistory() - } + Process { + id: decodeProc + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + const out = String(stdout.text) + if (root._decodeCallback) { + try { + root._decodeCallback(out) + } finally { + root._decodeCallback = null } } - - // Mark as initialized and clean up loading states - root.initialized = true - if (!imageProcess.running) { - typeProcess.isLoading = false - } } + } + Process { + id: copyProc stdout: StdioCollector {} } - function refresh() { - if (!typeProcess.isLoading && !textProcess.isLoading && !imageProcess.running) { - typeProcess.isLoading = true - typeProcess.command = ["wl-paste", "-l"] - typeProcess.running = true + // Base64 decode pipeline (queued) + Process { + id: decodeB64Proc + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + const b64 = String(stdout.text).trim() + if (root._b64CurrentCb) { + const url = `data:${root._b64CurrentMime};base64,${b64}` + try { + root._b64CurrentCb(url) + } finally { + + /* noop */ } + } + if (root._b64CurrentId !== "") { + root.imageDataById[root._b64CurrentId] = `data:${root._b64CurrentMime};base64,${b64}` + root.revision += 1 + } + root._b64CurrentCb = null + root._b64CurrentMime = "" + root._b64CurrentId = "" + Qt.callLater(root._startNextB64) } } - function loadFromHistory() { - // Populate in-memory history from cached file - try { - const items = historyAdapter.history || [] - root.history = items.slice(0, maxHistory) // Apply limit when loading - Logger.log("Clipboard", "Loaded", root.history.length, "entries from cache") - } catch (e) { - Logger.error("Clipboard", "Failed to load history:", e) - root.history = [] + // Long-running watchers to store new clipboard contents + Process { + id: watchText + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + // Auto-restart if watcher dies + if (root.autoWatch) + Qt.callLater(() => { + running = true + }) + } + } + Process { + id: watchImage + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch) + Qt.callLater(() => { + running = true + }) } } - function saveHistory() { - try { - // Ensure we don't exceed the maximum history limit - const limitedHistory = root.history.slice(0, maxHistory) + function startWatchers() { + if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) + return + watchersStarted = true + // Start text watcher + watchText.command = ["wl-paste", "--type", "text", "--watch", "cliphist", "store"] + watchText.running = true + // Start image watcher + watchImage.command = ["wl-paste", "--type", "image", "--watch", "cliphist", "store"] + watchImage.running = true + } - historyAdapter.history = limitedHistory - historyAdapter.timestamp = Time.timestamp + function stopWatchers() { + if (!watchersStarted) + return + watchText.running = false + watchImage.running = false + watchersStarted = false + } - // Ensure cache directory exists - Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) + function list(maxPreviewWidth) { + if (!root.active || !root.cliphistAvailable) { + return + } + if (listProc.running) + return + loading = true + const width = maxPreviewWidth || 100 + listProc.command = ["cliphist", "list", "-preview-width", String(width)] + listProc.running = true + } - Qt.callLater(function () { - historyFileView.writeAdapter() - }) - } catch (e) { - Logger.error("Clipboard", "Failed to save history:", e) + function decode(id, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb("") + return + } + root._decodeCallback = cb + decodeProc.command = ["cliphist", "decode", id] + decodeProc.running = true + } + + function decodeToDataUrl(id, mime, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb("") + return + } + // If cached, return immediately + if (root.imageDataById[id]) { + if (cb) + cb(root.imageDataById[id]) + return + } + // Queue request; ensures single process handles sequentially + root._b64Queue.push({ + "id": id, + "mime": mime || "image/*", + "cb": cb + }) + if (!decodeB64Proc.running && root._b64CurrentCb === null) { + _startNextB64() } } - function clearHistory() { - root.history = [] - saveHistory() + function getImageData(id) { + if (id === undefined) { + return null + } + return root.imageDataById[id] + } + + function _startNextB64() { + if (root._b64Queue.length === 0 || !root.cliphistAvailable) + return + const job = root._b64Queue.shift() + root._b64CurrentCb = job.cb + root._b64CurrentMime = job.mime + root._b64CurrentId = job.id + decodeB64Proc.command = ["sh", "-lc", `cliphist decode ${job.id} | base64 -w 0`] + decodeB64Proc.running = true + } + + function copyToClipboard(id) { + if (!root.cliphistAvailable) { + return + } + // decode and pipe to wl-copy; implement via shell to preserve binary + copyProc.command = ["sh", "-lc", `cliphist decode ${id} | wl-copy`] + copyProc.running = true + } + + function deleteById(id) { + if (!root.cliphistAvailable) { + return + } + Quickshell.execDetached(["cliphist", "delete", id]) + Qt.callLater(() => list()) + } + + function wipeAll() { + if (!root.cliphistAvailable) { + return + } + + Quickshell.execDetached(["cliphist", "wipe"]) + + revision++ + + Qt.callLater(() => list()) } } diff --git a/Services/CliphistService.qml b/Services/CliphistService.qml deleted file mode 100644 index e521369..0000000 --- a/Services/CliphistService.qml +++ /dev/null @@ -1,338 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Commons - -// Thin wrapper around the cliphist CLI -Singleton { - id: root - - // Public API - property var items: [] // [{id, preview, mime, isImage}] - property bool loading: false - // Active only when feature is enabled, settings have finished initial load, and cliphist is available - property bool active: Settings.data.appLauncher.enableClipboardHistory && Settings.isLoaded && cliphistAvailable - - // Check if cliphist is available on the system - property bool cliphistAvailable: false - property bool dependencyChecked: false - - // Optional automatic watchers to feed cliphist DB - property bool autoWatch: true - property bool watchersStarted: false - - // Expose decoded thumbnails by id and a revision to notify bindings - property var imageDataById: ({}) - property int revision: 0 - - // Approximate first-seen timestamps for entries this session (seconds) - property var firstSeenById: ({}) - - // Internal: store callback for decode - property var _decodeCallback: null - - // Queue for base64 decodes - property var _b64Queue: [] - property var _b64CurrentCb: null - property string _b64CurrentMime: "" - property string _b64CurrentId: "" - - signal listCompleted() - - // Check if cliphist is available - Component.onCompleted: { - checkCliphistAvailability() - } - - // Check dependency availability - function checkCliphistAvailability() { - if (dependencyChecked) - return - - dependencyCheckProcess.command = ["which", "cliphist"] - dependencyCheckProcess.running = true - } - - // Process to check if cliphist is available - Process { - id: dependencyCheckProcess - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - root.dependencyChecked = true - if (exitCode === 0) { - root.cliphistAvailable = true - // Start watchers if feature is enabled - if (root.active) { - startWatchers() - } - } else { - root.cliphistAvailable = false - // Show toast notification if feature is enabled but cliphist is missing - if (Settings.data.appLauncher.enableClipboardHistory) { - ToastService.showWarning( - "Clipboard History Unavailable", - "The 'cliphist' application is not installed. Please install it to use clipboard history features.", - false, 6000) - } - } - } - } - - // Start/stop watchers when enabled changes - onActiveChanged: { - if (root.active && root.cliphistAvailable) { - startWatchers() - } else { - stopWatchers() - loading = false - // Optional: clear items to avoid stale UI - items = [] - } - } - - // Fallback: periodically refresh list so UI updates even if not in clip mode - Timer { - interval: 5000 - repeat: true - running: root.active && root.cliphistAvailable - onTriggered: list() - } - - // Internal process objects - Process { - id: listProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - const out = String(stdout.text) - const lines = out.split('\n').filter(l => l.length > 0) - // cliphist list default format: " " or "\t" - const parsed = lines.map(l => { - let id = "" - let preview = "" - const m = l.match(/^(\d+)\s+(.+)$/) - if (m) { - id = m[1] - preview = m[2] - } else { - const tab = l.indexOf('\t') - id = tab > -1 ? l.slice(0, tab) : l - preview = tab > -1 ? l.slice(tab + 1) : "" - } - const lower = preview.toLowerCase() - const isImage = lower.startsWith("[image]") || lower.includes(" binary data ") - // Best-effort mime guess from preview - var mime = "text/plain" - if (isImage) { - if (lower.includes(" png")) - mime = "image/png" - else if (lower.includes(" jpg") || lower.includes(" jpeg")) - mime = "image/jpeg" - else if (lower.includes(" webp")) - mime = "image/webp" - else if (lower.includes(" gif")) - mime = "image/gif" - else - mime = "image/*" - } - // Record first seen time for new ids (approximate copy time) - if (!root.firstSeenById[id]) { - root.firstSeenById[id] = Time.timestamp - } - return { - "id": id, - "preview": preview, - "isImage": isImage, - "mime": mime - } - }) - items = parsed - loading = false - - // Emit the signal for subscribers - root.listCompleted() - } - } - - Process { - id: decodeProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - const out = String(stdout.text) - if (root._decodeCallback) { - try { - root._decodeCallback(out) - } finally { - root._decodeCallback = null - } - } - } - } - - Process { - id: copyProc - stdout: StdioCollector {} - } - - // Base64 decode pipeline (queued) - Process { - id: decodeB64Proc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - const b64 = String(stdout.text).trim() - if (root._b64CurrentCb) { - const url = `data:${root._b64CurrentMime};base64,${b64}` - try { - root._b64CurrentCb(url) - } finally { - - /* noop */ } - } - if (root._b64CurrentId !== "") { - root.imageDataById[root._b64CurrentId] = `data:${root._b64CurrentMime};base64,${b64}` - root.revision += 1 - } - root._b64CurrentCb = null - root._b64CurrentMime = "" - root._b64CurrentId = "" - Qt.callLater(root._startNextB64) - } - } - - // Long-running watchers to store new clipboard contents - Process { - id: watchText - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - // Auto-restart if watcher dies - if (root.autoWatch) - Qt.callLater(() => { - running = true - }) - } - } - Process { - id: watchImage - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (root.autoWatch) - Qt.callLater(() => { - running = true - }) - } - } - - function startWatchers() { - if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) - return - watchersStarted = true - // Start text watcher - watchText.command = ["wl-paste", "--type", "text", "--watch", "cliphist", "store"] - watchText.running = true - // Start image watcher - watchImage.command = ["wl-paste", "--type", "image", "--watch", "cliphist", "store"] - watchImage.running = true - } - - function stopWatchers() { - if (!watchersStarted) - return - watchText.running = false - watchImage.running = false - watchersStarted = false - } - - function list(maxPreviewWidth) { - if (!root.active || !root.cliphistAvailable) { - return - } - if (listProc.running) - return - loading = true - const width = maxPreviewWidth || 100 - listProc.command = ["cliphist", "list", "-preview-width", String(width)] - listProc.running = true - } - - function decode(id, cb) { - if (!root.cliphistAvailable) { - if (cb) - cb("") - return - } - root._decodeCallback = cb - decodeProc.command = ["cliphist", "decode", id] - decodeProc.running = true - } - - function decodeToDataUrl(id, mime, cb) { - if (!root.cliphistAvailable) { - if (cb) - cb("") - return - } - // If cached, return immediately - if (root.imageDataById[id]) { - if (cb) - cb(root.imageDataById[id]) - return - } - // Queue request; ensures single process handles sequentially - root._b64Queue.push({ - "id": id, - "mime": mime || "image/*", - "cb": cb - }) - if (!decodeB64Proc.running && root._b64CurrentCb === null) { - _startNextB64() - } - } - - function getImageData(id) { - if (id === undefined) { - return null - } - return root.imageDataById[id] - } - - function _startNextB64() { - if (root._b64Queue.length === 0 || !root.cliphistAvailable) - return - const job = root._b64Queue.shift() - root._b64CurrentCb = job.cb - root._b64CurrentMime = job.mime - root._b64CurrentId = job.id - decodeB64Proc.command = ["sh", "-lc", `cliphist decode ${job.id} | base64 -w 0`] - decodeB64Proc.running = true - } - - function copyToClipboard(id) { - if (!root.cliphistAvailable) { - return - } - // decode and pipe to wl-copy; implement via shell to preserve binary - copyProc.command = ["sh", "-lc", `cliphist decode ${id} | wl-copy`] - copyProc.running = true - } - - function deleteById(id) { - if (!root.cliphistAvailable) { - return - } - Quickshell.execDetached(["cliphist", "delete", id]) - Qt.callLater(() => list()) - } - - function wipeAll() { - if (!root.cliphistAvailable) { - return - } - - Quickshell.execDetached(["cliphist", "wipe"]) - - revision++ - - Qt.callLater(() => list()) - } -}