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

View file

@ -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}'`)
}
}

View file

@ -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
}
}
}
}

View file

@ -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) {

View file

@ -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: `<u><font color="${Appearance.m3colors.m3primary}">`
property string highlightSuffix: `</font></u>`
@ -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

View file

@ -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);

View file

@ -40,7 +40,6 @@ Singleton {
function refresh() {
readProc.buffer = []
readProc.running = false
readProc.running = true
}