import QtQuick import Quickshell import qs.Commons import qs.Services.Keyboard import qs.Services.Noctalia Item { id: root // Provider metadata property string name: I18n.tr("launcher.providers.clipboard") property var launcher: null property string iconMode: Settings.data.appLauncher.iconMode property string supportedLayouts: "list" // List view for clipboard content property bool wrapNavigation: false // Don't wrap at end of list // Provider capabilities property bool handleSearch: false // Don't handle regular search // Preview support property bool hasPreview: Settings.data.appLauncher.enableClipPreview property string previewComponentPath: "./ClipboardPreview.qml" // Image handling - expose revision for reactive updates in delegates readonly property int imageRevision: ClipboardService.revision // Categories property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : [] property string selectedCategory: "All" function selectCategory(cat) { if (selectedCategory !== cat) { selectedCategory = cat; if (launcher) { launcher.updateResults(); } } } property var categoryIcons: { "All": iconMode === "tabler" ? "border-all" : "view-grid", "Images": iconMode === "tabler" ? "photo" : "image", "Links": iconMode === "tabler" ? "link" : "insert-link", "Files": iconMode === "tabler" ? "file" : "text-x-generic", "Code": iconMode === "tabler" ? "code" : "text-x-script", "Colors": iconMode === "tabler" ? "palette" : "color-picker" } // Internal state property bool isWaitingForData: false property bool gotResults: false property string lastSearchText: "" // Listen for clipboard data updates Connections { target: ClipboardService function onListCompleted() { if (gotResults && (lastSearchText === searchText)) { // Do not update results after the first fetch. // This will avoid the list resetting every 2seconds when the service updates. return; } // Refresh results if we're waiting for data or if clipboard plugin is active if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) { isWaitingForData = false; gotResults = true; if (launcher) { launcher.updateResults(); } } } function onActiveChanged() { // When active state changes (e.g. dependency check completes), refresh results if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) { isWaitingForData = true; gotResults = false; ClipboardService.list(100); } } } // Initialize provider function init() { Logger.d("ClipboardProvider", "Initialized"); // Pre-load clipboard data if service is active if (ClipboardService.active) { ClipboardService.list(100); } } // Called when launcher opens function onOpened() { isWaitingForData = true; gotResults = false; lastSearchText = ""; // Refresh clipboard history when launcher opens if (ClipboardService.active) { ClipboardService.list(100); } } // Check if this provider handles the command function handleCommand(searchText) { return searchText.startsWith(">clip"); } // Return available commands when user types ">" function commands() { return [ { "name": ">clip", "description": I18n.tr("launcher.providers.clipboard-search-description"), "icon": iconMode === "tabler" ? "clipboard" : "diodon", "isTablerIcon": true, "isImage": false, "onActivate": function () { launcher.setSearchText(">clip "); } }, { "name": ">clip clear", "description": I18n.tr("launcher.providers.clipboard-clear-description"), "icon": iconMode === "tabler" ? "trash" : "user-trash", "isTablerIcon": true, "isImage": false, "onActivate": function () { ClipboardService.wipeAll(); launcher.close(); } } ]; } // Get search results function getResults(searchText) { if (!searchText.startsWith(">clip")) { return []; } lastSearchText = searchText; const results = []; const query = searchText.slice(5).trim(); // Check if clipboard service is not active if (!ClipboardService.active) { // If dependency check hasn't completed yet, show loading instead of disabled if (!ClipboardService.dependencyChecked) { return [ { "name": I18n.tr("launcher.providers.clipboard-loading"), "description": I18n.tr("launcher.providers.emoji-loading-description"), "icon": iconMode === "tabler" ? "refresh" : "view-refresh", "isTablerIcon": true, "isImage": false, "onActivate": function () {} } ]; } return [ { "name": I18n.tr("launcher.providers.clipboard-history-disabled"), "description": I18n.tr("launcher.providers.clipboard-history-disabled-description"), "icon": iconMode === "tabler" ? "refresh" : "view-refresh", "isTablerIcon": true, "isImage": false, "onActivate": function () {} } ]; } // Special command: clear if (query === "clear") { return [ { "name": I18n.tr("launcher.providers.clipboard-clear-history"), "description": I18n.tr("launcher.providers.clipboard-clear-description-full"), "icon": iconMode === "tabler" ? "trash" : "user-trash", "isTablerIcon": true, "isImage": false, "onActivate": function () { ClipboardService.wipeAll(); launcher.close(); } } ]; } // Show loading state if data is being loaded if (ClipboardService.loading || isWaitingForData) { return [ { "name": I18n.tr("launcher.providers.clipboard-loading"), "description": I18n.tr("launcher.providers.emoji-loading-description"), "icon": iconMode === "tabler" ? "refresh" : "view-refresh", "isTablerIcon": true, "isImage": false, "onActivate": function () {} } ]; } // Get clipboard items const items = ClipboardService.items || []; // If no items and we haven't tried loading yet, trigger a load if (items.count === 0 && !ClipboardService.loading) { isWaitingForData = true; ClipboardService.list(100); return [ { "name": I18n.tr("launcher.providers.clipboard-loading"), "description": I18n.tr("launcher.providers.emoji-loading-description"), "icon": iconMode === "tabler" ? "refresh" : "view-refresh", "isTablerIcon": true, "isImage": false, "onActivate": function () {} } ]; } // Search clipboard items const searchTerm = query.toLowerCase(); const now = Date.now() / 1000; const catMap = { "Images": "image", "Links": "link", "Files": "file", "Code": "code", "Colors": "color" }; // Filter and format results items.forEach(function (item) { // Category filter if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") { if (item.contentType !== catMap[root.selectedCategory]) { return; } } const preview = (item.preview || "").toLowerCase(); // Skip if search term doesn't match if (searchTerm && preview.indexOf(searchTerm) === -1) { return; } const firstSeen = ClipboardService.firstSeenById[item.id] || now; // Format the result based on type let entry; if (item.isImage) { entry = formatImageEntry(item, firstSeen); } else { entry = formatTextEntry(item, firstSeen); } // Add activation handler entry.onActivate = function () { if (Settings.data.appLauncher.autoPasteClipboard) { launcher.closeImmediately(); Qt.callLater(() => { ClipboardService.pasteFromClipboard(item.id, item.mime); }); } else { ClipboardService.copyToClipboard(item.id); launcher.close(); } }; results.push(entry); }); // Show empty state if no results if (results.length === 0) { results.push({ "name": searchTerm ? "No matching clipboard items" : "Clipboard is empty", "description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here", "icon": iconMode === "tabler" ? "clipboard" : "text-x-generic", "isTablerIcon": true, "isImage": false, "onActivate": function () {// Do nothing } }); } //Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`) return results; } function formatImageEntry(item, firstSeen) { const meta = ClipboardService.parseImageMeta(item.preview); const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000)); let desc = meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data"; if (timeStr) desc += ` • ${timeStr}`; return { "name": meta ? `Image ${meta.w}×${meta.h}` : "Image", "description": desc, "icon": iconMode === "tabler" ? "photo" : "image", "isTablerIcon": true, "isImage": true, "imageWidth": meta ? meta.w : 0, "imageHeight": meta ? meta.h : 0, "clipboardId": item.id, "mime": item.mime, "preview": item.preview, "provider": root }; } function formatTextEntry(item, firstSeen) { const preview = (item.preview || "").trim(); const lines = preview.split('\n').filter(l => l.trim()); let title = lines[0] || "Empty text"; if (title.length > 60) { title = title.substring(0, 57) + "..."; } let description = ""; if (lines.length > 1) { description = lines[1]; if (description.length > 80) { description = description.substring(0, 77) + "..."; } } else { // Preview is truncated at ~100 chars, so we can't show exact count if (preview.length >= 100) { description = I18n.tr("toast.clipboard.long-text"); } else { const chars = preview.length; const words = preview.split(/\s+/).length; description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`; } } const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000)); if (timeStr) description += ` • ${timeStr}`; let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic"; let colorHex = ""; if (Settings.data.appLauncher.enableClipboardSmartIcons) { if (item.contentType === "link") defaultIcon = iconMode === "tabler" ? "link" : "insert-link"; else if (item.contentType === "file") defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic"; else if (item.contentType === "code") defaultIcon = iconMode === "tabler" ? "code" : "text-x-script"; else if (item.contentType === "color") { defaultIcon = iconMode === "tabler" ? "palette" : "color-picker"; colorHex = preview; } } return { "name": title, "description": description, "icon": defaultIcon, "isTablerIcon": true, "isImage": false, "clipboardId": item.id, "preview": preview, "contentType": item.contentType, "colorHex": colorHex, "provider": root }; } function getImageForItem(clipboardId) { return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; } // ------------------------- // Item actions for launcher delegate function getItemActions(item) { if (!item || !item.clipboardId) return []; var actions = []; // Annotation tool for images if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") { actions.push({ "icon": "pencil", "tooltip": I18n.tr("tooltips.open-annotation-tool"), "action": function () { var tool = Settings.data.appLauncher.screenshotAnnotationTool; Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]); if (launcher) launcher.close(); } }); } // Delete action actions.push({ "icon": "trash", "tooltip": I18n.tr("launcher.providers.clipboard-delete"), "action": function () { deleteItem(item); } }); return actions; } function canDeleteItem(item) { return item && !!item.clipboardId; } function deleteItem(item) { if (!item || !item.clipboardId) return; // Set provider state before deletion so refresh works gotResults = false; isWaitingForData = true; lastSearchText = launcher ? launcher.searchText : ""; // Delete the item ClipboardService.deleteById(String(item.clipboardId)); } // Prepare item for display (handles image decoding) function prepareItem(item) { if (item && item.isImage && item.clipboardId) { if (!ClipboardService.getImageData(item.clipboardId)) { ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null); } } } // Get image URL for item (used by delegates) function getImageUrl(item) { if (!item || !item.clipboardId) return ""; return ClipboardService.getImageData(item.clipboardId) || ""; } // Get preview data for the preview panel function getPreviewData(item) { if (!item) return null; return { "clipboardId": item.clipboardId, "isImage": item.isImage, "mime": item.mime, "preview": item.preview }; } }