268 lines
7.7 KiB
QML
268 lines
7.7 KiB
QML
import QtQuick
|
||
import Quickshell
|
||
import qs.Commons
|
||
import qs.Services
|
||
|
||
Item {
|
||
id: root
|
||
|
||
// Plugin metadata
|
||
property string name: "Clipboard History"
|
||
property var launcher: null
|
||
|
||
// Plugin capabilities
|
||
property bool handleSearch: false // Don't handle regular search
|
||
|
||
// 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()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize plugin
|
||
function init() {
|
||
Logger.log("ClipboardPlugin", "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 plugin handles the command
|
||
function handleCommand(searchText) {
|
||
return searchText.startsWith(">clip")
|
||
}
|
||
|
||
// Return available commands when user types ">"
|
||
function commands() {
|
||
return [{
|
||
"name": ">clip",
|
||
"description": "Search clipboard history",
|
||
"icon": "text-x-generic",
|
||
"isImage": false,
|
||
"onActivate": function () {
|
||
launcher.setSearchText(">clip ")
|
||
}
|
||
}, {
|
||
"name": ">clip clear",
|
||
"description": "Clear all clipboard history",
|
||
"icon": "text-x-generic",
|
||
"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) {
|
||
return [{
|
||
"name": "Clipboard History Disabled",
|
||
"description": "Enable clipboard history in settings or install cliphist",
|
||
"icon": "view-refresh",
|
||
"isImage": false,
|
||
"onActivate": function () {}
|
||
}]
|
||
}
|
||
|
||
// Special command: clear
|
||
if (query === "clear") {
|
||
return [{
|
||
"name": "Clear Clipboard History",
|
||
"description": "Remove all items from clipboard history",
|
||
"icon": "delete_sweep",
|
||
"isImage": false,
|
||
"onActivate": function () {
|
||
ClipboardService.wipeAll()
|
||
launcher.close()
|
||
}
|
||
}]
|
||
}
|
||
|
||
// Show loading state if data is being loaded
|
||
if (ClipboardService.loading || isWaitingForData) {
|
||
return [{
|
||
"name": "Loading clipboard history...",
|
||
"description": "Please wait",
|
||
"icon": "view-refresh",
|
||
"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": "Loading clipboard history...",
|
||
"description": "Please wait",
|
||
"icon": "view-refresh",
|
||
"isImage": false,
|
||
"onActivate": function () {}
|
||
}]
|
||
}
|
||
|
||
// Search clipboard items
|
||
const searchTerm = query.toLowerCase()
|
||
|
||
// Filter and format results
|
||
items.forEach(function (item) {
|
||
const preview = (item.preview || "").toLowerCase()
|
||
|
||
// Skip if search term doesn't match
|
||
if (searchTerm && preview.indexOf(searchTerm) === -1) {
|
||
return
|
||
}
|
||
|
||
// Format the result based on type
|
||
let entry
|
||
if (item.isImage) {
|
||
entry = formatImageEntry(item)
|
||
} else {
|
||
entry = formatTextEntry(item)
|
||
}
|
||
|
||
// Add activation handler
|
||
entry.onActivate = function () {
|
||
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": "text-x-generic",
|
||
"isImage": false,
|
||
"onActivate": function () {// Do nothing
|
||
}
|
||
})
|
||
}
|
||
|
||
//Logger.log("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
|
||
return results
|
||
}
|
||
|
||
// Helper: Format image clipboard entry
|
||
function formatImageEntry(item) {
|
||
const meta = parseImageMeta(item.preview)
|
||
|
||
// The launcher's delegate will now be responsible for fetching the image data.
|
||
// This function's role is to provide the necessary metadata for that request.
|
||
return {
|
||
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
|
||
"description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data",
|
||
"icon": "image",
|
||
"isImage": true,
|
||
"imageWidth": meta ? meta.w : 0,
|
||
"imageHeight": meta ? meta.h : 0,
|
||
"clipboardId": item.id,
|
||
"mime": item.mime
|
||
}
|
||
}
|
||
|
||
// Helper: Format text clipboard entry with preview
|
||
function formatTextEntry(item) {
|
||
const preview = (item.preview || "").trim()
|
||
const lines = preview.split('\n').filter(l => l.trim())
|
||
|
||
// Use first line as title, limit length
|
||
let title = lines[0] || "Empty text"
|
||
if (title.length > 60) {
|
||
title = title.substring(0, 57) + "..."
|
||
}
|
||
|
||
// Use second line or character count as description
|
||
let description = ""
|
||
if (lines.length > 1) {
|
||
description = lines[1]
|
||
if (description.length > 80) {
|
||
description = description.substring(0, 77) + "..."
|
||
}
|
||
} else {
|
||
const chars = preview.length
|
||
const words = preview.split(/\s+/).length
|
||
description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`
|
||
}
|
||
|
||
return {
|
||
"name": title,
|
||
"description": description,
|
||
"icon": "text-x-generic",
|
||
"isImage": false
|
||
}
|
||
}
|
||
|
||
// Helper: Parse image metadata from preview string
|
||
function parseImageMeta(preview) {
|
||
const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i
|
||
const match = (preview || "").match(re)
|
||
|
||
if (!match) {
|
||
return null
|
||
}
|
||
|
||
return {
|
||
"size": match[1],
|
||
"fmt": (match[2] || "").toUpperCase(),
|
||
"w": Number(match[3]),
|
||
"h": Number(match[4])
|
||
}
|
||
}
|
||
|
||
// Public method to get image data for a clipboard item
|
||
// This can be called by the launcher when rendering
|
||
function getImageForItem(clipboardId) {
|
||
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null
|
||
}
|
||
}
|