diff --git a/.config/quickshell/modules/common/Directories.qml b/.config/quickshell/modules/common/Directories.qml index db210d7a..fd18f193 100644 --- a/.config/quickshell/modules/common/Directories.qml +++ b/.config/quickshell/modules/common/Directories.qml @@ -26,15 +26,16 @@ Singleton { property string shellConfigName: "config.json" property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}` property string todoPath: FileUtils.trimFileProtocol(`${Directories.state}/user/todo.json`) - property string notificationsPath: `${Directories.cache}/notifications/notifications.json` - property string generatedMaterialThemePath: `${Directories.state}/user/generated/colors.json` + property string notificationsPath: FileUtils.trimFileProtocol(`${Directories.cache}/notifications/notifications.json`) + property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`) + property string cliphistDecode: FileUtils.trimFileProtocol(`${Directories.cache}/media/cliphist`) // Cleanup on init Component.onCompleted: { - Hyprland.dispatch(`exec mkdir -p ${Directories.shellConfig}`) - Hyprland.dispatch(`exec mkdir -p ${favicons}`) - Hyprland.dispatch(`exec rm -rf ${coverArt} && mkdir -p ${coverArt}`) - Hyprland.dispatch(`exec rm -rf '${booruPreviews}' && mkdir -p '${booruPreviews}'`) + Hyprland.dispatch(`exec mkdir -p '${favicons}'`) + Hyprland.dispatch(`exec rm -rf '${coverArt}'; mkdir -p '${coverArt}'`) + Hyprland.dispatch(`exec rm -rf '${booruPreviews}'; mkdir -p '${booruPreviews}'`) Hyprland.dispatch(`exec mkdir -p '${booruDownloads}' && mkdir -p '${booruDownloadsNsfw}'`) - Hyprland.dispatch(`exec rm -rf ${latexOutput} && mkdir -p ${latexOutput}`) + Hyprland.dispatch(`exec rm -rf '${latexOutput}'; mkdir -p '${latexOutput}'`) + Hyprland.dispatch(`exec rm -rf '${cliphistDecode}'; mkdir -p '${cliphistDecode}'`) } } diff --git a/.config/quickshell/modules/common/widgets/CliphistImage.qml b/.config/quickshell/modules/common/widgets/CliphistImage.qml new file mode 100644 index 00000000..f0b07d3f --- /dev/null +++ b/.config/quickshell/modules/common/widgets/CliphistImage.qml @@ -0,0 +1,96 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/services" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import "root:/modules/common/functions/file_utils.js" as FileUtils +import Qt5Compat.GraphicalEffects +import Qt.labs.platform +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Hyprland + +Rectangle { + id: root + property string entry + property real maxWidth + property real maxHeight + + property string imageDecodePath: Directories.cliphistDecode + property string imageDecodeFileName: `${entryNumber}` + property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}` + property string source + + property int entryNumber: { + if (!root.entry) return 0 + const match = root.entry.match(/^(\d+)\t/) + return match ? parseInt(match[1]) : 0 + } + property int imageWidth: { + if (!root.entry) return 0 + const match = root.entry.match(/(\d+)x(\d+)/) + return match ? parseInt(match[1]) : 0 + } + property int imageHeight: { + if (!root.entry) return 0 + const match = root.entry.match(/(\d+)x(\d+)/) + return match ? parseInt(match[2]) : 0 + } + property real scale: { + return Math.min( + root.maxWidth / imageWidth, + root.maxHeight / imageHeight + ) + } + + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.small + implicitHeight: imageHeight * scale + implicitWidth: imageWidth * scale + + Component.onCompleted: { + decodeImageProcess.running = true + } + + Process { + id: decodeImageProcess + command: ["bash", "-c", + `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | cliphist decode > '${imageDecodeFilePath}'` + ] + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + root.source = imageDecodeFilePath + } else { + console.error("[CliphistImage] Failed to decode image for entry:", root.entry) + root.source = "" + } + } + } + + Image { + id: image + anchors.fill: parent + + source: Qt.resolvedUrl(root.source) + fillMode: Image.PreserveAspectFit + antialiasing: true + asynchronous: true + + width: root.imageWidth * root.scale + height: root.imageHeight * root.scale + sourceSize.width: width + sourceSize.height: height + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: image.width + height: image.height + radius: root.radius + } + } + } +} + diff --git a/.config/quickshell/modules/overview/Overview.qml b/.config/quickshell/modules/overview/Overview.qml index 27434a5d..5711a943 100644 --- a/.config/quickshell/modules/overview/Overview.qml +++ b/.config/quickshell/modules/overview/Overview.qml @@ -1,4 +1,5 @@ import "root:/" +import "root:/services" import "root:/modules/common" import "root:/modules/common/widgets" import QtQuick @@ -191,6 +192,7 @@ Scope { GlobalStates.overviewOpen = false; return; } + Cliphist.refresh() for (let i = 0; i < overviewVariants.instances.length; i++) { let panelWindow = overviewVariants.instances[i]; if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { diff --git a/.config/quickshell/modules/overview/SearchItem.qml b/.config/quickshell/modules/overview/SearchItem.qml index 6f04165d..cd415bb3 100644 --- a/.config/quickshell/modules/overview/SearchItem.qml +++ b/.config/quickshell/modules/overview/SearchItem.qml @@ -25,6 +25,7 @@ RippleButton { property string fontType: entry?.fontType ?? "main" property string itemClickActionName: entry?.clickActionName property string materialSymbol: entry?.materialSymbol ?? "" + property string cliphistRawString: entry?.cliphistRawString ?? "" property string highlightPrefix: `` property string highlightSuffix: `` @@ -62,8 +63,8 @@ RippleButton { if (!root.itemName) return []; // Regular expression to match URLs const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi; - const matches = root.itemName.match(urlRegex) - .filter(url => !url.includes("…")) // Elided = invalid + const matches = root.itemName?.match(urlRegex) + ?.filter(url => !url.includes("…")) // Elided = invalid return matches ? matches : []; } @@ -143,6 +144,7 @@ RippleButton { // Main text ColumnLayout { + id: contentColumn Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter spacing: 0 @@ -173,6 +175,15 @@ RippleButton { text: `${root.displayContent}` } } + Loader { + active: root.cliphistRawString && /^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(root.cliphistRawString) + sourceComponent: CliphistImage { + Layout.fillWidth: true + entry: root.cliphistRawString + maxWidth: contentColumn.width + maxHeight: 140 + } + } } // Action text diff --git a/.config/quickshell/modules/overview/SearchWidget.qml b/.config/quickshell/modules/overview/SearchWidget.qml index ed8e5f01..84de2f6f 100644 --- a/.config/quickshell/modules/overview/SearchWidget.qml +++ b/.config/quickshell/modules/overview/SearchWidget.qml @@ -275,7 +275,7 @@ Item { // Wrapper clip: true topMargin: 10 bottomMargin: 10 - spacing: 0 + spacing: 2 KeyNavigation.up: searchBar onFocusChanged: { @@ -305,11 +305,13 @@ Item { // Wrapper const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length); return Cliphist.fuzzyQuery(searchString).map(entry => { return { + cliphistRawString: entry, name: entry.replace(/^\s*\S+\s+/, ""), - clickActionName: qsTr("Copy"), + clickActionName: "", type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`, execute: () => { Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`); + Cliphist.refresh() } }; }).filter(Boolean); diff --git a/.config/quickshell/services/Cliphist.qml b/.config/quickshell/services/Cliphist.qml index a2650a19..51514e86 100644 --- a/.config/quickshell/services/Cliphist.qml +++ b/.config/quickshell/services/Cliphist.qml @@ -40,7 +40,6 @@ Singleton { function refresh() { readProc.buffer = [] - readProc.running = false readProc.running = true }