From 562770595f1f254e5430ea9fc6b2cab8c3bccfc3 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 3 Jul 2026 12:28:54 +0200 Subject: [PATCH 01/11] fix: add generic gpu vram support --- Config/DashboardConfig.qml | 1 + Helpers/NetworkUsage.qml | 8 +- Modules/Resources/Cards/MemoryCard.qml | 1 + Modules/Resources/Cards/NetworkCard.qml | 3 - Modules/Resources/Cards/VramCard.qml | 89 +++++++++++++++++++ Modules/Resources/Content.qml | 25 +++++- .../Settings/Pages/Panels/ResourcesPanel.qml | 8 ++ Plugins/ZShell/Services/gpu.cpp | 41 +++++++++ Plugins/ZShell/Services/gpu.hpp | 1 + 9 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 Modules/Resources/Cards/VramCard.qml diff --git a/Config/DashboardConfig.qml b/Config/DashboardConfig.qml index 6f2e38e..7bd7fe4 100644 --- a/Config/DashboardConfig.qml +++ b/Config/DashboardConfig.qml @@ -17,6 +17,7 @@ JsonObject { property bool showMemory: true property bool showNetwork: true property bool showStorage: true + property bool showVram: true } component Sizes: JsonObject { readonly property int dateTimeWidth: 110 diff --git a/Helpers/NetworkUsage.qml b/Helpers/NetworkUsage.qml index f206d2c..49be574 100644 --- a/Helpers/NetworkUsage.qml +++ b/Helpers/NetworkUsage.qml @@ -9,31 +9,25 @@ import qs.Config Singleton { id: root - // Private properties property real _downloadSpeed: 0 property real _downloadTotal: 0 - // Initial readings for calculating totals property real _initialRxBytes: 0 property real _initialTxBytes: 0 property bool _initialized: false - // Previous readings for calculating speed property real _prevRxBytes: 0 property real _prevTimestamp: 0 property real _prevTxBytes: 0 property real _uploadSpeed: 0 property real _uploadTotal: 0 - // History buffers for sparkline readonly property CircularBuffer downloadBuffer: _downloadBuffer - // Current speeds in bytes per second readonly property real downloadSpeed: _downloadSpeed - // Total bytes transferred since tracking started readonly property real downloadTotal: _downloadTotal - readonly property int historyLength: 30 + readonly property int historyLength: Config.dashboard.performance.showVram ? 70 : 30 property int refCount: 0 readonly property CircularBuffer uploadBuffer: _uploadBuffer readonly property real uploadSpeed: _uploadSpeed diff --git a/Modules/Resources/Cards/MemoryCard.qml b/Modules/Resources/Cards/MemoryCard.qml index 1f8c2fd..3130d51 100644 --- a/Modules/Resources/Cards/MemoryCard.qml +++ b/Modules/Resources/Cards/MemoryCard.qml @@ -9,6 +9,7 @@ CustomRect { readonly property color accent: DynamicColors.palette.m3tertiary + Layout.fillWidth: true color: DynamicColors.tPalette.m3surfaceContainer implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 implicitWidth: layout.implicitWidth + Appearance.padding.extraLargeIncreased * 2 diff --git a/Modules/Resources/Cards/NetworkCard.qml b/Modules/Resources/Cards/NetworkCard.qml index b3bbebc..7ddb26b 100644 --- a/Modules/Resources/Cards/NetworkCard.qml +++ b/Modules/Resources/Cards/NetworkCard.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Layouts import ZShell.Internal -import qs.Modules.Resources import qs.Helpers import qs.Components import qs.Config @@ -9,8 +8,6 @@ import qs.Config CustomRect { id: root - required property Wrapper wrapper - color: DynamicColors.tPalette.m3surfaceContainer implicitHeight: 220 implicitWidth: 300 diff --git a/Modules/Resources/Cards/VramCard.qml b/Modules/Resources/Cards/VramCard.qml new file mode 100644 index 0000000..4cee29e --- /dev/null +++ b/Modules/Resources/Cards/VramCard.qml @@ -0,0 +1,89 @@ +import QtQuick +import QtQuick.Layouts +import ZShell.Services +import qs.Components +import qs.Config + +CustomRect { + id: root + + readonly property color accent: DynamicColors.palette.m3tertiary + + Layout.fillWidth: true + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + implicitWidth: layout.implicitWidth + Appearance.padding.extraLargeIncreased + radius: Appearance.rounding.medium + + ServiceRef { + service: Gpu + } + + ColumnLayout { + id: layout + + anchors.centerIn: parent + spacing: Appearance.spacing.extraSmall + + RowLayout { + Layout.leftMargin: -Appearance.padding.extraSmall + spacing: Appearance.spacing.small + + MaterialIcon { + color: root.accent + fill: 1 + text: "memory_alt" + } + + CustomText { + text: qsTr("Video memory") + } + } + + CircularProgress { + id: circularIndicator + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.spacing.large + fgColor: root.accent + implicitSize: usageColumn.implicitHeight + thickness + Appearance.padding.largeIncreased * 2 + startAngle: -225 + sweepAngle: 270 + value: Gpu.memoryUsed / Gpu.memoryTotal + + Behavior on clampedVal { + Anim { + } + } + + ColumnLayout { + id: usageColumn + + anchors.centerIn: parent + anchors.verticalCenterOffset: Appearance.padding.extraSmall + spacing: 0 + + CustomText { + Layout.alignment: Qt.AlignHCenter + color: root.accent + font.pointSize: Appearance.font.size.large + text: Math.round(circularIndicator.value * 100) + "%" + } + + CustomText { + Layout.alignment: Qt.AlignHCenter + color: DynamicColors.palette.m3onSurfaceVariant + text: qsTr("Used") + } + } + } + + CustomText { + Layout.alignment: Qt.AlignHCenter + text: { + const fmt = UsageFmt.formatKib(Gpu.memoryUsed, Gpu.memoryTotal); + return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`; + } + } + } +} diff --git a/Modules/Resources/Content.qml b/Modules/Resources/Content.qml index 55bcff1..359f1c4 100644 --- a/Modules/Resources/Content.qml +++ b/Modules/Resources/Content.qml @@ -74,7 +74,7 @@ Item { RowLayout { spacing: Appearance.spacing.normal - visible: storageCard.active || networkCard.active || memoryCard.active + visible: storageCard.active || memoryCard.active || vramCard.active || networkCard1.active WrappedLoader { id: storageCard @@ -95,15 +95,32 @@ Item { } WrappedLoader { - id: networkCard + id: vramCard - active: Config.dashboard.performance.showNetwork + active: Config.dashboard.performance.showVram + + sourceComponent: VramCard { + } + } + + WrappedLoader { + id: networkCard1 + + active: Config.dashboard.performance.showNetwork && !vramCard.active sourceComponent: NetworkCard { - wrapper: root.wrapper } } } + + WrappedLoader { + id: networkCard2 + + active: Config.dashboard.performance.showNetwork && vramCard.active + + sourceComponent: NetworkCard { + } + } } WrappedLoader { diff --git a/Modules/Settings/Pages/Panels/ResourcesPanel.qml b/Modules/Settings/Pages/Panels/ResourcesPanel.qml index 11e9f5b..9e41e3a 100644 --- a/Modules/Settings/Pages/Panels/ResourcesPanel.qml +++ b/Modules/Settings/Pages/Panels/ResourcesPanel.qml @@ -56,6 +56,14 @@ PageBase { onToggled: Config.dashboard.performance.showGpu = checked } + ToggleRow { + checked: Config.dashboard.performance.showVram + settingAnchor: "resources-vram" + text: qsTr("Video memory") + + onToggled: Config.dashboard.performance.showVram = checked + } + ToggleRow { checked: Config.dashboard.performance.showCpu settingAnchor: "resources-cpu" diff --git a/Plugins/ZShell/Services/gpu.cpp b/Plugins/ZShell/Services/gpu.cpp index f1bcf94..a742e8c 100644 --- a/Plugins/ZShell/Services/gpu.cpp +++ b/Plugins/ZShell/Services/gpu.cpp @@ -3,8 +3,10 @@ #include "sensorslib.hpp" #include +#include #include #include +#include #include #include #include @@ -243,6 +245,45 @@ void Gpu::detectNameOnce() { {QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript)}); } +void Gpu::readGenericMemory() { + const QStringList paths = QDir(QStringLiteral("/sys/class/drm")) + .entryList( + QStringList() << QStringLiteral("card*"), + QDir::Dirs | QDir::NoDotAndDotDot); + + qreal totalMem = 0.0; + qreal usedMem = 0.0; + for (const QString& card : paths) { + QFile total( + QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_total") + .arg(card)); + if (!total.open(QIODevice::ReadOnly | QIODevice::Text)) { + continue; + } + bool ok = false; + const qreal v = total.readAll().trimmed().toDouble(&ok); + total.close(); + if (ok) { + totalMem += v; + } + + QFile used(QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_used") + .arg(card)); + if (!used.open(QIODevice::ReadOnly | QIODevice::Text)) { + continue; + } + bool ok1 = false; + const qreal v1 = used.readAll().trimmed().toDouble(&ok1); + used.close(); + if (ok1) { + usedMem += v1; + } + } + + setMemoryTotal(totalMem); + setMemoryUsed(usedMem); +} + void Gpu::readGenericUsage() { const QStringList paths = QDir(QStringLiteral("/sys/class/drm")) .entryList( diff --git a/Plugins/ZShell/Services/gpu.hpp b/Plugins/ZShell/Services/gpu.hpp index 4b91122..dbe567c 100644 --- a/Plugins/ZShell/Services/gpu.hpp +++ b/Plugins/ZShell/Services/gpu.hpp @@ -62,6 +62,7 @@ class Gpu : public TickingService { void readGenericUsage(); void startNvidiaUsage(); void readGpuTemperature(); + void readGenericMemory(); void setUserType(Type value); void setAutoType(Type value); From cdf66bff2328731ab8615e3194adfb16c2538f13 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 3 Jul 2026 15:02:00 +0200 Subject: [PATCH 02/11] fix: better width + height calculations for tray menu popouts --- Modules/SysTray/Popouts/TrayMenuPopout.qml | 49 ++++++++-------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/Modules/SysTray/Popouts/TrayMenuPopout.qml b/Modules/SysTray/Popouts/TrayMenuPopout.qml index c212261..87b8884 100644 --- a/Modules/SysTray/Popouts/TrayMenuPopout.qml +++ b/Modules/SysTray/Popouts/TrayMenuPopout.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import Quickshell import Quickshell.Widgets import QtQuick +import QtQuick.Layouts import QtQuick.Controls import QtQuick.Effects import qs.Components @@ -12,14 +13,13 @@ import qs.Config StackView { id: root - property int biggestWidth: 0 readonly property int itemHeight: 30 readonly property int panelRadius: ((itemHeight / 2) + Appearance.padding.small) * Appearance.rounding.scale required property PopoutState popouts property int rootWidth: 0 required property QsMenuHandle trayItem - implicitHeight: currentItem.implicitHeight + implicitHeight: currentItem.isSubMenu ? currentItem.implicitHeight : currentItem.implicitHeight - currentItem.spacing implicitWidth: currentItem.implicitWidth initialItem: SubMenu { @@ -46,7 +46,7 @@ StackView { duration: 0 } } - component SubMenu: Column { + component SubMenu: ColumnLayout { id: menu required property QsMenuHandle handle @@ -54,9 +54,8 @@ StackView { property bool shown opacity: shown ? 1 : 0 - padding: 0 scale: shown ? 1 : 0.8 - spacing: 4 + spacing: Appearance.spacing.extraSmall Behavior on opacity { Anim { @@ -87,22 +86,27 @@ StackView { required property int index required property QsMenuEntry modelData + Layout.fillWidth: true + Layout.leftMargin: modelData.isSeparator ? Appearance.padding.normal : 0 + Layout.rightMargin: modelData.isSeparator ? Appearance.padding.normal : 0 color: modelData.isSeparator ? DynamicColors.palette.m3outlineVariant : "transparent" - implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight - implicitWidth: root.biggestWidth + implicitHeight: modelData.isSeparator ? (visible ? 1 : 0) : childrenLoader.item.implicitHeight + implicitWidth: childrenLoader.item?.implicitWidth ?? 0 radius: Appearance.rounding.full visible: index !== (menuOpener.children.values.length - 1) ? true : (modelData.isSeparator ? false : true) Loader { - id: children + id: childrenLoader active: !item.modelData.isSeparator - anchors.left: parent.left - anchors.right: parent.right + anchors.fill: parent asynchronous: true sourceComponent: Item { + property int iconWidth: icon.active ? icon.width + Appearance.spacing.normal + icon.anchors.rightMargin : 0 + implicitHeight: root.itemHeight + implicitWidth: label.width + label.anchors.leftMargin * 2 + iconWidth StateLayer { enabled: item.modelData.enabled @@ -111,8 +115,6 @@ StackView { onClicked: { const entry = item.modelData; if (entry.hasChildren) { - root.rootWidth = root.biggestWidth; - root.biggestWidth = 0; root.push(subMenuComp.createObject(null, { handle: entry, isSubMenu: true @@ -161,23 +163,7 @@ StackView { anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter color: item.modelData.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3outline - text: labelMetrics.elidedText - } - - TextMetrics { - id: labelMetrics - - font.family: label.font.family - font.pointSize: label.font.pointSize text: item.modelData.text - - Component.onCompleted: { - var biggestWidth = root.biggestWidth; - var currentWidth = labelMetrics.width + (item.modelData.icon ?? "" ? 30 : 0) + (item.modelData.hasChildren ? 30 : 0) + 20; - if (currentWidth > biggestWidth) { - root.biggestWidth = currentWidth; - } - } } Loader { @@ -201,17 +187,17 @@ StackView { Loader { id: loader + Layout.fillWidth: true + Layout.maximumHeight: active ? implicitHeight : 0 active: menu.isSubMenu asynchronous: true sourceComponent: Item { implicitHeight: 30 - implicitWidth: back.implicitWidth Item { - anchors.bottom: parent.bottom + anchors.fill: parent implicitHeight: 30 - implicitWidth: root.biggestWidth CustomRect { anchors.fill: parent @@ -224,7 +210,6 @@ StackView { onClicked: { root.pop(); - root.biggestWidth = root.rootWidth; } } } From 69774e7759ad9d196011072c5a2a8ff4d29b7d1e Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 3 Jul 2026 15:52:27 +0200 Subject: [PATCH 03/11] feat: greeter support hyprland lua --- Greeter/scripts/start-zshell-greeter | 2 +- Greeter/scripts/zshell-hyprland.conf | 12 ------------ Greeter/scripts/zshell-hyprland.lua | 14 ++++++++++++++ scripts/prepare-greeter.sh | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) delete mode 100644 Greeter/scripts/zshell-hyprland.conf create mode 100644 Greeter/scripts/zshell-hyprland.lua diff --git a/Greeter/scripts/start-zshell-greeter b/Greeter/scripts/start-zshell-greeter index 8fa9a59..0dd4f35 100755 --- a/Greeter/scripts/start-zshell-greeter +++ b/Greeter/scripts/start-zshell-greeter @@ -9,5 +9,5 @@ export EGL_PLATFORM=gbm if command -v start-hyprland >/dev/null 2>&1; then exec start-hyprland -- -c /etc/zshell-greeter/zshell-hyprland.conf else - exec Hyprland -c /etc/zshell-greeter/zshell-hyprland.conf + exec Hyprland -c /etc/zshell-greeter/zshell-hyprland.lua fi diff --git a/Greeter/scripts/zshell-hyprland.conf b/Greeter/scripts/zshell-hyprland.conf deleted file mode 100644 index f452209..0000000 --- a/Greeter/scripts/zshell-hyprland.conf +++ /dev/null @@ -1,12 +0,0 @@ -monitor = ,preferred,auto,1 - -env = XDG_SESSION_TYPE,wayland -env = QT_QPA_PLATFORM,wayland -env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1 - -misc { - disable_hyprland_logo = true - disable_splash_rendering = true -} - -exec = sh -lc 'qs -c zshell-greeter; hyprctl dispatch exit' diff --git a/Greeter/scripts/zshell-hyprland.lua b/Greeter/scripts/zshell-hyprland.lua new file mode 100644 index 0000000..ef94468 --- /dev/null +++ b/Greeter/scripts/zshell-hyprland.lua @@ -0,0 +1,14 @@ +hl.env("XDG_SESSION_TYPE", "wayland") +hl.env("QT_QPA_PLATFORM", "wayland") +hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1") + +hl.config({ + misc = { + disable_hyprland_logo = true, + disable_splash_rendering = true, + }, +}) + +hl.on("hyprland.start", function() + hl.exec_cmd("sh -lc 'qs -c zshell-greeter; hyprctl dispatch exit'") +end) diff --git a/scripts/prepare-greeter.sh b/scripts/prepare-greeter.sh index 7b6fc72..ac339be 100755 --- a/scripts/prepare-greeter.sh +++ b/scripts/prepare-greeter.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash BINARY="../Greeter/scripts/start-zshell-greeter" -CONFIG="../Greeter/scripts/zshell-hyprland.conf" +CONFIG="../Greeter/scripts/zshell-hyprland.lua" GREETD_CONFIG="../Greeter/scripts/greeter-config.toml" WALLPAPER="$HOME/.local/state/zshell/lockscreen_bg.png" From d013be0f9a2d2e70e92a13d4bbeee7207b9ef577 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 3 Jul 2026 17:40:45 +0200 Subject: [PATCH 04/11] fix: include spacing for expand icon tray menu --- Modules/SysTray/Popouts/TrayMenuPopout.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/SysTray/Popouts/TrayMenuPopout.qml b/Modules/SysTray/Popouts/TrayMenuPopout.qml index 87b8884..6f640e4 100644 --- a/Modules/SysTray/Popouts/TrayMenuPopout.qml +++ b/Modules/SysTray/Popouts/TrayMenuPopout.qml @@ -106,7 +106,7 @@ StackView { property int iconWidth: icon.active ? icon.width + Appearance.spacing.normal + icon.anchors.rightMargin : 0 implicitHeight: root.itemHeight - implicitWidth: label.width + label.anchors.leftMargin * 2 + iconWidth + implicitWidth: label.width + label.anchors.leftMargin * 2 + iconWidth + (expand.item?.width ?? 0) StateLayer { enabled: item.modelData.enabled From 8c11aead18dbb5d963e0831f148f1b7dc5d97df3 Mon Sep 17 00:00:00 2001 From: zach Date: Sat, 4 Jul 2026 15:54:44 +0200 Subject: [PATCH 05/11] fix: osd stays open when hovered --- Drawers/Interactions.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 3fd7ee2..b254194 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -196,7 +196,7 @@ Item { if (!root.visibilities.bar && Config.bar.autoHide && y < root.bar.implicitHeight) root.bar.isHovered = true; - if (root.panels.sidebar.width === 0) { + if (root.panels.sidebar.offsetScale === 1) { const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y); if (showOsd) { @@ -204,7 +204,7 @@ Item { root.panels.osd.hovered = true; } } else { - const outOfSidebar = x < root.width - root.panels.sidebar.width; + const outOfSidebar = x < root.width - root.panels.sidebar.width * (1 - root.panels.sidebar.offsetScale); const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y); if (!root.osdShortcutActive) { From 6ad9fe5e074e523383cb7b62bab0fa40b19024e3 Mon Sep 17 00:00:00 2001 From: zach Date: Sun, 5 Jul 2026 18:01:52 +0200 Subject: [PATCH 06/11] settings highlight optimized --- Modules/Settings/NavPane/NavLocations.qml | 4 +-- Modules/Settings/SettingsSearcher.qml | 31 ++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/Modules/Settings/NavPane/NavLocations.qml b/Modules/Settings/NavPane/NavLocations.qml index ec0e1fb..f07eb90 100644 --- a/Modules/Settings/NavPane/NavLocations.qml +++ b/Modules/Settings/NavPane/NavLocations.qml @@ -249,7 +249,7 @@ VerticalFadeFlickable { elide: Text.ElideRight font.pointSize: Appearance.font.size.medium text: SettingsSearcher.highlight(result.modelData.title, root.search, DynamicColors.palette.m3primary) - textFormat: Text.StyledText + textFormat: text.includes(" 0 } } diff --git a/Modules/Settings/SettingsSearcher.qml b/Modules/Settings/SettingsSearcher.qml index eb61042..9c41253 100644 --- a/Modules/Settings/SettingsSearcher.qml +++ b/Modules/Settings/SettingsSearcher.qml @@ -10,16 +10,39 @@ Singleton { id: root property var fzfFinder: null + readonly property var highlightCache: ({ + "search": "", + "pattern": null + }) property var inverted: ({}) property var ranking: ({}) function highlight(text: string, search: string, colour: color): string { const escaped = text.replace(/&/g, "&").replace(//g, ">"); - const tokens = tokenize(search); - if (tokens.length === 0) + if (search.length === 0) return escaped; - const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); - const pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi"); + + const cache = root.highlightCache; + if (search !== cache.search) { + const tokens = tokenize(search); + cache.search = search; + if (tokens.length === 0) + cache.pattern = null; + else { + const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + cache.pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi"); + } + } + + const pattern = cache.pattern; + if (!pattern) + return escaped; + + pattern.lastIndex = 0; + if (!pattern.test(escaped)) + return escaped; + + pattern.lastIndex = 0; return escaped.replace(pattern, `$1`); } From 0c5871e1081f3177a3ec00982a58306b139fd7b4 Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 6 Jul 2026 14:12:07 +0200 Subject: [PATCH 07/11] add burn-in prevention for lock screen --- Modules/Lock/LockSurface.qml | 38 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/Modules/Lock/LockSurface.qml b/Modules/Lock/LockSurface.qml index 1ad7885..ad16bc4 100644 --- a/Modules/Lock/LockSurface.qml +++ b/Modules/Lock/LockSurface.qml @@ -157,28 +157,38 @@ WlSessionLockSurface { Item { id: lockContent + property int positionIndex: 0 + readonly property var positions: [Qt.point(0, 0), Qt.point(4, 0), Qt.point(4, 4), Qt.point(0, 4), Qt.point(-4, 4), Qt.point(-4, 0), Qt.point(-4, -4), Qt.point(0, -4), Qt.point(4, -4),] readonly property int radius: size / 4 * Appearance.rounding.scale readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 anchors.centerIn: parent + anchors.horizontalCenterOffset: positions[positionIndex].x + anchors.verticalCenterOffset: positions[positionIndex].y implicitHeight: size implicitWidth: size scale: 0 - // MultiEffect { - // anchors.fill: lockBg - // autoPaddingEnabled: false - // blur: 1 - // blurEnabled: true - // blurMax: 64 - // maskEnabled: true - // maskSource: lockBg - // - // source: ShaderEffectSource { - // sourceItem: background - // sourceRect: Qt.rect(lockBg.x, lockBg.y, lockBg.width, lockBg, height) - // } - // } + Behavior on anchors.horizontalCenterOffset { + Anim { + duration: 5000 + } + } + Behavior on anchors.verticalCenterOffset { + Anim { + duration: 5000 + } + } + + Timer { + interval: 120000 + repeat: true + running: true + + onTriggered: { + lockContent.positionIndex = (lockContent.positionIndex + 1) % lockContent.positions.length; + } + } CustomRect { id: lockBg From 2db6a57c0ba8eaadc9f6ae3203c68d7533f220fc Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 6 Jul 2026 19:40:57 +0200 Subject: [PATCH 08/11] fix: shift background in accordance with content --- Modules/Lock/LockSurface.qml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Modules/Lock/LockSurface.qml b/Modules/Lock/LockSurface.qml index ad16bc4..aecb5be 100644 --- a/Modules/Lock/LockSurface.qml +++ b/Modules/Lock/LockSurface.qml @@ -149,9 +149,34 @@ WlSessionLockSurface { Image { id: background + anchors.bottomMargin: -8 - lockContent.positions[lockContent.positionIndex].y anchors.fill: parent + anchors.leftMargin: -8 + lockContent.positions[lockContent.positionIndex].x + anchors.rightMargin: -8 - lockContent.positions[lockContent.positionIndex].x + anchors.topMargin: -8 + lockContent.positions[lockContent.positionIndex].y fillMode: Image.PreserveAspectCrop source: WallpaperPath.lockscreenBg + + Behavior on anchors.bottomMargin { + Anim { + duration: 5000 + } + } + Behavior on anchors.leftMargin { + Anim { + duration: 5000 + } + } + Behavior on anchors.rightMargin { + Anim { + duration: 5000 + } + } + Behavior on anchors.topMargin { + Anim { + duration: 5000 + } + } } Item { From ffefd3e2027eed3e859d01e3699525b3dbe4dc6d Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 7 Jul 2026 18:47:48 +0200 Subject: [PATCH 09/11] clip settings at bar bounds --- Drawers/Panels.qml | 10 +++------- Drawers/Regions.qml | 2 +- Drawers/Windows.qml | 10 ++-------- Modules/Settings/Common/WallpaperCropper.qml | 2 -- Modules/Settings/Wrapper.qml | 2 -- 5 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 9f2f67a..8a587be 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -184,18 +184,14 @@ Item { Item { id: settingsWrapper + anchors.fill: parent clip: true - implicitHeight: settings.implicitHeight - implicitWidth: settings.implicitWidth - x: (root.width - settings.implicitWidth) / 2 - y: (settings.implicitHeight + (root.height - root.bar.implicitHeight - settings.implicitHeight) / 2) * (1 - settings.offsetScale) - settings.implicitHeight - 5 Settings.Wrapper { id: settings - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - // anchors.centerIn: parent + anchors.centerIn: parent + anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale panels: root screen: root.screen visibilities: root.visibilities diff --git a/Drawers/Regions.qml b/Drawers/Regions.qml index 5d74faf..ff7a655 100644 --- a/Drawers/Regions.qml +++ b/Drawers/Regions.qml @@ -60,7 +60,7 @@ Region { } R { - panel: root.panels.settingsWrapper + panel: root.panels.settings } R { diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index adc63bd..a714e67 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -306,15 +306,9 @@ CustomWindow { PanelBg { id: settingsBg - property real extraHeight: 0 - deformAmount: 0.03 - implicitHeight: panels.settings.height * (1 + extraHeight) - implicitWidth: panels.settings.width - panel: panels.settingsWrapper + panel: panels.settings radius: Appearance.rounding.large + Appearance.padding.normal - x: panels.settingsWrapper.x + panels.settings.x + root.borderThickness - y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight } PanelBg { @@ -422,7 +416,7 @@ CustomWindow { resources.transform: Matrix4x4 { matrix: resourcesBg.deformMatrix } - settingsWrapper.transform: Matrix4x4 { + settings.transform: Matrix4x4 { matrix: settingsBg.deformMatrix } sidebar.transform: Matrix4x4 { diff --git a/Modules/Settings/Common/WallpaperCropper.qml b/Modules/Settings/Common/WallpaperCropper.qml index 21c3b69..fe7f970 100644 --- a/Modules/Settings/Common/WallpaperCropper.qml +++ b/Modules/Settings/Common/WallpaperCropper.qml @@ -267,8 +267,6 @@ Item { function restoreFromData() { let data = Wallpapers.getCrop(wrapper.currentScreen.name); - console.log(data.x, data.y); - if (data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001)) { zoom = data.zoom > 0 ? data.zoom : 1.0; x = imageX + (data.x * scaledImg.paintedWidth); diff --git a/Modules/Settings/Wrapper.qml b/Modules/Settings/Wrapper.qml index 1895ae7..cc5a9fb 100644 --- a/Modules/Settings/Wrapper.qml +++ b/Modules/Settings/Wrapper.qml @@ -39,8 +39,6 @@ Item { sState.animatingContainer: content.opacity < 1 sState.currentPageIdx: ["wallpaper"][0] sState.screen: root.screen - - onClose: console.log("shouldclose") } } } From 6d5609fe5b91afb9e6fcf1bf879314a551e35e43 Mon Sep 17 00:00:00 2001 From: zach Date: Wed, 8 Jul 2026 12:23:45 +0200 Subject: [PATCH 10/11] Add ahead-of-time compilation for cli and optimize thumbnail caching --- CMakeLists.txt | 39 ++++++++++++++++++++++ Plugins/cmake/zshell-cli.cmake | 4 +++ cli/bin/zshell | 5 --- cli/pyproject.toml | 5 +++ cli/src/zshell/__main__.py | 2 +- cli/src/zshell/subcommands/scheme.py | 46 ++++++++++++++++++-------- cli/src/zshell/utils/schemepalettes.py | 27 +++++++++------ 7 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 Plugins/cmake/zshell-cli.cmake delete mode 100755 cli/bin/zshell diff --git a/CMakeLists.txt b/CMakeLists.txt index 1681ef4..6105dd9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,7 +51,9 @@ add_compile_options( -Wunreachable-code ) + if("shell" IN_LIST ENABLE_MODULES) + # Build settings index find_package(Python3 COMPONENTS Interpreter REQUIRED) set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json") execute_process( @@ -64,6 +66,43 @@ if("shell" IN_LIST ENABLE_MODULES) if(NOT SETTINGS_INDEX_RESULT EQUAL 0) message(FATAL_ERROR "Failed to build settings search index") endif() + + # Nuitka compilation + set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli") + set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist") + set(ZSHELL_CLI_SRC "${CMAKE_SOURCE_DIR}/cli/src/zshell") + + find_program(NUITKA_EXECUTABLE nuitka REQUIRED) + + file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS + "${ZSHELL_CLI_SRC}/*.py" + ) + file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS + "${ZSHELL_CLI_SRC}/assets/*" + ) + + add_custom_command( + OUTPUT "${ZSHELL_CLI_DIST}/zshell-cli" + COMMAND ${CMAKE_COMMAND} -E make_directory "${ZSHELL_CLI_BUILD_DIR}" + COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST}" + + COMMAND + ${NUITKA_EXECUTABLE} + --standalone + --include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets + --output-dir=${ZSHELL_CLI_BUILD_DIR} + --output-filename=zshell-cli + ${CMAKE_SOURCE_DIR}/cli/src/zshell/ + + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli + DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS} + ) + + add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_DIST}/zshell-cli") + + install(PROGRAMS "${ZSHELL_CLI_DIST}/zshell-cli" DESTINATION "${INSTALL_LIBDIR}/zshell-cli") + install(DIRECTORY "${ZSHELL_CLI_DIST}/" DESTINATION "${INSTALL_LIBDIR}/zshell-cli" PATTERN "zshell-cli" EXCLUDE) + install(SCRIPT "${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake") endif() if("plugin" IN_LIST ENABLE_MODULES) diff --git a/Plugins/cmake/zshell-cli.cmake b/Plugins/cmake/zshell-cli.cmake new file mode 100644 index 0000000..4e7bef6 --- /dev/null +++ b/Plugins/cmake/zshell-cli.cmake @@ -0,0 +1,4 @@ +file(CREATE_LINK + "../lib/ZShell/zshell-cli/zshell-cli" + "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/usr/bin/zshell-cli" SYMBOLIC +) diff --git a/cli/bin/zshell b/cli/bin/zshell deleted file mode 100755 index cf03348..0000000 --- a/cli/bin/zshell +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname $0)/../src" || exit - -python3 -m zshell "$@" diff --git a/cli/pyproject.toml b/cli/pyproject.toml index ae332cc..990ef54 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -19,6 +19,11 @@ zshell-cli = "zshell:main" [tool.hatch.version] source = "vcs" +[tool.hatch.build] +include = [ + "src/zshell/assets/**", +] + [tool.hatch.build.targets.sdist] only-include = [ "src", diff --git a/cli/src/zshell/__main__.py b/cli/src/zshell/__main__.py index 868d99e..1520f59 100644 --- a/cli/src/zshell/__main__.py +++ b/cli/src/zshell/__main__.py @@ -1,4 +1,4 @@ -from . import main +from zshell import main if __name__ == "__main__": main() diff --git a/cli/src/zshell/subcommands/scheme.py b/cli/src/zshell/subcommands/scheme.py index 70343e5..9792a85 100644 --- a/cli/src/zshell/subcommands/scheme.py +++ b/cli/src/zshell/subcommands/scheme.py @@ -73,7 +73,8 @@ def _complete_accent(ctx, incomplete): @app.command() def list_presets( - json_format: bool = typer.Option(False, "--json", help="Output in JSON format"), + json_format: bool = typer.Option( + False, "--json", help="Output in JSON format"), ): schemes = list_schemes() if json_format: @@ -139,7 +140,7 @@ def generate( HOME = str(os.getenv("HOME")) OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json") SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt") - THUMB_PATH = Path(HOME + "/.cache/zshell/imagecache/thumbnail.jpg") + THUMB_DIR = Path(HOME + "/.cache/zshell/imagecache/thumbnails") WALL_DIR_PATH = Path(HOME + "/.local/state/zshell/wallpaper_path.json") TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates") @@ -147,7 +148,8 @@ def generate( CONFIG = Path(HOME + "/.config/zshell/config.json") if preset is not None and image_path is not None: - raise typer.BadParameter("Use either --image-path or --preset, not both.") + raise typer.BadParameter( + "Use either --image-path or --preset, not both.") def get_scheme_class(scheme_name: str): match scheme_name: @@ -273,7 +275,8 @@ def generate( diff = difference_degrees(from_hct.hue, to_hct.hue) rotation = min(diff * 0.8, 100) output_hue = sanitize_degrees_double( - from_hct.hue + rotation * rotation_direction(from_hct.hue, to_hct.hue) + from_hct.hue + rotation * + rotation_direction(from_hct.hue, to_hct.hue) ) tone = max(0.0, min(100.0, from_hct.tone * (1 + tone_boost))) return Hct.from_hct(output_hue, from_hct.chroma, tone) @@ -307,15 +310,26 @@ def generate( return out - def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)): - thumbnail_file = Path(thumbnail_path) + def thumbnail_cache_path(image_path: Path, thumb_dir: Path) -> Path: + stat = image_path.stat() + key = f"{image_path.stem}_{stat.st_size}_{int(stat.st_mtime)}" + safe_key = re.sub(r"[^A-Za-z0-9._-]", "_", key) + return thumb_dir / f"{safe_key}_thumbnail.jpg" + + def generate_thumbnail(image_path: Path, thumb_dir: Path, size=(128, 128)) -> Path: + thumb_dir.mkdir(parents=True, exist_ok=True) + cache_path = thumbnail_cache_path(image_path, thumb_dir) + + if cache_path.exists(): + return cache_path image = Image.open(image_path) + image.draft("RGB", size) image = image.convert("RGB") image.thumbnail(size, Image.Resampling.NEAREST) + image.save(cache_path, "JPEG") - thumbnail_file.parent.mkdir(parents=True, exist_ok=True) - image.save(thumbnail_path, "JPEG") + return cache_path def apply_terms(sequences: str, sequences_tmux: str, state_path: Path) -> None: state_path.parent.mkdir(parents=True, exist_ok=True) @@ -523,7 +537,8 @@ def generate( template = env.from_string(body) text = template.render(**context) except Exception as e: - raise RuntimeError(f"Template render failed for '{rel}': {e}") from e + raise RuntimeError( + f"Template render failed for '{rel}': {e}") from e out_path.write_text(text, encoding="utf-8") @@ -586,7 +601,8 @@ def generate( (v.accents for v in meta.variants if v.id == p_variant), () ) if accent not in var_accents: - available = ", ".join(var_accents) if var_accents else "none" + available = ", ".join( + var_accents) if var_accents else "none" raise typer.BadParameter( f"Accent '{accent}' not available for '{p_scheme}:{p_variant}'. Available accents: {available}" ) @@ -623,13 +639,13 @@ def generate( seed = hex_to_hct(colors.get("primary", "#000000").lstrip("#")) else: image_path = image_path or Path(WALL_PATH) - generate_thumbnail(image_path, str(THUMB_PATH)) - seed = seed_from_image(THUMB_PATH) + thumb_path = generate_thumbnail(image_path, THUMB_DIR) + seed = seed_from_image(thumb_path) name = "dynamic" flavor = "default" if smart: - effective_mode = smart_mode(THUMB_PATH) + effective_mode = smart_mode(thumb_path) elif mode is not None: effective_mode = mode else: @@ -675,7 +691,9 @@ def generate( print(f"rendered: {p}") OUTPUT.parent.mkdir(parents=True, exist_ok=True) - with open(OUTPUT, "w") as f: + tmp_output = OUTPUT.with_suffix(".json.tmp") + with open(tmp_output, "w") as f: json.dump(output_dict, f, indent=4) + os.replace(tmp_output, OUTPUT) except Exception as e: print(f"Error: {e}") diff --git a/cli/src/zshell/utils/schemepalettes.py b/cli/src/zshell/utils/schemepalettes.py index 0fc136c..09d5f28 100644 --- a/cli/src/zshell/utils/schemepalettes.py +++ b/cli/src/zshell/utils/schemepalettes.py @@ -1,9 +1,11 @@ from __future__ import annotations from dataclasses import dataclass -from pathlib import Path +from importlib.resources import files +from importlib.resources.abc import Traversable +from pathlib import PurePosixPath -ASSETS = Path(__file__).resolve().parent.parent / "assets" / "schemes" +ASSETS: Traversable = files("zshell") / "assets" / "schemes" @dataclass(frozen=True) @@ -30,7 +32,7 @@ class Palette: accent: str | None = None -def _parse_txt(path: Path) -> dict[str, str]: +def _parse_txt(path: Traversable) -> dict[str, str]: colors: dict[str, str] = {} for line in path.read_text().splitlines(): line = line.strip() @@ -46,7 +48,7 @@ def _parse_txt(path: Path) -> dict[str, str]: def _discover_schemes() -> dict[str, SchemeMeta]: schemes: dict[str, SchemeMeta] = {} - for scheme_dir in sorted(ASSETS.iterdir()): + for scheme_dir in sorted(ASSETS.iterdir(), key=lambda p: p.name): if not scheme_dir.is_dir() or scheme_dir.name.startswith("."): continue @@ -54,7 +56,7 @@ def _discover_schemes() -> dict[str, SchemeMeta]: display_name = sid.capitalize() variants: list[SchemeVariant] = [] - for var_dir in sorted(scheme_dir.iterdir()): + for var_dir in sorted(scheme_dir.iterdir(), key=lambda p: p.name): if not var_dir.is_dir() or var_dir.name.startswith("."): continue @@ -62,9 +64,10 @@ def _discover_schemes() -> dict[str, SchemeMeta]: accents: set[str] = set() for f in var_dir.iterdir(): - if f.suffix != ".txt": + name = PurePosixPath(f.name) + if name.suffix != ".txt": continue - stem = f.stem + stem = name.stem if "-" in stem: maybe_accent, maybe_mode = stem.rsplit("-", 1) if maybe_mode in ("dark", "light"): @@ -101,12 +104,14 @@ SCHEMES: dict[str, SchemeMeta] = _discover_schemes() def get_palette(scheme: str, variant: str, mode: str, accent: str | None = None) -> Palette: if scheme not in SCHEMES: - raise KeyError(f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}") + raise KeyError( + f"Unknown scheme '{scheme}'. Available: {', '.join(SCHEMES)}") meta = SCHEMES[scheme] var_ids = {v.id for v in meta.variants} if variant not in var_ids: - raise KeyError(f"Unknown variant '{variant}' for scheme '{scheme}'. Available: {', '.join(sorted(var_ids))}") + raise KeyError( + f"Unknown variant '{variant}' for scheme '{scheme}'. Available: {', '.join(sorted(var_ids))}") if accent: filename = f"{accent}-{mode}.txt" @@ -114,10 +119,10 @@ def get_palette(scheme: str, variant: str, mode: str, accent: str | None = None) filename = f"{mode}.txt" txt_path = ASSETS / scheme / variant / filename - if not txt_path.exists(): + if not txt_path.is_file(): txt_path = ASSETS / scheme / variant / f"{mode}.txt" - if not txt_path.exists(): + if not txt_path.is_file(): var_info = next(v for v in meta.variants if v.id == variant) raise FileNotFoundError( f"No {mode} palette for '{scheme}:{variant}'. Available modes: {sorted(var_info.modes)}" From 87433334c71643aa3a3d9fcda2190ea1ae534eb8 Mon Sep 17 00:00:00 2001 From: zach Date: Wed, 8 Jul 2026 13:01:53 +0200 Subject: [PATCH 11/11] fix cmake compilation of python code --- CMakeLists.txt | 8 +++++++- Plugins/cmake/zshell-cli.cmake | 4 ---- Plugins/cmake/zshell-cli.cmake.in | 12 ++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) delete mode 100644 Plugins/cmake/zshell-cli.cmake create mode 100644 Plugins/cmake/zshell-cli.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 6105dd9..1d3932b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,7 +102,13 @@ if("shell" IN_LIST ENABLE_MODULES) install(PROGRAMS "${ZSHELL_CLI_DIST}/zshell-cli" DESTINATION "${INSTALL_LIBDIR}/zshell-cli") install(DIRECTORY "${ZSHELL_CLI_DIST}/" DESTINATION "${INSTALL_LIBDIR}/zshell-cli" PATTERN "zshell-cli" EXCLUDE) - install(SCRIPT "${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake") + + configure_file( + "${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake.in" + "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake" + @ONLY + ) + install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake") endif() if("plugin" IN_LIST ENABLE_MODULES) diff --git a/Plugins/cmake/zshell-cli.cmake b/Plugins/cmake/zshell-cli.cmake deleted file mode 100644 index 4e7bef6..0000000 --- a/Plugins/cmake/zshell-cli.cmake +++ /dev/null @@ -1,4 +0,0 @@ -file(CREATE_LINK - "../lib/ZShell/zshell-cli/zshell-cli" - "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/usr/bin/zshell-cli" SYMBOLIC -) diff --git a/Plugins/cmake/zshell-cli.cmake.in b/Plugins/cmake/zshell-cli.cmake.in new file mode 100644 index 0000000..3704716 --- /dev/null +++ b/Plugins/cmake/zshell-cli.cmake.in @@ -0,0 +1,12 @@ +set(ZSHELL_CLI_BIN_DIR "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/usr/bin") +file(MAKE_DIRECTORY "${ZSHELL_CLI_BIN_DIR}") + +file(RELATIVE_PATH ZSHELL_CLI_TARGET + "${ZSHELL_CLI_BIN_DIR}" + "${CMAKE_INSTALL_PREFIX}/@INSTALL_LIBDIR@/zshell-cli/zshell-cli" +) + +file(CREATE_LINK + "${ZSHELL_CLI_TARGET}" + "${ZSHELL_CLI_BIN_DIR}/zshell-cli" SYMBOLIC +)