clipboard preview with image support
This commit is contained in:
+86
-8
@@ -11,23 +11,33 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string cliphistBinary: "cliphist"
|
property string cliphistBinary: "cliphist"
|
||||||
|
property string currentEntry: ""
|
||||||
property list<string> entries: []
|
property list<string> entries: []
|
||||||
property real pasteDelay: 0.05
|
property real pasteDelay: 0.05
|
||||||
readonly property var preparedEntries: entries.map(a => ({
|
readonly property var preparedEntries: entries.map(a => ({
|
||||||
name: Fuzzy.prepare(`${a.replace(/^\s*\S+\s+/, "")}`),
|
name: Fuzzy.prepare(displayText(a)),
|
||||||
entry: a
|
entry: a
|
||||||
}))
|
}))
|
||||||
|
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
|
||||||
|
property string previewImageSource: ""
|
||||||
|
property bool previewIsImage: false
|
||||||
|
property string previewText: ""
|
||||||
|
property int previewToken: 0
|
||||||
property real scoreThreshold: 0.2
|
property real scoreThreshold: 0.2
|
||||||
|
|
||||||
function copy(entry) {
|
function copy(entry): void {
|
||||||
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy`]);
|
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEntry(entry) {
|
function deleteEntry(entry): void {
|
||||||
deleteProc.deleteEntry(entry);
|
deleteProc.deleteEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
function entryIsImage(entry) {
|
function displayText(entry): string {
|
||||||
|
return entry.replace(/^\s*\d+\s+/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryIsImage(entry): bool {
|
||||||
return !!(/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(entry));
|
return !!(/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,20 +53,42 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function paste(entry) {
|
function paste(entry): void {
|
||||||
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy && wl-paste`]);
|
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy && wl-paste`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh(): void {
|
||||||
readProc.buffer = [];
|
readProc.buffer = [];
|
||||||
readProc.running = true;
|
readProc.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shellSingleQuoteEscape(str) {
|
function refreshPreview(): void {
|
||||||
|
previewToken += 1;
|
||||||
|
const token = previewToken;
|
||||||
|
|
||||||
|
if (!currentEntry) {
|
||||||
|
previewText = "";
|
||||||
|
previewImageSource = "";
|
||||||
|
previewIsImage = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previewIsImage = entryIsImage(currentEntry);
|
||||||
|
|
||||||
|
if (previewIsImage) {
|
||||||
|
previewImageProc.token = token;
|
||||||
|
previewImageProc.running = true;
|
||||||
|
} else {
|
||||||
|
previewTextProc.token = token;
|
||||||
|
previewTextProc.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellSingleQuoteEscape(str): string {
|
||||||
return String(str).replace(/'/g, "'\\''");
|
return String(str).replace(/'/g, "'\\''");
|
||||||
}
|
}
|
||||||
|
|
||||||
function wipe() {
|
function wipe(): void {
|
||||||
wipeProc.running = true;
|
wipeProc.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +139,52 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: previewTextProc
|
||||||
|
|
||||||
|
property int token: 0
|
||||||
|
|
||||||
|
command: ["bash", "-c", `
|
||||||
|
printf '%s' '${root.shellSingleQuoteEscape(root.currentEntry)}' | ${root.cliphistBinary} decode
|
||||||
|
`]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (previewTextProc.token !== root.previewToken)
|
||||||
|
return;
|
||||||
|
root.previewText = this.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: previewImageProc
|
||||||
|
|
||||||
|
property int token: 0
|
||||||
|
|
||||||
|
command: ["bash", "-c", `
|
||||||
|
set -euo pipefail
|
||||||
|
tmp='${root.shellSingleQuoteEscape(root.previewImageFile)}'
|
||||||
|
printf '%s' '${root.shellSingleQuoteEscape(root.currentEntry)}' | ${root.cliphistBinary} decode > "$tmp"
|
||||||
|
`]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (token !== root.previewToken)
|
||||||
|
return;
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.error("[Cliphist] image preview failed", exitCode, exitStatus);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.previewImageSource = "";
|
||||||
|
Qt.callLater(() => {
|
||||||
|
root.previewImageSource = `file://${root.previewImageFile}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: readProc
|
id: readProc
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
@@ -15,10 +16,10 @@ Item {
|
|||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
implicitHeight: search.implicitHeight + entries.anchors.topMargin + ((view.spacing + itemHeight) * Config.clipboard.maxEntriesShown) - view.spacing
|
implicitHeight: search.implicitHeight + entries.anchors.topMargin + ((view.spacing + itemHeight) * Config.clipboard.maxEntriesShown) - view.spacing
|
||||||
implicitWidth: Config.clipboard.sizes.width
|
implicitWidth: Config.clipboard.sizes.width + 500
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!ClipHistory.entries.length > 0)
|
if (ClipHistory.entries.length === 0)
|
||||||
ClipHistory.refresh();
|
ClipHistory.refresh();
|
||||||
searchField.forceActiveFocus();
|
searchField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,7 @@ Item {
|
|||||||
id: search
|
id: search
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: entries.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
implicitHeight: 50
|
implicitHeight: 50
|
||||||
@@ -62,14 +63,50 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
id: preview
|
||||||
|
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: entries.right
|
||||||
|
anchors.leftMargin: Appearance.spacing.normal
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
radius: 25
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.margins: Appearance.padding.large
|
||||||
|
anchors.top: parent.top
|
||||||
|
text: ClipHistory.previewText
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
visible: !ClipHistory.previewIsImage
|
||||||
|
width: preview.width - Appearance.padding.large * 2
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Appearance.padding.large
|
||||||
|
asynchronous: true
|
||||||
|
cache: false
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
mipmap: true
|
||||||
|
retainWhileLoading: true
|
||||||
|
smooth: true
|
||||||
|
source: ClipHistory.previewImageSource
|
||||||
|
visible: ClipHistory.previewIsImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CustomClippingRect {
|
CustomClippingRect {
|
||||||
id: entries
|
id: entries
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: search.bottom
|
anchors.top: search.bottom
|
||||||
anchors.topMargin: Appearance.spacing.normal
|
anchors.topMargin: Appearance.spacing.normal
|
||||||
|
implicitWidth: Config.clipboard.sizes.width
|
||||||
radius: Appearance.rounding.small
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
CustomListView {
|
CustomListView {
|
||||||
@@ -88,6 +125,7 @@ Item {
|
|||||||
delegate: RowLayout {
|
delegate: RowLayout {
|
||||||
id: clipItem
|
id: clipItem
|
||||||
|
|
||||||
|
readonly property bool isImage: ClipHistory.entryIsImage(modelData)
|
||||||
required property string modelData
|
required property string modelData
|
||||||
|
|
||||||
height: root.itemHeight
|
height: root.itemHeight
|
||||||
@@ -122,14 +160,24 @@ Item {
|
|||||||
maskSource: fadeMask
|
maskSource: fadeMask
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
MaterialIcon {
|
||||||
id: text
|
id: icon
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.margins: Appearance.padding.normal
|
anchors.margins: Appearance.padding.normal
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
text: clipItem.isImage ? "image" : "text_fields"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: text
|
||||||
|
|
||||||
|
anchors.left: icon.right
|
||||||
|
anchors.margins: Appearance.spacing.normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
text: clipItem.modelData
|
text: clipItem.isImage ? qsTr("Image") : ClipHistory.displayText(clipItem.modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +247,14 @@ Item {
|
|||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: ClipHistory.fuzzyQuery(searchField.text)
|
values: ClipHistory.fuzzyQuery(searchField.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCurrentItemChanged: {
|
||||||
|
if (!currentItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ClipHistory.currentEntry = currentItem.modelData;
|
||||||
|
ClipHistory.refreshPreview();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user