clipboard preview with syntax support
This commit is contained in:
@@ -8,6 +8,8 @@ JsonObject {
|
|||||||
|
|
||||||
component Sizes: JsonObject {
|
component Sizes: JsonObject {
|
||||||
property int itemHeight: 60
|
property int itemHeight: 60
|
||||||
|
property int minPreviewWidth: 200
|
||||||
|
property int previewWidth: 500
|
||||||
property int width: 500
|
property int width: 500
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ Singleton {
|
|||||||
maxEntriesShown: clipboard.maxEntriesShown,
|
maxEntriesShown: clipboard.maxEntriesShown,
|
||||||
sizes: {
|
sizes: {
|
||||||
width: clipboard.sizes.width,
|
width: clipboard.sizes.width,
|
||||||
|
previewWidth: clipboard.sizes.previewWidth,
|
||||||
|
minPreviewWidth: clipboard.sizes.minPreviewWidth,
|
||||||
itemHeight: clipboard.sizes.itemHeight
|
itemHeight: clipboard.sizes.itemHeight
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ Singleton {
|
|||||||
}))
|
}))
|
||||||
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
|
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
|
||||||
property string previewImageSource: ""
|
property string previewImageSource: ""
|
||||||
|
property bool previewIsCode: looksLikeCode(previewText)
|
||||||
property bool previewIsImage: false
|
property bool previewIsImage: false
|
||||||
|
readonly property string previewMarkup: previewIsCode ? generateHighlightedMarkup(previewText) : previewText
|
||||||
property string previewText: ""
|
property string previewText: ""
|
||||||
property int previewToken: 0
|
property int previewToken: 0
|
||||||
property real scoreThreshold: 0.2
|
property real scoreThreshold: 0.2
|
||||||
@@ -41,6 +43,10 @@ Singleton {
|
|||||||
return !!(/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(entry));
|
return !!(/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str): string {
|
||||||
|
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
function fuzzyQuery(search: string): var {
|
function fuzzyQuery(search: string): var {
|
||||||
if (search.trim() === "") {
|
if (search.trim() === "") {
|
||||||
return entries;
|
return entries;
|
||||||
@@ -53,6 +59,115 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateHighlightedMarkup(text): string {
|
||||||
|
const raw = String(text ?? "");
|
||||||
|
const lines = raw.split("\n").map(line => highlightCodeLine(line));
|
||||||
|
return `<div style="font-family:monospace; white-space:pre-wrap;">${lines.join("<br>")}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightCodeLine(rawLine): string {
|
||||||
|
const line = String(rawLine ?? "");
|
||||||
|
|
||||||
|
const kwColor = DynamicColors.palette.m3primary;
|
||||||
|
const strColor = DynamicColors.palette.m3tertiary;
|
||||||
|
const comColor = Qt.alpha(DynamicColors.palette.m3onSurface, 0.55);
|
||||||
|
|
||||||
|
const keywordRe = /\b(function|class|import|const|let|var|if|else|for|while|return|switch|case|break|continue|try|catch|throw|async|await|new|null|true|false|public|private|protected|static|extends|struct|enum)\b/g;
|
||||||
|
|
||||||
|
let out = "";
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < line.length) {
|
||||||
|
const ch = line[i];
|
||||||
|
|
||||||
|
// Line comment
|
||||||
|
if (line.slice(i, i + 2) === "//") {
|
||||||
|
out += `<span style="color:${comColor};">${escapeHtml(line.slice(i))}</span>`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shell/Python-style comment
|
||||||
|
if (ch === "#" && i === 0) {
|
||||||
|
out += `<span style="color:${comColor};">${escapeHtml(line.slice(i))}</span>`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quoted string
|
||||||
|
if (ch === "'" || ch === '"' || ch === "`") {
|
||||||
|
const quote = ch;
|
||||||
|
let j = i + 1;
|
||||||
|
while (j < line.length) {
|
||||||
|
if (line[j] === "\\") {
|
||||||
|
j += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line[j] === quote) {
|
||||||
|
j += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
out += `<span style="color:${strColor};">${escapeHtml(line.slice(i, j))}</span>`;
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain code text until next special token
|
||||||
|
let j = i;
|
||||||
|
while (j < line.length) {
|
||||||
|
const two = line.slice(j, j + 2);
|
||||||
|
const c = line[j];
|
||||||
|
|
||||||
|
if (two === "//")
|
||||||
|
break;
|
||||||
|
if (c === "'" || c === '"' || c === "`")
|
||||||
|
break;
|
||||||
|
if (c === "#" && j === 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let segment = escapeHtml(line.slice(i, j));
|
||||||
|
segment = segment.replace(keywordRe, `<span style="color:${kwColor}; font-weight:600;">$1</span>`);
|
||||||
|
out += segment;
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out === "" ? " " : out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeCode(text): bool {
|
||||||
|
const t = String(text ?? "").trim();
|
||||||
|
|
||||||
|
if (t === "")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const lines = t.split("\n");
|
||||||
|
|
||||||
|
if (lines.length < 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (/^\s{4,}|\t/.test(line))
|
||||||
|
score += 2;
|
||||||
|
|
||||||
|
if (/[{}()[\];]/.test(line))
|
||||||
|
score += 1;
|
||||||
|
|
||||||
|
if (/\b(function|class|import|const|let|var|if|else|for|while|return|switch|case|try|catch|async|await)\b/.test(line))
|
||||||
|
score += 2;
|
||||||
|
|
||||||
|
if (/=>|:=/.test(line))
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score >= 4;
|
||||||
|
}
|
||||||
|
|
||||||
function paste(entry): void {
|
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`]);
|
||||||
}
|
}
|
||||||
@@ -73,6 +188,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previewImageSource = "";
|
||||||
previewIsImage = entryIsImage(currentEntry);
|
previewIsImage = entryIsImage(currentEntry);
|
||||||
|
|
||||||
if (previewIsImage) {
|
if (previewIsImage) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ 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 + 500
|
implicitWidth: Config.clipboard.sizes.width + preview.width + preview.anchors.leftMargin
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (ClipHistory.entries.length === 0)
|
if (ClipHistory.entries.length === 0)
|
||||||
@@ -69,23 +69,33 @@ Item {
|
|||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: entries.right
|
anchors.left: entries.right
|
||||||
anchors.leftMargin: Appearance.spacing.normal
|
anchors.leftMargin: Appearance.spacing.normal
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + imagePreview.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreview.paintedWidth + textPreview.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
|
||||||
radius: 25
|
radius: 25
|
||||||
|
|
||||||
|
Behavior on implicitWidth {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
|
id: textPreview
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.margins: Appearance.padding.large
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
text: ClipHistory.previewText
|
font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
|
||||||
textFormat: Text.PlainText
|
text: ClipHistory.previewMarkup
|
||||||
|
textFormat: ClipHistory.previewIsCode ? Text.RichText : Text.PlainText
|
||||||
visible: !ClipHistory.previewIsImage
|
visible: !ClipHistory.previewIsImage
|
||||||
width: preview.width - Appearance.padding.large * 2
|
width: Config.clipboard.sizes.previewWidth - anchors.margins * 2
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
id: imagePreview
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.margins: Appearance.padding.large
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
@@ -95,7 +105,7 @@ Item {
|
|||||||
retainWhileLoading: true
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
source: ClipHistory.previewImageSource
|
source: ClipHistory.previewImageSource
|
||||||
visible: ClipHistory.previewIsImage
|
visible: ClipHistory.previewIsImage && ClipHistory.previewImageSource !== ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Item {
|
|||||||
|
|
||||||
CustomClippingWrapperRect {
|
CustomClippingWrapperRect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
child: flickable.contentItem
|
child: flickable
|
||||||
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
|
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user