diff --git a/.qmlformat.ini b/.qmlformat.ini
index 405a8d1..828d0c9 100644
--- a/.qmlformat.ini
+++ b/.qmlformat.ini
@@ -3,8 +3,9 @@ FunctionsSpacing=true
IndentWidth=4
MaxColumnWidth=-1
NewlineType=native
-NormalizeOrder=true
+GroupAttributesTogether=false
ObjectsSpacing=true
SemicolonRule=always
+SingleLineEmptyObjects=true
SortImports=false
UseTabs=true
diff --git a/Components/CustomFlickable.qml b/Components/CustomFlickable.qml
index 249f093..556d439 100644
--- a/Components/CustomFlickable.qml
+++ b/Components/CustomFlickable.qml
@@ -28,6 +28,8 @@ Flickable {
interval: 10
running: root.doneFakeFlick
- onTriggered: root.doneFakeFlick = false
+ onTriggered: {
+ root.doneFakeFlick = false;
+ }
}
}
diff --git a/Components/CustomListView.qml b/Components/CustomListView.qml
index 0b8d618..84c6aba 100644
--- a/Components/CustomListView.qml
+++ b/Components/CustomListView.qml
@@ -10,24 +10,27 @@ ListView {
maximumFlickVelocity: 3000
rebound: Transition {
- onRunningChanged: {
- if (!running && !root.doneFakeFlick) {
- root.doneFakeFlick = true;
- root.flick(1, 1);
- root.flick(-1, -1);
- Qt.callLater(() => root.cancelFlick());
- }
- }
+ // onRunningChanged: {
+ // if (!running && !root.doneFakeFlick) {
+ // root.doneFakeFlick = true;
+ // root.flick(1, 1);
+ // root.flick(-1, -1);
+ // Qt.callLater(() => root.cancelFlick());
+ // }
+ // }
Anim {
properties: "x,y"
+ type: Anim.DefaultEffects
}
}
- Timer {
- interval: 10
- running: root.doneFakeFlick
-
- onTriggered: root.doneFakeFlick = false
- }
+ // Timer {
+ // interval: 10
+ // running: root.doneFakeFlick
+ //
+ // onTriggered: {
+ // root.doneFakeFlick = false;
+ // }
+ // }
}
diff --git a/Components/CustomMouseArea.qml b/Components/CustomMouseArea.qml
index 6d52d16..0737476 100644
--- a/Components/CustomMouseArea.qml
+++ b/Components/CustomMouseArea.qml
@@ -4,6 +4,7 @@ MouseArea {
property int scrollAccumulatedY: 0
function onWheel(event: WheelEvent): void {
+ event.accepted = false;
}
onWheel: event => {
diff --git a/Components/CustomScrollBar.qml b/Components/CustomScrollBar.qml
index 0159df0..6df6539 100644
--- a/Components/CustomScrollBar.qml
+++ b/Components/CustomScrollBar.qml
@@ -1,32 +1,37 @@
+pragma ComponentBehavior: Bound
+
import QtQuick
import QtQuick.Templates
import qs.Helpers
import ZShell.Config
-import qs.Services
ScrollBar {
id: root
+ required property Flickable flickable
+ readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root
+ readonly property real axisSize: isHorizontal ? flickable.width : flickable.height
+ readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight
+ readonly property real axisContentPos: isHorizontal ? (flickable.contentX - flickable.originX) : (flickable.contentY - flickable.originY)
+ readonly property real axisLength: isHorizontal ? root.width : root.height
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
- required property Flickable flickable
- readonly property real nonAnimHeight: flickable.height / flickable.contentHeight
- readonly property real nonAnimY: flickable.contentY / flickable.contentHeight
+ readonly property real nonAnimHeight: root.axisSize / root.axisContentSize
+ readonly property real nonAnimY: root.axisContentPos / root.axisContentSize
readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight)
- readonly property bool reversed: flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop
+ readonly property bool reversed: isHorizontal ? (flickable instanceof ListView && flickable.layoutDirection === Qt.RightToLeft) : (flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop)
property bool shouldBeActive
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
enabled: !Visibilities.getForActive().isDrawing
- implicitWidth: Tokens.padding.extraSmall * 2
+ implicitWidth: size === 1 ? 0 : isHorizontal ? 0 : Tokens.padding.extraSmall * 2
+ implicitHeight: size === 1 ? 0 : isHorizontal ? Tokens.padding.extraSmall * 2 : 0
- contentItem: Item {
- }
+ contentItem: Item {}
Behavior on position {
enabled: !fullMouse.pressed
- Anim {
- }
+ Anim {}
}
onHoveredChanged: {
@@ -47,54 +52,27 @@ ScrollBar {
target: root.flickable
}
- CustomClippingRect {
- anchors.bottom: parent.bottom
- anchors.right: parent.right
- anchors.top: parent.top
- implicitWidth: handle.implicitWidth
- radius: Tokens.rounding.full
+ Loader {
+ anchors.fill: parent
+ active: root.size < 1
+ sourceComponent: root.isHorizontal ? horizontalTrack : verticalTrack
+ }
- CustomRect {
- id: handle
+ Component {
+ id: verticalTrack
- anchors.right: parent.right
- color: Colors.palette.m3secondary
- implicitHeight: root.height * root.effectiveSize
- implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
- opacity: {
- if (!root.enabled)
- return 0;
- if (root.size === 1)
- return 0;
- if (fullMouse.pressed)
- return 1;
- if (fullMouse.containsMouse)
- return 0.8;
- if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
- return 0.6;
- return 0;
- }
- radius: Tokens.rounding.full
- y: root.reversed ? root.height * (1 + root.nonAnimY) * root.travelScale : root.height * root.nonAnimY * root.travelScale
+ VerticalScrollBarTrack {
+ scrollBar: root
+ mouseArea: fullMouse
+ }
+ }
- Behavior on implicitWidth {
- Anim {
- }
- }
- Behavior on opacity {
- Anim {
- type: Anim.DefaultEffects
- }
- }
+ Component {
+ id: horizontalTrack
- MouseArea {
- id: mouse
-
- acceptedButtons: Qt.NoButton
- anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
- hoverEnabled: true
- }
+ HorizontalScrollBarTrack {
+ scrollBar: root
+ mouseArea: fullMouse
}
}
@@ -111,25 +89,40 @@ ScrollBar {
property real pressOffset: 0
- function contentYFromThumbTop(thumbTop) {
- var visualPos = root.effectiveTravel > 0 ? thumbTop / root.travelScale : 0;
-
- return root.reversed ? (visualPos - 1) * root.flickable.contentHeight : visualPos * root.flickable.contentHeight;
+ function contentPosFromThumbStart(thumbStart) {
+ var visualPos = root.effectiveTravel > 0 ? thumbStart / root.travelScale : 0;
+ return root.reversed ? (visualPos - 1) * root.axisContentSize : visualPos * root.axisContentSize;
}
function updateFromEvent(event) {
- var posInTrack = event.y / root.height;
- var thumbTop = posInTrack - pressOffset;
- thumbTop = Math.max(0, Math.min(root.effectiveTravel, thumbTop));
+ var eventPos = root.isHorizontal ? event.x : event.y;
+ var posInTrack = eventPos / root.axisLength;
+ var thumbStart = posInTrack - pressOffset;
+ thumbStart = Math.max(0, Math.min(root.effectiveTravel, thumbStart));
- root.flickable.contentY = contentYFromThumbTop(thumbTop);
+ var newPos = contentPosFromThumbStart(thumbStart);
+ if (root.isHorizontal)
+ root.flickable.contentX = newPos + root.flickable.originX;
+ else
+ root.flickable.contentY = newPos + root.flickable.originY;
}
- function visualThumbTop() {
+ function visualThumbStart() {
const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY;
return visualPos * root.travelScale;
}
+ function onWheel(event: WheelEvent): void {
+ if (root.horizontal) {
+ event.accepted = false;
+ return;
+ }
+
+ var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
+ var newPos = Math.max(0, Math.min(1 - root.size, root.position + delta));
+ root.position = newPos;
+ }
+
anchors.fill: parent
cursorShape: undefined
hoverEnabled: true
@@ -140,24 +133,15 @@ ScrollBar {
updateFromEvent(event);
}
onPressed: event => {
- var currentTop = visualThumbTop();
- var currentBottom = currentTop + root.effectiveSize;
- var clickPos = event.y / root.height;
+ var currentStart = visualThumbStart();
+ var currentEnd = currentStart + root.effectiveSize;
+ var eventPos = root.isHorizontal ? event.x : event.y;
+ var clickPos = eventPos / root.axisLength;
- var clickedInsideThumb = clickPos >= currentTop && clickPos <= currentBottom;
-
- if (clickedInsideThumb) {
- pressOffset = clickPos - currentTop;
- } else {
- pressOffset = root.effectiveSize / 2;
- }
+ var clickedInsideThumb = clickPos >= currentStart && clickPos <= currentEnd;
+ pressOffset = clickedInsideThumb ? (clickPos - currentStart) : root.effectiveSize / 2;
updateFromEvent(event);
}
- onWheel: event => {
- var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
- var newPos = Math.max(0, Math.min(1 - root.size, root.position + delta));
- root.position = newPos;
- }
}
}
diff --git a/Components/HorizontalScrollBarTrack.qml b/Components/HorizontalScrollBarTrack.qml
new file mode 100644
index 0000000..92185c4
--- /dev/null
+++ b/Components/HorizontalScrollBarTrack.qml
@@ -0,0 +1,56 @@
+import QtQuick
+import ZShell.Config
+import qs.Services
+
+CustomClippingRect {
+ id: track
+
+ required property var scrollBar
+ required property var mouseArea
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ implicitHeight: handle.implicitHeight
+ radius: Tokens.rounding.full
+
+ CustomRect {
+ id: handle
+
+ anchors.bottom: parent.bottom
+ color: Colors.palette.m3secondary
+ implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
+ implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
+ opacity: {
+ if (!track.scrollBar.enabled)
+ return 0;
+ if (track.scrollBar.size === 1)
+ return 0;
+ if (track.mouseArea.pressed)
+ return 1;
+ if (track.mouseArea.containsMouse)
+ return 0.8;
+ if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
+ return 0.6;
+ return 0;
+ }
+ radius: Tokens.rounding.full
+ x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale
+
+ Behavior on implicitHeight {
+ Anim {}
+ }
+ Behavior on opacity {
+ Anim {
+ type: Anim.DefaultEffects
+ }
+ }
+
+ MouseArea {
+ acceptedButtons: Qt.NoButton
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ }
+ }
+}
diff --git a/Components/TextAreaBase.qml b/Components/TextAreaBase.qml
new file mode 100644
index 0000000..a96b9c0
--- /dev/null
+++ b/Components/TextAreaBase.qml
@@ -0,0 +1,86 @@
+import QtQuick
+import QtQuick.Templates
+import ZShell.Config
+import qs.Services
+
+TextArea {
+ id: root
+
+ color: Colors.palette.m3onSurface
+ cursorVisible: !readOnly
+ font.pointSize: Tokens.font.size.small
+ implicitHeight: contentHeight + topPadding + bottomPadding
+ implicitWidth: contentWidth + leftPadding + rightPadding
+ placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
+ renderType: TextArea.NativeRendering
+ selectedTextColor: color
+ selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
+ verticalAlignment: TextInput.AlignVCenter
+
+ Behavior on color {
+ CAnim {}
+ }
+ Behavior on selectionColor {
+ CAnim {}
+ }
+ cursorDelegate: Item {}
+
+ CustomRect {
+ id: cursor
+
+ property bool disableBlink
+
+ color: Colors.palette.m3primary
+ implicitHeight: root.cursorRectangle.height
+ implicitWidth: 1.5
+ radius: Tokens.rounding.largeIncreased
+ x: root.cursorRectangle.x
+ y: root.cursorRectangle.y
+
+ Behavior on x {
+ Anim {
+ duration: Tokens.anim.durations.expressiveFastEffects
+ easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
+ }
+ }
+ Behavior on opacity {
+ Anim {
+ type: Anim.StandardSmall
+ }
+ }
+
+ Connections {
+ function onCursorPositionChanged(): void {
+ if (root.activeFocus && root.cursorVisible) {
+ cursor.opacity = 1;
+ cursor.disableBlink = true;
+ enableBlink.restart();
+ }
+ }
+
+ target: root
+ }
+
+ Timer {
+ id: enableBlink
+
+ interval: 500
+
+ onTriggered: cursor.disableBlink = false
+ }
+
+ Timer {
+ interval: 500
+ repeat: true
+ running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
+ triggeredOnStart: true
+
+ onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
+ }
+
+ Binding {
+ cursor.opacity: 0
+ when: !root.activeFocus || !root.cursorVisible
+ }
+ }
+}
diff --git a/Components/TextEditBase.qml b/Components/TextEditBase.qml
new file mode 100644
index 0000000..8eb49a3
--- /dev/null
+++ b/Components/TextEditBase.qml
@@ -0,0 +1,100 @@
+import QtQuick
+import QtQuick.Templates
+import ZShell.Config
+import qs.Services
+
+TextEdit {
+ id: root
+
+ property bool animateCursor: true
+ property alias cursor: cursor
+
+ color: Colors.palette.m3onSurface
+ cursorVisible: !readOnly
+ font.pointSize: Tokens.font.size.small
+ font.family: Config.appearance.font.family.sans
+ renderType: TextField.NativeRendering
+ selectedTextColor: color
+ selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
+ verticalAlignment: TextInput.AlignVCenter
+
+ Behavior on color {
+ CAnim {
+ }
+ }
+ cursorDelegate: Item {
+ }
+ Behavior on selectionColor {
+ CAnim {
+ }
+ }
+
+ CustomRect {
+ id: cursor
+
+ property bool disableBlink
+
+ color: Colors.palette.m3primary
+ implicitHeight: root.cursorRectangle.height
+ implicitWidth: 1.5
+ radius: Tokens.rounding.largeIncreased
+ x: root.cursorRectangle.x
+ y: root.cursorRectangle.y
+
+ Behavior on opacity {
+ Anim {
+ type: Anim.StandardSmall
+ }
+ }
+ Behavior on x {
+ enabled: root.animateCursor
+
+ Anim {
+ duration: Tokens.anim.durations.expressiveFastEffects
+ easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
+ }
+ }
+ Behavior on y {
+ enabled: root.animateCursor
+
+ Anim {
+ duration: Tokens.anim.durations.expressiveFastEffects
+ easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
+ }
+ }
+
+ Connections {
+ function onCursorPositionChanged(): void {
+ if (root.activeFocus && root.cursorVisible) {
+ cursor.opacity = 1;
+ cursor.disableBlink = true;
+ enableBlink.restart();
+ }
+ }
+
+ target: root
+ }
+
+ Timer {
+ id: enableBlink
+
+ interval: 500
+
+ onTriggered: cursor.disableBlink = false
+ }
+
+ Timer {
+ interval: 500
+ repeat: true
+ running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
+ triggeredOnStart: true
+
+ onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
+ }
+
+ Binding {
+ cursor.opacity: 0
+ when: !root.activeFocus || !root.cursorVisible
+ }
+ }
+}
diff --git a/Components/VerticalFadeListView.qml b/Components/VerticalFadeListView.qml
index c563a0b..2430660 100644
--- a/Components/VerticalFadeListView.qml
+++ b/Components/VerticalFadeListView.qml
@@ -8,6 +8,7 @@ CustomListView {
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property real fadeAmount: 0.1
+ property real fadeThreshold: 0.0
readonly property bool horizontal: orientation === ListView.Horizontal
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
@@ -28,11 +29,11 @@ CustomListView {
}
function marginEnd(): real {
- return horizontal ? rightMargin : bottomMargin;
+ return horizontal ? rightMargin - fadeThreshold : bottomMargin - fadeThreshold;
}
function marginStart(): real {
- return horizontal ? leftMargin : topMargin;
+ return horizontal ? leftMargin - fadeThreshold : topMargin - fadeThreshold;
}
function overshootStart(): real {
diff --git a/Components/VerticalScrollBarTrack.qml b/Components/VerticalScrollBarTrack.qml
new file mode 100644
index 0000000..62e7b5a
--- /dev/null
+++ b/Components/VerticalScrollBarTrack.qml
@@ -0,0 +1,56 @@
+import QtQuick
+import ZShell.Config
+import qs.Services
+
+CustomClippingRect {
+ id: track
+
+ required property var scrollBar
+ required property var mouseArea
+
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.top: parent.top
+ implicitWidth: handle.implicitWidth
+ radius: Tokens.rounding.full
+
+ CustomRect {
+ id: handle
+
+ anchors.right: parent.right
+ color: Colors.palette.m3secondary
+ implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
+ implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
+ opacity: {
+ if (!track.scrollBar.enabled)
+ return 0;
+ if (track.scrollBar.size === 1)
+ return 0;
+ if (track.mouseArea.pressed)
+ return 1;
+ if (track.mouseArea.containsMouse)
+ return 0.8;
+ if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
+ return 0.6;
+ return 0;
+ }
+ radius: Tokens.rounding.full
+ y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
+
+ Behavior on implicitWidth {
+ Anim {}
+ }
+ Behavior on opacity {
+ Anim {
+ type: Anim.DefaultEffects
+ }
+ }
+
+ MouseArea {
+ acceptedButtons: Qt.NoButton
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ }
+ }
+}
diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml
index 96ca9f8..8dd15c3 100644
--- a/Drawers/Panels.qml
+++ b/Drawers/Panels.qml
@@ -140,7 +140,6 @@ Item {
anchors.bottom: parent.bottom
anchors.right: parent.right
- popouts: popouts
sidebar: sidebar
visibilities: root.visibilities
}
diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml
index bac1fbc..b2ecfb6 100644
--- a/Drawers/Windows.qml
+++ b/Drawers/Windows.qml
@@ -55,7 +55,7 @@ CustomWindow {
property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore
- WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
+ WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings || visibilities.sidebar ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent"
contentItem.focus: true
@@ -63,12 +63,10 @@ CustomWindow {
name: "Bar"
Behavior on fsTransitionProg {
- Anim {
- }
+ Anim {}
}
Behavior on surfaceColor {
- CAnim {
- }
+ CAnim {}
}
contentItem.Keys.onEscapePressed: {
@@ -293,8 +291,7 @@ CustomWindow {
y: panels.popoutsWrapper.y + panels.popouts.y + geometry.insetTop(root.borderThickness) - (geometry.barOnTop ? panels.popouts.height * extraExtent : 0)
Behavior on extraExtent {
- Anim {
- }
+ Anim {}
}
}
diff --git a/Greeter/Services/Colors.qml b/Greeter/Services/Colors.qml
index ee9d678..ac4dce6 100644
--- a/Greeter/Services/Colors.qml
+++ b/Greeter/Services/Colors.qml
@@ -13,21 +13,17 @@ import qs.Paths
Singleton {
id: root
- readonly property M3Palette current: M3Palette {
- }
+ readonly property M3Palette current: M3Palette {}
property bool currentLight
property string flavour
readonly property bool light: showPreview ? previewLight : currentLight
readonly property M3Palette palette: showPreview ? preview : current
- readonly property M3Palette preview: M3Palette {
- }
+ readonly property M3Palette preview: M3Palette {}
property bool previewLight
property string scheme
property bool showPreview
- readonly property M3TPalette tPalette: M3TPalette {
- }
- readonly property Transparency transparency: Transparency {
- }
+ readonly property M3TPalette tPalette: M3TPalette {}
+ readonly property Transparency transparency: Transparency {}
readonly property alias wallLuminance: analyser.luminance
function alterColor(c: color, a: real, layer: int): color {
@@ -112,8 +108,7 @@ Singleton {
function setMode(mode: string): void {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]);
- Config.general.color.mode = mode;
- Config.save();
+ Config.colors.mode = mode;
}
function swapRG(c: color): color {
diff --git a/Helpers/Hyprsunset.qml b/Helpers/Hyprsunset.qml
index ca82be2..29857f3 100644
--- a/Helpers/Hyprsunset.qml
+++ b/Helpers/Hyprsunset.qml
@@ -9,13 +9,13 @@ Singleton {
id: root
readonly property bool enabled: service.enabled
- readonly property int end: Config.general.color.scheduleHyprsunsetEnd
+ readonly property int end: Config.display.nightlight.scheduleEnd
property bool manualToggle: false
- readonly property int start: Config.general.color.scheduleHyprsunsetStart
- readonly property int temp: Config.general.color.hyprsunsetTemp
+ readonly property int start: Config.display.nightlight.scheduleStart
+ readonly property int temp: Config.display.nightlight.temp
function checkStartup(): void {
- if (!Config.general.color.scheduleHyprsunset)
+ if (!Config.display.nightlight.schedule)
return;
service.apply();
@@ -26,12 +26,14 @@ Singleton {
service.toggle();
}
- HyprsunsetManager {
+ NightlightManager {
id: service
- activeAuto: Config.general.color.scheduleHyprsunset
+ activeAuto: Config.display.nightlight.schedule
endTime: root.end
startTime: root.start
temp: root.temp
+ fadeDuration: Config.display.nightlight.fadeDuration / 1000
+ useNativeNightlight: Config.display.nightlight.useNative
}
}
diff --git a/Helpers/ModeScheduler.qml b/Helpers/ModeScheduler.qml
index a45e15a..14b6aa8 100644
--- a/Helpers/ModeScheduler.qml
+++ b/Helpers/ModeScheduler.qml
@@ -11,14 +11,14 @@ import qs.Services
Singleton {
id: root
- readonly property int darkEnd: Config.general.color.scheduleDarkEnd
- readonly property int darkStart: Config.general.color.scheduleDarkStart
- readonly property bool enabled: Config.general.color.scheduleDark && Config.general.color.schemeGeneration
+ readonly property int darkEnd: Config.colors.scheduleDarkEnd
+ readonly property int darkStart: Config.colors.scheduleDarkStart
+ readonly property bool enabled: Config.colors.scheduleDark && Config.colors.schemeGen
function applyDarkMode() {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "dark"]);
- Config.general.color.mode = "dark";
+ Config.colors.mode = "dark";
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-dark'"]);
@@ -30,13 +30,13 @@ Singleton {
function applyLightMode() {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "light"]);
- Config.general.color.mode = "light";
+ Config.colors.mode = "light";
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-light'"]);
Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Dark.colors/color_scheme_path=\\1Light.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]);
- if (Config.general.color.neovimColors)
+ if (Config.colors.neovimColors)
Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"onelight\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]);
}
diff --git a/Helpers/Picker.qml b/Helpers/Picker.qml
index 05b4146..b87f53e 100755
--- a/Helpers/Picker.qml
+++ b/Helpers/Picker.qml
@@ -126,33 +126,29 @@ Item {
focus: true
opacity: 0
- Behavior on opacity {
- Anim {
- duration: 300
- }
- }
Behavior on rsx {
enabled: !selectArea.pressed && root.mode === "select"
- ExAnim {
- }
+ ExAnim {}
}
Behavior on rsy {
enabled: !selectArea.pressed && root.mode === "select"
- ExAnim {
- }
+ ExAnim {}
}
Behavior on sh {
enabled: !selectArea.pressed && root.mode === "select"
- ExAnim {
- }
+ ExAnim {}
}
Behavior on sw {
enabled: !selectArea.pressed && root.mode === "select"
- ExAnim {
+ ExAnim {}
+ }
+ Behavior on opacity {
+ Anim {
+ duration: 300
}
}
@@ -352,8 +348,7 @@ Item {
y: selectionRect.y - root.realBorderWidth
Behavior on border.color {
- Anim {
- }
+ Anim {}
}
}
@@ -402,6 +397,5 @@ Item {
onUndoRequested: annotations.undo()
}
- component ExAnim: Anim {
- }
+ component ExAnim: Anim {}
}
diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml
index ba4b64c..d02b01a 100644
--- a/Helpers/Wallpapers.qml
+++ b/Helpers/Wallpapers.qml
@@ -38,7 +38,7 @@ Searcher {
previewPath = path;
showPreview = true;
- if (Config.general.color.schemeGeneration)
+ if (Config.colors.schemeGen)
previewColorsProc.running = true;
}
@@ -62,8 +62,8 @@ Searcher {
WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
- if (Config.general.color.schemeGeneration)
- Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
+ if (Config.colors.schemeGen)
+ Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
}
function stopPreview(): void {
diff --git a/Modules/Bar/Components/Clock.qml b/Modules/Bar/Components/Clock.qml
index e7de27d..dbe05b9 100644
--- a/Modules/Bar/Components/Clock.qml
+++ b/Modules/Bar/Components/Clock.qml
@@ -44,7 +44,7 @@ WidgetBase {
}
required property GridLayout loader
required property Wrapper popouts
- readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.small * 2 : verticalColumn.implicitHeight + Tokens.padding.small * 2
+ readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
required property PersistentProperties visibilities
color: visibilities.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
@@ -57,14 +57,13 @@ WidgetBase {
anchors.centerIn: parent
color: root.contentColor
- font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
+ font.family: Config.appearance.font.family.clock // qmllint disable incompatible-type
height: implicitHeight
text: Time.dateStr
visible: root.horizontal
Behavior on color {
- CAnim {
- }
+ CAnim {}
}
}
@@ -84,12 +83,11 @@ WidgetBase {
Layout.alignment: Qt.AlignHCenter
color: root.contentColor
- font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
+ font.family: Config.appearance.font.family.clock // qmllint disable incompatible-type
text: Qt.formatDateTime(Time.date, modelData)
Behavior on color {
- CAnim {
- }
+ CAnim {}
}
}
}
@@ -114,7 +112,7 @@ WidgetBase {
Layout.alignment: Qt.AlignHCenter
color: root.contentColor
- font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
+ font.family: Config.appearance.font.family.clock // qmllint disable incompatible-type
text: {
if (modelData.includes("h"))
return Time.hourStr;
@@ -126,8 +124,7 @@ WidgetBase {
}
Behavior on color {
- CAnim {
- }
+ CAnim {}
}
}
}
@@ -135,13 +132,12 @@ WidgetBase {
CustomText {
Layout.alignment: Qt.AlignHCenter
color: root.contentColor
- font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
+ font.family: Config.appearance.font.family.clock // qmllint disable incompatible-type
text: Qt.formatDateTime(Time.date, "AP")
visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0
Behavior on color {
- CAnim {
- }
+ CAnim {}
}
}
}
diff --git a/Modules/Notifications/Sidebar/Chat/ChatContent.qml b/Modules/Notifications/Sidebar/Chat/ChatContent.qml
new file mode 100644
index 0000000..85e2a6d
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/ChatContent.qml
@@ -0,0 +1,323 @@
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Components
+import ZShell.Config
+import ZShell.Llm
+import qs.Modules.Notifications.Sidebar.Chat.Content
+import qs.Components
+import qs.Services
+
+Item {
+ id: root
+
+ property ChatSession chatData
+ property bool following: true
+ // qmlformat off
+ readonly property var keys: [
+ Qt.Key_A,
+ Qt.Key_B,
+ Qt.Key_C,
+ Qt.Key_D,
+ Qt.Key_E,
+ Qt.Key_F,
+ Qt.Key_G,
+ Qt.Key_H,
+ Qt.Key_I,
+ Qt.Key_J,
+ Qt.Key_K,
+ Qt.Key_L,
+ Qt.Key_M,
+ Qt.Key_N,
+ Qt.Key_O,
+ Qt.Key_P,
+ Qt.Key_Q,
+ Qt.Key_R,
+ Qt.Key_S,
+ Qt.Key_T,
+ Qt.Key_U,
+ Qt.Key_V,
+ Qt.Key_W,
+ Qt.Key_X,
+ Qt.Key_Y,
+ Qt.Key_Z,
+
+ Qt.Key_Agrave,
+ Qt.Key_Aacute,
+ Qt.Key_Acircumflex,
+ Qt.Key_Atilde,
+ Qt.Key_Adiaeresis,
+ Qt.Key_Aring,
+ Qt.Key_AE,
+
+ Qt.Key_Ccedilla,
+
+ Qt.Key_Egrave,
+ Qt.Key_Eacute,
+ Qt.Key_Ecircumflex,
+ Qt.Key_Ediaeresis,
+
+ Qt.Key_Igrave,
+ Qt.Key_Iacute,
+ Qt.Key_Icircumflex,
+ Qt.Key_Idiaeresis,
+
+ Qt.Key_ETH,
+
+ Qt.Key_Ntilde,
+
+ Qt.Key_Ograve,
+ Qt.Key_Oacute,
+ Qt.Key_Ocircumflex,
+ Qt.Key_Otilde,
+ Qt.Key_Odiaeresis,
+ Qt.Key_Ooblique,
+
+ Qt.Key_Ugrave,
+ Qt.Key_Uacute,
+ Qt.Key_Ucircumflex,
+ Qt.Key_Udiaeresis,
+
+ Qt.Key_Yacute,
+ Qt.Key_ydiaeresis,
+
+ Qt.Key_THORN,
+ Qt.Key_ssharp
+ ]
+ // qmlformat on
+
+ signal requestClose
+
+ function send(text: string): void {
+ if (text.trim() === "")
+ return;
+ following = true;
+ chatData.sendMessage(text);
+ input.text = "";
+ }
+
+ function focusInput(): void {
+ Qt.callLater(() => input.forceActiveFocus());
+ }
+
+ onChatDataChanged: {
+ if (chatData)
+ focusInput();
+ }
+ Keys.onPressed: e => {
+ if (root.keys.includes(e.key)) {
+ input.insert(input.length, e.text);
+ focusInput();
+ }
+ }
+
+ RowLayout {
+ id: header
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ spacing: Tokens.spacing.large
+
+ IconButton {
+ icon: "arrow_back"
+ inactiveColor: Colors.tPalette.m3surfaceContainerHigh
+ inactiveOnColor: Colors.palette.m3onSurfaceVariant
+ isRound: true
+ type: IconButton.Tonal
+
+ enabled: !ChatState.isWindow
+ visible: enabled
+
+ onClicked: root.requestClose()
+ }
+
+ CustomText {
+ Layout.fillWidth: ChatState.isWindow ? false : true
+ Layout.alignment: ChatState.isWindow ? Qt.AlignCenter : Qt.AlignLeft | Qt.AlignVCenter
+ elide: Text.ElideRight
+ font.pointSize: Tokens.font.size.larger
+ text: qsTr(root.chatData.title)
+ }
+ }
+
+ Loader {
+ id: listLoader
+
+ anchors.bottom: input.top
+ anchors.bottomMargin: Tokens.spacing.medium
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: header.bottom
+ anchors.topMargin: Tokens.spacing.medium
+ active: root.chatData && root.chatData.loaded
+
+ sourceComponent: VerticalFadeListView {
+ id: list
+
+ property bool userScrolledUp: false
+ property real lastCHeight: 0.0
+
+ function scrollToBottom(): void {
+ scrollAnim.start();
+ }
+
+ cacheBuffer: Math.max(height * 20, 0)
+ clip: true
+ fadeAmount: 0.05
+ fadeThreshold: Tokens.padding.medium
+ model: root.chatData.messagesModel
+ spacing: 0
+ rotation: 180
+ add: Transition {
+ Anim {
+ from: -100
+ property: "y"
+ }
+ }
+
+ CustomScrollBar.vertical: CustomScrollBar {
+ id: scrollBar
+
+ flickable: list
+ parent: list.parent
+ anchors.top: list.top
+ anchors.right: list.right
+ anchors.bottom: list.bottom
+
+ transform: Rotation {
+ origin.y: list.height / 2
+ origin.x: scrollBar.width / 2
+ angle: 180
+
+ axis {
+ y: 0
+ x: 1
+ z: 0
+ }
+ }
+ }
+ delegate: MessageDelegate {
+ rotation: 180
+ }
+
+ displaced: Transition {
+ Anim {
+ property: "y"
+ }
+ }
+ move: Transition {
+ Anim {
+ property: "y"
+ }
+ }
+
+ Component.onCompleted: {
+ positionViewAtBeginning();
+ }
+ onAtYBeginningChanged: {
+ if (atYBeginning)
+ userScrolledUp = false;
+ }
+ onContentHeightChanged: {
+ if (userScrolledUp && Chat.busy) {
+ const delta = contentHeight - lastCHeight;
+ contentY += delta;
+ }
+
+ lastCHeight = contentHeight;
+ }
+ onMovingChanged: {
+ if (moving)
+ userScrolledUp = !atYBeginning;
+ }
+
+ Anim {
+ id: scrollAnim
+
+ property: "contentY"
+ target: list
+ type: Anim.DefaultEffects
+ to: list.originY
+ }
+
+ WheelInverter {
+ target: list
+ }
+ }
+
+ // Trigger the load even before we're ready to show the list.
+ Component.onCompleted: if (root.chatData)
+ root.chatData.messagesModel
+ }
+
+ EmptyBackground {
+ id: emptyState
+
+ anchors.fill: parent
+ spacing: Tokens.spacing.small
+ visible: root.chatData.messageCount < 1
+ }
+
+ Item {
+ anchors.bottom: input.top
+ anchors.bottomMargin: Tokens.spacing.extraLarge
+ anchors.horizontalCenter: parent.horizontalCenter
+ implicitHeight: scrollToBottom.implicitHeight
+ implicitWidth: scrollToBottom.implicitWidth
+ scale: !listLoader.item.atYBeginning && listLoader.item.contentHeight > listLoader.item.height ? 1 : 0
+ visible: scale > 0
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ Elevation {
+ anchors.fill: parent
+ level: 2
+ radius: scrollToBottom.radius
+ }
+
+ IconButton {
+ id: scrollToBottom
+
+ anchors.centerIn: parent
+ font.pointSize: Tokens.font.size.large
+ icon: "arrow_downward"
+ isRound: true
+ padding: Tokens.padding.extraSmall
+ type: IconButton.Tonal
+
+ onClicked: {
+ listLoader.item.scrollToBottom();
+ }
+ }
+ }
+
+ ChatInput {
+ id: input
+
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ bg.border.color: Colors.palette.m3outlineVariant
+ bg.color: Colors.tPalette.m3surfaceContainerLowest
+ font.pointSize: Tokens.font.size.normal
+ implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
+ focus: true
+ placeholderText: qsTr("Send a message")
+ sendIcon.font.pointSize: Tokens.font.size.large
+ sendIcon.icon: "arrow_upward"
+ sendIcon.padding: Tokens.padding.extraSmall
+
+ Component.onCompleted: root.focusInput()
+ Keys.onPressed: e => {
+ if (e.key == Qt.Key_Return) {
+ if (!(e.modifiers & Qt.ShiftModifier)) {
+ root.send(text);
+ e.accepted = true;
+ }
+ }
+ }
+ onSendPressed: root.send(text)
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml b/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml
new file mode 100644
index 0000000..1bcb78f
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/ChatDelegate.qml
@@ -0,0 +1,371 @@
+import QtQuick
+import ZShell.Components
+import ZShell.Config
+import ZShell.Llm
+import qs.Effects
+import qs.Components
+import qs.Services
+
+CustomClippingRect {
+ id: root
+
+ property bool expanded: false
+ property bool highlighted: false
+ required property ChatSession modelData
+
+ signal clicked(content: ChatSession)
+ signal remove(content: ChatSession)
+
+ color: highlighted ? Qt.alpha(Colors.palette.m3primaryContainer, 0.3) : Qt.alpha(Colors.tPalette.m3surfaceContainer, 0.2)
+ implicitHeight: {
+ let h = 0;
+
+ h += infoContainer.implicitHeight;
+
+ if (expanded)
+ h += body.implicitHeight + body.topMargin + actionRow.implicitHeight + actionRow.anchors.topMargin;
+ else
+ h += preview.implicitHeight + preview.topMargin;
+
+ h += Tokens.padding.small;
+
+ const icon = chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
+ if (h < icon)
+ return icon;
+ return h;
+ }
+ radius: Tokens.rounding.large
+ clip: true
+
+ CustomRect {
+ id: borderHighlight
+
+ anchors.fill: parent
+ border.width: root.highlighted ? 2 : 0
+ border.color: Colors.palette.m3secondary
+ radius: root.radius - border.width + 1
+
+ Behavior on border.width {
+ Anim {}
+ }
+ }
+
+ Behavior on implicitHeight {
+ enabled: titleText.readOnly
+
+ Anim {}
+ }
+
+ MaterialIcon {
+ id: chatIcon
+
+ anchors.left: parent.left
+ anchors.leftMargin: Tokens.padding.large
+ anchors.top: parent.top
+ anchors.topMargin: Tokens.padding.large
+ font.pointSize: Tokens.font.size.extraLarge
+ text: root.modelData.icon || "check"
+ color: Colors.palette.m3onSurface
+ }
+
+ Item {
+ id: infoContainer
+
+ anchors.left: chatIcon.right
+ anchors.leftMargin: Tokens.spacing.medium
+ anchors.top: parent.top
+ anchors.right: expandBtn.left
+ anchors.rightMargin: Tokens.spacing.medium
+ implicitHeight: {
+ let h = 0;
+ h += title.implicitHeight + title.anchors.topMargin;
+ h += timestamp.implicitHeight + timestamp.anchors.topMargin;
+ if (root.expanded)
+ h += created.implicitHeight + created.anchors.topMargin;
+ return h;
+ }
+
+ CustomRect {
+ id: title
+
+ anchors.left: parent.left
+ anchors.topMargin: Tokens.padding.extraSmall
+ anchors.top: parent.top
+ color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
+ implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
+ radius: Tokens.rounding.full
+ implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2
+ opacity: titleText.readOnly ? 0 : 1
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ Item {
+ id: titleWrapper
+
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.right: parent.right
+ implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: fadeMask
+ }
+
+ TextFieldBase {
+ id: titleText
+
+ anchors.left: parent.left
+ anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
+ anchors.top: parent.top
+ anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
+ font.pointSize: Tokens.font.size.normal
+ text: root.modelData.title || qsTr("New chat")
+ readOnly: true
+ color: Colors.palette.m3onSurface
+
+ Behavior on anchors.leftMargin {
+ Anim {}
+ }
+ Behavior on anchors.topMargin {
+ Anim {}
+ }
+
+ onEditingFinished: root.modelData.title = text
+ onReadOnlyChanged: {
+ if (!readOnly) {
+ this.forceActiveFocus();
+ cursorPosition = text.length;
+ } else {
+ root.forceActiveFocus();
+ }
+ }
+ }
+ }
+
+ CustomRect {
+ id: fadeMask
+
+ // anchors.top: titleText.top
+ // anchors.bottom: titleText.bottom
+ // anchors.left: titleText.left
+ // anchors.right: parent.right
+ anchors.fill: titleWrapper
+
+ layer.enabled: true
+ visible: false
+
+ gradient: Gradient {
+ orientation: Gradient.Horizontal
+
+ GradientStop {
+ color: Qt.rgba(1, 1, 1, 1.0)
+ position: 0.85
+ }
+
+ GradientStop {
+ color: Qt.rgba(1, 1, 1, 0)
+ position: 1.0
+ }
+ }
+ }
+
+ CustomText {
+ id: created
+
+ anchors.top: title.bottom
+ anchors.topMargin: Tokens.spacing.extraSmall
+ anchors.left: parent.left
+ anchors.right: parent.right
+ font.pointSize: Tokens.font.size.small
+ color: Colors.palette.m3onSurfaceVariant
+ text: qsTr("Created on: %1").arg(root.modelData.createdAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP"))
+ opacity: root.expanded ? 1 : 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ CustomText {
+ id: updated
+
+ anchors.top: created.bottom
+ anchors.topMargin: Tokens.spacing.small
+ anchors.left: parent.left
+ font.pointSize: Tokens.font.size.small
+ color: Colors.palette.m3onSurfaceVariant
+ text: qsTr("Updated on: ")
+ opacity: root.expanded ? 1 : 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ CustomText {
+ id: timestamp
+
+ anchors.left: parent.left
+ anchors.top: title.bottom
+ anchors.topMargin: Tokens.spacing.extraSmall
+ color: Colors.palette.m3onSurfaceVariant
+ font.pointSize: Tokens.font.size.small
+ text: root.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP")
+
+ states: State {
+ name: "expanded"
+ when: root.expanded
+
+ AnchorChanges {
+ target: timestamp
+ anchors.left: updated.right
+ anchors.top: undefined
+ anchors.verticalCenter: updated.verticalCenter
+ }
+ }
+ transitions: Transition {
+ AnchorAnim {}
+ }
+ }
+ }
+
+ CustomText {
+ id: preview
+
+ readonly property int topMargin: Tokens.spacing.extraSmall
+
+ anchors.left: chatIcon.right
+ anchors.margins: Tokens.spacing.medium
+ anchors.right: parent.right
+ y: infoContainer.implicitHeight + topMargin
+ elide: Text.ElideRight
+ color: Colors.palette.m3onSurfaceVariant
+ font.pointSize: Tokens.font.size.small
+ maximumLineCount: 1
+ opacity: !root.expanded ? 1 : 0
+ text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
+
+ Behavior on y {
+ Anim {}
+ }
+ Behavior on opacity {
+ Anim {
+ type: Anim.DefaultEffects
+ }
+ }
+ }
+
+ CustomText {
+ id: body
+
+ readonly property int topMargin: root.expanded ? Tokens.spacing.medium : Tokens.spacing.extraSmall
+
+ anchors.left: chatIcon.right
+ anchors.margins: Tokens.spacing.medium
+ anchors.right: parent.right
+ y: infoContainer.implicitHeight + topMargin
+ animate: true
+ color: Colors.palette.m3onSurfaceVariant
+ maximumLineCount: 5
+ opacity: root.expanded ? 1 : 0
+ text: root.modelData.messagesModel.lastMessage?.activeGeneration.content ?? qsTr("No messages yet")
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+
+ Behavior on y {
+ Anim {}
+ }
+ Behavior on opacity {
+ Anim {
+ type: Anim.DefaultEffects
+ }
+ }
+ }
+
+ StateLayer {
+ id: sLayer
+
+ property int startY
+
+ onClicked: {
+ root.clicked(root.modelData);
+ }
+ }
+
+ ButtonRow {
+ id: actionRow
+
+ anchors.top: body.bottom
+ anchors.left: chatIcon.right
+ anchors.right: parent.right
+ anchors.topMargin: Tokens.spacing.small
+ anchors.leftMargin: Tokens.spacing.medium
+ anchors.rightMargin: Tokens.padding.medium
+ spacing: Tokens.spacing.medium
+
+ IconButton {
+ icon: "close"
+ fillWidth: true
+ shapeMorph: true
+ isRound: true
+ font.pointSize: Tokens.font.size.large
+ inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
+ inactiveOnColor: Colors.palette.m3onSurfaceVariant
+
+ onClicked: root.remove(root.modelData)
+ }
+
+ IconButton {
+ icon: titleText.readOnly ? "edit" : "check"
+ label.animate: true
+ isToggle: true
+ fillWidth: true
+ shapeMorph: true
+ font.pointSize: Tokens.font.size.large
+ isRound: true
+ inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
+ inactiveOnColor: Colors.palette.m3onSurfaceVariant
+ activeColor: Colors.palette.m3secondary
+ activeOnColor: Colors.palette.m3onSecondary
+ checked: !titleText.readOnly
+
+ onClicked: titleText.readOnly = !titleText.readOnly
+ }
+ }
+
+ CustomRect {
+ id: expandBtn
+
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Tokens.padding.small
+ implicitHeight: expandIcon.implicitHeight
+ implicitWidth: expandIcon.implicitHeight
+ radius: Tokens.rounding.full
+ color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
+
+ StateLayer {
+ onClicked: root.expanded = !root.expanded
+ }
+
+ MaterialIcon {
+ id: expandIcon
+
+ anchors.centerIn: parent
+ anchors.verticalCenterOffset: root.expanded ? -1 : 1
+ rotation: root.expanded ? 180 : 0
+ color: Colors.palette.m3onSurface
+ text: "expand_more"
+
+ Behavior on anchors.verticalCenterOffset {
+ Anim {}
+ }
+ Behavior on rotation {
+ Anim {}
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/ChatInput.qml b/Modules/Notifications/Sidebar/Chat/ChatInput.qml
new file mode 100644
index 0000000..058ac31
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/ChatInput.qml
@@ -0,0 +1,87 @@
+import QtQuick
+import ZShell.Config
+import qs.Components
+import qs.Services
+
+TextAreaBase {
+ id: root
+
+ readonly property alias bg: bg
+ readonly property alias sendIcon: sendIcon
+
+ signal sendPressed
+
+ bottomPadding: Tokens.padding.large
+ leftPadding: Tokens.padding.extraLarge
+ rightPadding: sendIcon.width + sendIcon.anchors.rightMargin + Tokens.spacing.small
+ topPadding: Tokens.padding.large
+ wrapMode: TextAreaBase.Wrap
+
+ background: CustomRect {
+ id: bg
+
+ anchors.fill: parent
+ color: Colors.tPalette.m3surfaceContainer
+ radius: Tokens.rounding.extraLarge
+
+ StateLayer {
+ id: stateLayer
+
+ cursorShape: Qt.IBeamCursor
+ enabled: !root.activeFocus
+ manualPressOverride: tapHandler.pressed
+
+ onClicked: root.forceActiveFocus()
+ }
+ }
+
+ onPressed: {
+ if (stateLayer.enabled)
+ stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
+ }
+
+ CustomText {
+ id: placeholder
+
+ anchors.left: parent.left
+ anchors.leftMargin: root.leftPadding
+ anchors.verticalCenter: parent.verticalCenter
+ color: root.placeholderTextColor
+ font: root.font
+ opacity: root.text ? 0 : 1
+ text: root.placeholderText
+
+ Behavior on opacity {
+ Anim {
+ type: Anim.DefaultEffects
+ }
+ }
+ }
+
+ IconButton {
+ id: sendIcon
+
+ anchors.right: parent.right
+ anchors.rightMargin: Tokens.padding.small
+ anchors.verticalCenter: parent.verticalCenter
+ enabled: root.text
+ icon: "upward_arrow"
+ opacity: root.text ? 1 : 0
+ radius: Tokens.rounding.full
+ radiusMorph: false
+ stateLayer.hoverEnabled: enabled
+ type: IconButton.Text
+
+ Behavior on opacity {
+ Anim {
+ type: Anim.DefaultEffects
+ }
+ }
+
+ onClicked: root.sendPressed()
+ }
+
+ TapHandler {
+ id: tapHandler
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/ChatList.qml b/Modules/Notifications/Sidebar/Chat/ChatList.qml
new file mode 100644
index 0000000..541eb14
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/ChatList.qml
@@ -0,0 +1,414 @@
+pragma ComponentBehavior: Bound
+
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Llm
+import ZShell.Config
+import qs.Services
+import qs.Components
+
+Item {
+ id: root
+
+ property alias list: list
+ property alias model: list.model
+ property bool highlight: false
+
+ signal deleteChatRequest(content: ChatSession)
+ signal loadChatRequest(content: ChatSession, index: int)
+ signal newChatRequest
+
+ CustomText {
+ id: header
+
+ anchors.left: parent.left
+ anchors.margins: Tokens.padding.extraSmall
+ anchors.right: parent.right
+ anchors.top: parent.top
+ color: Colors.palette.m3outline
+ font.family: "CaskaydiaCove NF"
+ font.pointSize: 13
+ font.weight: 500
+ text: qsTr("%1 Chat%2").arg(list.count).arg(list.count > 1 ? "s" : "")
+ }
+
+ CustomListView {
+ id: list
+
+ anchors.bottom: modelsContainer.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: header.bottom
+ anchors.topMargin: Tokens.spacing.medium
+ clip: true
+ spacing: Tokens.spacing.medium
+
+ add: Transition {
+ Anim {
+ from: list.width
+ property: "x"
+ }
+ }
+ remove: Transition {
+ Anim {
+ to: list.width
+ property: "x"
+ }
+
+ Anim {
+ to: 0
+ property: "opacity"
+ }
+ }
+ displaced: Transition {
+ Anim {
+ property: "y"
+ }
+ }
+ move: Transition {
+ Anim {
+ property: "y"
+ }
+ }
+
+ delegate: ChatDelegate {
+ id: chat
+
+ required property int index
+
+ implicitWidth: ListView.view.width
+ highlighted: root.highlight && ChatState.chatSession === modelData
+
+ onHighlightedChanged: if (highlighted)
+ list.currentIndex = index
+ onClicked: content => root.loadChatRequest(content, index)
+ onRemove: content => root.deleteChatRequest(content)
+ }
+
+ TapHandler {
+ onTapped: {
+ root.focus = true;
+ ChatState.fabExpanded = false;
+ modelsContainer.expanded = false;
+ }
+ }
+ }
+
+ CustomClippingRect {
+ id: modelsContainer
+
+ property bool expanded: false
+
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: fabRoot.left
+ anchors.margins: Tokens.padding.small
+ anchors.rightMargin: Tokens.spacing.small
+
+ implicitHeight: expanded ? 400 : fabRoot.implicitHeight
+ radius: modelLayer.pressed ? Tokens.rounding.small : modelName.implicitHeight / 2
+
+ function prettyModelName(): string {
+ const pathList = Chat.model.split("\/");
+ const name = pathList[pathList.length - 1];
+ return name;
+ }
+
+ Behavior on implicitHeight {
+ Anim {}
+ }
+
+ Behavior on radius {
+ Anim {}
+ }
+
+ Item {
+ id: modelName
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ implicitHeight: fabRoot.implicitHeight
+
+ CustomText {
+ text: modelsContainer.prettyModelName()
+ color: Colors.palette.m3onSurfaceVariant
+ anchors.left: parent.left
+ anchors.leftMargin: Tokens.padding.large
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ MaterialIcon {
+ color: Colors.palette.m3onSurfaceVariant
+ anchors.right: parent.right
+ anchors.rightMargin: Tokens.padding.large
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelsContainer.expanded ? "unfold_less" : "unfold_more"
+ animate: true
+ font.pointSize: Tokens.font.size.large
+ }
+
+ StateLayer {
+ id: modelLayer
+ onClicked: {
+ ChatState.fabExpanded = false;
+ modelsContainer.expanded = !modelsContainer.expanded;
+ }
+ radius: modelsContainer.radius
+ }
+ }
+
+ VerticalFadeListView {
+ id: modelsList
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.top: modelName.bottom
+ anchors.margins: Tokens.padding.medium
+ anchors.topMargin: Tokens.spacing.medium
+ model: Chat.availableModels
+ opacity: modelsContainer.expanded ? 1 : 0
+ currentIndex: Chat.availableModels.indexOf(Chat.model)
+ visible: opacity > 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ CustomScrollBar.vertical: CustomScrollBar {
+ flickable: modelsList
+ }
+
+ delegate: CustomRect {
+ id: model
+
+ required property var modelData
+ required property int index
+ readonly property bool selected: model.ListView.view.currentIndex === model.index
+
+ implicitWidth: ListView.view.width
+ implicitHeight: layout.implicitHeight + Tokens.padding.small * 2
+ radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
+ color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
+
+ Behavior on radius {
+ Anim {
+ type: Anim.SlowEffects
+ }
+ }
+
+ RowLayout {
+ id: layout
+
+ anchors.fill: parent
+ anchors.margins: Tokens.padding.small
+ anchors.leftMargin: Tokens.padding.large
+ anchors.rightMargin: Tokens.padding.large
+
+ CustomText {
+ text: model.modelData
+ color: model.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
+ }
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignRight
+ visible: model.selected
+ color: Colors.palette.m3onTertiaryContainer
+ font.pointSize: Tokens.font.size.normal
+ opacity: model.selected ? 1 : 0
+ text: "check"
+
+ Behavior on opacity {
+ Anim {
+ type: Anim.SlowEffects
+ }
+ }
+ }
+ }
+
+ StateLayer {
+ id: stateLayer
+
+ onClicked: {
+ Chat.selectModel(model.modelData);
+ }
+ }
+ }
+ }
+ }
+
+ IconButton {
+ id: fabRoot
+
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.margins: Tokens.padding.small
+ padding: 8
+ font.pointSize: Math.round(18 * 1.2)
+ icon: "add"
+ isRound: ChatState.fabExpanded
+
+ label.transform: Rotation {
+ origin.y: fabRoot.label.height / 2
+ origin.x: fabRoot.label.width / 2
+ angle: ChatState.fabExpanded ? 135 : 0
+
+ Behavior on angle {
+ Anim {}
+ }
+ }
+
+ onClicked: {
+ modelsContainer.expanded = false;
+ ChatState.fabExpanded = !ChatState.fabExpanded;
+ }
+
+ Elevation {
+ anchors.fill: parent
+ level: fabRoot.stateLayer.containsMouse ? 4 : 3
+ radius: parent.radius
+ z: -1
+ }
+ }
+
+ ColumnLayout {
+ id: fabMenu
+
+ anchors.bottom: fabRoot.top
+ anchors.right: fabRoot.right
+ anchors.bottomMargin: Tokens.padding.medium
+
+ Repeater {
+ id: fabRep
+
+ model: ListModel {
+ ListElement {
+ name: "new chat"
+ icon: "add"
+ }
+ ListElement {
+ name: "pop out in window"
+ icon: "open_in_new"
+ }
+ }
+
+ CustomClippingRect {
+ id: fabMenuItem
+
+ required property var modelData
+ required property int index
+
+ Layout.alignment: Qt.AlignRight
+ implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.medium * 2
+
+ radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
+ color: Colors.palette.m3primaryContainer
+ visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
+
+ Behavior on radius {
+ Anim {
+ type: Anim.DefaultEffects
+ }
+ }
+
+ opacity: 0
+
+ states: State {
+ name: "visible"
+ when: ChatState.fabExpanded
+
+ PropertyChanges {
+ fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Tokens.padding.large * 2
+ fabMenuItem.opacity: 1
+ fabMenuItemInner.opacity: 1
+ }
+ }
+
+ transitions: [
+ Transition {
+ to: "visible"
+
+ SequentialAnimation {
+ PauseAnimation {
+ duration: (fabRep.count - 1 - fabMenuItem.index) * 50
+ }
+ ParallelAnimation {
+ Anim {
+ property: "implicitWidth"
+ type: Anim.FastSpatial
+ }
+ Anim {
+ property: "opacity"
+ duration: Tokens.anim.durations.small
+ easing: Tokens.anim.standard
+ }
+ }
+ }
+ },
+ Transition {
+ from: "visible"
+
+ SequentialAnimation {
+ PauseAnimation {
+ duration: fabMenuItem.index * Tokens.anim.durations.small / 8
+ }
+ ParallelAnimation {
+ Anim {
+ property: "implicitWidth"
+ type: Anim.FastSpatial
+ }
+ Anim {
+ property: "opacity"
+ duration: Tokens.anim.durations.small
+ easing: Tokens.anim.standard
+ }
+ }
+ }
+ }
+ ]
+
+ RowLayout {
+ id: fabMenuItemInner
+
+ anchors.centerIn: parent
+ spacing: Tokens.spacing.medium
+ opacity: 0
+
+ MaterialIcon {
+ text: fabMenuItem.modelData.icon
+ color: Colors.palette.m3onPrimaryContainer
+ font.pointSize: Tokens.font.size.large
+ fill: 1
+ }
+
+ CustomText {
+ animate: true
+ text: fabMenuItem.modelData.name
+ font.capitalization: Font.Capitalize
+ color: Colors.palette.m3onPrimaryContainer
+ Layout.preferredWidth: implicitWidth
+
+ Behavior on Layout.preferredWidth {
+ Anim {}
+ }
+ }
+ }
+
+ StateLayer {
+ id: sLayer
+ onClicked: {
+ const name = fabMenuItem.modelData.name;
+
+ if (name === "new chat")
+ root.newChatRequest();
+ else if (name === "pop out in window")
+ Detach.create();
+
+ ChatState.fabExpanded = false;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/ChatPanel.qml b/Modules/Notifications/Sidebar/Chat/ChatPanel.qml
new file mode 100644
index 0000000..6f8c765
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/ChatPanel.qml
@@ -0,0 +1,73 @@
+pragma ComponentBehavior: Bound
+
+import ZShell.Config
+import ZShell.Llm
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import qs.Helpers
+import qs.Modules.Notifications.Sidebar
+import qs.Components
+import qs.Services
+
+Item {
+ id: root
+
+ anchors.fill: parent
+ anchors.margins: Tokens.padding.small
+
+ Component.onCompleted: {
+ if (ChatState.inChat) {
+ stack.push(chatList);
+ stack.push(chatContent, {
+ "chatData": ChatState.chatSession
+ });
+ } else
+ stack.push(chatList);
+ }
+
+ StackView {
+ id: stack
+ anchors.fill: parent
+ }
+
+ Component {
+ id: chatList
+
+ ChatList {
+ model: ScriptModel {
+ values: Chat.chats.values
+ }
+
+ onDeleteChatRequest: chat => {
+ Chat.chats.remove(chat);
+ }
+ onLoadChatRequest: (chat, index) => {
+ stack.push(chatContent, {
+ "chatData": chat
+ });
+ ChatState.inChat = true;
+ ChatState.chatSession = chat;
+ }
+ onNewChatRequest: {
+ const data = Chat.chats.insert();
+ stack.push(chatContent, {
+ "chatData": data
+ });
+ ChatState.inChat = true;
+ ChatState.chatSession = data;
+ }
+ }
+ }
+
+ Component {
+ id: chatContent
+
+ ChatContent {
+ onRequestClose: {
+ stack.pop();
+ ChatState.inChat = false;
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/ChatState.qml b/Modules/Notifications/Sidebar/Chat/ChatState.qml
new file mode 100644
index 0000000..c24e59a
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/ChatState.qml
@@ -0,0 +1,22 @@
+pragma Singleton
+
+import QtQuick
+import Quickshell
+import ZShell.Llm
+import qs.Helpers
+
+Singleton {
+ id: root
+
+ property ChatSession chatSession
+ readonly property string title: chatSession ? chatSession.title : "Chat"
+ property int currentIdx: -1
+ property bool inChat: false
+ property bool fabExpanded: false
+ property bool isWindow: false
+ property ShellScreen screen
+ property bool sidebarVisible: Visibilities.getForActive().sidebar
+
+ onSidebarVisibleChanged: if (!sidebarVisible)
+ fabExpanded = false
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/Actions.qml b/Modules/Notifications/Sidebar/Chat/Content/Actions.qml
new file mode 100644
index 0000000..1d4efdf
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/Actions.qml
@@ -0,0 +1,115 @@
+import QtQuick
+import Quickshell
+import ZShell.Config
+import ZShell.Components
+import ZShell.Llm
+import qs.Components
+import qs.Services
+
+Item {
+ id: root
+
+ required property ChatGeneration current
+ required property TextEdit edit
+ required property bool hovered
+ required property bool isUser
+ required property ChatMessage message
+ required property LlmSegment segment
+
+ implicitHeight: editButton.implicitHeight
+ implicitWidth: generationTools.implicitWidth + editTools.implicitWidth + Tokens.spacing.small
+
+ ButtonRow {
+ id: generationTools
+
+ enabled: visible
+ visible: !root.isUser
+
+ IconButton {
+ enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== 0
+ icon: "chevron_left"
+ type: IconButton.Tonal
+
+ onClicked: {
+ if (!root.edit.readOnly)
+ root.edit.readOnly = true;
+ root.message.setActiveGeneration(root.message.activeGenerationIndex - 1);
+ }
+ }
+
+ IconButton {
+ enabled: false
+ icon: `${root.message.activeGenerationIndex + 1}`
+ label.font.family: Config.appearance.font.family.sans
+ label.font.pointSize: Tokens.font.size.small
+ type: IconButton.Text
+ }
+
+ IconButton {
+ enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== root.message.generationCount - 1
+ icon: "chevron_right"
+ type: IconButton.Tonal
+
+ onClicked: {
+ if (!root.edit.readOnly)
+ root.edit.readOnly = true;
+ root.message.setActiveGeneration(root.message.activeGenerationIndex + 1);
+ }
+ }
+ }
+
+ ButtonRow {
+ id: editTools
+
+ anchors.right: parent.right
+ opacity: !root.current.streaming && root.hovered ? 1 : 0
+ spacing: Tokens.spacing.small
+ visible: opacity > 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ IconButton {
+ icon: "refresh"
+ inactiveColor: Colors.palette.m3secondary
+ inactiveOnColor: Colors.palette.m3onSecondary
+ scale: root.edit.readOnly ? 1 : 0
+ visible: !root.isUser
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ onClicked: {
+ root.message.retry();
+ }
+ }
+
+ IconButton {
+ icon: "content_copy"
+ inactiveColor: Colors.palette.m3secondary
+ inactiveOnColor: Colors.palette.m3onSecondary
+ scale: root.edit.readOnly ? 1 : 0
+
+ Behavior on scale {
+ Anim {}
+ }
+
+ onClicked: Quickshell.clipboardText = root.current.content
+ }
+
+ IconButton {
+ id: editButton
+
+ icon: root.edit.readOnly ? "edit" : "check"
+ inactiveColor: Colors.palette.m3tertiary
+ inactiveOnColor: Colors.palette.m3onTertiary
+ visible: root.isUser
+
+ onClicked: {
+ root.edit.readOnly = !root.edit.readOnly;
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/BubbleEdit.qml b/Modules/Notifications/Sidebar/Chat/Content/BubbleEdit.qml
new file mode 100644
index 0000000..e335cf5
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/BubbleEdit.qml
@@ -0,0 +1,28 @@
+import Quickshell
+import QtQuick
+import ZShell.Config
+import qs.Components
+import qs.Services
+
+TextEditBase {
+ id: root
+
+ color: Colors.palette.m3onSurface
+ readOnly: true
+ anchors.margins: Tokens.padding.medium
+ textFormat: Text.MarkdownText
+ font.pointSize: Tokens.font.size.smaller
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+
+ onLinkActivated: link => {
+ Qt.openUrlExternally(link);
+ }
+
+ CustomMouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.NoButton
+ cursorShape: root.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
+ preventStealing: false
+ hoverEnabled: true
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml b/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml
new file mode 100644
index 0000000..0a75bb3
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/ChatHost.qml
@@ -0,0 +1,132 @@
+import QtQuick
+import ZShell.Llm
+import ZShell.Config
+import qs.Components
+import qs.Services
+import qs.Modules.Notifications.Sidebar.Chat
+
+Item {
+ id: root
+
+ property int animOff
+ property Item currentItem
+ property bool loading: false
+ property int lastIdx: -1
+
+ readonly property Component conversationComp: ChatContent {}
+
+ function loadConversation(chat): void {
+ if (currentItem) {
+ currentItem.destroy();
+ currentItem = null;
+ }
+
+ if (!chat)
+ return;
+
+ root.loading = true;
+
+ const incubator = root.conversationComp.incubateObject(container, {
+ chatData: chat
+ });
+
+ const attach = () => {
+ incubator.object.anchors.fill = container;
+ currentItem = incubator.object;
+ root.loading = false;
+ enterAnim.start();
+ };
+
+ if (incubator.status === Component.Ready)
+ attach();
+ else
+ incubator.onStatusChanged = status => {
+ if (status === Component.Ready)
+ attach();
+ };
+ }
+
+ Item {
+ id: container
+
+ anchors.fill: parent
+ layer.enabled: opacity < 1
+ objectName: "ConversationContainer"
+
+ Component.onCompleted: {
+ if (ChatState.chatSession)
+ root.loadConversation(ChatState.chatSession);
+ }
+ }
+
+ LoadingIndicator {
+ anchors.centerIn: parent
+ implicitSize: Tokens.font.size.extraLarge * 4
+ opacity: root.loading ? 1 : 0
+ visible: opacity > 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ Connections {
+ function onChatSessionChanged(): void {
+ exitAnim.complete();
+ enterAnim.complete();
+ root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
+ root.lastIdx = ChatState.currentIdx;
+ exitAnim.start();
+ }
+
+ target: ChatState
+ }
+
+ SequentialAnimation {
+ id: exitAnim
+
+ Anim {
+ property: "opacity"
+ target: container
+ to: 0
+ type: Anim.DefaultEffects
+ }
+
+ ScriptAction {
+ script: root.loadConversation(ChatState.chatSession)
+ }
+ }
+
+ SequentialAnimation {
+ id: enterAnim
+
+ PropertyAction {
+ property: "topMargin"
+ target: container.anchors
+ value: root.animOff
+ }
+
+ PropertyAction {
+ property: "bottomMargin"
+ target: container.anchors
+ value: -root.animOff
+ }
+
+ ParallelAnimation {
+ Anim {
+ from: 0
+ property: "opacity"
+ target: container
+ to: 1
+ type: Anim.SlowEffects
+ }
+
+ Anim {
+ properties: "topMargin,bottomMargin"
+ target: container.anchors
+ to: 0
+ type: Anim.SlowEffects
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/CodeBlockView.qml b/Modules/Notifications/Sidebar/Chat/Content/CodeBlockView.qml
new file mode 100644
index 0000000..e264c85
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/CodeBlockView.qml
@@ -0,0 +1,276 @@
+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
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/CodeColors.qml b/Modules/Notifications/Sidebar/Chat/Content/CodeColors.qml
new file mode 100644
index 0000000..7f14d8b
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/CodeColors.qml
@@ -0,0 +1,231 @@
+pragma Singleton
+
+import Quickshell
+import Quickshell.Io
+import QtQuick
+import ZShell.Config
+
+Singleton {
+ id: root
+
+ readonly property JsonObject schemes: JsonObject {
+ readonly property Scheme oneDark: Scheme {
+ comment: "#5c6370"
+ string: "#98c379"
+ stringKey: "#e06c75"
+ number: "#d19a66"
+ keyword: "#c678dd"
+ type: "#e5c07b"
+ functions: "#61afef"
+ method: "#61afef"
+ macro: "#98c379"
+ preproc: "#abb2bf"
+ operator: "#abb2bf"
+ property: "#abb2bf"
+ label: "#e06c75"
+ attribute: "#d19a66"
+ bg: "#282c34"
+ normal: "#abb2bf"
+ }
+ readonly property Scheme nord: Scheme {
+ comment: "#4c566a"
+ string: "#a3be8c"
+ stringKey: "#ebcb8b"
+ number: "#b48ead"
+ keyword: "#81a1c1"
+ type: "#8fbcbb"
+ functions: "#88c0d0"
+ method: "#88c0d0"
+ macro: "#5e81ac"
+ preproc: "#5e81ac"
+ operator: "#81a1c1"
+ property: "#d8dee9"
+ label: "#d08770"
+ attribute: "#d8dee9"
+ bg: "#2e3440"
+ normal: "#d8dee9"
+ }
+ readonly property Scheme dracula: Scheme {
+ comment: "#6272a4"
+ string: "#f1fa8c"
+ stringKey: "#f8f8f2"
+ number: "#ffb86c"
+ keyword: "#ff79c6"
+ type: "#8be9fd"
+ functions: "#50fa7b"
+ method: "#50fa7b"
+ macro: "#ff79c6"
+ preproc: "#ff79c6"
+ operator: "#f8f8f2"
+ property: "#f8f8f2"
+ label: "#6272a4"
+ attribute: "#8be9fd"
+ bg: "#282a36"
+ normal: "#f8f8f2"
+ }
+ readonly property Scheme githubDark: Scheme {
+ comment: "#8b949e"
+ string: "#a5d6ff"
+ stringKey: "#79c0ff"
+ number: "#79c0ff"
+ keyword: "#ff7b72"
+ type: "#ffa657"
+ functions: "#d2a8ff"
+ method: "#d2a8ff"
+ macro: "#ff7b72"
+ preproc: "#79c0ff"
+ operator: "#ff7b72"
+ property: "#79c0ff"
+ label: "#7ee787"
+ attribute: "#7ee787"
+ bg: "#0d1117"
+ normal: "#c9d1d9"
+ }
+ readonly property Scheme solarizedDark: Scheme {
+ comment: "#586e75"
+ string: "#2aa198"
+ stringKey: "#268bd2"
+ number: "#2aa198"
+ keyword: "#859900"
+ type: "#b58900"
+ functions: "#268bd2"
+ method: "#268bd2"
+ macro: "#cb4b16"
+ preproc: "#cb4b16"
+ operator: "#859900"
+ property: "#268bd2"
+ label: "#6c71c4"
+ attribute: "#657b83"
+ bg: "#002b36"
+ normal: "#839496"
+ }
+ readonly property Scheme monokai: Scheme {
+ comment: "#75715e"
+ string: "#e6db74"
+ stringKey: "#f8f8f2"
+ number: "#ae81ff"
+ keyword: "#f92672"
+ type: "#a6e22e"
+ functions: "#a6e22e"
+ method: "#a6e22e"
+ macro: "#a6e22e"
+ preproc: "#f92672"
+ operator: "#f92672"
+ property: "#fda5ff"
+ label: "#f92672"
+ attribute: "#a6e22e"
+ bg: "#272822"
+ normal: "#f8f8f2"
+ }
+ readonly property Scheme gruvboxDark: Scheme {
+ comment: "#928374"
+ string: "#b8bb26"
+ stringKey: "#ebdbb2"
+ number: "#d3869b"
+ keyword: "#fb4934"
+ type: "#fabd2f"
+ functions: "#b8bb26"
+ method: "#b8bb26"
+ macro: "#8ec07c"
+ preproc: "#8ec07c"
+ operator: "#ebdbb2"
+ property: "#83a598"
+ label: "#fb4934"
+ attribute: "#8ec07c"
+ bg: "#1d2021"
+ normal: "#ebdbb2"
+ }
+ readonly property Scheme catppuccinMocha: Scheme {
+ comment: "#9399b2"
+ string: "#a6e3a1"
+ stringKey: "#b4befe"
+ number: "#fab387"
+ keyword: "#cba6f7"
+ type: "#f9e2af"
+ functions: "#89b4fa"
+ method: "#89b4fa"
+ macro: "#cba6f7"
+ preproc: "#f5c2e7"
+ operator: "#89dceb"
+ property: "#b4befe"
+ label: "#74c7ec"
+ attribute: "#f9e2af"
+ bg: "#1e1e2e"
+ normal: "#cdd6f4"
+ }
+ readonly property Scheme tokyoNight: Scheme {
+ comment: "#565f89"
+ string: "#9ece6a"
+ stringKey: "#73daca"
+ number: "#ff9e64"
+ keyword: "#9d7cd8"
+ type: "#2ac3de"
+ functions: "#7aa2f7"
+ method: "#7aa2f7"
+ macro: "#7dcfff"
+ preproc: "#7dcfff"
+ operator: "#89ddff"
+ property: "#73daca"
+ label: "#7aa2f7"
+ attribute: "#73daca"
+ bg: "#1a1b26"
+ normal: "#c0caf5"
+ }
+ readonly property Scheme ayuDark: Scheme {
+ comment: "#5a6673"
+ string: "#aad94c"
+ stringKey: "#aad94c"
+ number: "#d2a6ff"
+ keyword: "#ff8f40"
+ type: "#59c2ff"
+ functions: "#ffb454"
+ method: "#ffb454"
+ macro: "#59c2ff"
+ preproc: "#ff8f40"
+ operator: "#f29668"
+ property: "#f07178"
+ label: "#59c2ff"
+ attribute: "#ffb454"
+ bg: "#0a0e14"
+ normal: "#bfbdb6"
+ }
+ readonly property Scheme palenight: Scheme {
+ comment: "#697098"
+ string: "#c3e88d"
+ stringKey: "#82b1ff"
+ number: "#f78c6c"
+ keyword: "#ff5370"
+ type: "#ffcb6b"
+ functions: "#82b1ff"
+ method: "#82b1ff"
+ macro: "#c792ea"
+ preproc: "#ffcb6b"
+ operator: "#89ddff"
+ property: "#c3e88d"
+ label: "#c792ea"
+ attribute: "#ffcb6b"
+ bg: "#292d3e"
+ normal: "#bfc7d5"
+ }
+ }
+ readonly property var active: schemes[Config.llm.appearance.scheme]
+
+ component Scheme: JsonObject {
+ property color comment: "#697098"
+ property color string: "#c3e88d"
+ property color stringKey: "#82b1ff"
+ property color number: "#f78c6c"
+ property color keyword: "#ff5370"
+ property color type: "#ffcb6b"
+ property color functions: "#82b1ff"
+ property color method: "#82b1ff"
+ property color macro: "#c792ea"
+ property color preproc: "#ffcb6b"
+ property color operator: "#89ddff"
+ property color property: "#c3e88d"
+ property color label: "#c792ea"
+ property color attribute: "#ffcb6b"
+ property color bg: "#292d3e"
+ property color normal: "#bfc7d5"
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/CodeIcons.qml b/Modules/Notifications/Sidebar/Chat/Content/CodeIcons.qml
new file mode 100644
index 0000000..ba81fb4
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/CodeIcons.qml
@@ -0,0 +1,443 @@
+pragma Singleton
+
+import Quickshell
+import QtQuick
+
+Singleton {
+ readonly property string c: "M 125 50 c -4 -32 -24 -50 -62 -50 C 29 0 3 24 3 64 c 0 39 24 64 64 64 c 32 0 55 -19 58 -50 H 87 c -2 11 -8 20 -20 20 c -21 0 -24 -16 -24 -33 c 0 -23 8 -35 22 -35 c 13 0 20 7 22 20 z"
+ readonly property string cpp: "M 63.443 0 c -1.782 0 -3.564 .39 -4.916 1.172 L 11.594 28.27 C 8.89 29.828 6.68 33.66 6.68 36.78 v 54.197 c 0 1.562 .55 3.298 1.441 4.841 l -.002 .002 c .89 1.543 2.123 2.89 3.475 3.672 l 46.931 27.094 c 2.703 1.562 7.13 1.562 9.832 0 h .002 l 46.934 -27.094 c 1.352 -.78 2.582 -2.129 3.473 -3.672 c .89 -1.543 1.441 -3.28 1.441 -4.843 V 36.779 c 0 -1.557 -.55 -3.295 -1.441 -4.838 v -.002 c -.891 -1.545 -2.121 -2.893 -3.473 -3.67 L 68.359 1.173 C 67.008 .39 65.226 0 63.443 0 z m .002 26.033 c 13.465 0 26.02 7.246 32.77 18.91 l -16.38 9.479 c -3.372 -5.836 -9.66 -9.467 -16.39 -9.467 c -10.432 0 -18.922 8.49 -18.922 18.924 S 53.013 82.8 63.445 82.8 c 6.735 0 13.015 -3.625 16.395 -9.465 l 16.375 9.477 c -6.746 11.662 -19.305 18.91 -32.77 18.91 c -20.867 0 -37.843 -16.977 -37.843 -37.844 s 16.976 -37.844 37.843 -37.844 v -.002 z M 92.881 57.57 h 4.201 v 4.207 h 4.203 v 4.203 h -4.203 v 4.207 h -4.201 V 65.98 h -4.207 v -4.203 h 4.207 V 57.57 z m 15.765 0 h 4.208 v 4.207 h 4.203 v 4.203 h -4.203 v 4.207 h -4.208 V 65.98 h -4.205 v -4.203 h 4.205 V 57.57 z"
+ readonly property string csharp: "M 117.5 33.5 l .3 -.2 c -.6 -1.1 -1.5 -2.1 -2.4 -2.6 L 67.1 2.9 c -.8 -.5 -1.9 -.7 -3.1 -.7 c -1.2 0 -2.3 .3 -3.1 .7 l -48 27.9 c -1.7 1 -2.9 3.5 -2.9 5.4 v 55.7 c 0 1.1 .2 2.3 .9 3.4 l -.2 .1 c .5 .8 1.2 1.5 1.9 1.9 l 48.2 27.9 c .8 .5 1.9 .7 3.1 .7 c 1.2 0 2.3 -.3 3.1 -.7 l 48 -27.9 c 1.7 -1 2.9 -3.5 2.9 -5.4 V 36.1 c .1 -.8 0 -1.7 -.4 -2.6 z m -53.5 70 c -21.8 0 -39.5 -17.7 -39.5 -39.5 S 42.2 24.5 64 24.5 c 14.7 0 27.5 8.1 34.3 20 l -13 7.5 C 81.1 44.5 73.1 39.5 64 39.5 c -13.5 0 -24.5 11 -24.5 24.5 s 11 24.5 24.5 24.5 c 9.1 0 17.1 -5 21.3 -12.4 l 12.9 7.6 c -6.8 11.8 -19.6 19.8 -34.2 19.8 z M 115 62 h -3.2 l -.9 4 h 4.1 v 5 h -5 l -1.2 6 h -4.9 l 1.2 -6 h -3.8 l -1.2 6 h -4.8 l 1.2 -6 H 94 v -5 h 3.5 l .9 -4 H 94 v -5 h 5.3 l 1.2 -6 h 4.9 l -1.2 6 h 3.8 l 1.2 -6 h 4.8 l -1.2 6 h 2.2 v 5 z m -12.7 4 h 3.8 l .9 -4 h -3.8 z"
+ readonly property string python: "M 49.33 62 h 29.159 C 86.606 62 93 55.132 93 46.981 V 19.183 c 0 -7.912 -6.632 -13.856 -14.555 -15.176 c -5.014 -.835 -10.195 -1.215 -15.187 -1.191 c -4.99 .023 -9.612 .448 -13.805 1.191 C 37.098 6.188 35 10.758 35 19.183 V 30 h 29 v 4 H 23.776 c -8.484 0 -15.914 5.108 -18.237 14.811 c -2.681 11.12 -2.8 17.919 0 29.53 C 7.614 86.983 12.569 93 21.054 93 H 31 V 79.952 C 31 70.315 39.428 62 49.33 62 z m -1.838 -39.11 c -3.026 0 -5.478 -2.479 -5.478 -5.545 c 0 -3.079 2.451 -5.581 5.478 -5.581 c 3.015 0 5.479 2.502 5.479 5.581 c -.001 3.066 -2.465 5.545 -5.479 5.545 z m 74.789 25.921 C 120.183 40.363 116.178 34 107.682 34 H 97 v 12.981 C 97 57.031 88.206 65 78.489 65 H 49.33 C 41.342 65 35 72.326 35 80.326 v 27.8 c 0 7.91 6.745 12.564 14.462 14.834 c 9.242 2.717 17.994 3.208 29.051 0 C 85.862 120.831 93 116.549 93 108.126 V 97 H 64 v -4 h 43.682 c 8.484 0 11.647 -5.776 14.599 -14.66 c 3.047 -9.145 2.916 -17.799 0 -29.529 z m -41.955 55.606 c 3.027 0 5.479 2.479 5.479 5.547 c 0 3.076 -2.451 5.579 -5.479 5.579 c -3.015 0 -5.478 -2.502 -5.478 -5.579 c 0 -3.068 2.463 -5.547 5.478 -5.547 z"
+ readonly property string rust: "M 62.96 .242 c -.232 .135 -1.203 1.528 -2.16 3.097 c -2.4 3.94 -2.426 3.942 -5.65 .55 c -2.098 -2.208 -2.605 -2.612 -3.28 -2.607 c -.44 .002 -.995 .152 -1.235 .332 c -.24 .18 -.916 1.612 -1.504 3.183 c -1.346 3.6 -1.41 3.715 -2.156 3.86 c -.46 .086 -1.343 -.407 -3.463 -1.929 c -1.565 -1.125 -3.1 -2.045 -3.411 -2.045 c -1.291 0 -1.655 .706 -2.27 4.4 c -.78 4.697 -.754 4.681 -4.988 2.758 c -1.71 -.776 -3.33 -1.41 -3.603 -1.41 c -.274 0 -.792 .293 -1.15 .652 c -.652 .652 -.653 .655 -.475 4.246 l .178 3.595 l -.68 .364 c -.602 .322 -1.017 .283 -3.684 -.348 c -3.48 -.822 -4.216 -.8 -4.92 .15 l -.516 .693 l .692 2.964 c .38 1.63 .745 3.2 .814 3.487 c .067 .287 -.05 .746 -.26 1.02 c -.348 .448 -.717 .49 -3.94 .44 c -5.452 -.086 -5.761 .382 -3.51 5.3 c .718 1.56 1.305 2.98 1.305 3.15 c 0 .898 -.717 1.224 -3.794 1.727 c -1.722 .28 -3.218 .51 -3.326 .51 c -.107 0 -.43 .235 -.717 .522 c -.937 .936 -.671 1.816 1.453 4.814 c 2.646 3.735 2.642 3.75 -1.73 5.421 c -4.971 1.902 -5.072 2.37 -1.287 5.96 c 3.525 3.344 3.53 3.295 -.461 5.804 C .208 62.8 .162 62.846 .085 63.876 c -.093 1.253 -.071 1.275 3.538 3.48 c 3.57 2.18 3.57 2.246 .067 5.56 C -.078 76.48 .038 77 5.013 78.877 c 4.347 1.64 4.353 1.66 1.702 5.394 c -1.502 2.117 -1.981 3 -1.981 3.653 c 0 1.223 .637 1.535 4.44 2.174 c 3.206 .54 3.92 .857 3.92 1.741 c 0 .182 -.588 1.612 -1.307 3.177 c -2.236 4.87 -1.981 5.275 3.31 5.275 c 4.93 0 4.799 -.15 3.737 4.294 c -.8 3.35 -.813 3.992 -.088 4.715 c .554 .556 1.6 .494 4.87 -.289 c 2.499 -.596 2.937 -.637 3.516 -.328 l .66 .354 l -.177 3.594 c -.178 3.593 -.177 3.595 .475 4.248 c .358 .36 .884 .652 1.165 .652 c .282 0 1.903 -.63 3.604 -1.404 c 4.22 -1.916 4.194 -1.932 4.973 2.75 c .617 3.711 .977 4.4 2.294 4.4 c .327 0 1.83 -.88 3.34 -1.958 c 2.654 -1.893 3.342 -2.19 4.049 -1.74 c .182 .115 .89 1.67 1.572 3.455 c 1.003 2.625 1.37 3.31 1.929 3.576 c 1.062 .51 1.72 .1 4.218 -2.62 c 3.016 -3.286 3.14 -3.27 5.602 .72 c 2.72 4.406 3.424 4.396 6.212 -.089 c 2.402 -3.864 2.374 -3.862 5.621 -.47 c 2.157 2.25 2.616 2.61 3.343 2.61 c .464 0 1.019 -.175 1.23 -.388 c .214 -.213 .92 -1.786 1.568 -3.496 c .649 -1.71 1.321 -3.2 1.495 -3.31 c .687 -.436 1.398 -.13 4.048 1.752 c 1.56 1.108 3.028 1.96 3.377 1.96 c 1.296 0 1.764 -.92 2.302 -4.535 c .46 -3.082 .554 -3.378 1.16 -3.685 c .596 -.302 .954 -.2 3.75 1.07 c 1.701 .77 3.323 1.402 3.604 1.402 c .282 0 .816 -.302 1.184 -.672 l .672 -.67 l -.184 -3.448 c -.177 -3.29 -.16 -3.468 .364 -3.943 c .54 -.488 .596 -.486 3.615 .204 c 3.656 .835 4.338 .857 5.025 .17 c .671 -.67 .664 -.818 -.254 -4.69 c -1.03 -4.346 -1.168 -4.19 3.78 -4.19 c 3.374 0 3.75 -.049 4.18 -.523 c .718 -.793 .547 -1.702 -.896 -4.779 c -.729 -1.55 -1.32 -2.96 -1.315 -3.135 c .024 -.914 .743 -1.227 4.065 -1.767 c 2.033 -.329 3.553 -.71 3.829 -.96 c .923 -.833 .584 -1.918 -1.523 -4.873 c -2.642 -3.703 -2.63 -3.738 1.599 -5.297 c 5.064 -1.866 5.209 -2.488 1.419 -6.09 c -3.51 -3.335 -3.512 -3.317 .333 -5.677 c 4.648 -2.853 4.655 -3.496 .082 -6.335 c -3.933 -2.44 -3.93 -2.406 -.405 -5.753 c 3.78 -3.593 3.678 -4.063 -1.295 -5.965 c -4.388 -1.679 -4.402 -1.72 -1.735 -5.38 c 1.588 -2.18 1.982 -2.903 1.982 -3.65 c 0 -1.306 -.586 -1.598 -4.436 -2.22 c -3.216 -.52 -3.924 -.835 -3.924 -1.75 c 0 -.174 .588 -1.574 1.307 -3.113 c 1.406 -3.013 1.604 -4.22 .808 -4.94 c -.428 -.387 -1 -.443 -4.067 -.392 c -3.208 .054 -3.618 .008 -4.063 -.439 c -.486 -.488 -.48 -.557 .278 -3.725 c .931 -3.88 .935 -3.975 .17 -4.694 c -.777 -.73 -1.262 -.718 -4.826 .121 c -2.597 .612 -3.027 .653 -3.617 .337 l -.67 -.36 l .185 -3.582 l .186 -3.58 l -.67 -.67 c -.369 -.37 -.891 -.67 -1.163 -.67 c -.27 0 -1.884 .64 -3.583 1.421 c -2.838 1.306 -3.143 1.393 -3.757 1.072 c -.612 -.32 -.714 -.637 -1.237 -3.829 c -.603 -3.693 -.977 -4.412 -2.288 -4.412 c -.311 0 -1.853 .925 -3.426 2.055 c -2.584 1.856 -2.93 2.032 -3.574 1.807 c -.533 -.186 -.843 -.59 -1.221 -1.599 c -.28 -.742 -.817 -2.172 -1.194 -3.177 c -.762 -2.028 -1.187 -2.482 -2.328 -2.482 c -.637 0 -1.213 .458 -3.28 2.604 c -3.25 3.375 -3.261 3.374 -5.65 -.545 C 66.073 1.78 65.075 .382 64.81 .24 c -.597 -.32 -1.3 -.32 -1.85 .002 m 2.96 11.798 c 2.83 2.014 1.326 6.75 -2.144 6.75 c -3.368 0 -5.064 -4.057 -2.66 -6.36 c 1.358 -1.3 3.304 -1.459 4.805 -.39 m -3.558 12.507 c 1.855 .705 2.616 .282 6.852 -3.8 l 3.182 -3.07 l 1.347 .18 c 4.225 .56 12.627 4.25 17.455 7.666 c 4.436 3.14 10.332 9.534 12.845 13.93 l .537 .942 l -2.38 5.364 c -1.31 2.95 -2.382 5.673 -2.382 6.053 c 0 .878 .576 2.267 1.13 2.726 c .234 .195 2.457 1.265 4.939 2.378 l 4.51 2.025 l .178 1.148 c .23 1.495 .26 5.167 .052 6.21 l -.163 .816 h -2.575 c -2.987 0 -2.756 -.267 -2.918 3.396 c -.118 2.656 -.76 4.124 -2.22 5.075 c -2.377 1.551 -6.304 1.27 -7.97 -.57 c -.255 -.284 -.752 -1.705 -1.105 -3.16 c -1.03 -4.254 -2.413 -6.64 -5.193 -8.965 c -.878 -.733 -1.595 -1.418 -1.595 -1.522 c 0 -.102 .965 -.915 2.145 -1.803 c 4.298 -3.24 6.77 -7.012 7.04 -10.747 c .519 -7.126 -5.158 -13.767 -13.602 -15.92 c -2.002 -.51 -2.857 -.526 -27.624 -.526 c -14.057 0 -25.56 -.092 -25.56 -.204 c 0 -.263 3.125 -3.295 4.965 -4.816 c 5.054 -4.178 11.618 -7.465 18.417 -9.22 l 2.35 -.61 l 3.34 3.387 c 1.839 1.863 3.64 3.5 4.003 3.637 M 20.3 46.34 c 1.539 1.008 2.17 3.54 1.26 5.062 c -1.405 2.356 -4.966 2.455 -6.373 .178 c -2.046 -3.309 1.895 -7.349 5.113 -5.24 m 90.672 .13 c 4.026 2.454 .906 8.493 -3.404 6.586 c -2.877 -1.273 -2.97 -5.206 -.155 -6.64 c 1.174 -.6 2.523 -.579 3.56 .053 M 32.163 61.5 v 15.02 h -13.28 l -.526 -2.285 c -1.036 -4.5 -1.472 -9.156 -1.211 -12.969 l .182 -2.679 l 4.565 -2.047 c 2.864 -1.283 4.706 -2.262 4.943 -2.625 c 1.038 -1.584 .94 -2.715 -.518 -5.933 l -.68 -1.502 h 6.523 V 61.5 M 70.39 47.132 c 2.843 .74 4.345 2.245 4.349 4.355 c .002 1.55 -.765 2.52 -2.67 3.38 c -1.348 .61 -1.562 .625 -10.063 .708 l -8.686 .084 v -8.92 h 7.782 c 6.078 0 8.112 .086 9.288 .393 m -2.934 21.554 c 1.41 .392 3.076 1.616 3.93 2.888 c .898 1.337 1.423 3.076 2.667 8.836 c 1.05 4.87 1.727 6.46 3.62 8.532 c 2.345 2.566 1.8 2.466 13.514 2.466 c 5.61 0 10.198 .09 10.198 .2 c 0 .197 -3.863 4.764 -4.03 4.764 c -.048 0 -2.066 -.422 -4.484 -.939 c -6.829 -1.458 -7.075 -1.287 -8.642 6.032 l -1.008 4.702 l -.91 .448 c -1.518 .75 -6.453 2.292 -9.01 2.82 c -4.228 .87 -8.828 1.162 -12.871 .821 c -6.893 -.585 -16.02 -3.259 -16.377 -4.8 c -.075 -.327 -.535 -2.443 -1.018 -4.704 c -.485 -2.26 -1.074 -4.404 -1.31 -4.764 c -1.13 -1.724 -2.318 -1.83 -7.547 -.674 c -1.98 .44 -3.708 .796 -3.84 .796 c -.248 0 -3.923 -4.249 -3.923 -4.535 c 0 -.09 8.728 -.194 19.396 -.23 l 19.395 -.066 l .07 -6.89 c .05 -4.865 -.018 -6.997 -.23 -7.25 c -.234 -.284 -1.485 -.358 -6.011 -.358 H 53.32 v -8.36 l 6.597 .001 c 3.626 .002 7.02 .12 7.539 .264 M 37.57 100.02 c 3.084 1.88 1.605 6.804 -2.043 6.8 c -3.74 0 -5.127 -4.88 -1.94 -6.826 c 1.055 -.643 2.908 -.63 3.983 .026 m 56.48 .206 c 1.512 1.108 2.015 3.413 1.079 4.95 c -2.46 4.034 -8.612 .827 -6.557 -3.419 c 1.01 -2.085 3.695 -2.837 5.478 -1.53"
+ readonly property string javascript: "M 2 1 v 125 h 125 V 1 H 2 z m 66.119 106.513 c -1.845 3.749 -5.367 6.212 -9.448 7.401 c -6.271 1.44 -12.269 .619 -16.731 -2.059 c -2.986 -1.832 -5.318 -4.652 -6.901 -7.901 l 9.52 -5.83 c .083 .035 .333 .487 .667 1.071 c 1.214 2.034 2.261 3.474 4.319 4.485 c 2.022 .69 6.461 1.131 8.175 -2.427 c 1.047 -1.81 .714 -7.628 .714 -14.065 C 58.433 78.073 58.48 68 58.48 58 h 11.709 c 0 11 .06 21.418 0 32.152 c .025 6.58 .596 12.446 -2.07 17.361 z m 48.574 -3.308 c -4.07 13.922 -26.762 14.374 -35.83 5.176 c -1.916 -2.165 -3.117 -3.296 -4.26 -5.795 c 4.819 -2.772 4.819 -2.772 9.508 -5.485 c 2.547 3.915 4.902 6.068 9.139 6.949 c 5.748 .702 11.531 -1.273 10.234 -7.378 c -1.333 -4.986 -11.77 -6.199 -18.873 -11.531 c -7.211 -4.843 -8.901 -16.611 -2.975 -23.335 c 1.975 -2.487 5.343 -4.343 8.877 -5.235 l 3.688 -.477 c 7.081 -.143 11.507 1.727 14.756 5.355 c .904 .916 1.642 1.904 3.022 4.045 c -3.772 2.404 -3.76 2.381 -9.163 5.879 c -1.154 -2.486 -3.069 -4.046 -5.093 -4.724 c -3.142 -.952 -7.104 .083 -7.926 3.403 c -.285 1.023 -.226 1.975 .227 3.665 c 1.273 2.903 5.545 4.165 9.377 5.926 c 11.031 4.474 14.756 9.271 15.672 14.981 c .882 4.916 -.213 8.105 -.38 8.581 z"
+ readonly property string typescript: "M 2 63.91 v 62.5 h 125 v -125 H 2 z m 100.73 -5 a 15.56 15.56 0 0 1 7.82 4.5 a 20.58 20.58 0 0 1 3 4 c 0 .16 -5.4 3.81 -8.69 5.85 c -.12 .08 -.6 -.44 -1.13 -1.23 a 7.09 7.09 0 0 0 -5.87 -3.53 c -3.79 -.26 -6.23 1.73 -6.21 5 a 4.58 4.58 0 0 0 .54 2.34 c .83 1.73 2.38 2.76 7.24 4.86 c 8.95 3.85 12.78 6.39 15.16 10 c 2.66 4 3.25 10.46 1.45 15.24 c -2 5.2 -6.9 8.73 -13.83 9.9 a 38.32 38.32 0 0 1 -9.52 -.1 A 23 23 0 0 1 80 109.19 c -1.15 -1.27 -3.39 -4.58 -3.25 -4.82 a 9.34 9.34 0 0 1 1.15 -.73 l 4.6 -2.64 l 3.59 -2.08 l .75 1.11 a 16.78 16.78 0 0 0 4.74 4.54 c 4 2.1 9.46 1.81 12.16 -.62 a 5.43 5.43 0 0 0 .69 -6.92 c -1 -1.39 -3 -2.56 -8.59 -5 c -6.45 -2.78 -9.23 -4.5 -11.77 -7.24 a 16.48 16.48 0 0 1 -3.43 -6.25 a 25 25 0 0 1 -.22 -8 c 1.33 -6.23 6 -10.58 12.82 -11.87 a 31.66 31.66 0 0 1 9.49 .26 z m -29.34 5.24 v 5.12 H 57.16 v 46.23 H 45.65 V 69.26 H 29.38 v -5 a 49.19 49.19 0 0 1 .14 -5.16 c .06 -.08 10 -.12 22 -.1 h 21.81 z"
+ readonly property string bash: "M 112.205 26.129 L 71.8 2.142 A 15.326 15.326 0 0 0 64.005 0 c -2.688 0 -5.386 .717 -7.796 2.152 L 15.795 26.14 C 10.976 28.999 8 34.289 8 40.018 v 47.975 c 0 5.729 2.967 11.019 7.796 13.878 L 56.2 125.858 A 15.193 15.193 0 0 0 63.995 128 a 15.32 15.32 0 0 0 7.796 -2.142 l 40.414 -23.987 c 4.819 -2.86 7.796 -8.16 7.796 -13.878 V 40.007 c 0 -5.718 -2.967 -11.019 -7.796 -13.878 z m -31.29 74.907 l .063 3.448 c 0 .418 -.267 .889 -.588 1.06 l -2.046 1.178 c -.321 .16 -.6 -.032 -.6 -.45 l -.032 -3.394 c -1.745 .728 -3.523 .9 -4.647 .45 c -.214 -.086 -.31 -.397 -.225 -.761 l .739 -3.116 c .064 -.246 .193 -.493 .364 -.643 a .725 .725 0 0 1 .193 -.139 c .117 -.064 .235 -.075 .332 -.032 c 1.22 .407 2.773 .214 4.272 -.535 c 1.907 -.964 3.18 -2.913 3.16 -4.84 c -.022 -1.757 -.964 -2.474 -3.267 -2.496 c -2.934 .01 -5.675 -.567 -5.718 -4.894 c -.032 -3.555 1.81 -7.26 4.744 -9.595 l -.032 -3.48 c 0 -.428 .257 -.9 .589 -1.07 l 1.98 -1.264 c .322 -.161 .6 .043 .6 .46 l .033 3.48 c 1.456 -.578 2.72 -.738 3.865 -.47 c .247 .064 .364 .406 .257 .802 l -.77 3.084 a 1.372 1.372 0 0 1 -.354 .622 a .825 .825 0 0 1 -.203 .15 c -.108 .053 -.204 .064 -.3 .053 c -.525 -.118 -1.767 -.385 -3.727 .6 c -2.056 1.038 -2.773 2.827 -2.763 4.155 c .022 1.585 .825 2.066 3.63 2.11 c 3.738 .064 5.344 1.691 5.387 5.45 c .053 3.684 -1.917 7.657 -4.937 10.077 z m 21.18 -5.794 c 0 .322 -.042 .621 -.31 .771 l -10.216 6.211 c -.267 .161 -.482 .022 -.482 -.3 V 99.29 c 0 -.321 .193 -.492 .46 -.653 l 10.067 -6.018 c .268 -.16 .482 -.022 .482 .3 z m 7.026 -58.993 L 70.89 59.86 c -4.765 2.784 -8.278 5.911 -8.288 11.662 v 47.107 c 0 3.437 1.392 5.665 3.523 6.318 a 12.81 12.81 0 0 1 -2.12 .204 c -2.239 0 -4.445 -.61 -6.383 -1.757 L 17.219 99.408 c -3.951 -2.345 -6.403 -6.725 -6.403 -11.426 V 40.007 c 0 -4.7 2.452 -9.08 6.403 -11.426 L 57.634 4.594 a 12.555 12.555 0 0 1 6.382 -1.756 c 2.238 0 4.444 .61 6.382 1.756 l 40.415 23.987 c 3.33 1.981 5.579 5.397 6.21 9.242 c -1.36 -2.86 -4.38 -3.63 -7.902 -1.574 z"
+ readonly property string zsh: "M 19.264 3.822 C 8.664 3.822 0 12.486 0 23.086 v 81.771 c 0 10.6 8.664 19.264 19.264 19.264 h 43.955 a 3.24 3.24 0 0 0 3.24 -3.24 a 3.24 3.24 0 0 0 -3.24 -3.24 H 19.264 a 12.736 12.736 0 0 1 -12.785 -12.784 v -72.26 h 115.043 v 68.471 a .713 .713 0 0 1 -.131 .186 a .716 .716 0 0 1 -.506 .21 h -3.965 a .716 .716 0 0 1 -.506 -.21 a .715 .715 0 0 1 -.209 -.506 V 88.84 l -.002 -.035 l .002 -.037 a 3.208 3.208 0 0 0 -3.207 -3.207 a 3.207 3.207 0 0 0 -2.267 .939 a 3.206 3.206 0 0 0 -.94 2.268 l .002 .037 l -.002 .035 v 32.13 c 0 .851 .338 1.667 .94 2.268 a 3.206 3.206 0 0 0 4.535 0 a 3.208 3.208 0 0 0 .94 -2.267 v -12.735 c 0 -.19 .074 -.371 .208 -.505 a .715 .715 0 0 1 .506 -.21 h 3.965 c .19 0 .372 .076 .506 .21 a .716 .716 0 0 1 .13 .185 v 12.965 a 3.24 3.24 0 0 0 .034 .12 c .003 .267 .033 .535 .101 .794 a 3.187 3.187 0 0 0 .64 1.23 a 3.2 3.2 0 0 0 1.104 .844 a 3.189 3.189 0 0 0 1.356 .297 l .014 -.002 l .011 .002 a 3.18 3.18 0 0 0 2.94 -1.965 c .009 -.02 .013 -.042 .021 -.062 a 3.24 3.24 0 0 0 .03 -.078 a 3.18 3.18 0 0 0 .095 -.313 a 3.24 3.24 0 0 0 .133 -.867 V 23.086 c 0 -10.6 -8.662 -19.264 -19.262 -19.264 H 19.264 z m 0 6.479 h 89.474 a 12.736 12.736 0 0 1 12.783 12.785 v 3.033 H 6.478 v -3.033 a 12.737 12.737 0 0 1 12.786 -12.785 z m 14.658 28.515 a 1.138 1.138 0 0 0 -.774 .4 l -21.14 25.178 a 1.138 1.138 0 0 0 .14 1.604 a 1.138 1.138 0 0 0 1.602 -.139 l 21.143 -25.177 a 1.138 1.138 0 0 0 -.141 -1.604 a 1.138 1.138 0 0 0 -.83 -.262 z m -15.68 .637 c -3.37 0 -6.14 2.772 -6.14 6.143 s 2.77 6.14 6.14 6.14 c 3.37 0 6.14 -2.77 6.14 -6.14 c 0 -3.371 -2.77 -6.143 -6.14 -6.143 z m 0 3.557 c 1.45 0 2.586 1.137 2.586 2.586 s -1.137 2.586 -2.586 2.586 a 2.56 2.56 0 0 1 -2.586 -2.586 a 2.56 2.56 0 0 1 2.586 -2.586 z m 10.344 10.344 c -3.37 0 -6.143 2.77 -6.143 6.14 c 0 3.37 2.772 6.14 6.143 6.14 c 3.37 0 6.14 -2.77 6.14 -6.14 s -2.77 -6.14 -6.14 -6.14 z m 0 3.554 a 2.56 2.56 0 0 1 2.586 2.586 a 2.56 2.56 0 0 1 -2.586 2.586 A 2.56 2.56 0 0 1 26 59.494 a 2.56 2.56 0 0 1 2.586 -2.586 z m 14.18 6.451 a 1.138 1.138 0 0 0 -1.139 1.137 a 1.138 1.138 0 0 0 1.139 1.139 H 57.86 A 1.138 1.138 0 0 0 59 64.496 a 1.138 1.138 0 0 0 -1.139 -1.137 H 42.766 z m 30.767 22.186 a 3.253 3.253 0 0 0 -1.244 .248 a 3.265 3.265 0 0 0 -1.057 .705 a 3.251 3.251 0 0 0 -.703 1.055 a 3.25 3.25 0 0 0 -.248 1.244 v .002 a 3.24 3.24 0 0 0 .951 2.299 a 3.246 3.246 0 0 0 1.057 .703 a 3.241 3.241 0 0 0 1.244 .248 c .051 0 .1 -.006 .15 -.008 h 5.99 a .404 .404 0 0 1 .335 .176 a .41 .41 0 0 1 .066 .183 a .408 .408 0 0 1 -.027 .194 l -9.51 23.748 a 3.58 3.58 0 0 0 -.256 1.33 v 2.912 c 0 .948 .377 1.857 1.047 2.527 a 3.58 3.58 0 0 0 2.53 1.05 h 11.146 a 3.334 3.334 0 0 0 1.275 -.255 a 3.343 3.343 0 0 0 1.082 -.722 a 3.327 3.327 0 0 0 .723 -1.082 a 3.326 3.326 0 0 0 .254 -1.276 v -.002 a 3.338 3.338 0 0 0 -.977 -2.36 a 3.338 3.338 0 0 0 -2.359 -.976 c -.026 0 -.05 .003 -.076 .004 h -7.498 a .387 .387 0 0 1 -.182 -.045 a .38 .38 0 0 1 -.137 -.125 a .39 .39 0 0 1 -.066 -.176 a .382 .382 0 0 1 .027 -.183 l 10.967 -27.344 a 2.964 2.964 0 0 0 .195 -1.418 a 2.955 2.955 0 0 0 -.49 -1.344 a 2.97 2.97 0 0 0 -1.066 -.957 a 2.961 2.961 0 0 0 -1.389 -.345 H 73.728 c -.065 -.004 -.128 -.01 -.195 -.01 z m 26.192 .18 c -1.523 0 -3.266 .147 -4.315 .441 a 5.042 5.042 0 0 0 -2.486 1.424 c -.609 .687 -1.05 1.619 -1.32 2.797 c -.272 1.145 -.405 2.602 -.405 4.369 c 0 1.701 .015 3.076 .049 4.123 a 19.145 19.145 0 0 0 .305 2.553 a 4.957 4.957 0 0 0 .66 1.57 a 6.3 6.3 0 0 0 1.015 1.129 a 10.337 10.337 0 0 0 1.725 1.228 c .677 .393 1.372 .767 2.082 1.127 a 29.432 29.432 0 0 1 2.03 1.13 a 8.647 8.647 0 0 1 1.675 1.228 c .2 .217 .386 .447 .557 .687 c .123 .146 .22 .311 .283 .49 c .059 .16 .11 .325 .148 .49 c .056 .26 .082 .522 .079 .786 c .033 .392 .048 .934 .048 1.62 a 20.153 20.153 0 0 1 -.15 2.798 a 3.99 3.99 0 0 1 -.406 1.521 a 1.397 1.397 0 0 1 -.813 .639 c -.338 .098 -1.508 .146 -2.015 .146 a 4.42 4.42 0 0 1 -1.219 -.146 a 1.676 1.676 0 0 1 -.76 -.688 a 5.713 5.713 0 0 1 -.406 -1.472 a 23.667 23.667 0 0 1 -.104 -2.45 c 0 -.09 -.004 -.182 -.013 -.273 h -.02 a 2.728 2.728 0 0 0 -.918 -1.678 a 2.757 2.757 0 0 0 -1.8 -.668 a 2.747 2.747 0 0 0 -1.798 .668 a 2.728 2.728 0 0 0 -.917 1.678 h -.018 a 2.6 2.6 0 0 0 -.014 .301 v .123 c .009 .645 .018 1.223 .018 1.514 c 0 1.93 .136 3.501 .406 4.713 c .305 1.177 .78 2.11 1.422 2.796 a 4.887 4.887 0 0 0 2.436 1.327 a 14.194 14.194 0 0 0 3.603 .392 c 1.625 0 3.724 -.147 4.807 -.441 a 4.78 4.78 0 0 0 2.588 -1.668 c .676 -.818 1.15 -1.949 1.422 -3.389 c .304 -1.472 .457 -3.354 .457 -5.644 c 0 -1.67 -.051 -3.011 -.153 -4.026 a 8.816 8.816 0 0 0 -.457 -2.504 a 4.07 4.07 0 0 0 -.965 -1.57 a 7.968 7.968 0 0 0 -1.47 -1.178 a 14.43 14.43 0 0 0 -1.524 -.834 a 23.424 23.424 0 0 0 -1.826 -.933 a 44.885 44.885 0 0 1 -1.828 -.98 a 15.28 15.28 0 0 1 -1.47 -1.032 a 5 5 0 0 1 -.61 -.59 a 2.125 2.125 0 0 1 -.305 -.736 a 7.721 7.721 0 0 1 -.152 -1.13 V 95.64 a 17.363 17.363 0 0 1 .101 -2.112 a 3.367 3.367 0 0 1 .356 -1.226 a 1.222 1.222 0 0 1 .76 -.54 c .304 -.098 1.153 -.148 1.627 -.148 c 1.082 0 1.758 .312 2.029 .934 c .214 .393 .335 .828 .353 1.275 c .012 .358 .017 .824 .018 1.31 h .02 v .02 c 0 .726 .289 1.421 .804 1.934 a 2.753 2.753 0 0 0 1.944 .8 c .728 0 1.428 -.287 1.943 -.8 a 2.728 2.728 0 0 0 .805 -1.934 l -.002 -.02 h .002 v -1.31 a 16.978 16.978 0 0 0 -.356 -3.779 c -.203 -1.047 -.61 -1.88 -1.219 -2.502 c -.575 -.655 -1.387 -1.113 -2.435 -1.375 c -1.015 -.294 -2.317 -.441 -3.906 -.441 h -.002 z"
+ readonly property string powershell: "M 124.912 19.358 c -.962 -1.199 -2.422 -1.858 -4.111 -1.858 h -92.61 c -3.397 0 -6.665 2.642 -7.444 6.015 L 2.162 104.022 c -.396 1.711 -.058 3.394 .926 4.619 c .963 1.199 2.423 1.858 4.111 1.858 v .001 H 99.81 c 3.396 0 6.665 -2.643 7.443 -6.016 l 18.586 -80.508 c .395 -1.711 .057 -3.395 -.927 -4.618 z m -98.589 77.17 c -1.743 -2.397 -1.323 -5.673 .94 -7.318 l 37.379 -27.067 v -.556 L 41.157 36.603 c -1.916 -2.038 -1.716 -5.333 .445 -7.361 c 2.162 -2.027 5.466 -2.019 7.382 .019 l 28.18 29.979 c 1.6 1.702 1.718 4.279 .457 6.264 c -.384 .774 -1.182 1.628 -2.593 2.618 l -41.45 29.769 c -2.263 1.644 -5.512 1.034 -7.255 -1.363 z m 59.543 .538 H 63.532 c -2.597 0 -4.702 -2.082 -4.702 -4.65 s 2.105 -4.65 4.702 -4.65 h 22.333 c 2.597 0 4.702 2.082 4.702 4.65 s -2.104 4.65 -4.701 4.65 z"
+ readonly property string cmake: "M 62.8 .4 L .3 123.8 l 68.1 -57.9 z m 61 127.3 l -84 -33.9 L 0 127.7 z m 4.2 -1.1 L 65.6 2.5 l 9.2 102.6 z M 71.9 104 l -3.1 -34.9 L 42 92 z"
+ readonly property string lua: "M 61.7 0 c -1.9 0 -3.8 .2 -5.6 .4 l .2 1.5 c 1.8 -.2 3.6 -.4 5.5 -.4 L 61.7 0 z m 5.6 0 l -.1 1.5 c 1.8 .1 3.6 .3 5.4 .5 l .3 -1.5 C 71 .3 69.2 .1 67.3 0 z m 45.7 .8 c -7.9 0 -14.4 6.3 -14.4 14.3 S 105 29.4 113 29.4 s 14.3 -6.4 14.3 -14.3 S 120.9 .8 113 .8 z m -62.4 .6 c -1.8 .4 -3.6 .9 -5.4 1.4 l .4 1.4 c 1.7 -.5 3.5 -1 5.3 -1.4 l -.3 -1.4 z m 27.6 .3 l -.3 1.4 c 1.8 .4 3.6 .8 5.3 1.4 l .4 -1.3 c -1.8 -.6 -3.6 -1.1 -5.4 -1.5 z m -38.3 3 c -1.7 .7 -3.4 1.5 -5.1 2.3 l .7 1.3 c 1.6 -.8 3.3 -1.6 5 -2.3 l -.6 -1.3 z m 49 .3 l -.5 1.4 c 1.6 .7 3.3 1.5 4.9 2.3 l .6 -1.3 c -1.6 -.9 -3.3 -1.7 -5 -2.4 z M 30 9.7 c -1.6 1 -3.1 2.1 -4.6 3.2 l .9 1.2 c 1.4 -1.1 2.9 -2.1 4.5 -3.2 L 30 9.7 z m 34 5.4 c -27 0 -49 21.9 -49 49 s 21.9 49 49 49 s 49 -21.9 49 -49 s -22 -49 -49 -49 z m -42.9 1.4 c -1.4 1.2 -2.7 2.5 -4 3.9 l 1.1 1 c 1.2 -1.3 2.5 -2.6 3.9 -3.8 l -1 -1.1 z m -7.6 8.2 c -1.1 1.4 -2.2 2.9 -3.2 4.5 l 1.2 .8 c 1 -1.5 2 -3 3.2 -4.4 l -1.2 -.9 z m 70.8 4.7 c 7.9 0 14.3 6.4 14.3 14.3 S 92.2 58 84.3 58 S 70 51.6 70 43.7 s 6.4 -14.3 14.3 -14.3 z M 7.4 34.1 c -.9 1.6 -1.7 3.3 -2.4 5 l 1.4 .5 c .7 -1.6 1.5 -3.3 2.3 -4.8 l -1.3 -.7 z m 113.6 .8 l -1.3 .7 c .9 1.6 1.6 3.3 2.3 5 l 1.3 -.6 c -.7 -1.7 -1.5 -3.4 -2.3 -5.1 z M 3.1 44.3 c -.6 1.8 -1.1 3.6 -1.5 5.4 L 3 50 c .4 -1.8 .9 -3.5 1.5 -5.2 l -1.4 -.5 z m 122.1 1 l -1.4 .4 c .6 1.7 1 3.5 1.4 5.3 l 1.4 -.3 c -.4 -1.8 -.9 -3.6 -1.4 -5.4 z M .5 55.1 C .3 57 .1 58.8 0 60.7 l 1.5 .1 c .1 -1.8 .3 -3.6 .5 -5.4 l -1.5 -.3 z m 127.1 1.1 l -1.5 .2 c .2 1.8 .3 3.6 .4 5.4 h 1.5 c 0 -1.9 -.2 -3.8 -.4 -5.6 z m -96.9 .2 h 4.1 v 28.5 h 15.9 v 3.6 h -20 V 56.4 z m 57.7 8.3 c 5.7 0 8.7 2.2 8.7 6.3 v 13.6 c 0 1.1 .7 1.8 2 1.8 c .2 0 .4 0 .8 -.1 l -.1 2.8 c -1.2 .3 -1.8 .4 -2.5 .4 c -2.4 0 -3.5 -1.1 -3.8 -3.4 c -2.6 2.4 -4.9 3.4 -7.8 3.4 c -4.7 0 -7.6 -2.6 -7.6 -6.8 c 0 -3 1.4 -5.1 4.1 -6.2 c 1.4 -.6 2.2 -.7 7.4 -1.4 c 2.9 -.4 3.8 -1 3.8 -2.6 v -1 c 0 -2.2 -1.9 -3.4 -5.2 -3.4 c -3.4 0 -5.1 1.3 -5.4 4.1 h -3.7 c .1 -2.3 .5 -3.6 1.6 -4.8 c 1.5 -1.7 4.3 -2.7 7.7 -2.7 z m -33.8 .7 h 3.7 v 16.3 c 0 2.8 1.9 4.5 4.8 4.5 c 3.8 0 6.3 -3.1 6.3 -7.8 v -13 H 73 v 23.1 h -3.3 v -3.2 c -2.2 3 -4.3 4.2 -7.7 4.2 c -4.5 0 -7.4 -2.5 -7.4 -6.3 V 65.4 z m -53.1 .8 l -1.5 .1 c .1 1.9 .2 3.7 .5 5.6 l 1.4 -.3 c -.2 -1.8 -.3 -3.6 -.4 -5.4 z m 124.9 1.1 c -.1 1.8 -.3 3.6 -.5 5.4 l 1.5 .2 c .3 -1.8 .5 -3.7 .5 -5.5 l -1.5 -.1 z M 2.8 77.1 l -1.4 .3 c .4 1.8 .9 3.6 1.4 5.4 l 1.4 -.4 c -.6 -1.8 -1 -3.5 -1.4 -5.3 z m 90.6 0 c -1.2 .6 -2 .7 -5.9 1.3 c -3.9 .5 -5.6 1.8 -5.6 4.2 c 0 2.3 1.7 3.7 4.5 3.7 c 2.2 0 4 -.7 5.5 -2.1 c 1.1 -1 1.5 -1.8 1.5 -3 v -4.1 z m 31.6 .9 c -.5 1.8 -.9 3.6 -1.5 5.3 l 1.4 .5 c .6 -1.8 1.1 -3.6 1.5 -5.5 L 125 78 z M 6 87.5 l -1.3 .5 c .7 1.7 1.5 3.4 2.3 5.1 l 1.3 -.6 c -.9 -1.7 -1.6 -3.3 -2.3 -5 z m 115.7 1 c -.8 1.6 -1.5 3.3 -2.4 4.9 l 1.3 .7 L 123 89 l -1.3 -.5 z M 10.9 97.2 l -1.2 .8 c 1 1.6 2.1 3.1 3.2 4.6 l 1.1 -.9 c -1.1 -1.5 -2.1 -3 -3.1 -4.5 z m 105.6 .9 c -1 1.5 -2.1 3 -3.2 4.4 l 1.2 .9 c 1.1 -1.4 2.2 -3 3.2 -4.5 l -1.2 -.8 z m -98.9 7.8 l -1.1 1 c 1.2 1.4 2.5 2.7 3.9 4 l 1 -1.1 c -1.3 -1.2 -2.6 -2.6 -3.8 -3.9 z m 92.2 .8 c -1.2 1.3 -2.6 2.6 -3.9 3.8 l 1 1.1 c 1.3 -1.3 2.7 -2.6 4 -3.9 l -1.1 -1 z m -84.2 6.6 l -.9 1.2 c 1.4 1.1 2.9 2.2 4.5 3.2 l .8 -1.2 c -1.5 -1 -3 -2.1 -4.4 -3.2 z m 76.1 .7 c -1.5 1.1 -3 2.1 -4.5 3.1 l .8 1.2 c 1.5 -1 3.1 -2 4.6 -3.1 l -.9 -1.2 z m -67 5.3 l -.7 1.3 c 1.6 .9 3.3 1.7 5 2.4 l .6 -1.3 c -1.6 -.8 -3.3 -1.5 -4.9 -2.4 z m 57.7 .4 c -1.7 .9 -3.3 1.6 -5 2.3 l .6 1.4 c 1.7 -.7 3.4 -1.5 5.1 -2.4 l -.7 -1.3 z m -47.7 3.8 l -.5 1.4 c 1.8 .6 3.6 1.1 5.4 1.5 l .4 -1.4 c -1.8 -.5 -3.6 -.9 -5.3 -1.5 z m 37.6 .3 c -1.8 .6 -3.5 1 -5.3 1.4 l .3 1.4 c 1.9 -.3 3.7 -.8 5.4 -1.4 l -.4 -1.4 z m -27 2.2 l -.2 1.5 c 1.9 .2 3.7 .4 5.6 .5 v -1.5 c -1.8 -.1 -3.6 -.3 -5.4 -.5 z m 16.3 .1 c -1.8 .2 -3.6 .3 -5.4 .4 l .1 1.5 c 1.8 -.1 3.7 -.2 5.5 -.4 l -.2 -1.5 z"
+ readonly property string java: "M 47.617 98.12 c -19.192 5.362 11.677 16.439 36.115 5.969 c -4.003 -1.556 -6.874 -3.351 -6.874 -3.351 c -10.897 2.06 -15.952 2.222 -25.844 1.092 c -8.164 -.935 -3.397 -3.71 -3.397 -3.71 z m 33.189 -10.46 c -14.444 2.779 -22.787 2.69 -33.354 1.6 c -8.171 -.845 -2.822 -4.805 -2.822 -4.805 c -21.137 7.016 11.767 14.977 41.309 6.336 c -3.14 -1.106 -5.133 -3.131 -5.133 -3.131 z m 11.319 -60.575 c .001 0 -42.731 10.669 -22.323 34.187 c 6.024 6.935 -1.58 13.17 -1.58 13.17 s 15.289 -7.891 8.269 -17.777 c -6.559 -9.215 -11.587 -13.793 15.634 -29.58 z m 9.998 81.144 s 3.529 2.91 -3.888 5.159 c -14.102 4.272 -58.706 5.56 -71.095 .171 c -4.45 -1.938 3.899 -4.625 6.526 -5.192 c 2.739 -.593 4.303 -.485 4.303 -.485 c -4.952 -3.487 -32.013 6.85 -13.742 9.815 c 49.821 8.076 90.817 -3.637 77.896 -9.468 z M 85 77.896 c 2.395 -1.634 5.703 -3.053 5.703 -3.053 s -9.424 1.685 -18.813 2.474 c -11.494 .964 -23.823 1.154 -30.012 .326 c -14.652 -1.959 8.033 -7.348 8.033 -7.348 s -8.812 -.596 -19.644 4.644 C 17.455 81.134 61.958 83.958 85 77.896 z m 5.609 15.145 c -.108 .29 -.468 .616 -.468 .616 c 31.273 -8.221 19.775 -28.979 4.822 -23.725 c -1.312 .464 -2 1.543 -2 1.543 s .829 -.334 2.678 -.72 c 7.559 -1.575 18.389 10.119 -5.032 22.286 z M 64.181 70.069 c -4.614 -10.429 -20.26 -19.553 .007 -35.559 C 89.459 14.563 76.492 1.587 76.492 1.587 c 5.23 20.608 -18.451 26.833 -26.999 39.667 c -5.821 8.745 2.857 18.142 14.688 28.815 z m 27.274 51.748 c -19.187 3.612 -42.854 3.191 -56.887 .874 c 0 0 2.874 2.38 17.646 3.331 c 22.476 1.437 57 -.8 57.816 -11.436 c .001 0 -1.57 4.032 -18.575 7.231 z"
+ readonly property string kotlin: "M 112.484 112.484 H 15.516 V 15.516 h 96.968 L 64 64 Z m 0 0"
+ readonly property string swift: "M 125.54 26.23 a 28.78 28.78 0 0 0 -2.65 -7.58 a 28.84 28.84 0 0 0 -4.76 -6.32 a 23.42 23.42 0 0 0 -6.62 -4.55 a 27.27 27.27 0 0 0 -7.68 -2.53 c -2.65 -.51 -5.56 -.51 -8.21 -.76 H 30.25 a 45.46 45.46 0 0 0 -6.09 .51 a 21.81 21.81 0 0 0 -5.82 1.52 c -.53 .25 -1.32 .51 -1.85 .76 a 33.82 33.82 0 0 0 -5 3.28 c -.53 .51 -1.06 .76 -1.59 1.26 a 22.41 22.41 0 0 0 -4.76 6.32 a 23.61 23.61 0 0 0 -2.65 7.58 a 78.47 78.47 0 0 0 -.79 7.83 v 60.39 a 39.32 39.32 0 0 0 .79 7.83 a 28.78 28.78 0 0 0 2.65 7.58 a 28.84 28.84 0 0 0 4.76 6.32 a 23.42 23.42 0 0 0 6.62 4.55 a 27.27 27.27 0 0 0 7.68 2.53 c 2.65 .51 5.56 .51 8.21 .76 h 63.22 a 45.08 45.08 0 0 0 8.21 -.76 a 27.27 27.27 0 0 0 7.68 -2.53 a 30.13 30.13 0 0 0 6.62 -4.55 a 22.41 22.41 0 0 0 4.76 -6.32 a 23.61 23.61 0 0 0 2.65 -7.58 a 78.47 78.47 0 0 0 .79 -7.83 V 34.06 a 39.32 39.32 0 0 0 -.8 -7.83 z m -18.79 75.54 C 101 91 90.37 94.33 85 96.5 c -11.11 6.13 -26.38 6.76 -41.75 .47 A 64.53 64.53 0 0 1 13.84 73 a 50 50 0 0 0 10.85 6.32 c 15.87 7.1 31.73 6.61 42.9 0 c -15.9 -11.66 -29.4 -26.82 -39.46 -39.2 a 43.47 43.47 0 0 1 -5.29 -6.82 c 12.16 10.61 31.5 24 38.38 27.79 a 271.77 271.77 0 0 1 -27 -32.34 a 266.8 266.8 0 0 0 44.47 34.87 c .71 .38 1.26 .7 1.7 1 a 32.71 32.71 0 0 0 1.21 -3.51 c 3.71 -12.89 -.53 -27.54 -9.79 -39.67 C 93.25 33.81 106 57.05 100.66 76.51 c -.14 .53 -.29 1 -.45 1.55 l .19 .22 c 10.6 12.63 7.67 26.02 6.35 23.49 z"
+ readonly property string go: "M 108.2 64.8 c -.1 -.1 -.2 -.2 -.4 -.2 l -.1 -.1 c -.1 -.1 -.2 -.1 -.2 -.2 l -.1 -.1 c -.1 0 -.2 -.1 -.2 -.1 l -.2 -.1 c -.1 0 -.2 -.1 -.2 -.1 l -.2 -.1 c -.1 0 -.2 -.1 -.2 -.1 c -.1 0 -.1 0 -.2 -.1 l -.3 -.1 c -.1 0 -.1 0 -.2 -.1 l -.3 -.1 h -.1 l -.4 -.1 h -.2 c -.1 0 -.2 0 -.3 -.1 h -2.3 c -.6 -13.3 .6 -26.8 -2.8 -39.6 c 12.9 -4.6 2.8 -22.3 -8.4 -14.4 c -7.4 -6.4 -17.6 -7.8 -28.3 -7.8 c -10.5 .7 -20.4 2.9 -27.4 8.4 c -2.8 -1.4 -5.5 -1.8 -7.9 -1.1 v .1 c -.1 0 -.3 .1 -.4 .2 c -.1 0 -.3 .1 -.4 .2 h -.1 c -.1 0 -.2 .1 -.4 .2 h -.1 l -.3 .2 h -.1 l -.3 .2 h -.1 l -.3 .2 s -.1 0 -.1 .1 l -.3 .2 s -.1 0 -.1 .1 l -.3 .2 s -.1 0 -.1 .1 l -.3 .2 l -.1 .1 c -.1 .1 -.2 .1 -.2 .2 l -.1 .1 l -.2 .2 l -.1 .1 c -.1 .1 -.1 .2 -.2 .2 l -.1 .1 c -.1 .1 -.1 .2 -.2 .2 l -.1 .1 c -.1 .1 -.1 .2 -.2 .2 l -.1 .1 c -.1 .1 -.1 .2 -.2 .2 l -.1 .1 c -.1 .1 -.1 .2 -.2 .2 l -.1 .1 l -.1 .3 s 0 .1 -.1 .1 l -.1 .3 s 0 .1 -.1 .1 l -.1 .3 s 0 .1 -.1 .1 l -.1 .3 s 0 .1 -.1 .1 c .4 .3 .4 .4 .4 .4 v .1 l -.1 .3 v .1 c 0 .1 0 .2 -.1 .3 v 3.1 c 0 .1 0 .2 .1 .3 v .1 l .1 .3 v .1 l .1 .3 s 0 .1 .1 .1 l .1 .3 s 0 .1 .1 .1 l .1 .3 s 0 .1 .1 .1 l .2 .3 s 0 .1 .1 .1 l .2 .3 s 0 .1 .1 .1 l .2 .3 l .1 .1 l .3 .3 l .3 .3 h .1 c 1 .9 2 1.6 4 2.2 v -.2 C 23 37.3 26.5 50 26.7 63 c -.6 0 -.7 .4 -1.7 .5 h -.5 c -.1 0 -.3 0 -.5 .1 c -.1 0 -.3 0 -.4 .1 l -.4 .1 h -.1 l -.4 .1 h -.1 l -.3 .1 h -.1 l -.3 .1 s -.1 0 -.1 .1 l -.3 .1 l -.2 .1 c -.1 0 -.2 .1 -.2 .1 l -.2 .1 l -.2 .1 c -.1 0 -.2 .1 -.2 .1 l -.2 .1 l -.4 .3 c -.1 .1 -.2 .2 -.3 .2 l -.4 .4 l -.1 .1 c -.1 .2 -.3 .4 -.4 .5 l -.2 .3 l -.3 .6 l -.1 .3 v .3 c 0 .5 .2 .9 .9 1.2 c .2 3.7 3.9 2 5.6 .8 l .1 -.1 c .2 -.2 .5 -.3 .6 -.3 h .1 l .2 -.1 c .1 0 .1 0 .2 -.1 c .2 -.1 .4 -.1 .5 -.2 c .1 0 .1 -.1 .1 -.2 l .1 -.1 c .1 -.2 .2 -.6 .2 -1.2 l .1 -1.3 v 1.8 c -.5 13.1 -4 30.7 3.3 42.5 c 1.3 2.1 2.9 3.9 4.7 5.4 h -.5 c -.2 .2 -.5 .4 -.8 .6 l -.9 .6 l -.3 .2 l -.6 .4 l -.9 .7 l -1.1 1 c -.2 .2 -.3 .4 -.4 .5 l -.4 .6 l -.2 .3 c -.1 .2 -.2 .4 -.2 .6 l -.1 .3 c -.2 .8 0 1.7 .6 2.7 l .4 .4 h .2 c .1 0 .2 0 .4 .1 c .2 .4 1.2 2.5 3.9 .9 c 2.8 -1.5 4.7 -4.6 8.1 -5.1 l -.5 -.6 c 5.9 2.8 12.8 4 19 4.2 c 8.7 .3 18.6 -.9 26.5 -5.2 c 2.2 .7 3.9 3.9 5.8 5.4 l .1 .1 l .1 .1 l .1 .1 l .1 .1 s .1 0 .1 .1 c 0 0 .1 0 .1 .1 c 0 0 .1 0 .1 .1 h 2.1 s .1 0 .1 -.1 h .1 s .1 0 .1 -.1 h .1 s .1 0 .1 -.1 c 0 0 .1 0 .1 -.1 l .1 -.1 s .1 0 .1 -.1 l .1 -.1 h .1 l .2 -.2 l .2 -.1 h .1 l .1 -.1 h .1 l .1 -.1 l .1 -.1 l .1 -.1 l .1 -.1 l .1 -.1 l .1 -.1 l .1 -.1 v -.1 s 0 -.1 .1 -.1 v -.1 s 0 -.1 .1 -.1 v -.1 s 0 -.1 .1 -.1 v -1.4 s -.3 0 -.3 -.1 l -.3 -.1 v -.1 l .3 -.1 s .2 0 .2 -.1 l .1 -.1 v -2.1 s 0 -.1 -.1 -.1 v -.1 s 0 -.1 -.1 -.1 v -.1 s 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 c 0 0 0 -.1 -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 v -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 l -.1 -.1 c 2 -1.9 3.8 -4.2 5.1 -6.9 c 5.9 -11.8 4.9 -26.2 4.1 -39.2 h .1 c .1 0 .2 .1 .2 .1 h .3 s .1 0 .1 .1 h .1 s .1 0 .1 .1 l .2 .1 c 1.7 1.2 5.4 2.9 5.6 -.8 c 1.6 .6 -.3 -1.8 -1.3 -2.5 z M 36 23 C 32.8 7 58.4 4 59.3 19.6 c .8 13 -20 16.3 -23.3 3.4 z m 36.1 15 c -1.3 1.4 -2.7 1.2 -4.1 .7 c 0 1.9 .4 3.9 .1 5.9 c -.5 .9 -1.5 1 -2.3 1.4 c -1.2 -.2 -2.1 -.9 -2.6 -2 l -.2 -.1 c -3.9 5.2 -6.3 -1.1 -5.2 -5 c -1.2 .1 -2.2 -.2 -3 -1.5 c -1.4 -2.6 .7 -5.8 3.4 -6.3 c .7 3 8.7 2.6 10.1 -.2 c 3.1 1.5 6.5 4.3 3.8 7.1 z m -7 -17.5 c -.9 -13.8 20.3 -17.5 23.4 -4 c 3.5 15 -20.8 18.9 -23.4 4 z M 41.7 17 c -1.9 0 -3.5 1.7 -3.5 3.8 c 0 2.1 1.6 3.8 3.5 3.8 s 3.5 -1.7 3.5 -3.8 c 0 -2.1 -1.5 -3.8 -3.5 -3.8 z m 1.6 5.7 c -.5 0 -.8 -.4 -.8 -1 c 0 -.5 .4 -1 .8 -1 c .5 0 .8 .4 .8 1 c 0 .5 -.3 1 -.8 1 z m 27.8 -6.6 c -1.9 0 -3.4 1.7 -3.4 3.8 c 0 2.1 1.5 3.8 3.4 3.8 s 3.4 -1.7 3.4 -3.8 c 0 -2.1 -1.5 -3.8 -3.4 -3.8 z m 1.6 5.6 c -.4 0 -.8 -.4 -.8 -1 c 0 -.5 .4 -1 .8 -1 s .8 .4 .8 1 s -.4 1 -.8 1 z"
+ readonly property string dart: "M 86.6 25 l 3 .1 c 1.1 .1 2.2 .3 3.4 .5 l -2.5 -7.4 L 75.7 3.5 c -3.4 -3.4 -8 -4.4 -10.4 -2.3 L 29.2 25.1 l 57.4 -.1 z m 6.1 3.6 c -1.2 -.2 -2.3 -.4 -3.3 -.5 l -2.9 -.1 l -56 .1 l 78.6 78.6 l 6.1 -13.8 l -22.5 -64.3 z M 28.9 92.2 l 64.3 22.7 l 13.8 -6.1 l -78.6 -78.6 v 56.1 l .1 2.7 c 0 .9 .1 2 .4 3.2 z M 106.9 34.3 c -2.6 -2.6 -7 -5.1 -11.3 -6.5 L 118.4 93 l -6.9 15.7 l 15.8 -5.2 V 54.8 l -20.4 -20.5 z m -13.5 83.8 l -65 -22.9 c 1.4 4.3 3.8 8.7 6.5 11.4 l 21.3 21.2 l 47.6 .1 l 5.3 -16.7 l -15.7 6.9 z m -67.9 -29 l -.1 -2.7 V 28.9 L 1.7 65.1 C -.4 67.3 .7 72 4 75.5 l 14.7 14.8 l 7.3 2.6 c -.3 -1.3 -.5 -2.5 -.5 -3.8 z"
+ readonly property string php: "M 64 30.332 C 28.654 30.332 0 45.407 0 64 s 28.654 33.668 64 33.668 c 35.345 0 64 -15.075 64 -33.668 S 99.346 30.332 64 30.332 z m -5.982 9.81 h 7.293 v .003 l -1.745 8.968 h 6.496 c 4.087 0 6.908 .714 8.458 2.139 c 1.553 1.427 2.017 3.737 1.398 6.93 l -3.053 15.7 h -7.408 l 2.902 -14.929 c .33 -1.698 .208 -2.855 -.365 -3.473 c -.573 -.617 -1.793 -.925 -3.658 -.925 h -5.828 L 58.752 73.88 h -7.291 l 6.557 -33.738 z M 26.73 49.114 h 14.133 c 4.252 0 7.355 1.116 9.305 3.348 c 1.95 2.232 2.536 5.346 1.758 9.346 c -.32 1.649 -.863 3.154 -1.625 4.52 c -.763 1.364 -1.76 2.613 -2.99 3.745 c -1.468 1.373 -3.098 2.353 -4.891 2.936 c -1.794 .585 -4.08 .875 -6.858 .875 h -6.294 l -1.745 8.97 h -7.35 l 6.557 -33.74 z m 57.366 0 h 14.13 c 4.252 0 7.353 1.116 9.303 3.348 h .002 c 1.95 2.232 2.538 5.346 1.76 9.346 c -.32 1.649 -.861 3.154 -1.623 4.52 c -.763 1.364 -1.76 2.613 -2.992 3.745 c -1.467 1.373 -3.098 2.353 -4.893 2.936 c -1.794 .585 -4.077 .875 -6.855 .875 h -6.295 l -1.744 8.97 h -7.35 l 6.557 -33.74 z m -51.051 5.325 l -2.742 14.12 h 4.468 c 2.963 0 5.172 -.556 6.622 -1.673 c 1.45 -1.116 2.428 -2.981 2.937 -5.592 c .485 -2.507 .264 -4.279 -.666 -5.309 c -.93 -1.032 -2.79 -1.547 -5.584 -1.547 h -5.035 z m 57.363 0 l -2.744 14.12 h 4.47 c 2.965 0 5.17 -.556 6.622 -1.673 c 1.449 -1.116 2.427 -2.981 2.935 -5.592 c .487 -2.507 .266 -4.279 -.664 -5.309 c -.93 -1.032 -2.792 -1.547 -5.584 -1.547 h -5.035 z"
+ readonly property string ruby: "m 35.971 111.33 l 81.958 11.188 c -9.374 -15.606 -18.507 -30.813 -27.713 -46.144 Z m 89.71 -86.383 L 93.513 73.339 c -.462 .696 -1.061 1.248 -.41 2.321 c 8.016 13.237 15.969 26.513 23.942 39.777 c 1.258 2.095 2.53 4.182 4.157 6.192 l 4.834 -96.58 z M 16.252 66.22 c .375 .355 1.311 .562 1.747 .347 c 7.689 -3.779 15.427 -7.474 22.948 -11.564 c 2.453 -1.333 4.339 -3.723 6.452 -5.661 c 6.997 -6.417 13.983 -12.847 20.966 -19.278 c .427 -.395 .933 -.777 1.188 -1.275 c 2.508 -4.902 4.973 -9.829 7.525 -14.898 c -3.043 -1.144 -5.928 -2.263 -8.849 -3.281 c -.396 -.138 -1.02 .136 -1.449 .375 c -6.761 3.777 -13.649 7.353 -20.195 11.472 c -3.275 2.061 -5.943 5.098 -8.843 7.743 c -4.674 4.266 -9.342 8.542 -13.948 12.882 a 24.011 24.011 0 0 0 -3.288 3.854 c -3.15 4.587 -6.206 9.24 -9.402 14.025 c 1.786 1.847 3.41 3.613 5.148 5.259 z m 28.102 -6.271 l -11.556 48.823 l 54.3 -34.987 z m 76.631 -34.846 l -46.15 7.71 l 15.662 38.096 z M 44.996 56.644 l 41.892 13.6 c -5.25 -12.79 -10.32 -25.133 -15.495 -37.737 Z M 16.831 75.643 L 2.169 110.691 l 27.925 -.825 Z m 13.593 26.096 l .346 -.076 c 3.353 -13.941 6.754 -27.786 10.177 -42.272 L 18.544 71.035 c 3.819 9.926 7.891 20.397 11.88 30.704 z m 84.927 -78.897 c -4.459 -1.181 -8.918 -2.366 -13.379 -3.539 c -6.412 -1.686 -12.829 -3.351 -19.237 -5.052 c -.801 -.213 -1.38 -.352 -1.851 .613 c -2.265 4.64 -4.6 9.245 -6.901 13.868 c -.071 .143 -.056 .328 -.111 .687 l 41.47 -6.285 z M 89.482 12.288 l 36.343 10.054 l -6.005 -17.11 l -30.285 6.715 Z M 33.505 114.007 c -4.501 -.519 -9.122 -.042 -13.687 .037 c -3.75 .063 -7.5 .206 -11.25 .323 c -.386 .012 -.771 .09 -1.156 .506 c 31.003 2.866 62.005 5.732 93.007 8.6 l .063 -.414 l -29.815 -4.07 c -12.384 -1.691 -24.747 -3.551 -37.162 -4.982 Z M 2.782 99.994 c 3.995 -9.27 7.973 -18.546 11.984 -27.809 c .401 -.929 .37 -1.56 -.415 -2.308 c -1.678 -1.597 -3.237 -3.318 -5.071 -5.226 c -2.479 12.24 -4.897 24.177 -7.317 36.113 l .271 .127 c .185 -.297 .411 -.578 .548 -.897 z m 78.74 -90.153 c 6.737 -1.738 13.572 -3.097 20.367 -4.613 c .44 -.099 .87 -.244 1.303 -.368 l -.067 -.332 l -29.194 3.928 c 2.741 1.197 4.853 2.091 7.591 1.385 z"
+ readonly property string scala: "M 25.411 110.572 V 95.077 l 11.842 -.474 c 12.315 -.473 21.45 -1.488 34.847 -3.789 c 15.225 -2.639 30.246 -7.375 31.803 -10.082 c .406 -.677 .676 4.534 .676 14.616 v 15.698 l -1.76 1.353 c -1.894 1.489 -9.202 3.993 -17.524 6.09 C 72.303 121.737 40.568 126 29.742 126 h -4.33 z M 25.411 71.327 V 55.83 l 11.842 -.406 c 13.127 -.541 23.344 -1.691 36.877 -4.195 c 15.157 -2.842 28.96 -7.443 29.976 -9.947 c .203 -.473 .406 6.09 .406 14.616 c .067 13.533 -.068 15.698 -1.083 16.78 c -2.368 2.64 -20.638 7.376 -39.449 10.286 c -11.435 1.76 -30.381 3.79 -35.66 3.79 h -2.909 z M 25.411 32.352 V 17.195 l 2.098 -.406 c 1.15 -.203 3.992 -.406 6.293 -.406 c 11.367 0 38.366 -3.722 51.628 -7.105 c 9.27 -2.436 15.698 -4.872 17.931 -6.902 c 1.15 -1.015 1.218 -.406 1.218 14.48 c 0 14.548 -.067 15.63 -1.285 16.714 c -1.827 1.691 -14.345 5.548 -24.09 7.51 c -15.765 3.113 -41.951 6.429 -50.883 6.429 h -2.91 z"
+ readonly property string haskell: "M 0 110.2 L 30.1 65 L 0 19.9 h 22.6 L 52.7 65 l -30.1 45.1 H 0 z M 30.1 110.2 L 60.2 65 L 30.1 19.9 h 22.6 l 60.2 90.3 H 90.4 L 71.5 81.9 l -18.8 28.2 H 30.1 z M 102.9 83.8 l -10 -15.1 H 128 v 15.1 h -25.1 z M 87.8 61.3 l -10 -15.1 H 128 v 15.1 H 87.8 z"
+ readonly property string elixir: "M 90.1 80 c .8 -.5 1.7 -1 2.5 -1.5 c 5.6 -4.8 3.6 -13.5 -5.9 -25.7 c .2 6.1 1.2 12.4 2.2 19.1 c .4 2.6 .8 5.3 1.2 8.1 z M 36 103.2 c 3.2 13.7 11.3 21.1 20.5 24.2 c 5.1 .9 10.2 .7 15.3 -.4 c .8 -.4 1.5 -.9 2.2 -1.4 c 2 -1.3 3.7 -2.8 5.3 -4.3 c -6.5 -2.8 -13.1 -8.2 -19.3 -14.6 c -4.8 -.3 -9.5 -.8 -13.6 -1.4 c -4 -.4 -7.4 -1.1 -10.4 -2.1 z m -1.4 -64.5 l -.2 1.4 c -1.4 7.9 -1.5 15 -.4 21.2 c 1.1 -7.4 2.6 -15 4.7 -22.3 c .3 -1.1 .6 -2.3 1 -3.5 h .1 c 1.6 -5 3.4 -9.8 5.5 -14.3 c -4 5.3 -7.5 11.2 -10.7 17.5 z M 33.9 114 c 3.5 4.4 7.5 7.6 11.9 9.9 c -5.6 -4.6 -10 -11.5 -12 -21.4 c -3.8 -1.4 -6.8 -3.3 -9.1 -5.7 c 1.6 6.4 4.6 12.3 9.2 17.2 z m -.6 -47 c -1.8 -5.9 -2.4 -12.7 -1.8 -20.4 c -1.1 2.3 -2.1 4.8 -3.2 7.3 c -4.5 13.1 -6.5 26.7 -4.6 38.7 c 2.1 3.2 5.2 5.8 9.6 7.6 c -1.4 -7.7 -1.5 -19.9 0 -33.2 z m 2.2 33.8 c 0 .1 0 .1 0 0 c 3.2 1.2 6.8 2 11.1 2.5 c 3.5 .6 7.3 1 11.3 1.3 c -8.3 -9.1 -15.6 -19.8 -20.3 -28.6 c -1.1 -1.6 -2 -3.3 -2.8 -5 c -1.1 12.2 -.8 23.1 .7 29.8 z m 51.4 -28.7 c -1.1 -7.5 -2.3 -15.1 -2.3 -22.3 C 71.9 38 59.6 25.4 60.1 5.1 c -.4 .3 -.8 .6 -1.1 .9 c -2.2 1.9 -4.4 4 -6.5 6.3 c -4.5 6.4 -8.1 14.6 -10.9 23.5 c -1.6 11.6 9.6 34.3 24.5 51.7 c 7.2 -.3 14.8 -2.5 22.1 -6.5 c -.4 -2.9 -.8 -5.9 -1.3 -8.9 z m 10 39.7 c -2.6 -.5 -5.2 -1.4 -7.8 -2.6 c -1.3 4.1 -3.4 7.9 -6.4 11.4 c 1.1 .3 2.1 .6 3.1 .7 c 4.2 -2.5 8 -5.8 11.1 -9.5 z M 62.1 3.6 C 61 23.1 72.2 35.5 84.4 46.8 C 73.7 32.6 67.5 24.1 67.5 .3 c -1.7 .9 -3.5 2 -5.4 3.3 z m 21.1 102.2 c -5.3 .8 -10.8 1.1 -16.2 1.1 c -1.4 0 -2.9 0 -4.3 -.1 c 5.8 5.8 12 10.6 18 13 c 3.2 -3.5 5.3 -7.4 6.6 -11.6 c -1.4 -.7 -2.8 -1.5 -4.1 -2.4 z m -2.4 -1.6 c -2 -1.4 -3.8 -2.9 -5.5 -4.4 c -3.5 -3.1 -6.9 -6.5 -10.1 -10.2 c -7.1 .1 -13.7 -1.6 -19.1 -5.1 c -.8 -.5 -1.6 -1.1 -2.3 -1.7 c 4.6 7.3 10.5 15.3 16.9 22 c 6.5 .3 13.5 .2 20.1 -.6 z m 9.6 .2 c -.2 1 -.4 1.9 -.7 2.9 c 2.8 1.4 5.7 2.4 8.6 2.7 c 2.5 -3.4 4.5 -7.2 5.7 -11.3 c -3.7 2.5 -8.4 4.4 -13.6 5.7 z M 47.2 82.8 c 4.7 3 10.3 4.6 16.3 4.8 c -12.3 -14.6 -21.8 -32.4 -23.7 -45 c -2.3 8.9 -3.1 14.4 -4.4 23.6 v .2 c .9 2.3 2.1 5.1 3.7 8.1 c 2.1 3.4 4.8 6.2 8.1 8.3 z m 41.3 .4 c -6.8 3.7 -13.9 5.8 -20.7 6.3 c 2.9 3.2 5.8 6.2 8.8 8.8 c 2.1 1.9 4.5 3.8 7.1 5.5 c 1.7 -.3 3.3 -.6 4.9 -.9 c 1 -6.3 .7 -12.9 -.1 -19.7 z m 2.2 19.1 c 5.6 -1.5 10.5 -3.6 14.1 -6.6 c 1.5 -6.9 1 -14.4 -2.3 -22.4 c -2.9 -4.9 -5.7 -9.2 -8.3 -13 c 4.8 8.8 4.7 15.2 -.4 19.6 l -.1 .1 c -1.1 .7 -2.2 1.4 -3.4 2.1 c .9 6.9 1.3 13.8 .4 20.2 z"
+ readonly property string erlang: "M 18.2 24.1 L 1 24 v 80 h 19.7 v -.1 C 11 93.6 5.2 79.2 5.3 62.1 C 5.2 47 10 33.9 18.2 24.1 z m 92.9 79.8 z M 127 24 h -16.4 c 6.2 9 9.6 19.3 9.1 32.1 c .1 1.2 .1 1.9 0 4.9 H 46.3 c 0 22 7.7 38.3 27.3 38.4 c 13.5 -.1 23.2 -10.1 29.9 -20.9 l 19 9.5 c -3.4 6.1 -7.2 11 -11.4 16 H 127 V 24 z m -16.5 .1 z m -45.4 1.5 c -9 0 -16.8 7.4 -17.6 16.4 H 81 c -.3 -9 -6.8 -16.4 -15.9 -16.4 z"
+ readonly property string clojure: "M 64 0 C 28.712 0 0 28.6 0 63.751 c 0 35.155 28.712 63.753 64 63.753 s 64 -28.598 64 -63.753 C 128 28.6 99.288 0 64 0 M 61.659 64.898 a 265.825 265.825 0 0 0 -1.867 4.12 c -2.322 5.241 -4.894 11.62 -5.834 15.706 c -.337 1.455 -.546 3.258 -.542 5.258 c 0 .79 .043 1.622 .11 2.469 a 30.74 30.74 0 0 0 10.533 1.87 a 30.796 30.796 0 0 0 9.642 -1.566 a 18.09 18.09 0 0 1 -2.011 -2.12 c -4.11 -5.221 -6.403 -12.872 -10.031 -25.737 M 46.485 38.96 c -7.85 5.51 -12.986 14.6 -13.005 24.9 c .019 10.145 5.001 19.116 12.653 24.65 c 1.877 -7.789 6.582 -14.92 13.637 -29.214 a 114.691 114.691 0 0 0 -1.43 -3.72 c -1.955 -4.884 -4.776 -10.556 -7.294 -13.124 c -1.283 -1.342 -2.84 -2.502 -4.561 -3.492 M 90.697 98.798 c -4.05 -.506 -7.392 -1.116 -10.317 -2.144 a 36.708 36.708 0 0 1 -16.32 3.807 c -20.293 0 -36.742 -16.383 -36.745 -36.602 c 0 -10.97 4.852 -20.805 12.528 -27.512 c -2.053 -.495 -4.194 -.783 -6.38 -.779 c -10.782 .101 -22.162 6.044 -26.9 22.095 c -.443 2.337 -.337 4.103 -.337 6.197 c 0 31.818 25.895 57.613 57.835 57.613 c 19.561 0 36.841 -9.682 47.305 -24.489 c -5.66 1.405 -11.103 2.077 -15.763 2.091 c -1.747 0 -3.387 -.093 -4.906 -.277 M 79.829 87.634 c .357 .176 1.167 .464 2.293 .783 c 7.579 -5.542 12.504 -14.469 12.523 -24.558 h -.003 c -.028 -16.82 -13.693 -30.43 -30.582 -30.462 a 30.765 30.765 0 0 0 -9.602 1.554 c 6.21 7.05 9.196 17.127 12.084 28.148 l .005 .013 c .005 .009 .924 3.06 2.501 7.11 c 1.566 4.042 3.797 9.048 6.23 12.696 c 1.597 2.444 3.354 4.2 4.551 4.716 M 17.057 30.311 c 5.463 -3.408 11.04 -4.637 15.908 -4.593 c 6.722 .02 12.008 2.096 14.544 3.516 c .612 .352 1.194 .73 1.764 1.12 a 36.714 36.714 0 0 1 14.786 -3.096 c 20.295 .003 36.747 16.386 36.75 36.601 c -.003 10.192 -4.188 19.408 -10.934 26.044 a 45.3 45.3 0 0 0 5.225 .29 c 6.406 .004 13.329 -1.404 18.52 -5.753 c 3.384 -2.84 6.22 -6.998 7.792 -13.233 c .307 -2.408 .484 -4.856 .484 -7.347 c 0 -31.817 -25.892 -57.614 -57.835 -57.614 c -19.372 0 -36.508 9.5 -47.004 24.065 z"
+ readonly property string r: "M 64 14.6465 v .002 c -35.346 0 -64 19.1902 -64 42.8632 c 0 20.764 22.0464 38.0766 51.3164 42.0176 v -12.83 c -15.55 -4.89 -26.166 -14.6943 -26.166 -25.9923 c 0 -16.183 21.7795 -29.3027 48.6465 -29.3027 c 26.866 0 46.6914 8.9748 46.6914 29.3027 c 0 10.486 -5.2715 17.9507 -14.0645 22.7207 c 1.204 .908 2.2184 2.073 2.9024 3.42 l .3886 .6543 C 121.0248 79.772 128 69.1888 128 57.5098 c 0 -23.672 -28.654 -42.8633 -64 -42.8633 z M 52.7363 41.2637 v 72.084 l 21.834 -.0098 l -.0039 -28.2188 h 5.8613 c 1.199 0 1.7167 .3481 2.9297 1.3301 c 1.454 1.177 3.8164 5.2383 3.8164 5.2383 l 11.5371 21.666 l 24.6739 -.0097 l -15.2657 -25.7403 a 8.388 8.388 0 0 0 -1.4199 -2.041 c -.974 -1.036 -2.3255 -1.8227 -3.1055 -2.2188 c -2.249 -1.1375 -6.12 -2.3076 -6.123 -2.3085 c 0 0 19.08 -1.4151 19.08 -20.4141 c 0 -18.999 -19.9706 -19.3574 -19.9706 -19.3574 H 52.7363 z m 22.0176 15.627 l 13.2188 .0077 s 6.123 -.3302 6.123 6.0098 c 0 6.216 -6.123 6.2344 -6.123 6.2344 l -13.2247 .0039 l .006 -12.2559 z m 9.3457 32.6366 c -2.612 .257 -5.3213 .411 -8.1133 .463 l .002 9.6288 a 88.362 88.362 0 0 0 12.4746 -2.4902 l -.502 -.9414 c -.68 -1.268 -1.3472 -2.5426 -2.0332 -3.8066 a 41.01 41.01 0 0 0 -1.828 -2.8516 v -.002 z"
+ readonly property string perl: "M 53.343 127.515 c -13.912 -2.458 -25.845 -8.812 -35.707 -19.004 C 9.252 99.845 3.48 88.926 .851 76.764 C -.284 71.51 -.284 56.477 .85 51.222 c 1.776 -8.219 5.228 -16.388 9.927 -23.509 c 3.112 -4.71 12.227 -13.825 16.938 -16.937 c 7.121 -4.698 15.292 -8.15 23.511 -9.925 c 5.256 -1.135 20.29 -1.135 25.546 0 c 12.809 2.769 23.454 8.553 32.638 17.736 c 9.188 9.187 14.969 19.827 17.738 32.635 c 1.135 5.255 1.135 20.287 0 25.542 c -2.769 12.808 -8.55 23.452 -17.738 32.635 c -9.043 9.042 -19.55 14.81 -32.146 17.652 c -4.469 1.005 -19.24 1.295 -23.922 .464 z m 11.565 -12.772 c 0 -4.194 -.06 -4.496 -.908 -4.496 c -.84 0 -.904 .29 -.868 3.899 c .04 4.262 .34 5.574 1.207 5.284 c .404 -.134 .57 -1.494 .57 -4.687 z m -6.758 1.445 c 1.196 -1.194 1.543 -1.917 1.543 -3.209 c 0 -1.315 -.162 -1.634 -.763 -1.517 c -.416 .08 -.92 .759 -1.114 1.505 c -.198 .751 -1.002 1.906 -1.785 2.572 c -1.417 1.194 -1.47 2.191 -.121 2.191 c .384 0 1.393 -.694 2.24 -1.542 z m 14.945 1.05 c .166 -.271 -.339 -1.037 -1.126 -1.699 c -.783 -.666 -1.587 -1.821 -1.784 -2.572 c -.194 -.746 -.699 -1.425 -1.115 -1.505 c -.601 -.117 -.763 .202 -.763 1.517 c 0 2.608 3.747 5.942 4.788 4.259 z m -20.66 -8.146 c 0 -.262 -.635 -.823 -1.41 -1.247 c -5.058 -2.769 -10.984 -7.177 -14.282 -10.612 c -6.435 -6.704 -9.33 -13.385 -9.402 -21.676 c -.044 -5.542 .67 -8.432 3.367 -13.607 c 2.608 -5 5.631 -8.779 13.947 -17.42 c 9.29 -9.648 11.429 -12.195 13.043 -15.53 c 1.147 -2.369 1.296 -3.232 1.458 -8.238 c .197 -6.216 -.182 -10.506 -.929 -10.506 c -.339 0 -.403 1.614 -.21 5.235 c .622 11.593 -1.53 15.19 -14.892 24.88 c -9.2 6.677 -13.422 10.302 -16.612 14.261 c -4.517 5.615 -6.52 10.471 -7.02 17.054 c -1.207 15.868 8.85 29.628 26.591 36.385 c 3.916 1.49 6.35 1.881 6.35 1.021 z m 30.696 -1.287 c 6.1 -2.539 10.738 -5.611 15.11 -10.007 c 6.665 -6.7 9.442 -12.965 9.858 -22.24 c .363 -8.134 -1.405 -13.515 -6.439 -19.61 c -3.447 -4.173 -7.161 -7.16 -17.173 -13.812 c -13.47 -8.95 -16.632 -12.513 -16.632 -18.746 c 0 -1.659 .299 -4.004 .662 -5.219 c .622 -2.066 .606 -3.491 -.02 -1.857 c -.593 1.546 -1.946 .836 -2.676 -1.408 l -.703 -2.156 l .267 2.043 c .94 7.241 1.061 10.272 .641 16.614 c -.56 8.565 -1.614 14.426 -4.505 25.074 c -2.87 10.572 -3.387 14.402 -3.031 22.475 c .298 6.826 1.255 11.932 3.475 18.592 c 2.06 6.188 2.443 6.656 6.23 7.625 c 2.086 .533 4.06 1.433 5.63 2.567 c 1.474 1.066 2.952 1.76 3.78 1.776 c .75 .012 3.237 -.759 5.526 -1.711 z m -1.369 -3.076 c -.565 -.565 -.302 -1.046 1.91 -3.492 c 6.972 -7.697 10.096 -15.645 10.185 -25.906 c .06 -6.995 -1.482 -11.625 -6.197 -18.592 c -2.135 -3.152 -9.636 -11.011 -13.265 -13.893 c -2.664 -2.115 -5.397 -5.72 -5.886 -7.762 c -.496 -2.067 .888 -1.522 2.495 .985 c .787 1.227 2.495 3.027 3.79 4 c 1.297 .977 5.132 3.834 8.523 6.357 c 11.666 8.67 16.858 16.065 18.024 25.668 c .679 5.558 -.395 11.302 -3.108 16.634 c -2.81 5.526 -7.937 11.545 -12.325 14.479 c -2.7 1.8 -3.552 2.115 -4.146 1.522 z m -22.836 .585 c .133 -.343 -1.034 -2.535 -2.592 -4.872 c -4.13 -6.192 -5.926 -9.61 -7.602 -14.454 c -1.413 -4.09 -1.49 -4.646 -1.501 -10.887 c -.016 -9.433 1.005 -12.424 8.49 -24.848 c 7.056 -11.722 8.013 -16.259 7.217 -34.286 c -.286 -6.462 -.61 -11.839 -.718 -11.948 c -.747 -.746 -.904 1.167 -.63 7.665 c .549 12.941 -.287 20.15 -3.016 26.064 c -1.857 4.024 -3.936 7.076 -9.53 14.002 c -7.788 9.64 -9.984 14.75 -9.944 23.125 c .029 5.744 .808 9.276 3.129 14.188 c 2.51 5.316 7.133 10.685 12.926 15.012 c 2.669 1.99 3.391 2.228 3.77 1.239 z"
+ readonly property string zig: "M 38.484 23.843 l -15.06 18.405 l -7.529 -11.712 z M 38.484 23.843 l -10.876 9.203 l -4.183 9.202 h -5.02 v 42.667 h 7.53 l -9.203 4.183 l -6.693 14.222 H 0 V 23.843 z M 25.935 84.915 L 10.039 103.32 l -6.693 -9.202 z M 46.85 23.843 l 5.02 11.713 l -20.916 6.692 z M 46.85 23.843 h 46.013 v 18.405 H 30.954 L 46.85 32.21 z M 97.046 84.915 L 81.15 103.32 l -5.856 -10.875 z M 97.046 84.915 l -13.386 7.53 l -2.51 10.875 H 35.137 V 84.915 z M 125.49 5.438 L 43.503 103.32 L 2.51 122.562 l 81.987 -98.719 z M 117.96 23.843 l -.836 15.06 l -15.059 4.182 z M 128 23.843 v 79.477 H 88.68 l 11.712 -10.039 l 4.183 -8.366 h 5.02 v -41.83 h -7.53 l 8.366 -7.53 l 7.53 -11.712 z M 104.575 84.915 l 4.183 12.55 l -20.078 5.855 z"
+ readonly property string nim: "M 64.508 20.135 v .004 s -4.905 3.873 -9.906 7.726 a 70.222 70.222 0 0 0 -20.696 2.975 c -5.028 -3.2 -9.463 -6.715 -9.463 -6.715 s -3.78 6.505 -6.158 10.322 a 52.032 52.032 0 0 0 -10.22 6.776 C 4.393 39.773 .136 37.989 0 37.943 c 4.86 9.806 8.129 19.622 17.016 25.524 c 14.171 -22.35 79.908 -20.294 94.35 -.13 c 9.32 -4.881 12.977 -15.335 16.634 -25.026 c -.402 .132 -5.398 1.804 -8.635 3.039 a 52.521 52.521 0 0 0 -9.08 -6.903 c -2.455 -4.498 -6.03 -10.574 -6.03 -10.574 s -4.237 3.151 -9.142 6.584 a 97.211 97.211 0 0 0 -21.398 -2.342 c -4.572 -3.776 -9.207 -7.98 -9.207 -7.98 z m 59.373 38.468 a 61.161 61.161 0 0 1 -21.028 17.686 a 55.85 55.85 0 0 1 -13.636 3.625 L 64.232 66.97 L 39.09 79.654 a 71.675 71.675 0 0 1 -13.637 -3.492 a 64.347 64.347 0 0 1 -20.424 -17.4 l 11.674 28.275 c 20.274 26.743 72.042 28.603 94.63 .516 c 5.338 -12.037 12.548 -28.95 12.548 -28.95 z"
+ readonly property string ocaml: "M 65.004 115.355 c -.461 -.894 -1.004 -2.796 -1.356 -3.601 c -.378 -.711 -1.46 -2.692 -1.984 -3.332 c -1.164 -1.332 -1.437 -1.438 -1.809 -3.23 c -.628 -3.067 -2.148 -8.462 -4.042 -12.227 c -1.004 -2 -2.626 -3.606 -4.067 -5.07 c -1.246 -1.247 -4.121 -3.31 -4.668 -3.227 c -4.766 .894 -6.226 5.586 -8.457 9.27 c -1.27 2.062 -2.516 3.769 -3.52 5.937 c -.898 1.98 -.812 4.23 -2.331 5.938 a 15.44 15.44 0 0 0 -3.333 5.855 c -.195 .453 -.546 4.957 -1.003 6.016 l 7.02 -.438 c 6.585 .461 4.687 2.961 14.858 2.438 l 16.098 -.54 a 24.864 24.864 0 0 0 -1.433 -3.792 z M 111.793 8.254 H 16.207 C 7.312 8.23 .086 15.457 .086 24.352 v 35.105 c 2.352 -.812 5.578 -5.75 6.668 -6.934 c 1.789 -2.062 2.16 -4.77 3.059 -6.378 c 2.062 -3.793 2.433 -6.477 7.101 -6.477 c 2.164 0 3.063 .516 4.5 2.516 c .996 1.332 2.79 3.957 3.602 5.668 c 1.004 1.98 2.523 4.582 3.254 5.125 c .515 .351 .972 .722 1.433 .894 c .707 .27 1.356 -.27 1.902 -.629 c .622 -.539 .895 -1.52 1.52 -2.953 c .895 -2.086 1.813 -4.418 2.332 -5.312 c .914 -1.461 1.273 -3.254 2.25 -4.067 c 1.461 -1.246 3.441 -1.355 3.957 -1.437 c 2.98 -.625 4.336 1.437 5.777 2.707 c .973 .894 2.243 2.605 3.246 4.851 c .708 1.793 1.606 3.52 2.067 4.5 c .351 .98 1.266 2.606 1.789 4.582 c .543 1.711 1.809 3.067 2.352 3.961 c 0 0 .812 2.164 5.476 4.145 a 34.992 34.992 0 0 0 4.336 1.52 c 2.066 .734 4.047 .644 6.563 .374 c 1.789 0 2.793 -2.625 3.601 -4.683 c .438 -1.254 .98 -4.774 1.25 -5.758 c .27 -.996 -.437 -1.707 .192 -2.625 c .722 -.977 1.164 -1.082 1.519 -2.332 c .914 -2.793 5.957 -2.875 8.832 -2.875 c 2.414 0 2.063 2.332 6.125 1.52 c 2.336 -.434 4.586 .273 7.023 .995 c 2.063 .543 4.043 1.168 5.204 2.524 c .73 .898 2.629 5.312 .73 5.476 c .164 .188 .36 .645 .625 .817 c -.46 1.707 -2.25 .46 -3.332 .27 c -1.355 -.27 -2.332 0 -3.684 .624 c -2.335 .996 -5.668 .918 -7.726 2.625 c -1.715 1.438 -1.715 4.582 -2.543 6.371 c 0 0 -2.254 5.696 -6.996 9.192 c -1.278 .914 -3.715 3.058 -8.918 3.871 c -2.356 .355 -4.586 .355 -7.024 .27 c -1.164 -.079 -2.332 -.079 -3.52 -.079 c -.706 0 -3.062 -.109 -2.96 .164 l -.27 .645 c .024 .29 .063 .602 .164 .895 c .102 .515 .102 .976 .192 1.437 c 0 .98 -.086 2.063 0 3.066 c .082 2.063 .894 3.957 1.004 6.102 c .078 2.355 1.246 4.875 2.414 6.77 c .46 .707 1.086 .789 1.355 1.71 c .352 .98 0 2.141 .188 3.227 c .625 4.227 1.875 8.73 3.773 12.61 v .078 c 2.332 -.352 4.77 -1.247 7.836 -1.684 c 5.664 -.832 13.5 -.461 18.54 -.914 c 12.796 -1.168 19.706 5.226 31.148 2.601 V 24.336 c -.063 -8.895 -7.293 -16.102 -16.207 -16.102 z M 64.086 83.855 c 0 -.187 0 -.187 0 0 z m -34.457 14.75 c .894 -1.98 1.433 -4.125 2.144 -6.101 c .73 -1.899 1.813 -4.61 3.684 -5.582 c -.246 -.274 -3.957 -.375 -4.934 -.461 c -1.082 -.086 -2.171 -.273 -3.25 -.438 a 135.241 135.241 0 0 1 -6.125 -1.265 c -1.168 -.274 -5.21 -1.715 -6.02 -2.067 c -2.085 -.894 -3.421 -3.52 -4.96 -3.246 c -.977 .188 -1.98 .54 -2.605 1.54 c -.543 .812 -.731 2.242 -1.083 3.226 c -.437 1.086 -1.168 2.164 -1.707 3.25 c -1.277 1.875 -3.332 3.582 -4.23 5.484 c -.191 .457 -.27 .895 -.457 1.356 v 21.683 c 1.082 .188 2.16 .371 3.328 .73 c 8.996 2.438 11.164 2.606 19.98 1.63 l .813 -.11 c .625 -1.437 1.188 -6.207 1.629 -7.644 c .352 -1.164 .812 -2.063 .996 -3.14 c .164 -1.09 0 -2.173 -.102 -3.15 c -.171 -2.628 1.895 -3.519 2.899 -5.69 z m 0 0"
+ readonly property string fsharp: "M 0 64.5 L 60.7 3.8 v 30.4 L 30.4 64.5 l 30.4 30.4 v 30.4 L 0 64.5 z m 39.1 0 l 21.7 -21.7 v 43.4 L 39.1 64.5 z m 88.9 0 L 65.1 3.8 v 30.4 l 30.4 30.4 l -30.4 30.3 v 30.4 L 128 64.5 z"
+ readonly property string visualbasic: "M 64 0 A 64 64 0 0 0 0 64 a 64 64 0 0 0 64 64 a 64 64 0 0 0 64 -64 A 64 64 0 0 0 64 0 z m -3.76 38.7 l 6.34 .1 L 48 89.202 h -6.58 L 23.14 38.8 h 6.579 l 14 40 a 23.74 23.74 0 0 1 1.02 4.46 h .141 a 22 22 0 0 1 1.119 -4.56 z m 13.6 .1 h 14.34 A 15.68 15.68 0 0 1 98.54 42 a 10.34 10.34 0 0 1 3.84 8.34 a 12.26 12.26 0 0 1 -2.38 7.44 a 12.52 12.52 0 0 1 -6.4 4.501 v .139 a 12.82 12.82 0 0 1 8.16 3.84 a 11.84 11.84 0 0 1 3.06 8.461 a 13.18 13.18 0 0 1 -4.64 10.48 a 17.28 17.28 0 0 1 -11.7 4 H 73.84 z m 12.7 5.26 l -6.7 .06 V 60.4 h 5.999 a 11.48 11.48 0 0 0 7.58 -2.4 a 8.14 8.14 0 0 0 2.781 -6.6 c 0 -4.893 -3.22 -7.34 -9.66 -7.34 z m -6.7 21.641 v 18.14 h 8 a 12 12 0 0 0 8 -2.46 a 8.42 8.42 0 0 0 2.86 -6.74 c 0 -5.947 -4.053 -8.92 -12.16 -8.92 z"
+ readonly property string fortran: "M 18.969 0 C 13.25 0 0 11 0 18.66 v 90.453 c 0 5.692 11.21 18.903 18.781 18.903 l 90.551 -.032 c 6.738 -.004 18.688 -9.683 18.688 -18.601 V 18.84 c 0 -6.078 -10.61 -18.832 -18.43 -18.832 L 18.969 0 z m -1.395 13.66 h 93.367 v 41.711 l -10.992 -.164 c -.101 -.098 -.402 -3.047 -.605 -5.758 C 98.19 36.7 95.328 29.363 89.809 26.5 c -2.914 -1.504 -7.457 -1.95 -22.02 -1.953 l -13.57 .004 v 31.273 h 2.41 c 4.066 -.05 9.234 -1.004 10.941 -2.058 c 2.211 -1.356 4.067 -5.27 4.72 -9.989 c .491 -3.445 .87 -6.023 .87 -6.023 h 10.676 v 49.691 H 72.793 v -1.957 c 0 -3.21 -1.508 -10.691 -2.563 -12.949 c -1.656 -3.465 -4.464 -4.668 -12.449 -5.422 l -3.664 -.351 l .203 16.113 c .149 15.308 .25 16.164 1.203 17.469 c 1.207 1.605 2.512 1.906 10.493 2.507 l 5.355 .258 l -.035 10.938 H 17.574 v -10.942 l 4.922 -.304 c 9.988 -.653 9.887 -.602 10.39 -8.43 c .45 -7.43 -.116 -65.598 -.452 -66.762 c -.551 -1.922 -2.618 -3.027 -8.786 -3.023 l -6.074 -.04 V 13.66 z"
+ readonly property string crystal: "m 127.806 81.328 l -46.325 45.987 c -.185 .185 -.464 .276 -.65 .185 l -63.283 -16.863 c -.279 -.095 -.464 -.28 -.464 -.464 L .035 47.317 c -.09 -.275 0 -.46 .186 -.645 L 46.55 .685 c .184 -.185 .463 -.276 .649 -.185 l 63.28 16.863 c .278 .095 .463 .28 .463 .464 L 127.9 80.682 c .185 .275 .09 .46 -.094 .645 z M 65.726 31.28 L 3.557 47.778 c -.095 0 -.185 .185 -.095 .28 l 45.495 45.156 c .09 .095 .28 .095 .28 -.09 l 16.675 -61.748 c .095 0 -.09 -.185 -.184 -.094 z m 0 0"
+ readonly property string gleam: "M 56.61 .014 c -.41 .021 -.818 .067 -1.221 .138 v .002 c -3.23 .57 -6.182 2.714 -7.405 6.178 l -9.523 26.98 a 9.599 9.599 0 0 1 -5.553 5.745 L 6.244 49.504 C -.598 52.184 -1.923 61.596 3.91 66.05 l 22.746 17.37 a 9.582 9.582 0 0 1 3.75 7.047 l 1.696 28.553 c .434 7.326 8.978 11.493 15.033 7.323 v -.002 l 23.58 -16.244 a 9.602 9.602 0 0 1 7.87 -1.389 l 27.714 7.2 c 7.116 1.848 13.72 -4.99 11.623 -12.022 l -8.17 -27.412 a 9.579 9.579 0 0 1 1.113 -7.905 l 15.432 -24.103 c 3.958 -6.182 -.503 -14.573 -7.85 -14.752 l -28.63 -.695 a 9.596 9.596 0 0 1 -7.182 -3.499 L 64.459 3.426 C 62.418 .944 59.477 -.137 56.609 .014 z m 30.435 55.513 a 5.082 5.082 0 1 1 .222 10.161 a 5.082 5.082 0 0 1 -.222 -10.16 z M 46.1 62.747 a 5.083 5.083 0 0 1 .992 10.085 a 5.082 5.082 0 1 1 -.992 -10.086 z m 26.283 7.382 a 2.59 2.59 0 0 1 1.812 .799 a 2.59 2.59 0 0 1 .713 1.847 a 6.723 6.723 0 0 1 -2.088 4.721 h -.002 a 6.733 6.733 0 0 1 -2.22 1.41 h -.002 a 6.75 6.75 0 0 1 -2.59 .451 h -.002 a 6.74 6.74 0 0 1 -2.567 -.576 h -.001 a 6.758 6.758 0 0 1 -3.563 -3.736 a 2.594 2.594 0 0 1 .63 -2.805 a 2.59 2.59 0 0 1 1.847 -.715 a 2.595 2.595 0 0 1 2.35 1.655 c .075 .192 .186 .368 .328 .517 v .002 c .143 .149 .312 .266 .5 .35 v .002 h .002 a 1.576 1.576 0 0 0 1.201 .03 l .002 -.003 a 1.552 1.552 0 0 0 .868 -.828 h .002 v -.002 c .083 -.188 .127 -.389 .132 -.594 a 2.593 2.593 0 0 1 1.653 -2.353 c .317 -.122 .654 -.18 .994 -.172 z"
+ readonly property string julia: "M 0.0 94.2 a 29.1 29.1 0 1 0 58.2 0 a 29.1 29.1 0 1 0 -58.2 0 Z M 69.80000000000001 94.2 a 29.1 29.1 0 1 0 58.2 0 a 29.1 29.1 0 1 0 -58.2 0 Z M 34.9 33.8 a 29.1 29.1 0 1 0 58.2 0 a 29.1 29.1 0 1 0 -58.2 0 Z"
+ readonly property string objectivec: "M 63.877 125.392 c -32.671 0 -60.37 -27.594 -60.627 -60.469 a 59.94 59.94 0 0 1 17.506 -42.759 a 60.939 60.939 0 0 1 43.279 -18.36 a 60.081 60.081 0 0 1 42.647 17.71 a 60.145 60.145 0 0 1 18.157 42.522 c .151 33.604 -26.864 61.021 -60.469 61.363 h -.493 z m .19 -118.406 a 57.774 57.774 0 0 0 -41.01 17.427 a 56.775 56.775 0 0 0 -16.63 40.484 c .236 31.159 26.495 57.286 57.43 57.286 h .414 c 31.863 -.29 57.504 -26.266 57.385 -58.128 a 56.97 56.97 0 0 0 -17.217 -40.273 A 56.7 56.7 0 0 0 64.068 6.986 z M 16.89 82.383 V 46.865 h 8.64 v 3.183 h -4.583 v 29.218 h 4.584 v 3.183 l -8.642 -.066 z M 46.213 64.272 c 0 6.478 -3.933 10.167 -9.26 10.167 s -8.877 -4.156 -8.877 -9.831 c 0 -5.939 3.722 -10.121 9.167 -10.121 s 8.97 4.36 8.97 9.785 z m -14.415 .29 c 0 3.932 1.973 7.05 5.36 7.05 s 5.333 -3.183 5.333 -7.195 c 0 -3.643 -1.796 -7.083 -5.334 -7.083 s -5.392 3.328 -5.392 7.307 l .033 -.08 z M 49.205 55.158 c 1.69 -.29 3.407 -.434 5.123 -.428 a 9.17 9.17 0 0 1 5.537 1.223 a 4.062 4.062 0 0 1 2.006 3.61 a 4.48 4.48 0 0 1 -3.183 4.183 c 2.269 .46 3.9 2.46 3.9 4.775 a 5.016 5.016 0 0 1 -1.861 3.978 c -1.368 1.21 -3.643 1.796 -7.162 1.796 a 33.966 33.966 0 0 1 -4.327 -.257 l -.033 -18.88 z m 3.499 7.622 h 1.795 c 2.433 0 3.801 -1.145 3.801 -2.782 c 0 -1.638 -1.368 -2.644 -3.61 -2.644 a 9.779 9.779 0 0 0 -2.006 .145 l .02 5.28 z m 0 8.878 c .618 .065 1.243 .092 1.86 .078 c 2.263 0 4.262 -.861 4.262 -3.182 s -1.94 -3.183 -4.373 -3.183 h -1.75 v 6.287 z M 69.54 54.901 h 3.517 v 12.554 c 0 5.334 -2.577 7.116 -6.365 7.116 a 9.313 9.313 0 0 1 -2.973 -.507 l .428 -2.834 c .703 .224 1.44 .335 2.183 .349 c 2.006 0 3.183 -.921 3.183 -4.262 l .026 -12.416 z M 83.067 65.357 v 2.434 h -7.32 v -2.434 h 7.32 z M 100.158 73.63 c -1.585 .632 -3.281 .921 -4.978 .862 c -6.129 0 -9.851 -3.834 -9.851 -9.707 c -.283 -5.353 3.827 -9.923 9.18 -10.206 c .375 -.02 .757 -.02 1.131 .006 a 11.112 11.112 0 0 1 4.775 .862 l -.783 2.801 a 9.476 9.476 0 0 0 -3.788 -.75 c -3.932 0 -6.76 2.467 -6.76 7.116 c 0 4.235 2.499 6.971 6.734 6.971 a 9.806 9.806 0 0 0 3.834 -.717 l .506 2.762 z M 111.2 46.766 v 35.61 h -8.641 v -3.182 h 4.583 V 49.949 h -4.583 v -3.183 h 8.64 z"
+ readonly property string vala: "m 62.959 32.051 c -7.551 0 -14.777 2.4271 -21.676 7.2832 c -3.0202 2.1912 -5.4913 4.7082 -7.416 7.5508 c -1.8951 2.813 -2.8438 5.745 -2.8438 8.7949 c 0 1.6581 0.20849 3.0486 0.62305 4.1738 c 1.214 3.1091 4.2474 4.6641 9.1035 4.6641 c 0 -0.17764 -0.072563 -0.38417 -0.2207 -0.62109 c -0.68102 -1.2437 -1.0215 -3.0951 -1.0215 -5.5527 c 1e-6 -5.0633 1.1839 -9.2979 3.5527 -12.703 c 2.3984 -3.4052 5.7302 -5.907 9.9941 -7.5059 l 1.4648 62.137 h 13.102 l 25.273 -67.777 h -6.3516 l -19.143 55.697 l -0.80078 -55.963 c -1.1844 -0.11892 -2.3969 -0.17774 -3.6406 -0.17774 z"
+ readonly property string groovy: "M 57.27 43.147 c -6.273 10.408 -6.633 10.955 -7.504 11.382 c -.78 .383 -.97 .407 -1.287 .164 c -1.296 -.996 -3.031 -.705 -4.248 .712 l -.676 .787 l -.143 -.843 c -.223 -1.318 -.299 -1.505 -.842 -2.092 c -.506 -.545 -.508 -.564 -.214 -2.21 c .364 -2.04 .385 -3.53 .071 -5.01 c -.613 -2.894 -2.139 -4.224 -4.845 -4.224 c -2.341 0 -5.13 1.864 -8.696 5.81 c -2.148 2.378 -5.401 6.847 -6.1 8.382 l -.272 .597 l -11.193 -.141 c -6.967 -.088 -11.117 -.066 -10.99 .059 c .111 .11 4.892 1.949 10.624 4.087 l 10.423 3.887 l .389 .909 c .463 1.081 1.665 2.462 2.696 3.099 l .742 .459 l -.866 .388 c -.644 .288 -.984 .63 -1.325 1.331 c -.673 1.385 -.451 2.176 1.102 3.925 c .698 .787 1.662 2.185 2.141 3.107 c .58 1.114 1.099 1.815 1.548 2.088 c .724 .442 2.059 .544 2.691 .206 c .713 -.382 4.905 -1.438 4.762 -1.201 c -.077 .129 -2.522 3.971 -5.433 8.537 c -2.911 4.566 -5.247 8.347 -5.192 8.403 c .056 .055 8.933 -3.328 19.727 -7.52 l 19.626 -7.62 L 83.553 88.2 c 10.762 4.178 19.648 7.569 19.747 7.536 c .098 -.033 -1.301 -2.376 -3.109 -5.207 l -3.288 -5.147 l 1.135 -.228 c 2.552 -.512 5.431 -2.527 6.98 -4.884 c 2.26 -3.438 2.587 -7.399 1.084 -13.136 c -.302 -1.151 -.499 -2.142 -.438 -2.202 c .06 -.06 4.99 -1.933 10.956 -4.161 c 5.966 -2.229 10.931 -4.135 11.034 -4.236 c .104 -.102 -5.137 -.129 -11.888 -.059 c -10.269 .106 -12.088 .08 -12.169 -.176 a 12.474 12.474 0 0 1 -.215 -.853 c -.141 -.65 -1.085 -1.654 -1.816 -1.933 c -.282 -.108 -1.21 -.21 -2.062 -.227 c -1.377 -.029 -1.631 .031 -2.298 .54 c -.413 .315 -.811 .765 -.886 1 c -.181 .571 -.402 .537 -.751 -.114 c -.812 -1.518 -3.259 -1.842 -4.504 -.596 l -.629 .628 l -1.245 -.617 c -1.536 -.761 -3.42 -.983 -4.504 -.53 c -.53 .221 -.906 .255 -1.279 .114 c -.888 -.337 -2.307 -.065 -2.969 .569 l -.595 .57 l -.976 -.659 c -.537 -.362 -1.246 -.753 -1.576 -.869 c -.496 -.174 -1.676 -2.002 -6.881 -10.66 c -3.455 -5.747 -6.342 -10.45 -6.416 -10.45 c -.074 0 -3.1 4.92 -6.725 10.934 m -18.682 1.788 c 1.109 .776 1.382 2.983 .769 6.212 c -.671 3.539 -1.702 5.813 -3.553 7.838 c -1.213 1.327 -2.574 2.061 -3.858 2.081 c -2.946 .044 -3.694 -2.859 -1.755 -6.813 c .706 -1.438 2.499 -3.906 2.839 -3.906 c .167 0 .677 2.003 .677 2.66 c 0 .224 -.403 1.073 -.895 1.888 c -.717 1.187 -.894 1.681 -.894 2.494 c 0 3.113 2.902 2.729 4.473 -.593 c 1.586 -3.353 2.474 -9.821 1.496 -10.905 c -.522 -.579 -.832 -.566 -2.253 .096 c -2.246 1.045 -6.923 6.488 -8.534 9.931 c -1.583 3.384 -1.245 6.914 .844 8.801 c 2.073 1.872 5.366 1.519 7.788 -.835 c 1.85 -1.799 3.249 -4.447 3.827 -7.244 c .348 -1.68 .86 -1.854 1.037 -.352 c .069 .58 .605 2.529 1.192 4.33 c 1.451 4.455 1.732 5.652 1.73 7.359 c -.003 1.889 -.619 3.24 -2.111 4.629 c -1.427 1.328 -3.429 2.232 -7.167 3.237 c -1.643 .441 -3.359 .933 -3.813 1.093 c -1.106 .389 -1.178 .376 -1.416 -.248 c -.352 -.926 -1.71 -2.943 -2.608 -3.872 l -.86 -.891 l .867 -.233 c 2.86 -.77 7.084 -2.305 9.12 -3.315 c 2.446 -1.212 4.011 -2.464 4.718 -3.773 c .561 -1.039 .526 -3.601 -.067 -4.871 l -.447 -.96 l -.249 .64 c -1.385 3.563 -4.003 6.201 -7.143 7.198 c -1.894 .602 -4.369 .435 -5.898 -.396 c -2.676 -1.457 -3.167 -4.382 -1.37 -8.167 c 2.085 -4.395 8.022 -11.31 10.979 -12.787 c 1.541 -.77 1.846 -.81 2.535 -.326 M 76.54 56.23 c 2.405 1.199 3.94 3.79 3.97 6.703 c .023 2.183 -.502 3.639 -1.768 4.905 c -1.196 1.196 -2.454 1.619 -4.202 1.414 c -2.996 -.352 -4.711 -2.078 -4.944 -4.974 c -.24 -2.992 1.276 -7.128 2.998 -8.178 c .997 -.608 2.57 -.556 3.946 .13 m -10.644 .499 c .364 .259 .951 1.082 1.348 1.887 c .66 1.341 .703 1.564 .703 3.676 c 0 2.11 -.044 2.337 -.7 3.671 c -1.305 2.652 -3.664 4.17 -6.158 3.963 c -1.429 -.118 -2.156 -.616 -2.814 -1.929 c -.925 -1.843 -.586 -5.413 .797 -8.384 c .669 -1.438 2.511 -3.436 3.269 -3.545 c 1.007 -.146 2.921 .21 3.555 .661 m 34.495 -.459 c .127 .086 .641 2.029 1.142 4.317 c .501 2.288 1.255 5.456 1.677 7.04 c .663 2.49 .768 3.212 .776 5.333 c .012 3.058 -.363 4.391 -1.75 6.233 c -2.407 3.196 -5.96 4.412 -11.737 4.015 c -1.324 -.091 -2.474 -.231 -2.554 -.312 c -.08 -.08 .164 -.387 .542 -.683 c 1.093 -.856 1.787 -2.241 1.914 -3.821 c .061 -.758 .152 -1.379 .203 -1.379 c .051 0 1.069 .25 2.264 .554 c 4.753 1.213 7.851 .145 9.344 -3.22 c .605 -1.362 .733 -3.696 .329 -5.974 l -.302 -1.706 l -.03 1.28 c -.016 .704 -.165 1.917 -.33 2.697 c -.566 2.669 -1.612 4.023 -3.109 4.023 c -1.706 0 -2.664 -1.871 -3.303 -6.451 c -1.025 -7.345 -1.366 -8.804 -2.365 -10.122 c -.598 -.788 -.617 -.913 -.22 -1.456 c .275 -.376 .33 -.332 .825 .653 c .292 .581 .938 2.303 1.435 3.828 c 1.344 4.122 1.929 5.129 3.276 5.641 c .736 .28 1.854 -.172 2.367 -.954 c 1.145 -1.748 .933 -4.101 -.659 -7.306 a 257.362 257.362 0 0 1 -1.102 -2.233 c -.094 -.206 1.063 -.203 1.367 .003 m -28.051 .662 c -.669 .94 -.895 1.708 -.902 3.063 c -.016 3.327 3.216 5.348 6.296 3.937 c 2.416 -1.105 2.693 -2.965 .811 -5.44 c -1.151 -1.514 -3.444 -2.585 -4.197 -1.96 c -.55 .456 -.358 1.016 .631 1.842 c 1.139 .953 1.313 1.319 .811 1.7 c -1.219 .925 -3.257 -.351 -3.257 -2.04 c 0 -.489 .1 -1.074 .221 -1.301 c .33 -.617 .082 -.498 -.414 .199 m 15.435 -.311 c 2.714 1.134 4.77 5.84 4.535 10.381 c -.124 2.407 -.676 3.92 -1.784 4.893 c -1.496 1.313 -3.662 .444 -4.89 -1.962 c -.569 -1.116 -1.076 -2.882 -1.915 -6.678 c -.634 -2.868 -1.195 -4.517 -1.799 -5.285 c -.37 -.469 -.378 -.561 -.086 -.977 c .177 -.253 .404 -.46 .504 -.46 c .327 0 1.038 1.771 1.587 3.957 c .622 2.477 .971 3.384 1.619 4.208 c 1.021 1.297 3.093 1.66 4.109 .718 c 1.286 -1.192 1.601 -2.947 .905 -5.042 c -.621 -1.87 -2.84 -3.752 -3.567 -3.025 c -.164 .163 -.129 .383 .122 .766 c .193 .295 .352 .728 .352 .961 c 0 .597 -.448 1.511 -.741 1.511 c -.302 0 -1.067 -.698 -1.514 -1.379 c -.402 -.614 -.456 -2.13 -.091 -2.569 c .336 -.405 1.705 -.414 2.654 -.018 m -24.628 .552 c -.616 .616 -.543 .892 .559 2.118 c .975 1.084 .982 1.102 .587 1.541 c -.22 .244 -.612 .525 -.872 .624 c -.597 .229 -1.706 -.142 -1.986 -.664 c -.307 -.576 -.253 -2.245 .101 -3.092 c .447 -1.07 .002 -.909 -.746 .27 c -1.182 1.863 -1.369 4.031 -.494 5.717 c 1.345 2.592 4.606 2.303 6.705 -.594 c .983 -1.357 .782 -3.022 -.579 -4.806 c -1.096 -1.437 -2.481 -1.908 -3.275 -1.114 M 47.202 58.78 c .263 .803 .591 1.496 .73 1.539 c .329 .104 1.765 -1.2 2.519 -2.289 c .326 -.471 .674 -.857 .773 -.857 c .098 0 .329 .292 .514 .649 c .185 .358 .592 .747 .905 .866 c .515 .196 .65 .129 1.429 -.703 l .861 -.918 l .066 .679 c .125 1.288 -1.479 3.458 -2.902 3.928 c -.653 .216 -.862 .172 -2.008 -.419 c -.966 -.498 -1.374 -.609 -1.667 -.453 c -.572 .307 -.684 1.227 -.319 2.622 c .412 1.574 .843 2.425 1.669 3.296 c .813 .857 1.703 .909 3.244 .191 l 1.07 -.498 l .011 2.413 l .01 2.413 l -.818 .327 c -1.031 .413 -2.359 .419 -3.139 .016 c -.894 -.462 -1.441 -1.869 -2.055 -5.284 c -.711 -3.954 -1.229 -5.526 -2.137 -6.489 c -.398 -.422 -.661 -.876 -.583 -1.008 c .339 -.578 1.198 -1.632 1.27 -1.56 c .044 .043 .295 .736 .557 1.539 M 93.653 73.6 c 0 .117 -.149 .213 -.332 .213 c -.183 0 -.274 -.096 -.201 -.213 c .073 -.117 .222 -.213 .333 -.213 c .11 0 .2 .096 .2 .213"
+ readonly property string racket: "M 64 .5 a 64.386 64.386 0 0 0 -21.89 3.81 c 25.982 13.525 60.352 57.646 72.246 98.881 C 122.904 92.4 128 78.792 128 64 C 128 28.93 99.347 .5 64 .5 Z M 21.828 16.236 C 8.45 27.876 0 44.961 0 64 c 0 16.01 5.975 30.637 15.83 41.804 c 8.688 -25.896 25.018 -50.855 39.006 -64.799 c -9.985 -10.672 -21.164 -19.146 -33.008 -24.769 Z m 44.8 39.17 C 52.77 70.22 38.963 96.155 33.968 120.085 c 8.957 4.728 19.176 7.415 30.031 7.415 c 11.161 0 21.65 -2.835 30.786 -7.817 c -5.268 -24.14 -15.204 -46.245 -28.157 -64.277 Z m 0 0"
+ readonly property string haxe: "M 0 0 h 32.3 L 64 16 L 96.3 0 H 128 v 32.6 L 111.8 64 L 128 95.8 V 128 H 95.2 L 64 112.1 L 32.6 128 H 0 V 94.7 L 15.7 64 L 0 31.8 z"
+ readonly property string purescript: "M 47.397 90.262 h 43.211 l -9.84 -9.14 h -43.21 Z m 43.211 -32.035 h -43.21 l -9.84 9.136 h 43.21 z M 47.397 44.465 h 43.207 l -9.836 -9.14 H 37.561 Z M 33.01 53.512 l -6.5 -6.465 L 2.409 71.012 a 4.535 4.535 0 0 0 -1.34 3.23 c 0 1.223 .477 2.371 1.34 3.23 l 24.101 23.97 l 6.5 -6.462 l -20.847 -20.738 z m 92.747 -5.395 l -24.094 -23.972 l -6.496 6.46 l 20.84 20.739 l -20.84 20.738 l 6.496 6.461 l 24.094 -23.961 a 4.558 4.558 0 0 0 1.347 -3.238 a 4.56 4.56 0 0 0 -1.347 -3.227"
+ readonly property string delphi: "M 64 0 C 28.656 0 0 28.656 0 64 s 28.656 64 64 64 s 64 -28.656 64 -64 S 99.344 0 64 0 z m 1.512 10.883 c 1.153 .012 2.309 .11 3.449 .234 c 4.332 .453 8.578 1.32 12.672 2.832 c 2.304 .855 4.586 1.778 6.734 2.98 a 110.26 110.26 0 0 1 4.414 2.614 c 1.106 .699 2.164 1.477 3.2 2.262 a 40.911 40.911 0 0 1 2.746 2.258 c .554 .512 .765 1.226 .687 1.988 c -.109 .969 -.562 1.722 -1.457 2.176 c -.074 .035 -.219 .015 -.289 -.032 c -.407 -.32 -.785 -.675 -1.207 -.984 c -1.262 -.918 -2.512 -1.856 -3.813 -2.723 a 46.178 46.178 0 0 0 -3.765 -2.281 c -1.308 -.699 -2.672 -1.301 -4.028 -1.906 a 31.344 31.344 0 0 0 -2.668 -1.074 a 93.06 93.06 0 0 0 -4.523 -1.422 c -1.903 -.543 -3.852 -.876 -5.809 -1.149 c -1.597 -.22 -3.199 -.406 -4.812 -.43 c -.938 -.016 -1.875 -.078 -2.813 -.046 a 90.33 90.33 0 0 0 -4.48 .25 c -1.113 .093 -2.21 .27 -3.32 .425 a 31.675 31.675 0 0 0 -4.543 .977 c -1.253 .367 -2.503 .75 -3.75 1.156 a 27.383 27.383 0 0 0 -2.25 .848 c -1.312 .57 -2.628 1.141 -3.91 1.777 a 54.43 54.43 0 0 0 -3.75 2.051 c -.964 .573 -1.883 1.242 -2.801 1.89 a 38.79 38.79 0 0 0 -2.102 1.579 a 62.68 62.68 0 0 0 -5.125 4.605 c -.613 .618 -1.168 1.282 -1.738 1.938 c -.586 .679 -1.18 1.355 -1.734 2.062 a 44.754 44.754 0 0 0 -1.766 2.407 a 53.159 53.159 0 0 0 -1.719 2.687 c -.516 .875 -.999 1.78 -1.469 2.68 c -.468 .902 -.937 1.793 -1.347 2.718 c -.418 .938 -.78 1.907 -1.153 2.875 c -.293 .77 -.585 1.543 -.835 2.333 c -.3 .968 -.606 1.937 -.82 2.925 c -.39 1.782 -.793 3.575 -1.063 5.38 c -.242 1.632 -.349 3.28 -.442 4.925 c -.07 1.289 -.05 2.582 -.02 3.871 c .032 1.203 .083 2.41 .208 3.61 c .148 1.425 .388 2.843 .586 4.288 c -1.144 .008 -2.129 -.335 -2.73 -1.363 c -.228 -.379 -.334 -.844 -.419 -1.285 a 39.903 39.903 0 0 1 -.449 -2.828 a 66.652 66.652 0 0 1 -.371 -3.734 c -.083 -1.282 -.145 -2.579 -.113 -3.86 c .039 -1.942 .14 -3.88 .383 -5.812 a 62.118 62.118 0 0 1 1.124 -6.344 a 44.585 44.585 0 0 1 1.93 -6.14 c .414 -1.05 .81 -2.114 1.29 -3.141 a 87.998 87.998 0 0 1 2.206 -4.317 a 37.195 37.195 0 0 1 1.844 -2.988 c .773 -1.144 1.598 -2.242 2.426 -3.344 a 40.166 40.166 0 0 1 1.687 -2.105 c .563 -.653 1.16 -1.262 1.75 -1.883 a 53.5 53.5 0 0 1 1.399 -1.418 c .552 -.531 1.101 -1.07 1.68 -1.57 a 89.046 89.046 0 0 1 2.902 -2.43 c .773 -.605 1.594 -1.148 2.406 -1.707 c .688 -.473 1.367 -.957 2.082 -1.387 c .91 -.55 1.843 -1.062 2.773 -1.57 c .8 -.438 1.602 -.859 2.418 -1.254 c .79 -.375 1.59 -.707 2.387 -1.043 c .62 -.269 1.235 -.547 1.871 -.758 c 1.317 -.449 2.646 -.875 3.973 -1.281 c .735 -.227 1.469 -.445 2.219 -.602 c 1.593 -.324 3.187 -.648 4.797 -.898 a 39.693 39.693 0 0 1 3.96 -.418 a 82.44 82.44 0 0 1 5.2 -.074 z m -.06 6.498 c .833 .011 1.665 .041 2.493 .1 c 1.825 .132 3.637 .374 5.45 .632 c .968 .137 1.925 .38 2.878 .625 c 1.532 .4 3.08 .783 4.582 1.274 c 1.313 .434 2.596 .976 3.856 1.539 c 1.384 .617 2.74 1.304 4.09 1.992 c .575 .289 1.118 .633 1.66 .977 c 1.04 .66 2.09 1.312 3.09 2.03 c 1.005 .712 1.957 1.5 2.93 2.255 c .081 .062 .151 .14 .261 .238 c -.875 .582 -1.722 1.125 -2.562 1.7 c -.891 .612 -1.774 1.25 -2.668 1.874 c -.555 .395 -1.117 .778 -1.676 1.172 c -.688 .492 -1.375 .992 -2.063 1.484 c -.793 .58 -1.597 1.141 -2.39 1.715 c -.59 .426 -1.164 .866 -1.746 1.301 c -.688 .503 -1.387 1.004 -2.063 1.531 c -1.16 .891 -2.363 .736 -3.426 .016 c -2.89 -1.952 -6.03 -3.399 -9.367 -4.422 a 31.389 31.389 0 0 0 -7.086 -1.309 c -1.015 -.066 -2.03 -.097 -3.047 -.136 a 22.752 22.752 0 0 0 -4.222 .25 c -1.106 .168 -2.207 .363 -3.309 .562 c -1.449 .26 -2.847 .707 -4.23 1.2 a 30.428 30.428 0 0 0 -2.813 1.164 a 37.46 37.46 0 0 0 -3.054 1.562 c -.95 .543 -1.844 1.188 -2.75 1.805 a 20.467 20.467 0 0 0 -1.668 1.238 c -.788 .665 -1.55 1.363 -2.32 2.055 c -1.376 1.25 -2.587 2.663 -3.712 4.144 c -.703 .934 -1.327 1.918 -1.957 2.906 c -1.043 1.625 -1.886 3.363 -2.605 5.153 c -.883 2.19 -1.559 4.441 -1.965 6.773 c -.273 1.602 -.504 3.206 -.52 4.824 c -.01 1.351 -.023 2.7 .02 4.051 c .031 .793 .167 1.585 .277 2.375 c .172 1.343 -.359 2.426 -1.582 2.98 c -.363 .165 -.812 .22 -1.215 .188 c -1.156 -.097 -2.305 -.27 -3.46 -.41 c -.387 -.046 -.782 -.09 -1.176 -.156 c -.086 -.016 -.207 -.133 -.223 -.219 c -.215 -1.328 -.452 -2.652 -.61 -3.984 c -.127 -1.094 -.16 -2.2 -.23 -3.297 c -.035 -.633 -.074 -1.265 -.066 -1.895 a 56.125 56.125 0 0 1 .086 -3.144 a 60.934 60.934 0 0 1 .32 -3.48 a 29.67 29.67 0 0 1 .441 -2.821 c .333 -1.551 .716 -3.086 1.106 -4.617 a 29.343 29.343 0 0 1 1.457 -4.29 c .418 -.991 .793 -2.003 1.25 -2.988 c .48 -1.035 1 -2.054 1.57 -3.046 c .645 -1.122 1.343 -2.207 2.035 -3.297 a 40.938 40.938 0 0 1 1.391 -2.047 c .391 -.546 .824 -1.05 1.242 -1.57 c .383 -.477 .758 -.95 1.157 -1.407 c .428 -.5 .882 -.98 1.335 -1.46 c .657 -.676 1.313 -1.351 1.985 -2.008 a 25.64 25.64 0 0 1 1.437 -1.325 c .922 -.777 1.848 -1.562 2.817 -2.277 a 83.903 83.903 0 0 1 3.718 -2.574 c .981 -.637 1.997 -1.255 3.043 -1.785 c 1.344 -.688 2.735 -1.308 4.122 -1.915 c .843 -.375 1.71 -.71 2.585 -1.011 a 58.079 58.079 0 0 1 3.407 -1.051 c 1.187 -.324 2.383 -.645 3.59 -.863 c 1.484 -.274 2.984 -.46 4.48 -.645 c 1.035 -.124 2.082 -.222 3.117 -.234 c .834 -.008 1.669 -.014 2.502 -.002 z m -6.007 23.412 a 45.502 45.502 0 0 1 4.317 .227 a 28.014 28.014 0 0 1 7.215 1.722 c 1.648 .625 3.25 1.352 4.78 2.238 a 35.797 35.797 0 0 1 4.907 3.458 c 1.11 .925 2.18 1.895 3.11 3 a 85.415 85.415 0 0 1 3.038 3.769 c .302 .398 .43 .93 .594 1.418 a 129.996 129.996 0 0 1 1.114 3.43 c .457 1.457 .885 2.934 1.343 4.394 c .343 1.106 .707 2.199 1.051 3.309 c .387 1.234 .75 2.472 1.129 3.71 c .258 .852 .53 1.696 .8 2.544 c .294 .945 .583 1.894 .876 2.843 c .27 .872 .55 1.74 .82 2.606 c .211 .688 .406 1.39 .625 2.078 c .191 .628 .4 1.26 .598 1.89 c .52 1.653 1.03 3.3 1.558 4.954 c .48 1.535 .97 3.066 1.461 4.605 c .055 .188 .125 .367 .188 .555 c .113 .352 -.082 .605 -.406 .48 a 16.18 16.18 0 0 1 -1.977 -.89 c -.16 -.09 -.203 -.406 -.281 -.625 c -.375 -1.06 -.735 -2.11 -1.098 -3.156 c -.414 -1.184 -.82 -2.364 -1.23 -3.547 c -.407 -1.157 -.821 -2.313 -1.227 -3.473 c -.492 -1.426 -.987 -2.843 -1.48 -4.27 c -.351 -1 -.715 -2.007 -1.07 -3.007 a 61.587 61.587 0 0 0 -.598 -1.618 c -.34 -.855 -.957 -1.52 -1.579 -2.175 c -.53 -.551 -1.062 -1.105 -1.625 -1.625 a 93.16 93.16 0 0 0 -3.222 -2.864 c -.788 -.66 -1.781 -.89 -2.781 -1.046 c -1.071 -.172 -2.145 -.344 -3.215 -.52 c -1.313 -.218 -2.625 -.449 -3.938 -.668 c -1.183 -.203 -2.364 -.401 -3.543 -.602 c -1.825 -.303 -3.644 -.613 -5.469 -.917 c -1.487 -.258 -2.968 -.528 -4.457 -.778 c -1.043 -.172 -2.093 -.398 -3.148 -.472 c -.582 -.04 -1.2 .136 -1.781 .3 c -.805 .22 -1.508 .68 -2.106 1.254 a 5.903 5.903 0 0 0 -1.445 2.137 c -.645 1.633 -.543 3.234 .039 4.844 c .375 1.05 .91 2.007 1.68 2.82 c .758 .793 1.636 1.367 2.738 1.625 c 1.324 .305 2.633 .687 3.938 1.043 c 2.35 .641 4.7 1.285 7.05 1.934 c 1.492 .41 2.989 .829 4.489 1.242 c 1.492 .419 2.984 .833 4.48 1.25 c 1.41 .395 2.555 1.195 3.305 2.449 c .382 .633 .625 1.352 .902 2.039 c .324 .833 .629 1.68 .93 2.523 c .418 1.2 .832 2.402 1.242 3.602 c .344 1.004 .694 2.012 1.031 3.02 c .296 .867 .578 1.734 .871 2.605 c .293 .848 .597 1.691 .887 2.543 c .344 .993 .668 2 1.012 2.992 c .488 1.452 .988 2.904 1.48 4.352 c .06 .157 .121 .312 .156 .469 c .051 .218 -.079 .352 -.28 .367 c -.892 .11 -1.786 .195 -2.68 .289 c -.45 .05 -.9 .125 -1.352 .176 c -.941 .105 -1.887 .2 -2.828 .304 c -1.008 .121 -2.016 .25 -3.02 .364 c -1.289 .156 -2.58 .3 -3.87 .437 c -.21 .023 -.438 .05 -.641 .031 a .453 .453 0 0 1 -.309 -.199 c -.363 -.676 -.688 -1.371 -1.063 -2.039 a 70.039 70.039 0 0 0 -1.773 -3.094 a 64.824 64.824 0 0 0 -2.133 -3.281 a 47.53 47.53 0 0 0 -2.27 -2.988 c -.96 -1.172 -1.95 -2.328 -3.003 -3.422 a 37.598 37.598 0 0 0 -3.168 -2.945 a 28.744 28.744 0 0 0 -3.344 -2.407 c -1.187 -.726 -2.414 -1.413 -3.75 -1.844 c -1.539 -.489 -3.032 -1.117 -4.582 -1.539 c -1.531 -.41 -3.125 -.626 -4.684 -.949 c -1.472 -.302 -2.972 -.293 -4.468 -.379 c -.66 -.039 -1.336 .015 -2.004 .055 a 44.18 44.18 0 0 0 -2.281 .164 c -.445 .043 -.883 .125 -1.32 .203 c -.844 .164 -1.692 .34 -2.614 .52 c -.273 -.848 -.574 -1.699 -.816 -2.563 a 27.57 27.57 0 0 1 -.625 -2.531 c -.188 -.973 -.313 -1.957 -.446 -2.941 c -.23 -1.821 -.242 -3.653 -.113 -5.477 c .175 -2.55 .602 -5.07 1.375 -7.52 c .508 -1.597 1.062 -3.187 1.832 -4.687 c .512 -.992 1.02 -1.992 1.59 -2.957 c .43 -.723 .949 -1.406 1.449 -2.094 a 26.073 26.073 0 0 1 1.191 -1.578 c .375 -.453 .778 -.883 1.188 -1.3 c .687 -.689 1.375 -1.385 2.09 -2.04 c .488 -.45 1.012 -.848 1.535 -1.25 a 33.322 33.322 0 0 1 1.715 -1.238 c .78 -.51 1.586 -.992 2.394 -1.461 c .72 -.414 1.438 -.836 2.188 -1.176 a 64.286 64.286 0 0 1 3.422 -1.363 c .672 -.25 1.35 -.484 2.047 -.637 c 1.312 -.293 2.628 -.562 3.96 -.754 c 1.117 -.164 2.251 -.277 3.383 -.277 z m -2.742 22.162 c .187 .009 .376 .03 .567 .065 c 1.71 .324 3.438 .598 5.156 .898 c 1.212 .214 2.418 .438 3.629 .645 c 1.425 .237 2.859 .457 4.28 .687 c 1.033 .176 2.06 .363 3.083 .54 c 1.195 .198 2.387 .385 3.582 .585 c 1.039 .18 2.07 .387 3.105 .55 c .676 .098 1.332 .224 1.934 .536 c .285 .157 .586 .32 .828 .539 a 93.038 93.038 0 0 1 2.578 2.383 a 56.576 56.576 0 0 1 2.203 2.234 c .563 .594 .927 1.301 1.157 2.09 c .175 .617 .406 1.211 .62 1.816 c .302 .832 .598 1.672 .895 2.508 c .375 1.055 .755 2.114 1.125 3.168 c .336 .95 .657 1.903 .985 2.844 c .312 .887 .632 1.77 .945 2.652 c .336 .97 .664 1.93 1 2.899 l .492 1.426 c -.019 .015 -.04 .03 -.062 .05 c -.938 -.652 -1.887 -1.289 -2.813 -1.953 c -.422 -.305 -.63 -.785 -.816 -1.262 c -.394 -.972 -.794 -1.957 -1.184 -2.937 c -.379 -.938 -.75 -1.886 -1.125 -2.824 c -.367 -.913 -.75 -1.82 -1.117 -2.73 c -.383 -.938 -.73 -1.895 -1.133 -2.825 c -.469 -1.09 -.75 -2.277 -1.492 -3.234 c -.48 -.625 -1 -1.22 -1.54 -1.793 a 14.223 14.223 0 0 0 -2.647 -2.207 c -.75 -.473 -1.586 -.63 -2.438 -.785 c -2.395 -.434 -4.773 -.883 -7.168 -1.313 c -2.433 -.445 -4.863 -.875 -7.293 -1.312 c -2.07 -.383 -4.152 -.73 -6.207 -1.165 c -1.375 -.285 -2.637 .864 -2.762 2.165 c -.125 1.312 .886 2.43 1.992 2.71 c 2.989 .75 5.96 1.563 8.946 2.352 c 3.523 .938 7.055 1.848 10.566 2.813 c 1.168 .324 2.219 .918 3.184 1.668 c 1.172 .906 2.136 2 2.816 3.304 c .457 .871 .813 1.79 1.184 2.695 a 75.718 75.718 0 0 1 1.074 2.844 c .492 1.426 .97 2.863 1.441 4.293 c .29 .875 .579 1.75 .852 2.625 c .395 1.262 .781 2.53 1.168 3.793 c .261 .87 .527 1.746 .781 2.621 c .34 1.125 .656 2.262 .989 3.395 c .25 .875 .52 1.742 .761 2.617 c .371 1.293 .746 2.586 1.09 3.887 c .172 .676 -.352 1.433 -1.062 1.531 c -1.465 .199 -2.923 .375 -4.383 .559 c -.97 .108 -1.936 .222 -2.906 .328 c -1.175 .125 -2.352 .246 -3.524 .37 c -1.484 .157 -2.968 .325 -4.453 .481 c -.628 .063 -1.26 .164 -1.89 .149 c -1.384 -.016 -2.615 -.453 -3.673 -1.387 c -.766 -.676 -1.187 -1.563 -1.68 -2.406 c -.417 -.72 -.812 -1.442 -1.257 -2.145 c -.687 -1.086 -1.375 -2.168 -2.117 -3.21 c -.711 -.993 -1.492 -1.946 -2.258 -2.895 a 26.025 26.025 0 0 0 -1.219 -1.418 c -1.254 -1.395 -2.578 -2.731 -4.031 -3.926 c -1.11 -.906 -2.234 -1.8 -3.418 -2.598 c -2.875 -1.945 -5.98 -3.414 -9.348 -4.277 c -2.329 -.598 -4.683 -1.035 -7.11 -1 c -1.218 .027 -2.417 .121 -3.608 .375 c -.852 .184 -1.645 .113 -2.352 -.473 c -.387 -.327 -.555 -.77 -.773 -1.187 c -.095 -.176 -.153 -.363 -.235 -.582 c .375 -.106 .758 -.227 1.145 -.313 c .402 -.093 .809 -.176 1.215 -.226 c 1.036 -.117 2.074 -.282 3.109 -.305 c 1.5 -.039 3.008 -.051 4.5 .063 c 1.394 .105 2.789 .324 4.164 .605 a 43.492 43.492 0 0 1 4.32 1.105 c 1.274 .407 2.516 .933 3.75 1.457 c 2.063 .886 3.875 2.168 5.598 3.586 c 1.145 .938 2.23 1.926 3.215 3.028 c 1.199 1.344 2.39 2.691 3.515 4.097 c .801 .997 1.508 2.08 2.235 3.133 c 1.219 1.77 2.292 3.634 3.312 5.524 c .25 .476 .493 .957 .7 1.453 c .343 .812 1.062 1.062 1.816 .972 c 1.019 -.117 2.03 -.25 3.05 -.367 c 1.247 -.145 2.49 -.293 3.731 -.425 c 1.438 -.157 2.875 -.293 4.309 -.458 a 95.975 95.975 0 0 0 3.332 -.425 c .626 -.094 1.074 -.7 1.11 -1.348 c .03 -.574 -.243 -1.062 -.411 -1.582 c -.477 -1.449 -.98 -2.887 -1.477 -4.332 c -.472 -1.398 -.94 -2.801 -1.41 -4.191 c -.496 -1.47 -.976 -2.937 -1.476 -4.407 c -.343 -1 -.699 -1.996 -1.051 -2.996 c -.285 -.832 -.552 -1.668 -.848 -2.492 c -.36 -.988 -.734 -1.977 -1.11 -2.957 c -.198 -.508 -.39 -1.02 -.636 -1.512 c -.574 -1.168 -1.449 -2.062 -2.574 -2.738 c -1.055 -.637 -2.257 -.844 -3.414 -1.168 c -3.405 -.964 -6.816 -1.902 -10.23 -2.844 c -1.46 -.406 -2.93 -.82 -4.395 -1.226 c -1.168 -.325 -2.324 -.689 -3.504 -.95 c -1.375 -.3 -2.309 -1.125 -3.039 -2.269 a 7.07 7.07 0 0 1 -1.074 -3.106 a 4.638 4.638 0 0 1 .636 -2.914 c .571 -.949 1.364 -1.609 2.352 -2.035 a 3.65 3.65 0 0 1 1.621 -.283 z"
+ readonly property string coffeescript: "M 50.3 29.6 c 11.7 -1 15 -8.5 28.7 -9.8 c 6.7 -.6 11 .8 11.4 3.1 c .4 2.2 -2.9 3.7 -7 4 c -5.6 .6 -8 -1.5 -8.4 -3.4 c -4.1 .4 -4.8 2.2 -4.6 3.5 c .4 2.4 5.5 4.7 14.1 3.9 c 9.8 -.8 13 -4.6 12.2 -8.5 c -1 -5 -8.5 -9.2 -22 -8 c -17.3 1.6 -17.2 9.5 -28.9 10.5 c -4.8 .4 -7.5 -.7 -8 -2.6 c -.3 -1.9 2 -2.8 4.8 -3 c 2.6 -.2 5.7 .2 7.2 1 c 1.1 -.6 1.5 -1.1 1.3 -1.8 c -.4 -1.8 -4 -2.6 -8.5 -2.2 c -8.7 .8 -8.7 4.7 -8.4 6.4 c 1.1 4.7 7.8 7.7 16.1 6.9 z m 58.6 19.8 c -10.8 2.5 -24.6 4.1 -41.2 4.1 c -16.9 0 -30.7 -1.8 -41.5 -4.1 c -9.6 -2.5 -14.8 -5.2 -16.6 -8 c .9 6.3 2.5 12.4 4.6 18.2 c -2.4 1.5 -4.7 3.5 -6.7 6 C 3.7 70.4 2 76 2.3 81.5 c .3 5.5 3 10 7.3 13.5 c 4.5 3.5 9.3 4.5 14.8 3.5 c 2.1 -.3 4.5 -1.5 6.6 -2.1 c -4.5 0 -8.3 -1.5 -12.1 -4.5 c -4.1 -3 -7 -7.3 -7.6 -12.4 c -1 -4.8 0 -9.3 2.7 -13.2 c .6 -.8 1.2 -1.4 1.9 -2 c 1.5 3.8 3.3 7.4 5.2 10.9 c 4.1 6.3 8.3 11.8 12.4 17.7 c 1.8 3.5 3 7 3.8 10.4 c 2.7 3.8 6.6 6.5 11.4 7.9 c 5.9 2.1 12.1 2.9 18.4 2.9 h .7 c 6.3 0 12.9 -1 19 -3 c 4.5 -1.5 8.3 -4 11.1 -8 h .3 c .7 -3 1.8 -6.8 3.5 -10.3 c 4.1 -5.9 8.3 -11.4 12.4 -17.7 c 5.5 -10 9.3 -21.4 11.4 -33.6 c -2.1 3 -7.3 5.8 -16.6 7.9 z m -82.7 -8.7 c 10.8 2.7 24.6 4.1 41.2 4.1 c 16.9 0 30.4 -1.5 41.2 -4.1 c 11.4 -2.7 16.9 -6.3 16.9 -9.6 c 0 -2.5 -2.5 -4.8 -7 -6.6 c 1 .7 1.8 1.8 1.8 3 c 0 3.5 -5.2 6.3 -15.6 8.6 c -9.6 2.1 -22 3.6 -37 3.6 c -14.5 0 -27.4 -1.5 -36.7 -3.5 c -10 -2.5 -15.3 -5.2 -15.3 -8.6 c 0 -1.5 .7 -2.7 2.7 -4.1 c -6.3 2.5 -9.6 4.5 -9.6 7.6 c .3 3.5 5.9 7 17.4 9.6 z"
+ readonly property string elm: "M 64 60.74 l 25.65 -25.65 h -51.3 L 64 60.74 z M 7.91 4.65 l 25.83 25.84 h 56.17 L 64.07 4.65 H 7.91 z M 67.263 63.993 l 28.08 -28.08 l 27.951 27.953 l -28.08 28.079 z M 123.35 57.42 V 4.65 H 70.58 l 52.77 52.77 z M 60.74 64 L 4.65 7.91 V 120.1 L 60.74 64 z M 98.47 95.21 l 24.88 24.89 V 70.33 L 98.47 95.21 z M 64 67.26 L 7.91 123.35 h 112.18 L 64 67.26 z"
+ readonly property string awk: "M 74.351 122.47 c .73 -2.936 -2.013 -5.706 -3.838 -3.88 c -2.244 2.264 -3.773 1.341 -3.124 -1.864 c .376 -1.932 .188 -3.021 -.547 -3.021 c -1.933 0 -1.235 -2.83 1.11 -4.488 c 1.235 -.859 4.3 -1.573 6.818 -1.594 c 2.517 0 5.26 -.483 6.081 -1.09 c 1.17 -.816 .902 -.944 -1.18 -.483 c -1.482 .338 -3.94 .129 -5.422 -.44 c -2.04 -.773 -3.462 -.644 -5.77 .505 c -3.838 1.905 -3.774 1.932 -3.774 -.42 c 0 -1.341 -.821 -2.055 -2.603 -2.264 l -2.577 -.338 l 2.201 -2.346 c 2.351 -2.518 4.906 -2.577 11.81 -.354 c 4.508 1.449 4.127 .021 -.715 -2.706 c -4.573 -2.56 -7.697 -6.103 -19.313 -21.847 c -12.373 -16.736 -13.253 -18.894 -13.795 -33.027 l -.461 -11.825 l -6.42 -7.525 c -3.521 -4.133 -6.87 -7.805 -7.44 -8.16 c -.569 -.332 -3.317 -.9 -6.103 -1.234 c -5.727 -.67 -6.081 -1.492 -2.936 -6.817 c 2.47 -4.235 5.743 -4.718 10.483 -1.572 c 1.68 1.11 3.714 1.97 4.552 1.948 c .945 -.037 .644 -.483 -.752 -1.17 c -1.261 -.59 -2.27 -1.406 -2.27 -1.804 c 0 -1.132 13.296 -2.012 17.177 -1.11 c 8.051 1.84 10.628 4.67 11.99 13.123 c .924 5.722 1.53 6.876 5.68 10.778 c 2.539 2.41 7.214 5.62 10.38 7.129 c 3.168 1.508 7.59 4.235 9.856 6.038 c 5.325 4.235 13.672 16.436 16.522 24.112 c 1.235 3.334 2.748 6.646 3.355 7.402 c 1.658 1.992 -1.004 3.355 -5.304 2.684 a 74.022 74.022 0 0 0 -8.577 -.628 c -2.791 -.043 -5.663 -.569 -6.393 -1.154 c -1.868 -1.508 -6.796 -12.684 -7.697 -17.402 c -1.573 -8.39 -4.59 -11.487 -15.201 -15.62 c -7.612 -2.957 -8.175 -3.419 -12.265 -9.796 c -3.758 -5.85 -4.466 -10.252 -1.61 -10.038 c 2.93 .226 2.136 -3.693 -1.031 -5.207 c -2.163 -1.046 -3.586 -1.09 -6.19 -.225 c -2.243 .73 -4.755 .794 -7.546 .161 c -2.287 -.52 -4.17 -.708 -4.17 -.413 c 0 .268 2.345 3.06 5.222 6.183 c 8.384 9.184 9.055 10.671 8.298 18.578 c -.44 4.574 -.268 7.257 .526 8.218 c .65 .779 1.09 1.804 .988 2.265 c -.107 .484 .564 2.56 1.465 4.633 c .924 2.056 1.68 4.444 1.68 5.287 c 0 .859 .71 1.551 1.59 1.551 c 1.18 0 1.384 .59 .858 2.265 c -.606 1.885 -.483 2.072 .714 1.069 c 1.181 -.967 1.68 -.59 2.748 2.034 c .714 1.76 1.74 4.278 2.266 5.599 c .526 1.299 1.825 2.598 2.898 2.871 c 1.085 .269 2.201 1.74 2.555 3.29 c .333 1.53 1.45 3.694 2.453 4.778 c 1.004 1.095 2.158 2.963 2.534 4.154 c .945 2.958 5.496 5.996 9.689 6.48 c 2.64 .316 3.5 .88 3.5 2.372 c 0 1.068 .525 1.84 1.153 1.717 c .65 -.123 1.718 .462 2.416 1.256 c .665 .822 1.696 1.493 2.281 1.493 c .59 0 0 -1.133 -1.299 -2.518 c -1.299 -1.385 -2.093 -2.807 -1.739 -3.167 c .354 -.333 1.847 .8 3.312 2.56 c 2.936 3.479 6.312 5.658 7.3 4.67 c .354 -.332 -1.406 -2.361 -3.88 -4.524 c -2.475 -2.137 -4.155 -4.236 -3.758 -4.654 c .424 -.419 1.89 .333 3.274 1.674 c 6.667 6.458 12.077 10.005 15.325 10.005 c 1.53 0 1.514 -.187 -.107 -1.975 c -.961 -1.068 -1.487 -2.388 -1.149 -2.952 a 1.165 1.165 0 0 0 -.376 -1.616 c -.547 -.338 -3.102 -4.696 -5.684 -9.688 c -2.577 -4.992 -5.534 -9.517 -6.586 -10.086 c -3.752 -2.013 -1.74 -2.641 7.826 -2.453 l 9.747 .214 l 8.385 8.927 c 4.616 4.927 8.223 9.103 8.014 9.313 c -.193 .21 -1.283 -.108 -2.416 -.714 c -2.79 -1.508 -3.06 -1.406 -2.453 .923 c .59 2.244 -.692 2.663 -2.512 .816 c -.67 -.644 -1.556 -.837 -1.975 -.419 c -.419 .44 1.932 3.253 5.244 6.248 c 3.543 3.232 5.62 5.808 5.035 6.313 c -1.326 1.127 -22.797 -.295 -25.438 -1.718 c -1.176 -.633 -3.983 -1.133 -6.248 -1.133 c -3.42 0 -4.106 .333 -4.106 1.986 c 0 4.053 -3.296 10.048 -7.698 14.074 c -2.496 2.266 -4.76 4.134 -5.013 4.134 c -.252 0 -.188 -1.074 .15 -2.351 z m 4.842 -6.565 c -.44 -1.342 -.333 -1.4 .44 -.29 c .816 1.112 1.299 .918 2.56 -1.03 c .838 -1.32 1.675 -3.312 1.863 -4.402 c .274 -1.717 -.145 -1.97 -2.705 -1.68 c -3.602 .419 -6.436 1.804 -8.825 4.278 c -2.163 2.265 -2.222 2.936 -.23 2.936 c .837 0 2.286 .902 3.21 1.992 c 1.969 2.286 4.589 1.009 3.687 -1.804 z m -3.521 -3.247 c 2.517 -2.094 4.17 -2.539 4.17 -1.133 c 0 .816 -4.444 3.226 -5.85 3.167 c -.376 -.021 .359 -.944 1.68 -2.04 z m 34.97 -14.09 c -.354 -.57 -1.09 -1.01 -1.631 -1.01 c -.57 0 -.736 .44 -.376 1.01 c .333 .542 1.068 1.004 1.61 1.004 c .57 0 .736 -.462 .397 -1.004 z M 107.52 85.84 c 2.748 -1.052 1.766 -2.416 -1.3 -1.782 c -1.653 .311 -3.204 .08 -3.585 -.548 c -.945 -1.508 -2.872 -1.406 -2.346 .124 c .231 .697 1.509 1.556 2.85 1.932 c 1.364 .376 2.54 .735 2.62 .773 c .107 .064 .902 -.161 1.76 -.5 z m -9.501 -5.346 c 0 -.483 -.709 -1.181 -1.59 -1.514 c -2.474 -.94 -2.806 -.73 -1.427 .902 c 1.385 1.68 3.022 2.013 3.022 .612 z M 85.082 68.058 c -.73 -.736 -1.192 -.795 -1.192 -.145 c 0 1.342 1.197 2.533 1.868 1.862 c .29 -.29 -.021 -1.068 -.67 -1.717 z m 2.727 -3.275 c -1.74 -1.717 -8.299 -5.593 -8.74 -5.131 c -.316 .29 -.375 .692 -.15 .859 c .231 .166 1.74 1.32 3.334 2.539 c 2.394 1.798 7.112 3.29 5.555 1.739 z M 95 62.228 c 0 -1.213 -3.709 -3.607 -7.085 -4.546 c -2.265 -.655 -2.158 -.424 1.025 2.367 c 3.543 3.124 6.066 4.026 6.066 2.18 z m -9.914 -16.37 c -.735 -.736 -1.197 -.8 -1.197 -.172 c 0 1.363 1.197 2.539 1.868 1.868 c .29 -.269 -.021 -1.047 -.67 -1.702 z M 77.83 40.57 c 0 -.252 -.462 -.735 -1.004 -1.073 c -.569 -.333 -1.009 -.14 -1.009 .445 c 0 .59 .44 1.074 1.01 1.074 c .541 0 1.003 -.194 1.003 -.446 z m -13.564 -4.133 c -2.077 -1.551 -3.231 -1.299 -2.12 .462 c .36 .612 1.428 1.073 2.372 1.052 c 1.487 -.022 1.466 -.215 -.252 -1.508 z m -22.77 -26.71 a 1.053 1.053 0 0 0 -1.073 -1.025 c -.586 0 -.795 .456 -.462 1.025 c .36 .548 .843 1.004 1.073 1.004 c .253 0 .462 -.456 .462 -1.004 z m -19.19 -1.025 c 0 -.548 -1.025 -.988 -2.281 -.988 c -1.825 .022 -1.976 .21 -.757 .988 c 1.97 1.277 3.038 1.277 3.038 0 z M 79.51 90.828 c -.36 -.376 -.671 -1.342 -.671 -2.158 c 0 -1.175 .29 -1.218 1.503 -.215 c .843 .698 1.535 1.664 1.535 2.147 c 0 1.047 -1.406 1.17 -2.367 .226 z m -13.843 -15.39 c -1.905 -1.61 -3.102 -3.311 -2.662 -3.773 c .461 -.461 1.304 -.043 1.868 .967 c .547 1.003 2.093 2.222 3.435 2.742 c 1.342 .505 2.458 1.385 2.458 1.954 c 0 1.718 -1.406 1.192 -5.099 -1.889 z M 8.447 11.403 L 5.2 10.232 l 2.555 -3.296 c 1.889 -2.41 3.397 -3.269 5.62 -3.269 c 1.68 0 2.855 .338 2.646 .751 c -.215 .425 -1.154 2.459 -2.12 4.53 c -.945 2.1 -1.825 3.758 -1.97 3.715 a 453.21 453.21 0 0 1 -3.484 -1.261 z"
+ readonly property string matlab: "M 123.965 91.902 c -7.246 -18.297 -13.262 -37.058 -20.184 -55.476 c -3.054 -7.84 -6.047 -15.746 -10.215 -23.082 c -1.656 -2.633 -3.238 -5.528 -5.953 -7.215 a 4.013 4.013 0 0 0 -2.222 -.606 c -1.27 .028 -2.536 .594 -3.504 1.415 c -3.645 2.886 -5.805 7.082 -8.227 10.949 c -4.277 7.172 -8.789 14.687 -15.941 19.347 c -3.36 2.371 -7.762 2.63 -11 5.172 c -4.43 3.34 -7.442 8.078 -11.074 12.184 c -.829 .988 -2.11 1.383 -3.227 1.918 C 21.578 60.93 10.738 65.336 0 69.98 c 9.09 7.032 18.777 13.29 28.05 20.079 c 2.544 -.504 5.098 -1.547 7.72 -1.082 c 4.16 1.3 6.597 5.285 8.503 8.93 c 3.875 7.94 6.676 16.323 9.813 24.57 c 5.246 -.375 9.969 -3.079 14.027 -6.258 c 7.809 -6.324 13.758 -14.5 20.305 -22.047 c 3.14 -3.3 6.34 -7.23 11.05 -8.149 c 4.762 -1.152 9.864 .555 13.395 3.836 c 4.957 4.43 9.344 9.551 15.137 12.942 c -.777 -3.836 -2.645 -7.278 -4.035 -10.899 z M 42.96 79.012 c -4.57 2.703 -9.426 4.93 -14.176 7.289 c -7.457 -4.996 -14.723 -10.29 -22.05 -15.465 c 9.878 -4.328 19.91 -8.348 29.917 -12.387 c 4.746 3.703 9.637 7.223 14.383 10.926 c -2.23 3.563 -4.914 6.871 -8.074 9.637 z m 10.168 -12.414 C 48.414 63.058 43.64 59.609 39 55.977 c 2.977 -4.055 6.238 -7.977 10.14 -11.172 c 2.587 -1.657 5.743 -2.117 8.426 -3.61 c 6.368 -3.18 10.711 -9.011 14.86 -14.582 c -5.317 13.805 -10.992 27.664 -19.297 39.985 z m 0 0"
+ readonly property string solidity: "M 43.322 0 L 22.756 36.576 l 20.566 36.561 l 20.564 -36.561 h 41.143 L 84.465 0 H 43.322 z m 41.342 54.863 L 64.1 91.424 H 22.955 L 43.519 128 h 41.145 l 20.58 -36.576 l -20.58 -36.561 z"
+ readonly property string wasm: "M .223 .222 v 127.555 h 127.555 V .222 H 78.594 c .014 .227 .036 .455 .036 .686 c 0 8.08 -6.55 14.626 -14.63 14.626 c -8.078 0 -14.625 -6.546 -14.625 -14.626 c 0 -.231 .022 -.459 .031 -.686 z m 29.595 68.746 h 8.445 l 5.782 30.738 h .107 l 6.968 -30.738 h 7.908 l 6.265 31.119 h .106 l 6.597 -31.119 h 8.284 l -10.765 45.156 H 61.12 l -6.213 -30.738 H 54.8 l -6.7 30.738 h -8.557 z m 59.994 0 h 13.334 l 13.284 45.156 h -8.77 l -2.879 -10.051 H 89.59 l -2.212 10.05 h -8.5 Z M 94.895 80.1 l -3.684 16.57 h 11.473 L 98.448 80.1 Z"
+ readonly property string vim: "M 72.6 80.5 c .2 .2 .6 .5 .9 .5 h 5.3 c .3 0 .7 -.3 .9 -.5 l 1.4 -1.5 c .2 -.2 .3 -.4 .3 -.6 l 1.5 -5.1 c .1 -.5 0 -1 -.3 -1.3 l -1.1 -.9 c -.2 -.2 -.6 -.1 -.9 -.1 h -4.8 l -.2 -.2 l -.1 -.1 c -.2 0 -.4 -.1 -.6 .1 L 73 72 c -.2 0 -.3 .5 -.4 .7 L 71 77.6 c -.2 .5 -.1 1.1 .3 1.5 l 1.3 1.4 z m .8 26.4 l -.4 .1 h -1.2 L 79 85.9 c .2 -.7 -.1 -1.5 -.8 -1.7 l -.4 -.1 H 65.7 c -.5 .1 -.9 .5 -1 1 l -.7 2.5 c -.2 .7 .3 1.3 1 1.5 l .3 -.1 h 1.8 l -7.3 20.9 c -.2 .7 .1 1.6 .8 1.9 l .4 .3 h 11.2 c .6 0 1.1 -.5 1.3 -1.1 l .7 -2.4 c .3 -.7 -.1 -1.5 -.8 -1.7 z m 53.1 -19.7 l -1.9 -2.5 v -.1 c -.3 -.3 -.6 -.6 -1 -.6 h -7.2 c -.4 0 -.7 .4 -1 .6 l -2 2.4 h -3.1 l -2.1 -2.4 v -.1 c -.2 -.3 -.6 -.5 -1 -.5 h -4 l 20.2 -20.2 l -22.6 -22.4 L 121 20.6 v -9 L 118.2 8 H 77.3 L 74 11.5 v 2.9 L 62.7 3 L 55 10.5 L 52.6 8 H 12.2 L 9 11.7 v 9.4 l 3 2.9 h 3 v 26.1 l -14 14 l 14 14 v 32 l 5.2 2.9 h 11.6 l 9.1 -9.5 l 21.6 21.6 L 77 110.6 c .1 .4 .4 .5 .9 .7 l .4 -.2 h 9.4 c .6 0 1.1 -.1 1.2 -.6 l .7 -2 c .2 -.7 -.1 -1.3 -.8 -1.5 l -.4 .1 H 88 l 3.4 -10.7 l 2.3 -2.3 h 5 l -5 15.9 c -.2 .7 .2 1.1 .9 1.4 l .4 -.2 h 9.1 c .5 0 1 -.1 1.2 -.6 l .8 -1.8 c .3 -.7 -.1 -1.3 -.7 -1.6 c -.1 -.1 -.3 0 -.5 0 h -.4 l 4.2 -13 h 6.1 l -5.1 15.9 c -.2 .7 .2 1.1 .9 1.3 l .4 -.3 h 10 c .5 0 1 -.1 1.2 -.6 l .8 -2 c .3 -.7 -.1 -1.3 -.8 -1.5 c -.1 -.1 -.3 .1 -.5 .1 h -.7 l 5.6 -18.5 c .2 -.5 .1 -1.1 -.1 -1.4 z M 62.7 4.9 L 74 16.2 v 4.7 l 3.4 4.1 H 79 L 50 53 V 25 h 3.3 l 2.7 -4.2 v -8.9 l -.2 -.3 l 6.9 -6.7 z M 2.9 64.1 L 15 52 v 24.2 L 2.9 64.1 z m 38.9 38.3 l 58.4 -60 l 21.4 21.5 l -20.2 20.2 h -.1 c -.3 .1 -.5 .3 -.7 .5 L 98.5 87 h -2.9 l -2.2 -2.4 c -.2 -.3 -.6 -.6 -1 -.6 h -8.8 c -.6 0 -1.1 .4 -1.3 1 l -.8 2.5 c -.2 .7 .1 1.3 .8 1.6 h 1.5 L 77.4 108 l -15.1 15.2 l -20.5 -20.8 z"
+ readonly property string sql: "M 115.95 2.781 c -5.504 -4.906 -12.16 -2.933 -18.738 2.902 a 47.9 47.9 0 0 0 -2.918 2.856 c -11.246 11.93 -21.684 34.02 -24.926 50.895 c 1.262 2.563 2.25 5.832 2.902 8.328 c .325 1.238 .617 2.488 .875 3.746 c 0 0 -.101 -.379 -.515 -1.578 l -.266 -.777 a 8.12 8.12 0 0 0 -.176 -.426 c -.734 -1.707 -2.761 -5.309 -3.656 -6.875 a 172.299 172.299 0 0 0 -2.008 6.27 c 2.582 4.714 4.149 12.8 4.149 12.8 s -.133 -.527 -.782 -2.355 c -.57 -1.617 -3.437 -6.637 -4.117 -7.809 c -1.16 4.29 -1.62 7.18 -1.207 7.883 c .813 1.363 1.578 3.723 2.25 6.324 c 1.528 5.868 2.586 13.016 2.586 13.016 l .094 1.192 c -.203 4.886 -.102 9.781 .297 14.656 c .508 6.113 1.457 11.359 2.668 14.172 l .824 -.45 c -1.781 -5.535 -2.504 -12.792 -2.184 -21.155 c .477 -12.79 3.422 -28.215 8.856 -44.29 c 9.191 -24.261 21.938 -43.733 33.602 -53.034 c -10.63 9.601 -25.023 40.695 -29.332 52.203 C 79.404 74.162 75.99 86.252 73.93 97.84 c 3.555 -10.863 15.043 -15.527 15.043 -15.527 s 5.637 -6.954 12.223 -16.883 c -3.945 .898 -10.426 2.441 -12.598 3.351 c -3.2 1.34 -4.063 1.797 -4.063 1.797 s 10.371 -6.312 19.27 -9.172 c 12.234 -19.27 25.566 -46.645 12.145 -58.625 M 16.896 5.681 c -5.398 .02 -9.77 4.39 -9.785 9.789 v 88.574 c .016 5.398 4.39 9.765 9.785 9.785 h 50.227 a 122.816 122.816 0 0 1 -.277 -14.438 c -.031 -.332 -.059 -.754 -.086 -1.067 a 143.095 143.095 0 0 0 -2.523 -12.684 c -.645 -2.507 -1.465 -4.789 -1.965 -5.636 c -.621 -1.051 -.524 -1.653 -.52 -2.305 c 0 -.64 .082 -1.305 .2 -2.059 c .316 -1.878 .73 -3.738 1.246 -5.574 l 1.156 -.148 c -.09 -.188 -.074 -.348 -.164 -.516 l -.219 -2.031 c .64 -2.137 1.316 -4.262 2.04 -6.371 l 1.066 -.102 c -.043 -.082 -.055 -.203 -.098 -.28 l -.23 -1.685 c 3.363 -17.496 13.8 -39.699 25.601 -52.219 c .352 -.37 .711 -.683 1.055 -1.035 z"
+ readonly property string json: "M 45.949 63.71 c 0 22.998 17.853 30.038 18.223 30.175 l .016 .005 a .823 .823 0 0 0 -.05 .021 h -.001 v .001 h .001 c 20.306 6.899 40.668 -10.261 40.668 -41.251 c 0 -17.054 -7.595 -37.361 -27.7 -48.894 C 105.528 10.083 126 34.393 126 63.903 c 0 35.822 -30.182 62.003 -61.873 62.003 c -2.078 0 -37.078 -10.935 -37.216 -51.164 c -.103 -30.18 19.208 -40.406 32.72 -38.442 c .009 .003 -13.682 7.806 -13.682 27.41 z M 82.051 64.29 c 0 -22.998 -17.853 -30.038 -18.223 -30.175 l -.016 -.005 a .823 .823 0 0 0 .05 -.021 h .001 v -.001 h -.001 C 43.556 27.19 23.193 44.35 23.193 75.34 c 0 17.054 7.595 37.361 27.7 48.894 C 22.472 117.917 2 93.607 2 64.097 C 2 28.275 32.182 2.094 63.873 2.094 c 2.078 0 37.078 10.935 37.216 51.164 c .103 30.18 -19.207 40.406 -32.72 38.443 c -.009 -.004 13.682 -7.807 13.682 -27.411 z"
+ readonly property string yaml: "m 0.5 5.6289 l 21.754 34.15 v 21.646 h 13.959 v -21.646 l 22.775 -34.15 h -15.02 l -13.947 21.988 l -13.832 -21.988 h -15.689 z m 63.994 0.13086 l -23.66 55.797 h 11.189 l 5.1387 -12.408 h 25.266 l 4.252 12.408 h 11.957 l -22.699 -55.797 h -11.443 z m 5.9922 11.773 l 7.7441 20.475 h -16.387 l 8.6426 -20.475 z m 16.195 50.139 v 54.451 h 40.818 v -11.635 v -0.00195 h -28.863 v -42.814 h -11.955 z m -64.428 0.011719 v 54.687 h 11.734 v -37.723 l 12.279 25.355 h 9.2344 l 12.699 -26.246 v 38.602 h 11.256 v -54.676 h -15.369 l -13.637 24.732 l -12.986 -24.732 h -15.211 z"
+ readonly property string xml: "M 23.25 37.459 c -2.661 .114 -2.363 3.885 -2.875 5.797 c -.837 3.125 -1.608 7.1 -1.853 10.324 c -.246 3.138 -.044 6.046 .353 9.219 c .309 2.467 1.02 4.104 .373 4.767 c -.52 .623 -17.199 15.92 -18.543 16.786 c -1.392 .903 -.428 3.13 .37 4.379 c .841 1.317 1.24 1.999 2.99 1.765 c 1.645 -.222 2.167 -2.182 3.127 -3.248 c 1.146 -1.353 12.917 -12.824 13.957 -12.824 l -.026 -.147 c .421 2.923 1.972 5.705 3.598 8.112 c .798 1.19 1.68 2.352 2.699 3.37 c .936 .937 2.145 1.702 3.145 2.409 c 2.352 .123 4.246 -1.618 1.875 -3.59 c -.905 -.757 -3.935 -5.212 -4.961 -7.676 c -.957 -2.303 -3.074 -5.762 -.782 -7.654 c 7.921 -6.6 19.887 -12.132 22.711 -12.406 c -1.377 5.422 -7.257 16.32 -9.408 21.48 c -.741 1.785 -1.233 3.92 -.982 5.864 c .297 2.324 2.046 4.152 3.144 4.326 c 2.229 -.127 3.74 -2.086 5.06 -3.463 c 1.832 -1.908 3.396 -3.672 5.221 -5.588 c 1.812 -1.907 3.593 -3.918 5.2 -5.734 c .988 -1.12 3.6 -3.829 5.152 -3.829 c -.584 3.732 -3.832 8.33 -4.486 12.094 c -.667 3.85 2.844 8.563 6.52 5.131 c 3.336 -3.117 6.72 -6.056 10 -9.22 c 1.162 -1.112 .935 .2 1.196 1.296 c .263 1.114 .945 4.875 1.473 5.89 c .968 1.857 2.653 3.176 4.799 3.212 c 1.951 .032 3.897 -.862 5.258 -2.219 c 1.015 -1.011 2.757 -3.749 .83 -4.463 c -1.23 -.456 -1.159 -.943 -2.65 -.693 c -.925 .158 -1.94 .395 -2.717 .959 c -1.967 -.008 -2.042 -4.756 -1.737 -6.215 c .36 -1.741 1.107 -3.522 .992 -5.34 c -.135 -2.24 -2.554 -3.707 -3.115 -3.865 c -.832 -.41 -1.343 .117 -2.447 1.133 c -.928 .857 -1.947 1.778 -2.863 2.646 c -1.829 1.733 -4.11 4.36 -5.041 4.36 c 1.634 -3.73 3.8 -14.16 -3.442 -13.24 c -3.01 .384 -5.085 2.02 -7.457 3.806 a 252.59 252.59 0 0 1 -5.014 4.674 c -1.269 1.146 -1.99 2.152 -2.654 2.152 c .707 -3.962 9.02 -16.976 4.74 -20.644 c -.858 -.722 -1.678 -1.742 -2.761 -1.864 c -1.511 .186 -3.002 .552 -4.457 1.016 c -3.023 1.043 -14.846 6.9 -22.399 12.306 c -.701 .508 -1.15 -.451 -1.236 -1.558 c -.472 -5.732 1.06 -11.749 2.654 -17.1 c 1.103 -3.732 -2.992 -6.563 -3.531 -6.693 z m 96.842 4.877 c -.889 1.952 -1.067 4.788 -1.332 7.06 v .008 c -2.02 -.238 -4.646 -.495 -6.668 -.617 c 1.273 1.586 3.698 3.149 5.658 3.88 c -.261 1.958 -2.326 3.584 -3.207 5.476 c 2.448 -.679 3.62 -2.514 5.291 -4.403 c 1.935 1.178 4.227 3.363 6.742 3.41 c -.952 -2.022 -1.792 -3.723 -3.525 -5.183 c 1.44 -.94 3.842 -1.927 4.949 -3.387 c -1.844 -.396 -3.832 -.5 -5.629 -.08 c -1.38 -1.523 -1.605 -4.141 -2.28 -6.164 z m -14.733 .387 c -1.233 .113 -3.28 2.633 -4.453 4.144 c -4.323 5.564 -8.265 16.89 -8.9 18.977 c -1.935 6.398 -2.686 14.285 1.502 19.916 c 2.436 3.284 6.728 3.532 10.935 2.324 c 2.17 -.624 4.344 -2.181 5.086 -3.875 c .615 -1.411 .932 -4.037 -2.166 -2.035 c -1.198 .773 -3.395 1.627 -7.334 1.564 c -1.32 -.024 -3.49 -1.92 -4.072 -3.88 c -1.024 -3.194 -.588 -7.458 -.588 -7.458 c .892 -5.505 3.447 -11.47 5.688 -16.474 c .869 -1.932 3.517 -6.748 4.51 -8.283 c 0 .004 1.27 -1.997 .7 -4.123 l -.01 .017 c -.167 -.624 -.487 -.852 -.898 -.814 z m 16.45 15.935 c -.647 1.417 -.774 3.483 -.965 5.133 h -.01 c -1.464 -.17 -3.38 -.358 -4.836 -.45 c .924 1.152 2.686 2.285 4.106 2.817 c -.18 1.416 -1.682 2.606 -2.325 3.975 c 1.781 -.493 2.639 -1.829 3.844 -3.194 c 1.404 .845 3.074 2.436 4.899 2.471 c -.679 -1.471 -1.294 -2.709 -2.555 -3.764 c 1.039 -.678 2.78 -1.408 3.582 -2.459 c -1.333 -.289 -2.789 -.36 -4.082 -.056 c -1.003 -1.107 -1.167 -3 -1.658 -4.473 z m -6.55 12.95 c -.443 .98 -.533 2.4 -.665 3.546 l -.012 -.004 a 62.34 62.34 0 0 0 -3.344 -.308 c .635 .8 1.853 1.58 2.836 1.949 c -.127 .98 -1.16 1.794 -1.605 2.742 c 1.23 -.345 1.82 -1.264 2.658 -2.213 c .979 .595 2.125 1.689 3.387 1.713 c -.472 -1.015 -.894 -1.863 -1.762 -2.598 c .717 -.472 1.92 -.972 2.479 -1.7 c -.927 -.2 -1.927 -.252 -2.823 -.042 c -.694 -.761 -.811 -2.07 -1.148 -3.086 z"
+ readonly property string html: "M 9.032 2 l 10.005 112.093 l 44.896 12.401 l 45.02 -12.387 L 118.968 2 H 9.032 z m 89.126 26.539 l -.627 7.172 L 97.255 39 H 44.59 l 1.257 14 h 50.156 l -.336 3.471 l -3.233 36.119 l -.238 2.27 L 64 102.609 v .002 l -.034 .018 l -28.177 -7.423 L 33.876 74 h 13.815 l .979 10.919 L 63.957 89 H 64 v -.546 l 15.355 -3.875 L 80.959 67 H 33.261 l -3.383 -38.117 L 29.549 25 h 68.939 l -.33 3.539 z"
+ readonly property string css: "M 8.76 1 l 10.055 112.883 l 45.118 12.58 l 45.244 -12.626 L 119.24 1 H 8.76 z m 89.591 25.862 l -3.347 37.605 l .01 .203 l -.014 .467 v -.004 l -2.378 26.294 l -.262 2.336 L 64 101.607 v .001 l -.022 .019 l -28.311 -7.888 L 33.75 72 h 13.883 l .985 11.054 l 15.386 4.17 l -.004 .008 v -.002 l 15.443 -4.229 L 81.075 65 H 48.792 l -.277 -3.043 l -.631 -7.129 L 47.553 51 h 34.749 l 1.264 -14 H 30.64 l -.277 -3.041 l -.63 -7.131 L 29.401 23 h 69.281 l -.331 3.862 z"
+ readonly property string sass: "M 1.219 56.156 c 0 .703 .207 1.167 .323 1.618 c .756 2.933 2.381 5.45 4.309 7.746 c 2.746 3.272 6.109 5.906 9.554 8.383 c 2.988 2.148 6.037 4.248 9.037 6.38 c .515 .366 1.002 .787 1.561 1.236 c -.481 .26 -.881 .489 -1.297 .7 c -3.959 2.008 -7.768 4.259 -11.279 6.986 c -2.116 1.644 -4.162 3.391 -5.607 5.674 c -2.325 3.672 -3.148 7.584 -1.415 11.761 c .506 1.22 1.278 2.274 2.367 3.053 c .353 .252 .749 .502 1.162 .6 c 1.058 .249 2.136 .412 3.207 .609 l 3.033 -.002 c 3.354 -.299 6.407 -1.448 9.166 -3.352 c 4.312 -2.976 7.217 -6.966 8.466 -12.087 c .908 -3.722 .945 -7.448 -.125 -11.153 a 11.696 11.696 0 0 0 -.354 -1.014 c -.13 -.333 -.283 -.657 -.463 -1.072 l 6.876 -3.954 l .103 .088 c -.125 .409 -.258 .817 -.371 1.23 c -.817 2.984 -1.36 6.02 -1.165 9.117 c .208 3.3 1.129 6.389 3.061 9.146 c 1.562 2.23 5.284 2.313 6.944 .075 c .589 -.795 1.16 -1.626 1.589 -2.513 c 1.121 -2.315 2.159 -4.671 3.23 -7.011 l .187 -.428 c -.077 1.108 -.167 2.081 -.208 3.055 c -.064 1.521 .025 3.033 .545 4.48 c .445 1.238 1.202 2.163 2.62 2.326 c .97 .111 1.743 -.333 2.456 -.896 a 10.384 10.384 0 0 0 2.691 -3.199 c 1.901 -3.491 3.853 -6.961 5.576 -10.54 c 1.864 -3.871 3.494 -7.855 5.225 -11.792 l .286 -.698 c .409 1.607 .694 3.181 1.219 4.671 c .61 1.729 1.365 3.417 2.187 5.058 c .389 .775 .344 1.278 -.195 1.928 c -2.256 2.72 -4.473 5.473 -6.692 8.223 c -.491 .607 -.98 1.225 -1.389 1.888 a 3.701 3.701 0 0 0 -.48 1.364 a 1.737 1.737 0 0 0 1.383 1.971 a 9.661 9.661 0 0 0 2.708 .193 c 3.097 -.228 5.909 -1.315 8.395 -3.157 c 3.221 -2.386 4.255 -5.642 3.475 -9.501 c -.211 -1.047 -.584 -2.065 -.947 -3.074 c -.163 -.455 -.174 -.774 .123 -1.198 c 2.575 -3.677 4.775 -7.578 6.821 -11.569 c .081 -.157 .164 -.314 .306 -.482 c .663 3.45 1.661 6.775 3.449 9.792 c -.912 .879 -1.815 1.676 -2.632 2.554 c -1.799 1.934 -3.359 4.034 -4.173 6.595 c -.35 1.104 -.619 2.226 -.463 3.405 c .242 1.831 1.742 3.021 3.543 2.604 c 3.854 -.892 7.181 -2.708 9.612 -5.925 c 1.636 -2.166 1.785 -4.582 1.1 -7.113 c -.188 -.688 -.411 -1.365 -.651 -2.154 c .951 -.295 1.878 -.649 2.837 -.868 c 4.979 -1.136 9.904 -.938 14.702 .86 c 2.801 1.05 5.064 2.807 6.406 5.571 c 1.639 3.379 .733 6.585 -2.452 8.721 c -.297 .199 -.637 .356 -.883 .605 a .869 .869 0 0 0 -.205 .67 c .021 .123 .346 .277 .533 .275 c 1.047 -.008 1.896 -.557 2.711 -1.121 c 2.042 -1.413 3.532 -3.314 3.853 -5.817 l .063 -.188 l -.077 -1.63 c -.031 -.094 .023 -.187 .016 -.258 c -.434 -3.645 -2.381 -6.472 -5.213 -8.688 c -3.28 -2.565 -7.153 -3.621 -11.249 -3.788 a 25.401 25.401 0 0 0 -9.765 1.503 c -.897 .325 -1.786 .71 -2.688 1.073 c -.121 -.219 -.251 -.429 -.358 -.646 c -.926 -1.896 -2.048 -3.708 -2.296 -5.882 c -.176 -1.544 -.392 -3.086 -.025 -4.613 c .353 -1.469 .813 -2.913 1.246 -4.362 c .223 -.746 .066 -1.164 -.646 -1.5 a 2.854 2.854 0 0 0 -.786 -.258 c -1.75 -.254 -3.476 -.109 -5.171 .384 c -.6 .175 -1.036 .511 -1.169 1.175 c -.076 .381 -.231 .746 -.339 1.122 c -.443 1.563 -.757 3.156 -1.473 4.645 c -1.794 3.735 -3.842 7.329 -5.938 10.897 c -.227 .385 -.466 .763 -.752 1.23 c -.736 -1.54 -1.521 -2.922 -1.759 -4.542 c -.269 -1.832 -.481 -3.661 -.025 -5.479 c .339 -1.356 .782 -2.687 1.19 -4.025 c .193 -.636 .104 -.97 -.472 -1.305 c -.291 -.169 -.62 -.319 -.948 -.368 a 11.643 11.643 0 0 0 -5.354 .438 c -.543 .176 -.828 .527 -.994 1.087 c -.488 1.652 -.904 3.344 -1.589 4.915 c -2.774 6.36 -5.628 12.687 -8.479 19.013 c -.595 1.321 -1.292 2.596 -1.963 3.882 c -.17 .326 -.418 .613 -.63 .919 c -.17 -.201 -.236 -.339 -.235 -.477 c .005 -.813 -.092 -1.65 .063 -2.436 a 172.189 172.189 0 0 1 1.578 -7.099 c .47 -1.946 1.017 -3.874 1.538 -5.807 c .175 -.647 .178 -1.252 -.287 -1.796 c -.781 -.911 -2.413 -1.111 -3.381 -.409 l -.428 .242 l .083 -.69 c .204 -1.479 .245 -2.953 -.161 -4.41 c -.506 -1.816 -1.802 -2.861 -3.686 -2.803 c -.878 .027 -1.8 .177 -2.613 .497 c -3.419 1.34 -6.048 3.713 -8.286 6.568 a 2.592 2.592 0 0 1 -.757 .654 c -2.893 1.604 -5.795 3.188 -8.696 4.778 l -3.229 1.769 c -.866 -.826 -1.653 -1.683 -2.546 -2.41 c -2.727 -2.224 -5.498 -4.393 -8.244 -6.592 c -2.434 -1.949 -4.792 -3.979 -6.596 -6.56 c -1.342 -1.92 -2.207 -4.021 -2.29 -6.395 c -.105 -3.025 .753 -5.789 2.293 -8.362 c 1.97 -3.292 4.657 -5.934 7.611 -8.327 c 3.125 -2.53 6.505 -4.678 10.008 -6.639 c 4.901 -2.743 9.942 -5.171 15.347 -6.774 c 5.542 -1.644 11.165 -2.585 16.965 -1.929 c 2.28 .258 4.494 .78 6.527 1.895 c 1.557 .853 2.834 1.97 3.428 3.716 c .586 1.718 .568 3.459 .162 5.204 c -.825 3.534 -2.76 6.447 -5.195 9.05 c -3.994 4.267 -8.866 7.172 -14.351 9.091 a 39.478 39.478 0 0 1 -9.765 2.083 c -2.729 .229 -5.401 -.013 -7.985 -.962 c -1.711 -.629 -3.201 -1.591 -4.399 -2.987 c -.214 -.25 -.488 -.521 -.887 -.287 c -.391 .23 -.46 .602 -.329 .979 c .219 .626 .421 1.278 .762 1.838 c .857 1.405 2.107 2.424 3.483 3.298 c 2.643 1.681 5.597 2.246 8.66 2.377 c 4.648 .201 9.183 -.493 13.654 -1.74 c 6.383 -1.78 11.933 -4.924 16.384 -9.884 c 3.706 -4.13 6.353 -8.791 6.92 -14.419 c .277 -2.747 -.018 -5.438 -1.304 -7.944 c -1.395 -2.715 -3.613 -4.734 -6.265 -6.125 C 68.756 18.179 64.588 17 60.286 17 h -4.31 c -5.21 0 -10.247 1.493 -15.143 3.274 c -3.706 1.349 -7.34 2.941 -10.868 4.703 c -7.683 3.839 -14.838 8.468 -20.715 14.833 c -2.928 3.171 -5.407 6.67 -6.833 10.79 a 40.494 40.494 0 0 0 -1.111 3.746 m 27.839 36.013 c -.333 4.459 -2.354 8.074 -5.657 11.002 c -1.858 1.646 -3.989 2.818 -6.471 3.23 c -.9 .149 -1.821 .185 -2.694 -.188 c -1.245 -.532 -1.524 -1.637 -1.548 -2.814 c -.037 -1.876 .62 -3.572 1.521 -5.186 c 1.176 -2.104 2.9 -3.708 4.741 -5.206 c 2.9 -2.361 6.046 -4.359 9.268 -6.245 l .243 -.1 c .498 1.84 .735 3.657 .597 5.507 z M 54.303 70.98 c -.235 1.424 -.529 2.849 -.945 4.229 c -1.438 4.777 -3.285 9.406 -5.282 13.973 c -.369 .845 -.906 1.616 -1.373 2.417 a 1.689 1.689 0 0 1 -.283 .334 c -.578 .571 -1.126 .541 -1.418 -.206 c -.34 -.868 -.549 -1.797 -.729 -2.716 c -.121 -.617 -.092 -1.265 -.13 -1.897 c .039 -4.494 1.41 -8.578 3.736 -12.38 c .959 -1.568 2.003 -3.062 3.598 -4.054 a 6.27 6.27 0 0 1 1.595 -.706 c .85 -.239 1.372 .154 1.231 1.006 z m 17.164 21.868 l 6.169 -7.203 c .257 2.675 -4.29 8.015 -6.169 7.203 z m 19.703 -4.847 c -.436 .25 -.911 .43 -1.358 .661 c -.409 .212 -.544 -.002 -.556 -.354 a 2.385 2.385 0 0 1 .093 -.721 c .833 -2.938 2.366 -5.446 4.647 -7.486 l .16 -.082 c 1.085 3.035 -.169 6.368 -2.986 7.982 z"
+ readonly property string markdown: "M 11.95 24.348 c -5.836 0 -10.618 4.867 -10.618 10.681 v 57.942 c 0 5.814 4.782 10.681 10.617 10.681 h 104.102 c 5.835 0 10.617 -4.867 10.617 -10.681 V 35.03 c 0 -5.814 -4.783 -10.681 -10.617 -10.681 H 14.898 l -.002 -.002 H 11.95 z m -.007 9.543 h 104.108 c .625 0 1.076 .423 1.076 1.14 v 57.94 c 0 .717 -.453 1.14 -1.076 1.14 H 11.949 c -.623 0 -1.076 -.423 -1.076 -1.14 V 35.029 c 0 -.715 .451 -1.135 1.07 -1.138 z M 20.721 84.1 V 43.9 H 32.42 l 11.697 14.78 L 55.81 43.9 h 11.696 v 40.2 H 55.81 V 61.044 l -11.694 14.78 l -11.698 -14.78 V 84.1 H 20.722 z m 73.104 0 L 76.28 64.591 h 11.697 V 43.9 h 11.698 v 20.69 h 11.698 z m 0 0"
+ readonly property string docker: "M 124.8 52.1 c -4.3 -2.5 -10 -2.8 -14.8 -1.4 c -.6 -5.2 -4 -9.7 -8 -12.9 l -1.6 -1.3 l -1.4 1.6 c -2.7 3.1 -3.5 8.3 -3.1 12.3 c .3 2.9 1.2 5.9 3 8.3 c -1.4 .8 -2.9 1.9 -4.3 2.4 c -2.8 1 -5.9 2 -8.9 2 H 79 V 49 H 66 V 24 H 51 v 12 H 26 v 13 H 13 v 14 H 1.8 l -.2 1.5 c -.5 6.4 .3 12.6 3 18.5 l 1.1 2.2 l .1 .2 c 7.9 13.4 21.7 19 36.8 19 c 29.2 0 53.3 -13.1 64.3 -40.6 c 7.4 .4 15 -1.8 18.6 -8.9 l .9 -1.8 l -1.6 -1 z M 28 39 h 10 v 11 H 28 V 39 z m 13.1 44.2 c 0 1.7 -1.4 3.1 -3.1 3.1 c -1.7 0 -3.1 -1.4 -3.1 -3.1 c 0 -1.7 1.4 -3.1 3.1 -3.1 c 1.7 .1 3.1 1.4 3.1 3.1 z M 28 52 h 10 v 11 H 28 V 52 z m -13 0 h 11 v 11 H 15 V 52 z m 27.7 50.2 c -15.8 -.1 -24.3 -5.4 -31.3 -12.4 c 2.1 .1 4.1 .2 5.9 .2 c 1.6 0 3.2 0 4.7 -.1 c 3.9 -.2 7.3 -.7 10.1 -1.5 c 2.3 5.3 6.5 10.2 14 13.8 h -3.4 z M 51 63 H 40 V 52 h 11 v 11 z m 0 -13 H 40 V 39 h 11 v 11 z m 13 13 H 53 V 52 h 11 v 11 z m 0 -13 H 53 V 39 h 11 v 11 z m 0 -13 H 53 V 26 h 11 v 11 z m 13 26 H 66 V 52 h 11 v 11 z M 38.8 81.2 c -.2 -.1 -.5 -.2 -.8 -.2 c -1.2 0 -2.2 1 -2.2 2.2 c 0 1.2 1 2.2 2.2 2.2 s 2.2 -1 2.2 -2.2 c 0 -.3 -.1 -.6 -.2 -.8 c -.2 .3 -.4 .5 -.8 .5 c -.5 0 -.9 -.4 -.9 -.9 c .1 -.4 .3 -.7 .5 -.8 z"
+ readonly property string latex: "M 29.2 63 H 28 c -.5 5.1 -1.2 11.3 -10 11.3 h -4 c -2.3 0 -2.4 -.3 -2.4 -2 V 45.8 c 0 -1.7 0 -2.4 4.7 -2.4 h 1.6 v -1.5 c -1.9 .1 -6.3 .1 -8.4 .1 c -1.9 0 -5.8 0 -7.5 -.1 v 1.5 h 1.1 c 3.8 0 3.9 .5 3.9 2.3 v 26.1 c 0 1.8 -.1 2.3 -3.9 2.3 H 2 v 1.5 h 25.8 L 29.2 63 z M 28.3 41.8 c -.2 -.6 -.3 -.8 -.9 -.8 s -.8 .2 -1 .8 l -8 20.3 c -.3 .8 -.9 2.4 -4 2.4 v 1.2 h 7.7 v -1.2 c -1.5 0 -2.5 -.7 -2.5 -1.7 c 0 -.2 0 -.3 .1 -.7 l 1.7 -4.3 h 9.9 l 2 5.1 c .1 .2 .2 .4 .2 .6 c 0 1 -1.9 1 -2.8 1 v 1.2 h 9.8 v -1.2 h -.7 c -2.3 0 -2.6 -.3 -2.9 -1.3 l -8.6 -21.4 z m -1.9 3.6 l 4.4 11.3 h -8.9 l 4.5 -11.3 z M 68.2 42.2 H 37.9 L 37 53.3 h 1.2 c .7 -8 1.4 -9.7 9 -9.7 c .9 0 2.2 0 2.7 .1 c 1 .2 1 .7 1 1.9 v 26.1 c 0 1.7 0 2.4 -5.2 2.4 h -2 v 1.5 c 2 -.1 7.1 -.1 9.4 -.1 s 7.4 0 9.5 .1 v -1.5 h -2 c -5.2 0 -5.2 -.7 -5.2 -2.4 v -26 c 0 -1 0 -1.7 .9 -1.9 c .5 -.1 1.9 -.1 2.8 -.1 c 7.5 0 8.2 1.6 8.9 9.7 h 1.2 l -1 -11.2 z M 94.9 74.2 h -1.2 c -1.2 7.6 -2.4 11.3 -10.9 11.3 h -6.6 c -2.3 0 -2.4 -.3 -2.4 -2 V 70.2 h 4.4 c 4.8 0 5.4 1.6 5.4 5.8 h 1.2 V 62.9 h -1.2 c 0 4.2 -.5 5.8 -5.4 5.8 h -4.4 v -12 c 0 -1.6 .1 -2 2.4 -2 h 6.4 c 7.6 0 8.9 2.7 9.7 9.7 h 1.2 l -1.4 -11.2 H 64.2 v 1.5 h 1.1 c 3.8 0 3.9 .5 3.9 2.3 v 26 c 0 1.8 -.1 2.3 -3.9 2.3 h -1.1 V 87 h 28.6 l 2.1 -12.8 z M 109.9 56.6 l 6.8 -10 c 1 -1.6 2.7 -3.2 7.2 -3.2 v -1.5 H 112 v 1.5 c 2 0 3.1 1.1 3.1 2.3 c 0 .5 -.1 .6 -.4 1.1 l -5.7 8.4 l -6.4 -9.6 c -.1 -.1 -.3 -.5 -.3 -.7 c 0 -.6 1.1 -1.4 3.2 -1.5 v -1.5 c -1.7 .1 -5.3 .1 -7.2 .1 c -1.5 0 -4.6 0 -6.5 -.1 v 1.5 h .9 c 2.7 0 3.7 .3 4.6 1.7 l 9.1 13.8 l -8.1 12 c -.7 1 -2.2 3.3 -7.2 3.3 v 1.5 H 103 v -1.5 c -2.3 0 -3.1 -1.4 -3.1 -2.3 c 0 -.4 .1 -.6 .5 -1.2 l 7 -10.4 l 7.9 11.9 c .1 .2 .2 .4 .2 .5 c 0 .6 -1.1 1.4 -3.2 1.5 v 1.5 c 1.7 -.1 5.4 -.1 7.2 -.1 c 2.1 0 4.4 0 6.5 .1 v -1.5 h -.9 c -2.6 0 -3.6 -.2 -4.7 -1.8 l -10.5 -15.8 z"
+ readonly property string graphql: "M 18.39 96.852 l -4.6 -2.657 L 65.04 5.434 l 4.597 2.656 z m 0 0 M 12.734 87.105 H 115.23 v 5.31 H 12.734 z m 0 0 M 66.031 119.688 L 14.766 90.09 l 2.656 -4.602 l 51.266 29.602 z m 0 0 M 110.566 42.543 L 59.301 12.941 l 2.656 -4.597 l 51.266 29.597 z m 0 0 M 17.434 42.523 l -2.657 -4.601 l 51.27 -29.598 l 2.656 4.598 z m 0 0 M 109.621 96.852 L 58.375 8.09 l 4.598 -2.656 l 51.25 88.761 z m 0 0 M 16.8 34.398 h 5.313 v 59.204 h -5.312 z m 0 0 M 105.887 34.398 h 5.312 v 59.204 h -5.312 z m 0 0 M 65.129 117.441 l -2.32 -4.02 l 44.586 -25.745 l 2.32 4.02 z m 0 0 M 118.238 95.328 c -3.07 5.344 -9.918 7.168 -15.261 4.098 c -5.344 -3.074 -7.168 -9.922 -4.098 -15.266 c 3.074 -5.344 9.922 -7.168 15.266 -4.097 c 5.375 3.105 7.199 9.921 4.093 15.265 M 29.09 43.84 c -3.074 5.344 -9.922 7.168 -15.266 4.097 c -5.344 -3.074 -7.168 -9.921 -4.097 -15.265 c 3.074 -5.344 9.921 -7.168 15.265 -4.098 c 5.344 3.106 7.168 9.922 4.098 15.266 M 9.762 95.328 c -3.075 -5.344 -1.25 -12.16 4.093 -15.266 c 5.344 -3.07 12.16 -1.246 15.266 4.098 c 3.07 5.344 1.246 12.16 -4.098 15.266 c -5.375 3.07 -12.191 1.246 -15.261 -4.098 M 98.91 43.84 c -3.07 -5.344 -1.246 -12.16 4.098 -15.266 c 5.344 -3.07 12.16 -1.246 15.265 4.098 c 3.07 5.344 1.247 12.16 -4.097 15.266 c -5.344 3.07 -12.192 1.246 -15.266 -4.098 M 64 126.656 a 11.158 11.158 0 0 1 -11.168 -11.168 A 11.158 11.158 0 0 1 64 104.32 a 11.158 11.158 0 0 1 11.168 11.168 c 0 6.145 -4.992 11.168 -11.168 11.168 M 64 23.68 a 11.158 11.158 0 0 1 -11.168 -11.168 A 11.158 11.158 0 0 1 64 1.344 a 11.158 11.158 0 0 1 11.168 11.168 A 11.158 11.158 0 0 1 64 23.68"
+
+ function path(language) {
+ const key = language.toLowerCase()
+
+ switch (key) {
+ case "c":
+ return c
+ case "cpp":
+ return cpp
+ case "csharp":
+ return csharp
+ case "python":
+ return python
+ case "rust":
+ return rust
+ case "javascript":
+ return javascript
+ case "typescript":
+ return typescript
+ case "bash":
+ return bash
+ case "zsh":
+ return zsh
+ case "powershell":
+ return powershell
+ case "cmake":
+ return cmake
+ case "lua":
+ return lua
+ case "java":
+ return java
+ case "kotlin":
+ return kotlin
+ case "swift":
+ return swift
+ case "go":
+ return go
+ case "dart":
+ return dart
+ case "php":
+ return php
+ case "ruby":
+ return ruby
+ case "scala":
+ return scala
+ case "haskell":
+ return haskell
+ case "elixir":
+ return elixir
+ case "erlang":
+ return erlang
+ case "clojure":
+ return clojure
+ case "r":
+ return r
+ case "perl":
+ return perl
+ case "zig":
+ return zig
+ case "nim":
+ return nim
+ case "ocaml":
+ return ocaml
+ case "fsharp":
+ return fsharp
+ case "visualbasic":
+ return visualbasic
+ case "fortran":
+ return fortran
+ case "crystal":
+ return crystal
+ case "gleam":
+ return gleam
+ case "julia":
+ return julia
+ case "objectivec":
+ return objectivec
+ case "vala":
+ return vala
+ case "groovy":
+ return groovy
+ case "racket":
+ return racket
+ case "haxe":
+ return haxe
+ case "purescript":
+ return purescript
+ case "delphi":
+ return delphi
+ case "coffeescript":
+ return coffeescript
+ case "elm":
+ return elm
+ case "awk":
+ return awk
+ case "matlab":
+ return matlab
+ case "solidity":
+ return solidity
+ case "wasm":
+ return wasm
+ case "vim":
+ return vim
+ case "sql":
+ return sql
+ case "json":
+ return json
+ case "yaml":
+ return yaml
+ case "xml":
+ return xml
+ case "html":
+ return html
+ case "css":
+ return css
+ case "sass":
+ return sass
+ case "markdown":
+ return markdown
+ case "docker":
+ return docker
+ case "latex":
+ return latex
+ case "graphql":
+ return graphql
+ case "cpp":
+ return cpp
+ case "cc":
+ return cpp
+ case "cxx":
+ return cpp
+ case "cs":
+ return csharp
+ case "js":
+ return javascript
+ case "jsx":
+ return javascript
+ case "ts":
+ return typescript
+ case "tsx":
+ return typescript
+ case "py":
+ return python
+ case "sh":
+ return bash
+ case "shell":
+ return bash
+ case "ps1":
+ return powershell
+ case "vb":
+ return visualbasic
+ case "objective-c":
+ return objectivec
+ case "obj-c":
+ return objectivec
+ case "groovyscript":
+ return groovy
+ case "pascal":
+ return delphi
+ case "coffee":
+ return coffeescript
+ case "mysql":
+ return sql
+ case "postgres":
+ return sql
+ case "postgresql":
+ return sql
+ case "sqlite":
+ return sql
+ case "yml":
+ return yaml
+ case "htm":
+ return html
+ case "scss":
+ return sass
+ case "md":
+ return markdown
+ case "dockerfile":
+ return docker
+ case "docker-compose":
+ return docker
+ case "tex":
+ return latex
+ case "gql":
+ return graphql
+ default:
+ return ""
+ }
+ }
+
+ function name(language) {
+ switch (language.toLowerCase()) {
+ case "c":
+ return "C"
+ case "cpp":
+ return "C++"
+ case "csharp":
+ return "C#"
+ case "python":
+ return "Python"
+ case "rust":
+ return "Rust"
+ case "javascript":
+ return "JavaScript"
+ case "typescript":
+ return "TypeScript"
+ case "bash":
+ return "Bash"
+ case "zsh":
+ return "Zsh"
+ case "powershell":
+ return "PowerShell"
+ case "cmake":
+ return "CMake"
+ case "lua":
+ return "Lua"
+ case "java":
+ return "Java"
+ case "kotlin":
+ return "Kotlin"
+ case "swift":
+ return "Swift"
+ case "go":
+ return "Go"
+ case "dart":
+ return "Dart"
+ case "php":
+ return "PHP"
+ case "ruby":
+ return "Ruby"
+ case "scala":
+ return "Scala"
+ case "haskell":
+ return "Haskell"
+ case "elixir":
+ return "Elixir"
+ case "erlang":
+ return "Erlang"
+ case "clojure":
+ return "Clojure"
+ case "r":
+ return "R"
+ case "perl":
+ return "Perl"
+ case "zig":
+ return "Zig"
+ case "nim":
+ return "Nim"
+ case "ocaml":
+ return "OCaml"
+ case "fsharp":
+ return "F#"
+ case "visualbasic":
+ return "Visual Basic"
+ case "fortran":
+ return "Fortran"
+ case "crystal":
+ return "Crystal"
+ case "gleam":
+ return "Gleam"
+ case "julia":
+ return "Julia"
+ case "objectivec":
+ return "Objective-C"
+ case "vala":
+ return "Vala"
+ case "groovy":
+ return "Groovy"
+ case "racket":
+ return "Racket"
+ case "haxe":
+ return "Haxe"
+ case "purescript":
+ return "PureScript"
+ case "delphi":
+ return "Delphi"
+ case "coffeescript":
+ return "CoffeeScript"
+ case "elm":
+ return "Elm"
+ case "awk":
+ return "AWK"
+ case "matlab":
+ return "MATLAB"
+ case "solidity":
+ return "Solidity"
+ case "wasm":
+ return "Wasm"
+ case "vim":
+ return "Vim"
+ case "sql":
+ return "SQL"
+ case "json":
+ return "JSON"
+ case "yaml":
+ return "YAML"
+ case "xml":
+ return "XML"
+ case "html":
+ return "HTML"
+ case "css":
+ return "CSS"
+ case "sass":
+ return "Sass"
+ case "markdown":
+ return "Markdown"
+ case "docker":
+ return "Docker"
+ case "latex":
+ return "LaTeX"
+ case "graphql":
+ return "GraphQL"
+ case "cpp":
+ return "C++"
+ case "cc":
+ return "C++"
+ case "cxx":
+ return "C++"
+ case "cs":
+ return "C#"
+ case "js":
+ return "JavaScript"
+ case "jsx":
+ return "JavaScript"
+ case "ts":
+ return "TypeScript"
+ case "tsx":
+ return "TypeScript"
+ case "py":
+ return "Python"
+ case "sh":
+ return "Bash"
+ case "shell":
+ return "Bash"
+ case "ps1":
+ return "PowerShell"
+ case "vb":
+ return "Visual Basic"
+ case "objective-c":
+ return "Objective-C"
+ case "obj-c":
+ return "Objective-C"
+ case "groovyscript":
+ return "Groovy"
+ case "pascal":
+ return "Delphi"
+ case "coffee":
+ return "CoffeeScript"
+ case "mysql":
+ return "SQL"
+ case "postgres":
+ return "SQL"
+ case "postgresql":
+ return "SQL"
+ case "sqlite":
+ return "SQL"
+ case "yml":
+ return "YAML"
+ case "htm":
+ return "HTML"
+ case "scss":
+ return "Sass"
+ case "md":
+ return "Markdown"
+ case "dockerfile":
+ return "Docker"
+ case "docker-compose":
+ return "Docker"
+ case "tex":
+ return "LaTeX"
+ case "gql":
+ return "GraphQL"
+ default:
+ return language
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/ContentBubble.qml b/Modules/Notifications/Sidebar/Chat/Content/ContentBubble.qml
new file mode 100644
index 0000000..75666af
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/ContentBubble.qml
@@ -0,0 +1,138 @@
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Llm
+import ZShell.Config
+import qs.Components
+import qs.Services
+
+Item {
+ id: root
+
+ required property bool isUser
+ required property LlmSegment segment
+ required property bool hovered
+ required property Repeater repeater
+ required property int index
+ required property ChatMessage message
+ required property ChatGeneration current
+
+ // Widest the bubble may grow to; assistant bubbles always use it so
+ // the markdown blocks have a deterministic width to wrap against.
+ readonly property real contentMaxWidth: width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge
+
+ signal edit(text: string)
+
+ implicitHeight: bubble.implicitHeight + actionsRow.implicitHeight + actionsRow.anchors.topMargin
+
+ CustomClippingRect {
+ id: bubble
+
+ radius: Tokens.rounding.medium
+ color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
+ implicitWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
+ implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : blocks.implicitHeight + blocks.anchors.topMargin * 2
+ anchors.right: root.isUser ? parent.right : undefined
+
+ // Behavior on implicitHeight {
+ // enabled: !root.segment.running
+ //
+ // Anim {
+ // type: Anim.DefaultEffects
+ // }
+ // }
+
+ // User messages stay a plain editable text field.
+ TextEditBase {
+ id: msgText
+
+ property string cachedText: root.segment.text
+ property bool cancelled: false
+
+ visible: root.isUser
+ anchors.left: parent.left
+ anchors.margins: Tokens.padding.medium
+ anchors.right: parent.right
+ anchors.top: parent.top
+ animateCursor: false
+ color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
+ cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary
+ font.pointSize: Tokens.font.size.smaller
+ readOnly: true
+ selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
+ text: root.isUser ? root.segment.text : ""
+ textFormat: CustomText.MarkdownText
+ wrapMode: Text.WordWrap
+
+ Keys.onPressed: event => {
+ if (event.key == Qt.Key_Return) {
+ if (!(event.modifiers & Qt.ShiftModifier)) {
+ readOnly = true;
+ event.accepted = true;
+ }
+ } else if (event.key == Qt.Key_Escape) {
+ cancelled = true;
+ readOnly = true;
+ event.accepted = true;
+ }
+ }
+ onEditingFinished: {
+ readOnly = true;
+ }
+ onReadOnlyChanged: {
+ if (readOnly) {
+ animateCursor = false;
+ textFormat = CustomText.MarkdownText;
+
+ if (cancelled) {
+ text = cachedText;
+ cancelled = false;
+ return;
+ }
+
+ root.edit(text);
+ } else {
+ textFormat = CustomText.PlainText;
+ text = cachedText;
+ forceActiveFocus();
+ cursorPosition = text.length;
+ animateCursor = true;
+ }
+ }
+ }
+
+ // Assistant content: parsed markdown blocks.
+ MarkdownBlocks {
+ id: blocks
+
+ visible: !root.isUser
+ anchors.topMargin: Tokens.padding.medium
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ blocks: root.segment.markdown
+ }
+ }
+
+ Actions {
+ id: actionsRow
+
+ readonly property bool shouldBeActive: (root.segment.type === LlmSegment.Type.Content) && !root.segment.running && root.repeater.count === (root.index + 1)
+
+ anchors.left: parent.left
+ anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
+ anchors.top: bubble.bottom
+ opacity: shouldBeActive ? 1 : 0
+ visible: opacity > 0
+ anchors.topMargin: Tokens.spacing.medium
+ edit: msgText
+ hovered: root.hovered
+ isUser: root.isUser
+ segment: root.segment
+ current: root.current
+ message: root.message
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/EmptyBackground.qml b/Modules/Notifications/Sidebar/Chat/Content/EmptyBackground.qml
new file mode 100644
index 0000000..d3370e7
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/EmptyBackground.qml
@@ -0,0 +1,32 @@
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Config
+import qs.Components
+import qs.Services
+
+ColumnLayout {
+ id: root
+
+ spacing: Tokens.spacing.small
+
+ Item {
+ Layout.fillHeight: true
+ }
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignHCenter
+ color: Colors.tPalette.m3outlineVariant
+ font.pointSize: 36
+ text: "chat"
+ }
+
+ CustomText {
+ Layout.alignment: Qt.AlignHCenter
+ color: Colors.tPalette.m3onSurfaceVariant
+ text: qsTr("Send a message to get started")
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/MarkdownBlocks.qml b/Modules/Notifications/Sidebar/Chat/Content/MarkdownBlocks.qml
new file mode 100644
index 0000000..32377ad
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/MarkdownBlocks.qml
@@ -0,0 +1,91 @@
+import QtQuick
+import Quickshell
+import ZShell.Config
+import ZShell.Llm
+import qs.Components
+import qs.Services
+
+Column {
+ id: root
+
+ required property var blocks
+
+ spacing: Tokens.spacing.small
+
+ Repeater {
+ id: blockRep
+
+ model: ScriptModel {
+ values: root.blocks
+ objectProp: "id"
+ }
+ delegate: DelegateChooser {
+ role: "type"
+
+ DelegateChoice {
+ roleValue: LlmMarkdown.Type.Code
+
+ delegate: CodeBlockView {
+ required property var modelData
+
+ language: modelData.language
+ code: modelData.code
+ anchors.right: parent.right
+ anchors.margins: Tokens.spacing.small
+ anchors.left: parent.left
+ }
+ }
+
+ DelegateChoice {
+ roleValue: LlmMarkdown.Type.Math
+
+ delegate: MathBlockView {
+ required property var modelData
+
+ latex: modelData.latex
+ anchors.right: parent.right
+ anchors.left: parent.left
+ anchors.margins: Tokens.padding.medium
+ }
+ }
+
+ DelegateChoice {
+ roleValue: LlmMarkdown.Type.Heading
+
+ delegate: TextEditBase {
+ required property var modelData
+
+ color: Colors.palette.m3onSurface
+ anchors.right: parent.right
+ anchors.left: parent.left
+ anchors.margins: Tokens.padding.medium
+ text: modelData.text
+ textFormat: Text.MarkdownText
+ readOnly: true
+ font.bold: true
+ font.pointSize: {
+ if (modelData.level <= 1)
+ return Tokens.font.size.large;
+ if (modelData.level === 2)
+ return Tokens.font.size.normal;
+ return Tokens.font.size.smaller;
+ }
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ }
+ }
+
+ DelegateChoice {
+ roleValue: LlmMarkdown.Type.Text
+
+ delegate: BubbleEdit {
+ required property var modelData
+
+ anchors.right: parent.right
+ anchors.left: parent.left
+ anchors.margins: Tokens.padding.medium
+ text: modelData.text
+ }
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/MathBlockView.qml b/Modules/Notifications/Sidebar/Chat/Content/MathBlockView.qml
new file mode 100644
index 0000000..0a8727d
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/MathBlockView.qml
@@ -0,0 +1,49 @@
+import QtQuick
+import ZShell.Config
+import ZShell.Llm
+import qs.Components
+import qs.Services
+
+Item {
+ id: root
+
+ required property string latex
+ property color mathColor: Colors.palette.m3onSurface
+ property real fontSize: Tokens.font.size.large
+
+ implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
+
+ LlmMathText {
+ id: math
+
+ latex: root.latex
+ color: root.mathColor
+ fontPointSize: root.fontSize
+ devicePixelRatio: Screen.devicePixelRatio
+ }
+
+ Image {
+ id: equationImage
+
+ anchors.centerIn: parent
+ source: math.imageUrl
+ asynchronous: true
+ visible: math.ok
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ width: Math.min(math.width, root.width)
+ height: math.width > 0 ? width * (math.height / math.width) : 0
+ }
+
+ CustomText {
+ id: fallbackText
+
+ anchors.centerIn: parent
+ color: root.mathColor
+ font.family: Config.appearance.font.family.mono
+ font.pointSize: Tokens.font.size.small
+ text: root.latex
+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+ visible: !math.ok
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/MessageDelegate.qml b/Modules/Notifications/Sidebar/Chat/Content/MessageDelegate.qml
new file mode 100644
index 0000000..7f267b1
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/MessageDelegate.qml
@@ -0,0 +1,210 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import ZShell.Config
+import ZShell.Llm
+import qs.Components
+import qs.Services
+
+MouseArea {
+ id: root
+
+ property ChatGeneration current: modelData.activeGeneration
+ required property int index
+ readonly property bool isUser: modelData.role === ChatMessage.Role.User
+ required property ChatMessage modelData
+ property bool reasoningExpanded: false
+ readonly property var blocks: blockify(current.segments)
+ property real savedOffset: -1
+ property bool stickActive: false
+
+ function handleReasoningToggle(expanded: bool): void {
+ const view = root.ListView.view;
+ if (!view)
+ return;
+
+ if (expanded) {
+ root.savedOffset = view.contentY - root.y;
+ root.reasoningExpanded = true;
+ } else {
+ root.reasoningExpanded = false;
+ root.stickActive = false;
+ settleTimer.stop();
+ if (!isNaN(root.savedOffset)) {
+ restoreAnim.to = root.y + root.savedOffset;
+ restoreAnim.restart();
+ root.savedOffset = NaN;
+ }
+ }
+ }
+
+ function blockify(segs) {
+ const blocks = [];
+ for (let i = 0; i < segs.length; i++) {
+ const seg = segs[i];
+ if (seg.type === LlmSegment.Type.Content) {
+ blocks.push({
+ kind: "content",
+ id: i,
+ segments: [seg]
+ });
+ } else {
+ const last = blocks.length ? blocks[blocks.length - 1] : null;
+ if (last && last.kind === "process") {
+ last.segments.push(seg);
+ } else {
+ blocks.push({
+ kind: "process",
+ id: i,
+ segments: [seg]
+ });
+ }
+ }
+ }
+
+ return blocks;
+ }
+
+ implicitWidth: ListView.view.width
+ hoverEnabled: true
+ preventStealing: false
+ implicitHeight: layout.implicitHeight + Tokens.padding.medium
+
+ Timer {
+ id: settleTimer
+ interval: 16
+
+ onTriggered: {
+ root.stickActive = false;
+ }
+ }
+
+ onImplicitHeightChanged: {
+ if (root.reasoningExpanded) {
+ root.stickActive = true;
+ settleTimer.restart();
+
+ const view = root.ListView.view;
+ if (!view)
+ return;
+ const bottomEdge = root.y + root.height + Tokens.padding.large * 2;
+ const viewportBottom = view.contentY + view.height;
+ if (bottomEdge > viewportBottom)
+ view.contentY = bottomEdge - view.height;
+ }
+ }
+
+ Behavior on current {
+ id: changeAnim
+
+ property var object: targetProperty
+ readonly property bool next: {
+ const cond = root.modelData.generations.indexOf(current) < root.modelData.generations.indexOf(changeAnim.targetValue);
+ return cond;
+ }
+
+ animation: SequentialAnimation {
+ ParallelAnimation {
+ Anim {
+ target: root
+ property: "x"
+ to: changeAnim.next ? root.width / 4 : -root.width / 4
+ from: 0
+ type: Anim.FastEffects
+ }
+
+ Anim {
+ target: root
+ property: "opacity"
+ from: 1
+ to: 0
+ type: Anim.FastEffects
+ }
+ }
+
+ PropertyAction {}
+
+ ParallelAnimation {
+ Anim {
+ target: root
+ property: "x"
+ from: changeAnim.next ? -root.width / 4 : root.width / 4
+ type: Anim.FastEffects
+ to: 0
+ }
+
+ Anim {
+ target: root
+ property: "opacity"
+ from: 0
+ type: Anim.FastEffects
+ to: 1
+ }
+ }
+ }
+ }
+
+ Anim {
+ id: restoreAnim
+
+ property: "contentY"
+ target: root.ListView.view
+ type: Anim.DefaultEffects
+ }
+
+ Column {
+ id: layout
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ Repeater {
+ id: segmentRep
+
+ model: ScriptModel {
+ values: root.blocks
+ objectProp: "id"
+ }
+ delegate: DelegateChooser {
+ role: "kind"
+
+ DelegateChoice {
+ roleValue: "process"
+
+ delegate: ProcessBlock {
+ width: root.width
+ blocks: root.blocks
+
+ onExpandedChanged: root.handleReasoningToggle(expanded)
+ }
+ }
+
+ DelegateChoice {
+ roleValue: "content"
+
+ delegate: ContentBubble {
+ required property var modelData
+
+ isUser: root.isUser
+ segment: modelData.segments[0]
+ width: root.width
+ repeater: segmentRep
+ message: root.modelData
+ current: root.current
+ hovered: root.containsMouse
+
+ onEdit: text => {
+ root.modelData.edit(text);
+
+ if (root.isUser)
+ root.modelData.generate();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Content/ProcessBlock.qml b/Modules/Notifications/Sidebar/Chat/Content/ProcessBlock.qml
new file mode 100644
index 0000000..c681bfb
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Content/ProcessBlock.qml
@@ -0,0 +1,163 @@
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Config
+import ZShell.Llm
+import qs.Components
+import qs.Services
+
+CustomClippingRect {
+ id: root
+
+ required property int index
+ required property var modelData
+ required property var blocks
+ readonly property var segments: modelData.segments
+ readonly property bool isActive: index === blocks.length - 1
+ property bool expanded: false
+ readonly property LlmSegment lastSegment: root.segments[root.segments.length - 1]
+ readonly property real totalElapsedMs: root.segments.reduce((sum, s) => sum + (s.elapsedMs ?? 0), 0)
+
+ implicitHeight: expanded ? expandedRect.implicitHeight + layout.implicitHeight + expandedRect.anchors.topMargin * 2 : layout.implicitHeight + Tokens.spacing.small
+
+ Behavior on implicitHeight {
+ Anim {
+ type: Anim.DefaultEffects
+ }
+ }
+
+ RowLayout {
+ id: layout
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ Item {
+ implicitHeight: expandBtn.implicitHeight
+ implicitWidth: expandBtn.implicitWidth
+
+ LoadingIndicator {
+ id: spinnerReasoning
+
+ implicitSize: collapsedText.implicitHeight
+ anchors.centerIn: parent
+ opacity: root.isActive ? 1 : 0
+ visible: opacity > 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+
+ IconButton {
+ id: expandBtn
+
+ font.pointSize: Tokens.font.size.large
+ icon: "keyboard_arrow_down"
+ inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
+ anchors.centerIn: parent
+ opacity: root.isActive ? 0 : 1
+ rotation: root.expanded ? 180 : 0
+ type: IconButton.Text
+
+ Behavior on rotation {
+ Anim {}
+ }
+
+ onClicked: root.expanded = !root.expanded
+ }
+ }
+
+ CustomText {
+ id: collapsedText
+
+ color: Colors.palette.m3outline
+ font.pointSize: Tokens.font.size.small
+ Layout.fillWidth: true
+ Layout.leftMargin: Tokens.spacing.medium
+ text: {
+ if (root.isActive) {
+ if (root.lastSegment.type === LlmSegment.Type.ToolCall)
+ return qsTr("Using %1...").arg(root.lastSegment.name);
+ return qsTr("Thinking...");
+ }
+ return qsTr("Worked for %1s").arg((root.totalElapsedMs / 1000).toFixed(1));
+ }
+
+ Behavior on opacity {
+ Anim {}
+ }
+ }
+ }
+
+ CustomRect {
+ id: expandedRect
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: layout.bottom
+ anchors.bottom: parent.bottom
+ anchors.topMargin: Tokens.spacing.small
+ anchors.bottomMargin: Tokens.spacing.small
+ color: Colors.palette.m3surfaceContainerLow
+ implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
+ opacity: root.expanded ? 1 : 0
+ radius: Tokens.rounding.medium
+ visible: opacity > 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ ListView {
+ id: expandedContent
+
+ anchors.fill: parent
+ anchors.margins: Tokens.padding.medium
+ model: root.segments
+ spacing: Tokens.spacing.medium
+
+ delegate: Item {
+ id: segment
+
+ required property LlmSegment modelData
+ required property int index
+
+ implicitWidth: ListView.view.width
+ implicitHeight: header.implicitHeight + content.implicitHeight + content.anchors.topMargin
+
+ RowLayout {
+ id: header
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: Tokens.spacing.small
+
+ MaterialIcon {
+ text: segment.modelData.type === LlmSegment.Type.Reasoning ? "cognition" : "build"
+ fill: 1
+ }
+
+ CustomText {
+ Layout.fillWidth: true
+ text: segment.modelData.type === LlmSegment.Type.Reasoning ? qsTr("Thought for %1s").arg((segment.modelData.elapsedMs / 1000).toFixed(1)) : qsTr("Used %1 for %2s").arg(segment.modelData.name).arg((segment.modelData.elapsedMs / 1000).toFixed(1))
+ }
+ }
+
+ CustomText {
+ id: content
+
+ anchors.left: parent.left
+ anchors.leftMargin: Tokens.padding.small
+ anchors.top: header.bottom
+ anchors.topMargin: Tokens.spacing.small
+ anchors.right: parent.right
+ color: Colors.palette.m3outline
+ font.pointSize: Tokens.font.size.small
+ wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
+ text: segment.modelData.type === LlmSegment.Type.Reasoning ? segment.modelData.text : qsTr("Fetched %1").arg(JSON.parse(segment.modelData.arguments)?.url ?? "website")
+ }
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/Detach.qml b/Modules/Notifications/Sidebar/Chat/Detach.qml
new file mode 100644
index 0000000..c0ab82d
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/Detach.qml
@@ -0,0 +1,53 @@
+pragma Singleton
+
+import Quickshell
+import QtQuick
+import ZShell.Config
+import qs.Helpers
+import qs.Services
+
+Singleton {
+ id: root
+
+ function create(parent: Item, props: var): void {
+ chatComp.createObject(parent ?? dummy, props);
+ Visibilities.getForActive().sidebar = false;
+ ChatState.isWindow = true;
+ }
+
+ QtObject {
+ id: dummy
+ }
+
+ Component {
+ id: chatComp
+
+ FloatingWindow {
+ id: win
+
+ property var props
+
+ color: Colors.tPalette.m3surface
+ implicitHeight: chat.implicitHeight
+ implicitWidth: chat.implicitWidth
+ minimumSize.height: Config.sidebar.sizes.width
+ minimumSize.width: Config.sidebar.sizes.width
+ surfaceFormat.opaque: false
+ title: qsTr("ZShell - %1").arg(ChatState.title)
+
+ Component.onCompleted: ChatState.screen = screen
+ onVisibleChanged: {
+ if (!visible) {
+ destroy();
+ ChatState.isWindow = false;
+ }
+ }
+
+ SidebarView {
+ id: chat
+
+ anchors.fill: parent
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Chat/SidebarView.qml b/Modules/Notifications/Sidebar/Chat/SidebarView.qml
new file mode 100644
index 0000000..1ffdb0c
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Chat/SidebarView.qml
@@ -0,0 +1,110 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import ZShell.Config
+import ZShell.Llm
+import qs.Components
+import qs.Modules.Notifications.Sidebar.Chat.Content
+import qs.Services
+
+RowLayout {
+ id: root
+
+ property int breakpoint: 700
+ property alias conversationModel: sidebar.model
+
+ readonly property bool isWide: root.width >= root.breakpoint
+ property bool narrowShowsSidebar: true
+
+ function openConversation(conv: ChatSession, index: int): void {
+ }
+
+ ChatList {
+ id: sidebar
+
+ Layout.fillHeight: true
+ Layout.margins: Tokens.padding.medium
+ Layout.rightMargin: Tokens.spacing.medium
+ Layout.maximumWidth: Config.sidebar.sizes.width
+ Layout.preferredWidth: root.width / 4
+ Layout.minimumWidth: Config.sidebar.sizes.width / 2
+ highlight: true
+
+ Behavior on implicitWidth {
+ Anim {}
+ }
+ model: ScriptModel {
+ values: Chat.chats.values
+ }
+
+ onLoadChatRequest: (chat, index) => {
+ ChatState.currentIdx = index;
+ ChatState.chatSession = chat;
+ }
+
+ onDeleteChatRequest: chat => {
+ Chat.chats.remove(chat);
+ ChatState.currentIdx = -1;
+ ChatState.chatSession = null;
+ }
+ onNewChatRequest: {
+ const data = Chat.chats.insert();
+ ChatState.currentIdx = 0;
+ ChatState.chatSession = data;
+ }
+ }
+
+ Item {
+ Layout.margins: Tokens.padding.extraLarge
+ Layout.leftMargin: Tokens.spacing.extraLarge
+ Layout.topMargin: Tokens.padding.large
+ Layout.preferredWidth: Config.sidebar.sizes.width * 2
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ ChatHost {
+ id: convHost
+
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ implicitWidth: Math.min(parent.width, 800)
+ clip: true
+
+ onImplicitWidthChanged: console.log(implicitWidth)
+ }
+
+ ColumnLayout {
+ id: noChatLayout
+ anchors.centerIn: parent
+ opacity: ChatState.chatSession ? 0 : 1
+ visible: opacity > 0
+
+ Behavior on opacity {
+ Anim {}
+ }
+
+ MaterialIcon {
+ Layout.alignment: Qt.AlignCenter
+ text: "chat_bubble_off"
+ font.pointSize: Tokens.font.size.extraLarge
+ color: Colors.tPalette.m3onSurfaceVariant
+ }
+
+ CustomText {
+ Layout.alignment: Qt.AlignCenter
+ text: qsTr("Start or create a chat")
+ color: Colors.tPalette.m3onSurfaceVariant
+ }
+ }
+ }
+
+ Component {
+ id: conversationView
+
+ ChatContent {}
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Content.qml b/Modules/Notifications/Sidebar/Content.qml
index 06f0c5d..4b76e12 100644
--- a/Modules/Notifications/Sidebar/Content.qml
+++ b/Modules/Notifications/Sidebar/Content.qml
@@ -1,7 +1,11 @@
-import qs.Components
+pragma ComponentBehavior: Bound
+
import ZShell.Config
+import ZShell.Llm
import QtQuick
import QtQuick.Layouts
+import qs.Components
+import qs.Modules.Notifications.Sidebar.Chat
import qs.Services
Item {
@@ -10,21 +14,94 @@ Item {
required property Props props
required property var visibilities
+ Connections {
+ function onIsWindowChanged(): void {
+ if (ChatState.isWindow)
+ root.props.currentTab = 0;
+ }
+
+ target: ChatState
+ }
+
ColumnLayout {
id: layout
anchors.fill: parent
spacing: Tokens.spacing.small
- CustomRect {
+ Tabs {
+ Layout.fillWidth: true
+ Layout.preferredHeight: ChatState.isWindow ? 0 : implicitHeight
+ dashState: root.props
+ nonAnimWidth: layout.width
+ visible: height > 0
+ }
+
+ CustomClippingRect {
Layout.fillHeight: true
Layout.fillWidth: true
- color: Colors.tPalette.m3surfaceContainerLow
radius: Tokens.rounding.small
- NotifDock {
- props: root.props
- visibilities: root.visibilities
+ Item {
+ id: pages
+
+ anchors.bottom: parent.bottom
+ anchors.top: parent.top
+ implicitWidth: parent.width
+ opacity: root.props.currentTab === 0 ? 1 : 0
+ visible: opacity > 0
+ x: root.props.currentTab === 0 ? 0 : -root.width
+
+ Behavior on opacity {
+ Anim {
+ }
+ }
+ Behavior on x {
+ Anim {
+ }
+ }
+
+ CustomRect {
+ anchors.fill: parent
+ color: Colors.tPalette.m3surfaceContainerLow
+ radius: Tokens.rounding.small
+
+ NotifDock {
+ props: root.props
+ visibilities: root.visibilities
+ }
+ }
+ }
+
+ Item {
+ id: chatPage
+
+ anchors.bottom: parent.bottom
+ anchors.top: parent.top
+ implicitWidth: parent.width
+ opacity: root.props.currentTab === 1 ? 1 : 0
+ visible: opacity > 0
+ x: root.props.currentTab === 0 ? root.width : 0
+ z: 1
+
+ Behavior on opacity {
+ Anim {
+ }
+ }
+ Behavior on x {
+ Anim {
+ }
+ }
+
+ CustomRect {
+ anchors.fill: parent
+ color: Colors.tPalette.m3surfaceContainerLow
+ radius: Tokens.rounding.small
+
+ ChatPanel {
+ anchors.fill: parent
+ }
+ }
}
}
diff --git a/Modules/Notifications/Sidebar/Props.qml b/Modules/Notifications/Sidebar/Props.qml
index 21a2541..dcb4eb0 100644
--- a/Modules/Notifications/Sidebar/Props.qml
+++ b/Modules/Notifications/Sidebar/Props.qml
@@ -1,6 +1,8 @@
import Quickshell
+import ZShell.Llm
PersistentProperties {
+ property int currentTab: 0
property list expandedNotifs: []
reloadableId: "sidebar"
diff --git a/Modules/Notifications/Sidebar/Tabs.qml b/Modules/Notifications/Sidebar/Tabs.qml
new file mode 100644
index 0000000..2a9bfb8
--- /dev/null
+++ b/Modules/Notifications/Sidebar/Tabs.qml
@@ -0,0 +1,142 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Templates
+import Quickshell
+import ZShell.Config
+import qs.Components
+import qs.Services
+
+Item {
+ id: root
+
+ readonly property alias count: bar.count
+ required property PersistentProperties dashState
+ required property real nonAnimWidth
+
+ implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
+
+ TabBar {
+ id: bar
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ background: null
+ currentIndex: root.dashState.currentTab
+ implicitHeight: contentHeight
+
+ contentItem: RowLayout {
+ spacing: 0
+
+ Repeater {
+ model: bar.contentModel
+ }
+ }
+
+ onCurrentIndexChanged: root.state.currentTab = currentIndex
+
+ Tab {
+ iconName: "notifications"
+ text: qsTr("Notifications")
+ }
+
+ Tab {
+ iconName: "chat"
+ text: qsTr("Chat")
+ }
+ }
+
+ Item {
+ id: indicator
+
+ anchors.top: bar.bottom
+ clip: true
+ implicitHeight: 3
+ implicitWidth: bar.currentItem.implicitWidth
+ x: {
+ const tab = bar.currentItem;
+ const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
+ return width * tab.TabBar.index + (width - tab.implicitWidth) / 2;
+ }
+
+ Behavior on implicitWidth {
+ Anim {
+ }
+ }
+ Behavior on x {
+ Anim {
+ }
+ }
+
+ CustomRect {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ color: Colors.palette.m3primary
+ implicitHeight: parent.implicitHeight * 2
+ radius: Tokens.rounding.full
+ }
+ }
+
+ CustomRect {
+ id: separator
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: indicator.bottom
+ color: Colors.palette.m3outlineVariant
+ implicitHeight: 1
+ }
+
+ component Tab: TabButton {
+ id: tab
+
+ readonly property bool current: TabBar.tabBar.currentItem === this
+ required property string iconName
+
+ Layout.fillWidth: true
+ Layout.preferredWidth: 1
+ background: null
+ implicitHeight: implicitContentHeight
+ implicitWidth: implicitContentWidth
+
+ contentItem: Item {
+ implicitHeight: icon.height + label.height
+ implicitWidth: Math.max(icon.width, label.width)
+
+ StateLayer {
+ color: tab.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
+ radius: Tokens.rounding.small
+
+ onClicked: root.dashState.currentTab = tab.TabBar.index
+ }
+
+ MaterialIcon {
+ id: icon
+
+ anchors.bottom: label.top
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
+ fill: tab.current ? 1 : 0
+ font.pointSize: 18
+ text: tab.iconName
+
+ Behavior on fill {
+ Anim {
+ }
+ }
+ }
+
+ CustomText {
+ id: label
+
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
+ text: tab.text
+ }
+ }
+ }
+}
diff --git a/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml b/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml
index f32e6b1..4fbf307 100644
--- a/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml
+++ b/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml
@@ -12,13 +12,11 @@ import qs.Services
import qs.Helpers
import qs.Daemons
import qs.Modules.Settings
-import qs.Modules.Bar.Popouts as BarPopouts
CustomRect {
id: root
readonly property bool needExtraRow: quickToggles.length > 6
- required property BarPopouts.Wrapper popouts
readonly property var quickToggles: {
const seenIds = new Set();
diff --git a/Modules/Notifications/Sidebar/Utils/Content.qml b/Modules/Notifications/Sidebar/Utils/Content.qml
index 922eff9..1bdf03d 100644
--- a/Modules/Notifications/Sidebar/Utils/Content.qml
+++ b/Modules/Notifications/Sidebar/Utils/Content.qml
@@ -1,14 +1,12 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
-import qs.Modules.Bar.Popouts as BarPopouts
import qs.Modules.Notifications.Sidebar.Utils.Cards
import ZShell.Config
Item {
id: root
- required property BarPopouts.Wrapper popouts
required property PersistentProperties props
required property var visibilities
@@ -21,8 +19,7 @@ Item {
anchors.fill: parent
spacing: 8
- IdleInhibit {
- }
+ IdleInhibit {}
Record {
props: root.props
@@ -32,7 +29,6 @@ Item {
Toggles {
Layout.fillWidth: true
- popouts: root.popouts
visibilities: root.visibilities
}
}
diff --git a/Modules/Notifications/Sidebar/Utils/Wrapper.qml b/Modules/Notifications/Sidebar/Utils/Wrapper.qml
index 63a01b6..fa10251 100644
--- a/Modules/Notifications/Sidebar/Utils/Wrapper.qml
+++ b/Modules/Notifications/Sidebar/Utils/Wrapper.qml
@@ -4,13 +4,11 @@ import QtQuick
import Quickshell
import ZShell.Config
import qs.Components
-import qs.Modules.Bar.Popouts as BarPopouts
Item {
id: root
property real offsetScale: shouldBeActive ? 0 : 1
- required property BarPopouts.Wrapper popouts
readonly property PersistentProperties props: PersistentProperties {
property string recordingConfirmDelete
property bool recordingListExpanded: false
@@ -18,12 +16,12 @@ Item {
reloadableId: "utilities"
}
- readonly property bool shouldBeActive: visibilities.sidebar
+ readonly property bool shouldBeActive: visibilities.sidebar && !sidebar.chatActive
required property Item sidebar
required property var visibilities
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
- implicitHeight: content.implicitHeight + 8 * 2
+ implicitHeight: content.implicitHeight + Tokens.padding.small * 2
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
opacity: 1 - offsetScale
visible: offsetScale < 1
@@ -45,7 +43,6 @@ Item {
sourceComponent: Content {
implicitWidth: root.implicitWidth - 8 * 2
- popouts: root.popouts
props: root.props
visibilities: root.visibilities
}
diff --git a/Modules/Notifications/Sidebar/Wrapper.qml b/Modules/Notifications/Sidebar/Wrapper.qml
index 245247f..dc92aa7 100644
--- a/Modules/Notifications/Sidebar/Wrapper.qml
+++ b/Modules/Notifications/Sidebar/Wrapper.qml
@@ -1,7 +1,11 @@
pragma ComponentBehavior: Bound
import qs.Components
+import qs.Components.Toast
+import ZShell
import ZShell.Config
+import ZShell.Llm
+import Quickshell
import QtQuick
Item {
@@ -12,6 +16,7 @@ Item {
readonly property Props props: Props {
}
readonly property bool shouldBeActive: root.visibilities.sidebar && Config.sidebar.enabled
+ readonly property bool chatActive: props.currentTab === 1
required property var visibilities
anchors.rightMargin: (-implicitWidth - 5) * offsetScale
@@ -26,6 +31,17 @@ Item {
}
}
+ Connections {
+ target: Chat
+
+ function onErrorOccurred(message: string): void {
+ if (root.shouldBeActive && root.chatActive)
+ return;
+
+ Toaster.toast(qsTr("Chat"), message, "error_outline", Toast.Error);
+ }
+ }
+
Loader {
id: content
diff --git a/Modules/Settings/Common/DialogRowButton.qml b/Modules/Settings/Common/DialogRowButton.qml
index 1d4d547..805682c 100644
--- a/Modules/Settings/Common/DialogRowButton.qml
+++ b/Modules/Settings/Common/DialogRowButton.qml
@@ -15,13 +15,19 @@ Item {
required property Component content
required property string header
property int horizontalContentMargin
- required property string icon
+ property string icon
required property string label
+ property alias iconLabel: openButton.iconLabel
+ property alias rowButton: openButton
+ property string subtext: ""
property bool open
property real openHeight: Math.min(rootParent.height * 0.8, 600)
property real openWidth: Math.min(rootParent.width * 0.8, 400)
required property Item rootParent
property bool separateContent
+ required property string settingAnchor
+ property bool first: false
+ property bool last: false
signal accepted
signal cancelled
@@ -44,8 +50,7 @@ Item {
color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer
Behavior on color {
- CAnim {
- }
+ CAnim {}
}
}
@@ -55,6 +60,7 @@ Item {
anchors.fill: parent
enabled: false
hoverEnabled: enabled
+ preventStealing: true
parent: root.open ? root.rootParent : root
onClicked: root.open = false
@@ -75,7 +81,8 @@ Item {
backdrop.enabled: true
dialogBg.bottomLeftRadius: Tokens.rounding.largeIncreased
dialogBg.bottomRightRadius: Tokens.rounding.largeIncreased
- dialogBg.radius: Tokens.rounding.largeIncreased
+ dialogBg.topRightRadius: Tokens.rounding.largeIncreased
+ dialogBg.topLeftRadius: Tokens.rounding.largeIncreased
dialogContent.opacity: 1
dialogWrapper.height: root.openHeight
dialogWrapper.width: root.openWidth
@@ -103,7 +110,7 @@ Item {
}
Anim {
- properties: "opacity,radius,bottomLeftRadius,bottomRightRadius"
+ properties: "opacity,topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius"
type: Anim.DefaultEffects
}
@@ -131,12 +138,13 @@ Item {
id: dialogBg
anchors.fill: parent
- bottomLeftRadius: Tokens.rounding.largeIncreased
- bottomRightRadius: Tokens.rounding.largeIncreased
+ bottomLeftRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+ bottomRightRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+ topRightRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+ topLeftRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
- radius: Tokens.rounding.extraSmall
}
RowButton {
@@ -145,9 +153,11 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
color: "transparent"
- height: Math.min(implicitHeight, parent.height) // Clamp to parent height due to overshoot anim
+ height: Math.min(implicitHeight, parent.height)
icon: root.icon
- last: true
+ subtext: root.subtext
+ last: root.last
+ first: root.first
text: root.label
transform: Matrix4x4 {
@@ -167,6 +177,7 @@ Item {
sourceComponent: MouseArea {
onWheel: event => event.accepted = true
+ preventStealing: true
ColumnLayout {
anchors.bottomMargin: Tokens.padding.largeIncreased
@@ -219,6 +230,7 @@ Item {
text: qsTr("Cancel")
type: TextButton.Text
verticalPadding: Tokens.padding.small
+ stateLayer.preventStealing: true
onClicked: {
root.cancelled();
@@ -232,6 +244,7 @@ Item {
isRound: true
text: root.acceptLabel
type: TextButton.Text
+ stateLayer.preventStealing: true
verticalPadding: Tokens.padding.small
onClicked: {
diff --git a/Modules/Settings/Common/DialogSelectButton.qml b/Modules/Settings/Common/DialogSelectButton.qml
index 3cce148..d8cb270 100644
--- a/Modules/Settings/Common/DialogSelectButton.qml
+++ b/Modules/Settings/Common/DialogSelectButton.qml
@@ -10,6 +10,7 @@ DialogRowButton {
required property var model
property var selectedItem
+ property var initialSelect
function keyFor(item: var): string {
return item.id;
@@ -91,7 +92,14 @@ DialogRowButton {
}
onOpenChanged: {
- if (open)
- selectedItem = null;
+ if (open) {
+ root.rootParent.interactive = false;
+ if (!initialSelect)
+ selectedItem = null;
+ else
+ selectedItem = initialSelect;
+ } else {
+ root.rootParent.interactive = true;
+ }
}
}
diff --git a/Modules/Settings/Common/RowButton.qml b/Modules/Settings/Common/RowButton.qml
index 5d2953d..50d1c3d 100644
--- a/Modules/Settings/Common/RowButton.qml
+++ b/Modules/Settings/Common/RowButton.qml
@@ -17,6 +17,7 @@ ConnectedRect {
property alias subtext: subLabel.text
property alias text: label.text
property string trailingIcon
+ property string activeItem
signal clicked(event: MouseEvent)
@@ -40,15 +41,16 @@ ConnectedRect {
spacing: Tokens.spacing.medium
Behavior on opacity {
- Anim {
- }
+ Anim {}
}
MaterialIcon {
id: iconLabel
color: Colors.palette.m3onSurfaceVariant
+ visible: root.icon !== ""
fill: 1
+ text: root.icon
font.pointSize: Tokens.font.size.normal
}
@@ -79,6 +81,26 @@ ConnectedRect {
}
}
+ Loader {
+ active: root.activeItem && root.subtext
+ asynchronous: true
+ visible: active
+ Layout.fillHeight: true
+
+ sourceComponent: CustomRect {
+ color: Colors.palette.m3secondaryContainer
+ radius: Tokens.rounding.full
+ implicitWidth: text.implicitWidth + Tokens.padding.medium * 2
+
+ CustomText {
+ id: text
+ anchors.centerIn: parent
+ color: Colors.palette.m3onSecondaryContainer
+ text: root.activeItem
+ }
+ }
+ }
+
Loader {
active: root.trailingIcon
asynchronous: true
diff --git a/Modules/Settings/Common/SearchPopup.qml b/Modules/Settings/Common/SearchPopup.qml
new file mode 100644
index 0000000..193fd1a
--- /dev/null
+++ b/Modules/Settings/Common/SearchPopup.qml
@@ -0,0 +1,128 @@
+pragma ComponentBehavior: Bound
+
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Config
+import qs.Components
+import qs.Services
+
+DialogRowButton {
+ id: root
+
+ required property var model
+ property var selectedItem
+ property var initialSelect
+
+ acceptAllowed: !!selectedItem
+ horizontalContentMargin: -Tokens.padding.extraSmall
+ separateContent: true
+
+ content: Component {
+ ColumnLayout {
+ VerticalFadeListView {
+ bottomMargin: Tokens.padding.large
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ model: ScriptModel {
+ values: {
+ const s = searchField.text;
+ var regex = new RegExp(s, "i");
+
+ return root.model.filter(n => regex.test(n));
+ }
+ }
+ spacing: 0
+ topMargin: Tokens.padding.large
+
+ delegate: CustomRect {
+ id: item
+
+ required property var modelData
+ readonly property bool selected: root.selectedItem === modelData
+
+ anchors.left: ListView.view.contentItem.left
+ anchors.margins: 1 // Gets cut off for some reason without this
+ anchors.right: ListView.view.contentItem.right
+ color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
+ implicitHeight: label.implicitHeight + Tokens.padding.small * 2
+ radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
+
+ Behavior on radius {
+ Anim {
+ type: Anim.SlowEffects
+ }
+ }
+
+ StateLayer {
+ id: stateLayer
+
+ onClicked: root.selectedItem = item.modelData
+ }
+
+ CustomText {
+ id: label
+
+ anchors.left: parent.left
+ anchors.margins: Tokens.padding.large
+ anchors.right: item.selected ? checkIcon.left : parent.right
+ anchors.rightMargin: item.selected ? Tokens.spacing.medium : anchors.margins
+ anchors.verticalCenter: parent.verticalCenter
+ color: item.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
+ elide: Text.ElideRight
+ font.pointSize: Tokens.font.size.smaller
+ text: item.modelData
+ }
+
+ MaterialIcon {
+ id: checkIcon
+
+ anchors.margins: Tokens.padding.large
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ color: Colors.palette.m3onTertiaryContainer
+ font.pointSize: Tokens.font.size.normal
+ opacity: item.selected ? 1 : 0
+ text: "check"
+
+ Behavior on opacity {
+ Anim {
+ type: Anim.SlowEffects
+ }
+ }
+ }
+ }
+ }
+
+ SearchBar {
+ id: searchField
+
+ Layout.fillWidth: true
+ bg.border.color: Colors.palette.m3outlineVariant
+ bg.color: Colors.tPalette.m3surfaceContainerLowest
+ clearIcon.font.pointSize: Tokens.font.size.large
+ clearIcon.padding: Tokens.padding.extraSmall
+ font.pointSize: Tokens.font.size.small
+ placeholderText: qsTr("Search...")
+ searchIcon.anchors.leftMargin: Tokens.padding.large
+ searchIcon.font.pointSize: Tokens.font.size.large
+
+ Behavior on bg.border.color {
+ CAnim {}
+ }
+ }
+ }
+ }
+
+ onOpenChanged: {
+ if (open) {
+ root.rootParent.interactive = false;
+ if (!initialSelect)
+ selectedItem = null;
+ else
+ selectedItem = initialSelect;
+ } else {
+ root.rootParent.interactive = true;
+ }
+ }
+}
diff --git a/Modules/Settings/Common/TimeDialogRow.qml b/Modules/Settings/Common/TimeDialogRow.qml
new file mode 100644
index 0000000..79d6035
--- /dev/null
+++ b/Modules/Settings/Common/TimeDialogRow.qml
@@ -0,0 +1,246 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Blobs
+import qs.Components
+import ZShell.Config
+import qs.Services
+
+Item {
+ id: root
+
+ property bool acceptAllowed: true
+ required property string acceptLabel
+ required property Component content
+ required property string header
+ property int horizontalContentMargin
+ property string icon
+ required property string label
+ property alias iconLabel: openButton.iconText
+ property alias rowButton: openButton
+ property alias contentItem: dialogContent.item
+ property string subtext: ""
+ property bool open
+ property real openHeight: Math.min(rootParent.height * 0.8, 400)
+ property real openWidth: Math.min(rootParent.width * 0.8, 600)
+ required property Item rootParent
+ property bool first: false
+ property bool last: false
+ required property string settingAnchor
+ property color openColor: Colors.palette.m3surfaceContainerHighest
+
+ signal accepted
+ signal cancelled
+
+ function reparentWrapper(): void {
+ const newParent = open ? rootParent : root;
+ const pos = dialogWrapper.mapToItem(newParent, 0, 0);
+ dialogWrapper.parent = newParent;
+ dialogWrapper.x = pos.x;
+ dialogWrapper.y = pos.y;
+ }
+
+ Layout.fillWidth: true
+ implicitHeight: openButton.implicitHeight
+ z: open || dialogTransition.running ? 2 : 0
+
+ BlobGroup {
+ id: blobGroup
+
+ color: root.open ? root.openColor : Colors.tPalette.m3surfaceContainer
+
+ Behavior on color {
+ CAnim {}
+ }
+ }
+
+ MouseArea {
+ id: backdrop
+
+ anchors.fill: parent
+ enabled: false
+ hoverEnabled: enabled
+ preventStealing: true
+ parent: root.open ? root.rootParent : root
+
+ onClicked: root.open = false
+ }
+
+ Item {
+ id: dialogWrapper
+
+ height: openButton.implicitHeight
+ width: root.width
+ z: 1
+
+ states: State {
+ name: "open"
+ when: root.open
+
+ PropertyChanges {
+ backdrop.enabled: true
+ dialogBg.bottomLeftRadius: Tokens.rounding.largeIncreased
+ dialogBg.bottomRightRadius: Tokens.rounding.largeIncreased
+ dialogBg.topRightRadius: Tokens.rounding.largeIncreased
+ dialogBg.topLeftRadius: Tokens.rounding.largeIncreased
+ dialogContent.opacity: 1
+ dialogWrapper.height: root.openHeight
+ dialogWrapper.width: root.openWidth
+ dialogWrapper.x: (root.rootParent.width - root.openWidth) / 2
+ dialogWrapper.y: (root.rootParent.height - root.openHeight) / 2
+ elevation.opacity: 1
+ openButton.opacity: 0
+ }
+ }
+ transitions: Transition {
+ id: dialogTransition
+
+ SequentialAnimation {
+ ScriptAction {
+ script: root.reparentWrapper()
+ }
+
+ Anim {
+ properties: "x,y"
+ }
+ }
+
+ PropertyAction {
+ property: "enabled"
+ }
+
+ Anim {
+ properties: "opacity,topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius"
+ type: Anim.DefaultEffects
+ }
+
+ Anim {
+ properties: "width,height"
+ }
+ }
+
+ Elevation {
+ id: elevation
+
+ anchors.fill: parent
+ bottomLeftRadius: dialogBg.bottomLeftRadius
+ bottomRightRadius: dialogBg.bottomRightRadius
+ level: 4
+ opacity: 0
+ radius: dialogBg.radius
+
+ transform: Matrix4x4 {
+ matrix: dialogBg.deformMatrix
+ }
+ }
+
+ BlobRect {
+ id: dialogBg
+
+ anchors.fill: parent
+ bottomLeftRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+ bottomRightRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+ topRightRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+ topLeftRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+ deformScale: 0
+ group: blobGroup
+ opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
+ }
+
+ TimeDialogToggle {
+ id: openButton
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ color: "transparent"
+ height: Math.min(implicitHeight, parent.height)
+ subtext: root.subtext
+ last: root.last
+ first: root.first
+ text: root.label
+
+ transform: Matrix4x4 {
+ matrix: dialogBg.deformMatrix
+ }
+
+ onClicked: root.open = true
+ }
+
+ Loader {
+ id: dialogContent
+
+ active: opacity > 0
+ anchors.fill: parent
+ asynchronous: false
+ opacity: 0
+
+ sourceComponent: MouseArea {
+ onWheel: event => event.accepted = true
+ preventStealing: true
+ clip: true
+ implicitWidth: innerLayout.implicitWidth + innerLayout.anchors.margins * 2
+ implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins + innerLayout.anchors.bottomMargin
+
+ ColumnLayout {
+ id: innerLayout
+ anchors.bottomMargin: Tokens.padding.largeIncreased
+ anchors.fill: parent
+ anchors.margins: Tokens.padding.extraLarge
+ spacing: 0
+
+ CustomText {
+ font.pointSize: Tokens.font.size.large
+ text: root.header
+ }
+
+ Loader {
+ Layout.fillWidth: true
+ Layout.leftMargin: root.horizontalContentMargin
+ Layout.topMargin: Tokens.spacing.medium
+ Layout.bottomMargin: Tokens.spacing.medium
+ Layout.rightMargin: root.horizontalContentMargin
+ sourceComponent: root.content
+ }
+
+ RowLayout {
+ Layout.alignment: Qt.AlignRight
+ spacing: Tokens.spacing.extraSmall
+
+ TextButton {
+ horizontalPadding: Tokens.padding.largeIncreased
+ isRound: true
+ text: qsTr("Cancel")
+ type: TextButton.Text
+ verticalPadding: Tokens.padding.small
+ stateLayer.preventStealing: true
+
+ onClicked: {
+ root.cancelled();
+ root.open = false;
+ }
+ }
+
+ TextButton {
+ enabled: root.acceptAllowed
+ horizontalPadding: Tokens.padding.largeIncreased
+ isRound: true
+ text: root.acceptLabel
+ type: TextButton.Text
+ stateLayer.preventStealing: true
+ verticalPadding: Tokens.padding.small
+
+ onClicked: {
+ root.accepted();
+ root.open = false;
+ }
+ }
+ }
+ }
+ }
+ transform: Matrix4x4 {
+ matrix: dialogBg.deformMatrix
+ }
+ }
+ }
+}
diff --git a/Modules/Settings/Common/TimeDialogSelect.qml b/Modules/Settings/Common/TimeDialogSelect.qml
new file mode 100644
index 0000000..33967dc
--- /dev/null
+++ b/Modules/Settings/Common/TimeDialogSelect.qml
@@ -0,0 +1,50 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import ZShell.Config
+import qs.Helpers
+import qs.Components
+import qs.Services
+
+TimeDialogRow {
+ id: root
+
+ required property int start
+ required property int end
+ property int endCurrent
+ property int startCurrent
+ openColor: Colors.palette.m3surfaceContainer
+ openHeight: Math.min(rootParent.height * 0.8, contentItem?.implicitHeight ?? 0)
+ openWidth: Math.min(rootParent.width * 0.8, contentItem?.implicitWidth ?? 0)
+
+ function keyFor(item: var): string {
+ return item.id;
+ }
+
+ function labelFor(item: var): string {
+ return item.label;
+ }
+
+ horizontalContentMargin: -Tokens.padding.extraSmall
+
+ content: Component {
+ TimeInput {
+ id: timeContent
+ start: root.start
+ end: root.end
+
+ onEndEdit: value => root.endCurrent = value
+ onStartEdit: value => root.startCurrent = value
+ }
+ }
+
+ onOpenChanged: {
+ if (open) {
+ root.rootParent.interactive = false;
+ root.z = 5;
+ } else {
+ root.rootParent.interactive = true;
+ root.z = 1;
+ }
+ }
+}
diff --git a/Modules/Settings/Common/OverlayRow.qml b/Modules/Settings/Common/TimeDialogToggle.qml
similarity index 84%
rename from Modules/Settings/Common/OverlayRow.qml
rename to Modules/Settings/Common/TimeDialogToggle.qml
index f7a6dae..0cd1555 100644
--- a/Modules/Settings/Common/OverlayRow.qml
+++ b/Modules/Settings/Common/TimeDialogToggle.qml
@@ -10,12 +10,14 @@ ConnectedRect {
property alias checked: switchButton.checked
property int horizontalPadding: Tokens.padding.largeIncreased
- required property Component popup
property alias subtext: subtext.text
property alias text: text.text
+ property alias icon: icon
+ property string iconText: "open_in_new"
property int verticalPadding: Tokens.padding.small
- signal clicked(checked: bool)
+ signal clicked(event: MouseEvent)
+ signal toggled(checked: bool)
Layout.fillWidth: true
implicitHeight: layout.implicitHeight + verticalPadding * 2
@@ -50,11 +52,11 @@ ConnectedRect {
anchors.right: switchButton.left
anchors.rightMargin: Tokens.spacing.medium
anchors.verticalCenter: parent.verticalCenter
- text: "open_in_new"
+ text: root.iconText
}
StateLayer {
- onClicked: PopupManager.requestOpen(root.popup)
+ onClicked: e => root.clicked(e)
}
CustomSwitch {
@@ -64,6 +66,6 @@ ConnectedRect {
anchors.rightMargin: root.horizontalPadding
anchors.verticalCenter: parent.verticalCenter
- onClicked: root.clicked(checked)
+ onClicked: root.toggled(checked)
}
}
diff --git a/Modules/Settings/Common/TimeInput.qml b/Modules/Settings/Common/TimeInput.qml
index 05e8052..c8bfd2b 100644
--- a/Modules/Settings/Common/TimeInput.qml
+++ b/Modules/Settings/Common/TimeInput.qml
@@ -6,15 +6,13 @@ import qs.Components
import qs.Helpers
import qs.Services
-CustomClippingRect {
+ColumnLayout {
id: root
- required property var object
- required property list settings
- property bool shouldBeActive: true
-
- signal applySettings(startTime: int, endTime: int)
- signal close
+ required property int start
+ required property int end
+ signal endEdit(time: int)
+ signal startEdit(time: int)
function convertHour(timeValue: int): int {
return Math.floor(timeValue / 60);
@@ -28,555 +26,459 @@ CustomClippingRect {
return hour * 60 + minute;
}
- color: Colors.palette.m3surfaceContainer
- implicitHeight: column.implicitHeight + column.anchors.margins * 2 + buttonRow.implicitHeight + buttonRow.anchors.topMargin
- implicitWidth: column.implicitWidth + column.anchors.margins * 2
- radius: Tokens.rounding.largeIncreased
+ spacing: Tokens.spacing.medium
- ColumnLayout {
- id: column
+ RowLayout {
+ CustomRect {
+ id: startHourRect
- anchors.left: parent.left
- anchors.margins: Tokens.padding.largeIncreased
- anchors.right: parent.right
- anchors.top: parent.top
- spacing: Tokens.spacing.medium
+ Layout.preferredHeight: 72
+ Layout.preferredWidth: 96
+ color: startHourField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
+ implicitHeight: 72
+ implicitWidth: 96
+ radius: Tokens.rounding.small
+
+ CustomRect {
+ anchors.fill: parent
+ border.color: startHourField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
+ border.width: startHourField.focus ? 2 : 0
+ radius: parent.radius - border.width
+
+ Behavior on border.width {
+ Anim {}
+ }
+ }
+
+ TextFieldBase {
+ id: startHourField
+
+ function setConfigText(): string {
+ var val = root.convertHour(root.start);
+ if (val === 0) {
+ return "00";
+ }
+ return String(val);
+ }
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ clip: true
+ color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
+ font.family: "Roboto"
+ font.letterSpacing: -0.25
+ font.pixelSize: 56
+ font.weight: 400
+ horizontalAlignment: TextInput.AlignHCenter
+ text: setConfigText()
+ verticalAlignment: TextInput.AlignVCenter
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Backspace) {
+ event.accepted = true;
+ if (startHourField.text.length >= 2) {
+ startHourField.text = "0" + startHourField.text[0];
+ } else if (startHourField.text.length === 1) {
+ startHourField.text = "0";
+ }
+ return;
+ } else if (event.key === Qt.Key_Escape) {
+ event.accepted = true;
+ startHourField.text = setConfigText();
+ startHourField.focus = false;
+ } else if (event.key === Qt.Key_Return) {
+ startHourField.focus = false;
+ return;
+ } else if (event.key === Qt.Key_Tab) {
+ startMinuteField.focus = true;
+ } else if (event.key === Qt.Key_Backtab) {
+ endMinuteField.focus = true;
+ }
+
+ if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
+ event.accepted = true;
+ var digit = event.text;
+ var textLen = startHourField.text.length;
+
+ if (textLen >= 2 && startHourField.text[0] !== '0') {
+ return;
+ }
+
+ var val = 0;
+ if (textLen === 0) {
+ val = parseInt(digit);
+ } else if (textLen === 1) {
+ val = parseInt(startHourField.text + digit);
+ } else {
+ val = parseInt(startHourField.text[1] + digit);
+ }
+
+ val = Math.max(0, Math.min(23, val));
+
+ if (textLen >= 2 && val < 10) {
+ startHourField.text = "0" + val;
+ } else {
+ startHourField.text = val.toString();
+ }
+ }
+
+ event.accepted = true;
+ }
+ onCursorPositionChanged: cursorPosition = 2
+ onTextChanged: {
+ var mins = parseInt(startMinuteField.text);
+ const hours = root.convertToMinutes(parseInt(startHourField.text));
+ root.startEdit(hours + mins);
+ }
+ }
+ }
CustomText {
- text: qsTr("Select time")
+ id: startSeparator
+
+ font.pointSize: Tokens.font.size.extraLarge
+ text: ":"
}
- RowLayout {
- CustomRect {
- id: startHourRect
+ CustomRect {
+ id: startMinuteRect
- Layout.preferredHeight: 72
- Layout.preferredWidth: 96
- color: startHourField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
- implicitHeight: 72
- implicitWidth: 96
- radius: Tokens.rounding.small
-
- CustomRect {
- anchors.fill: parent
- border.color: startHourField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
- border.width: startHourField.focus ? 2 : 0
- radius: parent.radius - border.width
-
- Behavior on border.width {
- Anim {
- }
- }
- }
-
- TextFieldBase {
- id: startHourField
-
- function setConfigText(setting: string): string {
- var val = root.convertHour(root.object[setting]);
- if (val === 0) {
- return "00";
- }
- return String(val);
- }
-
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- clip: true
- color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
- font.family: "Roboto"
- font.letterSpacing: -0.25
- font.pixelSize: 56
- font.weight: 400
- horizontalAlignment: TextInput.AlignHCenter
- text: setConfigText(root.settings[1])
- verticalAlignment: TextInput.AlignVCenter
-
- Keys.onPressed: event => {
- if (event.key === Qt.Key_Backspace) {
- event.accepted = true;
- if (startHourField.text.length >= 2) {
- startHourField.text = "0" + startHourField.text[0];
- } else if (startHourField.text.length === 1) {
- startHourField.text = "0";
- }
- return;
- } else if (event.key === Qt.Key_Escape) {
- event.accepted = true;
- startHourField.text = setConfigText(root.settings[1]);
- startHourField.focus = false;
- } else if (event.key === Qt.Key_Return) {
- startHourField.focus = false;
- return;
- } else if (event.key === Qt.Key_Tab) {
- startMinuteField.focus = true;
- } else if (event.key === Qt.Key_Backtab) {
- endMinuteField.focus = true;
- }
-
- if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
- event.accepted = true;
- var digit = event.text;
- var textLen = startHourField.text.length;
-
- if (textLen >= 2 && startHourField.text[0] !== '0') {
- return;
- }
-
- var val = 0;
- if (textLen === 0) {
- val = parseInt(digit);
- } else if (textLen === 1) {
- val = parseInt(startHourField.text + digit);
- } else {
- val = parseInt(startHourField.text[1] + digit);
- }
-
- val = Math.max(0, Math.min(23, val));
-
- if (textLen >= 2 && val < 10) {
- startHourField.text = "0" + val;
- } else {
- startHourField.text = val.toString();
- }
- }
-
- event.accepted = true;
- }
- onCursorPositionChanged: cursorPosition = 2
- onTextEdited: {
- if (startHourField.text === "")
- return;
- var val = parseInt(startHourField.text);
- if (isNaN(val))
- return;
- val = Math.max(0, Math.min(23, val));
- var newText = val.toString();
- if (newText !== startHourField.text)
- startHourField.text = newText;
- }
- }
- }
-
- CustomText {
- id: startSeparator
-
- font.pointSize: Tokens.font.size.extraLarge
- text: ":"
- }
+ Layout.preferredHeight: 72
+ Layout.preferredWidth: 96
+ color: startMinuteField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
+ implicitHeight: 72
+ implicitWidth: 96
+ radius: Tokens.rounding.small
CustomRect {
- id: startMinuteRect
+ anchors.fill: parent
+ border.color: startMinuteField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
+ border.width: startMinuteField.focus ? 2 : 0
+ radius: parent.radius - border.width
- Layout.preferredHeight: 72
- Layout.preferredWidth: 96
- color: startMinuteField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
- implicitHeight: 72
- implicitWidth: 96
- radius: Tokens.rounding.small
+ Behavior on border.width {
+ Anim {}
+ }
+ }
- CustomRect {
- anchors.fill: parent
- border.color: startMinuteField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
- border.width: startMinuteField.focus ? 2 : 0
- radius: parent.radius - border.width
+ TextFieldBase {
+ id: startMinuteField
- Behavior on border.width {
- Anim {
- }
+ function setConfigText(): string {
+ var val = root.convertMinute(root.start);
+ if (val === 0) {
+ return "00";
}
+ return String(val);
}
- TextFieldBase {
- id: startMinuteField
-
- function setConfigText(setting: string): string {
- var val = root.convertMinute(root.object[setting]);
- if (val === 0) {
- return "00";
- }
- return String(val);
- }
-
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- clip: true
- color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
- font.family: "Roboto"
- font.letterSpacing: -0.25
- font.pixelSize: 56
- font.weight: 400
- horizontalAlignment: TextInput.AlignHCenter
- text: setConfigText(root.settings[1])
- verticalAlignment: TextInput.AlignVCenter
-
- Keys.onPressed: event => {
- if (event.key === Qt.Key_Backspace) {
- event.accepted = true;
- if (startMinuteField.text.length >= 2) {
- startMinuteField.text = "0" + startMinuteField.text[0];
- } else if (startMinuteField.text.length === 1) {
- startMinuteField.text = "0";
- }
- return;
- } else if (event.key === Qt.Key_Escape) {
- event.accepted = true;
- startMinuteField.text = setConfigText(root.settings[1]);
- startMinuteField.focus = false;
- } else if (event.key === Qt.Key_Return) {
- startMinuteField.focus = false;
- return;
- } else if (event.key === Qt.Key_Tab) {
- endHourField.focus = true;
- } else if (event.key === Qt.Key_Backtab) {
- startHourField.focus = true;
- }
-
- if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
- event.accepted = true;
- var digit = event.text;
- var textLen = startMinuteField.text.length;
-
- if (textLen >= 2 && startMinuteField.text[0] !== '0') {
- return;
- }
-
- var val = 0;
- if (textLen === 0) {
- val = parseInt(digit);
- } else if (textLen === 1) {
- val = parseInt(startMinuteField.text + digit);
- } else {
- val = parseInt(startMinuteField.text[1] + digit);
- }
-
- val = Math.max(0, Math.min(59, val));
-
- if (textLen >= 2 && val < 10) {
- startMinuteField.text = "0" + val;
- } else {
- startMinuteField.text = val.toString();
- }
- }
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ clip: true
+ color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
+ font.family: "Roboto"
+ font.letterSpacing: -0.25
+ font.pixelSize: 56
+ font.weight: 400
+ horizontalAlignment: TextInput.AlignHCenter
+ text: setConfigText()
+ verticalAlignment: TextInput.AlignVCenter
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Backspace) {
event.accepted = true;
- }
- onCursorPositionChanged: cursorPosition = 2
- onTextEdited: {
- if (startMinuteField.text === "")
- return;
- var val = parseInt(startMinuteField.text);
- if (isNaN(val))
- return;
- val = Math.max(0, Math.min(23, val));
- var newText = val.toString();
- if (newText !== startMinuteField.text)
- startMinuteField.text = newText;
- }
- }
- }
-
- Item {
- id: spacer
-
- Layout.preferredWidth: Tokens.spacing.largeIncreased
- }
-
- CustomRect {
- id: endHourRect
-
- Layout.preferredHeight: 72
- Layout.preferredWidth: 96
- color: endHourField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
- implicitHeight: 72
- implicitWidth: 96
- radius: Tokens.rounding.small
-
- CustomRect {
- anchors.fill: parent
- border.color: endHourField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
- border.width: endHourField.focus ? 2 : 0
- radius: parent.radius - border.width
-
- Behavior on border.width {
- Anim {
+ if (startMinuteField.text.length >= 2) {
+ startMinuteField.text = "0" + startMinuteField.text[0];
+ } else if (startMinuteField.text.length === 1) {
+ startMinuteField.text = "0";
}
- }
- }
-
- TextFieldBase {
- id: endHourField
-
- function setConfigText(setting: string): string {
- var val = root.convertHour(root.object[setting]);
- if (val === 0) {
- return "00";
- }
- return String(val);
- }
-
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- clip: true
- color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
- font.family: "Roboto"
- font.letterSpacing: -0.25
- font.pixelSize: 56
- font.weight: 400
- horizontalAlignment: TextInput.AlignHCenter
- text: setConfigText(root.settings[2])
- verticalAlignment: TextInput.AlignVCenter
-
- Keys.onPressed: event => {
- if (event.key === Qt.Key_Backspace) {
- event.accepted = true;
- if (endHourField.text.length >= 2) {
- endHourField.text = "0" + endHourField.text[0];
- } else if (endHourField.text.length === 1) {
- endHourField.text = "0";
- }
- return;
- } else if (event.key === Qt.Key_Escape) {
- event.accepted = true;
- endHourField.text = setConfigText(root.settings[2]);
- endHourField.focus = false;
- } else if (event.key === Qt.Key_Return) {
- endHourField.focus = false;
- return;
- } else if (event.key === Qt.Key_Tab) {
- endMinuteField.focus = true;
- } else if (event.key === Qt.Key_Backtab) {
- startMinuteField.focus = true;
- }
-
- if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
- event.accepted = true;
- var digit = event.text;
- var textLen = endHourField.text.length;
-
- if (textLen >= 2 && endHourField.text[0] !== '0') {
- return;
- }
-
- var val = 0;
- if (textLen === 0) {
- val = parseInt(digit);
- } else if (textLen === 1) {
- val = parseInt(endHourField.text + digit);
- } else {
- val = parseInt(endHourField.text[1] + digit);
- }
-
- val = Math.max(0, Math.min(23, val));
-
- if (textLen >= 2 && val < 10) {
- endHourField.text = "0" + val;
- } else {
- endHourField.text = val.toString();
- }
- }
-
+ return;
+ } else if (event.key === Qt.Key_Escape) {
event.accepted = true;
- }
- onCursorPositionChanged: cursorPosition = 2
- onTextEdited: {
- if (endHourField.text === "")
- return;
- var val = parseInt(endHourField.text);
- if (isNaN(val))
- return;
- val = Math.max(0, Math.min(23, val));
- var newText = val.toString();
- if (newText !== endHourField.text)
- endHourField.text = newText;
- }
- }
- }
-
- CustomText {
- id: endSeparator
-
- font.pointSize: Tokens.font.size.extraLarge
- text: ":"
- }
-
- CustomRect {
- id: endMinuteRect
-
- Layout.preferredHeight: 72
- Layout.preferredWidth: 96
- color: endMinuteField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
- implicitHeight: 72
- implicitWidth: 96
- radius: Tokens.rounding.small
-
- CustomRect {
- anchors.fill: parent
- border.color: endMinuteField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
- border.width: endMinuteField.focus ? 2 : 0
- radius: parent.radius - border.width
-
- Behavior on border.width {
- Anim {
- }
- }
- }
-
- TextFieldBase {
- id: endMinuteField
-
- function setConfigText(setting: string): string {
- var val = root.convertMinute(root.object[setting]);
- if (val === 0) {
- return "00";
- }
- return String(val);
+ startMinuteField.text = setConfigText();
+ startMinuteField.focus = false;
+ } else if (event.key === Qt.Key_Return) {
+ startMinuteField.focus = false;
+ return;
+ } else if (event.key === Qt.Key_Tab) {
+ endHourField.focus = true;
+ } else if (event.key === Qt.Key_Backtab) {
+ startHourField.focus = true;
}
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- clip: true
- color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
- font.family: "Roboto"
- font.letterSpacing: -0.25
- font.pixelSize: 56
- font.weight: 400
- horizontalAlignment: TextInput.AlignHCenter
- text: setConfigText(root.settings[2])
- verticalAlignment: TextInput.AlignVCenter
-
- Keys.onPressed: event => {
- if (event.key === Qt.Key_Backspace) {
- event.accepted = true;
- if (endMinuteField.text.length >= 2) {
- endMinuteField.text = "0" + endMinuteField.text[0];
- } else if (endMinuteField.text.length === 1) {
- endMinuteField.text = "0";
- }
- return;
- } else if (event.key === Qt.Key_Escape) {
- event.accepted = true;
- endMinuteField.text = setConfigText(root.settings[2]);
- endMinuteField.focus = false;
- } else if (event.key === Qt.Key_Return) {
- endMinuteField.focus = false;
- return;
- } else if (event.key === Qt.Key_Tab) {
- startHourField.focus = true;
- } else if (event.key === Qt.Key_Backtab) {
- endHourField.focus = true;
- }
-
- if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
- event.accepted = true;
- var digit = event.text;
- var textLen = endMinuteField.text.length;
-
- if (textLen >= 2 && endMinuteField.text[0] !== '0') {
- return;
- }
-
- var val = 0;
- if (textLen === 0) {
- val = parseInt(digit);
- } else if (textLen === 1) {
- val = parseInt(endMinuteField.text + digit);
- } else {
- val = parseInt(endMinuteField.text[1] + digit);
- }
-
- val = Math.max(0, Math.min(59, val));
-
- if (textLen >= 2 && val < 10) {
- endMinuteField.text = "0" + val;
- } else {
- endMinuteField.text = val.toString();
- }
- }
-
+ if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
event.accepted = true;
- }
- onCursorPositionChanged: cursorPosition = 2
- onTextEdited: {
- if (endMinuteField.text === "")
+ var digit = event.text;
+ var textLen = startMinuteField.text.length;
+
+ if (textLen >= 2 && startMinuteField.text[0] !== '0') {
return;
- var val = parseInt(endMinuteField.text);
- if (isNaN(val))
- return;
- val = Math.max(0, Math.min(23, val));
- var newText = val.toString();
- if (newText !== endMinuteField.text)
- endMinuteField.text = newText;
+ }
+
+ var val = 0;
+ if (textLen === 0) {
+ val = parseInt(digit);
+ } else if (textLen === 1) {
+ val = parseInt(startMinuteField.text + digit);
+ } else {
+ val = parseInt(startMinuteField.text[1] + digit);
+ }
+
+ val = Math.max(0, Math.min(59, val));
+
+ if (textLen >= 2 && val < 10) {
+ startMinuteField.text = "0" + val;
+ } else {
+ startMinuteField.text = val.toString();
+ }
}
+
+ event.accepted = true;
+ }
+ onCursorPositionChanged: cursorPosition = 2
+ onTextChanged: {
+ var mins = parseInt(startMinuteField.text);
+ const hours = root.convertToMinutes(parseInt(startHourField.text));
+ root.startEdit(hours + mins);
}
}
}
- RowLayout {
- CustomText {
- Layout.preferredWidth: spacer.x + spacer.width
- text: qsTr("Start")
+ Item {
+ id: spacer
+
+ Layout.preferredWidth: Tokens.spacing.largeIncreased
+ }
+
+ CustomRect {
+ id: endHourRect
+
+ Layout.preferredHeight: 72
+ Layout.preferredWidth: 96
+ color: endHourField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
+ implicitHeight: 72
+ implicitWidth: 96
+ radius: Tokens.rounding.small
+
+ CustomRect {
+ anchors.fill: parent
+ border.color: endHourField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
+ border.width: endHourField.focus ? 2 : 0
+ radius: parent.radius - border.width
+
+ Behavior on border.width {
+ Anim {}
+ }
}
- CustomText {
- Layout.preferredWidth: endMinuteRect.width + endHourRect.width
- text: qsTr("End")
+ TextFieldBase {
+ id: endHourField
+
+ function setConfigText(): string {
+ var val = root.convertHour(root.end);
+ if (val === 0) {
+ return "00";
+ }
+ return String(val);
+ }
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ clip: true
+ color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
+ font.family: "Roboto"
+ font.letterSpacing: -0.25
+ font.pixelSize: 56
+ font.weight: 400
+ horizontalAlignment: TextInput.AlignHCenter
+ text: setConfigText()
+ verticalAlignment: TextInput.AlignVCenter
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Backspace) {
+ event.accepted = true;
+ if (endHourField.text.length >= 2) {
+ endHourField.text = "0" + endHourField.text[0];
+ } else if (endHourField.text.length === 1) {
+ endHourField.text = "0";
+ }
+ return;
+ } else if (event.key === Qt.Key_Escape) {
+ event.accepted = true;
+ endHourField.text = setConfigText();
+ endHourField.focus = false;
+ } else if (event.key === Qt.Key_Return) {
+ endHourField.focus = false;
+ return;
+ } else if (event.key === Qt.Key_Tab) {
+ endMinuteField.focus = true;
+ } else if (event.key === Qt.Key_Backtab) {
+ startMinuteField.focus = true;
+ }
+
+ if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
+ event.accepted = true;
+ var digit = event.text;
+ var textLen = endHourField.text.length;
+
+ if (textLen >= 2 && endHourField.text[0] !== '0') {
+ return;
+ }
+
+ var val = 0;
+ if (textLen === 0) {
+ val = parseInt(digit);
+ } else if (textLen === 1) {
+ val = parseInt(endHourField.text + digit);
+ } else {
+ val = parseInt(endHourField.text[1] + digit);
+ }
+
+ val = Math.max(0, Math.min(23, val));
+
+ if (textLen >= 2 && val < 10) {
+ endHourField.text = "0" + val;
+ } else {
+ endHourField.text = val.toString();
+ }
+ }
+
+ event.accepted = true;
+ }
+ onCursorPositionChanged: cursorPosition = 2
+ onTextChanged: {
+ var mins = parseInt(endMinuteField.text);
+ const hours = root.convertToMinutes(parseInt(endHourField.text));
+ root.endEdit(hours + mins);
+ }
+ }
+ }
+
+ CustomText {
+ id: endSeparator
+
+ font.pointSize: Tokens.font.size.extraLarge
+ text: ":"
+ }
+
+ CustomRect {
+ id: endMinuteRect
+
+ Layout.preferredHeight: 72
+ Layout.preferredWidth: 96
+ color: endMinuteField.focus ? Colors.palette.m3onPrimaryContainer : Colors.palette.m3surfaceContainerHighest
+ implicitHeight: 72
+ implicitWidth: 96
+ radius: Tokens.rounding.small
+
+ CustomRect {
+ anchors.fill: parent
+ border.color: endMinuteField.focus ? Colors.palette.m3primaryContainer : Colors.palette.m3surfaceContainerHighest
+ border.width: endMinuteField.focus ? 2 : 0
+ radius: parent.radius - border.width
+
+ Behavior on border.width {
+ Anim {}
+ }
+ }
+
+ TextFieldBase {
+ id: endMinuteField
+
+ function setConfigText(): string {
+ var val = root.convertMinute(root.end);
+ if (val === 0) {
+ return "00";
+ }
+ return String(val);
+ }
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ clip: true
+ color: focus ? Colors.palette.m3primaryContainer : Colors.palette.m3onSurface
+ font.family: "Roboto"
+ font.letterSpacing: -0.25
+ font.pixelSize: 56
+ font.weight: 400
+ horizontalAlignment: TextInput.AlignHCenter
+ text: setConfigText()
+ verticalAlignment: TextInput.AlignVCenter
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Backspace) {
+ event.accepted = true;
+ if (endMinuteField.text.length >= 2) {
+ endMinuteField.text = "0" + endMinuteField.text[0];
+ } else if (endMinuteField.text.length === 1) {
+ endMinuteField.text = "0";
+ }
+ return;
+ } else if (event.key === Qt.Key_Escape) {
+ event.accepted = true;
+ endMinuteField.text = setConfigText();
+ endMinuteField.focus = false;
+ } else if (event.key === Qt.Key_Return) {
+ endMinuteField.focus = false;
+ return;
+ } else if (event.key === Qt.Key_Tab) {
+ startHourField.focus = true;
+ } else if (event.key === Qt.Key_Backtab) {
+ endHourField.focus = true;
+ }
+
+ if (event.text.length === 1 && event.text >= "0" && event.text <= "9") {
+ event.accepted = true;
+ var digit = event.text;
+ var textLen = endMinuteField.text.length;
+
+ if (textLen >= 2 && endMinuteField.text[0] !== '0') {
+ return;
+ }
+
+ var val = 0;
+ if (textLen === 0) {
+ val = parseInt(digit);
+ } else if (textLen === 1) {
+ val = parseInt(endMinuteField.text + digit);
+ } else {
+ val = parseInt(endMinuteField.text[1] + digit);
+ }
+
+ val = Math.max(0, Math.min(59, val));
+
+ if (textLen >= 2 && val < 10) {
+ endMinuteField.text = "0" + val;
+ } else {
+ endMinuteField.text = val.toString();
+ }
+ }
+
+ event.accepted = true;
+ }
+ onCursorPositionChanged: cursorPosition = 2
+ onTextChanged: {
+ var mins = parseInt(endMinuteField.text);
+ const hours = root.convertToMinutes(parseInt(endHourField.text));
+ root.endEdit(hours + mins);
+ }
}
}
}
RowLayout {
- id: buttonRow
-
- anchors.bottom: parent.bottom
- anchors.left: parent.left
- anchors.margins: Tokens.padding.largeIncreased
- anchors.right: parent.right
- anchors.top: column.bottom
- anchors.topMargin: Tokens.spacing.medium
-
- Item {
- id: buttonSpacer
-
- Layout.fillWidth: true
+ CustomText {
+ Layout.preferredWidth: spacer.x + spacer.width
+ text: qsTr("Start")
}
- ButtonRow {
- spacing: Tokens.spacing.medium
-
- IconTextButton {
- font.pointSize: Tokens.font.size.normal
- icon: "close"
- inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
- inactiveOnColor: Colors.palette.m3onSurfaceVariant
- isRound: true
- isToggle: false
- shapeMorph: true
- shapeMorphExpansion: pressed ? 12 : 0
- text: "Cancel"
-
- onClicked: root.close()
- }
-
- IconTextButton {
- font.pointSize: Tokens.font.size.normal
- icon: "check"
- isRound: true
- isToggle: false
- shapeMorph: true
- shapeMorphExpansion: pressed ? 12 : 0
- text: "Apply"
-
- onClicked: {
- const start = root.convertToMinutes(parseInt(startHourField.text)) + parseInt(startMinuteField.text);
- const end = root.convertToMinutes(parseInt(endHourField.text)) + parseInt(endMinuteField.text);
- root.applySettings(start, end);
- }
- }
+ CustomText {
+ Layout.preferredWidth: endMinuteRect.width + endHourRect.width
+ text: qsTr("End")
}
}
}
diff --git a/Modules/Settings/Common/ToggleRow.qml b/Modules/Settings/Common/ToggleRow.qml
index 09f22f5..36fb583 100644
--- a/Modules/Settings/Common/ToggleRow.qml
+++ b/Modules/Settings/Common/ToggleRow.qml
@@ -12,6 +12,7 @@ CustomSwitch {
property alias last: bg.last
property string settingAnchor
property string subtext
+ property string iconText
function flashHighlight(): void {
bg.flashHighlight();
@@ -63,8 +64,7 @@ CustomSwitch {
text: root.text
Behavior on opacity {
- Anim {
- }
+ Anim {}
}
}
@@ -81,10 +81,16 @@ CustomSwitch {
visible: root.subtext
Behavior on opacity {
- Anim {
- }
+ Anim {}
}
}
+
+ MaterialIcon {
+ id: icon
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.iconText ?? ""
+ }
}
}
diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml
index 66b616a..75a05c2 100644
--- a/Modules/Settings/Content.qml
+++ b/Modules/Settings/Content.qml
@@ -24,8 +24,7 @@ CustomClippingRect {
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
Behavior on blobColor {
- CAnim {
- }
+ CAnim {}
}
TapHandler {
@@ -70,8 +69,4 @@ CustomClippingRect {
anchors.top: parent.top
sState: root.sState
}
-
- PopupOverlay {
- anchors.fill: parent
- }
}
diff --git a/Modules/Settings/PageCompRegistry.qml b/Modules/Settings/PageCompRegistry.qml
index 22aa679..ee45aba 100644
--- a/Modules/Settings/PageCompRegistry.qml
+++ b/Modules/Settings/PageCompRegistry.qml
@@ -11,6 +11,7 @@ import qs.Modules.Settings.Pages.Audio
import qs.Modules.Settings.Pages.Apps
import qs.Modules.Settings.Pages.Panels
import qs.Modules.Settings.Pages.Panels.Bar
+import qs.Modules.Settings.Pages.Panels.Sidebar
import qs.Modules.Settings.Pages.Services
import qs.Services
@@ -23,13 +24,15 @@ QtObject {
// Wallpaper & style
StackPage {
Component {
- WallpaperPage {
- }
+ WallpaperPage {}
}
Component {
- WallpaperSelect {
- }
+ WallpaperSelect {}
+ }
+
+ Component {
+ ColorsFonts {}
}
}
},
@@ -37,32 +40,27 @@ QtObject {
// Screenshot
StackPage {
Component {
- ScreenshotPage {
- }
+ ScreenshotPage {}
}
}
},
// Connectivity
Component {
- PlaceholderComp {
- }
+ PlaceholderComp {}
},
Component {
- PlaceholderComp {
- }
+ PlaceholderComp {}
},
Component {
// Audio
StackPage {
Component {
- AudioPage {
- }
+ AudioPage {}
}
Component {
- AppVolumes {
- }
+ AppVolumes {}
}
}
},
@@ -71,49 +69,45 @@ QtObject {
Component {
StackPage {
Component {
- PanelsPage {
- }
+ PanelsPage {}
}
Component {
- BarPanel {
- }
+ BarPanel {}
}
Component {
- DashboardPanel {
- }
+ DashboardPanel {}
}
Component {
- ResourcesPanel {
- }
+ ResourcesPanel {}
}
Component {
- LauncherPanel {
- }
+ LauncherPanel {}
}
Component {
- SidebarPanel {
- }
+ SidebarPanel {}
}
// Bar sub pages
Component {
- BarTray {
- }
+ BarTray {}
}
Component {
- BarStatusIcons {
- }
+ BarStatusIcons {}
}
Component {
- BarClock {
- }
+ BarClock {}
+ }
+
+ // Sidebar sub pages
+ Component {
+ SidebarLlm {}
}
}
},
@@ -122,18 +116,15 @@ QtObject {
Component {
StackPage {
Component {
- AppsPage {
- }
+ AppsPage {}
}
Component {
- AllApps {
- }
+ AllApps {}
}
Component {
- AppInfo {
- }
+ AppInfo {}
}
}
},
@@ -142,13 +133,11 @@ QtObject {
Component {
StackPage {
Component {
- ServicesPage {
- }
+ ServicesPage {}
}
Component {
- NotificationsPage {
- }
+ NotificationsPage {}
}
}
},
@@ -157,15 +146,13 @@ QtObject {
Component {
StackPage {
Component {
- AboutPage {
- }
+ AboutPage {}
}
}
}
]
readonly property Component placeholderComp: Component {
- PlaceholderComp {
- }
+ PlaceholderComp {}
}
component PlaceholderComp: Item {
diff --git a/Modules/Settings/Pages.qml b/Modules/Settings/Pages.qml
index f853699..4aacc6d 100644
--- a/Modules/Settings/Pages.qml
+++ b/Modules/Settings/Pages.qml
@@ -1,6 +1,7 @@
import QtQuick
import qs.Components
import ZShell.Config
+import qs.Services
Item {
id: root
@@ -41,6 +42,18 @@ Item {
objectName: "PageContainer"
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
+
+ // CustomRect {
+ // anchors.fill: parent
+ // color: Qt.alpha(Colors.palette.m3shadow, 0.3)
+ // opacity: root.sState.dimmed ? 1 : 0
+ // visible: opacity > 0
+ // z: 0
+ //
+ // Behavior on opacity {
+ // Anim {}
+ // }
+ // }
}
Connections {
diff --git a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml
index ae17efa..8985fb0 100644
--- a/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml
+++ b/Modules/Settings/Pages/Panels/Bar/BarStatusIcons.qml
@@ -55,6 +55,11 @@ PageBase {
enabled: Object.keys(model).length > 0
header: qsTr("Add new entry")
icon: "add"
+ settingAnchor: "panels-bar-status-icons-add-item"
+ onOpenChanged: {
+ root.sState.dimmed = open;
+ }
+ last: true
label: qsTr("Add entry")
model: {
const present = new Set(Config.bar.tray.statusIcons.values.map(item => item.id));
diff --git a/Modules/Settings/Pages/Panels/Sidebar/SidebarLlm.qml b/Modules/Settings/Pages/Panels/Sidebar/SidebarLlm.qml
new file mode 100644
index 0000000..ce750e5
--- /dev/null
+++ b/Modules/Settings/Pages/Panels/Sidebar/SidebarLlm.qml
@@ -0,0 +1,105 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Config
+import qs.Components
+import qs.Modules.Settings.Common
+
+PageBase {
+ id: root
+
+ readonly property var schemes: [
+ {
+ id: "oneDark",
+ label: qsTr("One Dark")
+ },
+ {
+ id: "nord",
+ label: qsTr("Nord")
+ },
+ {
+ id: "dracula",
+ label: qsTr("Dracula")
+ },
+ {
+ id: "githubDark",
+ label: qsTr("Github Dark")
+ },
+ {
+ id: "solarizedDark",
+ label: qsTr("Solarized Dark")
+ },
+ {
+ id: "monokai",
+ label: qsTr("Monokai")
+ },
+ {
+ id: "gruvboxDark",
+ label: qsTr("Gruvbox Dark")
+ },
+ {
+ id: "catppuccinMocha",
+ label: qsTr("Catppuccin Mocha")
+ },
+ {
+ id: "tokyoNight",
+ label: qsTr("Tokyo Night")
+ },
+ {
+ id: "ayuDark",
+ label: qsTr("Ayu Dark")
+ },
+ {
+ id: "palenight",
+ label: qsTr("Pale Night")
+ }
+ ]
+
+ isSubPage: true
+ title: qsTr("Sidebar")
+
+ ColumnLayout {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ spacing: Tokens.spacing.extraSmall / 2
+ width: root.cappedWidth
+
+ SectionHeader {
+ first: true
+ text: qsTr("Appearance")
+ }
+
+ DialogSelectButton {
+ id: selectScheme
+
+ acceptLabel: qsTr("Confirm")
+ enabled: Object.keys(model).length > 0
+ settingAnchor: "panels-sidebar-select-scheme"
+ separateContent: false
+ header: qsTr("Select scheme")
+ first: true
+ last: true
+ icon: "format_paint"
+ iconLabel.font.pointSize: Tokens.font.size.large
+ rowButton.trailingIcon: "open_in_new"
+ onOpenChanged: {
+ root.sState.dimmed = open;
+ }
+ rowButton.activeItem: root.schemes.find(s => s.id === Config.llm.appearance.scheme).label
+ label: qsTr("Color scheme")
+ subtext: qsTr("Select the color scheme used for code blocks")
+ model: root.schemes
+ rootParent: root.flickable
+
+ initialSelect: Config.llm.appearance.scheme
+
+ onAccepted: {
+ if (!selectedItem)
+ return;
+
+ Config.llm.appearance.scheme = selectedItem;
+ }
+ }
+ }
+}
diff --git a/Modules/Settings/Pages/Panels/SidebarPanel.qml b/Modules/Settings/Pages/Panels/SidebarPanel.qml
index e99a36e..6c8abd3 100644
--- a/Modules/Settings/Pages/Panels/SidebarPanel.qml
+++ b/Modules/Settings/Pages/Panels/SidebarPanel.qml
@@ -88,7 +88,12 @@ PageBase {
enabled: Object.keys(model).length > 0
header: qsTr("Add new entry")
icon: "add"
+ onOpenChanged: {
+ root.sState.dimmed = open;
+ }
+ settingAnchor: "panels-sidebar-quick-toggles-add-item"
label: qsTr("Add entry")
+ last: true
model: {
const present = new Set(Config.utilities.quickToggles.values.map(item => item.id));
return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({
@@ -108,5 +113,18 @@ PageBase {
});
}
}
+
+ SectionHeader {
+ text: qsTr("AI chat")
+ }
+
+ NavRow {
+ text: qsTr("AI chat")
+ icon: "robot_2"
+ first: true
+ last: true
+
+ onClicked: root.sState.openSubPage(9)
+ }
}
}
diff --git a/Modules/Settings/Pages/ServicesPage.qml b/Modules/Settings/Pages/ServicesPage.qml
index e179635..8a7ffb7 100644
--- a/Modules/Settings/Pages/ServicesPage.qml
+++ b/Modules/Settings/Pages/ServicesPage.qml
@@ -202,12 +202,12 @@ PageBase {
}
ToggleRow {
- checked: Config.general.color.smart
+ checked: Config.colors.smart
settingAnchor: "services-smart-color-scheme"
subtext: qsTr("Derive theme mode from the wallpaper")
text: qsTr("Smart color scheme")
- onToggled: Config.general.color.smart = checked
+ onToggled: Config.colors.smart = checked
}
SelectRow {
diff --git a/Modules/Settings/Pages/Wallpaper/ColorsFonts.qml b/Modules/Settings/Pages/Wallpaper/ColorsFonts.qml
new file mode 100644
index 0000000..be09d14
--- /dev/null
+++ b/Modules/Settings/Pages/Wallpaper/ColorsFonts.qml
@@ -0,0 +1,327 @@
+pragma ComponentBehavior: Bound
+
+import Quickshell
+import Quickshell.Io
+import QtQuick
+import QtQuick.Layouts
+import ZShell.Config
+import qs.Paths
+import qs.Services
+import qs.Modules.Settings.Common
+
+PageBase {
+ id: root
+
+ readonly property list fonts: Qt.fontFamilies()
+ readonly property var schemes: [
+ {
+ id: "onedark",
+ label: qsTr("One Dark")
+ },
+ {
+ id: "nord",
+ label: qsTr("Nord")
+ },
+ {
+ id: "dracula",
+ label: qsTr("Dracula")
+ },
+ {
+ id: "darkgreen",
+ label: qsTr("Darkgreen")
+ },
+ {
+ id: "solarized",
+ label: qsTr("Solarized")
+ },
+ {
+ id: "rosepine",
+ label: qsTr("Rosepine")
+ },
+ {
+ id: "gruvbox",
+ label: qsTr("Gruvbox")
+ },
+ {
+ id: "catppuccin",
+ label: qsTr("Catppuccin")
+ },
+ {
+ id: "tokyonight",
+ label: qsTr("Tokyo Night")
+ },
+ {
+ id: "shadotheme",
+ label: qsTr("Shadotheme")
+ }
+ ]
+
+ isSubPage: true
+ title: qsTr("Colors & font")
+
+ ColumnLayout {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ spacing: Tokens.spacing.extraSmall / 2
+ width: root.cappedWidth
+
+ SectionHeader {
+ first: true
+ text: qsTr("Colors")
+ }
+
+ ToggleRow {
+ text: qsTr("Dynamic colors")
+ first: true
+ subtext: qsTr("Automatic color scheme based on wallpaper is %1").arg(Config.colors.schemeGen ? "enabled" : "disabled")
+ checked: Config.colors.schemeGen
+ settingAnchor: "colors-fonts-dynamic-colors"
+
+ onToggled: {
+ Config.colors.schemeGen = checked;
+
+ if (checked)
+ Colors.setMode(Config.colors.mode);
+ }
+ }
+
+ DialogSelectButton {
+ settingAnchor: "colors-fonts-preset-scheme"
+ model: root.schemes
+ acceptLabel: qsTr("Confirm")
+ enabled: !Config.colors.schemeGen
+ header: qsTr("Select scheme")
+ rowButton.activeItem: root.schemes.find(s => {
+ var regex = new RegExp(Config.colors.presets.name, "i");
+ return regex.test(s.id);
+ }).label ?? ""
+ label: qsTr("Preset scheme")
+ subtext: qsTr("Select preset color scheme")
+ rootParent: root.flickable
+
+ FileView {
+ id: view
+ path: `${Paths.state}/presets.json`
+
+ watchChanges: true
+ onFileChanged: reload()
+
+ JsonAdapter {
+ id: adapter
+
+ property var presets
+ }
+ }
+
+ initialSelect: Config.colors.presets.name.toLowerCase()
+
+ onAccepted: {
+ if (!selectedItem)
+ return;
+
+ const s = selectedItem.toLowerCase();
+ let v = "";
+ let a = "";
+ const existingVariant = Config.colors.presets.variant;
+ const existingAccent = Config.colors.presets.accent;
+ const variantKeys = Object.keys(adapter.presets[s].variants);
+
+ if (existingVariant !== "" && variantKeys.some(v => v === existingVariant))
+ v = existingVariant;
+
+ if (v !== "" && existingAccent !== "" && adapter.presets[s].variants[v]?.accents.some(a => a === existingAccent))
+ a = existingAccent;
+
+ if (a !== "")
+ Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`, "-a", `${a}`]);
+ else if (v !== "")
+ Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`]);
+ else
+ Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${variantKeys[0]}`]);
+ }
+ }
+
+ DialogSelectButton {
+ settingAnchor: "colors-fonts-preset-variant"
+ // qmlformat off
+ model: {
+ const v = adapter.presets[Config.colors.presets.name].variants;
+ const map = Object.keys(v).map(v => ({
+ id: v,
+ label: qsTr("%1%2")
+ .arg(v.charAt(0).toUpperCase())
+ .arg(v.slice(1).toLowerCase())
+ }));
+
+ return map;
+ }
+ // qmlformat on
+ acceptLabel: qsTr("Confirm")
+ enabled: !Config.colors.schemeGen && model.length > 1
+ header: qsTr("Select variant")
+ rowButton.activeItem: {
+ const v = Config.colors.presets.variant ?? "";
+ if (v === "")
+ return "";
+ return qsTr("%1%2").arg(v.charAt(0).toUpperCase()).arg(v.slice(1).toLowerCase());
+ }
+ label: qsTr("Variant")
+ subtext: qsTr("Select preset variant")
+ rootParent: root.flickable
+
+ initialSelect: Config.colors.presets.variant
+
+ onAccepted: {
+ if (!selectedItem)
+ return;
+
+ const v = selectedItem.toLowerCase();
+ const s = Config.colors.presets.name;
+ const existingAccent = Config.colors.presets.accent;
+ let a = "";
+
+ if (existingAccent !== "" && adapter.presets[s].variants[v]?.accents.some(a => a === existingAccent))
+ a = existingAccent;
+
+ if (a !== "")
+ Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`, "-a", `${a}`]);
+ else
+ Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`]);
+ }
+ }
+
+ DialogSelectButton {
+ settingAnchor: "colors-fonts-preset-accent"
+ // qmlformat off
+ model: {
+ const a = adapter.presets[Config.colors.presets.name].variants[Config.colors.presets.variant]?.accents ?? [];
+
+ if (a.length === 0)
+ return a;
+
+ const map = a.map(a => ({
+ id: a,
+ label: qsTr("%1%2")
+ .arg(a.charAt(0).toUpperCase())
+ .arg(a.slice(1).toLowerCase())
+ }));
+
+ return map;
+ }
+ // qmlformat on
+ acceptLabel: qsTr("Confirm")
+ enabled: !Config.colors.schemeGen && model.length > 1
+ header: qsTr("Select accent")
+ rowButton.activeItem: {
+ const a = Config.colors.presets.accent ?? "";
+ if (a === "")
+ return "";
+ return qsTr("%1%2").arg(a.charAt(0).toUpperCase()).arg(a.slice(1).toLowerCase());
+ }
+ label: qsTr("Accent")
+ last: true
+ subtext: qsTr("Select preset accent")
+ rootParent: root.flickable
+
+ initialSelect: Config.colors.presets.accent
+
+ onAccepted: {
+ if (!selectedItem)
+ return;
+
+ const a = selectedItem.toLowerCase();
+ const v = Config.colors.presets.variant;
+ const s = Config.colors.presets.name;
+
+ Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${s}:${v}`, "-a", `${a}`]);
+ }
+ }
+
+ SectionHeader {
+ text: qsTr("Fonts")
+ }
+
+ SearchPopup {
+ id: selectSansFont
+
+ settingAnchor: "colors-fonts-sans-font"
+ model: root.fonts
+ acceptLabel: qsTr("Confirm")
+ enabled: Object.keys(model).length > 0
+ separateContent: false
+ header: qsTr("Select font")
+ first: true
+ rowButton.activeItem: root.fonts.find(s => {
+ var regex = new RegExp(Config.appearance.font.family.sans, "i");
+ return regex.test(s);
+ }) ?? ""
+ label: qsTr("Sans font")
+ subtext: qsTr("Select desired sans font")
+ rootParent: root.flickable
+
+ initialSelect: Config.appearance.font.family.sans
+
+ onAccepted: {
+ if (!selectedItem)
+ return;
+
+ Config.appearance.font.family.sans = selectedItem;
+ }
+ }
+
+ SearchPopup {
+ id: selectMonoFont
+
+ acceptLabel: qsTr("Confirm")
+ enabled: Object.keys(model).length > 0
+ settingAnchor: "colors-fonts-mono-font"
+ separateContent: false
+ header: qsTr("Select font")
+ rowButton.activeItem: root.fonts.find(s => {
+ var regex = new RegExp(Config.appearance.font.family.mono, "i");
+ return regex.test(s);
+ }) ?? ""
+ label: qsTr("Mono font")
+ subtext: qsTr("Select desired mono font")
+ model: root.fonts
+ rootParent: root.flickable
+
+ initialSelect: Config.appearance.font.family.mono
+
+ onAccepted: {
+ if (!selectedItem)
+ return;
+
+ Config.appearance.font.family.mono = selectedItem;
+ }
+ }
+
+ SearchPopup {
+ id: selectClockFont
+
+ acceptLabel: qsTr("Confirm")
+ enabled: Object.keys(model).length > 0
+ separateContent: false
+ settingAnchor: "colors-fonts-clock-font"
+ header: qsTr("Select font")
+ last: true
+ rowButton.activeItem: root.fonts.find(s => {
+ var regex = new RegExp(Config.appearance.font.family.clock, "i");
+ return regex.test(s);
+ }) ?? ""
+ label: qsTr("Clock font")
+ subtext: qsTr("Select desired clock font")
+ model: root.fonts
+ rootParent: root.flickable
+
+ initialSelect: Config.appearance.font.family.clock
+
+ onAccepted: {
+ if (!selectedItem)
+ return;
+
+ Config.appearance.font.family.clock = selectedItem;
+ }
+ }
+ }
+}
diff --git a/Modules/Settings/Pages/WallpaperPage.qml b/Modules/Settings/Pages/WallpaperPage.qml
index 40274ad..8c4b524 100644
--- a/Modules/Settings/Pages/WallpaperPage.qml
+++ b/Modules/Settings/Pages/WallpaperPage.qml
@@ -33,18 +33,35 @@ PageBase {
}
}
- IconTextButton {
+ ButtonRow {
Layout.alignment: Qt.AlignHCenter
- enabled: Config.background.enabled
- horizontalPadding: Tokens.padding.extraLarge
- icon: "wallpaper"
- isRound: true
- shapeMorph: true
- text: qsTr("Wallpapers")
- type: IconTextButton.Tonal
- verticalPadding: Tokens.padding.small
+ spacing: Tokens.spacing.small
- onClicked: root.sState.openSubPage(1) // Wallpaper page
+ IconTextButton {
+ enabled: Config.background.enabled
+ horizontalPadding: Tokens.padding.extraLarge
+ icon: "wallpaper"
+ isRound: true
+ shapeMorph: true
+ text: qsTr("Wallpapers")
+ type: IconTextButton.Tonal
+ verticalPadding: Tokens.padding.small
+
+ onClicked: root.sState.openSubPage(1) // Wallpaper page
+ }
+
+ IconTextButton {
+ enabled: Config.background.enabled
+ horizontalPadding: Tokens.padding.extraLarge
+ icon: "palette"
+ isRound: true
+ shapeMorph: true
+ text: qsTr("Colors & font")
+ type: IconTextButton.Tonal
+ verticalPadding: Tokens.padding.small
+
+ onClicked: root.sState.openSubPage(2) // Colors & font page
+ }
}
ToggleRow {
@@ -76,85 +93,116 @@ PageBase {
onToggled: Colors.setMode(checked ? "dark" : "light")
}
- OverlayRow {
- id: darkMode
-
+ TimeDialogSelect {
readonly property string endTime: {
var d = new Date(0, 0, 0, 0, 0, 0, 0);
- d.setMinutes(Config.general.color.scheduleDarkEnd);
+ d.setMinutes(Config.colors.scheduleDarkEnd);
return Qt.formatTime(d, "hh:mm AP");
}
readonly property string startTime: {
var d = new Date(0, 0, 0, 0, 0, 0, 0);
- d.setMinutes(Config.general.color.scheduleDarkStart);
+ d.setMinutes(Config.colors.scheduleDarkStart);
return Qt.formatTime(d, "hh:mm AP");
}
- checked: Config.general.color.scheduleDark
+ rowButton.checked: Config.colors.scheduleDark
+ rowButton.onToggled: Config.colors.scheduleDark = rowButton.checked
+ start: Config.colors.scheduleDarkStart
+ end: Config.colors.scheduleDarkEnd
first: true
+ last: true
settingAnchor: "style-schedule-dark-mode"
+ acceptLabel: qsTr("Apply")
+ rootParent: root.flickable
+ header: qsTr("Select time")
subtext: qsTr("Dark mode will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
- text: qsTr("Schedule dark mode")
+ label: qsTr("Schedule dark mode")
- popup: Component {
- TimeInput {
- object: Config.general.color
- settings: ["scheduleDark", "scheduleDarkStart", "scheduleDarkEnd"]
-
- onApplySettings: (start, end) => {
- Config.general.color.scheduleDarkStart = start;
- Config.general.color.scheduleDarkEnd = end;
- ModeScheduler.checkStartup();
- PopupManager.requestClose();
- }
- onClose: PopupManager.requestClose()
- }
+ onOpenChanged: {
+ root.sState.dimmed = open;
}
- onClicked: value => {
- Config.general.color.scheduleDark = value;
+ onAccepted: {
+ Config.colors.scheduleDarkStart = startCurrent;
+ Config.colors.scheduleDarkEnd = endCurrent;
+ ModeScheduler.checkStartup();
}
}
- OverlayRow {
- id: hyprsunset
-
+ TimeDialogSelect {
readonly property string endTime: {
var d = new Date(0, 0, 0, 0, 0, 0, 0);
- d.setMinutes(Config.general.color.scheduleHyprsunsetEnd);
+ d.setMinutes(Config.display.nightlight.scheduleEnd);
return Qt.formatTime(d, "hh:mm AP");
}
readonly property string startTime: {
var d = new Date(0, 0, 0, 0, 0, 0, 0);
- d.setMinutes(Config.general.color.scheduleHyprsunsetStart);
+ d.setMinutes(Config.display.nightlight.scheduleStart);
return Qt.formatTime(d, "hh:mm AP");
}
+ rowButton.checked: Config.display.nightlight.schedule
+ rowButton.onToggled: Config.display.nightlight.schedule = rowButton.checked
+ start: Config.display.nightlight.scheduleStart
+ end: Config.display.nightlight.scheduleEnd
+ first: true
+ settingAnchor: "style-schedule-nightlight"
+ acceptLabel: qsTr("Apply")
+ rootParent: root.flickable
+ header: qsTr("Select time")
+ subtext: qsTr("Nightlight will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
+ label: qsTr("Schedule nightlight")
+
+ onOpenChanged: {
+ root.sState.dimmed = open;
+ }
+
+ onAccepted: {
+ Config.display.nightlight.scheduleStart = startCurrent;
+ Config.display.nightlight.scheduleEnd = endCurrent;
+ Hyprsunset.checkStartup();
+ }
+ }
+
+ ToggleRow {
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
- checked: Config.general.color.scheduleHyprsunset
+
+ checked: Config.display.nightlight.useNative
+ settingAnchor: "display-nightlight-useNative"
+ text: qsTr("Use native")
+ subtext: qsTr("Use native shader nightlight for more features")
+
+ onToggled: Config.display.nightlight.useNative = checked
+ }
+
+ SpinRow {
+ Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
+
+ from: 2000
+ settingAnchor: "display-nightlight-temp"
+ stepSize: 100
+ subtext: qsTr("How warm/orange the screen will be. Lower is warmer (Kelvin)")
+ text: qsTr("Temperature")
+ to: 6500
+ value: Config.display.nightlight.temp
+
+ onMoved: v => Config.display.nightlight.temp = v
+ }
+
+ SpinRow {
+ Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
+
+ from: 0
+ settingAnchor: "display-nightlight-fade-duration"
+ enabled: Config.display.nightlight.useNative
last: true
- settingAnchor: "style-schedule-hyprsunset"
- subtext: qsTr("Hyprsunset will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime)
- text: qsTr("Schedule hyprsunset")
+ stepSize: 1
+ subtext: qsTr("How long it takes for the gradual nightlight to finish (s)")
+ text: qsTr("Fade duration")
+ to: 60 * 5
+ value: Config.display.nightlight.fadeDuration / 1000
- popup: Component {
- TimeInput {
- object: Config.general.color
- settings: ["scheduleHyprsunset", "scheduleHyprsunsetStart", "scheduleHyprsunsetEnd"]
-
- onApplySettings: (start, end) => {
- Config.general.color.scheduleHyprsunsetStart = start;
- Config.general.color.scheduleHyprsunsetEnd = end;
- Hyprsunset.checkStartup();
- PopupManager.requestClose();
- }
- onClose: PopupManager.requestClose()
- }
- }
-
- onClicked: value => {
- Config.general.color.scheduleHyprsunset = value;
- }
+ onMoved: v => Config.display.nightlight.fadeDuration = v * 1000
}
}
}
diff --git a/Modules/Settings/PopupManager.qml b/Modules/Settings/PopupManager.qml
deleted file mode 100644
index 4b37c02..0000000
--- a/Modules/Settings/PopupManager.qml
+++ /dev/null
@@ -1,20 +0,0 @@
-pragma Singleton
-
-import Quickshell
-import QtQuick
-
-Singleton {
- id: root
-
- property bool closed: true
- property Component currentPopup: null
-
- function requestClose(): void {
- closed = true;
- }
-
- function requestOpen(component: Component): void {
- currentPopup = component;
- closed = false;
- }
-}
diff --git a/Modules/Settings/PopupOverlay.qml b/Modules/Settings/PopupOverlay.qml
deleted file mode 100644
index d18440e..0000000
--- a/Modules/Settings/PopupOverlay.qml
+++ /dev/null
@@ -1,47 +0,0 @@
-import QtQuick
-import ZShell.Config
-import qs.Components
-import qs.Services
-
-CustomRect {
- id: root
-
- readonly property bool closing: PopupManager.closed
- readonly property var currentPopup: PopupManager.currentPopup
- property bool shouldBeVisible: loader.status === Loader.Ready
-
- color: Qt.alpha(Colors.palette.m3shadow, 0.3)
- opacity: shouldBeVisible && !closing ? 1 : 0
- visible: opacity > 0
-
- Behavior on opacity {
- Anim {
- }
- }
-
- onVisibleChanged: if (!visible)
- PopupManager.currentPopup = null
-
- CustomMouseArea {
- anchors.fill: parent
- hoverEnabled: true
- preventStealing: true
- propagateComposedEvents: false
-
- onClicked: {
- const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x;
- const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y;
- if (insideItemHeight && insideItemWidth)
- return;
-
- PopupManager.requestClose();
- }
- }
-
- Loader {
- id: loader
-
- anchors.centerIn: parent
- sourceComponent: root.currentPopup
- }
-}
diff --git a/Modules/Settings/SettingsState.qml b/Modules/Settings/SettingsState.qml
index 79e80d8..11933e1 100644
--- a/Modules/Settings/SettingsState.qml
+++ b/Modules/Settings/SettingsState.qml
@@ -16,6 +16,7 @@ QtObject {
property string searchText
property DesktopEntry selectedApp
property BluetoothDevice selectedBtDevice
+ property bool dimmed
property string selectedWallpaperCategory
property list subPageIdxStack
diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt
index d260f80..9e7303f 100644
--- a/Plugins/ZShell/CMakeLists.txt
+++ b/Plugins/ZShell/CMakeLists.txt
@@ -79,3 +79,12 @@ add_subdirectory(Services)
add_subdirectory(Components)
add_subdirectory(Blobs)
add_subdirectory(Config)
+add_subdirectory(Llm)
+
+pkg_check_modules(HYPRLAND_PROBE hyprland)
+if(HYPRLAND_PROBE_FOUND)
+ add_subdirectory(HyprPlugins)
+else()
+ message(STATUS
+ "hyprland development headers not found; skipping zshell-nightlight plugin")
+endif()
diff --git a/Plugins/ZShell/Components/CMakeLists.txt b/Plugins/ZShell/Components/CMakeLists.txt
index 3d3e4a7..5d1cd78 100644
--- a/Plugins/ZShell/Components/CMakeLists.txt
+++ b/Plugins/ZShell/Components/CMakeLists.txt
@@ -1,10 +1,11 @@
qml_module(ZShell-components
URI ZShell.Components
- SOURCES
- lazylistview.hpp lazylistview.cpp
+ SOURCES
+ lazylistview.hpp lazylistview.cpp
wavyline.hpp wavyline.cpp
buttonrow.hpp buttonrow.cpp
carouselview.hpp carouselview.cpp
- LIBRARIES
- Qt::Quick
+ wheelinverter.hpp
+ LIBRARIES
+ Qt::Quick
)
diff --git a/Plugins/ZShell/Components/wheelinverter.hpp b/Plugins/ZShell/Components/wheelinverter.hpp
new file mode 100644
index 0000000..38aa599
--- /dev/null
+++ b/Plugins/ZShell/Components/wheelinverter.hpp
@@ -0,0 +1,90 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ZShell::components {
+
+class WheelInverter : public QQuickItem {
+ Q_OBJECT
+ QML_ELEMENT
+
+ Q_PROPERTY(
+ QQuickItem* target READ target WRITE setTarget NOTIFY targetChanged)
+
+ public:
+ using QQuickItem::QQuickItem;
+
+ QQuickItem* target() const { return m_target; }
+
+ void setTarget(QQuickItem* target) {
+ if (m_target == target) return;
+
+ if (m_target) m_target->removeEventFilter(this);
+
+ m_target = target;
+
+ if (m_target) m_target->installEventFilter(this);
+
+ emit targetChanged();
+ }
+
+ protected:
+ class InvertedWheelEvent : public QWheelEvent {
+ public:
+ using QWheelEvent::QWheelEvent;
+
+ bool invertedByWheelInverter = true;
+ };
+
+ bool eventFilter(QObject* watched, QEvent* event) override {
+ if (watched != m_target || event->type() != QEvent::Wheel)
+ return QQuickItem::eventFilter(watched, event);
+
+ auto* wheel = static_cast(event);
+
+ const bool inverted = dynamic_cast(wheel) !=
+ nullptr;
+
+ if (inverted) return false;
+
+ auto* window = m_target ? m_target->window() : nullptr;
+ if (!window) {
+ qInfo() << "[WheelInverter] no window";
+ return false;
+ }
+
+ const QPointF windowPos =
+ window->mapFromGlobal(wheel->globalPosition());
+
+ auto* invertedEvent = new InvertedWheelEvent(
+ windowPos,
+ wheel->globalPosition(),
+ -wheel->pixelDelta(),
+ -wheel->angleDelta(),
+ wheel->buttons(),
+ wheel->modifiers(),
+ wheel->phase(),
+ wheel->inverted(),
+ wheel->source(),
+ wheel->pointingDevice());
+
+ invertedEvent->setTimestamp(wheel->timestamp());
+
+ QCoreApplication::postEvent(window, invertedEvent);
+
+ return true;
+ }
+
+ signals:
+ void targetChanged();
+
+ private:
+ QPointer m_target;
+};
+
+} // namespace ZShell::components
diff --git a/Plugins/ZShell/Config/CMakeLists.txt b/Plugins/ZShell/Config/CMakeLists.txt
index bee1215..dfe9d59 100644
--- a/Plugins/ZShell/Config/CMakeLists.txt
+++ b/Plugins/ZShell/Config/CMakeLists.txt
@@ -5,6 +5,7 @@ qml_module(ZShell-config
configobject.hpp configobject.cpp
configlist.hpp configlist.cpp
config.hpp config.cpp
+ migration.hpp migration.cpp
anim.hpp anim.cpp
tokens.hpp
appearance.hpp
@@ -13,9 +14,11 @@ qml_module(ZShell-config
clipboard.hpp
colors.hpp
dashboard.hpp
+ display.hpp
dock.hpp
general.hpp
launcher.hpp
+ llm.hpp
lock.hpp
notifs.hpp
osd.hpp
diff --git a/Plugins/ZShell/Config/colors.hpp b/Plugins/ZShell/Config/colors.hpp
index 0554df6..20f8657 100644
--- a/Plugins/ZShell/Config/colors.hpp
+++ b/Plugins/ZShell/Config/colors.hpp
@@ -22,6 +22,13 @@ class Colors : public ConfigObject {
CONFIG_SUBOBJECT(Presets, presets)
CFG_PROPERTY(QString, schemeType, QStringLiteral("fidelity"))
+ CFG_PROPERTY(bool, scheduleDark, false)
+ CFG_PROPERTY(int, scheduleDarkEnd, 600)
+ CFG_PROPERTY(int, scheduleDarkStart, 1140)
+ CFG_PROPERTY(bool, schemeGen, true)
+ CFG_PROPERTY(bool, neovimColors, false)
+ CFG_PROPERTY(QString, mode, QStringLiteral("dark"))
+ CFG_PROPERTY(bool, smart, false)
public:
explicit Colors(QObject* parent = nullptr)
diff --git a/Plugins/ZShell/Config/config.cpp b/Plugins/ZShell/Config/config.cpp
index 268b4c1..d8e3a03 100644
--- a/Plugins/ZShell/Config/config.cpp
+++ b/Plugins/ZShell/Config/config.cpp
@@ -6,10 +6,13 @@
#include "clipboard.hpp"
#include "colors.hpp"
#include "dashboard.hpp"
+#include "display.hpp"
#include "dock.hpp"
#include "general.hpp"
#include "launcher.hpp"
+#include "llm.hpp"
#include "lock.hpp"
+#include "migration.hpp"
#include "notifs.hpp"
#include "osd.hpp"
#include "screenshot.hpp"
@@ -33,6 +36,8 @@
namespace ZShell::config {
+Config* Config::s_instance = nullptr;
+
Config::Config(QObject* parent)
: ConfigObject(parent)
, m_appearance(new Appearance(this))
@@ -41,9 +46,11 @@ Config::Config(QObject* parent)
, m_clipboard(new Clipboard(this))
, m_colors(new Colors(this))
, m_dashboard(new Dashboard(this))
+ , m_display(new Display(this))
, m_dock(new Dock(this))
, m_general(new General(this))
, m_launcher(new Launcher(this))
+ , m_llm(new Llm(this))
, m_lock(new Lock(this))
, m_notifs(new Notifs(this))
, m_osd(new Osd(this))
@@ -51,6 +58,7 @@ Config::Config(QObject* parent)
, m_services(new Services(this))
, m_sidebar(new Sidebar(this))
, m_utilities(new Utilities(this)) {
+ s_instance = this;
connect(this, &ConfigObject::propertiesChanged, this, &Config::scheduleSave);
m_saveTimer.setSingleShot(true);
@@ -81,8 +89,13 @@ Config::Config(QObject* parent)
m_firstLoadDone = true;
}
+Config* Config::instance() {
+ return s_instance;
+}
+
Config* Config::create(QQmlEngine*, QJSEngine*) {
- return new Config();
+ if (!s_instance) s_instance = new Config();
+ return s_instance;
}
QString Config::filePath() const {
@@ -100,11 +113,14 @@ void Config::loadSync() {
QJsonObject before;
bool existed = false;
+ bool migrated = false;
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
if (doc.isObject()) {
- before = doc.object();
+ const QJsonObject raw = doc.object();
+ before = ConfigMigrations::apply(raw);
+ migrated = before != raw;
existed = true;
} else {
qInfo() << "Config: existing config at" << filePath()
@@ -119,7 +135,7 @@ void Config::loadSync() {
m_loading = false;
const auto after = toJson().toObject();
- if (!existed || after != before) saveNow();
+ if (!existed || migrated || after != before) saveNow();
}
void Config::updateWatch() {
@@ -165,16 +181,18 @@ void Config::loadAsync() {
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
const bool valid = doc.isObject();
- const QJsonObject before = valid ? doc.object() : QJsonObject();
+ const QJsonObject raw = valid ? doc.object() : QJsonObject();
+ const QJsonObject before = ConfigMigrations::apply(raw);
+ const bool migrated = valid && before != raw;
QMetaObject::invokeMethod(
this,
- [this, valid, before]() {
+ [this, valid, migrated, before]() {
loadFromJson(QJsonValue(before));
m_loading = false;
const auto after = toJson().toObject();
- if (!valid || after != before) saveNow();
+ if (!valid || migrated || after != before) saveNow();
if (m_reloadPending) m_reloadTimer.start();
},
diff --git a/Plugins/ZShell/Config/config.hpp b/Plugins/ZShell/Config/config.hpp
index e1881c5..2ad92ea 100644
--- a/Plugins/ZShell/Config/config.hpp
+++ b/Plugins/ZShell/Config/config.hpp
@@ -21,9 +21,11 @@ class Bar;
class Clipboard;
class Colors;
class Dashboard;
+class Display;
class Dock;
class General;
class Launcher;
+class Llm;
class Lock;
class Notifs;
class Osd;
@@ -43,9 +45,11 @@ class Config : public ConfigObject {
Q_MOC_INCLUDE("clipboard.hpp")
Q_MOC_INCLUDE("colors.hpp")
Q_MOC_INCLUDE("dashboard.hpp")
+ Q_MOC_INCLUDE("display.hpp")
Q_MOC_INCLUDE("dock.hpp")
Q_MOC_INCLUDE("general.hpp")
Q_MOC_INCLUDE("launcher.hpp")
+ Q_MOC_INCLUDE("llm.hpp")
Q_MOC_INCLUDE("lock.hpp")
Q_MOC_INCLUDE("notifs.hpp")
Q_MOC_INCLUDE("osd.hpp")
@@ -60,9 +64,11 @@ class Config : public ConfigObject {
CONFIG_SUBOBJECT(Clipboard, clipboard)
CONFIG_SUBOBJECT(Colors, colors)
CONFIG_SUBOBJECT(Dashboard, dashboard)
+ CONFIG_SUBOBJECT(Display, display)
CONFIG_SUBOBJECT(Dock, dock)
CONFIG_SUBOBJECT(General, general)
CONFIG_SUBOBJECT(Launcher, launcher)
+ CONFIG_SUBOBJECT(Llm, llm)
CONFIG_SUBOBJECT(Lock, lock)
CONFIG_SUBOBJECT(Notifs, notifs)
CONFIG_SUBOBJECT(Osd, osd)
@@ -74,6 +80,7 @@ class Config : public ConfigObject {
public:
explicit Config(QObject* parent = nullptr);
static Config* create(QQmlEngine*, QJSEngine*);
+ [[nodiscard]] static Config* instance();
Q_INVOKABLE void load();
Q_INVOKABLE void saveNow();
@@ -103,6 +110,8 @@ class Config : public ConfigObject {
bool m_loading = false;
bool m_firstLoadDone = false;
QFuture m_loadFuture;
+
+ static Config* s_instance;
};
} // namespace ZShell::config
diff --git a/Plugins/ZShell/Config/display.hpp b/Plugins/ZShell/Config/display.hpp
new file mode 100644
index 0000000..b22d1e8
--- /dev/null
+++ b/Plugins/ZShell/Config/display.hpp
@@ -0,0 +1,33 @@
+#pragma once
+#include "configobject.hpp"
+#include
+
+namespace ZShell::config {
+
+class Nightlight : public ConfigObject {
+ Q_OBJECT
+ QML_ANONYMOUS
+
+ CFG_PROPERTY(bool, schedule, true)
+ CFG_PROPERTY(int, scheduleStart, 1200)
+ CFG_PROPERTY(int, scheduleEnd, 570)
+ CFG_PROPERTY(bool, useNative, true)
+ CFG_PROPERTY(int, fadeDuration, 2000)
+ CFG_PROPERTY(int, temp, 2600)
+
+ public:
+ explicit Nightlight(QObject* parent = nullptr) : ConfigObject(parent) {}
+};
+
+class Display : public ConfigObject {
+ Q_OBJECT
+ QML_ANONYMOUS
+
+ CONFIG_SUBOBJECT(Nightlight, nightlight)
+
+ public:
+ explicit Display(QObject* parent = nullptr)
+ : ConfigObject(parent), m_nightlight(new Nightlight(this)) {}
+};
+
+} // namespace ZShell::config
diff --git a/Plugins/ZShell/Config/general.hpp b/Plugins/ZShell/Config/general.hpp
index 9d0f8fd..558ff82 100644
--- a/Plugins/ZShell/Config/general.hpp
+++ b/Plugins/ZShell/Config/general.hpp
@@ -91,26 +91,6 @@ class Battery : public ConfigObject {
explicit Battery(QObject* parent = nullptr) : ConfigObject(parent) {}
};
-class ColorSettings : public ConfigObject {
- Q_OBJECT
- QML_ANONYMOUS
-
- CFG_PROPERTY(int, hyprsunsetTemp, 2600)
- CFG_PROPERTY(QString, mode, QStringLiteral("dark"))
- CFG_PROPERTY(bool, neovimColors, false)
- CFG_PROPERTY(bool, scheduleDark, false)
- CFG_PROPERTY(int, scheduleDarkEnd, 600)
- CFG_PROPERTY(int, scheduleDarkStart, 1140)
- CFG_PROPERTY(bool, scheduleHyprsunset, true)
- CFG_PROPERTY(int, scheduleHyprsunsetEnd, 570)
- CFG_PROPERTY(int, scheduleHyprsunsetStart, 1200)
- CFG_PROPERTY(bool, schemeGeneration, true)
- CFG_PROPERTY(bool, smart, false)
-
- public:
- explicit ColorSettings(QObject* parent = nullptr) : ConfigObject(parent) {}
-};
-
class Idle : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
@@ -135,7 +115,6 @@ class General : public ConfigObject {
CONFIG_SUBOBJECT(Apps, apps)
CONFIG_SUBOBJECT(Battery, battery)
- CONFIG_SUBOBJECT(ColorSettings, color)
CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss"))
CFG_PROPERTY(bool, desktopIcons, true)
CONFIG_SUBOBJECT(Idle, idle)
@@ -151,7 +130,6 @@ class General : public ConfigObject {
: ConfigObject(parent)
, m_apps(new Apps(this))
, m_battery(new Battery(this))
- , m_color(new ColorSettings(this))
, m_idle(new Idle(this)) {}
};
diff --git a/Plugins/ZShell/Config/llm.hpp b/Plugins/ZShell/Config/llm.hpp
new file mode 100644
index 0000000..b6a1353
--- /dev/null
+++ b/Plugins/ZShell/Config/llm.hpp
@@ -0,0 +1,34 @@
+#pragma once
+#include "configobject.hpp"
+#include
+#include
+#include
+
+namespace ZShell::config {
+
+class LlmAppearance : public ConfigObject {
+ Q_OBJECT
+ QML_ANONYMOUS
+
+ CFG_PROPERTY(QString, scheme, QStringLiteral("tokyoNight"))
+
+ public:
+ explicit LlmAppearance(QObject* parent = nullptr) : ConfigObject(parent) {}
+};
+
+class Llm : public ConfigObject {
+ Q_OBJECT
+ QML_ANONYMOUS
+
+ CFG_PROPERTY(QString, endpoint, "http://localhost:8080")
+ CFG_PROPERTY(QString, model, "")
+ CFG_PROPERTY(double, temperature, 0.7)
+ CFG_PROPERTY(bool, tools, true)
+ CONFIG_SUBOBJECT(LlmAppearance, appearance)
+
+ public:
+ explicit Llm(QObject* parent = nullptr)
+ : ConfigObject(parent), m_appearance(new LlmAppearance(this)) {}
+};
+
+} // namespace ZShell::config
diff --git a/Plugins/ZShell/Config/migration.cpp b/Plugins/ZShell/Config/migration.cpp
new file mode 100644
index 0000000..da78206
--- /dev/null
+++ b/Plugins/ZShell/Config/migration.cpp
@@ -0,0 +1,123 @@
+#include "migration.hpp"
+
+#include
+
+namespace ZShell::config {
+
+namespace {
+
+// A stored value is "missing" if the path does not resolve to an entry.
+// Note: default-constructed QJsonValue is Null, not Undefined.
+bool isMissing(const QJsonValue& v) {
+ return v.isUndefined() || v.isNull();
+}
+
+QJsonValue valueAt(const QJsonObject& obj, const QStringList& path) {
+ QJsonObject cur = obj;
+
+ for (int i = 0; i < path.size() - 1; ++i) {
+ if (!cur.value(path.at(i)).isObject()) return QJsonValue::Undefined;
+ cur = cur.value(path.at(i)).toObject();
+ }
+
+ const QString key = path.last();
+ if (!cur.contains(key)) return QJsonValue::Undefined;
+ return cur.value(key);
+}
+
+QJsonObject placeAt(QJsonObject obj, QStringList path, const QJsonValue& value) {
+ const QString head = path.takeFirst();
+
+ if (path.isEmpty()) {
+ obj[head] = value;
+ return obj;
+ }
+
+ QJsonObject child = obj.value(head).isObject() ? obj.value(head).toObject()
+ : QJsonObject();
+ obj[head] = placeAt(child, path, value);
+ return obj;
+}
+
+// Removes `path` from `obj`. Returns true if a value was removed.
+// Parent objects left empty are removed as well.
+bool removeAt(QJsonObject& obj, const QStringList& path) {
+ const QString head = path.first();
+
+ if (path.size() == 1) {
+ if (!obj.contains(head)) return false;
+ obj.remove(head);
+ return true;
+ }
+
+ if (!obj.value(head).isObject()) return false;
+
+ QJsonObject child = obj.value(head).toObject();
+ const bool removed = removeAt(child, path.mid(1));
+
+ if (child.isEmpty())
+ obj.remove(head);
+ else
+ obj[head] = child;
+
+ return removed;
+}
+
+} // namespace
+
+const QList& ConfigMigrations::rules() {
+ static const QList s_rules = {
+ migrate(
+ "general.color.scheduleHyprsunset", "display.nightlight.schedule"),
+ migrate(
+ "general.color.scheduleHyprsunsetStart",
+ "display.nightlight.scheduleStart"),
+ migrate(
+ "general.color.scheduleHyprsunsetEnd",
+ "display.nightlight.scheduleEnd"),
+ migrate("general.color.scheduleDark", "colors.scheduleDark"),
+ migrate("general.color.scheduleDarkStart", "colors.scheduleDarkStart"),
+ migrate("general.color.scheduleDarkEnd", "colors.scheduleDarkEnd"),
+ migrate("general.color.schemeGeneration", "colors.schemeGen"),
+ migrate(
+ "general.color.useNativeNightlight",
+ "display.nightlight.useNative"),
+ migrate(
+ "general.color.nativeFadeDuration",
+ "display.nightlight.fadeDuration"),
+ migrate("general.color.hyprsunsetTemp", "display.nightlight.temp"),
+ migrate("general.color.neovimColors", "colors.neovimColors"),
+ migrate("general.color.mode", "colors.mode"),
+ migrate("general.color.smart", "colors.smart"),
+ };
+ return s_rules;
+}
+
+QJsonObject ConfigMigrations::apply(const QJsonObject& json) {
+ QJsonObject obj = json;
+
+ for (const auto& rule : rules()) {
+ const QStringList from =
+ rule.from.split(QLatin1Char('.'), Qt::SkipEmptyParts);
+ const QStringList to =
+ rule.to.split(QLatin1Char('.'), Qt::SkipEmptyParts);
+
+ if (from.isEmpty() || to.isEmpty()) continue;
+
+ const QJsonValue oldValue = valueAt(obj, from);
+ if (isMissing(oldValue)) continue;
+
+ const bool targetExists = !isMissing(valueAt(obj, to));
+ if (!targetExists) obj = placeAt(obj, to, oldValue);
+
+ if (removeAt(obj, from)) {
+ qInfo() << "Config migration:" << rule.from << "->" << rule.to
+ << (targetExists ? "(target already set, old value dropped)"
+ : "(value moved)");
+ }
+ }
+
+ return obj;
+}
+
+} // namespace ZShell::config
diff --git a/Plugins/ZShell/Config/migration.hpp b/Plugins/ZShell/Config/migration.hpp
new file mode 100644
index 0000000..1c03505
--- /dev/null
+++ b/Plugins/ZShell/Config/migration.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include
+#include
+
+namespace ZShell::config {
+
+// A single migration rule: move the value stored at the dotted `from` path
+// to the dotted `to` path, then delete the old key (and any parent objects
+// left empty).
+//
+// Rules are applied to the raw JSON file on every load, before the schema
+// loads it. They are idempotent: once the old key is gone the rule never
+// fires again, so no version stamping is needed. Old rules can stay in the
+// table forever.
+//
+// WARNING: the target path must exist in the schema (i.e. a ConfigObject
+// property somewhere in the tree). If it does not, the migrated value is
+// loaded as an unknown key and silently dropped on the next save.
+struct ConfigMigrationRule {
+ QString from;
+ QString to;
+};
+
+// Helper for writing rules: migrate("general.color.foo", "display.bar.foo")
+inline ConfigMigrationRule migrate(const QString& from, const QString& to) {
+ return {from, to};
+}
+
+class ConfigMigrations {
+ public:
+ [[nodiscard]] static const QList& rules();
+
+ // Applies all rules to `json`, returning the migrated object.
+ [[nodiscard]] static QJsonObject apply(const QJsonObject& json);
+};
+
+} // namespace ZShell::config
diff --git a/Plugins/ZShell/HyprPlugins/CMakeLists.txt b/Plugins/ZShell/HyprPlugins/CMakeLists.txt
new file mode 100644
index 0000000..0316838
--- /dev/null
+++ b/Plugins/ZShell/HyprPlugins/CMakeLists.txt
@@ -0,0 +1,12 @@
+pkg_check_modules(HYPRLAND REQUIRED hyprland)
+
+add_library(zshell-nightlight SHARED main.cpp)
+
+target_compile_features(zshell-nightlight PRIVATE cxx_std_23)
+target_include_directories(zshell-nightlight PRIVATE ${HYPRLAND_INCLUDE_DIRS})
+target_link_directories(zshell-nightlight PRIVATE ${HYPRLAND_LIBRARY_DIRS})
+target_compile_options(zshell-nightlight PRIVATE ${HYPRLAND_CFLAGS_OTHER})
+
+target_compile_options(zshell-nightlight PRIVATE -fno-gnu-unique)
+
+install(TARGETS zshell-nightlight LIBRARY DESTINATION "${INSTALL_LIBDIR}/plugins")
diff --git a/Plugins/ZShell/HyprPlugins/colortemp.hpp b/Plugins/ZShell/HyprPlugins/colortemp.hpp
new file mode 100644
index 0000000..0da6f88
--- /dev/null
+++ b/Plugins/ZShell/HyprPlugins/colortemp.hpp
@@ -0,0 +1,32 @@
+#pragma once
+#include
+#include
+#include
+
+namespace ZShell::services {
+
+inline std::array kelvinToGain(int kelvin) {
+ const double t = std::clamp(kelvin, 1000, 40000) / 100.0;
+ double r, g, b;
+
+ r = t <= 66.0
+ ? 1.0
+ : std::clamp(
+ 1.29293618606 * std::pow(t - 60.0, -0.1332047592), 0.0, 1.0);
+ g = t <= 66.0
+ ? std::clamp(0.39008157876 * std::log(t) - 0.63184144378, 0.0, 1.0)
+ : std::clamp(
+ 1.12989086089 * std::pow(t - 60.0, -0.0755148492), 0.0, 1.0);
+ b = t >= 66.0
+ ? 1.0
+ : (t <= 19.0
+ ? 0.0
+ : std::clamp(
+ 0.54320678911 * std::log(t - 10.0) - 1.19625408914,
+ 0.0,
+ 1.0));
+
+ return {static_cast(r), static_cast(g), static_cast(b)};
+}
+
+} // namespace ZShell::services
diff --git a/Plugins/ZShell/HyprPlugins/main.cpp b/Plugins/ZShell/HyprPlugins/main.cpp
new file mode 100644
index 0000000..8e8f2e4
--- /dev/null
+++ b/Plugins/ZShell/HyprPlugins/main.cpp
@@ -0,0 +1,447 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define private public
+#include
+#include
+#include
+#undef private
+
+#include
+
+extern "C" {
+#include
+#include
+}
+
+#include "colortemp.hpp"
+
+using ZShell::services::kelvinToGain;
+
+using Render::IFramebuffer;
+using Render::GL::CHyprOpenGLImpl;
+using Render::GL::g_pHyprOpenGL;
+
+inline HANDLE PHANDLE = nullptr;
+
+static std::filesystem::path g_runtimeDir{"/tmp"};
+static std::filesystem::path g_cacheBase{"/tmp/.cache"};
+
+static void initEnvPaths() {
+ if (const char* env = getenv("XDG_RUNTIME_DIR")) g_runtimeDir = env;
+
+ if (const char* env = getenv("XDG_CACHE_HOME")) {
+ if (*env) g_cacheBase = env;
+ } else if (const char* home = getenv("HOME")) {
+ g_cacheBase = std::filesystem::path(home) / ".cache";
+ }
+}
+
+static std::ofstream& debugLog() {
+ static std::ofstream log(
+ g_runtimeDir / "zshell-nightlight-debug.log", std::ios::app);
+ return log;
+}
+
+static std::mutex g_logMutex;
+static std::atomic g_logSeq{0};
+
+static void dlog(const std::string& msg) {
+ std::lock_guard lk(g_logMutex);
+ auto& log = debugLog();
+ log << "[" << g_logSeq++ << "][tid=" << std::this_thread::get_id() << "] "
+ << msg << std::endl;
+}
+
+static constexpr int BAKED_STEPS = 24;
+
+static std::string glslFloat(float v) {
+ std::ostringstream ss;
+ ss << std::fixed << std::setprecision(6) << v;
+ return ss.str();
+}
+
+static std::filesystem::path cacheDir() {
+ return g_cacheBase / "zshell" / "night-light";
+}
+
+static std::filesystem::path shaderPath() {
+ return cacheDir() / "nightlight.frag";
+}
+
+static std::filesystem::path bakedShaderPath(const std::array& gain) {
+ return cacheDir() /
+ ("nightlight-" + glslFloat(gain[0]) + "-" + glslFloat(gain[1]) +
+ "-" + glslFloat(gain[2]) + ".frag");
+}
+
+static std::string defaultShaderSource() {
+ return "#version 100\n"
+ "precision highp float;\n"
+ "varying vec2 v_texcoord;\n"
+ "uniform sampler2D tex;\n"
+ "uniform vec3 tint;\n"
+ "void main() {\n"
+ " vec4 pixColor = texture2D(tex, v_texcoord);\n"
+ " gl_FragColor = vec4(pixColor.rgb * tint, pixColor.a);\n"
+ "}\n";
+}
+
+static std::string bakedShaderSource(const std::array& gain) {
+ std::ostringstream src;
+ src << "#version 100\n"
+ "precision highp float;\n"
+ "varying vec2 v_texcoord;\n"
+ "uniform sampler2D tex;\n"
+ "const vec3 GAIN = vec3("
+ << glslFloat(gain[0]) << "," << glslFloat(gain[1]) << ","
+ << glslFloat(gain[2])
+ << ");\n"
+ "void main() {\n"
+ " vec4 pixColor = texture2D(tex, v_texcoord);\n"
+ " gl_FragColor = vec4(pixColor.rgb * GAIN, pixColor.a);\n"
+ "}\n";
+ return src.str();
+}
+
+static void ensureShaderFile(
+ const std::filesystem::path& path, const std::string& source) {
+ std::error_code ec;
+ if (std::filesystem::is_regular_file(path, ec)) return;
+ std::filesystem::create_directories(path.parent_path(), ec);
+ std::ofstream out(path, std::ios::trunc);
+ out << source;
+}
+
+static bool g_active = false;
+static bool g_uniformMode = false;
+static bool g_modeDecided = false;
+static std::array g_currentGain{1.f, 1.f, 1.f};
+
+static int g_currentKelvin = 6500;
+static int g_lastKelvin = 6500;
+
+struct STransition {
+ bool active = false;
+ std::chrono::steady_clock::time_point start;
+ float durationSec = 0.4f;
+ std::array fromGain{1.f, 1.f, 1.f};
+ std::array toGain{1.f, 1.f, 1.f};
+ int lastBakedStep = -1;
+};
+static STransition g_transition;
+
+static void postStateEvent(bool enabled, bool animating);
+
+static bool gainIsNeutral(const std::array& gain) {
+ for (float v : gain)
+ if (std::abs(v - 1.f) > 0.001f) return false;
+ return true;
+}
+
+static CFunctionHook* g_pBeginHook = nullptr;
+using origBegin = void (*)(
+ CHyprOpenGLImpl*,
+ PHLMONITOR,
+ const CRegion&,
+ SP,
+ std::optional);
+
+static CFunctionHook* g_pSaveMirrorHook = nullptr;
+using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
+
+static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
+ const bool saved = thisptr->m_applyFinalShader;
+ thisptr->m_applyFinalShader = false;
+ const bool result = (*reinterpret_cast(
+ g_pSaveMirrorHook->m_original))(thisptr, box);
+ thisptr->m_applyFinalShader = saved;
+ return result;
+}
+
+static void installShader() {
+ auto* gl = g_pHyprOpenGL.get();
+ if (!gl) return;
+
+ ensureShaderFile(shaderPath(), defaultShaderSource());
+ gl->applyScreenShader(shaderPath().string());
+
+ auto* shader = gl->m_finalScreenShader.get();
+ if (shader && shader->program() >= 1 &&
+ shader->getUniformLocation(SHADER_TINT) != -1) {
+ g_uniformMode = true;
+ } else {
+ g_uniformMode = false;
+ if (!g_modeDecided)
+ dlog(
+ "shader has no 'tint' uniform, falling back to "
+ "per-step recompiled shaders");
+ }
+ g_modeDecided = true;
+}
+
+static void setUniformGain(const std::array& gain) {
+ auto* gl = g_pHyprOpenGL.get();
+ auto* shader = gl ? gl->m_finalScreenShader.get() : nullptr;
+ if (!gl || !shader) return;
+
+ gl->useShader(gl->m_finalScreenShader);
+ shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
+}
+
+static void hkBegin(
+ CHyprOpenGLImpl* thisptr,
+ PHLMONITOR mon,
+ const CRegion& damage,
+ SP fb,
+ std::optional finalDamage) {
+ (*reinterpret_cast(
+ g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
+
+ if (fb) {
+ auto* renderer = g_pHyprRenderer.get();
+ if (renderer) renderer->m_renderData.blockScreenShader = true;
+ }
+
+ if (!g_active) return;
+
+ auto* gl = g_pHyprOpenGL.get();
+ if (!gl) return;
+
+ if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
+ installShader();
+ if (!gl->m_finalScreenShader.get() ||
+ gl->m_finalScreenShader->program() < 1)
+ return;
+
+ if (g_transition.active) {
+ auto* monPtr = mon.get();
+ if (!monPtr) return;
+ const float elapsed =
+ std::chrono::duration(
+ std::chrono::steady_clock::now() - g_transition.start)
+ .count();
+ const float t =
+ std::clamp(elapsed / g_transition.durationSec, 0.f, 1.f);
+
+ for (size_t i = 0; i < 3; ++i)
+ g_currentGain[i] =
+ g_transition.fromGain[i] +
+ (g_transition.toGain[i] - g_transition.fromGain[i]) * t;
+
+ if (g_uniformMode) {
+ setUniformGain(g_currentGain);
+ } else {
+ const int step = static_cast(t * BAKED_STEPS);
+ if (step != g_transition.lastBakedStep) {
+ g_transition.lastBakedStep = step;
+ ensureShaderFile(
+ bakedShaderPath(g_currentGain),
+ bakedShaderSource(g_currentGain));
+ gl->applyScreenShader(bakedShaderPath(g_currentGain).string());
+ }
+ }
+
+ if (t >= 1.f) {
+ g_transition.active = false;
+ if (gainIsNeutral(g_currentGain)) {
+ g_active = false;
+ gl->applyScreenShader("");
+ }
+ postStateEvent(g_active, false);
+ } else {
+ monPtr->m_forceFullFrames = 2;
+ monPtr->scheduleFrame();
+ }
+ } else if (g_uniformMode) {
+ setUniformGain(g_currentGain);
+ }
+}
+
+static constexpr int KELVIN_MIN = 2000;
+static constexpr int KELVIN_MAX = 6500;
+
+static int luaNightlightSet(lua_State* L) {
+ const int rawKelvin = static_cast(luaL_checknumber(L, 1));
+ const float durationSec =
+ lua_gettop(L) >= 2 ? static_cast(luaL_checknumber(L, 2)) : 0.4f;
+
+ const int targetKelvin = std::clamp(rawKelvin, KELVIN_MIN, KELVIN_MAX);
+ if (targetKelvin != rawKelvin)
+ dlog(
+ "clamped " + std::to_string(rawKelvin) + "K to " +
+ std::to_string(targetKelvin) + "K");
+
+ g_transition.fromGain = g_currentGain;
+ g_transition.toGain = kelvinToGain(targetKelvin);
+ g_transition.active = true;
+ g_transition.start = std::chrono::steady_clock::now();
+ g_transition.durationSec = durationSec;
+ g_transition.lastBakedStep = -1;
+ g_active = true;
+
+ g_lastKelvin = g_currentKelvin;
+ g_currentKelvin = targetKelvin;
+
+ postStateEvent(true, true);
+
+ lua_pushinteger(L, targetKelvin);
+ return 1;
+}
+
+static int luaNightlightDisable(lua_State* L) {
+ if (!g_active) return 0;
+
+ const float durationSec =
+ lua_gettop(L) >= 1 ? static_cast(luaL_checknumber(L, 1)) : 0.4f;
+
+ g_transition.fromGain = g_currentGain;
+ g_transition.toGain = {1.f, 1.f, 1.f};
+ g_transition.active = true;
+ g_transition.start = std::chrono::steady_clock::now();
+ g_transition.durationSec = durationSec;
+ g_transition.lastBakedStep = -1;
+
+ g_lastKelvin = g_currentKelvin;
+ g_currentKelvin = 6500;
+
+ postStateEvent(false, true);
+
+ return 0;
+}
+
+static int luaNightlightState(lua_State* L) {
+ lua_pushstring(
+ L,
+ std::format(
+ "{{\"enabled\":{},\"animating\":{},\"last\":{},\"current\":{}}}",
+ g_active ? "true" : "false",
+ g_transition.active ? "true" : "false",
+ g_lastKelvin,
+ g_currentKelvin)
+ .c_str());
+ return 1;
+}
+
+static void postStateEvent(bool enabled, bool animating) {
+ if (!g_pEventManager) return;
+ g_pEventManager->postEvent(
+ SHyprIPCEvent{
+ .event = "zshell-nightlight",
+ .data = std::format(
+ "{},{},{},{}",
+ enabled ? 1 : 0,
+ animating ? 1 : 0,
+ g_lastKelvin,
+ g_currentKelvin)});
+}
+
+static const SFunctionMatch* findExactMethod(
+ const std::vector& methods,
+ const std::string& classAndMethod) {
+ for (const auto& m : methods) {
+ if (m.signature.find(classAndMethod) != std::string::npos) return &m;
+ }
+ return nullptr;
+}
+
+static std::string manglePrefix(
+ const std::string& className, const std::string& methodName) {
+ return std::to_string(className.size()) + className +
+ std::to_string(methodName.size()) + methodName;
+}
+
+APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
+ PHANDLE = handle;
+ initEnvPaths();
+ dlog("=== PLUGIN_INIT, build timestamp " __DATE__ " " __TIME__ " ===");
+
+ const std::string serverHash = __hyprland_api_get_hash();
+ const std::string clientHash = __hyprland_api_get_client_hash();
+
+ if (serverHash != clientHash) {
+ HyprlandAPI::addNotification(
+ PHANDLE,
+ "[zshell-nightlight] Mismatched headers! Can't proceed.",
+ CHyprColor{1.0f, 0.2f, 0.2f, 1.0f},
+ 5000);
+ throw std::runtime_error("[zshell-nightlight] version mismatch");
+ }
+
+ static const auto SAVEMIRROR_METHODS =
+ HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
+ const SFunctionMatch* saveMirrorTarget = findExactMethod(
+ SAVEMIRROR_METHODS,
+ manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
+
+ if (saveMirrorTarget) {
+ g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
+ PHANDLE,
+ saveMirrorTarget->address,
+ reinterpret_cast(&hkSaveBufferForMirror));
+ if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
+ dlog(
+ "saveBufferForMirror hook installed: " +
+ saveMirrorTarget->signature);
+ }
+
+ if (!g_pSaveMirrorHook)
+ HyprlandAPI::addNotification(
+ PHANDLE,
+ "[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
+ "captures may still show the nightlight",
+ CHyprColor{1.f, 0.6f, 0.2f, 1.f},
+ 8000);
+
+ static const auto METHODS =
+ HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
+ const SFunctionMatch* target =
+ findExactMethod(METHODS, manglePrefix("CHyprOpenGLImpl", "begin"));
+ if (!target)
+ throw std::runtime_error(
+ "[zshell-nightlight] couldn't uniquely resolve "
+ "CHyprOpenGLImpl::begin");
+
+ g_pBeginHook = HyprlandAPI::createFunctionHook(
+ PHANDLE, target->address, reinterpret_cast(&hkBegin));
+ if (!g_pBeginHook || !g_pBeginHook->hook())
+ throw std::runtime_error("[zshell-nightlight] failed to hook begin()");
+
+ if (!HyprlandAPI::addLuaFunction(
+ PHANDLE, "zshell", "nlSet", luaNightlightSet))
+ throw std::runtime_error(
+ "[zshell-nightlight] failed to register Lua function");
+
+ if (!HyprlandAPI::addLuaFunction(
+ PHANDLE, "zshell", "nlDisable", luaNightlightDisable))
+ throw std::runtime_error(
+ "[zshell-nightlight] failed to register Lua function");
+
+ if (!HyprlandAPI::addLuaFunction(
+ PHANDLE, "zshell", "nlState", luaNightlightState))
+ throw std::runtime_error(
+ "[zshell-nightlight] failed to register Lua function");
+
+ return {
+ "zshell-nightlight",
+ "Animated nightlight, excluded from screencopy",
+ "ZShell",
+ "1.2"};
+}
+
+
+APICALL EXPORT std::string PLUGIN_API_VERSION() {
+ return HYPRLAND_API_VERSION;
+}
+
+
+APICALL EXPORT void PLUGIN_EXIT() {}
diff --git a/Plugins/ZShell/Llm/CMakeLists.txt b/Plugins/ZShell/Llm/CMakeLists.txt
new file mode 100644
index 0000000..bca7ec0
--- /dev/null
+++ b/Plugins/ZShell/Llm/CMakeLists.txt
@@ -0,0 +1,314 @@
+# cmark-gfm: markdown -> block structure (pkg-config; no CMake config
+# package is installed).
+pkg_check_modules(CMARK_GFM REQUIRED IMPORTED_TARGET libcmark-gfm)
+
+# tree-sitter runtime for code block highlighting (grammars are
+# dlopen()'d at runtime and optional).
+pkg_check_modules(TREE_SITTER REQUIRED IMPORTED_TARGET tree-sitter)
+
+# JKQTMathText (JKQtPlotter) for LaTeX rendering. The config files live
+# in a shared JKQTPlotter6 directory, not one named after the package.
+find_path(JKQTPlotter6_CMAKE_DIR
+ NAMES JKQTMathText6Config.cmake
+ HINTS /usr/lib/cmake/JKQTPlotter6 /usr/local/lib/cmake/JKQTPlotter6
+ DOC "Directory containing the JKQtPlotter cmake package files")
+find_package(JKQTMathText6 REQUIRED PATHS "${JKQTPlotter6_CMAKE_DIR}")
+
+# --- tree-sitter grammar discovery (configure time) ---
+#
+# Discover installed tree-sitter grammars — system packages
+# (libtree-sitter-.so) and the parsers Neovim's nvim-treesitter
+# installs (~/.local/share/nvim/site/parser/*.so) — and pair each with
+# highlight queries. Query sources, in priority order:
+# 1. vendored highlight-queries/*.scm (version-pinned; see the
+# per-file source headers, MIT),
+# 2. Neovim's own queries (version-matched to its parsers),
+# 3. tree-sitter/highlighting from GitHub (cached in the build dir).
+# The result is embedded as highlight-queries.hpp. Re-run cmake to pick
+# up grammars installed later.
+#
+# Candidate entry points are read from the .so with `nm` rather than
+# assumed to be tree_sitter_; a missing entry point just makes that
+# candidate fail at runtime.
+function(_ts_entry_point out file)
+ # OUTPUT_VARIABLE + OUTPUT_QUIET loses the output on CMake 4, so
+ # capture through a temp file.
+ get_filename_component(_ts_nm_base "${file}" NAME)
+ set(_ts_nm_file "${CMAKE_CURRENT_BINARY_DIR}/ts-entry-${_ts_nm_base}")
+ execute_process(
+ COMMAND nm -D --defined-only "${file}"
+ RESULT_VARIABLE _ts_nm_rc
+ OUTPUT_FILE "${_ts_nm_file}"
+ ERROR_FILE "${_ts_nm_file}.err")
+ set(_ts_sym "tree_sitter_missing")
+ if(_ts_nm_rc EQUAL 0 AND EXISTS "${_ts_nm_file}")
+ file(READ "${_ts_nm_file}" _ts_nm_out)
+ file(REMOVE "${_ts_nm_file}" "${_ts_nm_file}.err")
+ # The library also exports the external scanner functions; the
+ # entry point is the one without the _external suffix.
+ string(REGEX MATCHALL " T tree_sitter_[A-Za-z0-9_]+" _ts_syms "${_ts_nm_out}")
+ foreach(_ts_s IN LISTS _ts_syms)
+ string(REPLACE " T " "" _ts_s "${_ts_s}")
+ if(NOT _ts_s MATCHES "_external")
+ set(_ts_sym "${_ts_s}")
+ break()
+ endif()
+ endforeach()
+ endif()
+ set(${out} "${_ts_sym}" PARENT_SCOPE)
+endfunction()
+
+# Append a query text to (PARENT_SCOPE) unless it duplicates a
+# hash in . The text is written to the build tree right away:
+# the s-expression `;` comments would split CMake list items and the
+# query `(` `)` break bracket arguments, so query text only ever lives
+# in variables and files, never in lists.
+# Expand Neovim's "; inherits:" directives by appending the inherited
+# query set's highlights file (recursively; a visited set prevents
+# cycles). Several languages are stubs that inherit the real query
+# (html inherits html_tags, qmljs inherits ecma, ...).
+function(_ts_expand_inherits query_dir text outVar)
+ set(result "${text}")
+ set(_visited "")
+ set(_depth 0)
+ while(_depth LESS 8)
+ # Do not match the leading `;`: a MATCHALL result that itself
+ # contains a semicolon is re-split into list items.
+ string(REGEX MATCHALL "inherits:[ \t]*[A-Za-z0-9_]+" _inh "${result}")
+ if(NOT _inh)
+ break()
+ endif()
+ set(_added FALSE)
+ foreach(_entry IN LISTS _inh)
+ string(REGEX REPLACE "^inherits:[ \t]*" "" _name "${_entry}")
+ set(_file "${query_dir}/${_name}/highlights.scm")
+ if(NOT EXISTS "${_file}" OR _file IN_LIST _visited)
+ continue()
+ endif()
+ list(APPEND _visited "${_file}")
+ file(READ "${_file}" _inh_text)
+ string(APPEND result "\n${_inh_text}")
+ set(_added TRUE)
+ endforeach()
+ if(NOT _added)
+ break()
+ endif()
+ math(EXPR _depth "${_depth} + 1")
+ endwhile()
+ set(${outVar} "${result}" PARENT_SCOPE)
+endfunction()
+
+function(_ts_add_query sid text filesVar hashesVar)
+ # filesVar/hashesVar hold the caller's variable names.
+ set(files "${${filesVar}}")
+ set(hashes "${${hashesVar}}")
+ string(SHA256 _ts_qh "${text}")
+ if(_ts_qh IN_LIST hashes)
+ return()
+ endif()
+ list(APPEND hashes "${_ts_qh}")
+ list(LENGTH files _ts_qi)
+ set(_ts_qfile "${CMAKE_CURRENT_BINARY_DIR}/ts-queries/${sid}_${_ts_qi}.scm")
+ file(WRITE "${_ts_qfile}" "${text}")
+ list(APPEND files "${_ts_qfile}")
+ set(${filesVar} "${files}" PARENT_SCOPE)
+ set(${hashesVar} "${hashes}" PARENT_SCOPE)
+endfunction()
+
+set(_ts_candidates "") # entries: |||
+file(GLOB _ts_sys_files
+ "/usr/lib/libtree-sitter-*.so" "/usr/local/lib/libtree-sitter-*.so")
+foreach(_ts_file IN LISTS _ts_sys_files)
+ get_filename_component(_ts_name "${_ts_file}" NAME)
+ string(REGEX REPLACE "^libtree-sitter-(.+)\.so$" "\\1" _ts_id "${_ts_name}")
+ string(REPLACE "-" "_" _ts_sid "${_ts_id}")
+ _ts_entry_point(_ts_sym "${_ts_file}")
+ list(APPEND _ts_candidates
+ "${_ts_id}|${_ts_sid}|libtree-sitter-${_ts_id}.so|${_ts_sym}")
+endforeach()
+set(_ts_nvim_parser_dirs
+ "$ENV{HOME}/.local/share/nvim/site/parser"
+ "$ENV{HOME}/.local/share/nvim/runtime/parser"
+ "$ENV{HOME}/.local/share/nvim/lazy/nvim-treesitter/parser")
+set(_ts_nvim_query_dirs
+ "$ENV{HOME}/.local/share/nvim/site/queries"
+ "$ENV{HOME}/.local/share/nvim/lazy/nvim-treesitter/runtime/queries")
+foreach(_ts_dir IN LISTS _ts_nvim_parser_dirs)
+ file(GLOB _ts_dir_files "${_ts_dir}/*.so")
+ foreach(_ts_file IN LISTS _ts_dir_files)
+ get_filename_component(_ts_name "${_ts_file}" NAME)
+ string(REGEX REPLACE "\\.so$" "" _ts_id "${_ts_name}")
+ string(REPLACE "-" "_" _ts_sid "${_ts_id}")
+ _ts_entry_point(_ts_sym "${_ts_file}")
+ list(APPEND _ts_candidates
+ "${_ts_id}|${_ts_sid}|${_ts_file}|${_ts_sym}")
+ endforeach()
+endforeach()
+
+set(_ts_ids "")
+foreach(_ts_c IN LISTS _ts_candidates)
+ string(REPLACE "|" ";" _ts_parts "${_ts_c}")
+ list(GET _ts_parts 0 _ts_id)
+ if(NOT _ts_id IN_LIST _ts_ids)
+ list(APPEND _ts_ids "${_ts_id}")
+ endif()
+endforeach()
+list(SORT _ts_ids)
+
+set(HIGHLIGHT_QUERIES_HPP "${CMAKE_CURRENT_BINARY_DIR}/highlight-queries.hpp")
+set(_hl_header
+"#pragma once\n\n// Generated by CMake. Discoverd tree-sitter grammars and their\n// highlight queries: vendored highlight-queries/*.scm (MIT), Neovim\n// nvim-treesitter queries, and tree-sitter/highlighting (MIT).\nnamespace ZShell::llm::hq {\nstruct Candidate { const char* lib; const char* symbol; };\nstruct Grammar { const char* id; int nCandidates; const Candidate* candidates; int nQueries; const char* const* queries; };\n")
+set(_ts_grammar_rows "")
+set(_ts_vendored
+ c cpp python javascript typescript tsx bash json rust go yaml toml sql)
+foreach(_ts_id IN LISTS _ts_ids)
+ string(REPLACE "-" "_" _ts_sid "${_ts_id}")
+
+ # Query candidates in priority order (deduped; identical texts are
+ # skipped, the nvim site and plugin copies are usually the same
+ # file).
+ set(_ts_qfiles "")
+ set(_ts_qhashes "")
+ if(_ts_id STREQUAL "cpp")
+ # The C++ grammar is a superset of C and its query only covers
+ # the C++ delta; base C coverage comes from the C query.
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/c.scm" _ts_qa)
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/cpp.scm" _ts_qb)
+ _ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes)
+ elseif(_ts_id STREQUAL "typescript" OR _ts_id STREQUAL "tsx")
+ # The TS grammars reuse the JS node names; the JS query usually
+ # compiles against them and gives full coverage.
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/javascript.scm" _ts_qa)
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/typescript.scm" _ts_qb)
+ _ts_add_query(${_ts_sid} "${_ts_qa}\n${_ts_qb}" _ts_qfiles _ts_qhashes)
+ _ts_add_query(${_ts_sid} "${_ts_qb}" _ts_qfiles _ts_qhashes)
+ elseif(_ts_id IN_LIST _ts_vendored)
+ file(READ
+ "${CMAKE_CURRENT_SOURCE_DIR}/highlight-queries/${_ts_id}.scm" _ts_q)
+ _ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
+ endif()
+ foreach(_ts_qd IN LISTS _ts_nvim_query_dirs)
+ if(EXISTS "${_ts_qd}/${_ts_id}/highlights.scm")
+ file(READ "${_ts_qd}/${_ts_id}/highlights.scm" _ts_q)
+ _ts_expand_inherits("${_ts_qd}" "${_ts_q}" _ts_q)
+ _ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
+ endif()
+ endforeach()
+ if(NOT _ts_qfiles)
+ # Last resort: fetch from the tree-sitter/highlighting repo.
+ # Version-skewed against locally installed grammars, so only
+ # used when nothing local exists. Cached; offline builds simply
+ # drop the language.
+ set(_ts_dl "${CMAKE_CURRENT_BINARY_DIR}/ts-query-downloads/${_ts_sid}.scm")
+ if(NOT EXISTS "${_ts_dl}")
+ file(DOWNLOAD
+ "https://raw.githubusercontent.com/tree-sitter/highlighting/main/queries/${_ts_id}/highlight.scm"
+ "${_ts_dl}" STATUS _ts_dl_status TIMEOUT 30)
+ list(GET _ts_dl_status 0 _ts_dl_rc)
+ if(NOT _ts_dl_rc EQUAL 0)
+ file(REMOVE "${_ts_dl}")
+ endif()
+ endif()
+ if(EXISTS "${_ts_dl}")
+ file(READ "${_ts_dl}" _ts_q)
+ _ts_add_query(${_ts_sid} "${_ts_q}" _ts_qfiles _ts_qhashes)
+ endif()
+ endif()
+ list(LENGTH _ts_qfiles _ts_nq)
+ if(_ts_nq EQUAL 0)
+ continue()
+ endif()
+
+ # Emit query sources.
+ set(_ts_qn 0)
+ set(_ts_q_ptrs "")
+ foreach(_ts_qfile IN LISTS _ts_qfiles)
+ file(READ "${_ts_qfile}" _ts_q)
+ string(APPEND _hl_header
+ "inline constexpr const char* q_${_ts_sid}_${_ts_qn} = R\"ZSQUERY(${_ts_q})ZSQUERY\";\n")
+ string(APPEND _ts_q_ptrs "q_${_ts_sid}_${_ts_qn}, ")
+ math(EXPR _ts_qn "${_ts_qn} + 1")
+ endforeach()
+ string(APPEND _hl_header
+ "inline constexpr const char* const q_${_ts_sid}[] = { ${_ts_q_ptrs} };\n")
+
+ # Emit library candidates (system package first, then Neovim).
+ string(APPEND _hl_header "inline constexpr Candidate cand_${_ts_sid}[] = {\n")
+ set(_ts_nc 0)
+ foreach(_ts_c IN LISTS _ts_candidates)
+ string(REPLACE "|" ";" _ts_parts "${_ts_c}")
+ list(GET _ts_parts 0 _ts_cid)
+ if(_ts_cid STREQUAL _ts_id)
+ list(GET _ts_parts 2 _ts_lib)
+ list(GET _ts_parts 3 _ts_sym)
+ string(APPEND _hl_header
+ " { R\"ZSLIB(${_ts_lib})ZSLIB\", R\"ZSSYM(${_ts_sym})ZSSYM\" },\n")
+ math(EXPR _ts_nc "${_ts_nc} + 1")
+ endif()
+ endforeach()
+ string(APPEND _hl_header "};\n")
+ if(_ts_nc EQUAL 0)
+ # Queries but no library: pointless, drop the language.
+ string(APPEND _hl_header "") # (arrays stay; grammar row is skipped)
+ continue()
+ endif()
+
+ list(APPEND _ts_grammar_rows
+ "{ \"${_ts_id}\", ${_ts_nc}, cand_${_ts_sid}, ${_ts_nq}, q_${_ts_sid} },")
+endforeach()
+string(APPEND _hl_header "inline constexpr Grammar grammars[] = {\n")
+foreach(_ts_row IN LISTS _ts_grammar_rows)
+ string(APPEND _hl_header " ${_ts_row}\n")
+endforeach()
+string(APPEND _hl_header "};\n}\n")
+file(WRITE "${HIGHLIGHT_QUERIES_HPP}" "${_hl_header}")
+
+# Embed the vendored Latin Modern fonts (GUST Font License; provenance
+# in fonts/latinmodern/GUST-FONT-LICENSE.txt) as byte arrays.
+set(LM_FONTS
+ lmroman10-regular
+ lmroman10-italic
+ lmroman10-bold
+ lmroman10-bolditalic
+ latinmodern-math)
+set(LM_FONTS_HPP "${CMAKE_CURRENT_BINARY_DIR}/latinmodern-fonts.hpp")
+set(_lm_header "#pragma once\n\n// Vendored Latin Modern fonts (GUST Font License; see\n// fonts/latinmodern/GUST-FONT-LICENSE.txt).\nnamespace ZShell::llm::lmfont {\n")
+foreach(_lm_font IN LISTS LM_FONTS)
+ string(REPLACE "-" "_" _lm_sym "${_lm_font}")
+ file(READ "${CMAKE_CURRENT_SOURCE_DIR}/fonts/latinmodern/${_lm_font}.otf" _lm_hex HEX)
+ string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _lm_bytes "${_lm_hex}")
+ string(APPEND _lm_header "inline const unsigned char ${_lm_sym}[] = { ${_lm_bytes} };\n")
+endforeach()
+string(APPEND _lm_header "}\n")
+file(WRITE "${LM_FONTS_HPP}" "${_lm_header}")
+
+qml_module(ZShell-llm
+ URI ZShell.Llm
+ SOURCES
+ chat.hpp chat.cpp
+ chatstore.hpp chatstore.cpp
+ codehighlighter.hpp codehighlighter.cpp
+ generation.hpp generation.cpp
+ llmclient.hpp llmclient.cpp
+ markdownblock.hpp
+ markdownparser.hpp markdownparser.cpp
+ mathtext.hpp mathtext.cpp
+ message.hpp message.cpp
+ messagemodel.hpp messagemodel.cpp
+ segment.hpp segment.cpp
+ session.hpp session.cpp
+ tool.hpp tool.cpp
+ webfetchtool.hpp webfetchtool.cpp
+ LIBRARIES
+ Qt::Network
+ Qt::Sql
+ Qt::Gui
+ Qt::Widgets
+ ZShell-config
+ JKQTPlotter::JKQTMathText
+ PkgConfig::CMARK_GFM
+ PkgConfig::TREE_SITTER
+)
+
+target_include_directories(ZShell-llm PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+target_include_directories(ZShell-llm PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../Config)
diff --git a/Plugins/ZShell/Llm/chat.cpp b/Plugins/ZShell/Llm/chat.cpp
new file mode 100644
index 0000000..b7adb9b
--- /dev/null
+++ b/Plugins/ZShell/Llm/chat.cpp
@@ -0,0 +1,167 @@
+#include "chat.hpp"
+
+#include "config.hpp"
+#include "llm.hpp"
+#include "llmclient.hpp"
+#include "webfetchtool.hpp"
+
+#include
+
+namespace ZShell::llm {
+
+Chat::Chat(QObject* parent)
+ : QObject(parent)
+ , m_store(new ChatStore(this))
+ , m_client(new LlmClient(this)) {
+ if (!config::Config::instance()) new config::Config();
+
+ m_store->setLlmClient(m_client);
+ m_client->tools()->registerTool(new WebFetchTool(m_client->tools()));
+
+ const auto* llm = config::Config::instance()->llm();
+ m_client->setEndpoint(llm->endpoint());
+ m_client->setModel(llm->model());
+ m_client->setTemperature(llm->temperature());
+ m_client->setToolsEnabled(llm->tools());
+
+ connect(llm, &config::Llm::endpointChanged, this, [this, llm]() {
+ m_client->setEndpoint(llm->endpoint());
+ });
+ connect(llm, &config::Llm::modelChanged, this, [this, llm]() {
+ m_client->setModel(llm->model());
+ });
+ connect(llm, &config::Llm::temperatureChanged, this, [this, llm]() {
+ m_client->setTemperature(llm->temperature());
+ });
+ connect(llm, &config::Llm::toolsChanged, this, [this, llm]() {
+ m_client->setToolsEnabled(llm->tools());
+ });
+
+ connect(m_client, &LlmClient::busyChanged, this, [this]() {
+ // A fresh run supersedes the previous error.
+ if (m_client->busy() && !m_lastError.isEmpty()) {
+ m_lastError.clear();
+ Q_EMIT lastErrorChanged();
+ }
+ Q_EMIT busyChanged();
+ });
+ connect(m_client, &LlmClient::endpointChanged, this, &Chat::endpointChanged);
+ connect(m_client, &LlmClient::modelChanged, this, &Chat::modelChanged);
+ connect(
+ m_client,
+ &LlmClient::availableModelsChanged,
+ this,
+ &Chat::availableModelsChanged);
+ connect(
+ m_client,
+ &LlmClient::contextSizeChanged,
+ this,
+ &Chat::contextSizeChanged);
+ connect(
+ m_client,
+ &LlmClient::streamingChatIdChanged,
+ this,
+ &Chat::streamingChatIdChanged);
+ connect(
+ m_client,
+ &LlmClient::toolsEnabledChanged,
+ this,
+ &Chat::toolsEnabledChanged);
+ connect(
+ m_client,
+ &LlmClient::errorOccurred,
+ this,
+ [this](const QString& message) {
+ m_lastError = message;
+ Q_EMIT lastErrorChanged();
+ Q_EMIT errorOccurred(message);
+ });
+ connect(
+ m_store,
+ &ChatStore::sessionRemoved,
+ this,
+ [this](ChatSession* session) { m_client->sessionRemoved(session); });
+ connect(
+ m_client,
+ &LlmClient::titleSuggested,
+ this,
+ [this](ChatSession* session, const QString& title) {
+ qInfo() << "Chat: applying generated title" << session->id()
+ << title << "(was" << session->title() << ")";
+ session->setTitle(title);
+ m_store->saveMeta(session);
+ });
+ connect(
+ m_client,
+ &LlmClient::iconSuggested,
+ this,
+ [this](ChatSession* session, const QString& icon) {
+ qInfo() << "Chat: applying generated icon" << session->id() << icon
+ << "(was" << session->icon() << ")";
+ session->setIcon(icon);
+ m_store->saveMeta(session);
+ });
+}
+
+bool Chat::busy() const {
+ return m_client->busy();
+}
+
+QString Chat::endpoint() const {
+ return m_client->endpoint();
+}
+
+QString Chat::model() const {
+ return m_client->model();
+}
+
+QStringList Chat::availableModels() const {
+ return m_client->availableModels();
+}
+
+int Chat::contextSize() const {
+ return m_client->contextSize();
+}
+
+bool Chat::toolsEnabled() const {
+ return m_client->toolsEnabled();
+}
+
+void Chat::setToolsEnabled(bool value) {
+ m_client->setToolsEnabled(value);
+ if (auto* config = config::Config::instance())
+ config->llm()->set_tools(value);
+}
+
+QString Chat::streamingChatId() const {
+ return m_client->streamingChatId();
+}
+
+Chat* Chat::s_instance = nullptr;
+
+Chat* Chat::create(QQmlEngine*, QJSEngine*) {
+ if (!s_instance) s_instance = new Chat();
+ return s_instance;
+}
+
+void Chat::stop() {
+ m_client->stop();
+}
+
+void Chat::dismissError() {
+ if (m_lastError.isEmpty()) return;
+ m_lastError.clear();
+ Q_EMIT lastErrorChanged();
+}
+
+void Chat::refreshModels() {
+ m_client->refreshModels();
+}
+
+void Chat::selectModel(const QString& id) {
+ if (id.isEmpty()) return;
+ m_client->setModel(id);
+ if (auto* config = config::Config::instance()) config->llm()->set_model(id);
+}
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/chat.hpp b/Plugins/ZShell/Llm/chat.hpp
new file mode 100644
index 0000000..276b4a3
--- /dev/null
+++ b/Plugins/ZShell/Llm/chat.hpp
@@ -0,0 +1,81 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "chatstore.hpp"
+
+class QQmlEngine;
+class QJSEngine;
+
+namespace ZShell::llm {
+
+class LlmClient;
+
+// QML-facing facade. Persistence lives in ChatStore, network streaming in
+// LlmClient; this class only wires them together and exposes the
+// application-wide state.
+class Chat : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_SINGLETON
+
+ Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
+ Q_PROPERTY(QString endpoint READ endpoint NOTIFY endpointChanged)
+ Q_PROPERTY(QString model READ model NOTIFY modelChanged)
+ Q_PROPERTY(
+ QStringList availableModels READ availableModels NOTIFY
+ availableModelsChanged)
+ Q_PROPERTY(int contextSize READ contextSize NOTIFY contextSizeChanged)
+ Q_PROPERTY(
+ bool toolsEnabled READ toolsEnabled WRITE setToolsEnabled NOTIFY
+ toolsEnabledChanged)
+ Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
+ Q_PROPERTY(ZShell::llm::ChatStore* chats READ chats CONSTANT)
+ Q_PROPERTY(
+ QString streamingChatId READ streamingChatId NOTIFY
+ streamingChatIdChanged)
+
+ public:
+ explicit Chat(QObject* parent = nullptr);
+
+ [[nodiscard]] bool busy() const;
+ [[nodiscard]] QString endpoint() const;
+ [[nodiscard]] QString model() const;
+ [[nodiscard]] QStringList availableModels() const;
+ [[nodiscard]] int contextSize() const;
+ [[nodiscard]] bool toolsEnabled() const;
+ void setToolsEnabled(bool value);
+ [[nodiscard]] QString lastError() const { return m_lastError; }
+ [[nodiscard]] ChatStore* chats() const { return m_store; }
+ [[nodiscard]] QString streamingChatId() const;
+
+ Q_INVOKABLE void stop();
+ Q_INVOKABLE void dismissError();
+ Q_INVOKABLE void refreshModels();
+ Q_INVOKABLE void selectModel(const QString& id);
+
+ static Chat* create(QQmlEngine*, QJSEngine*);
+
+ Q_SIGNALS:
+ void busyChanged();
+ void endpointChanged();
+ void modelChanged();
+ void availableModelsChanged();
+ void contextSizeChanged();
+ void toolsEnabledChanged();
+ void errorOccurred(const QString& message);
+ void lastErrorChanged();
+ void streamingChatIdChanged();
+
+ private:
+ ChatStore* m_store = nullptr;
+ LlmClient* m_client = nullptr;
+ QString m_lastError;
+
+ static Chat* s_instance;
+};
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/chatstore.cpp b/Plugins/ZShell/Llm/chatstore.cpp
new file mode 100644
index 0000000..3d9056f
--- /dev/null
+++ b/Plugins/ZShell/Llm/chatstore.cpp
@@ -0,0 +1,615 @@
+#include "chatstore.hpp"
+
+#include "llmclient.hpp"
+#include "message.hpp"
+#include "segment.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace ZShell::llm {
+
+namespace {
+
+QString segmentTypeName(LlmSegment::Type type) {
+ switch (type) {
+ case LlmSegment::Type::Reasoning:
+ return QStringLiteral("reasoning");
+ case LlmSegment::Type::ToolCall:
+ return QStringLiteral("tool_call");
+ case LlmSegment::Type::Content:
+ return QStringLiteral("content");
+ }
+ return QStringLiteral("reasoning");
+}
+
+LlmSegment::Type segmentTypeFromName(const QString& name) {
+ if (name == QLatin1String("tool_call")) return LlmSegment::Type::ToolCall;
+ if (name == QLatin1String("content")) return LlmSegment::Type::Content;
+ return LlmSegment::Type::Reasoning;
+}
+
+QString sqlText(const QString& value) {
+ if (value.isNull()) return QStringLiteral("");
+ return value;
+}
+
+struct SegmentRow {
+ QString type;
+ QString text;
+ QString name;
+ QString toolCallId;
+ QString arguments;
+ QString result;
+ int status = 0;
+ qint64 elapsedMs = 0;
+ qint64 timestamp = 0;
+};
+
+struct GenerationRow {
+ qint64 timestamp = 0;
+ bool active = false;
+ QVector segments;
+};
+
+struct MessageRow {
+ bool user = false;
+ qint64 timestamp = 0;
+ QVector generations;
+};
+
+} // namespace
+
+ChatStore::ChatStore(QObject* parent)
+ : QObject(parent), m_connectionName(QUuid::createUuid().toString()) {
+ openDb();
+ load();
+}
+
+ChatStore::~ChatStore() {
+ if (m_connectionName.isEmpty()) return;
+ QSqlDatabase db = QSqlDatabase::database(m_connectionName, false);
+ db.close();
+ QSqlDatabase::removeDatabase(m_connectionName);
+}
+
+QSqlDatabase ChatStore::db() const {
+ return QSqlDatabase::database(m_connectionName);
+}
+
+void ChatStore::openDb() {
+ m_dbPath =
+ QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation) +
+ QStringLiteral("/zshell/chats.sqlite");
+ QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
+
+ QSqlDatabase db =
+ QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
+ db.setDatabaseName(m_dbPath);
+ if (!db.open()) {
+ qWarning() << "ChatStore: failed to open database" << m_dbPath << ":"
+ << db.lastError().text();
+ return;
+ }
+ {
+ QSqlQuery pragma(db);
+ pragma.exec(QStringLiteral("PRAGMA foreign_keys = ON"));
+ }
+ {
+ QSqlQuery query(db);
+ query.exec(QStringLiteral(
+ "CREATE TABLE IF NOT EXISTS sessions (\n"
+ " id TEXT PRIMARY KEY,\n"
+ " title TEXT NOT NULL DEFAULT '',\n"
+ " icon TEXT NOT NULL DEFAULT '',\n"
+ " created_at INTEGER NOT NULL,\n"
+ " updated_at INTEGER NOT NULL,\n"
+ " pinned INTEGER NOT NULL DEFAULT 0\n"
+ ")"));
+ }
+ {
+ QSqlQuery query(db);
+ query.exec(QStringLiteral(
+ "CREATE TABLE IF NOT EXISTS messages (\n"
+ " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
+ " session_id TEXT NOT NULL REFERENCES sessions (id) "
+ "ON DELETE CASCADE,\n"
+ " role TEXT NOT NULL,\n"
+ " timestamp INTEGER NOT NULL\n"
+ ")"));
+ }
+ {
+ QSqlQuery query(db);
+ query.exec(QStringLiteral(
+ "CREATE TABLE IF NOT EXISTS generations (\n"
+ " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
+ " message_id INTEGER NOT NULL REFERENCES messages "
+ "(id) ON DELETE CASCADE,\n"
+ " timestamp INTEGER NOT NULL,\n"
+ " is_active INTEGER NOT NULL DEFAULT 1\n"
+ ")"));
+ }
+ {
+ QSqlQuery query(db);
+ query.exec(QStringLiteral(
+ "CREATE TABLE IF NOT EXISTS segments (\n"
+ " id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
+ " generation_id INTEGER NOT NULL REFERENCES generations "
+ "(id) ON DELETE CASCADE,\n"
+ " type TEXT NOT NULL,\n"
+ " text TEXT NOT NULL DEFAULT '',\n"
+ " name TEXT NOT NULL DEFAULT '',\n"
+ " tool_call_id TEXT NOT NULL DEFAULT '',\n"
+ " arguments TEXT NOT NULL DEFAULT '',\n"
+ " result TEXT NOT NULL DEFAULT '',\n"
+ " status INTEGER NOT NULL DEFAULT 0,\n"
+ " elapsed_ms INTEGER NOT NULL DEFAULT 0,\n"
+ " timestamp INTEGER NOT NULL\n"
+ ")"));
+ }
+ {
+ QSqlQuery query(db);
+ query.exec(QStringLiteral(
+ "CREATE INDEX IF NOT EXISTS idx_messages_session "
+ "ON messages (session_id)"));
+ query.exec(QStringLiteral(
+ "CREATE INDEX IF NOT EXISTS idx_generations_message "
+ "ON generations (message_id)"));
+ query.exec(QStringLiteral(
+ "CREATE INDEX IF NOT EXISTS idx_segments_generation "
+ "ON segments (generation_id)"));
+ }
+}
+
+int ChatStore::count() const {
+ return static_cast(m_sessions.size());
+}
+
+QVariantList ChatStore::values() const {
+ QVariantList vals;
+ vals.reserve(m_sessions.size());
+ for (const auto* session : m_sessions)
+ vals.append(QVariant::fromValue(session));
+ return vals;
+}
+
+ChatSession* ChatStore::at(int index) const {
+ if (index < 0 || index >= m_sessions.size()) return nullptr;
+ return m_sessions.at(index);
+}
+
+ChatSession* ChatStore::insert(int index) {
+ const qint64 now = QDateTime::currentMSecsSinceEpoch();
+ const QString id = QUuid::createUuid().toString();
+ {
+ QSqlQuery query(db());
+ query.prepare(
+ "INSERT INTO sessions (id, title, created_at, updated_at) "
+ "VALUES (:id, '', :created_at, :updated_at)");
+ query.bindValue(":id", id);
+ query.bindValue(":created_at", now);
+ query.bindValue(":updated_at", now);
+ if (!query.exec())
+ qWarning() << "ChatStore: failed to insert session" << id << ":"
+ << query.lastError().text();
+ }
+ auto* session = new ChatSession(id, this);
+ session->setMeta(QString(), now, now, 0);
+ const int pos = index >= 0 && index <= m_sessions.size() ? index : 0;
+ m_sessions.insert(pos, session);
+ Q_EMIT countChanged();
+ Q_EMIT valuesChanged();
+ return session;
+}
+
+void ChatStore::remove(int index) {
+ removeSession(at(index));
+}
+
+void ChatStore::remove(ChatSession* chat) {
+ removeSession(chat);
+}
+
+void ChatStore::removeSession(ChatSession* session) {
+ if (!session || !m_sessions.contains(session)) return;
+ const QList before = m_sessions;
+ Q_EMIT sessionRemoved(session);
+ {
+ QSqlQuery query(db());
+ query.prepare("DELETE FROM sessions WHERE id = :id");
+ query.bindValue(":id", session->id());
+ query.exec();
+ }
+ m_sessions.removeOne(session);
+ session->deleteLater();
+ notify(before);
+}
+
+void ChatStore::move(int from, int to) {
+ if (from < 0 || from >= m_sessions.size() || to < 0 ||
+ to >= m_sessions.size() || from == to)
+ return;
+ m_sessions.move(from, to);
+ Q_EMIT valuesChanged();
+}
+
+void ChatStore::clear() {
+ const QList sessions = m_sessions;
+ for (ChatSession* session : sessions)
+ removeSession(session);
+}
+
+ChatSession* ChatStore::sessionById(const QString& id) {
+ for (auto* session : m_sessions)
+ if (session->id() == id) return session;
+ return nullptr;
+}
+
+void ChatStore::setLlmClient(LlmClient* client) {
+ m_llmClient = client;
+}
+
+void ChatStore::persist(ChatSession* session) {
+ if (!session || !m_sessions.contains(session)) return;
+ if (!session->isLoaded()) {
+ m_pendingPersists.insert(session);
+ return;
+ }
+ session->setUpdatedAt(QDateTime::currentMSecsSinceEpoch());
+ if (!saveSession(session)) return;
+ sortAndNotify();
+}
+
+void ChatStore::saveMeta(ChatSession* session) {
+ if (!session || !m_sessions.contains(session)) return;
+ QSqlQuery query(db());
+ query.prepare(
+ "UPDATE sessions SET title = :title, icon = :icon "
+ "WHERE id = :id");
+ query.bindValue(":title", sqlText(session->title()));
+ query.bindValue(":icon", sqlText(session->icon()));
+ query.bindValue(":id", session->id());
+ if (!query.exec())
+ qWarning() << "ChatStore: failed to save meta for" << session->id()
+ << ":" << query.lastError().text();
+}
+
+bool ChatStore::saveSession(ChatSession* session) {
+ const QString id = session->id();
+ QSqlDatabase handle = db();
+ if (!handle.transaction()) {
+ qWarning() << "ChatStore: failed to begin transaction:"
+ << handle.lastError().text();
+ return false;
+ }
+ bool ok = true;
+ {
+ QSqlQuery query(handle);
+ query.prepare(
+ "UPDATE sessions SET title = :title, updated_at = :updated_at "
+ "WHERE id = :id");
+ query.bindValue(":title", sqlText(session->title()));
+ query.bindValue(":updated_at", session->updatedAtMs());
+ query.bindValue(":id", session->id());
+ ok = query.exec();
+ }
+ if (ok) {
+ QSqlQuery query(handle);
+ query.prepare("DELETE FROM messages WHERE session_id = :id");
+ query.bindValue(":id", session->id());
+ ok = query.exec();
+ }
+ if (ok) {
+ QSqlQuery messageInsert(handle);
+ ok = messageInsert.prepare(
+ "INSERT INTO messages (session_id, role, timestamp) "
+ "VALUES (:id, :role, :timestamp)");
+ QSqlQuery generationInsert(handle);
+ ok = ok &&
+ generationInsert.prepare(
+ "INSERT INTO generations (message_id, timestamp, is_active) "
+ "VALUES (:mid, :timestamp, :is_active)");
+ QSqlQuery segmentInsert(handle);
+ ok = ok &&
+ segmentInsert.prepare(
+ "INSERT INTO segments (generation_id, type, text, name, "
+ "tool_call_id, arguments, result, status, elapsed_ms, "
+ "timestamp) VALUES (:gid, :type, :text, :name, "
+ ":tool_call_id, :arguments, :result, :status, :elapsed_ms, "
+ ":timestamp)");
+ const auto* model = session->messagesModel();
+ for (int row = model->rowCount() - 1; ok && row >= 0; --row) {
+ const auto* message = model->at(row);
+ messageInsert.bindValue(":id", session->id());
+ messageInsert.bindValue(
+ ":role",
+ message->role() == ChatMessage::Role::User
+ ? QStringLiteral("user")
+ : QStringLiteral("assistant"));
+ messageInsert.bindValue(":timestamp", message->timestamp());
+ if (!messageInsert.exec()) {
+ ok = false;
+ qWarning() << "ChatStore: saveSession" << id
+ << "message insert failed:"
+ << messageInsert.lastError().text();
+ break;
+ }
+ const int messageId = messageInsert.lastInsertId().toInt();
+ for (int i = 0; ok && i < message->generationCount(); ++i) {
+ const auto* generation = message->generation(i);
+ generationInsert.bindValue(":mid", messageId);
+ generationInsert.bindValue(
+ ":timestamp", generation->timestamp());
+ generationInsert.bindValue(
+ ":is_active",
+ i == message->activeGenerationIndex() ? 1 : 0);
+ if (!generationInsert.exec()) {
+ ok = false;
+ qWarning() << "ChatStore: saveSession" << id
+ << "generation insert failed:"
+ << generationInsert.lastError().text();
+ break;
+ }
+ const int generationId =
+ generationInsert.lastInsertId().toInt();
+ for (const auto* segment : generation->segments()) {
+ segmentInsert.bindValue(":gid", generationId);
+ segmentInsert.bindValue(
+ ":type", segmentTypeName(segment->type()));
+ segmentInsert.bindValue(":text", sqlText(segment->text()));
+ segmentInsert.bindValue(":name", sqlText(segment->name()));
+ segmentInsert.bindValue(
+ ":tool_call_id", sqlText(segment->toolCallId()));
+ segmentInsert.bindValue(
+ ":arguments", sqlText(segment->arguments()));
+ segmentInsert.bindValue(
+ ":result", sqlText(segment->result()));
+ segmentInsert.bindValue(
+ ":status", static_cast(segment->status()));
+ segmentInsert.bindValue(":elapsed_ms", segment->elapsedMs());
+ segmentInsert.bindValue(":timestamp", segment->timestamp());
+ if (!segmentInsert.exec()) {
+ ok = false;
+ qWarning() << "ChatStore: saveSession" << id
+ << "segment insert failed:"
+ << segmentInsert.lastError().text();
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!ok || !handle.commit()) {
+ qWarning() << "ChatStore: saveSession" << id
+ << "commit failed, rolling back";
+ handle.rollback();
+ ok = false;
+ qWarning() << "ChatStore: failed to save session" << session->id()
+ << ":" << handle.lastError().text();
+ }
+ return ok;
+}
+void ChatStore::loadMessagesInto(ChatSession* session) {
+ if (!session) return;
+ const QString sessionId = session->id();
+ const QString path = m_dbPath;
+
+ QThreadPool::globalInstance()->start([store = QPointer(this),
+ session =
+ QPointer(session),
+ sessionId,
+ path]() {
+ QList rows;
+ const QString connName = QUuid::createUuid().toString();
+ {
+ QSqlDatabase db =
+ QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connName);
+ db.setDatabaseName(path);
+ if (db.open()) {
+ QSqlQuery busy(db);
+ busy.exec(QStringLiteral("PRAGMA busy_timeout = 3000"));
+ QSqlQuery query(db);
+ query.prepare(
+ "SELECT id, role, timestamp FROM messages "
+ "WHERE session_id = :id ORDER BY rowid DESC");
+ query.bindValue(":id", sessionId);
+ if (!query.exec()) {
+ qWarning() << "ChatStore: failed to load messages for"
+ << sessionId << ":" << query.lastError().text();
+ } else {
+ while (query.next()) {
+ const int messageId = query.value(0).toInt();
+ MessageRow message;
+ message.user = query.value(1).toString() ==
+ QLatin1String("user");
+ message.timestamp = query.value(2).toLongLong();
+ QSqlQuery generationQuery(db);
+ generationQuery.prepare(
+ "SELECT id, timestamp, is_active FROM "
+ "generations WHERE message_id = :mid "
+ "ORDER BY rowid");
+ generationQuery.bindValue(":mid", messageId);
+ if (generationQuery.exec()) {
+ while (generationQuery.next()) {
+ GenerationRow generation;
+ generation.timestamp =
+ generationQuery.value(1).toLongLong();
+ generation.active =
+ generationQuery.value(2).toInt() != 0;
+ QSqlQuery segmentQuery(db);
+ segmentQuery.prepare(
+ "SELECT type, text, name, tool_call_id, "
+ "arguments, result, status, elapsed_ms, "
+ "timestamp FROM segments WHERE "
+ "generation_id = :gid ORDER BY rowid");
+ segmentQuery.bindValue(
+ ":gid", generationQuery.value(0).toInt());
+ if (segmentQuery.exec()) {
+ while (segmentQuery.next()) {
+ SegmentRow segment;
+ segment.type =
+ segmentQuery.value(0).toString();
+ segment.text =
+ segmentQuery.value(1).toString();
+ segment.name =
+ segmentQuery.value(2).toString();
+ segment.toolCallId =
+ segmentQuery.value(3).toString();
+ segment.arguments =
+ segmentQuery.value(4).toString();
+ segment.result =
+ segmentQuery.value(5).toString();
+ segment.status =
+ segmentQuery.value(6).toInt();
+ segment.elapsedMs =
+ segmentQuery.value(7).toLongLong();
+ segment.timestamp =
+ segmentQuery.value(8).toLongLong();
+ generation.segments.append(segment);
+ }
+ } else {
+ qWarning()
+ << "ChatStore: failed to load "
+ "segments for generation"
+ << generationQuery.value(0).toInt()
+ << ":"
+ << segmentQuery.lastError().text();
+ }
+ message.generations.append(generation);
+ }
+ } else {
+ qWarning()
+ << "ChatStore: failed to load generations "
+ "for message"
+ << messageId << ":"
+ << generationQuery.lastError().text();
+ }
+ rows.append(message);
+ }
+ }
+ db.close();
+ } else {
+ qWarning() << "ChatStore: failed to open database for load:"
+ << db.lastError().text();
+ }
+ }
+ QSqlDatabase::removeDatabase(connName);
+
+ QMetaObject::invokeMethod(
+ QCoreApplication::instance(),
+ [store, session, rows = std::move(rows)]() mutable {
+ ChatStore* st = store;
+ ChatSession* s = session;
+ if (!st || !s) return;
+
+ auto* model = s->model();
+ if (!model) return;
+ QList messages;
+ for (const MessageRow& row : rows) {
+ auto* message = model->createMessage(
+ row.user ? ChatMessage::Role::User
+ : ChatMessage::Role::Assistant,
+ row.timestamp);
+ int activeIndex = 0;
+ for (int i = 0; i < row.generations.size(); ++i) {
+ const GenerationRow& generationRow =
+ row.generations.at(i);
+ auto* generation =
+ message->addGeneration(generationRow.timestamp);
+ for (const SegmentRow& segmentRow :
+ generationRow.segments) {
+ auto* segment = new LlmSegment(
+ segmentTypeFromName(segmentRow.type),
+ segmentRow.timestamp,
+ generation);
+ segment->setText(segmentRow.text);
+ segment->setName(segmentRow.name);
+ segment->setToolCallId(segmentRow.toolCallId);
+ segment->appendArguments(segmentRow.arguments);
+ segment->setResult(segmentRow.result);
+ segment->setStatus(
+ static_cast(
+ segmentRow.status));
+ segment->restore(segmentRow.elapsedMs);
+ generation->addSegment(segment);
+ }
+ if (generationRow.active) activeIndex = i;
+ }
+ message->setActiveGeneration(activeIndex);
+ messages.append(message);
+ }
+ if (model->rowCount() > 0) {
+ QList live = messages;
+ for (int r = 0; r < model->rowCount(); ++r)
+ live.prepend(model->at(r));
+ messages = live;
+ }
+ if (!messages.isEmpty() || model->rowCount() > 0)
+ s->adoptMessages(messages);
+
+ s->markLoaded();
+
+ if (s->takeClearPending()) {
+ s->clear();
+ } else if (st->m_pendingPersists.remove(s)) {
+ st->persist(s);
+ }
+ },
+ Qt::QueuedConnection);
+ });
+}
+
+void ChatStore::load() {
+ QSqlQuery query(db());
+ query.exec(
+ "SELECT s.id, s.title, s.icon, s.created_at, s.updated_at, s.pinned, "
+ "(SELECT COUNT(*) FROM messages m WHERE m.session_id = s.id) AS cnt "
+ "FROM sessions s ORDER BY s.pinned DESC, s.updated_at DESC");
+ while (query.next()) {
+ auto* session = new ChatSession(query.value(0).toString(), this);
+ session->setMeta(
+ query.value(1).toString(),
+ query.value(3).toLongLong(),
+ query.value(4).toLongLong(),
+ query.value(6).toInt());
+ session->setIcon(query.value(2).toString());
+ session->setPinned(query.value(5).toInt() != 0);
+ m_sessions.append(session);
+ }
+ sortAndNotify();
+}
+
+void ChatStore::sortAndNotify() {
+ const QList before = m_sessions;
+ std::stable_sort(
+ m_sessions.begin(),
+ m_sessions.end(),
+ [](const ChatSession* a, const ChatSession* b) {
+ if (a->pinned() != b->pinned()) return a->pinned() > b->pinned();
+ return a->updatedAtMs() > b->updatedAtMs();
+ });
+ notify(before);
+}
+
+void ChatStore::notify(const QList& before) {
+ if (before.size() != m_sessions.size()) Q_EMIT countChanged();
+ bool same = before.size() == m_sessions.size();
+ for (int i = 0; same && i < m_sessions.size(); ++i)
+ if (before.at(i) != m_sessions.at(i)) same = false;
+ if (!same) Q_EMIT valuesChanged();
+}
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/chatstore.hpp b/Plugins/ZShell/Llm/chatstore.hpp
new file mode 100644
index 0000000..75b335b
--- /dev/null
+++ b/Plugins/ZShell/Llm/chatstore.hpp
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "session.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace ZShell::llm {
+
+class LlmClient;
+
+class ChatStore : public QObject {
+ Q_OBJECT
+ QML_ANONYMOUS
+
+ Q_PROPERTY(int count READ count NOTIFY countChanged)
+ Q_PROPERTY(QVariantList values READ values NOTIFY valuesChanged)
+
+ public:
+ explicit ChatStore(QObject* parent = nullptr);
+ ~ChatStore() override;
+
+ [[nodiscard]] int count() const;
+ [[nodiscard]] QVariantList values() const;
+ [[nodiscard]] ChatSession* at(int index) const;
+
+ Q_INVOKABLE ZShell::llm::ChatSession* insert(int index = -1);
+ Q_INVOKABLE void remove(int index);
+ Q_INVOKABLE void remove(ZShell::llm::ChatSession* chat);
+ Q_INVOKABLE void move(int from, int to);
+ Q_INVOKABLE void clear();
+
+ [[nodiscard]] ChatSession* sessionById(const QString& id);
+ [[nodiscard]] LlmClient* llmClient() const { return m_llmClient; }
+ void setLlmClient(LlmClient* client);
+
+ void persist(ChatSession* session);
+ void saveMeta(ChatSession* session);
+ void loadMessagesInto(ChatSession* session);
+
+ Q_SIGNALS:
+ void countChanged();
+ void valuesChanged();
+ void sessionRemoved(ZShell::llm::ChatSession* session);
+
+ private:
+ void openDb();
+ void load();
+ bool saveSession(ChatSession* session);
+ void sortAndNotify();
+ void removeSession(ChatSession* session);
+ void notify(const QList& before);
+
+ QList m_sessions;
+ LlmClient* m_llmClient = nullptr;
+ QString m_connectionName;
+ QString m_dbPath;
+ QSet m_pendingPersists;
+
+ [[nodiscard]] QSqlDatabase db() const;
+};
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/codehighlighter.cpp b/Plugins/ZShell/Llm/codehighlighter.cpp
new file mode 100644
index 0000000..86e2f54
--- /dev/null
+++ b/Plugins/ZShell/Llm/codehighlighter.cpp
@@ -0,0 +1,422 @@
+#include "codehighlighter.hpp"
+
+#include "highlight-queries.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+namespace ZShell::llm {
+
+namespace hl {
+
+enum Role : uint8_t {
+ None = 0,
+ Comment,
+ String,
+ StringKey,
+ Number,
+ Constant,
+ Keyword,
+ Type,
+ Function,
+ Method,
+ Macro,
+ Preproc,
+ Operator,
+ Property,
+ Label,
+ Attribute,
+};
+
+const char* roleName(Role role) {
+ switch (role) {
+ case Comment:
+ return "comment";
+ case String:
+ return "string";
+ case StringKey:
+ return "string.key";
+ case Number:
+ return "number";
+ case Constant:
+ return "constant";
+ case Keyword:
+ return "keyword";
+ case Type:
+ return "type";
+ case Function:
+ return "function";
+ case Method:
+ return "method";
+ case Macro:
+ return "macro";
+ case Preproc:
+ return "preproc";
+ case Operator:
+ return "operator";
+ case Property:
+ return "property";
+ case Label:
+ return "label";
+ case Attribute:
+ return "attribute";
+ default:
+ return "";
+ }
+}
+
+using LanguageFn = const TSLanguage* (*)();
+
+const QHash& grammars() {
+ static const QHash grammars = [] {
+ QHash map;
+ for (const auto& g : hq::grammars) {
+ CodeHighlighter::Grammar grammar;
+ for (int i = 0; i < g.nCandidates; ++i) {
+ grammar.libs.push_back(g.candidates[i].lib);
+ grammar.symbols.push_back(g.candidates[i].symbol);
+ }
+ for (int i = 0; i < g.nQueries; ++i)
+ grammar.queries.push_back(g.queries[i]);
+ map.insert(g.id, std::move(grammar));
+ }
+ return map;
+ }();
+ return grammars;
+}
+
+} // namespace hl
+
+CodeHighlighter* CodeHighlighter::s_instance = nullptr;
+
+const QHash& CodeHighlighter::aliases() {
+ static const QHash aliases = [] {
+ QHash map;
+ map.insert("c", "c");
+ map.insert("h", "c");
+ map.insert("cpp", "cpp");
+ map.insert("c++", "cpp");
+ map.insert("cc", "cpp");
+ map.insert("cxx", "cpp");
+ map.insert("h++", "cpp");
+ map.insert("hpp", "cpp");
+ map.insert("hh", "cpp");
+ map.insert("python", "python");
+ map.insert("py", "python");
+ map.insert("javascript", "javascript");
+ map.insert("js", "javascript");
+ map.insert("jsx", "javascript");
+ map.insert("mjs", "javascript");
+ map.insert("cjs", "javascript");
+ map.insert("typescript", "typescript");
+ map.insert("ts", "typescript");
+ map.insert("mts", "typescript");
+ map.insert("cts", "typescript");
+ map.insert("tsx", "tsx");
+ map.insert("bash", "bash");
+ map.insert("sh", "bash");
+ map.insert("shell", "bash");
+ map.insert("shellscript", "bash");
+ map.insert("shell-session", "bash");
+ map.insert("zsh", "bash");
+ map.insert("console", "bash");
+ map.insert("qml", "qmljs");
+ map.insert("qmljs", "qmljs");
+ map.insert("json", "json");
+ map.insert("jsonc", "json");
+ map.insert("rust", "rust");
+ map.insert("rs", "rust");
+ map.insert("go", "go");
+ map.insert("golang", "go");
+ map.insert("yaml", "yaml");
+ map.insert("yml", "yaml");
+ map.insert("toml", "toml");
+ map.insert("sql", "sql");
+ map.insert("mysql", "sql");
+ map.insert("postgres", "sql");
+ map.insert("postgresql", "sql");
+ map.insert("sqlite", "sql");
+ map.insert("sqlite3", "sql");
+ return map;
+ }();
+ return aliases;
+}
+
+uint8_t CodeHighlighter::roleFor(const char* name, uint32_t length) {
+ const QString n = QString::fromUtf8(name, length);
+ if (n == "comment") return hl::Role::Comment;
+ if (n.startsWith("string"))
+ return n == "string.special.key" ? hl::Role::StringKey
+ : hl::Role::String;
+ if (n == "escape" || n == "regexp") return hl::Role::String;
+ if (n.startsWith("number")) return hl::Role::Number;
+ if (n.startsWith("constant") || n == "boolean" || n == "bool" ||
+ n.startsWith("character"))
+ return hl::Role::Constant;
+ if (n.startsWith("keyword")) return hl::Role::Keyword;
+ if (n == "type" || n.startsWith("type.")) return hl::Role::Type;
+ if (n.startsWith("namespace") || n.startsWith("module") ||
+ n == "support.type" || n == "support.namespace")
+ return hl::Role::Type;
+ if (n.startsWith("function") || n == "constructor" ||
+ n.startsWith("support.function"))
+ return hl::Role::Function;
+ if (n == "method" || n == "method.builtin") return hl::Role::Method;
+ if (n.startsWith("macro")) return hl::Role::Macro;
+ if (n.startsWith("preproc")) return hl::Role::Preproc;
+ if (n == "operator" || n == "punctuation.operator" ||
+ n.startsWith("operator.") || n.startsWith("punctuation"))
+ return hl::Role::Operator;
+ if (n == "property" || n == "field" || n.startsWith("property."))
+ return hl::Role::Property;
+ if (n == "label") return hl::Role::Label;
+ if (n.startsWith("attribute") || n == "annotation")
+ return hl::Role::Attribute;
+ if (n == "tag" || (n.startsWith("tag.") && n != "tag.delimiter"))
+ return hl::Role::Keyword;
+ if (n.startsWith("variable")) return hl::Role::Constant;
+ if (n.startsWith("support")) return hl::Role::Function;
+ return hl::Role::None;
+}
+
+const char* CodeHighlighter::roleName(uint8_t role) {
+ return hl::roleName(static_cast(role));
+}
+
+QString CodeHighlighter::resolveId(const QString& language) {
+ const QString tag = language.trimmed().toLower();
+ const QString alias = aliases().value(tag);
+ return alias.isEmpty() ? tag : alias; // unknown tags = grammar id
+}
+
+QString CodeHighlighter::cacheKey(const QString& id, const QString& code) {
+ return id + QLatin1Char('\x01') + QString::number(code.size()) +
+ QLatin1Char('\x01') + QString::number(qHash(code));
+}
+
+QVariantList CodeHighlighter::lookupSpans(
+ const QString& code, const QString& language) const {
+ if (code.isEmpty()) return {};
+ const QString key = cacheKey(resolveId(language), code);
+ QMutexLocker locker(&m_cacheMutex);
+ const auto it = m_spanCache.constFind(key);
+ if (it == m_spanCache.constEnd() || it->code != code) return {};
+ const qsizetype pos = m_spanCacheOrder.indexOf(key);
+ if (pos >= 0) m_spanCacheOrder.move(pos, m_spanCacheOrder.size() - 1);
+ return it->spans;
+}
+
+void CodeHighlighter::storeSpans(
+ const QString& code,
+ const QString& language,
+ const QVariantList& spans) const {
+ if (spans.isEmpty() || code.isEmpty()) return;
+ static constexpr int kMaxEntries = 32;
+ static constexpr int kMaxBytes = 1024 * 1024;
+ const QString key = cacheKey(resolveId(language), code);
+ const int bytes = static_cast(code.toUtf8().size());
+ QMutexLocker locker(&m_cacheMutex);
+ auto it = m_spanCache.find(key);
+ if (it != m_spanCache.end()) {
+ m_spanCacheBytes -= static_cast(it->code.toUtf8().size());
+ m_spanCache.erase(it);
+ m_spanCacheOrder.removeAll(key);
+ }
+ while (m_spanCacheOrder.size() >= kMaxEntries ||
+ m_spanCacheBytes + bytes > kMaxBytes) {
+ if (m_spanCacheOrder.isEmpty()) break;
+ const QString oldest = m_spanCacheOrder.takeFirst();
+ m_spanCacheBytes -=
+ static_cast(m_spanCache.value(oldest).code.toUtf8().size());
+ m_spanCache.remove(oldest);
+ }
+ m_spanCache.insert(key, SpanCacheEntry{code, spans});
+ m_spanCacheOrder.append(key);
+ m_spanCacheBytes += bytes;
+}
+
+void CodeHighlighter::highlight(
+ const QString& code, const QString& language, QObject* target, int token) {
+ QPointer targetGuard(target);
+ const QVariantList cached = lookupSpans(code, language);
+ if (!cached.isEmpty()) {
+ QMetaObject::invokeMethod(
+ targetGuard,
+ "onHighlightSpans",
+ Qt::DirectConnection,
+ Q_ARG(QVariant, token),
+ Q_ARG(QVariant, cached));
+ return;
+ }
+ QThreadPool::globalInstance()->start(
+ [this, target, token, code, language]() {
+ const QVariantList spans = doHighlight(code, language);
+ storeSpans(code, language, spans);
+ QPointer guard(target);
+ QMetaObject::invokeMethod(
+ QCoreApplication::instance(),
+ [guard, token, spans]() {
+ if (!guard) return;
+ QMetaObject::invokeMethod(
+ guard,
+ "onHighlightSpans",
+ Q_ARG(QVariant, token),
+ Q_ARG(QVariant, spans));
+ },
+ Qt::QueuedConnection);
+ });
+}
+
+QVariantList CodeHighlighter::doHighlight(
+ const QString& code, const QString& language) const {
+ QVariantList spans;
+ if (code.isEmpty()) return spans;
+
+ const QString id = resolveId(language);
+ const Grammar& grammar = hl::grammars().value(id);
+ if (grammar.libs.empty()) return spans;
+
+ static constexpr size_t kMaxBytes = 512 * 1024;
+ const QByteArray utf8 = code.toUtf8();
+ if (static_cast(utf8.size()) > kMaxBytes) return spans;
+
+ const TSLanguage* lang = nullptr;
+ TSQuery* query = nullptr;
+ {
+ QMutexLocker locker(&m_stateMutex);
+ auto& state = m_states[id];
+ if (!state || (!state->lang && !state->bad)) {
+ std::shared_ptr fresh = std::make_shared();
+ bool abiMismatch = false;
+ for (size_t i = 0; i < grammar.libs.size(); ++i) {
+ void* lib =
+ dlopen(grammar.libs[i].c_str(), RTLD_NOW | RTLD_LOCAL);
+ if (!lib) continue;
+ auto* symbol = reinterpret_cast(
+ dlsym(lib, grammar.symbols[i].c_str()));
+ if (!symbol) {
+ dlclose(lib);
+ continue;
+ }
+ const TSLanguage* candidate = symbol();
+ const uint32_t version = ts_language_abi_version(candidate);
+ if (version < TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION ||
+ version > TREE_SITTER_LANGUAGE_VERSION) {
+ dlclose(lib);
+ abiMismatch = true;
+ continue;
+ }
+ fresh->lib = lib;
+ fresh->lang = candidate;
+ break;
+ }
+ if (fresh->lang) {
+ for (const char* source : grammar.queries) {
+ TSQueryError errorType = TSQueryErrorNone;
+ uint32_t errorOffset = 0;
+ TSQuery* candidate = ts_query_new(
+ static_cast(fresh->lang),
+ source,
+ static_cast(std::strlen(source)),
+ &errorOffset,
+ &errorType);
+ if (!candidate) continue;
+ fresh->query = candidate;
+ break;
+ }
+ if (!fresh->query) fresh->bad = true;
+ } else if (abiMismatch) {
+ fresh->bad = true;
+ }
+ if (fresh->lang || fresh->bad) state = std::move(fresh);
+ }
+ if (!state || state->bad || !state->lang) return spans;
+ lang = static_cast(state->lang);
+ query = static_cast(state->query);
+ }
+
+ TSParser* parser = ts_parser_new();
+ ts_parser_set_language(parser, lang);
+ TSTree* tree = ts_parser_parse_string(
+ parser, nullptr, utf8.constData(), static_cast(utf8.size()));
+ if (!tree) {
+ ts_parser_delete(parser);
+ return spans;
+ }
+
+ TSQueryCursor* cursor = ts_query_cursor_new();
+ ts_query_cursor_exec(cursor, query, ts_tree_root_node(tree));
+
+ const uint32_t size = static_cast(utf8.size());
+ std::vector kinds(size, 0);
+
+ std::vector cu(size + 1, 0);
+ for (uint32_t b = 0; b < size; ++b) {
+ cu[b + 1] = cu[b];
+ const unsigned char c = static_cast(utf8[b]);
+ if (c < 0x80)
+ cu[b + 1] += 1;
+ else if (c < 0xC0)
+ ; // continuation byte
+ else if (c < 0xF0)
+ cu[b + 1] += 1;
+ else
+ cu[b + 1] += 2;
+ }
+
+ TSQueryMatch match;
+ uint32_t captureIndex = 0;
+ while (ts_query_cursor_next_capture(cursor, &match, &captureIndex)) {
+ const TSQueryCapture& capture = match.captures[captureIndex];
+ uint32_t nameLength = 0;
+ const char* name =
+ ts_query_capture_name_for_id(query, capture.index, &nameLength);
+ const uint8_t role = roleFor(name, nameLength);
+ if (role == 0) continue;
+ const uint32_t start = ts_node_start_byte(capture.node);
+ const uint32_t end = ts_node_end_byte(capture.node);
+ if (end <= start || end > size) continue;
+ std::fill(kinds.begin() + start, kinds.begin() + end, role);
+ }
+
+ uint32_t position = 0;
+ while (position < size) {
+ if (kinds[position] == 0) {
+ ++position;
+ continue;
+ }
+ const uint8_t role = kinds[position];
+ const uint32_t start = position;
+ while (position < size && kinds[position] == role)
+ ++position;
+ QVariantMap span;
+ span.insert("start", static_cast(cu[start]));
+ span.insert("length", static_cast(cu[position] - cu[start]));
+ span.insert("kind", roleName(role));
+ spans.append(span);
+ }
+
+ ts_query_cursor_delete(cursor);
+ ts_tree_delete(tree);
+ ts_parser_delete(parser);
+ return spans;
+}
+
+CodeHighlighter* CodeHighlighter::create(QQmlEngine*, QJSEngine*) {
+ if (!s_instance) s_instance = new CodeHighlighter();
+ return s_instance;
+}
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/codehighlighter.hpp b/Plugins/ZShell/Llm/codehighlighter.hpp
new file mode 100644
index 0000000..4f0b116
--- /dev/null
+++ b/Plugins/ZShell/Llm/codehighlighter.hpp
@@ -0,0 +1,76 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+class QQmlEngine;
+class QJSEngine;
+
+namespace ZShell::llm {
+
+class CodeHighlighter : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_SINGLETON
+
+ public:
+ Q_INVOKABLE void highlight(
+ const QString& code,
+ const QString& language,
+ QObject* target,
+ int token);
+
+ static CodeHighlighter* create(QQmlEngine*, QJSEngine*);
+
+ struct Grammar {
+ std::vector libs;
+ std::vector symbols;
+ std::vector queries;
+ };
+
+ private:
+ struct State {
+ bool bad = false; // permanent failure, do not retry
+ void* lib = nullptr;
+ const void* lang = nullptr; // const TSLanguage*
+ void* query = nullptr; // TSQuery*
+ };
+
+ [[nodiscard]] static const QHash& aliases();
+ [[nodiscard]] static uint8_t roleFor(const char* name, uint32_t length);
+ [[nodiscard]] static const char* roleName(uint8_t role);
+ [[nodiscard]] static QString resolveId(const QString& language);
+ [[nodiscard]] static QString cacheKey(
+ const QString& id, const QString& code);
+ [[nodiscard]] QVariantList doHighlight(
+ const QString& code, const QString& language) const;
+ [[nodiscard]] QVariantList lookupSpans(
+ const QString& code, const QString& language) const;
+ void storeSpans(
+ const QString& code,
+ const QString& language,
+ const QVariantList& spans) const;
+
+ struct SpanCacheEntry {
+ QString code; // re-compared on lookup; a hash collision can
+ // never deliver the wrong spans
+ QVariantList spans;
+ };
+
+ mutable QHash> m_states;
+ mutable QMutex m_stateMutex;
+ mutable QHash m_spanCache;
+ mutable QStringList m_spanCacheOrder; // LRU order, oldest first
+ mutable int m_spanCacheBytes = 0;
+ mutable QMutex m_cacheMutex;
+ static CodeHighlighter* s_instance;
+};
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/fonts/latinmodern/GUST-FONT-LICENSE.txt b/Plugins/ZShell/Llm/fonts/latinmodern/GUST-FONT-LICENSE.txt
new file mode 100644
index 0000000..d656332
--- /dev/null
+++ b/Plugins/ZShell/Llm/fonts/latinmodern/GUST-FONT-LICENSE.txt
@@ -0,0 +1,40 @@
+% This is a preliminary version (2006-09-30), barring acceptance from
+% the LaTeX Project Team and other feedback, of the GUST Font License.
+% (GUST is the Polish TeX Users Group, http://www.gust.org.pl)
+%
+% For the most recent version of this license see
+% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
+% or
+% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
+%
+% This work may be distributed and/or modified under the conditions
+% of the LaTeX Project Public License, either version 1.3c of this
+% license or (at your option) any later version.
+%
+% Please also observe the following clause:
+% 1) it is requested, but not legally required, that derived works be
+% distributed only after changing the names of the fonts comprising this
+% work and given in an accompanying "manifest", and that the
+% files comprising the Work, as listed in the manifest, also be given
+% new names. Any exceptions to this request are also given in the
+% manifest.
+%
+% We recommend the manifest be given in a separate file named
+% MANIFEST-.txt, where is some unique identification
+% of the font family. If a separate "readme" file accompanies the Work,
+% we recommend a name of the form README-.txt.
+%
+% The latest version of the LaTeX Project Public License is in
+% http://www.latex-project.org/lppl.txt and version 1.3c or later
+% is part of all distributions of LaTeX version 2006/05/20 or later.
+
+
+---
+
+Provenance:
+ lmroman10-*.otf: Latin Modern v2.007 (GUST, 31-03-2026)
+ https://www.gust.org.pl/projects/e-foundry/latin-modern/download
+ (Latin_Modern-otf-2_007-31_03_2026.zip)
+ latinmodern-math.otf: Latin Modern Math v1.959 (GUST)
+ https://www.gust.org.pl/projects/e-foundry/lm-math/download
+ (latinmodern-math-1959.zip; same release as CTAN fonts/lm-math)
diff --git a/Plugins/ZShell/Llm/fonts/latinmodern/latinmodern-math.otf b/Plugins/ZShell/Llm/fonts/latinmodern/latinmodern-math.otf
new file mode 100644
index 0000000..0e4642e
Binary files /dev/null and b/Plugins/ZShell/Llm/fonts/latinmodern/latinmodern-math.otf differ
diff --git a/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-bold.otf b/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-bold.otf
new file mode 100644
index 0000000..e54ad36
Binary files /dev/null and b/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-bold.otf differ
diff --git a/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-bolditalic.otf b/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-bolditalic.otf
new file mode 100644
index 0000000..b7f00bd
Binary files /dev/null and b/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-bolditalic.otf differ
diff --git a/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-italic.otf b/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-italic.otf
new file mode 100644
index 0000000..721f973
Binary files /dev/null and b/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-italic.otf differ
diff --git a/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-regular.otf b/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-regular.otf
new file mode 100644
index 0000000..2d10885
Binary files /dev/null and b/Plugins/ZShell/Llm/fonts/latinmodern/lmroman10-regular.otf differ
diff --git a/Plugins/ZShell/Llm/generation.cpp b/Plugins/ZShell/Llm/generation.cpp
new file mode 100644
index 0000000..129de52
--- /dev/null
+++ b/Plugins/ZShell/Llm/generation.cpp
@@ -0,0 +1,218 @@
+#include "generation.hpp"
+
+#include
+
+namespace ZShell::llm {
+
+ChatGeneration::ChatGeneration(qint64 timestamp, QObject* parent)
+ : QObject(parent), m_timestamp(timestamp) {
+ m_timer.setParent(this);
+ m_timer.setInterval(500);
+ m_timer.setTimerType(Qt::CoarseTimer);
+ connect(&m_timer, &QTimer::timeout, this, [this]() {
+ bool anyRunning = false;
+ for (auto* segment : m_segments) {
+ if (!segment->running()) continue;
+ anyRunning = true;
+ segment->elapsedMsChanged();
+ }
+ if (anyRunning) Q_EMIT elapsedMsChanged();
+ if (!anyRunning && !m_streaming) m_timer.stop();
+ });
+}
+
+QString ChatGeneration::content() const {
+ QStringList parts;
+ for (const auto* segment : m_segments) {
+ if (segment->type() != LlmSegment::Type::Content ||
+ segment->text().isEmpty())
+ continue;
+ parts.append(segment->text());
+ }
+ return parts.join(QStringLiteral("\n\n"));
+}
+
+QString ChatGeneration::reasoning() const {
+ QStringList parts;
+ for (const auto* segment : m_segments) {
+ if (segment->type() != LlmSegment::Type::Reasoning ||
+ segment->text().isEmpty())
+ continue;
+ parts.append(segment->text());
+ }
+ return parts.join(QStringLiteral("\n\n"));
+}
+
+bool ChatGeneration::reasoningActive() const {
+ if (!m_streaming) return false;
+ if (!content().isEmpty()) return false;
+ return !hasRunningTool();
+}
+
+qint64 ChatGeneration::reasoningElapsedMs() const {
+ qint64 total = 0;
+ for (const auto* segment : m_segments)
+ if (segment->type() == LlmSegment::Type::Reasoning)
+ total += segment->elapsedMs();
+ return total;
+}
+
+qint64 ChatGeneration::contentElapsedMs() const {
+ qint64 total = 0;
+ for (const auto* segment : m_segments)
+ if (segment->type() == LlmSegment::Type::Content)
+ total += segment->elapsedMs();
+ return total;
+}
+
+qint64 ChatGeneration::toolsElapsedMs() const {
+ qint64 total = 0;
+ for (const auto* segment : m_segments)
+ if (segment->type() == LlmSegment::Type::ToolCall)
+ total += segment->elapsedMs();
+ return total;
+}
+
+int ChatGeneration::toolCallCount() const {
+ int count = 0;
+ for (const auto* segment : m_segments)
+ if (segment->type() == LlmSegment::Type::ToolCall) ++count;
+ return count;
+}
+
+bool ChatGeneration::hasRunningTool() const {
+ for (const auto* segment : m_segments)
+ if (segment->type() == LlmSegment::Type::ToolCall && segment->running())
+ return true;
+ return false;
+}
+
+void ChatGeneration::updateReasoningActive() {
+ const bool active = reasoningActive();
+ if (m_reasoningActive == active) return;
+ m_reasoningActive = active;
+ Q_EMIT reasoningActiveChanged();
+}
+
+void ChatGeneration::setContent(const QString& value) {
+ LlmSegment* first = nullptr;
+ for (auto* segment : m_segments) {
+ if (segment->type() != LlmSegment::Type::Content) continue;
+ if (!first)
+ first = segment;
+ else
+ segment->setText(QString());
+ }
+ if (!first) {
+ if (value.isEmpty()) return;
+ first = new LlmSegment(
+ LlmSegment::Type::Content,
+ QDateTime::currentMSecsSinceEpoch(),
+ this);
+ addSegment(first);
+ }
+ first->setText(value);
+}
+
+void ChatGeneration::appendContent(const QString& piece) {
+ if (piece.isEmpty()) return;
+ for (auto* segment : m_segments) {
+ if (segment->type() == LlmSegment::Type::Reasoning &&
+ segment->running())
+ segment->close();
+ }
+ openContentSegment()->appendText(piece);
+}
+
+void ChatGeneration::appendReasoning(const QString& piece) {
+ if (piece.isEmpty()) return;
+ for (auto* segment : m_segments) {
+ if (segment->type() == LlmSegment::Type::Content && segment->running())
+ segment->close();
+ }
+ openReasoningSegment()->appendText(piece);
+}
+
+void ChatGeneration::setStreaming(bool value) {
+ if (m_streaming == value) return;
+ m_streaming = value;
+ Q_EMIT streamingChanged();
+ if (value) {
+ if (!m_timer.isActive()) m_timer.start();
+ } else {
+ closeOpenSegments();
+ m_timer.stop();
+ }
+ Q_EMIT elapsedMsChanged();
+ updateReasoningActive();
+}
+
+LlmSegment* ChatGeneration::openContentSegment() {
+ for (auto* segment : m_segments)
+ if (segment->type() == LlmSegment::Type::Content && segment->running())
+ return segment;
+ auto* segment = new LlmSegment(
+ LlmSegment::Type::Content, QDateTime::currentMSecsSinceEpoch(), this);
+ segment->begin();
+ addSegment(segment);
+ return segment;
+}
+
+LlmSegment* ChatGeneration::openReasoningSegment() {
+ for (auto* segment : m_segments)
+ if (segment->type() == LlmSegment::Type::Reasoning &&
+ segment->running())
+ return segment;
+ auto* segment = new LlmSegment(
+ LlmSegment::Type::Reasoning, QDateTime::currentMSecsSinceEpoch(), this);
+ segment->begin();
+ addSegment(segment);
+ return segment;
+}
+
+LlmSegment* ChatGeneration::beginToolCall(
+ const QString& name, const QString& toolCallId) {
+ closeOpenSegments();
+ auto* segment = new LlmSegment(
+ LlmSegment::Type::ToolCall, QDateTime::currentMSecsSinceEpoch(), this);
+ segment->setName(name);
+ segment->setToolCallId(toolCallId);
+ segment->setStatus(LlmSegment::Status::Running);
+ segment->begin();
+ addSegment(segment);
+ Q_EMIT toolStateChanged();
+ return segment;
+}
+
+void ChatGeneration::addSegment(LlmSegment* segment) {
+ if (!segment || m_segments.contains(segment)) return;
+ segment->setParent(this);
+ connect(segment, &LlmSegment::textChanged, this, [this, segment]() {
+ if (segment->type() == LlmSegment::Type::Reasoning)
+ Q_EMIT reasoningChanged();
+ else if (segment->type() == LlmSegment::Type::Content)
+ Q_EMIT contentChanged();
+ updateReasoningActive();
+ });
+ connect(segment, &LlmSegment::statusChanged, this, [this]() {
+ Q_EMIT toolStateChanged();
+ });
+ connect(segment, &LlmSegment::resultChanged, this, [this]() {
+ Q_EMIT toolStateChanged();
+ });
+ connect(segment, &LlmSegment::runningChanged, this, [this]() {
+ Q_EMIT elapsedMsChanged();
+ Q_EMIT toolStateChanged();
+ updateReasoningActive();
+ });
+ m_segments.append(segment);
+ Q_EMIT segmentsChanged();
+}
+
+void ChatGeneration::closeOpenSegments() {
+ for (auto* segment : m_segments)
+ if (segment->running()) segment->close();
+ updateReasoningActive();
+}
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/generation.hpp b/Plugins/ZShell/Llm/generation.hpp
new file mode 100644
index 0000000..20b4709
--- /dev/null
+++ b/Plugins/ZShell/Llm/generation.hpp
@@ -0,0 +1,83 @@
+#pragma once
+
+#include "segment.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace ZShell::llm {
+
+class ChatGeneration : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("Chat generations are managed by ChatMessage")
+
+ Q_PROPERTY(
+ QString content READ content WRITE setContent NOTIFY contentChanged)
+ Q_PROPERTY(QString reasoning READ reasoning NOTIFY reasoningChanged)
+ Q_PROPERTY(
+ qint64 reasoningElapsedMs READ reasoningElapsedMs NOTIFY
+ elapsedMsChanged)
+ Q_PROPERTY(
+ qint64 contentElapsedMs READ contentElapsedMs NOTIFY elapsedMsChanged)
+ Q_PROPERTY(qint64 toolsElapsedMs READ toolsElapsedMs NOTIFY elapsedMsChanged)
+ Q_PROPERTY(bool streaming READ streaming NOTIFY streamingChanged)
+ Q_PROPERTY(
+ bool reasoningActive READ reasoningActive NOTIFY reasoningActiveChanged)
+ Q_PROPERTY(bool hasRunningTool READ hasRunningTool NOTIFY toolStateChanged)
+ Q_PROPERTY(qint64 timestamp READ timestamp CONSTANT)
+ Q_PROPERTY(
+ QList segments READ segments NOTIFY
+ segmentsChanged)
+ Q_PROPERTY(int toolCallCount READ toolCallCount NOTIFY segmentsChanged)
+
+ public:
+ explicit ChatGeneration(qint64 timestamp, QObject* parent = nullptr);
+
+ [[nodiscard]] QString content() const;
+ [[nodiscard]] QString reasoning() const;
+ [[nodiscard]] bool reasoningActive() const;
+ [[nodiscard]] qint64 reasoningElapsedMs() const;
+ [[nodiscard]] qint64 contentElapsedMs() const;
+ [[nodiscard]] qint64 toolsElapsedMs() const;
+ [[nodiscard]] bool streaming() const { return m_streaming; }
+ [[nodiscard]] qint64 timestamp() const { return m_timestamp; }
+ [[nodiscard]] QList segments() const { return m_segments; }
+ [[nodiscard]] int toolCallCount() const;
+ [[nodiscard]] bool hasRunningTool() const;
+
+ void setContent(const QString& value);
+ void appendContent(const QString& piece);
+ void appendReasoning(const QString& piece);
+ void setStreaming(bool value);
+
+ [[nodiscard]] LlmSegment* openContentSegment();
+ [[nodiscard]] LlmSegment* openReasoningSegment();
+ [[nodiscard]] LlmSegment* beginToolCall(
+ const QString& name, const QString& toolCallId);
+ void addSegment(LlmSegment* segment);
+ void closeOpenSegments();
+
+ Q_SIGNALS:
+ void contentChanged();
+ void reasoningChanged();
+ void reasoningActiveChanged();
+ void elapsedMsChanged();
+ void streamingChanged();
+ void toolStateChanged();
+ void segmentsChanged();
+
+ private:
+ void updateReasoningActive();
+
+ QTimer m_timer;
+ QList m_segments;
+ bool m_reasoningActive = false;
+ bool m_streaming = false;
+ qint64 m_timestamp;
+};
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/highlight-queries/bash.scm b/Plugins/ZShell/Llm/highlight-queries/bash.scm
new file mode 100644
index 0000000..a45327d
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/bash.scm
@@ -0,0 +1,59 @@
+; Vendored from bash (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-bash
+
+[
+ (string)
+ (raw_string)
+ (heredoc_body)
+ (heredoc_start)
+] @string
+
+(command_name) @function
+
+(variable_name) @property
+
+[
+ "case"
+ "do"
+ "done"
+ "elif"
+ "else"
+ "esac"
+ "export"
+ "fi"
+ "for"
+ "function"
+ "if"
+ "in"
+ "select"
+ "then"
+ "unset"
+ "until"
+ "while"
+] @keyword
+
+(comment) @comment
+
+(function_definition name: (word) @function)
+
+(file_descriptor) @number
+
+[
+ (command_substitution)
+ (process_substitution)
+ (expansion)
+]@embedded
+
+[
+ "$"
+ "&&"
+ ">"
+ ">>"
+ "<"
+ "|"
+] @operator
+
+(
+ (command (_) @constant)
+ (#match? @constant "^-")
+)
diff --git a/Plugins/ZShell/Llm/highlight-queries/c.scm b/Plugins/ZShell/Llm/highlight-queries/c.scm
new file mode 100644
index 0000000..c9652e5
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/c.scm
@@ -0,0 +1,84 @@
+; Vendored from c (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-c
+
+(identifier) @variable
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z\\d_]*$"))
+
+"break" @keyword
+"case" @keyword
+"const" @keyword
+"continue" @keyword
+"default" @keyword
+"do" @keyword
+"else" @keyword
+"enum" @keyword
+"extern" @keyword
+"for" @keyword
+"if" @keyword
+"inline" @keyword
+"return" @keyword
+"sizeof" @keyword
+"static" @keyword
+"struct" @keyword
+"switch" @keyword
+"typedef" @keyword
+"union" @keyword
+"volatile" @keyword
+"while" @keyword
+
+"#define" @keyword
+"#elif" @keyword
+"#else" @keyword
+"#endif" @keyword
+"#if" @keyword
+"#ifdef" @keyword
+"#ifndef" @keyword
+"#include" @keyword
+(preproc_directive) @keyword
+
+"--" @operator
+"-" @operator
+"-=" @operator
+"->" @operator
+"=" @operator
+"!=" @operator
+"*" @operator
+"&" @operator
+"&&" @operator
+"+" @operator
+"++" @operator
+"+=" @operator
+"<" @operator
+"==" @operator
+">" @operator
+"||" @operator
+
+"." @delimiter
+";" @delimiter
+
+(string_literal) @string
+(system_lib_string) @string
+
+(null) @constant
+(number_literal) @number
+(char_literal) @number
+
+(field_identifier) @property
+(statement_identifier) @label
+(type_identifier) @type
+(primitive_type) @type
+(sized_type_specifier) @type
+
+(call_expression
+ function: (identifier) @function)
+(call_expression
+ function: (field_expression
+ field: (field_identifier) @function))
+(function_declarator
+ declarator: (identifier) @function)
+(preproc_function_def
+ name: (identifier) @function.special)
+
+(comment) @comment
diff --git a/Plugins/ZShell/Llm/highlight-queries/cpp.scm b/Plugins/ZShell/Llm/highlight-queries/cpp.scm
new file mode 100644
index 0000000..127c6a9
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/cpp.scm
@@ -0,0 +1,73 @@
+; Vendored from cpp (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-cpp (tag v0.23.4, matching the distro grammar)
+
+; Functions
+
+(call_expression
+ function: (qualified_identifier
+ name: (identifier) @function))
+
+(template_function
+ name: (identifier) @function)
+
+(template_method
+ name: (field_identifier) @function)
+
+(template_function
+ name: (identifier) @function)
+
+(function_declarator
+ declarator: (qualified_identifier
+ name: (identifier) @function))
+
+(function_declarator
+ declarator: (field_identifier) @function)
+
+; Types
+
+((namespace_identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(auto) @type
+
+; Constants
+
+(this) @variable.builtin
+(null "nullptr" @constant)
+
+; Keywords
+
+[
+ "catch"
+ "class"
+ "co_await"
+ "co_return"
+ "co_yield"
+ "constexpr"
+ "constinit"
+ "consteval"
+ "delete"
+ "explicit"
+ "final"
+ "friend"
+ "mutable"
+ "namespace"
+ "noexcept"
+ "new"
+ "override"
+ "private"
+ "protected"
+ "public"
+ "template"
+ "throw"
+ "try"
+ "typename"
+ "using"
+ "concept"
+ "requires"
+ "virtual"
+] @keyword
+
+; Strings
+
+(raw_string_literal) @string
diff --git a/Plugins/ZShell/Llm/highlight-queries/go.scm b/Plugins/ZShell/Llm/highlight-queries/go.scm
new file mode 100644
index 0000000..481d5cc
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/go.scm
@@ -0,0 +1,126 @@
+; Vendored from go (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-go
+
+; Function calls
+
+(call_expression
+ function: (identifier) @function)
+
+(call_expression
+ function: (identifier) @function.builtin
+ (#match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$"))
+
+(call_expression
+ function: (selector_expression
+ field: (field_identifier) @function.method))
+
+; Function definitions
+
+(function_declaration
+ name: (identifier) @function)
+
+(method_declaration
+ name: (field_identifier) @function.method)
+
+; Identifiers
+
+(type_identifier) @type
+(field_identifier) @property
+(identifier) @variable
+
+; Operators
+
+[
+ "--"
+ "-"
+ "-="
+ ":="
+ "!"
+ "!="
+ "..."
+ "*"
+ "*"
+ "*="
+ "/"
+ "/="
+ "&"
+ "&&"
+ "&="
+ "%"
+ "%="
+ "^"
+ "^="
+ "+"
+ "++"
+ "+="
+ "<-"
+ "<"
+ "<<"
+ "<<="
+ "<="
+ "="
+ "=="
+ ">"
+ ">="
+ ">>"
+ ">>="
+ "|"
+ "|="
+ "||"
+ "~"
+] @operator
+
+; Keywords
+
+[
+ "break"
+ "case"
+ "chan"
+ "const"
+ "continue"
+ "default"
+ "defer"
+ "else"
+ "fallthrough"
+ "for"
+ "func"
+ "go"
+ "goto"
+ "if"
+ "import"
+ "interface"
+ "map"
+ "package"
+ "range"
+ "return"
+ "select"
+ "struct"
+ "switch"
+ "type"
+ "var"
+] @keyword
+
+; Literals
+
+[
+ (interpreted_string_literal)
+ (raw_string_literal)
+ (rune_literal)
+] @string
+
+(escape_sequence) @escape
+
+[
+ (int_literal)
+ (float_literal)
+ (imaginary_literal)
+] @number
+
+[
+ (true)
+ (false)
+ (nil)
+ (iota)
+] @constant.builtin
+
+(comment) @comment
diff --git a/Plugins/ZShell/Llm/highlight-queries/javascript.scm b/Plugins/ZShell/Llm/highlight-queries/javascript.scm
new file mode 100644
index 0000000..92b1a52
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/javascript.scm
@@ -0,0 +1,207 @@
+; Vendored from javascript (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-javascript
+
+; Variables
+;----------
+
+(identifier) @variable
+
+; Properties
+;-----------
+
+(property_identifier) @property
+
+; Function and method definitions
+;--------------------------------
+
+(function_expression
+ name: (identifier) @function)
+(function_declaration
+ name: (identifier) @function)
+(method_definition
+ name: (property_identifier) @function.method)
+
+(pair
+ key: (property_identifier) @function.method
+ value: [(function_expression) (arrow_function)])
+
+(assignment_expression
+ left: (member_expression
+ property: (property_identifier) @function.method)
+ right: [(function_expression) (arrow_function)])
+
+(variable_declarator
+ name: (identifier) @function
+ value: [(function_expression) (arrow_function)])
+
+(assignment_expression
+ left: (identifier) @function
+ right: [(function_expression) (arrow_function)])
+
+; Function and method calls
+;--------------------------
+
+(call_expression
+ function: (identifier) @function)
+
+(call_expression
+ function: (member_expression
+ property: (property_identifier) @function.method))
+
+; Special identifiers
+;--------------------
+
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+([
+ (identifier)
+ (shorthand_property_identifier)
+ (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^[A-Z_][A-Z\\d_]+$"))
+
+((identifier) @variable.builtin
+ (#match? @variable.builtin "^(arguments|module|console|window|document)$")
+ (#is-not? local))
+
+((identifier) @function.builtin
+ (#eq? @function.builtin "require")
+ (#is-not? local))
+
+; Literals
+;---------
+
+(this) @variable.builtin
+(super) @variable.builtin
+
+[
+ (true)
+ (false)
+ (null)
+ (undefined)
+] @constant.builtin
+
+(comment) @comment
+
+[
+ (string)
+ (template_string)
+] @string
+
+(regex) @string.special
+(number) @number
+
+; Tokens
+;-------
+
+[
+ ";"
+ (optional_chain)
+ "."
+ ","
+] @punctuation.delimiter
+
+[
+ "-"
+ "--"
+ "-="
+ "+"
+ "++"
+ "+="
+ "*"
+ "*="
+ "**"
+ "**="
+ "/"
+ "/="
+ "%"
+ "%="
+ "<"
+ "<="
+ "<<"
+ "<<="
+ "="
+ "=="
+ "==="
+ "!"
+ "!="
+ "!=="
+ "=>"
+ ">"
+ ">="
+ ">>"
+ ">>="
+ ">>>"
+ ">>>="
+ "~"
+ "^"
+ "&"
+ "|"
+ "^="
+ "&="
+ "|="
+ "&&"
+ "||"
+ "??"
+ "&&="
+ "||="
+ "??="
+] @operator
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+(template_substitution
+ "${" @punctuation.special
+ "}" @punctuation.special) @embedded
+
+[
+ "as"
+ "async"
+ "await"
+ "break"
+ "case"
+ "catch"
+ "class"
+ "const"
+ "continue"
+ "debugger"
+ "default"
+ "delete"
+ "do"
+ "else"
+ "export"
+ "extends"
+ "finally"
+ "for"
+ "from"
+ "function"
+ "get"
+ "if"
+ "import"
+ "in"
+ "instanceof"
+ "let"
+ "new"
+ "of"
+ "return"
+ "set"
+ "static"
+ "switch"
+ "target"
+ "throw"
+ "try"
+ "typeof"
+ "var"
+ "void"
+ "while"
+ "with"
+ "yield"
+] @keyword
diff --git a/Plugins/ZShell/Llm/highlight-queries/json.scm b/Plugins/ZShell/Llm/highlight-queries/json.scm
new file mode 100644
index 0000000..ede99aa
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/json.scm
@@ -0,0 +1,19 @@
+; Vendored from json (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-json
+
+(pair
+ key: (_) @string.special.key)
+
+(string) @string
+
+(number) @number
+
+[
+ (null)
+ (true)
+ (false)
+] @constant.builtin
+
+(escape_sequence) @escape
+
+(comment) @comment
diff --git a/Plugins/ZShell/Llm/highlight-queries/python.scm b/Plugins/ZShell/Llm/highlight-queries/python.scm
new file mode 100644
index 0000000..fd315a9
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/python.scm
@@ -0,0 +1,140 @@
+; Vendored from python (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-python
+
+; Identifier naming conventions
+
+(identifier) @variable
+
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z_]*$"))
+
+; Function calls
+
+(decorator) @function
+(decorator
+ (identifier) @function)
+
+(call
+ function: (attribute attribute: (identifier) @function.method))
+(call
+ function: (identifier) @function)
+
+; Builtin functions
+
+((call
+ function: (identifier) @function.builtin)
+ (#match?
+ @function.builtin
+ "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$"))
+
+; Function definitions
+
+(function_definition
+ name: (identifier) @function)
+
+(attribute attribute: (identifier) @property)
+(type (identifier) @type)
+
+; Literals
+
+[
+ (none)
+ (true)
+ (false)
+] @constant.builtin
+
+[
+ (integer)
+ (float)
+] @number
+
+(comment) @comment
+(string) @string
+(escape_sequence) @escape
+
+(interpolation
+ "{" @punctuation.special
+ "}" @punctuation.special) @embedded
+
+[
+ "-"
+ "-="
+ "!="
+ "*"
+ "**"
+ "**="
+ "*="
+ "/"
+ "//"
+ "//="
+ "/="
+ "&"
+ "&="
+ "%"
+ "%="
+ "^"
+ "^="
+ "+"
+ "->"
+ "+="
+ "<"
+ "<<"
+ "<<="
+ "<="
+ "<>"
+ "="
+ ":="
+ "=="
+ ">"
+ ">="
+ ">>"
+ ">>="
+ "|"
+ "|="
+ "~"
+ "@="
+ "and"
+ "in"
+ "is"
+ "not"
+ "or"
+ "is not"
+ "not in"
+] @operator
+
+[
+ "as"
+ "assert"
+ "async"
+ "await"
+ "break"
+ "class"
+ "continue"
+ "def"
+ "del"
+ "elif"
+ "else"
+ "except"
+ "exec"
+ "finally"
+ "for"
+ "from"
+ "global"
+ "if"
+ "import"
+ "lambda"
+ "nonlocal"
+ "pass"
+ "print"
+ "raise"
+ "return"
+ "try"
+ "while"
+ "with"
+ "yield"
+ "match"
+ "case"
+] @keyword
diff --git a/Plugins/ZShell/Llm/highlight-queries/rust.scm b/Plugins/ZShell/Llm/highlight-queries/rust.scm
new file mode 100644
index 0000000..c1acc9e
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/rust.scm
@@ -0,0 +1,164 @@
+; Vendored from rust (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-rust
+
+; Identifiers
+
+(type_identifier) @type
+(primitive_type) @type.builtin
+(field_identifier) @property
+
+; Identifier conventions
+
+; Assume all-caps names are constants
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z\\d_]+$'"))
+
+; Assume uppercase names are enum constructors
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+; Assume that uppercase names in paths are types
+((scoped_identifier
+ path: (identifier) @type)
+ (#match? @type "^[A-Z]"))
+((scoped_identifier
+ path: (scoped_identifier
+ name: (identifier) @type))
+ (#match? @type "^[A-Z]"))
+((scoped_type_identifier
+ path: (identifier) @type)
+ (#match? @type "^[A-Z]"))
+((scoped_type_identifier
+ path: (scoped_identifier
+ name: (identifier) @type))
+ (#match? @type "^[A-Z]"))
+
+; Assume all qualified names in struct patterns are enum constructors. (They're
+; either that, or struct names; highlighting both as constructors seems to be
+; the less glaring choice of error, visually.)
+(struct_pattern
+ type: (scoped_type_identifier
+ name: (type_identifier) @constructor))
+
+; Function calls
+
+(call_expression
+ function: (identifier) @function)
+(call_expression
+ function: (field_expression
+ field: (field_identifier) @function.method))
+(call_expression
+ function: (scoped_identifier
+ "::"
+ name: (identifier) @function))
+
+(generic_function
+ function: (identifier) @function)
+(generic_function
+ function: (scoped_identifier
+ name: (identifier) @function))
+(generic_function
+ function: (field_expression
+ field: (field_identifier) @function.method))
+
+(macro_invocation
+ macro: (identifier) @function.macro
+ "!" @function.macro)
+
+; Function definitions
+
+(function_item (identifier) @function)
+(function_signature_item (identifier) @function)
+
+(line_comment) @comment
+(block_comment) @comment
+
+(line_comment (doc_comment)) @comment.documentation
+(block_comment (doc_comment)) @comment.documentation
+
+"(" @punctuation.bracket
+")" @punctuation.bracket
+"[" @punctuation.bracket
+"]" @punctuation.bracket
+"{" @punctuation.bracket
+"}" @punctuation.bracket
+
+(type_arguments
+ "<" @punctuation.bracket
+ ">" @punctuation.bracket)
+(type_parameters
+ "<" @punctuation.bracket
+ ">" @punctuation.bracket)
+
+"::" @punctuation.delimiter
+":" @punctuation.delimiter
+"." @punctuation.delimiter
+"," @punctuation.delimiter
+";" @punctuation.delimiter
+
+(parameter (identifier) @variable.parameter)
+
+(lifetime (identifier) @label)
+
+"as" @keyword
+"async" @keyword
+"await" @keyword
+"break" @keyword
+"const" @keyword
+"continue" @keyword
+"default" @keyword
+"dyn" @keyword
+"else" @keyword
+"enum" @keyword
+"extern" @keyword
+"fn" @keyword
+"for" @keyword
+"gen" @keyword
+"if" @keyword
+"impl" @keyword
+"in" @keyword
+"let" @keyword
+"loop" @keyword
+"macro_rules!" @keyword
+"match" @keyword
+"mod" @keyword
+"move" @keyword
+"pub" @keyword
+"raw" @keyword
+"ref" @keyword
+"return" @keyword
+"static" @keyword
+"struct" @keyword
+"trait" @keyword
+"type" @keyword
+"union" @keyword
+"unsafe" @keyword
+"use" @keyword
+"where" @keyword
+"while" @keyword
+"yield" @keyword
+(crate) @keyword
+(mutable_specifier) @keyword
+(use_list (self) @keyword)
+(scoped_use_list (self) @keyword)
+(scoped_identifier (self) @keyword)
+(super) @keyword
+
+(self) @variable.builtin
+
+(char_literal) @string
+(string_literal) @string
+(raw_string_literal) @string
+
+(boolean_literal) @constant.builtin
+(integer_literal) @constant.builtin
+(float_literal) @constant.builtin
+
+(escape_sequence) @escape
+
+(attribute_item) @attribute
+(inner_attribute_item) @attribute
+
+"*" @operator
+"&" @operator
+"'" @operator
diff --git a/Plugins/ZShell/Llm/highlight-queries/sql.scm b/Plugins/ZShell/Llm/highlight-queries/sql.scm
new file mode 100644
index 0000000..de1438a
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/sql.scm
@@ -0,0 +1,463 @@
+; Vendored from sql (MIT License)
+; Source: https://github.com/DerekStride/tree-sitter-sql
+
+(object_reference
+ name: (identifier) @type)
+
+(invocation
+ (object_reference
+ name: (identifier) @function.call))
+
+[
+ (keyword_gist)
+ (keyword_btree)
+ (keyword_hash)
+ (keyword_spgist)
+ (keyword_gin)
+ (keyword_brin)
+ (keyword_array)
+ (keyword_object_id)
+] @function.call
+
+(relation
+ alias: (identifier) @variable)
+
+(field
+ name: (identifier) @field)
+
+(term
+ alias: (identifier) @variable)
+
+((term
+ value: (cast
+ name: (keyword_cast) @function.call
+ parameter: [(literal)]?)))
+
+(literal) @string
+(comment) @comment @spell
+(marginalia) @comment
+
+((literal) @number
+ (#match? @number "^[-+]?%d+$"))
+
+((literal) @float
+ (#match? @float "^[-+]?%d*\.%d*$"))
+
+(parameter) @parameter
+
+[
+ (keyword_true)
+ (keyword_false)
+] @boolean
+
+[
+ (keyword_asc)
+ (keyword_desc)
+ (keyword_terminated)
+ (keyword_escaped)
+ (keyword_unsigned)
+ (keyword_nulls)
+ (keyword_last)
+ (keyword_delimited)
+ (keyword_replication)
+ (keyword_auto_increment)
+ (keyword_default)
+ (keyword_collate)
+ (keyword_concurrently)
+ (keyword_engine)
+ (keyword_always)
+ (keyword_generated)
+ (keyword_preceding)
+ (keyword_following)
+ (keyword_first)
+ (keyword_current_timestamp)
+ (keyword_immutable)
+ (keyword_atomic)
+ (keyword_parallel)
+ (keyword_leakproof)
+ (keyword_safe)
+ (keyword_cost)
+ (keyword_strict)
+] @attribute
+
+[
+ (keyword_materialized)
+ (keyword_recursive)
+ (keyword_temp)
+ (keyword_temporary)
+ (keyword_unlogged)
+ (keyword_external)
+ (keyword_parquet)
+ (keyword_csv)
+ (keyword_rcfile)
+ (keyword_textfile)
+ (keyword_orc)
+ (keyword_avro)
+ (keyword_jsonfile)
+ (keyword_sequencefile)
+ (keyword_volatile)
+] @storageclass
+
+[
+ (keyword_case)
+ (keyword_when)
+ (keyword_then)
+ (keyword_else)
+] @conditional
+
+[
+ (keyword_select)
+ (keyword_from)
+ (keyword_where)
+ (keyword_index)
+ (keyword_join)
+ (keyword_primary)
+ (keyword_delete)
+ (keyword_create)
+ (keyword_show)
+ (keyword_unload)
+ (keyword_insert)
+ (keyword_merge)
+ (keyword_distinct)
+ (keyword_replace)
+ (keyword_update)
+ (keyword_into)
+ (keyword_overwrite)
+ (keyword_matched)
+ (keyword_values)
+ (keyword_value)
+ (keyword_attribute)
+ (keyword_set)
+ (keyword_left)
+ (keyword_right)
+ (keyword_outer)
+ (keyword_inner)
+ (keyword_full)
+ (keyword_order)
+ (keyword_partition)
+ (keyword_group)
+ (keyword_with)
+ (keyword_without)
+ (keyword_as)
+ (keyword_having)
+ (keyword_limit)
+ (keyword_offset)
+ (keyword_table)
+ (keyword_tables)
+ (keyword_key)
+ (keyword_references)
+ (keyword_foreign)
+ (keyword_constraint)
+ (keyword_force)
+ (keyword_use)
+ (keyword_include)
+ (keyword_for)
+ (keyword_if)
+ (keyword_exists)
+ (keyword_column)
+ (keyword_columns)
+ (keyword_cross)
+ (keyword_lateral)
+ (keyword_natural)
+ (keyword_alter)
+ (keyword_drop)
+ (keyword_add)
+ (keyword_view)
+ (keyword_end)
+ (keyword_is)
+ (keyword_using)
+ (keyword_between)
+ (keyword_window)
+ (keyword_no)
+ (keyword_data)
+ (keyword_type)
+ (keyword_rename)
+ (keyword_refresh)
+ (keyword_to)
+ (keyword_schema)
+ (keyword_owner)
+ (keyword_authorization)
+ (keyword_all)
+ (keyword_any)
+ (keyword_some)
+ (keyword_returning)
+ (keyword_begin)
+ (keyword_commit)
+ (keyword_rollback)
+ (keyword_transaction)
+ (keyword_only)
+ (keyword_like)
+ (keyword_rlike)
+ (keyword_similar)
+ (keyword_over)
+ (keyword_change)
+ (keyword_modify)
+ (keyword_after)
+ (keyword_before)
+ (keyword_range)
+ (keyword_rows)
+ (keyword_groups)
+ (keyword_exclude)
+ (keyword_current)
+ (keyword_ties)
+ (keyword_others)
+ (keyword_zerofill)
+ (keyword_format)
+ (keyword_fields)
+ (keyword_row)
+ (keyword_sort)
+ (keyword_compute)
+ (keyword_comment)
+ (keyword_location)
+ (keyword_cached)
+ (keyword_uncached)
+ (keyword_lines)
+ (keyword_stored)
+ (keyword_virtual)
+ (keyword_partitioned)
+ (keyword_analyze)
+ (keyword_explain)
+ (keyword_verbose)
+ (keyword_truncate)
+ (keyword_rewrite)
+ (keyword_optimize)
+ (keyword_vacuum)
+ (keyword_cache)
+ (keyword_language)
+ (keyword_called)
+ (keyword_conflict)
+ (keyword_declare)
+ (keyword_filter)
+ (keyword_function)
+ (keyword_input)
+ (keyword_name)
+ (keyword_oid)
+ (keyword_oids)
+ (keyword_precision)
+ (keyword_regclass)
+ (keyword_regnamespace)
+ (keyword_regproc)
+ (keyword_regtype)
+ (keyword_restricted)
+ (keyword_return)
+ (keyword_returns)
+ (keyword_separator)
+ (keyword_setof)
+ (keyword_stable)
+ (keyword_support)
+ (keyword_tblproperties)
+ (keyword_trigger)
+ (keyword_unsafe)
+ (keyword_admin)
+ (keyword_connection)
+ (keyword_cycle)
+ (keyword_database)
+ (keyword_encrypted)
+ (keyword_increment)
+ (keyword_logged)
+ (keyword_none)
+ (keyword_owned)
+ (keyword_password)
+ (keyword_reset)
+ (keyword_role)
+ (keyword_current_role)
+ (keyword_sequence)
+ (keyword_start)
+ (keyword_restart)
+ (keyword_tablespace)
+ (keyword_split)
+ (keyword_tablets)
+ (keyword_until)
+ (keyword_user)
+ (keyword_current_user)
+ (keyword_session_user)
+ (keyword_valid)
+ (keyword_action)
+ (keyword_definer)
+ (keyword_invoker)
+ (keyword_enable)
+ (keyword_disable)
+ (keyword_security)
+ (keyword_policy)
+ (keyword_permissive)
+ (keyword_restrictive)
+ (keyword_public)
+ (keyword_extension)
+ (keyword_version)
+ (keyword_out)
+ (keyword_inout)
+ (keyword_variadic)
+ (keyword_ordinality)
+ (keyword_session)
+ (keyword_isolation)
+ (keyword_level)
+ (keyword_serializable)
+ (keyword_repeatable)
+ (keyword_read)
+ (keyword_write)
+ (keyword_committed)
+ (keyword_uncommitted)
+ (keyword_deferrable)
+ (keyword_names)
+ (keyword_zone)
+ (keyword_immediate)
+ (keyword_deferred)
+ (keyword_constraints)
+ (keyword_snapshot)
+ (keyword_characteristics)
+ (keyword_off)
+ (keyword_follows)
+ (keyword_precedes)
+ (keyword_each)
+ (keyword_instead)
+ (keyword_of)
+ (keyword_initially)
+ (keyword_old)
+ (keyword_new)
+ (keyword_referencing)
+ (keyword_statement)
+ (keyword_execute)
+ (keyword_procedure)
+ (keyword_copy)
+ (keyword_delimiter)
+ (keyword_encoding)
+ (keyword_escape)
+ (keyword_force_not_null)
+ (keyword_force_null)
+ (keyword_force_quote)
+ (keyword_freeze)
+ (keyword_header)
+ (keyword_match)
+ (keyword_program)
+ (keyword_quote)
+ (keyword_stdin)
+ (keyword_extended)
+ (keyword_main)
+ (keyword_plain)
+ (keyword_storage)
+ (keyword_compression)
+ (keyword_duplicate)
+ (keyword_while)
+] @keyword
+
+[
+ (keyword_restrict)
+ (keyword_unbounded)
+ (keyword_unique)
+ (keyword_cascade)
+ (keyword_delayed)
+ (keyword_high_priority)
+ (keyword_low_priority)
+ (keyword_ignore)
+ (keyword_nothing)
+ (keyword_check)
+ (keyword_option)
+ (keyword_local)
+ (keyword_cascaded)
+ (keyword_wait)
+ (keyword_nowait)
+ (keyword_metadata)
+ (keyword_incremental)
+ (keyword_bin_pack)
+ (keyword_noscan)
+ (keyword_stats)
+ (keyword_statistics)
+ (keyword_maxvalue)
+ (keyword_minvalue)
+] @type.qualifier
+
+[
+ (keyword_int)
+ (keyword_null)
+ (keyword_boolean)
+ (keyword_binary)
+ (keyword_varbinary)
+ (keyword_image)
+ (keyword_bit)
+ (keyword_inet)
+ (keyword_character)
+ (keyword_smallserial)
+ (keyword_serial)
+ (keyword_bigserial)
+ (keyword_smallint)
+ (keyword_mediumint)
+ (keyword_bigint)
+ (keyword_tinyint)
+ (keyword_decimal)
+ (keyword_float)
+ (keyword_double)
+ (keyword_numeric)
+ (keyword_real)
+ (double)
+ (keyword_money)
+ (keyword_smallmoney)
+ (keyword_char)
+ (keyword_nchar)
+ (keyword_varchar)
+ (keyword_nvarchar)
+ (keyword_varying)
+ (keyword_text)
+ (keyword_string)
+ (keyword_uuid)
+ (keyword_json)
+ (keyword_jsonb)
+ (keyword_xml)
+ (keyword_bytea)
+ (keyword_enum)
+ (keyword_date)
+ (keyword_datetime)
+ (keyword_time)
+ (keyword_datetime2)
+ (keyword_datetimeoffset)
+ (keyword_smalldatetime)
+ (keyword_timestamp)
+ (keyword_timestamptz)
+ (keyword_geometry)
+ (keyword_geography)
+ (keyword_box2d)
+ (keyword_box3d)
+ (keyword_interval)
+] @type.builtin
+
+[
+ (keyword_in)
+ (keyword_and)
+ (keyword_or)
+ (keyword_not)
+ (keyword_by)
+ (keyword_on)
+ (keyword_do)
+ (keyword_union)
+ (keyword_except)
+ (keyword_intersect)
+] @keyword.operator
+
+[
+ "+"
+ "-"
+ "*"
+ "/"
+ "%"
+ "^"
+ ":="
+ "="
+ "<"
+ "<="
+ "!="
+ ">="
+ ">"
+ "<>"
+ (op_other)
+ (op_unary_other)
+] @operator
+
+[
+ "("
+ ")"
+] @punctuation.bracket
+
+[
+ ";"
+ ","
+ "."
+] @punctuation.delimiter
diff --git a/Plugins/ZShell/Llm/highlight-queries/toml.scm b/Plugins/ZShell/Llm/highlight-queries/toml.scm
new file mode 100644
index 0000000..b26f3a9
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/toml.scm
@@ -0,0 +1,36 @@
+; Vendored from toml (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-toml
+
+; Properties
+;-----------
+
+(bare_key) @property
+(quoted_key) @string
+
+; Literals
+;---------
+
+(boolean) @constant.builtin
+(comment) @comment
+(string) @string
+(integer) @number
+(float) @number
+(offset_date_time) @string.special
+(local_date_time) @string.special
+(local_date) @string.special
+(local_time) @string.special
+
+; Punctuation
+;------------
+
+"." @punctuation.delimiter
+"," @punctuation.delimiter
+
+"=" @operator
+
+"[" @punctuation.bracket
+"]" @punctuation.bracket
+"[[" @punctuation.bracket
+"]]" @punctuation.bracket
+"{" @punctuation.bracket
+"}" @punctuation.bracket
diff --git a/Plugins/ZShell/Llm/highlight-queries/typescript.scm b/Plugins/ZShell/Llm/highlight-queries/typescript.scm
new file mode 100644
index 0000000..72500ee
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/typescript.scm
@@ -0,0 +1,38 @@
+; Vendored from typescript (MIT License)
+; Source: https://github.com/tree-sitter/tree-sitter-typescript
+
+; Types
+
+(type_identifier) @type
+(predefined_type) @type.builtin
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(type_arguments
+ "<" @punctuation.bracket
+ ">" @punctuation.bracket)
+
+; Variables
+
+(required_parameter (identifier) @variable.parameter)
+(optional_parameter (identifier) @variable.parameter)
+
+; Keywords
+
+[ "abstract"
+ "declare"
+ "enum"
+ "export"
+ "implements"
+ "interface"
+ "keyof"
+ "namespace"
+ "private"
+ "protected"
+ "public"
+ "type"
+ "readonly"
+ "override"
+ "satisfies"
+] @keyword
diff --git a/Plugins/ZShell/Llm/highlight-queries/yaml.scm b/Plugins/ZShell/Llm/highlight-queries/yaml.scm
new file mode 100644
index 0000000..bbfa7e6
--- /dev/null
+++ b/Plugins/ZShell/Llm/highlight-queries/yaml.scm
@@ -0,0 +1,82 @@
+; Vendored from yaml (MIT License)
+; Source: https://github.com/tree-sitter-grammars/tree-sitter-yaml
+
+(boolean_scalar) @boolean
+
+(null_scalar) @constant.builtin
+
+[
+ (double_quote_scalar)
+ (single_quote_scalar)
+ (block_scalar)
+ (string_scalar)
+] @string
+
+[
+ (integer_scalar)
+ (float_scalar)
+] @number
+
+(comment) @comment
+
+[
+ (anchor_name)
+ (alias_name)
+] @label
+
+(tag) @type
+
+[
+ (yaml_directive)
+ (tag_directive)
+ (reserved_directive)
+] @attribute
+
+(block_mapping_pair
+ key: (flow_node
+ [
+ (double_quote_scalar)
+ (single_quote_scalar)
+ ] @property))
+
+(block_mapping_pair
+ key: (flow_node
+ (plain_scalar
+ (string_scalar) @property)))
+
+(flow_mapping
+ (_
+ key: (flow_node
+ [
+ (double_quote_scalar)
+ (single_quote_scalar)
+ ] @property)))
+
+(flow_mapping
+ (_
+ key: (flow_node
+ (plain_scalar
+ (string_scalar) @property))))
+
+[
+ ","
+ "-"
+ ":"
+ ">"
+ "?"
+ "|"
+] @punctuation.delimiter
+
+[
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "*"
+ "&"
+ "---"
+ "..."
+] @punctuation.special
diff --git a/Plugins/ZShell/Llm/llmclient.cpp b/Plugins/ZShell/Llm/llmclient.cpp
new file mode 100644
index 0000000..b29f92b
--- /dev/null
+++ b/Plugins/ZShell/Llm/llmclient.cpp
@@ -0,0 +1,768 @@
+#include "llmclient.hpp"
+
+#include "generation.hpp"
+#include "message.hpp"
+#include "messagemodel.hpp"
+#include "segment.hpp"
+#include "session.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ZShell::llm {
+
+QString LlmClient::completionsPath(
+ const QString& endpoint, const QString& subpath) {
+ QString base = endpoint.trimmed();
+ while (base.endsWith('/'))
+ base.chop(1);
+ if (!base.endsWith("/v1")) base += "/v1";
+ return base + subpath;
+}
+
+QString LlmClient::serverErrorMessage(
+ const QByteArray& body, const QString& fallback) {
+ const QJsonDocument doc = QJsonDocument::fromJson(body);
+ if (doc.isObject()) {
+ const QJsonObject obj = doc.object();
+ if (obj.contains("error")) {
+ const QJsonValue errorValue = obj["error"];
+ if (errorValue.isObject()) {
+ const QString message =
+ errorValue.toObject()["message"].toString();
+ if (!message.isEmpty()) return message;
+ } else if (!errorValue.toString().isEmpty()) {
+ return errorValue.toString();
+ }
+ }
+ }
+ return fallback.isEmpty() ? QStringLiteral("Request to LLM server failed")
+ : fallback;
+}
+
+void LlmClient::setBusy(bool value) {
+ if (m_busy == value) return;
+ m_busy = value;
+ Q_EMIT busyChanged();
+}
+
+void LlmClient::setStreamingChatId(const QString& id) {
+ if (m_streamingChatId == id) return;
+ m_streamingChatId = id;
+ Q_EMIT streamingChatIdChanged();
+}
+
+LlmClient::LlmClient(QObject* parent) : QObject(parent) {
+ m_tools = new ToolRegistry(this);
+ connect(
+ m_tools,
+ &ToolRegistry::enabledChanged,
+ this,
+ &LlmClient::toolsEnabledChanged);
+ probeContextSize();
+}
+
+LlmClient::~LlmClient() {
+ if (m_reply) m_reply->abort();
+ m_tools->cancelAll();
+ endStream();
+}
+
+void LlmClient::setEndpoint(const QString& value) {
+ if (m_endpoint == value) return;
+ m_endpoint = value;
+ Q_EMIT endpointChanged();
+ probeContextSize();
+ if (m_model.isEmpty()) refreshModels();
+}
+
+void LlmClient::setModel(const QString& value) {
+ if (m_model == value) return;
+ m_model = value;
+ Q_EMIT modelChanged();
+ if (m_model.isEmpty()) refreshModels();
+}
+
+void LlmClient::setTemperature(double value) {
+ m_temperature = value;
+}
+
+void LlmClient::startGeneration(ChatSession* session, ChatGeneration* target) {
+ if (m_busy || !session || !target) return;
+ m_active = session;
+ m_streaming = target;
+ m_streaming->setStreaming(true);
+ setBusy(true);
+ setStreamingChatId(session->id());
+
+ const QUrl url =
+ QUrl::fromUserInput(completionsPath(m_endpoint, "/chat/completions"));
+ if (!url.isValid() || url.host().isEmpty()) {
+ fail(QStringLiteral("Invalid LLM endpoint: %1").arg(m_endpoint));
+ return;
+ }
+
+ const auto* model = session->messagesModel();
+ const int targetRow =
+ model->rowOf(qobject_cast(target->parent()));
+ if (targetRow < 0) {
+ fail(QStringLiteral(
+ "Internal error: generation target is not in the "
+ "session"));
+ return;
+ }
+
+ m_transcript = QJsonArray();
+ m_callBuilders.clear();
+ m_callResults.clear();
+ m_finishReason.clear();
+ m_round = 0;
+ m_contentMark = 0;
+ m_reasoningMark = 0;
+
+ sendRound();
+}
+
+void LlmClient::sendRound() {
+ if (!m_active || !m_streaming) return;
+ m_finishReason.clear();
+ m_callBuilders.clear();
+ m_callResults.clear();
+ m_roundDone = false;
+ m_toolPhase = false;
+ m_pendingCalls = 0;
+ m_buffer.clear();
+
+ const QUrl url =
+ QUrl::fromUserInput(completionsPath(m_endpoint, "/chat/completions"));
+ if (!url.isValid() || url.host().isEmpty()) {
+ fail(QStringLiteral("Invalid LLM endpoint: %1").arg(m_endpoint));
+ return;
+ }
+
+ const auto* model = m_active->messagesModel();
+ const int targetRow =
+ model->rowOf(qobject_cast(m_streaming->parent()));
+ if (targetRow < 0) {
+ fail(QStringLiteral(
+ "Internal error: generation target is not in the "
+ "session"));
+ return;
+ }
+
+ QJsonArray messages = buildContextMessages(m_active, targetRow);
+ for (const QJsonValue& value : m_transcript)
+ messages.append(value);
+
+ QJsonObject body;
+ QJsonObject streamOptions;
+ streamOptions[QStringLiteral("include_usage")] = true;
+ body[QStringLiteral("stream_options")] = streamOptions;
+ body[QStringLiteral("messages")] = messages;
+ body[QStringLiteral("stream")] = true;
+ body[QStringLiteral("temperature")] = m_temperature;
+ if (!m_model.isEmpty()) body[QStringLiteral("model")] = m_model;
+ const QJsonArray toolSpecs = m_tools->specifications();
+ if (!toolSpecs.isEmpty()) body[QStringLiteral("tools")] = toolSpecs;
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "text/event-stream");
+
+ m_reply = m_manager.post(request, QJsonDocument(body).toJson());
+
+ connect(m_reply, &QNetworkReply::readyRead, this, [this]() {
+ if (m_reply) m_buffer.append(m_reply->readAll());
+ drainBuffer();
+ });
+ connect(m_reply, &QNetworkReply::finished, this, [this]() {
+ QNetworkReply* reply = m_reply;
+ if (!reply) return;
+ m_reply = nullptr;
+
+ const QNetworkReply::NetworkError error = reply->error();
+ const QString errorString = reply->errorString();
+ const QByteArray responseBody = reply->readAll();
+ m_buffer.append(responseBody);
+ reply->deleteLater();
+
+ drainBuffer();
+ if (!m_streaming) return;
+
+ if (error == QNetworkReply::NoError)
+ roundFinished();
+ else if (error == QNetworkReply::OperationCanceledError)
+ finishTurn();
+ else
+ fail(serverErrorMessage(responseBody, errorString));
+ });
+}
+
+QJsonArray LlmClient::buildContextMessages(
+ ChatSession* session, int stopBeforeRow) const {
+ const auto* model = session->messagesModel();
+ QJsonArray messages;
+ for (int row = model->rowCount() - 1; row > stopBeforeRow; --row) {
+ const auto* message = model->at(row);
+ const auto* generation = message->activeGeneration();
+ if (!generation) continue;
+
+ if (message->role() == ChatMessage::Role::User) {
+ QJsonObject user;
+ user[QStringLiteral("role")] = QStringLiteral("user");
+ user[QStringLiteral("content")] = generation->content();
+ messages.append(user);
+ continue;
+ }
+
+ QList toolSegments;
+ for (const auto* segment : generation->segments())
+ if (segment->type() == LlmSegment::Type::ToolCall)
+ toolSegments.append(segment);
+
+ QJsonObject assistant;
+ assistant[QStringLiteral("role")] = QStringLiteral("assistant");
+ if (toolSegments.isEmpty())
+ assistant[QStringLiteral("content")] = generation->content();
+ else if (!generation->content().isEmpty())
+ assistant[QStringLiteral("content")] = generation->content();
+ if (!generation->reasoning().isEmpty())
+ assistant[QStringLiteral("reasoning_content")] =
+ generation->reasoning();
+
+ if (toolSegments.isEmpty()) {
+ messages.append(assistant);
+ continue;
+ }
+ QJsonArray calls;
+ for (const auto* segment : toolSegments) {
+ QJsonObject function;
+ function[QStringLiteral("name")] = segment->name();
+ function[QStringLiteral("arguments")] = segment->arguments();
+ QJsonObject call;
+ call[QStringLiteral("id")] = segment->toolCallId();
+ call[QStringLiteral("type")] = QStringLiteral("function");
+ call[QStringLiteral("function")] = function;
+ calls.append(call);
+ }
+ assistant[QStringLiteral("tool_calls")] = calls;
+ messages.append(assistant);
+ for (const auto* segment : toolSegments) {
+ QJsonObject toolMessage;
+ toolMessage[QStringLiteral("role")] = QStringLiteral("tool");
+ toolMessage[QStringLiteral("tool_call_id")] = segment->toolCallId();
+ toolMessage[QStringLiteral("content")] = segment->result();
+ messages.append(toolMessage);
+ }
+ }
+ return messages;
+}
+
+void LlmClient::applyToolCallDelta(const QJsonObject& call) {
+ if (!m_streaming) return;
+ const int index = call[QStringLiteral("index")].toInt(-1);
+ if (index < 0) return;
+ while (m_callBuilders.size() <= index)
+ m_callBuilders.append(ToolCallBuilder{});
+ ToolCallBuilder& builder = m_callBuilders[index];
+ builder.seen = true;
+ const QString id = call[QStringLiteral("id")].toString();
+ if (!id.isEmpty()) builder.id = id;
+ const QJsonObject function = call[QStringLiteral("function")].toObject();
+ const QString name = function[QStringLiteral("name")].toString();
+ if (!name.isEmpty()) builder.name = name;
+ const QString arguments = function[QStringLiteral("arguments")].toString();
+ if (!arguments.isEmpty()) builder.arguments += arguments;
+
+ if (!builder.segment) {
+ m_streaming->closeOpenSegments();
+ builder.segment = m_streaming->beginToolCall(builder.name, builder.id);
+ }
+ builder.segment->setName(builder.name);
+ builder.segment->setToolCallId(builder.id);
+ if (!arguments.isEmpty()) builder.segment->appendArguments(arguments);
+}
+
+void LlmClient::roundFinished() {
+ if (m_roundDone || !m_streaming) return;
+ m_roundDone = true;
+
+ bool hasCalls = false;
+ for (const auto& builder : m_callBuilders)
+ if (builder.seen) hasCalls = true;
+ if (m_finishReason != QLatin1String("tool_calls") && !hasCalls) {
+ finishTurn();
+ return;
+ }
+ if (m_round >= kMaxToolRounds) {
+ qWarning() << "LlmClient: tool round limit reached, ending turn";
+ finishTurn();
+ return;
+ }
+
+ m_streaming->closeOpenSegments();
+
+ const QString content = m_streaming->content();
+ const QString reasoning = m_streaming->reasoning();
+ QJsonObject assistant;
+ assistant[QStringLiteral("role")] = QStringLiteral("assistant");
+ if (content.size() > m_contentMark) {
+ assistant[QStringLiteral("content")] = content.mid(m_contentMark);
+ m_contentMark = content.size();
+ }
+ if (reasoning.size() > m_reasoningMark) {
+ assistant[QStringLiteral("reasoning_content")] =
+ reasoning.mid(m_reasoningMark);
+ m_reasoningMark = reasoning.size();
+ }
+ QJsonArray calls;
+ for (const auto& builder : m_callBuilders) {
+ if (!builder.seen) continue;
+ QJsonObject function;
+ function[QStringLiteral("name")] = builder.name;
+ function[QStringLiteral("arguments")] = builder.arguments;
+ QJsonObject call;
+ call[QStringLiteral("id")] = builder.id;
+ call[QStringLiteral("type")] = QStringLiteral("function");
+ call[QStringLiteral("function")] = function;
+ calls.append(call);
+ }
+ assistant[QStringLiteral("tool_calls")] = calls;
+ m_transcript.append(assistant);
+ m_round++;
+
+ executeAllCalls();
+}
+
+void LlmClient::executeAllCalls() {
+ m_toolPhase = true;
+ m_pendingCalls = 0;
+ m_callResults = QList(m_callBuilders.size());
+
+ for (int i = 0; i < m_callBuilders.size(); ++i) {
+ const auto& call = m_callBuilders.at(i);
+ if (!call.seen) continue;
+
+ LlmTool* tool = m_tools->tool(call.name);
+ QJsonObject args;
+ QString errorText;
+ if (!tool) {
+ errorText =
+ QStringLiteral("Error: unknown tool '%1'").arg(call.name);
+ } else if (!call.arguments.isEmpty()) {
+ const QJsonDocument doc =
+ QJsonDocument::fromJson(call.arguments.toUtf8());
+ if (!doc.isObject()) {
+ errorText = QStringLiteral(
+ "Error: tool arguments are not valid "
+ "JSON: %1")
+ .arg(call.arguments);
+ } else {
+ args = doc.object();
+ }
+ }
+ if (!errorText.isEmpty()) {
+ m_callResults[i] = {errorText, false};
+ if (LlmSegment* segment = call.segment)
+ segment->finishTool(errorText, false);
+ continue;
+ }
+
+ ++m_pendingCalls;
+ tool->execute(args, [this, i, call](const QJsonObject& result) {
+ if (!m_streaming) return;
+ const bool success = result.contains(QStringLiteral("output"));
+ const QString content =
+ success ? result[QStringLiteral("output")].toString()
+ : QStringLiteral("Error: ") +
+ result[QStringLiteral("error")].toString();
+ m_callResults[i] = {content, success};
+ if (LlmSegment* segment = call.segment)
+ segment->finishTool(content, success);
+ if (--m_pendingCalls == 0) flushCallResults();
+ });
+ }
+ if (m_pendingCalls == 0) flushCallResults();
+}
+
+void LlmClient::flushCallResults() {
+ if (!m_toolPhase) return;
+ m_toolPhase = false;
+ if (!m_streaming) return;
+ for (int i = 0; i < m_callBuilders.size(); ++i) {
+ const auto& call = m_callBuilders.at(i);
+ if (!call.seen) continue;
+ QJsonObject toolMessage;
+ toolMessage[QStringLiteral("role")] = QStringLiteral("tool");
+ toolMessage[QStringLiteral("tool_call_id")] = call.id;
+ toolMessage[QStringLiteral("content")] = m_callResults.at(i).content;
+ m_transcript.append(toolMessage);
+ }
+ sendRound();
+}
+
+void LlmClient::stop() {
+ if (!m_busy) return;
+ if (m_toolPhase) {
+ m_tools->cancelAll();
+ if (m_streaming) {
+ for (auto* segment : m_streaming->segments()) {
+ if (segment->type() == LlmSegment::Type::ToolCall &&
+ segment->running())
+ segment->finishTool(QStringLiteral("Cancelled"), false);
+ }
+ }
+ finishTurn();
+ return;
+ }
+ if (m_reply) m_reply->abort();
+}
+
+void LlmClient::endStream() {
+ if (!m_streaming) return;
+ auto* generation = m_streaming;
+ auto* session = m_active;
+ m_streaming = nullptr;
+ m_active = nullptr;
+ generation->setStreaming(false);
+ if (generation->content().isEmpty() && generation->reasoning().isEmpty() &&
+ generation->toolCallCount() == 0) {
+ if (auto* message = qobject_cast(generation->parent())) {
+ if (message->generationCount() <= 1) {
+ if (session) session->removeMessage(message);
+ } else {
+ message->removeGeneration(generation);
+ }
+ }
+ }
+ setBusy(false);
+ setStreamingChatId(QString());
+}
+
+void LlmClient::finishTurn() {
+ if (!m_streaming) return;
+ ChatSession* session = m_active;
+ endStream();
+ if (session && m_pendingClear == session) {
+ session->clearMessages();
+ m_pendingClear.clear();
+ }
+ if (session) session->persist();
+}
+
+void LlmClient::clearOnFinish(ChatSession* session) {
+ m_pendingClear = session;
+}
+
+void LlmClient::sessionRemoved(ChatSession* session) {
+ if (m_pendingClear == session) m_pendingClear.clear();
+ if (m_active == session) {
+ stop();
+ endStream();
+ }
+}
+
+void LlmClient::fail(const QString& message) {
+ qWarning() << "LlmClient:" << message;
+ finishTurn();
+ Q_EMIT errorOccurred(message);
+}
+
+void LlmClient::drainBuffer() {
+ while (true) {
+ const qsizetype newline = m_buffer.indexOf('\n');
+ if (newline < 0) break;
+ const QByteArray line = m_buffer.left(newline).trimmed();
+ m_buffer.remove(0, newline + 1);
+ handleLine(line);
+ }
+}
+
+void LlmClient::handleLine(const QByteArray& line) {
+ if (!m_streaming || line.isEmpty() || !line.startsWith("data:")) return;
+
+ const QByteArray data = line.mid(5).trimmed();
+ if (data == "[DONE]") {
+ roundFinished();
+ return;
+ }
+
+ const QJsonDocument doc = QJsonDocument::fromJson(data);
+ if (!doc.isObject()) return;
+ const QJsonObject obj = doc.object();
+ updateTokenUsage(obj);
+
+ if (obj.contains("error")) {
+ const QJsonObject error = obj["error"].toObject();
+ const QString message = error["message"].toString();
+ fail(
+ message.isEmpty() ? QStringLiteral("LLM server returned an error")
+ : message);
+ return;
+ }
+
+ for (const QJsonValue& choiceValue : obj["choices"].toArray()) {
+ if (!m_streaming) continue;
+ const QJsonObject choice = choiceValue.toObject();
+ const QJsonObject delta = choice["delta"].toObject();
+
+ const QString finishReason =
+ choice[QStringLiteral("finish_reason")].toString();
+ if (!finishReason.isEmpty()) m_finishReason = finishReason;
+
+ m_streaming->appendContent(delta["content"].toString());
+ QString reasoning = delta["reasoning_content"].toString();
+ if (reasoning.isEmpty()) reasoning = delta["reasoning"].toString();
+ m_streaming->appendReasoning(reasoning);
+
+ for (const QJsonValue& callValue : delta["tool_calls"].toArray()) {
+ if (!m_streaming) break;
+ applyToolCallDelta(callValue.toObject());
+ }
+ }
+}
+
+void LlmClient::refreshModels() {
+ const QUrl url =
+ QUrl::fromUserInput(completionsPath(m_endpoint, "/models"));
+ if (!url.isValid() || url.host().isEmpty()) return;
+
+ auto* reply = m_manager.get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+ const QNetworkReply::NetworkError error = reply->error();
+ const QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "LlmClient: failed to fetch models:" << error;
+ return;
+ }
+ const QJsonArray dataArr =
+ QJsonDocument::fromJson(data).object()["data"].toArray();
+
+ QStringList models;
+ QSet seen;
+ for (const QJsonValue& value : dataArr) {
+ const QString id = value.toObject()["id"].toString();
+ if (!id.isEmpty() && !seen.contains(id)) {
+ seen.insert(id);
+ models.append(id);
+ }
+ }
+ if (models.isEmpty()) return;
+
+ m_availableModels = models;
+ Q_EMIT availableModelsChanged();
+
+ if (m_model.isEmpty()) {
+ m_model = models.first();
+ Q_EMIT modelChanged();
+ }
+ });
+}
+
+void LlmClient::setContextSize(int size) {
+ if (size <= 0 || m_contextSize == size) return;
+ m_contextSize = size;
+ Q_EMIT contextSizeChanged();
+}
+
+void LlmClient::probeContextSize() {
+ QString base = m_endpoint.trimmed();
+ while (base.endsWith('/'))
+ base.chop(1);
+ const QUrl url = QUrl::fromUserInput(base + "/props");
+ if (!url.isValid() || url.host().isEmpty()) return;
+
+ auto* reply = m_manager.get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+ const QNetworkReply::NetworkError error = reply->error();
+ const QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ int size = 0;
+ if (error == QNetworkReply::NoError) {
+ const QJsonDocument doc = QJsonDocument::fromJson(data);
+ if (doc.isArray()) {
+ for (const auto& value : doc.array()) {
+ const QJsonObject slot = value.toObject();
+ if (slot.contains("n_ctx")) {
+ size = slot["n_ctx"].toInt(0);
+ if (size > 0) break;
+ }
+ }
+ } else if (doc.isObject()) {
+ const QJsonObject obj = doc.object();
+ size = obj["n_ctx"].toInt(0);
+ if (size <= 0)
+ size = obj["default_generation_settings"]
+ .toObject()["n_ctx"]
+ .toInt(0);
+ }
+ }
+ setContextSize(size > 0 ? size : 4096);
+ });
+}
+
+void LlmClient::updateTokenUsage(const QJsonObject& data) {
+ if (!m_active || m_contextSize <= 0) return;
+ const QJsonObject usage = data["usage"].toObject();
+ if (usage.isEmpty()) return;
+ const double used = usage.value("prompt_tokens").toDouble() +
+ usage.value("completion_tokens").toDouble();
+ if (used > 0) m_active->setLastTokenCount(static_cast(used));
+}
+
+void LlmClient::shortRequest(
+ const QString& tag,
+ const QString& systemPrompt,
+ const QString& userText,
+ std::function onResult) {
+ const QUrl url =
+ QUrl::fromUserInput(completionsPath(m_endpoint, "/chat/completions"));
+ if (!url.isValid() || url.host().isEmpty()) return;
+
+ qInfo() << "LlmClient:" << tag << "request POST" << url.toString()
+ << "model=" << m_model;
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+
+ QJsonArray messages;
+ QJsonObject system;
+ system[QStringLiteral("role")] = QStringLiteral("system");
+ system[QStringLiteral("content")] = systemPrompt;
+ messages.append(system);
+ QJsonObject user;
+ user[QStringLiteral("role")] = QStringLiteral("user");
+ user[QStringLiteral("content")] = userText.simplified().mid(0, 512);
+ messages.append(user);
+
+ QJsonObject body;
+ if (!m_model.isEmpty()) body[QStringLiteral("model")] = m_model;
+ body[QStringLiteral("stream")] = false;
+ body[QStringLiteral("temperature")] = 0.3;
+ body[QStringLiteral("max_tokens")] = 128;
+ QJsonObject templateKwargs;
+ templateKwargs[QStringLiteral("enable_thinking")] = false;
+ body[QStringLiteral("chat_template_kwargs")] = templateKwargs;
+ body[QStringLiteral("messages")] = messages;
+
+ auto* reply = m_manager.post(request, QJsonDocument(body).toJson());
+ connect(
+ reply,
+ &QNetworkReply::finished,
+ this,
+ [this, reply, tag, onResult = std::move(onResult)]() {
+ const QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ qInfo() << "LlmClient:" << tag << "request finished"
+ << "error=" << reply->error() << reply->errorString()
+ << "http="
+ << reply
+ ->attribute(QNetworkRequest::HttpStatusCodeAttribute)
+ .toInt()
+ << "response="
+ << QString::fromUtf8(data.left(400)).simplified();
+
+ if (reply->error() != QNetworkReply::NoError) return;
+
+ const QJsonDocument doc = QJsonDocument::fromJson(data);
+ const QJsonArray choices =
+ doc.object()[QStringLiteral("choices")].toArray();
+ if (choices.isEmpty()) return;
+ const QString result = choices.at(0)
+ .toObject()[QStringLiteral("message")]
+ .toObject()[QStringLiteral("content")]
+ .toString()
+ .trimmed();
+ qInfo() << "LlmClient:" << tag << "raw result" << result;
+ onResult(result);
+ });
+}
+
+void LlmClient::requestTitle(ChatSession* session, const QString& userText) {
+ shortRequest(
+ QStringLiteral("title"),
+ QStringLiteral(
+ "Write a short, concise title for a chat conversation starting "
+ "with the user's message below. At most six words, no quotation "
+ "marks, no trailing punctuation. Reply with the title only."),
+ userText,
+ [this, session = QPointer(session)](QString title) {
+ if (!session) {
+ qWarning() << "LlmClient: title request: session gone";
+ return;
+ }
+ const auto isQuote = [](QChar c) {
+ return c == QLatin1Char('"') || c == QLatin1Char('\'') ||
+ c == QChar(u'\u201C') || c == QChar(u'\u201D') ||
+ c == QChar(u'\u2018') || c == QChar(u'\u2019');
+ };
+ while (title.size() >= 2 && isQuote(title.at(0)) &&
+ isQuote(title.at(title.size() - 1)))
+ title = title.mid(1, title.size() - 2).simplified();
+ while (!title.isEmpty() && (title.endsWith(QLatin1Char('.')) ||
+ title.endsWith(QLatin1Char('!')) ||
+ title.endsWith(QLatin1Char('?'))))
+ title.chop(1);
+ if (title.size() < 2) {
+ qWarning() << "LlmClient: title rejected (too short)" << title;
+ return;
+ }
+ if (title.size() > 48) title = title.left(47) + QStringLiteral("…");
+ qInfo() << "LlmClient: suggesting title" << title;
+ Q_EMIT titleSuggested(session, title);
+ });
+}
+
+void LlmClient::requestIcon(ChatSession* session, const QString& userText) {
+ const QStringList icons = {
+ "chat", "lightbulb", "code", "description",
+ "article", "school", "work", "build",
+ "science", "palette", "music_note", "sports_esports",
+ "takeout_dining", "flight", "photo_camera", "psychology_alt",
+ "favorite", "savings", "gamepad", "auto_awesome",
+ };
+ const QString prompt =
+ QStringLiteral(
+ "Pick the single icon name from this list that best matches the "
+ "topic of the user's message below: %1. Reply with only the icon "
+ "name, exactly as written in the list, and nothing else.")
+ .arg(icons.join(QStringLiteral(", ")));
+ shortRequest(
+ QStringLiteral("icon"),
+ prompt,
+ userText,
+ [this, session = QPointer(session), icons](QString name) {
+ if (!session) {
+ qWarning() << "LlmClient: icon request: session gone";
+ return;
+ }
+ name = name.simplified().toLower();
+ const auto isQuote = [](QChar c) {
+ return c == QLatin1Char('"') || c == QLatin1Char('\'');
+ };
+ while (name.size() >= 2 && isQuote(name.at(0)) &&
+ isQuote(name.at(name.size() - 1)))
+ name = name.mid(1, name.size() - 2).simplified();
+ name.replace(QLatin1Char(' '), QLatin1Char('_'));
+ if (!icons.contains(name)) {
+ qWarning() << "LlmClient: icon not in list, using default"
+ << name;
+ name = QStringLiteral("chat");
+ }
+ qInfo() << "LlmClient: suggesting icon" << name;
+ Q_EMIT iconSuggested(session, name);
+ });
+}
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/llmclient.hpp b/Plugins/ZShell/Llm/llmclient.hpp
new file mode 100644
index 0000000..56fdfc4
--- /dev/null
+++ b/Plugins/ZShell/Llm/llmclient.hpp
@@ -0,0 +1,140 @@
+#pragma once
+
+#include "segment.hpp"
+#include "tool.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+class QJsonObject;
+class QNetworkReply;
+
+namespace ZShell::llm {
+
+class ChatGeneration;
+class ChatSession;
+class LlmSegment;
+class LlmTool;
+
+class LlmClient : public QObject {
+ Q_OBJECT
+
+ public:
+ explicit LlmClient(QObject* parent = nullptr);
+ ~LlmClient() override;
+
+ [[nodiscard]] QString endpoint() const { return m_endpoint; }
+ [[nodiscard]] QString model() const { return m_model; }
+ [[nodiscard]] double temperature() const { return m_temperature; }
+ [[nodiscard]] QStringList availableModels() const {
+ return m_availableModels;
+ }
+ [[nodiscard]] int contextSize() const { return m_contextSize; }
+ [[nodiscard]] ToolRegistry* tools() const { return m_tools; }
+ void setEndpoint(const QString& value);
+ void setModel(const QString& value);
+ void setTemperature(double value);
+ void setContextSize(int size);
+ void probeContextSize();
+
+ [[nodiscard]] bool busy() const { return m_busy; }
+ [[nodiscard]] bool toolsEnabled() const { return m_tools->enabled(); }
+ void setToolsEnabled(bool value) { m_tools->setEnabled(value); }
+ [[nodiscard]] QString streamingChatId() const { return m_streamingChatId; }
+ [[nodiscard]] ChatSession* streamingSession() const { return m_active; }
+
+ void startGeneration(ChatSession* session, ChatGeneration* target);
+ void stop();
+ void endStream();
+ void clearOnFinish(ChatSession* session);
+ void sessionRemoved(ChatSession* session);
+
+ void refreshModels();
+ void requestTitle(ChatSession* session, const QString& userText);
+ void requestIcon(ChatSession* session, const QString& userText);
+
+ Q_SIGNALS:
+ void busyChanged();
+ void endpointChanged();
+ void modelChanged();
+ void availableModelsChanged();
+ void contextSizeChanged();
+ void toolsEnabledChanged();
+ void streamingChatIdChanged();
+ void errorOccurred(const QString& message);
+ void titleSuggested(ZShell::llm::ChatSession* session, const QString& title);
+ void iconSuggested(ZShell::llm::ChatSession* session, const QString& icon);
+
+ private:
+ struct ToolCallBuilder {
+ QString id;
+ QString name;
+ QString arguments;
+ QPointer segment;
+ bool seen = false;
+ };
+
+ void sendRound();
+ QJsonArray buildContextMessages(
+ ChatSession* session, int stopBeforeRow) const;
+ void applyToolCallDelta(const QJsonObject& call);
+ void roundFinished();
+ void executeAllCalls();
+ void flushCallResults();
+ void finishTurn();
+ void fail(const QString& message);
+ void handleLine(const QByteArray& line);
+ void drainBuffer();
+ void updateTokenUsage(const QJsonObject& data);
+ void setBusy(bool value);
+ void setStreamingChatId(const QString& id);
+ void shortRequest(
+ const QString& tag,
+ const QString& systemPrompt,
+ const QString& userText,
+ std::function onResult);
+ static QString completionsPath(
+ const QString& endpoint, const QString& subpath);
+ static QString serverErrorMessage(
+ const QByteArray& body, const QString& fallback);
+
+ QNetworkAccessManager m_manager;
+ ToolRegistry* m_tools = nullptr;
+ QNetworkReply* m_reply = nullptr;
+ QByteArray m_buffer;
+ ChatSession* m_active = nullptr;
+ ChatGeneration* m_streaming = nullptr;
+ QPointer m_pendingClear;
+ bool m_busy = false;
+ QString m_streamingChatId;
+ QString m_endpoint;
+ QString m_model;
+ QStringList m_availableModels;
+ double m_temperature = 0.7;
+ int m_contextSize = 0;
+
+ QJsonArray m_transcript;
+ QList m_callBuilders;
+ struct ToolCallResult {
+ QString content;
+ bool success = false;
+ };
+ QList m_callResults;
+ QString m_finishReason;
+ int m_round = 0;
+ qsizetype m_contentMark = 0;
+ qsizetype m_reasoningMark = 0;
+ bool m_roundDone = false;
+ bool m_toolPhase = false;
+ int m_pendingCalls = 0;
+ static constexpr int kMaxToolRounds = 12;
+};
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/markdownblock.hpp b/Plugins/ZShell/Llm/markdownblock.hpp
new file mode 100644
index 0000000..7242ed3
--- /dev/null
+++ b/Plugins/ZShell/Llm/markdownblock.hpp
@@ -0,0 +1,25 @@
+#pragma once
+
+#include
+#include
+
+namespace ZShell::llm {
+
+class LlmMarkdown : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("Blocks are produced by LlmSegment")
+
+ public:
+ enum class Type : int {
+ Text = 0, // Paragraph, list, quote, table; "text" is markdown source
+ Heading, // "level" + "text" (markdown source of the content)
+ Code, // "language" + "code"
+ Math // "latex" (display math, without the $$ delimiters)
+ };
+ Q_ENUM(Type)
+
+ explicit LlmMarkdown(QObject* parent = nullptr) : QObject(parent) {}
+};
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/markdownparser.cpp b/Plugins/ZShell/Llm/markdownparser.cpp
new file mode 100644
index 0000000..756334d
--- /dev/null
+++ b/Plugins/ZShell/Llm/markdownparser.cpp
@@ -0,0 +1,134 @@
+#include "markdownparser.hpp"
+
+#include "markdownblock.hpp"
+
+#include
+#include
+
+#include
+#include
+#include
+
+namespace ZShell::llm {
+
+QVariantList MarkdownParser::parse(const QString& source) {
+ QVariantList blocks;
+ if (source.trimmed().isEmpty()) return blocks;
+
+ const QStringList lines = source.split('\n');
+
+ auto sliceSource = [&](int startLine, int endLine) -> QString {
+ if (startLine < 1 || endLine < startLine) return QString();
+ const int from = startLine;
+ const int to = qMin(endLine, static_cast(lines.size()));
+ return lines.mid(from - 1, to - from + 1).join('\n').trimmed();
+ };
+
+ const QByteArray utf8 = source.toUtf8();
+ cmark_node* doc = cmark_parse_document(
+ utf8.constData(),
+ static_cast(utf8.size()),
+ CMARK_OPT_DEFAULT | CMARK_OPT_SOURCEPOS);
+ if (!doc) return blocks;
+
+ const QRegularExpression mathRe(
+ QStringLiteral("\\$\\$(.+?)\\$\\$"),
+ QRegularExpression::DotMatchesEverythingOption);
+
+ auto makeBlock = [&](LlmMarkdown::Type type) {
+ QVariantMap block;
+ block.insert("type", static_cast(type));
+ block.insert(
+ "id",
+ QString::number(static_cast(blocks.size())) +
+ QLatin1Char(':') + QString::number(static_cast(type)));
+ return block;
+ };
+
+ auto appendText = [&](const QString& text) {
+ if (text.trimmed().isEmpty()) return;
+ QVariantMap block = makeBlock(LlmMarkdown::Type::Text);
+ block.insert("text", text);
+ blocks.append(block);
+ };
+
+ auto appendMath = [&](const QString& latex) {
+ if (latex.trimmed().isEmpty()) return;
+ QVariantMap block = makeBlock(LlmMarkdown::Type::Math);
+ block.insert("latex", latex);
+ blocks.append(block);
+ };
+
+ auto appendCode = [&](const QString& language, const QString& code) {
+ QVariantMap block = makeBlock(LlmMarkdown::Type::Code);
+ block.insert("language", language);
+ block.insert("code", code);
+ blocks.append(block);
+ };
+
+ auto appendHeading = [&](int level, const QString& text) {
+ if (text.trimmed().isEmpty()) return;
+ QVariantMap block = makeBlock(LlmMarkdown::Type::Heading);
+ block.insert("level", level);
+ block.insert("text", text);
+ blocks.append(block);
+ };
+
+ for (cmark_node* node = cmark_node_first_child(doc); node;
+ node = cmark_node_next(node)) {
+ const cmark_node_type type = cmark_node_get_type(node);
+ const int startLine = cmark_node_get_start_line(node);
+ const int endLine = cmark_node_get_end_line(node);
+
+ if (type == CMARK_NODE_CODE_BLOCK) {
+ const char* literal = cmark_node_get_literal(node);
+ QString code = literal ? QString::fromUtf8(literal) : QString();
+ if (code.endsWith('\n')) code.chop(1);
+
+ QString language;
+ if (const char* info = cmark_node_get_fence_info(node); info)
+ language = QString::fromUtf8(info)
+ .section(' ', 0, 0)
+ .trimmed()
+ .toLower();
+
+ appendCode(language, code);
+ continue;
+ }
+
+ if (type == CMARK_NODE_HEADING) {
+ const char* content = cmark_node_get_string_content(node);
+ appendHeading(
+ cmark_node_get_heading_level(node),
+ content ? QString::fromUtf8(content).trimmed() : QString());
+ continue;
+ }
+
+ if (type == CMARK_NODE_PARAGRAPH) {
+ const QString text = sliceSource(startLine, endLine);
+ int cursor = 0;
+ bool anyMath = false;
+ for (auto it = mathRe.globalMatch(text, cursor); it.hasNext();
+ it = mathRe.globalMatch(text, cursor)) {
+ const QRegularExpressionMatch m = it.next();
+ anyMath = true;
+ appendText(text.mid(
+ cursor, static_cast(m.capturedStart() - cursor)));
+ appendMath(m.captured(1).trimmed());
+ cursor = static_cast(m.capturedEnd());
+ }
+ if (!anyMath)
+ appendText(text);
+ else
+ appendText(text.mid(cursor));
+ continue;
+ }
+
+ appendText(sliceSource(startLine, endLine));
+ }
+
+ cmark_node_free(doc);
+ return blocks;
+}
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/markdownparser.hpp b/Plugins/ZShell/Llm/markdownparser.hpp
new file mode 100644
index 0000000..433e06b
--- /dev/null
+++ b/Plugins/ZShell/Llm/markdownparser.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include
+#include
+
+namespace ZShell::llm {
+
+class MarkdownParser {
+ public:
+ [[nodiscard]] static QVariantList parse(const QString& source);
+};
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/mathtext.cpp b/Plugins/ZShell/Llm/mathtext.cpp
new file mode 100644
index 0000000..8abcede
--- /dev/null
+++ b/Plugins/ZShell/Llm/mathtext.cpp
@@ -0,0 +1,226 @@
+#include "mathtext.hpp"
+
+#include "latinmodern-fonts.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ZShell::llm {
+
+namespace {
+constexpr int kRenderMargin = 2;
+constexpr unsigned int kResolutionDpi = 96;
+constexpr int kCacheLimit = 512;
+
+struct LatinModern {
+ bool roman = false;
+ bool math = false;
+};
+
+const LatinModern& loadLatinModern() {
+ static const LatinModern fonts = [] {
+ LatinModern result;
+ const auto add = [](const unsigned char* data,
+ size_t size,
+ const QString& fileName,
+ const QString& family) {
+ const QString path = QDir::tempPath() + QLatin1Char('/') + fileName;
+ {
+ QFile f(path);
+ if (!f.open(QIODevice::WriteOnly) ||
+ f.write(
+ reinterpret_cast(data),
+ static_cast(size)) != static_cast(size))
+ return false;
+ }
+ const int key = QFontDatabase::addApplicationFont(path);
+ if (key < 0) return false;
+ return QFontDatabase::applicationFontFamilies(key).contains(family);
+ };
+ result.roman = add(lmfont::lmroman10_regular,
+ sizeof(lmfont::lmroman10_regular),
+ QStringLiteral("lmroman10-regular.otf"),
+ QStringLiteral("LMRoman10")) &&
+ add(lmfont::lmroman10_italic,
+ sizeof(lmfont::lmroman10_italic),
+ QStringLiteral("lmroman10-italic.otf"),
+ QStringLiteral("LMRoman10")) &&
+ add(lmfont::lmroman10_bold,
+ sizeof(lmfont::lmroman10_bold),
+ QStringLiteral("lmroman10-bold.otf"),
+ QStringLiteral("LMRoman10")) &&
+ add(lmfont::lmroman10_bolditalic,
+ sizeof(lmfont::lmroman10_bolditalic),
+ QStringLiteral("lmroman10-bolditalic.otf"),
+ QStringLiteral("LMRoman10"));
+ result.math =
+ add(lmfont::latinmodern_math,
+ sizeof(lmfont::latinmodern_math),
+ QStringLiteral("latinmodern-math.otf"),
+ QStringLiteral("Latin Modern Math"));
+ return result;
+ }();
+ return fonts;
+}
+
+} // namespace
+
+LlmMathText::LlmMathText(QObject* parent)
+ : QObject(parent)
+ , m_renderer(
+ std::make_shared(nullptr, /* useFontsForGUI */ true)) {
+ const LatinModern& fonts = loadLatinModern();
+ if (fonts.roman)
+ m_renderer->setFontRomanAndMath(
+ QStringLiteral("LMRoman10"), JKQTMathTextFontEncoding::MTFEUnicode);
+ if (fonts.math) {
+ m_renderer->setFontMathRoman(
+ QStringLiteral("Latin Modern Math"),
+ JKQTMathTextFontEncoding::MTFEUnicode);
+ m_renderer->setFallbackFontSymbols(
+ QStringLiteral("Latin Modern Math"),
+ JKQTMathTextFontEncoding::MTFEUnicode);
+ }
+}
+
+void LlmMathText::setLatex(const QString& value) {
+ if (m_latex == value) return;
+ m_latex = value;
+ reRender();
+}
+
+void LlmMathText::setColor(const QColor& value) {
+ if (m_color == value) return;
+ m_color = value;
+ reRender();
+}
+
+void LlmMathText::setFontPointSize(double value) {
+ if (qFuzzyCompare(m_fontPointSize, value)) return;
+ m_fontPointSize = value;
+ reRender();
+}
+
+void LlmMathText::setDevicePixelRatio(qreal value) {
+ if (qFuzzyCompare(m_devicePixelRatio, value)) return;
+ m_devicePixelRatio = value;
+ reRender();
+}
+
+namespace {
+
+struct MathRender {
+ bool ok = false;
+ QImage image;
+ QUrl url;
+ qreal width = 0;
+ qreal height = 0;
+};
+
+QHash& mathCache() {
+ static QHash cache;
+ return cache;
+}
+
+} // namespace
+
+void LlmMathText::reRender() {
+ ++m_requestId;
+ if (m_latex.trimmed().isEmpty()) {
+ m_image = QImage();
+ m_imageUrl = QUrl();
+ m_width = 0;
+ m_height = 0;
+ m_ok = false;
+ Q_EMIT changed();
+ return;
+ }
+
+ const QString key = m_latex + QLatin1Char(0x1f) + m_color.name() +
+ QLatin1Char(0x1f) + QString::number(m_fontPointSize) +
+ QLatin1Char(0x1f) + QString::number(m_devicePixelRatio);
+ if (auto it = mathCache().find(key); it != mathCache().end()) {
+ m_image = it->image;
+ m_imageUrl = it->url;
+ m_width = it->width;
+ m_height = it->height;
+ m_ok = it->ok;
+ Q_EMIT changed();
+ return;
+ }
+
+ if (m_inFlight) return;
+ m_inFlight = true;
+
+ const QString latex = m_latex;
+ const QColor color = m_color;
+ const double pointSize = m_fontPointSize;
+ const qreal dpr = m_devicePixelRatio;
+ auto renderer = m_renderer;
+ QThreadPool::globalInstance()->start(
+ [this, renderer, id = m_requestId, key, latex, color, pointSize, dpr]() {
+ MathRender render;
+ renderer->setFontPointSize(pointSize);
+ renderer->setFontColor(color);
+ if (renderer->parse(
+ latex,
+ JKQTMathText::LatexParser,
+ JKQTMathText::DefaultParseOptions)) {
+ const QImage image = renderer->drawIntoImage(
+ /* drawBoxes */ false,
+ QColor(Qt::transparent),
+ kRenderMargin,
+ dpr,
+ kResolutionDpi);
+ if (!image.isNull()) {
+ QByteArray png;
+ {
+ QBuffer buffer(&png);
+ buffer.open(QIODevice::WriteOnly);
+ image.save(&buffer, "PNG");
+ }
+ render.image = image;
+ render.url = QUrl(
+ QStringLiteral("data:image/png;base64,") +
+ QString::fromLatin1(png.toBase64()));
+ render.width = image.width() / dpr;
+ render.height = image.height() / dpr;
+ render.ok = true;
+ }
+ }
+ QPointer guard(this);
+ QMetaObject::invokeMethod(
+ QCoreApplication::instance(),
+ [guard, id, key, render = std::move(render)]() mutable {
+ LlmMathText* self = guard;
+ if (!self) return;
+ self->m_inFlight = false;
+ if (id != self->m_requestId) {
+ self->reRender();
+ return;
+ }
+ if (render.ok) {
+ auto& cache = mathCache();
+ if (cache.size() >= kCacheLimit) cache.clear();
+ cache.insert(key, render);
+ }
+ self->m_image = render.image;
+ self->m_imageUrl = render.url;
+ self->m_width = render.width;
+ self->m_height = render.height;
+ self->m_ok = render.ok;
+ Q_EMIT self->changed();
+ },
+ Qt::QueuedConnection);
+ });
+}
+
+} // namespace ZShell::llm
diff --git a/Plugins/ZShell/Llm/mathtext.hpp b/Plugins/ZShell/Llm/mathtext.hpp
new file mode 100644
index 0000000..3f97a59
--- /dev/null
+++ b/Plugins/ZShell/Llm/mathtext.hpp
@@ -0,0 +1,71 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include