diff --git a/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml b/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml index 1f866139..d3350357 100644 --- a/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml +++ b/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml @@ -29,16 +29,7 @@ DankModal { property int activeImageLoads: 0 readonly property int maxConcurrentLoads: 3 readonly property bool clipboardAvailable: DMSService.isConnected && (DMSService.capabilities.length === 0 || DMSService.capabilities.includes("clipboard")) - property bool wtypeAvailable: false - - Process { - id: wtypeCheck - command: ["which", "wtype"] - running: true - onExited: exitCode => { - clipboardHistoryModal.wtypeAvailable = (exitCode === 0); - } - } + readonly property bool wtypeAvailable: SessionService.wtypeAvailable Process { id: wtypeProcess diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index a3c672dd..cd2e5d79 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Quickshell.Io import qs.Common import qs.Services import "Scorer.js" as Scorer @@ -41,6 +42,47 @@ Item { } } + Connections { + target: PluginService + function onRequestLauncherUpdate(pluginId) { + if (activePluginId === pluginId || searchQuery) { + performSearch(); + } + } + } + + Process { + id: wtypeProcess + command: ["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"] + running: false + } + + Timer { + id: pasteTimer + interval: 200 + repeat: false + onTriggered: wtypeProcess.running = true + } + + function pasteSelected() { + if (!selectedItem) + return; + if (!SessionService.wtypeAvailable) { + ToastService.showError("wtype not available - install wtype for paste support"); + return; + } + + const pluginId = selectedItem.pluginId; + if (!pluginId) + return; + const pasteText = AppSearchService.getPluginPasteText(pluginId, selectedItem.data); + if (!pasteText) + return; + Quickshell.execDetached(["dms", "cl", "copy", pasteText]); + itemExecuted(); + pasteTimer.start(); + } + readonly property var sectionDefinitions: [ { id: "calculator", diff --git a/quickshell/Modals/DankLauncherV2/LauncherContent.qml b/quickshell/Modals/DankLauncherV2/LauncherContent.qml index 472a32e4..7d925e92 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContent.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContent.qml @@ -209,6 +209,10 @@ FocusScope { return; case Qt.Key_Return: case Qt.Key_Enter: + if (event.modifiers & Qt.ShiftModifier) { + controller.pasteSelected(); + return; + } if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) { actionPanel.executeSelectedAction(); } else { diff --git a/quickshell/Modals/DankLauncherV2/TileItem.qml b/quickshell/Modals/DankLauncherV2/TileItem.qml index 83f14893..b0fea6db 100644 --- a/quickshell/Modals/DankLauncherV2/TileItem.qml +++ b/quickshell/Modals/DankLauncherV2/TileItem.qml @@ -109,6 +109,18 @@ Rectangle { color: Theme.primaryText } } + + Image { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Theme.spacingXS + width: 40 + height: 16 + fillMode: Image.PreserveAspectFit + source: root.item?.data?.attribution || "" + visible: source !== "" + opacity: 0.9 + } } } diff --git a/quickshell/Services/AppSearchService.qml b/quickshell/Services/AppSearchService.qml index 406e4428..effa3404 100644 --- a/quickshell/Services/AppSearchService.qml +++ b/quickshell/Services/AppSearchService.qml @@ -855,6 +855,21 @@ Singleton { return false; } + function getPluginPasteText(pluginId, item) { + if (typeof PluginService === "undefined") + return null; + + const instance = PluginService.pluginInstances[pluginId]; + if (!instance) + return null; + + if (typeof instance.getPasteText === "function") { + return instance.getPasteText(item); + } + + return null; + } + function searchPluginItems(query) { if (typeof PluginService === "undefined") return []; diff --git a/quickshell/Services/PluginService.qml b/quickshell/Services/PluginService.qml index 25104538..156017a9 100644 --- a/quickshell/Services/PluginService.qml +++ b/quickshell/Services/PluginService.qml @@ -592,6 +592,13 @@ Singleton { return SettingsData.getPluginSetting(pluginId, key, defaultValue); } + function getPluginPath(pluginId) { + const plugin = availablePlugins[pluginId]; + if (!plugin) + return ""; + return plugin.pluginDirectory || ""; + } + function saveAllPluginSettings() { SettingsData.savePluginSettings(); } diff --git a/quickshell/Services/SessionService.qml b/quickshell/Services/SessionService.qml index c2181f49..27d7a645 100644 --- a/quickshell/Services/SessionService.qml +++ b/quickshell/Services/SessionService.qml @@ -29,6 +29,7 @@ Singleton { } property bool loginctlAvailable: false + property bool wtypeAvailable: false property string sessionId: "" property string sessionPath: "" property bool locked: false @@ -59,6 +60,7 @@ Singleton { detectElogindProcess.running = true; detectHibernateProcess.running = true; detectPrimeRunProcess.running = true; + detectWtypeProcess.running = true; console.info("SessionService: Native inhibitor available:", nativeInhibitorAvailable); if (!SettingsData.loginctlLockIntegration) { console.log("SessionService: loginctl lock integration disabled by user"); @@ -124,6 +126,15 @@ Singleton { } } + Process { + id: detectWtypeProcess + running: false + command: ["which", "wtype"] + onExited: exitCode => { + wtypeAvailable = (exitCode === 0); + } + } + Process { id: detectPrimeRunProcess running: false diff --git a/quickshell/Widgets/CachingImage.qml b/quickshell/Widgets/CachingImage.qml index a3f3d815..5ffec09b 100644 --- a/quickshell/Widgets/CachingImage.qml +++ b/quickshell/Widgets/CachingImage.qml @@ -1,13 +1,21 @@ import QtQuick import qs.Common -Image { +Item { id: root property string imagePath: "" property int maxCacheSize: 512 + property int status: isAnimated ? animatedImg.status : staticImg.status + property int fillMode: Image.PreserveAspectCrop readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://") + readonly property bool isAnimated: { + if (!imagePath) + return false; + const lower = imagePath.toLowerCase(); + return lower.endsWith(".gif") || lower.endsWith(".webp"); + } readonly property string normalizedPath: { if (!imagePath) return ""; @@ -30,7 +38,7 @@ Image { } readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : "" - readonly property string cachePath: imageHash && !isRemoteUrl ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : "" + readonly property string cachePath: imageHash && !isRemoteUrl && !isAnimated ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : "" readonly property string encodedImagePath: { if (!normalizedPath) return ""; @@ -39,39 +47,56 @@ Image { return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/'); } - asynchronous: true - fillMode: Image.PreserveAspectCrop - sourceSize.width: maxCacheSize - sourceSize.height: maxCacheSize - smooth: true + AnimatedImage { + id: animatedImg + anchors.fill: parent + visible: root.isAnimated + asynchronous: true + fillMode: root.fillMode + source: root.isAnimated ? root.imagePath : "" + playing: visible && status === AnimatedImage.Ready + } + + Image { + id: staticImg + anchors.fill: parent + visible: !root.isAnimated + asynchronous: true + fillMode: root.fillMode + sourceSize.width: root.maxCacheSize + sourceSize.height: root.maxCacheSize + smooth: true + + onStatusChanged: { + if (source == root.cachePath && status === Image.Error) { + source = root.encodedImagePath; + return; + } + if (root.isRemoteUrl || source != root.encodedImagePath || status !== Image.Ready || !root.cachePath) + return; + Paths.mkdir(Paths.imagecache); + const grabPath = root.cachePath; + if (visible && width > 0 && height > 0 && Window.window?.visible) { + grabToImage(res => res.saveToFile(grabPath)); + } + } + } onImagePathChanged: { if (!imagePath) { - source = ""; + staticImg.source = ""; return; } + if (isAnimated) + return; if (isRemoteUrl) { - source = imagePath; + staticImg.source = imagePath; return; } Paths.mkdir(Paths.imagecache); const hash = djb2Hash(normalizedPath); const cPath = hash ? `${Paths.stringify(Paths.imagecache)}/${hash}@${maxCacheSize}x${maxCacheSize}.png` : ""; const encoded = "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/'); - source = cPath || encoded; - } - - onStatusChanged: { - if (source == cachePath && status === Image.Error) { - source = encodedImagePath; - return; - } - if (isRemoteUrl || source != encodedImagePath || status !== Image.Ready || !cachePath) - return; - Paths.mkdir(Paths.imagecache); - const grabPath = cachePath; - if (visible && width > 0 && height > 0 && Window.window?.visible) { - grabToImage(res => res.saveToFile(grabPath)); - } + staticImg.source = cPath || encoded; } }