clipboard history: images

This commit is contained in:
end-4
2025-05-28 00:09:38 +02:00
parent f04d5f6202
commit 442ddc1a7b
6 changed files with 123 additions and 12 deletions
@@ -26,15 +26,16 @@ Singleton {
property string shellConfigName: "config.json" property string shellConfigName: "config.json"
property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}` property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}`
property string todoPath: FileUtils.trimFileProtocol(`${Directories.state}/user/todo.json`) property string todoPath: FileUtils.trimFileProtocol(`${Directories.state}/user/todo.json`)
property string notificationsPath: `${Directories.cache}/notifications/notifications.json` property string notificationsPath: FileUtils.trimFileProtocol(`${Directories.cache}/notifications/notifications.json`)
property string generatedMaterialThemePath: `${Directories.state}/user/generated/colors.json` property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`)
property string cliphistDecode: FileUtils.trimFileProtocol(`${Directories.cache}/media/cliphist`)
// Cleanup on init // Cleanup on init
Component.onCompleted: { Component.onCompleted: {
Hyprland.dispatch(`exec mkdir -p ${Directories.shellConfig}`) Hyprland.dispatch(`exec mkdir -p '${favicons}'`)
Hyprland.dispatch(`exec mkdir -p ${favicons}`) Hyprland.dispatch(`exec rm -rf '${coverArt}'; mkdir -p '${coverArt}'`)
Hyprland.dispatch(`exec rm -rf ${coverArt} && mkdir -p ${coverArt}`) Hyprland.dispatch(`exec rm -rf '${booruPreviews}'; mkdir -p '${booruPreviews}'`)
Hyprland.dispatch(`exec rm -rf '${booruPreviews}' && mkdir -p '${booruPreviews}'`)
Hyprland.dispatch(`exec mkdir -p '${booruDownloads}' && mkdir -p '${booruDownloadsNsfw}'`) 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}'`)
} }
} }
@@ -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
}
}
}
}
@@ -1,4 +1,5 @@
import "root:/" import "root:/"
import "root:/services"
import "root:/modules/common" import "root:/modules/common"
import "root:/modules/common/widgets" import "root:/modules/common/widgets"
import QtQuick import QtQuick
@@ -191,6 +192,7 @@ Scope {
GlobalStates.overviewOpen = false; GlobalStates.overviewOpen = false;
return; return;
} }
Cliphist.refresh()
for (let i = 0; i < overviewVariants.instances.length; i++) { for (let i = 0; i < overviewVariants.instances.length; i++) {
let panelWindow = overviewVariants.instances[i]; let panelWindow = overviewVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
@@ -25,6 +25,7 @@ RippleButton {
property string fontType: entry?.fontType ?? "main" property string fontType: entry?.fontType ?? "main"
property string itemClickActionName: entry?.clickActionName property string itemClickActionName: entry?.clickActionName
property string materialSymbol: entry?.materialSymbol ?? "" property string materialSymbol: entry?.materialSymbol ?? ""
property string cliphistRawString: entry?.cliphistRawString ?? ""
property string highlightPrefix: `<u><font color="${Appearance.m3colors.m3primary}">` property string highlightPrefix: `<u><font color="${Appearance.m3colors.m3primary}">`
property string highlightSuffix: `</font></u>` property string highlightSuffix: `</font></u>`
@@ -62,8 +63,8 @@ RippleButton {
if (!root.itemName) return []; if (!root.itemName) return [];
// Regular expression to match URLs // Regular expression to match URLs
const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi; const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi;
const matches = root.itemName.match(urlRegex) const matches = root.itemName?.match(urlRegex)
.filter(url => !url.includes("…")) // Elided = invalid ?.filter(url => !url.includes("…")) // Elided = invalid
return matches ? matches : []; return matches ? matches : [];
} }
@@ -143,6 +144,7 @@ RippleButton {
// Main text // Main text
ColumnLayout { ColumnLayout {
id: contentColumn
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
spacing: 0 spacing: 0
@@ -173,6 +175,15 @@ RippleButton {
text: `${root.displayContent}` 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 // Action text
@@ -275,7 +275,7 @@ Item { // Wrapper
clip: true clip: true
topMargin: 10 topMargin: 10
bottomMargin: 10 bottomMargin: 10
spacing: 0 spacing: 2
KeyNavigation.up: searchBar KeyNavigation.up: searchBar
onFocusChanged: { onFocusChanged: {
@@ -305,11 +305,13 @@ Item { // Wrapper
const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length); const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length);
return Cliphist.fuzzyQuery(searchString).map(entry => { return Cliphist.fuzzyQuery(searchString).map(entry => {
return { return {
cliphistRawString: entry,
name: entry.replace(/^\s*\S+\s+/, ""), name: entry.replace(/^\s*\S+\s+/, ""),
clickActionName: qsTr("Copy"), clickActionName: "",
type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`, type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`,
execute: () => { execute: () => {
Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`); Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`);
Cliphist.refresh()
} }
}; };
}).filter(Boolean); }).filter(Boolean);
-1
View File
@@ -40,7 +40,6 @@ Singleton {
function refresh() { function refresh() {
readProc.buffer = [] readProc.buffer = []
readProc.running = false
readProc.running = true readProc.running = true
} }