import QtQuick import QtQuick.Layouts import Quickshell import ZShell.Config import ZShell.Llm import qs.Components import qs.Services CustomClippingRect { id: root required property string language required property string code property bool copied: false property color codeColor: Colors.palette.m3onSurfaceVariant property color codeBackgroundColor: Colors.palette.m3surfaceContainerLow property color codeHeaderColor: Colors.palette.m3outline property color codeAccentColor: Colors.palette.m3primary // Highlighter spans for the current code; refreshed when the code or // its language changes. property var codeSpans: [] function refresh() { codeSpans = CodeHighlighter.highlight(root.code, root.language); } function roleColor(kind) { switch (kind) { case "comment": return Colors.palette.m3outline; case "string": return Colors.palette.m3tertiary; case "string.key": return Colors.palette.m3secondary; case "number": case "constant": return Colors.palette.m3tertiaryFixed; case "keyword": return root.codeAccentColor; case "type": return Colors.palette.m3secondary; case "function": case "method": return Colors.palette.m3onSurface; case "macro": case "preproc": return Colors.palette.m3secondaryContainer; case "operator": case "property": return root.codeColor; case "label": case "attribute": return Colors.palette.m3tertiaryFixedDim; default: return root.codeColor; } } function escapeHtml(text) { return text.replace(/&/g, "&").replace(//g, ">"); } // Wraps the code in tags following the highlighter spans. function highlightedHtml(code, spans) { let out; if (!spans.length) { out = escapeHtml(code); } else { out = ""; let pos = 0; for (let i = 0; i < spans.length; i++) { const span = spans[i]; if (span.start > pos) out += escapeHtml(code.slice(pos, span.start)); out += `` + escapeHtml(code.slice(span.start, span.start + span.length)) + ""; pos = span.start + span.length; } if (pos < code.length) out += escapeHtml(code.slice(pos)); } // RichText collapses HTML whitespace: break newlines, and protect // line-leading indentation with nbsp (internal spaces stay normal // so long lines can still wrap). return out .replace(/(^|\n)[ \t]+/g, ws => ws.replace(/[ \t]/g, " ")) .replace(/\n/g, "
"); } implicitWidth: headerRow.implicitWidth + headerRow.anchors.leftMargin + headerRow.anchors.rightMargin implicitHeight: headerRow.anchors.topMargin + headerRow.implicitHeight + codeText.implicitHeight + codeText.anchors.margins * 2 color: root.codeBackgroundColor radius: Tokens.rounding.medium onLanguageChanged: refresh() onCodeChanged: refresh() CustomText { id: codeText anchors.left: parent.left anchors.top: headerRow.bottom anchors.right: parent.right anchors.margins: Tokens.padding.small color: root.codeColor font.family: Config.appearance.font.family.mono font.pointSize: Tokens.font.size.small textFormat: Text.RichText text: root.highlightedHtml(root.code, root.codeSpans) wrapMode: Text.WrapAtWordBoundaryOrAnywhere } RowLayout { id: headerRow anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.topMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small spacing: Tokens.spacing.small CustomText { Layout.alignment: Qt.AlignVCenter color: root.codeHeaderColor font.pointSize: Tokens.font.size.small text: root.language visible: root.language.length > 0 } Item { Layout.fillWidth: true } IconButton { icon: root.copied ? "check" : "content_copy" inactiveColor: "transparent" inactiveOnColor: root.codeHeaderColor type: IconButton.Text onClicked: { Quickshell.clipboardText = root.code; root.copied = true; copyResetTimer.restart(); } Timer { id: copyResetTimer interval: 1500 onTriggered: root.copied = false } } } }