diff --git a/Config/ClipboardConfig.qml b/Config/ClipboardConfig.qml
index 5d52ef1..4867301 100644
--- a/Config/ClipboardConfig.qml
+++ b/Config/ClipboardConfig.qml
@@ -8,6 +8,8 @@ JsonObject {
component Sizes: JsonObject {
property int itemHeight: 60
+ property int minPreviewWidth: 200
+ property int previewWidth: 500
property int width: 500
}
}
diff --git a/Config/Config.qml b/Config/Config.qml
index b10be07..f7f8600 100644
--- a/Config/Config.qml
+++ b/Config/Config.qml
@@ -123,6 +123,8 @@ Singleton {
maxEntriesShown: clipboard.maxEntriesShown,
sizes: {
width: clipboard.sizes.width,
+ previewWidth: clipboard.sizes.previewWidth,
+ minPreviewWidth: clipboard.sizes.minPreviewWidth,
itemHeight: clipboard.sizes.itemHeight
}
};
diff --git a/Helpers/ClipHistory.qml b/Helpers/ClipHistory.qml
index 39c66ec..3c7fca4 100644
--- a/Helpers/ClipHistory.qml
+++ b/Helpers/ClipHistory.qml
@@ -20,7 +20,9 @@ Singleton {
}))
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
property string previewImageSource: ""
+ property bool previewIsCode: looksLikeCode(previewText)
property bool previewIsImage: false
+ readonly property string previewMarkup: previewIsCode ? generateHighlightedMarkup(previewText) : previewText
property string previewText: ""
property int previewToken: 0
property real scoreThreshold: 0.2
@@ -41,6 +43,10 @@ Singleton {
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, "'");
+ }
+
function fuzzyQuery(search: string): var {
if (search.trim() === "") {
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 `
${lines.join("
")}
`;
+ }
+
+ 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 += `${escapeHtml(line.slice(i))}`;
+ break;
+ }
+
+ // Shell/Python-style comment
+ if (ch === "#" && i === 0) {
+ out += `${escapeHtml(line.slice(i))}`;
+ 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 += `${escapeHtml(line.slice(i, j))}`;
+ 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, `$1`);
+ 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 {
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy && wl-paste`]);
}
@@ -73,6 +188,7 @@ Singleton {
return;
}
+ previewImageSource = "";
previewIsImage = entryIsImage(currentEntry);
if (previewIsImage) {
diff --git a/Modules/Clipboard/Content.qml b/Modules/Clipboard/Content.qml
index b07dbe2..a9b9b18 100644
--- a/Modules/Clipboard/Content.qml
+++ b/Modules/Clipboard/Content.qml
@@ -16,7 +16,7 @@ Item {
required property PersistentProperties visibilities
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: {
if (ClipHistory.entries.length === 0)
@@ -69,23 +69,33 @@ Item {
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
+ 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
+ Behavior on implicitWidth {
+ Anim {
+ }
+ }
+
CustomText {
+ id: textPreview
+
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.top: parent.top
- text: ClipHistory.previewText
- textFormat: Text.PlainText
+ font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
+ text: ClipHistory.previewMarkup
+ textFormat: ClipHistory.previewIsCode ? Text.RichText : Text.PlainText
visible: !ClipHistory.previewIsImage
- width: preview.width - Appearance.padding.large * 2
+ width: Config.clipboard.sizes.previewWidth - anchors.margins * 2
wrapMode: Text.Wrap
}
Image {
+ id: imagePreview
+
anchors.fill: parent
anchors.margins: Appearance.padding.large
asynchronous: true
@@ -95,7 +105,7 @@ Item {
retainWhileLoading: true
smooth: true
source: ClipHistory.previewImageSource
- visible: ClipHistory.previewIsImage
+ visible: ClipHistory.previewIsImage && ClipHistory.previewImageSource !== ""
}
}
diff --git a/Modules/Settings/Controls/SettingsPage.qml b/Modules/Settings/Controls/SettingsPage.qml
index 7aa54ed..b3cae9c 100644
--- a/Modules/Settings/Controls/SettingsPage.qml
+++ b/Modules/Settings/Controls/SettingsPage.qml
@@ -25,7 +25,7 @@ Item {
CustomClippingWrapperRect {
anchors.fill: parent
- child: flickable.contentItem
+ child: flickable
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
}