clipboard preview leading tab trimming while preserving indentation + proper width calculations
Python / lint-format (pull_request) Successful in 27s
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 30s
Python / test (pull_request) Successful in 32s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s

This commit is contained in:
2026-06-15 18:23:56 +02:00
parent 2034f3a8db
commit 578a10c950
7 changed files with 136 additions and 135 deletions
+1 -1
View File
@@ -91,7 +91,7 @@ Slider {
MaterialIcon {
id: inset
readonly property bool attached: root.pos < 0.1
readonly property bool attached: root.filledWidth < (inset.paintedWidth + Appearance.spacing.extraSmall * 2)
property real dockT: attached ? 1 : 0
anchors.bottom: parent.bottom
+30 -114
View File
@@ -20,10 +20,8 @@ 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 list<var> previewText: []
property int previewToken: 0
property real scoreThreshold: 0.2
@@ -59,119 +57,37 @@ 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 === "" ? "&nbsp;" : 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`]);
}
function processPreviewLines(text: string): var {
if (text === "")
return [];
const lines = text.split("\n");
let minIndent = Infinity;
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^(\s*)/);
const indent = m ? m[1].length : 0;
if (lines[i].trim().length > 0 && indent < minIndent)
minIndent = indent;
}
if (minIndent === Infinity)
minIndent = 0;
const result = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const indentMatch = line.match(/^(\s*)/);
const indentLen = indentMatch ? indentMatch[1].length : 0;
const stripped = line.slice(Math.min(minIndent, indentLen));
result.push({
line: i + 1,
text: stripped
});
}
return result;
}
function refresh(): void {
readProc.buffer = [];
readProc.running = true;
@@ -182,7 +98,7 @@ Singleton {
const token = previewToken;
if (!currentEntry) {
previewText = "";
previewText = [];
previewImageSource = "";
previewIsImage = false;
return;
@@ -269,7 +185,7 @@ Singleton {
onStreamFinished: {
if (previewTextProc.token !== root.previewToken)
return;
root.previewText = this.text;
root.previewText = root.processPreviewLines(this.text);
}
}
}
+97 -9
View File
@@ -71,7 +71,7 @@ Item {
anchors.leftMargin: Appearance.spacing.normal
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Appearance.padding.large * 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)
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Appearance.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
radius: 25
Behavior on implicitWidth {
@@ -79,18 +79,105 @@ Item {
}
}
CustomText {
id: textPreview
Column {
id: textPreviewColumn
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.leftMargin: 0
anchors.margins: Appearance.padding.normal
anchors.top: parent.top
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: Config.clipboard.sizes.previewWidth - anchors.margins * 2
wrapMode: Text.Wrap
Repeater {
id: processedLines
model: ClipHistory.previewText
Row {
id: lineRow
required property int index
required property var modelData
spacing: Appearance.spacing.normal
CustomRect {
color: lineRow.index % 2 ? DynamicColors.tPalette.m3surfaceContainer : "transparent"
implicitHeight: lineText.paintedHeight + Appearance.padding.extraSmall * 2
implicitWidth: 50
CustomText {
id: number
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: lineRow.modelData.line
}
}
Repeater {
model: lineRow.modelData.text.split("\t").slice(1)
RowLayout {
height: lineText.height
width: lineText.height + Appearance.spacing.extraSmall
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
}
}
CustomText {
id: lineText
color: DynamicColors.palette.m3onSurface
font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
height: lineText.paintedHeight + Appearance.padding.extraSmall * 2
text: lineRow.modelData.text.trim()
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
}
}
Image {
@@ -258,6 +345,7 @@ Item {
ClipHistory.currentEntry = currentItem.modelData;
ClipHistory.refreshPreview();
}
onVisibleChanged: currentIndex = 0
CustomClippingWrapperRect {
anchors.fill: parent
-1
View File
@@ -32,7 +32,6 @@ Item {
active: root.shouldBeActive || root.visible
anchors.centerIn: parent
asynchronous: true
sourceComponent: Content {
screen: root.screen
+1 -2
View File
@@ -181,7 +181,7 @@ Item {
}
}
CustomRect {
CustomClippingRect {
id: categoryContent
anchors.bottom: parent.bottom
@@ -197,7 +197,6 @@ Item {
id: stack
anchors.fill: parent
anchors.margins: Appearance.padding.extraSmall
initialItem: general
popEnter: Transition {
+6 -7
View File
@@ -23,18 +23,15 @@ Item {
return false;
}
CustomClippingWrapperRect {
anchors.fill: parent
child: flickable
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
}
CustomFlickable {
id: flickable
anchors.fill: parent
// for future:
// anchors.leftMargin: Appearance.padding.extraLarge
// anchors.rightMargin: Appearance.padding.extraLarge
clip: true
contentHeight: clayout.implicitHeight
contentHeight: clayout.implicitHeight + clayout.anchors.margins * 2
CustomScrollBar.vertical: CustomScrollBar {
flickable: flickable
@@ -64,7 +61,9 @@ Item {
id: clayout
anchors.left: parent.left
anchors.margins: Appearance.padding.extraSmall
anchors.right: parent.right
anchors.top: parent.top
spacing: Appearance.spacing.small
// move: Transition {
@@ -14,7 +14,7 @@ CustomRect {
anchors.right: parent.right
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.height + contentPadding * 2
radius: Appearance.rounding.normal - Appearance.padding.smaller
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
Behavior on implicitHeight {
Anim {