From cc9aeca98be3249b16736a67852074657c3a92d8 Mon Sep 17 00:00:00 2001 From: zach Date: Sun, 27 Sep 2026 22:47:31 +0200 Subject: [PATCH] fix: column index desync after reorder --- Components/StateLayer.qml | 1 - Modules/Dashboard/Content.qml | 53 +-- Modules/Dashboard/Dash.qml | 2 +- Modules/Dashboard/Dash/User.qml | 8 +- .../Dashboard/Processes/ProcessesColumns.qml | 15 +- .../Dashboard/Processes/ProcessesHeader.qml | 153 +++------ .../Dashboard/Processes}/ResourceUsage.qml | 0 Modules/Dashboard/Processes/SystemInfo.qml | 1 - Modules/Dashboard/ProcessesTab.qml | 13 - Modules/Resources/Cards/NetworkCard.qml | 16 +- .../Settings/Pages/Panels/DashboardPanel.qml | 40 +++ Plugins/ZShell/CMakeLists.txt | 1 + Plugins/ZShell/Config/anim.cpp | 4 - Plugins/ZShell/Config/anim.hpp | 2 +- Plugins/ZShell/Config/appearance.cpp | 10 - Plugins/ZShell/Config/appearance.hpp | 1 - Plugins/ZShell/Config/dashboard.hpp | 23 +- Plugins/ZShell/Config/tokens.hpp | 13 + Plugins/ZShell/Services/CMakeLists.txt | 6 + Plugins/ZShell/Services/networkusage.cpp | 153 +++++++++ Plugins/ZShell/Services/networkusage.hpp | 66 ++++ Plugins/ZShell/Services/processes.cpp | 318 ++++++++++++++---- Plugins/ZShell/Services/processes.hpp | 29 ++ Plugins/ZShell/Services/tickingservice.cpp | 64 +--- Plugins/ZShell/Services/tickingservice.hpp | 3 +- Plugins/ZShell/Services/usagefmt.cpp | 16 + Plugins/ZShell/Services/usagefmt.hpp | 1 + Plugins/ZShell/Settings/macros.hpp | 311 +++++++++-------- 28 files changed, 859 insertions(+), 464 deletions(-) rename {Helpers => Modules/Dashboard/Processes}/ResourceUsage.qml (100%) create mode 100644 Plugins/ZShell/Services/networkusage.cpp create mode 100644 Plugins/ZShell/Services/networkusage.hpp diff --git a/Components/StateLayer.qml b/Components/StateLayer.qml index 8857eb0..aeda8ed 100644 --- a/Components/StateLayer.qml +++ b/Components/StateLayer.qml @@ -3,7 +3,6 @@ import QtQuick.Shapes import ZShell import ZShell.Config import qs.Services -import qs.Services MouseArea { id: root diff --git a/Modules/Dashboard/Content.qml b/Modules/Dashboard/Content.qml index d34b536..b4173e7 100644 --- a/Modules/Dashboard/Content.qml +++ b/Modules/Dashboard/Content.qml @@ -12,7 +12,7 @@ Item { id: root readonly property real nonAnimWidth: view.implicitWidth + clipWrapper.anchors.margins * 2 - readonly property real nonAnimHeight: tabs.implicitHeight + clipWrapper.anchors.topMargin + view.implicitHeight + clipWrapper.anchors.margins * 2 + readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + clipWrapper.anchors.margins + clipWrapper.anchors.topMargin required property ScreenState screenState @@ -22,25 +22,25 @@ Item { component: dashComponent, iconName: "dashboard", text: qsTr("Dashboard"), - enabled: true + enabled: Config.dashboard.showDashboard }, { component: weatherComponent, iconName: "cloud", text: qsTr("Weather"), - enabled: true + enabled: Config.dashboard.showWeather }, { component: mediaComponent, iconName: "queue_music", text: qsTr("Media"), - enabled: true + enabled: Config.dashboard.showMedia }, { component: procsComponent, iconName: "speed", text: qsTr("Processes"), - enabled: true + enabled: Config.dashboard.showProcesses }, ]; return allTabs.filter(tab => tab.enabled); @@ -60,33 +60,34 @@ Item { } } - Item { - id: clipWrapper - anchors.fill: parent - anchors.margins: Tokens.padding.small - // radius: Tokens.rounding.extraLarge - anchors.margins + Tabs { + id: tabs + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + nonAnimWidth: view.implicitWidth + screenState: root.screenState + tabs: root.dashboardTabs anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins) + anchors.margins: Tokens.padding.small + } - Tabs { - id: tabs - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - nonAnimWidth: view.implicitWidth - screenState: root.screenState - tabs: root.dashboardTabs - } + CustomClippingRect { + id: clipWrapper + anchors.top: tabs.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: Tokens.spacing.small + anchors.margins: Tokens.padding.small + radius: Tokens.rounding.extraLarge - anchors.margins Item { id: viewWrapper - anchors.top: tabs.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.topMargin: Tokens.spacing.small + anchors.fill: parent Flickable { id: view diff --git a/Modules/Dashboard/Dash.qml b/Modules/Dashboard/Dash.qml index 54d6e51..31a78b6 100644 --- a/Modules/Dashboard/Dash.qml +++ b/Modules/Dashboard/Dash.qml @@ -67,7 +67,7 @@ GridLayout { Rect { Layout.columnSpan: 2 Layout.fillHeight: true - Layout.preferredWidth: Config.dashboard.sizes.weatherWidth + Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth Layout.row: 0 radius: root.radius diff --git a/Modules/Dashboard/Dash/User.qml b/Modules/Dashboard/Dash/User.qml index 264ae5f..34da84c 100644 --- a/Modules/Dashboard/Dash/User.qml +++ b/Modules/Dashboard/Dash/User.qml @@ -51,7 +51,7 @@ Row { id: icon anchors.left: parent.left - anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 color: Colors.palette.m3primary implicitSize: Math.floor(13 * 1.34) source: SystemInfo.osLogo @@ -65,7 +65,7 @@ Row { anchors.verticalCenter: icon.verticalCenter elide: Text.ElideRight text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}` - width: Config.dashboard.sizes.infoWidth + width: Tokens.sizes.dashboard.infoWidth } } @@ -98,7 +98,7 @@ Row { id: icon anchors.left: parent.left - anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 color: line.colour fill: 1 fontStyle: Tokens.font.icon.small @@ -113,7 +113,7 @@ Row { anchors.verticalCenter: icon.verticalCenter elide: Text.ElideNone text: `: ${line.text}` - width: Config.dashboard.sizes.infoWidth + width: Tokens.sizes.dashboard.infoWidth } } } diff --git a/Modules/Dashboard/Processes/ProcessesColumns.qml b/Modules/Dashboard/Processes/ProcessesColumns.qml index 40b9694..0dec3d4 100644 --- a/Modules/Dashboard/Processes/ProcessesColumns.qml +++ b/Modules/Dashboard/Processes/ProcessesColumns.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import Quickshell import ZShell.Config +import ZShell.Services Singleton { id: root @@ -88,6 +89,18 @@ Singleton { } ] + readonly property var desktopEntries: DesktopEntries.applications.values.map(e => ({ + icon: e.icon, + command: e.command + })) + + Component.onCompleted: { + Processes.setDesktopEntries(root.desktopEntries); + Qt.callLater(ensurePinnedFirst); + } + + onDesktopEntriesChanged: Processes.setDesktopEntries(root.desktopEntries) + readonly property string pinnedColumnId: "command" readonly property var hiddenColumns: { @@ -113,8 +126,6 @@ Singleton { }); } - Component.onCompleted: Qt.callLater(ensurePinnedFirst) - function defFor(id: string): var { for (const def of root.columnDefs) if (def.id === id) diff --git a/Modules/Dashboard/Processes/ProcessesHeader.qml b/Modules/Dashboard/Processes/ProcessesHeader.qml index 83225dd..307d4da 100644 --- a/Modules/Dashboard/Processes/ProcessesHeader.qml +++ b/Modules/Dashboard/Processes/ProcessesHeader.qml @@ -64,109 +64,83 @@ Item { function onWidthChanged() { root.fillMissingWidths(); } + + function onCountChanged() { + resizeOverlay.geometryTick++; + root.fillMissingWidths(); + } + + function onContentWidthChanged() { + resizeOverlay.geometryTick++; + } } Connections { target: columnsData function onValuesChanged() { root.fillMissingWidths(); + resizeOverlay.geometryTick++; } } Component.onCompleted: Qt.callLater(fillMissingWidths) Component.onDestruction: Cursor.clearOverride() - RowLayout { - id: rowLayout + CustomListView { + id: columnList anchors.fill: parent - + orientation: ListView.Horizontal + interactive: false spacing: Tokens.spacing.extraSmall / 2 - CustomListView { - id: columnList - - Layout.fillWidth: true - orientation: ListView.Horizontal - interactive: false - spacing: parent.spacing - implicitHeight: root.headerHeight - - add: Transition { - Anim { - from: 0 - properties: "opacity" - to: 1 - type: Anim.DefaultEffects - } + add: Transition { + Anim { + from: 0 + properties: "opacity" + to: 1 + type: Anim.DefaultEffects } - displaced: Transition { - Anim { - properties: "opacity" - to: 1 - type: Anim.DefaultEffects - } - Anim { - properties: "x" - } + } + displaced: Transition { + Anim { + properties: "opacity" + to: 1 + type: Anim.DefaultEffects } - move: Transition { - Anim { - properties: "opacity" - to: 1 - type: Anim.DefaultEffects - } - Anim { - properties: "x" - } + Anim { + properties: "x" } - remove: Transition { - Anim { - to: 0 - properties: "opacity" - type: Anim.DefaultEffects - } + } + move: Transition { + Anim { + properties: "opacity" + to: 1 + type: Anim.DefaultEffects } - - model: DelegateModel { - id: columnsVisual - - delegate: ColumnHeaderCell { - parentModel: columnsVisual - } - - model: ScriptModel { - id: columnsData - values: Config.dashboard.processColumns.values - } + Anim { + properties: "x" + } + } + remove: Transition { + Anim { + to: 0 + properties: "opacity" + type: Anim.DefaultEffects } } - IconButton { - id: addColumnBtn + model: DelegateModel { + id: columnsVisual - type: IconButton.Text - icon: "add" - font: Tokens.font.icon.medium - Layout.alignment: Qt.AlignVCenter - visible: ProcessesColumns.hiddenColumns.length > 0 - onClicked: addMenu.expanded = true - } + delegate: ColumnHeaderCell { + parentModel: columnsVisual + } - Variants { - id: hiddenColumnList - - model: ProcessesColumns.hiddenColumns - - HiddenColumnItem {} - } - - Menu { - id: addMenu - - attachTo: addColumnBtn - items: hiddenColumnList.instances - onItemSelected: item => ProcessesColumns.addColumn(item.modelData.id) + model: ScriptModel { + id: columnsData + values: Config.dashboard.processColumns.values + } } } @@ -178,26 +152,9 @@ Item { property int geometryTick: 0 property int activeGapIndex: -1 - anchors.fill: rowLayout + anchors.fill: columnList z: 10 - Connections { - target: columnsData - function onValuesChanged() { - resizeOverlay.geometryTick++; - } - } - - Connections { - target: columnList - function onCountChanged() { - resizeOverlay.geometryTick++; - } - function onContentWidthChanged() { - resizeOverlay.geometryTick++; - } - } - Component.onCompleted: Qt.callLater(() => geometryTick++) Repeater { @@ -571,8 +528,6 @@ Item { const rightMid = rightItem.x + rightItem.width / 2; if (refX > rightMid) { columnsVisual.items.move(myIndex, myIndex + 1); - resizeOverlay.geometryTick++; - Qt.callLater(() => resizeOverlay.geometryTick++); return; } } @@ -583,8 +538,6 @@ Item { const pinnedAtZero = columnList.itemAtIndex(0)?.modelData?.id === ProcessesColumns.pinnedColumnId; if (refX < leftMid && !(myIndex === 1 && pinnedAtZero)) { columnsVisual.items.move(myIndex, myIndex - 1); - resizeOverlay.geometryTick++; - Qt.callLater(() => resizeOverlay.geometryTick++); } } } diff --git a/Helpers/ResourceUsage.qml b/Modules/Dashboard/Processes/ResourceUsage.qml similarity index 100% rename from Helpers/ResourceUsage.qml rename to Modules/Dashboard/Processes/ResourceUsage.qml diff --git a/Modules/Dashboard/Processes/SystemInfo.qml b/Modules/Dashboard/Processes/SystemInfo.qml index 3346b56..086e90b 100644 --- a/Modules/Dashboard/Processes/SystemInfo.qml +++ b/Modules/Dashboard/Processes/SystemInfo.qml @@ -7,7 +7,6 @@ import ZShell.Components import ZShell.Services import qs.Components import qs.Services -import qs.Helpers Item { id: root diff --git a/Modules/Dashboard/ProcessesTab.qml b/Modules/Dashboard/ProcessesTab.qml index 76bc140..35687fc 100644 --- a/Modules/Dashboard/ProcessesTab.qml +++ b/Modules/Dashboard/ProcessesTab.qml @@ -9,11 +9,6 @@ import qs.Services Item { id: root - readonly property var desktopEntries: DesktopEntries.applications.values.map(e => ({ - icon: e.icon, - command: e.command - })) - implicitWidth: Tokens.sizes.dashboard.processesWidth implicitHeight: Tokens.sizes.dashboard.processesHeight @@ -21,14 +16,6 @@ Item { service: Processes } - Connections { - target: DesktopEntries.applications - function onValuesChanged(): void { - Processes.setDesktopEntries(root.desktopEntries); - } - } - Component.onCompleted: Processes.setDesktopEntries(root.desktopEntries) - ProcessesHeader { id: header diff --git a/Modules/Resources/Cards/NetworkCard.qml b/Modules/Resources/Cards/NetworkCard.qml index 2184b53..445ccf6 100644 --- a/Modules/Resources/Cards/NetworkCard.qml +++ b/Modules/Resources/Cards/NetworkCard.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts import ZShell.Components +import ZShell.Services import ZShell.Config -import qs.Helpers import qs.Components import qs.Services @@ -14,7 +14,7 @@ CustomRect { implicitWidth: 300 radius: Tokens.rounding.largeIncreased - Ref { + ServiceRef { service: NetworkUsage } @@ -129,8 +129,8 @@ CustomRect { CustomText { color: Colors.palette.m3tertiary text: { - const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); - return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + const fmt = UsageFmt.formatBytes(NetworkUsage.downloadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}/s` : "0.0 B/s"; } } } @@ -157,8 +157,8 @@ CustomRect { CustomText { color: Colors.palette.m3secondary text: { - const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); - return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + const fmt = UsageFmt.formatBytes(NetworkUsage.uploadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}/s` : "0.0 B/s"; } } } @@ -185,8 +185,8 @@ CustomRect { CustomText { color: Colors.palette.m3onSurfaceVariant text: { - const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); - const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); + const down = UsageFmt.formatBytes(NetworkUsage.downloadTotal ?? 0); + const up = UsageFmt.formatBytes(NetworkUsage.uploadTotal ?? 0); return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; } } diff --git a/Modules/Settings/Pages/Panels/DashboardPanel.qml b/Modules/Settings/Pages/Panels/DashboardPanel.qml index af8beb8..12d7d3e 100644 --- a/Modules/Settings/Pages/Panels/DashboardPanel.qml +++ b/Modules/Settings/Pages/Panels/DashboardPanel.qml @@ -34,6 +34,45 @@ PageBase { onToggled: Config.dashboard.enabled = checked } + // Tabs + SectionHeader { + text: qsTr("Tabs") + } + + ToggleRow { + first: true + checked: Config.dashboard.showDashboard + settingAnchor: "dashboard-show-dashboard" + text: qsTr("Dashboard") + + onToggled: Config.dashboard.showDashboard = checked + } + + ToggleRow { + checked: Config.dashboard.showWeather + settingAnchor: "dashboard-show-weather" + text: qsTr("Weather") + + onToggled: Config.dashboard.showWeather = checked + } + + ToggleRow { + checked: Config.dashboard.showMedia + settingAnchor: "dashboard-show-media" + text: qsTr("Media") + + onToggled: Config.dashboard.showMedia = checked + } + + ToggleRow { + checked: Config.dashboard.showProcesses + settingAnchor: "dashboard-show-processes" + text: qsTr("Processes") + last: true + + onToggled: Config.dashboard.showProcesses = checked + } + // Behavior SectionHeader { text: qsTr("Behavior") @@ -58,6 +97,7 @@ PageBase { text: qsTr("Drag threshold") to: 100 value: Config.dashboard.dragThreshold + last: true onMoved: v => Config.dashboard.dragThreshold = v } diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index 8cecf26..7d1bbaf 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -1,5 +1,6 @@ qml_module(ZShell URI ZShell + INCLUDE_PREFIX core SOURCES writefile.hpp writefile.cpp appdb.hpp appdb.cpp diff --git a/Plugins/ZShell/Config/anim.cpp b/Plugins/ZShell/Config/anim.cpp index f5b97ff..0f89423 100644 --- a/Plugins/ZShell/Config/anim.cpp +++ b/Plugins/ZShell/Config/anim.cpp @@ -64,9 +64,6 @@ AnimDurations* AnimTokens::durations() const { QEasingCurve AnimTokens::buildCurve(const QList& points) { QEasingCurve curve(QEasingCurve::BezierSpline); - // Points come in pairs of (x, y) forming cubic bezier segments. - // Each segment needs 3 control points: c1, c2, endPoint. - // So 6 values per segment: c1x, c1y, c2x, c2y, endX, endY. for (int i = 0; i + 5 < points.size(); i += 6) { const QPointF c1(points[i], points[i + 1]); const QPointF c2(points[i + 2], points[i + 3]); @@ -101,7 +98,6 @@ void AnimTokens::rebuildCurves() { void AnimTokens::bindCurves(AnimCurves* curves) { m_curves = curves; - // Rebuild when any curve control points change connect( curves, &AnimCurves::optionChanged, this, &AnimTokens::rebuildCurves); diff --git a/Plugins/ZShell/Config/anim.hpp b/Plugins/ZShell/Config/anim.hpp index 2a76c9d..8ff9694 100644 --- a/Plugins/ZShell/Config/anim.hpp +++ b/Plugins/ZShell/Config/anim.hpp @@ -11,7 +11,7 @@ class AnimDurations; class AnimTokens : public QObject { Q_OBJECT - Q_MOC_INCLUDE("tokens.hpp") // AnimCurves + Q_MOC_INCLUDE("tokens.hpp") Q_MOC_INCLUDE("appearance.hpp") QML_ANONYMOUS diff --git a/Plugins/ZShell/Config/appearance.cpp b/Plugins/ZShell/Config/appearance.cpp index 1d125ec..3ba878d 100644 --- a/Plugins/ZShell/Config/appearance.cpp +++ b/Plugins/ZShell/Config/appearance.cpp @@ -6,8 +6,6 @@ namespace ZShell::config { namespace { -// Helper: connect all changed signals from a token object to a single valuesChanged signal, -// plus connect the local scaleChanged signal. template void connectTokenSignals(Source* source, Target* target) { const auto* meta = source->metaObject(); @@ -30,8 +28,6 @@ void connectTokenSignals(Source* source, Target* target) { } // namespace -// AppearanceRounding - void AppearanceRounding::bindTokens(RoundingTokens* tokens) { m_tokens = tokens; connectTokenSignals(tokens, this); @@ -77,8 +73,6 @@ int AppearanceRounding::full() const { return m_tokens ? m_tokens->full() : 0; } -// AppearanceSpacing - void AppearanceSpacing::bindTokens(SpacingTokens* tokens) { m_tokens = tokens; connectTokenSignals(tokens, this); @@ -120,8 +114,6 @@ int AppearanceSpacing::extraExtraLarge() const { : 0; } -// AppearancePadding - void AppearancePadding::bindTokens(PaddingTokens* tokens) { m_tokens = tokens; connectTokenSignals(tokens, this); @@ -167,8 +159,6 @@ int AppearancePadding::extraExtraLarge() const { : 0; } -// AnimDurations - void AnimDurations::bindTokens(AnimDurationTokens* tokens) { m_tokens = tokens; connectTokenSignals(tokens, this); diff --git a/Plugins/ZShell/Config/appearance.hpp b/Plugins/ZShell/Config/appearance.hpp index 4166253..b774969 100644 --- a/Plugins/ZShell/Config/appearance.hpp +++ b/Plugins/ZShell/Config/appearance.hpp @@ -11,7 +11,6 @@ namespace ZShell::config { using Qt::StringLiterals::operator""_s; -// Forward declare token types from tokens.hpp class RoundingTokens; class SpacingTokens; class PaddingTokens; diff --git a/Plugins/ZShell/Config/dashboard.hpp b/Plugins/ZShell/Config/dashboard.hpp index 3160a48..e9fd3d0 100644 --- a/Plugins/ZShell/Config/dashboard.hpp +++ b/Plugins/ZShell/Config/dashboard.hpp @@ -17,24 +17,6 @@ class Performance : public settings::ObjectNode { CONFIG_PROPERTY(bool, showVram, true) }; -class DashboardSizes : public settings::ObjectNode { - CONFIG_NODE(DashboardSizes, settings::ObjectNode) - - CONFIG_PROPERTY(int, dateTimeWidth, 110) - CONFIG_PROPERTY(int, infoIconSize, 25) - CONFIG_PROPERTY(int, infoWidth, 200) - CONFIG_PROPERTY(int, mediaCoverArtSize, 150) - CONFIG_PROPERTY(int, mediaProgressSweep, 180) - CONFIG_PROPERTY(int, mediaProgressThickness, 8) - CONFIG_PROPERTY(int, mediaVisualizerSize, 200) - CONFIG_PROPERTY(int, mediaWidth, 200) - CONFIG_PROPERTY(int, resourceProgessThickness, 10) - CONFIG_PROPERTY(int, resourceSize, 200) - CONFIG_PROPERTY(int, tabIndicatorHeight, 3) - CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) - CONFIG_PROPERTY(int, weatherWidth, 250) -}; - class ProcessColumnEntry : public settings::ObjectNode { CONFIG_NODE(ProcessColumnEntry, settings::ObjectNode) @@ -51,9 +33,12 @@ class Dashboard : public settings::ObjectNode { CONFIG_PROPERTY(bool, showOnHover, true) CONFIG_PROPERTY(int, dragThreshold, 50) CONFIG_PROPERTY(int, mediaUpdateInterval, 2000) + CONFIG_PROPERTY(bool, showWeather, true) + CONFIG_PROPERTY(bool, showDashboard, true) + CONFIG_PROPERTY(bool, showMedia, true) + CONFIG_PROPERTY(bool, showProcesses, false) CONFIG_SUBOBJECT(Performance, performance) CONFIG_PROPERTY(int, resourceUpdateInterval, 1000) - CONFIG_SUBOBJECT(DashboardSizes, sizes) CONFIG_LIST( ProcessColumnList, processColumns, diff --git a/Plugins/ZShell/Config/tokens.hpp b/Plugins/ZShell/Config/tokens.hpp index 25c8388..c19848f 100644 --- a/Plugins/ZShell/Config/tokens.hpp +++ b/Plugins/ZShell/Config/tokens.hpp @@ -149,6 +149,19 @@ class DashboardTokens : public settings::ObjectNode { CONFIG_PROPERTY(int, processesHeight, 800) CONFIG_PROPERTY(int, processesHeaderHeight, 40) CONFIG_PROPERTY(int, processesRowHeight, 40) + CONFIG_PROPERTY(int, dateTimeWidth, 110) + CONFIG_PROPERTY(int, infoIconSize, 25) + CONFIG_PROPERTY(int, infoWidth, 200) + CONFIG_PROPERTY(int, mediaCoverArtSize, 150) + CONFIG_PROPERTY(int, mediaProgressSweep, 180) + CONFIG_PROPERTY(int, mediaProgressThickness, 8) + CONFIG_PROPERTY(int, mediaVisualizerSize, 200) + CONFIG_PROPERTY(int, mediaWidth, 200) + CONFIG_PROPERTY(int, resourceProgessThickness, 10) + CONFIG_PROPERTY(int, resourceSize, 200) + CONFIG_PROPERTY(int, tabIndicatorHeight, 3) + CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) + CONFIG_PROPERTY(int, weatherWidth, 250) }; class SizeTokens : public settings::ObjectNode { diff --git a/Plugins/ZShell/Services/CMakeLists.txt b/Plugins/ZShell/Services/CMakeLists.txt index e6c413c..2e99e62 100644 --- a/Plugins/ZShell/Services/CMakeLists.txt +++ b/Plugins/ZShell/Services/CMakeLists.txt @@ -19,6 +19,7 @@ qml_module(ZShell-services diskinfo.hpp diskinfo.cpp storage.hpp storage.cpp processes.hpp processes.cpp + networkusage.hpp networkusage.cpp lyriccandidate.hpp lyriccandidate.cpp lyrics.hpp lyrics.cpp LIBRARIES @@ -30,6 +31,11 @@ qml_module(ZShell-services Sensors::Sensors ZShell-config ZShell-settings + ZShell-internal + ZShell + zshell-util DEPENDENCIES + ZShell ZShell.Config + ZShell.Internal ) diff --git a/Plugins/ZShell/Services/networkusage.cpp b/Plugins/ZShell/Services/networkusage.cpp new file mode 100644 index 0000000..493b03f --- /dev/null +++ b/Plugins/ZShell/Services/networkusage.cpp @@ -0,0 +1,153 @@ +#include "networkusage.hpp" + +#include +#include +#include + +#include +#include +#include + +#include "config/rootnodes.hpp" +#include "config/dashboard.hpp" + +namespace ZShell::services { + +using Qt::StringLiterals::operator""_s; + +NetworkUsage::NetworkUsage(QObject* parent) + : TickingService(parent) + , m_downloadBuffer(new internal::CircularBuffer(this)) + , m_uploadBuffer(new internal::CircularBuffer(this)) { + m_downloadBuffer->setSmoothingRadius(2); + m_uploadBuffer->setSmoothingRadius(2); + + auto* resources = + config::ConfigSingleton::instance()->dashboard()->performance(); + setHistoryLength(resources->showVram() ? 70 : 30); + QObject::connect( + resources, + &ZShell::config::Performance::showVramChanged, + this, + [this, resources] { + setHistoryLength(resources->showVram() ? 70 : 30); + }); +} + +qreal NetworkUsage::downloadSpeed() const { + return m_downloadSpeed; +} + +qreal NetworkUsage::uploadSpeed() const { + return m_uploadSpeed; +} + +qreal NetworkUsage::downloadTotal() const { + return m_downloadTotal; +} + +qreal NetworkUsage::uploadTotal() const { + return m_uploadTotal; +} + +int NetworkUsage::historyLength() const { + return m_historyLength; +} + +void NetworkUsage::setHistoryLength(int length) { + if (length <= 0 || length == m_historyLength) { + return; + } + m_historyLength = length; + m_downloadBuffer->setCapacity(m_historyLength + 1); + m_uploadBuffer->setCapacity(m_historyLength + 1); + Q_EMIT historyLengthChanged(); +} + +internal::CircularBuffer* NetworkUsage::downloadBuffer() const { + return m_downloadBuffer; +} + +internal::CircularBuffer* NetworkUsage::uploadBuffer() const { + return m_uploadBuffer; +} + +void NetworkUsage::tick() { + QFile f(u"/proc/net/dev"_s); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + return; + } + // Skip headers + f.readLine(); + f.readLine(); + + quint64 totalRx = 0; + quint64 totalTx = 0; + + while (!f.atEnd()) { + const QByteArray line = f.readLine(); + const qsizetype splitIdx = line.indexOf(':'); + if (splitIdx == -1) { + continue; + } + const QByteArray iface = line.left(splitIdx).trimmed(); + if (iface == QByteArrayView("lo")) { + continue; // Skip loopback interface + } + + // Parse every counter through tx bytes to validate the row + const char* pos = line.constData() + splitIdx + 1; + const char* const end = line.constData() + line.size(); + + std::array fields{}; + bool valid = true; + for (unsigned long long& field : fields) { + while (pos < end && (*pos == ' ' || *pos == '\t')) + ++pos; + + const auto [next, ec] = std::from_chars(pos, end, field); + if (ec != std::errc{}) { + valid = false; + break; + } + pos = next; + } + + if (!valid) continue; + + totalRx += static_cast(fields[0]); + totalTx += static_cast(fields[8]); + } + f.close(); + + if (!m_initialized) { + m_prevRx = totalRx; + m_prevTx = totalTx; + m_timer.start(); + m_initialized = true; + return; + } + + const qreal elapsed = static_cast(m_timer.restart()) / 1000.0; + const quint64 rxDelta = totalRx >= m_prevRx ? totalRx - m_prevRx : 0; + const quint64 txDelta = totalTx >= m_prevTx ? totalTx - m_prevTx : 0; + + m_downloadTotal += static_cast(rxDelta); + m_uploadTotal += static_cast(txDelta); + + if (elapsed > 0.0) { + // Calculate speeds + m_downloadSpeed = static_cast(rxDelta) / elapsed; + m_uploadSpeed = static_cast(txDelta) / elapsed; + + m_downloadBuffer->push(m_downloadSpeed); + m_uploadBuffer->push(m_uploadSpeed); + } + + m_prevRx = totalRx; + m_prevTx = totalTx; + + emit changed(); +} + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/networkusage.hpp b/Plugins/ZShell/Services/networkusage.hpp new file mode 100644 index 0000000..7a6555b --- /dev/null +++ b/Plugins/ZShell/Services/networkusage.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +#include "internal/circularbuffer.hpp" +#include "tickingservice.hpp" + +namespace ZShell::services { + +class NetworkUsage : public TickingService { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(qreal downloadSpeed READ downloadSpeed NOTIFY changed) + Q_PROPERTY(qreal uploadSpeed READ uploadSpeed NOTIFY changed) + Q_PROPERTY(qreal downloadTotal READ downloadTotal NOTIFY changed) + Q_PROPERTY(qreal uploadTotal READ uploadTotal NOTIFY changed) + Q_PROPERTY(int historyLength READ historyLength NOTIFY historyLengthChanged) + + Q_PROPERTY( + ZShell::internal::CircularBuffer* downloadBuffer READ downloadBuffer + CONSTANT) + Q_PROPERTY( + ZShell::internal::CircularBuffer* uploadBuffer READ uploadBuffer + CONSTANT) + + public: + explicit NetworkUsage(QObject* parent = nullptr); + + [[nodiscard]] qreal downloadSpeed() const; + [[nodiscard]] qreal uploadSpeed() const; + [[nodiscard]] qreal downloadTotal() const; + [[nodiscard]] qreal uploadTotal() const; + [[nodiscard]] int historyLength() const; + + [[nodiscard]] internal::CircularBuffer* downloadBuffer() const; + [[nodiscard]] internal::CircularBuffer* uploadBuffer() const; + + signals: + void changed(); + void historyLengthChanged(); + + protected: + void tick() override; + + private: + void setHistoryLength(int length); + + qreal m_downloadSpeed = 0.0; + qreal m_uploadSpeed = 0.0; + qreal m_downloadTotal = 0.0; + qreal m_uploadTotal = 0.0; + int m_historyLength = 30; + + internal::CircularBuffer* m_downloadBuffer = nullptr; + internal::CircularBuffer* m_uploadBuffer = nullptr; + + quint64 m_prevRx = 0; + quint64 m_prevTx = 0; + bool m_initialized = false; + QElapsedTimer m_timer; +}; + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/processes.cpp b/Plugins/ZShell/Services/processes.cpp index 6175736..96cef22 100644 --- a/Plugins/ZShell/Services/processes.cpp +++ b/Plugins/ZShell/Services/processes.cpp @@ -2,8 +2,13 @@ #include #include +#include +#include +#include +#include #include #include +#include #include #include @@ -12,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +59,82 @@ template int cmpVals(const T& a, const T& b) { return (a < b) - (a > b); } +// glibc hides getdents64 behind __USE_MISC; declare directly (no _GNU_SOURCE +// in this project). +extern "C" { +struct ProcDirEnt64 { + unsigned long d_ino; + off_t d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[1]; +}; + +ssize_t getdents64(int fd, void* dirp, size_t count); +} + +// Raw open/read/close instead of QFile: QFSFileEngine issues an extra +// statx per open, and the per-file object churn adds up across ~500 +// processes per refresh. +QByteArray readProcFile(const char* path) { + const int fd = ::open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return {}; + } + QByteArray data; + char buf[4096]; + for (;;) { + const ssize_t n = ::read(fd, buf, sizeof(buf)); + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + data.append(buf, static_cast(n)); + } + ::close(fd); + return data; +} + +void procPath(char* out, size_t size, pid_t pid, const char* what) { + ::snprintf(out, size, "/proc/%d/%s", static_cast(pid), what); +} + +QVector listProcPids() { + QVector pids; + const int fd = ::openat(AT_FDCWD, "/proc", O_RDONLY | O_DIRECTORY); + if (fd < 0) { + return pids; + } + + std::array buf; + for (;;) { + const ssize_t n = ::getdents64( + fd, reinterpret_cast(buf.data()), buf.size()); + if (n <= 0) { + break; + } + for (ssize_t off = 0; off < n;) { + const auto* d = + reinterpret_cast(buf.data() + off); + if (d->d_type == DT_DIR) { + bool ok = false; + const long v = QByteArray(d->d_name).toLong(&ok); + if (ok && v > 0) { + pids.push_back(static_cast(v)); + } + } + off += static_cast(d->d_reclen); + } + } + ::close(fd); + return pids; +} + struct FuzzyText { QString chars; @@ -460,19 +542,24 @@ void ProcessModel::setDesktopEntryIcons( } QString ProcessModel::resolveProcessExe(pid_t pid) { - const QString path = QStringLiteral("/proc/%1/exe").arg(pid); - const QString target = QFile::symLinkTarget(path); - - return target; + char path[40]; + procPath(path, sizeof(path), pid, "exe"); + char buf[4096]; + const ssize_t n = ::readlink(path, buf, sizeof(buf) - 1); + if (n < 0) { + return {}; + } + buf[n] = '\0'; + return QString::fromLocal8Bit(buf, static_cast(n)); } std::optional ProcessModel::readStat(pid_t pid) { - QFile f(QStringLiteral("/proc/%1/stat").arg(pid)); - if (!f.open(QIODevice::ReadOnly)) { + char path[40]; + procPath(path, sizeof(path), pid, "stat"); + const QByteArray data = readProcFile(path); + if (data.isEmpty()) { return std::nullopt; } - const QByteArray data = f.readAll(); - f.close(); const qsizetype openParen = data.indexOf('('); const qsizetype closeParen = data.lastIndexOf(')'); @@ -500,12 +587,9 @@ std::optional ProcessModel::readStat(pid_t pid) { } QString ProcessModel::readCommandLine(pid_t pid, const QString& fallbackComm) { - QFile f(QStringLiteral("/proc/%1/cmdline").arg(pid)); - if (!f.open(QIODevice::ReadOnly)) { - return QStringLiteral("[%1]").arg(fallbackComm); - } - QByteArray data = f.readAll(); - f.close(); + char path[40]; + procPath(path, sizeof(path), pid, "cmdline"); + QByteArray data = readProcFile(path); if (data.isEmpty()) { return QStringLiteral("[%1]").arg(fallbackComm); @@ -523,12 +607,9 @@ QString ProcessModel::readCommandLine(pid_t pid, const QString& fallbackComm) { } ProcessModel::StatusInfo ProcessModel::readStatus(pid_t pid) { - QFile f(QStringLiteral("/proc/%1/status").arg(pid)); - if (!f.open(QIODevice::ReadOnly)) { - return {}; - } - const QByteArray data = f.readAll(); - f.close(); + char path[40]; + procPath(path, sizeof(path), pid, "status"); + const QByteArray data = readProcFile(path); StatusInfo out; qsizetype pos = 0; @@ -554,12 +635,9 @@ ProcessModel::StatusInfo ProcessModel::readStatus(pid_t pid) { } ProcessModel::IoInfo ProcessModel::readIo(pid_t pid) { - QFile f(QStringLiteral("/proc/%1/io").arg(pid)); - if (!f.open(QIODevice::ReadOnly)) { - return {}; - } - const QByteArray data = f.readAll(); - f.close(); + char path[40]; + procPath(path, sizeof(path), pid, "io"); + const QByteArray data = readProcFile(path); IoInfo out; qsizetype pos = 0; @@ -606,12 +684,10 @@ QString ProcessModel::resolveUsername(uid_t uid, QHash& cache) { } std::optional ProcessModel::readUptimeSeconds() { - QFile f(QStringLiteral("/proc/uptime")); - if (!f.open(QIODevice::ReadOnly)) { + const QByteArray data = readProcFile("/proc/uptime"); + if (data.isEmpty()) { return std::nullopt; } - const QByteArray data = f.readAll(); - f.close(); const auto fields = splitWhitespace(QByteArrayView(data)); if (fields.isEmpty()) { @@ -623,35 +699,17 @@ std::optional ProcessModel::readUptimeSeconds() { return ok ? std::make_optional(uptime) : std::nullopt; } -ProcessModel::ScanResult ProcessModel::scanProcesses( - double uptime, - double deltaSecs, - QHash prevPidToAll, - QVector prevAll, - QHash uidCache, - long clockTicksPerSec, - QHash execPathToIcon, - QHash execNameToIcon, - QStringList pathDirs, - bool desktopEntryIconsChanged, - quint64 iconVersion) { - Q_UNUSED( - pathDirs); // kept for future cgroup fallback; sanitizeExec already ran on GUI thread - - QDir procDir(QStringLiteral("/proc")); - const auto entries = - procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::NoSort); - - QVector fresh; - fresh.reserve(entries.size()); - - for (const QString& name : entries) { - bool isPid = false; - const pid_t pid = static_cast(name.toLong(&isPid)); - if (!isPid || pid <= 0) { - continue; - } +void ProcessModel::scanChunk( + const ScanContext& ctx, + const QVector& pids, + QVector& out, + QHash& uidCache) { + const QHash& prevPidToAll = *ctx.prevPidToAll; + const QVector& prevAll = *ctx.prevAll; + const QHash& execPathToIcon = *ctx.execPathToIcon; + const QHash& execNameToIcon = *ctx.execNameToIcon; + for (pid_t pid : pids) { const auto statOpt = readStat(pid); if (!statOpt) { continue; @@ -675,7 +733,7 @@ ProcessModel::ScanResult ProcessModel::scanProcesses( entry.commandLine = readCommandLine(pid, stat.comm); } - if (!samePidInstance || desktopEntryIconsChanged) { + if (!samePidInstance || ctx.iconsChanged) { const QString exePath = resolveProcessExe(pid); QString iconName; @@ -711,18 +769,23 @@ ProcessModel::ScanResult ProcessModel::scanProcesses( entry.uid = status.uid; entry.username = resolveUsername(status.uid, uidCache); - const IoInfo io = readIo(pid); + // /proc/PID/io is only readable by root or the process owner; skip + // the open when it is guaranteed to fail. + IoInfo io; + if (ctx.selfEuid == 0 || status.uid == ctx.selfEuid) { + io = readIo(pid); + } entry.ioReadBytes = io.readBytes; entry.ioWriteBytes = io.writeBytes; entry.elapsedSecs = std::max( 0.0, - uptime - (static_cast(stat.starttime) / - static_cast(clockTicksPerSec))); + ctx.uptime - (static_cast(stat.starttime) / + static_cast(ctx.clockTicksPerSec))); entry.lastUtime = stat.utime; entry.lastStime = stat.stime; - if (samePidInstance && deltaSecs > 0.0) { + if (samePidInstance && ctx.deltaSecs > 0.0) { const ProcessEntry& prev = prevAll.at(prevIt.value()); const quint64 prevTicks = prev.lastUtime + prev.lastStime; const quint64 nowTicks = stat.utime + stat.stime; @@ -730,13 +793,97 @@ ProcessModel::ScanResult ProcessModel::scanProcesses( nowTicks > prevTicks ? nowTicks - prevTicks : 0; entry.cpuPercent = (static_cast(deltaTicks) / - (deltaSecs * static_cast(clockTicksPerSec))) * + (ctx.deltaSecs * + static_cast(ctx.clockTicksPerSec))) * 100.0; } else { entry.cpuPercent = 0.0; } - fresh.push_back(std::move(entry)); + out.push_back(std::move(entry)); + } +} + +ProcessModel::ScanResult ProcessModel::scanProcesses( + double uptime, + double deltaSecs, + QHash prevPidToAll, + QVector prevAll, + QHash uidCache, + long clockTicksPerSec, + QHash execPathToIcon, + QHash execNameToIcon, + QStringList pathDirs, + bool desktopEntryIconsChanged, + quint64 iconVersion) { + Q_UNUSED( + pathDirs); // kept for future cgroup fallback; sanitizeExec already ran on GUI thread + + const QVector pids = listProcPids(); + + ScanContext ctx; + ctx.uptime = uptime; + ctx.deltaSecs = deltaSecs; + ctx.prevPidToAll = &prevPidToAll; + ctx.prevAll = &prevAll; + ctx.clockTicksPerSec = clockTicksPerSec; + ctx.execPathToIcon = &execPathToIcon; + ctx.execNameToIcon = &execNameToIcon; + ctx.iconsChanged = desktopEntryIconsChanged; + ctx.selfEuid = ::geteuid(); + + QVector fresh; + fresh.reserve(pids.size()); + + // Split the pid list across worker threads; a single pass over /proc is + // syscall-bound (open/read/close per file per process) and serializes + // badly on large systems. Keep one pool thread free for this function, + // which itself runs inside the global pool. + int nThreads = qMin(16, QThreadPool::globalInstance()->maxThreadCount() - 1); + if (pids.size() < 128 || nThreads < 2) { + nThreads = 1; + } + + if (nThreads <= 1) { + scanChunk(ctx, pids, fresh, uidCache); + } else { + const qsizetype per = (pids.size() + nThreads - 1) / nThreads; + QVector> chunks(nThreads); + QVector> outs(nThreads); + QVector> uidParts(nThreads); + QVector> futures; + + for (int t = 0; t < nThreads; ++t) { + const qsizetype a = static_cast(t) * per; + const qsizetype b = qMin(a + per, pids.size()); + if (a >= b) { + continue; + } + chunks[t] = pids.mid(a, b - a); + futures.append(QtConcurrent::run( + [&ctx, &chunks, &outs, &uidParts, t]() { + scanChunk(ctx, chunks[t], outs[t], uidParts[t]); + })); + } + for (QFuture& f : futures) { + f.waitForFinished(); + } + + for (qsizetype t = 0; t < nThreads; ++t) { + for (ProcessEntry& e : outs[t]) { + fresh.push_back(std::move(e)); + } + for (auto it = uidParts[t].constBegin(); + it != uidParts[t].constEnd(); + ++it) { + uidCache.insert(it.key(), it.value()); + } + } + std::sort( + fresh.begin(), fresh.end(), [](const ProcessEntry& a, + const ProcessEntry& b) { + return a.pid < b.pid; + }); } ScanResult result; @@ -810,6 +957,13 @@ void ProcessModel::onScanFinished() { for (int i = 0; i < mAll.size(); ++i) { mPidToAll.insert(mAll.at(i).pid, i); } + for (auto it = mFuzzyCache.begin(); it != mFuzzyCache.end();) { + if (mPidToAll.contains(it.key())) { + ++it; + } else { + it = mFuzzyCache.erase(it); + } + } for (auto it = result.newUidNames.constBegin(); it != result.newUidNames.constEnd(); ++it) { @@ -846,11 +1000,35 @@ void ProcessModel::applyView() { ProcessEntry entry = src; if (searching) { - int score = fuzzyQueryScore(fuzzyPrepare(src.command), terms); - if (!src.commandLine.isEmpty() && src.commandLine != src.command) { - score = qMax( - score, - fuzzyQueryScore(fuzzyPrepare(src.commandLine), terms)); + // Normalizing (NFD) + lowering per process per tick is the + // expensive part of search; commands are stable for the life of + // a process instance (pid + starttime), so cache the prepared + // text. comm can still change without an exec (prctl), so + // re-prepare when the raw command differs. + FuzzyCacheEntry& fe = mFuzzyCache[src.pid]; + if (fe.starttime != src.starttime || fe.command != src.command) { + fe.starttime = src.starttime; + fe.command = src.command; + const FuzzyText prep = fuzzyPrepare(src.command); + fe.commandChars = std::move(prep.chars); + fe.commandClasses = std::move(prep.classes); + fe.hasCommandLine = + !src.commandLine.isEmpty() && src.commandLine != src.command; + if (fe.hasCommandLine) { + const FuzzyText prepCl = fuzzyPrepare(src.commandLine); + fe.commandLineChars = std::move(prepCl.chars); + fe.commandLineClasses = std::move(prepCl.classes); + } else { + fe.commandLineChars = QString(); + fe.commandLineClasses = {}; + } + } + const FuzzyText cmdFuzzy{fe.commandChars, fe.commandClasses}; + int score = fuzzyQueryScore(cmdFuzzy, terms); + if (fe.hasCommandLine) { + const FuzzyText clFuzzy{ + fe.commandLineChars, fe.commandLineClasses}; + score = qMax(score, fuzzyQueryScore(clFuzzy, terms)); } if (score < 0) { continue; diff --git a/Plugins/ZShell/Services/processes.hpp b/Plugins/ZShell/Services/processes.hpp index b82cac6..ea48f4c 100644 --- a/Plugins/ZShell/Services/processes.hpp +++ b/Plugins/ZShell/Services/processes.hpp @@ -75,6 +75,16 @@ class ProcessModel : public QAbstractListModel { void sortAscendingChanged(); private: + struct FuzzyCacheEntry { + quint64 starttime = 0; + QString command; + QString commandChars; + QVector commandClasses; + QString commandLineChars; + QVector commandLineClasses; + bool hasCommandLine = false; + }; + struct RawStat { pid_t pid = 0; pid_t ppid = 0; @@ -116,6 +126,18 @@ class ProcessModel : public QAbstractListModel { quint64 writeBytes = 0; }; + struct ScanContext { + double uptime = 0.0; + double deltaSecs = -1.0; + const QHash* prevPidToAll = nullptr; + const QVector* prevAll = nullptr; + long clockTicksPerSec = 100; + const QHash* execPathToIcon = nullptr; + const QHash* execNameToIcon = nullptr; + bool iconsChanged = false; + uid_t selfEuid = 0; + }; + struct ScanResult { QVector entries; QHash newUidNames; @@ -130,6 +152,12 @@ class ProcessModel : public QAbstractListModel { static QString resolveUsername(uid_t uid, QHash& cache); static QString resolveProcessExe(pid_t pid); + static void scanChunk( + const ScanContext& ctx, + const QVector& pids, + QVector& out, + QHash& uidCache); + static ScanResult scanProcesses( double uptime, double deltaSecs, @@ -153,6 +181,7 @@ class ProcessModel : public QAbstractListModel { QVector mAll; QHash mPidToAll; + QHash mFuzzyCache; QVector mProcesses; QHash mUidToName; diff --git a/Plugins/ZShell/Services/tickingservice.cpp b/Plugins/ZShell/Services/tickingservice.cpp index 05e6c83..0dc67fa 100644 --- a/Plugins/ZShell/Services/tickingservice.cpp +++ b/Plugins/ZShell/Services/tickingservice.cpp @@ -1,11 +1,7 @@ #include "tickingservice.hpp" -#include -#include -#include -#include -#include -#include -#include + +#include "config/dashboard.hpp" +#include "config/rootnodes.hpp" namespace ZShell::services { @@ -17,53 +13,13 @@ TickingService::TickingService(QObject* parent) updateTriggered(); }); - QString configPath = - QDir::homePath() + QStringLiteral("/.config/zshell/config.json"); - - auto reloadConfig = [this, configPath]() { - QFile file(configPath); - if (!file.open(QIODevice::ReadOnly)) { - return; - } - QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - if (doc.isNull()) { - return; - } - QJsonObject dashboard = doc.object().value("dashboard").toObject(); - if (dashboard.contains("resourceUpdateInterval")) { - applyInterval( - dashboard.value("resourceUpdateInterval").toInt(1000)); - } - }; - - reloadConfig(); - - static auto* watcher = new QFileSystemWatcher(); - if (!watcher->files().contains(configPath)) { - QObject::connect( - watcher, - &QFileSystemWatcher::fileChanged, - this, - [this, configPath]() { - QTimer::singleShot(100, this, [this, configPath]() { - QFile file(configPath); - if (!file.open(QIODevice::ReadOnly)) { - return; - } - QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - if (doc.isNull()) { - return; - } - QJsonObject dashboard = - doc.object().value("dashboard").toObject(); - if (dashboard.contains("resourceUpdateInterval")) { - applyInterval(dashboard.value("resourceUpdateInterval") - .toInt(1000)); - } - }); - }); - watcher->addPath(configPath); - } + auto* dash = ZShell::config::ConfigSingleton::instance()->dashboard(); + applyInterval(dash->resourceUpdateInterval()); + QObject::connect( + dash, + &ZShell::config::Dashboard::resourceUpdateIntervalChanged, + this, + [this, dash] { applyInterval(dash->resourceUpdateInterval()); }); } int TickingService::updateInterval() const { diff --git a/Plugins/ZShell/Services/tickingservice.hpp b/Plugins/ZShell/Services/tickingservice.hpp index e73a911..5cd7fe1 100644 --- a/Plugins/ZShell/Services/tickingservice.hpp +++ b/Plugins/ZShell/Services/tickingservice.hpp @@ -1,8 +1,9 @@ #pragma once -#include "service.hpp" #include +#include "service.hpp" + namespace ZShell::services { class TickingService : public Service { diff --git a/Plugins/ZShell/Services/usagefmt.cpp b/Plugins/ZShell/Services/usagefmt.cpp index 1d4cf32..28d1f5b 100644 --- a/Plugins/ZShell/Services/usagefmt.cpp +++ b/Plugins/ZShell/Services/usagefmt.cpp @@ -48,4 +48,20 @@ FormatResult UsageFmt::formatKib(qreal kib, qreal total) const { return {kib, total, "KiB"}; } +FormatResult UsageFmt::formatBytes(qreal bytes) const { + static constexpr std::array units = { + "B", "KB", "MB", "GB", "TB"}; + + qreal value = finitePositive(bytes) ? bytes : 0.0; + const qreal k = 1024.0; + std::size_t i = 0; + + while (value >= k && i < units.size() - 1) { + value /= k; + i++; + } + + return {value, 0.0, units[i]}; +} + } // namespace ZShell::services::usagefmt diff --git a/Plugins/ZShell/Services/usagefmt.hpp b/Plugins/ZShell/Services/usagefmt.hpp index 38a06fb..288ce2e 100644 --- a/Plugins/ZShell/Services/usagefmt.hpp +++ b/Plugins/ZShell/Services/usagefmt.hpp @@ -31,6 +31,7 @@ class UsageFmt : public QObject { Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib) const; Q_INVOKABLE [[nodiscard]] FormatResult formatKib( qreal kib, qreal total) const; + Q_INVOKABLE [[nodiscard]] FormatResult formatBytes(qreal bytes) const; }; } // namespace ZShell::services::usagefmt diff --git a/Plugins/ZShell/Settings/macros.hpp b/Plugins/ZShell/Settings/macros.hpp index 6d065fe..b3b47fb 100644 --- a/Plugins/ZShell/Settings/macros.hpp +++ b/Plugins/ZShell/Settings/macros.hpp @@ -4,190 +4,205 @@ #include #include -#include "listnode.hpp" // IWYU pragma: keep -#include "schema.hpp" // IWYU pragma: keep +#include "listnode.hpp" +#include "schema.hpp" namespace ZShell::settings { -inline QVariantMap vmap(std::initializer_list> entries) { - QVariantMap map; - for (const auto& [key, value] : entries) - map.insert(key, value); - return map; +inline QVariantMap vmap( + std::initializer_list> entries) { + QVariantMap map; + for (const auto& [key, value] : entries) + map.insert(key, value); + return map; } template inline QList unionTypes() { - static_assert(sizeof...(Ts) >= 2, "A union needs at least two types"); - // If the max size is changed, common.cpp `mismatchStr` must be updated - static_assert(sizeof...(Ts) <= 4, "A union cannot have more than 4 types"); - static_assert((!std::is_same_v && ...), "A union cannot contain QVariant"); + static_assert(sizeof...(Ts) >= 2, "A union needs at least two types"); + static_assert(sizeof...(Ts) <= 4, "A union cannot have more than 4 types"); + static_assert( + (!std::is_same_v && ...), + "A union cannot contain QVariant"); - return { QMetaType::fromType()... }; + return {QMetaType::fromType()...}; } namespace detail { template inline bool compare(const T& a, const T& b) { - return a == b; + return a == b; } template bool compare(const T& a, const T& b) { - return qFuzzyCompare(a + T(1), b + T(1)); + return qFuzzyCompare(a + T(1), b + T(1)); } -template bool compare(const QList& a, const QList& b) { - if (a.size() != b.size()) - return false; +template +bool compare(const QList& a, const QList& b) { + if (a.size() != b.size()) return false; - for (qsizetype i = 0; i < a.size(); ++i) - if (!compare(a.at(i), b.at(i))) - return false; - return true; + for (qsizetype i = 0; i < a.size(); ++i) + if (!compare(a.at(i), b.at(i))) return false; + return true; } } // namespace detail } // namespace ZShell::settings -// Helper macro to prevent splitting initialiser lists #define DEFAULT_ARG(...) __VA_ARGS__ -// Declares a class to be a node class. This replaces the Q_OBJECT call at the top of the class. -#define SETTINGS_NODE_NO_CTOR(Class, Base) \ - Q_OBJECT \ - \ -public: \ - [[nodiscard]] const ZShell::settings::Schema& schema() const override { \ - static const auto schema = \ - ZShell::settings::Schema::build(&staticMetaObject, Base::staticMetaObject.propertyCount()); \ - return schema; \ - } \ - \ -private: \ - using Self = Class; // For use in the below macros +#define SETTINGS_NODE_NO_CTOR(Class, Base) \ + Q_OBJECT \ + \ + public: \ + [[nodiscard]] const ZShell::settings::Schema& schema() const override { \ + static const auto schema = ZShell::settings::Schema::build( \ + &staticMetaObject, Base::staticMetaObject.propertyCount()); \ + return schema; \ + } \ + \ + private: \ + using Self = Class; // For use in the below macros -#define SETTINGS_NODE(Class, Base) \ - SETTINGS_NODE_NO_CTOR(Class, Base) \ - QML_ANONYMOUS \ - \ -public: \ - explicit Class(Class* fallback = nullptr, QObject* parent = nullptr, bool globalOnly = false) \ - : Base(fallback, parent, globalOnly) {} \ - \ -private: +#define SETTINGS_NODE(Class, Base) \ + SETTINGS_NODE_NO_CTOR(Class, Base) \ + QML_ANONYMOUS \ + \ + public: \ + explicit Class( \ + Class* fallback = nullptr, \ + QObject* parent = nullptr, \ + bool globalOnly = false) \ + : Base(fallback, parent, globalOnly) {} \ + \ + private: -// Defines a property on a node. -#define SETTINGS_PROPERTY_IMPL(Type, name, global, defaultVal, ...) \ - Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ - \ -public: \ - [[nodiscard]] Type name() const { \ - if (global || m_globalOnly) \ - warnGlobalRead(QStringLiteral(#name)); \ - return m_##name; \ - } \ - \ - void set_##name(const Type& value) { \ - if (!true /* TODO: validation */) \ - return; \ - \ - if (rejectInvalidWrite(QStringLiteral(#name), value)) \ - return; /* Skip writes of the wrong type */ \ - \ - if (rejectGlobalWrite(QStringLiteral(#name))) \ - return; /* Skip writes to global only keys, they should be sent to the global layer */ \ - \ - const auto needsNotify = !ZShell::settings::detail::compare(value, m_##name); \ - m_##name = value; \ - if (recordWrite(QStringLiteral(#name), needsNotify)) \ - Q_EMIT name##Changed(); \ - } \ - \ - Q_SIGNAL void name##Changed(); \ - \ -private: \ - Type m_##name = fallbackValue(&Self::m_##name, defaultVal); \ - inline static const bool s_register_##name = \ - (ZShell::settings::Schema::annotate(&staticMetaObject, QStringLiteral(#name), \ - { .defaultValue = QVariant::fromValue(defaultVal), .globalOnly = global, __VA_ARGS__ }), \ - true); +#define SETTINGS_PROPERTY_IMPL(Type, name, global, defaultVal, ...) \ + Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ + \ + public: \ + [[nodiscard]] Type name() const { \ + if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \ + return m_##name; \ + } \ + \ + void set_##name(const Type& value) { \ + if (!true /* TODO: validation */) return; \ + \ + if (rejectInvalidWrite(QStringLiteral(#name), value)) \ + return; /* Skip writes of the wrong type */ \ + \ + if (rejectGlobalWrite(QStringLiteral(#name))) \ + return; /* Skip writes to global only keys */ \ + \ + const auto needsNotify = \ + !ZShell::settings::detail::compare(value, m_##name); \ + m_##name = value; \ + if (recordWrite(QStringLiteral(#name), needsNotify)) \ + Q_EMIT name##Changed(); \ + } \ + \ + Q_SIGNAL void name##Changed(); \ + \ + private: \ + Type m_##name = fallbackValue(&Self::m_##name, defaultVal); \ + inline static const bool s_register_##name = \ + (ZShell::settings::Schema::annotate( \ + &staticMetaObject, \ + QStringLiteral(#name), \ + {.defaultValue = QVariant::fromValue(defaultVal), \ + .globalOnly = global, \ + __VA_ARGS__}), \ + true); -#define SETTINGS_PROPERTY(Type, name, defaultVal, ...) \ - SETTINGS_PROPERTY_IMPL(Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__) +#define SETTINGS_PROPERTY(Type, name, defaultVal, ...) \ + SETTINGS_PROPERTY_IMPL( \ + Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__) // Defines a global property on a node. Shorthand for .globalOnly = true. -#define SETTINGS_GLOBAL_PROPERTY(Type, name, defaultVal, ...) \ - SETTINGS_PROPERTY_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__) +#define SETTINGS_GLOBAL_PROPERTY(Type, name, defaultVal, ...) \ + SETTINGS_PROPERTY_IMPL( \ + Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__) // Defines a subobject property on a node. Subobject properties are CONSTANT. -#define SETTINGS_SUBOBJECT_IMPL(Type, name, global) \ - Q_PROPERTY(Type* name READ name CONSTANT) \ - \ -public: \ - [[nodiscard]] Type* name() const { \ - if (global || m_globalOnly) \ - warnGlobalRead(QStringLiteral(#name)); \ - return m_##name; \ - } \ - \ -private: \ - Type* m_##name = new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \ - inline static const bool s_register_##name = \ - (ZShell::settings::Schema::annotate( \ - &staticMetaObject, QStringLiteral(#name), { .defaultValue = {}, .globalOnly = global }), \ - true); +#define SETTINGS_SUBOBJECT_IMPL(Type, name, global) \ + Q_PROPERTY(Type* name READ name CONSTANT) \ + \ + public: \ + [[nodiscard]] Type* name() const { \ + if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \ + return m_##name; \ + } \ + \ + private: \ + Type* m_##name = \ + new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \ + inline static const bool s_register_##name = \ + (ZShell::settings::Schema::annotate( \ + &staticMetaObject, \ + QStringLiteral(#name), \ + {.defaultValue = {}, .globalOnly = global}), \ + true); -#define SETTINGS_SUBOBJECT(Type, name) SETTINGS_SUBOBJECT_IMPL(Type, name, false) +#define SETTINGS_SUBOBJECT(Type, name) \ + SETTINGS_SUBOBJECT_IMPL(Type, name, false) -// Defines a global subobject on a node. Everything inside it is global only. -#define SETTINGS_GLOBAL_SUBOBJECT(Type, name) SETTINGS_SUBOBJECT_IMPL(Type, name, true) +#define SETTINGS_GLOBAL_SUBOBJECT(Type, name) \ + SETTINGS_SUBOBJECT_IMPL(Type, name, true) -// Defines a list type for use with SETTINGS_LIST. -#define SETTINGS_LIST_TYPE(Element, Name) \ - class Name : public ZShell::settings::ListNode { \ - Q_OBJECT \ - QML_ANONYMOUS \ - \ - public: \ - explicit Name(Name* fallback = nullptr, QObject* parent = nullptr, bool globalOnly = false) \ - : ZShell::settings::ListNode(fallback, parent, globalOnly) {} \ - \ - [[nodiscard]] Q_INVOKABLE Element* at(qsizetype index) const { /* Format ugh */ \ - return static_cast(elementAt(index)); \ - } \ - [[nodiscard]] Q_INVOKABLE Element* insert(const QVariantMap& props, qsizetype index = -1) { \ - return static_cast(insertElement(props, index)); \ - } \ - \ - protected: \ - [[nodiscard]] ZShell::settings::Node* createElement(ZShell::settings::Node* fallback) override { \ - return new Element(static_cast(fallback), this); \ - } \ - }; +#define SETTINGS_LIST_TYPE(Element, Name) \ + class Name : public ZShell::settings::ListNode { \ + Q_OBJECT \ + QML_ANONYMOUS \ + \ + public: \ + explicit Name( \ + Name* fallback = nullptr, \ + QObject* parent = nullptr, \ + bool globalOnly = false) \ + : ZShell::settings::ListNode(fallback, parent, globalOnly) {} \ + \ + [[nodiscard]] Q_INVOKABLE Element* at( \ + qsizetype index) const { /* Format ugh */ \ + return static_cast(elementAt(index)); \ + } \ + [[nodiscard]] Q_INVOKABLE Element* insert( \ + const QVariantMap& props, qsizetype index = -1) { \ + return static_cast(insertElement(props, index)); \ + } \ + \ + protected: \ + [[nodiscard]] ZShell::settings::Node* createElement( \ + ZShell::settings::Node* fallback) override { \ + return new Element(static_cast(fallback), this); \ + } \ + }; -// Defines a list property on a node. List properties are CONSTANT. -#define SETTINGS_LIST_IMPL(Type, name, global, defaultVal, ...) \ - Q_PROPERTY(Type* name READ name CONSTANT) \ - \ -public: \ - [[nodiscard]] Type* name() const { \ - if (global || m_globalOnly) \ - warnGlobalRead(QStringLiteral(#name)); \ - return m_##name; \ - } \ - \ -private: \ - Type* m_##name = new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \ - inline static const bool s_register_##name = \ - (ZShell::settings::Schema::annotate(&staticMetaObject, QStringLiteral(#name), \ - { .defaultValue = QVariant::fromValue>(defaultVal), \ - .globalOnly = global, \ - __VA_ARGS__ }), \ - true); +#define SETTINGS_LIST_IMPL(Type, name, global, defaultVal, ...) \ + Q_PROPERTY(Type* name READ name CONSTANT) \ + \ + public: \ + [[nodiscard]] Type* name() const { \ + if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \ + return m_##name; \ + } \ + \ + private: \ + Type* m_##name = \ + new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \ + inline static const bool s_register_##name = \ + (ZShell::settings::Schema::annotate( \ + &staticMetaObject, \ + QStringLiteral(#name), \ + {.defaultValue = \ + QVariant::fromValue>(defaultVal), \ + .globalOnly = global, \ + __VA_ARGS__}), \ + true); -#define SETTINGS_LIST(Type, name, defaultVal, ...) \ - SETTINGS_LIST_IMPL(Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__) +#define SETTINGS_LIST(Type, name, defaultVal, ...) \ + SETTINGS_LIST_IMPL(Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__) -// Defines a global list on a node. Everything inside it is global only. -#define SETTINGS_GLOBAL_LIST(Type, name, defaultVal, ...) \ - SETTINGS_LIST_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__) +#define SETTINGS_GLOBAL_LIST(Type, name, defaultVal, ...) \ + SETTINGS_LIST_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)