forked from Shinonome/dots-hyprland
code block syntax highlighting
This commit is contained in:
@@ -11,6 +11,7 @@ Singleton {
|
|||||||
property QtObject rounding
|
property QtObject rounding
|
||||||
property QtObject font
|
property QtObject font
|
||||||
property QtObject sizes
|
property QtObject sizes
|
||||||
|
property string syntaxHighlightingTheme
|
||||||
|
|
||||||
function mix(color1, color2, percentage) {
|
function mix(color1, color2, percentage) {
|
||||||
var c1 = Qt.color(color1);
|
var c1 = Qt.color(color1);
|
||||||
@@ -238,4 +239,5 @@ Singleton {
|
|||||||
property int fabHoveredShadowRadius: 7
|
property int fabHoveredShadowRadius: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syntaxHighlightingTheme: Appearance.m3colors.darkmode ? "Monokai" : "ayu Light"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,35 @@
|
|||||||
function format(str, ...args) {
|
function format(str, ...args) {
|
||||||
return str.replace(/{(\d+)}/g, (match, index) =>
|
return str.replace(/{(\d+)}/g, (match, index) =>
|
||||||
typeof args[index] !== 'undefined' ? args[index] : match
|
typeof args[index] !== 'undefined' ? args[index] : match
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDomain(url) {
|
function getDomain(url) {
|
||||||
const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
|
const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
|
||||||
return match ? match[1] : null;
|
return match ? match[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shellSingleQuoteEscape(str) {
|
function shellSingleQuoteEscape(str) {
|
||||||
// First escape backslashes, then escape single quotes
|
// First escape backslashes, then escape single quotes
|
||||||
return String(str)
|
return String(str)
|
||||||
.replace(/\\/g, '\\\\')
|
.replace(/\\/g, '\\\\')
|
||||||
.replace(/'/g, "'\\''");
|
.replace(/'/g, "'\\''");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitMarkdownBlocks(markdown) {
|
||||||
|
const regex = /```(\w+)?\n([\s\S]*?)```/g;
|
||||||
|
let result = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(markdown)) !== null) {
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
result.push({ type: "text", content: markdown.slice(lastIndex, match.index) });
|
||||||
|
}
|
||||||
|
result.push({ type: "code", lang: match[1] || "", content: match[2] });
|
||||||
|
lastIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
if (lastIndex < markdown.length) {
|
||||||
|
result.push({ type: "text", content: markdown.slice(lastIndex) });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ Item {
|
|||||||
+ "- **Bold**, *Italic*, `Monospace`, [Link](https://example.com)\n\n"
|
+ "- **Bold**, *Italic*, `Monospace`, [Link](https://example.com)\n\n"
|
||||||
+ "- Table:\n\n"
|
+ "- Table:\n\n"
|
||||||
+ "| | Quickshell | AGS/Astal |\n"
|
+ "| | Quickshell | AGS/Astal |\n"
|
||||||
+ "|:-------------------------|:----------------:|:-----------------:|\n"
|
+ "|--------------------------|------------------|-------------------|\n"
|
||||||
+ "| UI Toolkit | Qt | Gtk3/Gtk4 |\n"
|
+ "| UI Toolkit | Qt | Gtk3/Gtk4 |\n"
|
||||||
+ "| Language | QML | Js/Ts/Lua |\n"
|
+ "| Language | QML | Js/Ts/Lua |\n"
|
||||||
+ "| Reactivity | Implied | Needs declaration |\n"
|
+ "| Reactivity | Implied | Needs declaration |\n"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import Quickshell.Widgets
|
|||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import Qt5Compat.GraphicalEffects
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import org.kde.syntaxhighlighting
|
||||||
|
// import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
@@ -22,6 +24,8 @@ Rectangle {
|
|||||||
|
|
||||||
property real messagePadding: 7
|
property real messagePadding: 7
|
||||||
property real contentSpacing: 3
|
property real contentSpacing: 3
|
||||||
|
property real codeBlockBackgroundRounding: Appearance.rounding.small
|
||||||
|
property real codeBlockComponentSpacing: 2
|
||||||
|
|
||||||
property bool renderMarkdown: true
|
property bool renderMarkdown: true
|
||||||
property bool editing: false
|
property bool editing: false
|
||||||
@@ -149,7 +153,25 @@ Rectangle {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
root.editing = !root.editing
|
root.editing = !root.editing
|
||||||
if (!root.editing) { // Save changes
|
if (!root.editing) { // Save changes
|
||||||
root.messageData.content = messageText.text
|
// Get all Loader children (each represents a segment)
|
||||||
|
const segments = messageContentColumnLayout.children
|
||||||
|
.map(child => child.segment)
|
||||||
|
.filter(segment => (segment));
|
||||||
|
// console.log("Segments: " + JSON.stringify(segments))
|
||||||
|
|
||||||
|
// Reconstruct markdown
|
||||||
|
const newContent = segments.map(segment => {
|
||||||
|
if (segment.type === "code") {
|
||||||
|
const lang = segment.lang ? segment.lang : "";
|
||||||
|
// Remove trailing newlines
|
||||||
|
const code = segment.content.replace(/\n+$/, "");
|
||||||
|
return "```" + lang + "\n" + code + "\n```";
|
||||||
|
} else {
|
||||||
|
return segment.content;
|
||||||
|
}
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
root.messageData.content = newContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StyledToolTip {
|
StyledToolTip {
|
||||||
@@ -159,15 +181,12 @@ Rectangle {
|
|||||||
AiMessageControlButton {
|
AiMessageControlButton {
|
||||||
id: toggleMarkdownButton
|
id: toggleMarkdownButton
|
||||||
activated: !root.renderMarkdown
|
activated: !root.renderMarkdown
|
||||||
buttonIcon: root.renderMarkdown ? "wysiwyg" : "code"
|
buttonIcon: "code"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.renderMarkdown = !root.renderMarkdown
|
root.renderMarkdown = !root.renderMarkdown
|
||||||
if (root.renderMarkdown && messageData.finished) {
|
|
||||||
messageText.text = root.messageData.content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
StyledToolTip {
|
StyledToolTip {
|
||||||
content: qsTr("Toggle Markdown rendering")
|
content: qsTr("View Markdown source")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AiMessageControlButton {
|
AiMessageControlButton {
|
||||||
@@ -183,45 +202,214 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEdit { // Message
|
ColumnLayout {
|
||||||
id: messageText
|
id: messageContentColumnLayout
|
||||||
Layout.fillWidth: true
|
Repeater {
|
||||||
Layout.margins: messagePadding
|
model: ScriptModel {
|
||||||
readOnly: !root.editing
|
values: {
|
||||||
selectByMouse: true
|
const result = StringUtils.splitMarkdownBlocks(root.messageData.content)
|
||||||
|
// console.log(JSON.stringify(result))
|
||||||
renderType: Text.NativeRendering
|
return result
|
||||||
font.family: Appearance.font.family.reading
|
}
|
||||||
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
|
||||||
font.pixelSize: Appearance.font.pixelSize.small
|
|
||||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
|
||||||
selectionColor: Appearance.m3colors.m3secondaryContainer
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
|
||||||
textFormat: root.renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
|
|
||||||
text: messageData.thinking ? qsTr("Waiting for response...") : root.messageData.content
|
|
||||||
|
|
||||||
Keys.onPressed: (event) => {
|
|
||||||
if (event.key === Qt.Key_Control) { // Prevent de-select
|
|
||||||
event.accepted = true
|
|
||||||
}
|
}
|
||||||
if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
delegate: Loader {
|
||||||
messageText.copy()
|
Layout.fillWidth: true
|
||||||
event.accepted = true
|
property var segment: modelData
|
||||||
|
sourceComponent: modelData.type === "code" ? codeBlockComponent : textBlockComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLinkActivated: (link) => {
|
|
||||||
Qt.openUrlExternally(link)
|
|
||||||
Hyprland.dispatch("global quickshell:sidebarLeftClose")
|
|
||||||
}
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.NoButton // Only for hover
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component { // Text block
|
||||||
|
id: textBlockComponent
|
||||||
|
TextArea {
|
||||||
|
readOnly: !root.editing
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.family: Appearance.font.family.reading
|
||||||
|
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||||
|
selectionColor: Appearance.m3colors.m3secondaryContainer
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
||||||
|
textFormat: root.renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
|
||||||
|
text: messageData.thinking ? qsTr("Waiting for response...") : segment.content
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
segment.content = text
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key === Qt.Key_Control || event.key == Qt.Key_Shift || event.key == Qt.Key_Alt || event.key == Qt.Key_Meta) { // Prevent de-select
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
||||||
|
messageText.copy()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLinkActivated: (link) => {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
Hyprland.dispatch("global quickshell:sidebarLeftClose")
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea { // Pointing hand for links
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.NoButton // Only for hover
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component { // Code block
|
||||||
|
id: codeBlockComponent
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: codeBlockComponentSpacing
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Rectangle { // Code background
|
||||||
|
Layout.fillWidth: true
|
||||||
|
topLeftRadius: codeBlockBackgroundRounding
|
||||||
|
topRightRadius: codeBlockBackgroundRounding
|
||||||
|
bottomLeftRadius: Appearance.rounding.unsharpen
|
||||||
|
bottomRightRadius: Appearance.rounding.unsharpen
|
||||||
|
color: Appearance.m3colors.m3surfaceContainerHighest
|
||||||
|
implicitHeight: codeBlockTitleBarRowLayout.implicitHeight
|
||||||
|
|
||||||
|
RowLayout { // Language and buttons
|
||||||
|
id: codeBlockTitleBarRowLayout
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: codeBlockLanguage
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
Layout.fillWidth: false
|
||||||
|
Layout.topMargin: 7
|
||||||
|
Layout.bottomMargin: 7
|
||||||
|
Layout.leftMargin: 10
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
color: Appearance.colors.colOnLayer2
|
||||||
|
text: segment.lang ? Repository.definitionForName(segment.lang).name : "plain"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout { // Line numbers and code
|
||||||
|
spacing: codeBlockComponentSpacing
|
||||||
|
|
||||||
|
Rectangle { // Line numbers
|
||||||
|
implicitWidth: 40
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: false
|
||||||
|
topLeftRadius: Appearance.rounding.unsharpen
|
||||||
|
bottomLeftRadius: codeBlockBackgroundRounding
|
||||||
|
topRightRadius: Appearance.rounding.unsharpen
|
||||||
|
bottomRightRadius: Appearance.rounding.unsharpen
|
||||||
|
color: Appearance.colors.colLayer2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: lineNumberColumnLayout
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 5
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: codeTextArea.text.split("\n").length
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
font.family: Appearance.font.family.monospace
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { // Code background
|
||||||
|
Layout.fillWidth: true
|
||||||
|
topLeftRadius: Appearance.rounding.unsharpen
|
||||||
|
bottomLeftRadius: Appearance.rounding.unsharpen
|
||||||
|
topRightRadius: Appearance.rounding.unsharpen
|
||||||
|
bottomRightRadius: codeBlockBackgroundRounding
|
||||||
|
color: Appearance.colors.colLayer2
|
||||||
|
// implicitWidth: codeTextArea.implicitWidth
|
||||||
|
implicitHeight: codeTextArea.implicitHeight
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: codeScrollView
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
implicitWidth: parent.width
|
||||||
|
implicitHeight: codeTextArea.contentHeight
|
||||||
|
contentWidth: codeTextArea.contentWidth
|
||||||
|
contentHeight: codeTextArea.contentHeight
|
||||||
|
clip: true
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
|
||||||
|
|
||||||
|
TextArea { // Code
|
||||||
|
|
||||||
|
id: codeTextArea
|
||||||
|
Layout.fillWidth: true
|
||||||
|
readOnly: !root.editing
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
font.family: Appearance.font.family.monospace
|
||||||
|
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.small
|
||||||
|
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||||
|
selectionColor: Appearance.m3colors.m3secondaryContainer
|
||||||
|
// wrapMode: TextEdit.Wrap
|
||||||
|
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
||||||
|
|
||||||
|
text: segment.content
|
||||||
|
onTextChanged: {
|
||||||
|
segment.content = text
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: (event) => {
|
||||||
|
if (event.key === Qt.Key_Tab) {
|
||||||
|
// Insert 4 spaces at cursor
|
||||||
|
const cursor = codeTextArea.cursorPosition;
|
||||||
|
codeTextArea.insert(cursor, " ");
|
||||||
|
codeTextArea.cursorPosition = cursor + 4;
|
||||||
|
event.accepted = true;
|
||||||
|
} else if (
|
||||||
|
event.key === Qt.Key_Control ||
|
||||||
|
event.key == Qt.Key_Shift ||
|
||||||
|
event.key == Qt.Key_Alt ||
|
||||||
|
event.key == Qt.Key_Meta
|
||||||
|
) {
|
||||||
|
event.accepted = true;
|
||||||
|
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
||||||
|
messageText.copy();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SyntaxHighlighter {
|
||||||
|
id: highlighter
|
||||||
|
textEdit: codeTextArea
|
||||||
|
repository: Repository
|
||||||
|
definition: Repository.definitionForName(segment.lang || "plaintext")
|
||||||
|
// definition: Repository.definitionForName("cpp")
|
||||||
|
theme: Appearance.syntaxHighlightingTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ Singleton {
|
|||||||
Appearance.m3colors[m3Key] = json[key]
|
Appearance.m3colors[m3Key] = json[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Appearance.m3colors.darkmode = (Appearance.m3colors.m3background.hslLightness < 0.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
## Not ready, but feel free to try it. It's simple:
|
## Not ready, but feel free to try it. It's simple:
|
||||||
|
|
||||||
- **Assumption**: You are already using the AGS illogical-impulse
|
- **Assumption**: You are already using the AGS illogical-impulse
|
||||||
- **Install Qt packages** (idk which are actually needed so this is everything I have): `qt5-base qt5-declarative qt5-graphicaleffects qt5-imageformats qt5-quickcontrols qt5-quickcontrols2 qt5-svg qt5-translations qt5-wayland qt5-x11extras qt6-5compat qt6-base qt6-declarative qt6-imageformats qt6-multimedia qt6-positioning qt6-quicktimeline qt6-sensors qt6-svg qt6-tools qt6-translations qt6-virtualkeyboard qt6-wayland qt6-webchannel qt6-webengine qt6-websockets qt6-webview`
|
- **Install Qt packages** (idk which are actually needed so this is everything I have): `qt5-base qt5-declarative qt5-graphicaleffects qt5-imageformats qt5-quickcontrols qt5-quickcontrols2 qt5-svg qt5-translations qt5-wayland qt5-x11extras qt6-5compat qt6-base qt6-declarative qt6-imageformats qt6-multimedia qt6-positioning qt6-quicktimeline qt6-sensors qt6-svg qt6-tools qt6-translations qt6-virtualkeyboard qt6-wayland qt6-webchannel qt6-webengine qt6-websockets qt6-webview syntax-highlighting`
|
||||||
- **Install quickshell and more stuff**: `yay -S quickshell matugen-bin grimblast`
|
- **Install quickshell and more stuff**: `yay -S quickshell matugen-bin grimblast`
|
||||||
- **Copy** `.config/quickshell` folder and hyprland config files in `.config/hypr/hyprland/` (backing up is your responsibility)
|
- **Copy** `.config/quickshell` folder and hyprland config files in `.config/hypr/hyprland/` (backing up is your responsibility)
|
||||||
- **Run quickshell** with `qs` and see how things are - it's not finished for daily use, but **feedback is very welcome**
|
- **Run quickshell** with `qs` and see how things are - it's not finished for daily use, but **feedback is very welcome**
|
||||||
|
|||||||
Reference in New Issue
Block a user