import QtQuick import QtQuick.Effects import QtQuick.Shapes import QtQuick.Layouts import Quickshell import ZShell.Config import ZShell.Llm import qs.Components import qs.Services CustomRect { id: root required property string language required property string code property bool copied: false property color codeBackgroundColor: Colors.palette.m3surfaceContainerHigh property color codeHeaderColor: Colors.palette.m3outline property var codeSpans: [] property int highlightToken: 0 function refresh() { const token = ++root.highlightToken; CodeHighlighter.highlight(root.code, root.language, root, token); } function onHighlightSpans(token, spans) { if (token !== root.highlightToken) return; codeSpans = spans; } function roleColor(kind) { var s = CodeColors.active; switch (kind) { case "comment": return s.comment; case "string": return s.string; case "string.key": return s.stringKey; case "number": case "constant": return s.number; case "keyword": return s.keyword; case "type": return s.type; case "function": return s.functions; case "method": return s.method ?? s.functions; case "macro": return s.macro; case "preproc": return s.preproc ?? s.macro; case "operator": return s.operator ?? s.normal; case "property": return s.property ?? s.normal; case "label": return s.label; case "attribute": return s.attribute ?? s.label; default: return s.normal; } } function escapeHtml(text): string { return text.replace(/&/g, "&").replace(//g, ">"); } function highlightedHtml(code, spans): string { 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]; const start = Math.min(span.start, code.length); const end = Math.min(span.start + span.length, code.length); if (end <= pos) continue; if (start > pos) out += escapeHtml(code.slice(pos, start)); out += `` + escapeHtml(code.slice(start, end)) + ""; pos = end; } if (pos < code.length) out += escapeHtml(code.slice(pos)); } 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 + codeRect.implicitHeight + codeRect.anchors.margins + codeRect.anchors.topMargin color: root.codeBackgroundColor radius: Tokens.rounding.medium onLanguageChanged: { codeSpans = []; refresh(); } onCodeChanged: refresh() 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 Item { id: iconItem readonly property string iconPath: CodeIcons.path(root.language) Layout.fillHeight: true implicitWidth: height Shape { id: shape anchors.centerIn: parent visible: iconItem.iconPath !== "" preferredRendererType: Shape.CurveRenderer scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width) ShapePath { strokeColor: "transparent" fillColor: Colors.palette.m3tertiary fillRule: ShapePath.WindingFill PathSvg { path: iconItem.iconPath } } } Shape { id: fallbackShape anchors.centerIn: parent anchors.verticalCenterOffset: -1 visible: iconItem.iconPath === "" preferredRendererType: Shape.CurveRenderer scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width) ShapePath { strokeColor: Colors.palette.m3tertiary strokeWidth: 16 capStyle: ShapePath.RoundCap joinStyle: ShapePath.RoundJoin fillColor: "transparent" PathSvg { path: "M 40 64 L 112 128 L 40 192" } } ShapePath { strokeColor: Colors.palette.m3tertiary strokeWidth: 16 capStyle: ShapePath.RoundCap fillColor: "transparent" PathSvg { path: "M 120 192 L 216 192" } } } } CustomText { id: langText Layout.alignment: Qt.AlignVCenter color: root.codeHeaderColor font.pointSize: Tokens.font.size.small text: CodeIcons.name(root.language) visible: root.language.length > 0 } Item { Layout.fillWidth: true } IconButton { icon: root.copied ? "check" : "content_copy" inactiveColor: "transparent" inactiveOnColor: root.codeHeaderColor label.animate: true type: IconButton.Text onClicked: { Quickshell.clipboardText = root.code; root.copied = true; copyResetTimer.restart(); } Timer { id: copyResetTimer interval: 1500 onTriggered: root.copied = false } } } CustomClippingRect { id: codeRect anchors.left: parent.left anchors.top: headerRow.bottom anchors.right: parent.right anchors.bottom: parent.bottom anchors.topMargin: Tokens.padding.small radius: root.radius - anchors.margins anchors.margins: Tokens.padding.extraSmall implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2 implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2 color: CodeColors.active.bg CustomText { id: code anchors.top: parent.top anchors.bottom: parent.bottom x: implicitWidth * (0 - codeFlick.visibleArea.xPosition) + Tokens.padding.small anchors.margins: Tokens.padding.small text: root.highlightedHtml(root.code, root.codeSpans) textFormat: Text.RichText color: CodeColors.active.normal layer.enabled: true clip: false font.family: Config.appearance.font.family.mono font.pointSize: Tokens.font.size.small } Flickable { id: codeFlick anchors.fill: parent anchors.margins: Tokens.padding.small CustomScrollBar.horizontal: CustomScrollBar { flickable: codeFlick parent: codeFlick.parent anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right } TextAreaBase.flickable: TextAreaBase { id: codeText color: CodeColors.active.normal layer.enabled: true leftInset: Tokens.padding.small clip: false font.family: Config.appearance.font.family.mono font.pointSize: Tokens.font.size.small textFormat: Text.RichText text: code.text readOnly: true } } } }