fix: column index desync after reorder
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 33s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Successful in 1m42s
Rust / fmt (pull_request) Successful in 26s
Rust / clippy (pull_request) Successful in 45s

This commit is contained in:
2026-09-27 22:47:31 +02:00
parent 9ea1f846ae
commit cc9aeca98b
28 changed files with 859 additions and 464 deletions
-1
View File
@@ -3,7 +3,6 @@ import QtQuick.Shapes
import ZShell import ZShell
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
import qs.Services
MouseArea { MouseArea {
id: root id: root
+27 -26
View File
@@ -12,7 +12,7 @@ Item {
id: root id: root
readonly property real nonAnimWidth: view.implicitWidth + clipWrapper.anchors.margins * 2 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 required property ScreenState screenState
@@ -22,25 +22,25 @@ Item {
component: dashComponent, component: dashComponent,
iconName: "dashboard", iconName: "dashboard",
text: qsTr("Dashboard"), text: qsTr("Dashboard"),
enabled: true enabled: Config.dashboard.showDashboard
}, },
{ {
component: weatherComponent, component: weatherComponent,
iconName: "cloud", iconName: "cloud",
text: qsTr("Weather"), text: qsTr("Weather"),
enabled: true enabled: Config.dashboard.showWeather
}, },
{ {
component: mediaComponent, component: mediaComponent,
iconName: "queue_music", iconName: "queue_music",
text: qsTr("Media"), text: qsTr("Media"),
enabled: true enabled: Config.dashboard.showMedia
}, },
{ {
component: procsComponent, component: procsComponent,
iconName: "speed", iconName: "speed",
text: qsTr("Processes"), text: qsTr("Processes"),
enabled: true enabled: Config.dashboard.showProcesses
}, },
]; ];
return allTabs.filter(tab => tab.enabled); return allTabs.filter(tab => tab.enabled);
@@ -60,33 +60,34 @@ Item {
} }
} }
Item { Tabs {
id: clipWrapper id: tabs
anchors.fill: parent
anchors.margins: Tokens.padding.small anchors.top: parent.top
// radius: Tokens.rounding.extraLarge - anchors.margins 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.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
anchors.margins: Tokens.padding.small
}
Tabs { CustomClippingRect {
id: tabs id: clipWrapper
anchors.top: tabs.bottom
anchors.top: parent.top anchors.left: parent.left
anchors.left: parent.left anchors.right: parent.right
anchors.right: parent.right anchors.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small
nonAnimWidth: view.implicitWidth anchors.margins: Tokens.padding.small
screenState: root.screenState radius: Tokens.rounding.extraLarge - anchors.margins
tabs: root.dashboardTabs
}
Item { Item {
id: viewWrapper id: viewWrapper
anchors.top: tabs.bottom anchors.fill: parent
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small
Flickable { Flickable {
id: view id: view
+1 -1
View File
@@ -67,7 +67,7 @@ GridLayout {
Rect { Rect {
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: Config.dashboard.sizes.weatherWidth Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth
Layout.row: 0 Layout.row: 0
radius: root.radius radius: root.radius
+4 -4
View File
@@ -51,7 +51,7 @@ Row {
id: icon id: icon
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2
color: Colors.palette.m3primary color: Colors.palette.m3primary
implicitSize: Math.floor(13 * 1.34) implicitSize: Math.floor(13 * 1.34)
source: SystemInfo.osLogo source: SystemInfo.osLogo
@@ -65,7 +65,7 @@ Row {
anchors.verticalCenter: icon.verticalCenter anchors.verticalCenter: icon.verticalCenter
elide: Text.ElideRight elide: Text.ElideRight
text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}` text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}`
width: Config.dashboard.sizes.infoWidth width: Tokens.sizes.dashboard.infoWidth
} }
} }
@@ -98,7 +98,7 @@ Row {
id: icon id: icon
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2
color: line.colour color: line.colour
fill: 1 fill: 1
fontStyle: Tokens.font.icon.small fontStyle: Tokens.font.icon.small
@@ -113,7 +113,7 @@ Row {
anchors.verticalCenter: icon.verticalCenter anchors.verticalCenter: icon.verticalCenter
elide: Text.ElideNone elide: Text.ElideNone
text: `: ${line.text}` text: `: ${line.text}`
width: Config.dashboard.sizes.infoWidth width: Tokens.sizes.dashboard.infoWidth
} }
} }
} }
@@ -3,6 +3,7 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
import ZShell.Config import ZShell.Config
import ZShell.Services
Singleton { Singleton {
id: root 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 string pinnedColumnId: "command"
readonly property var hiddenColumns: { readonly property var hiddenColumns: {
@@ -113,8 +126,6 @@ Singleton {
}); });
} }
Component.onCompleted: Qt.callLater(ensurePinnedFirst)
function defFor(id: string): var { function defFor(id: string): var {
for (const def of root.columnDefs) for (const def of root.columnDefs)
if (def.id === id) if (def.id === id)
+53 -100
View File
@@ -64,109 +64,83 @@ Item {
function onWidthChanged() { function onWidthChanged() {
root.fillMissingWidths(); root.fillMissingWidths();
} }
function onCountChanged() {
resizeOverlay.geometryTick++;
root.fillMissingWidths();
}
function onContentWidthChanged() {
resizeOverlay.geometryTick++;
}
} }
Connections { Connections {
target: columnsData target: columnsData
function onValuesChanged() { function onValuesChanged() {
root.fillMissingWidths(); root.fillMissingWidths();
resizeOverlay.geometryTick++;
} }
} }
Component.onCompleted: Qt.callLater(fillMissingWidths) Component.onCompleted: Qt.callLater(fillMissingWidths)
Component.onDestruction: Cursor.clearOverride() Component.onDestruction: Cursor.clearOverride()
RowLayout { CustomListView {
id: rowLayout id: columnList
anchors.fill: parent anchors.fill: parent
orientation: ListView.Horizontal
interactive: false
spacing: Tokens.spacing.extraSmall / 2 spacing: Tokens.spacing.extraSmall / 2
CustomListView { add: Transition {
id: columnList Anim {
from: 0
Layout.fillWidth: true properties: "opacity"
orientation: ListView.Horizontal to: 1
interactive: false type: Anim.DefaultEffects
spacing: parent.spacing
implicitHeight: root.headerHeight
add: Transition {
Anim {
from: 0
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
} }
displaced: Transition { }
Anim { displaced: Transition {
properties: "opacity" Anim {
to: 1 properties: "opacity"
type: Anim.DefaultEffects to: 1
} type: Anim.DefaultEffects
Anim {
properties: "x"
}
} }
move: Transition { Anim {
Anim { properties: "x"
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "x"
}
} }
remove: Transition { }
Anim { move: Transition {
to: 0 Anim {
properties: "opacity" properties: "opacity"
type: Anim.DefaultEffects to: 1
} type: Anim.DefaultEffects
} }
Anim {
model: DelegateModel { properties: "x"
id: columnsVisual }
}
delegate: ColumnHeaderCell { remove: Transition {
parentModel: columnsVisual Anim {
} to: 0
properties: "opacity"
model: ScriptModel { type: Anim.DefaultEffects
id: columnsData
values: Config.dashboard.processColumns.values
}
} }
} }
IconButton { model: DelegateModel {
id: addColumnBtn id: columnsVisual
type: IconButton.Text delegate: ColumnHeaderCell {
icon: "add" parentModel: columnsVisual
font: Tokens.font.icon.medium }
Layout.alignment: Qt.AlignVCenter
visible: ProcessesColumns.hiddenColumns.length > 0
onClicked: addMenu.expanded = true
}
Variants { model: ScriptModel {
id: hiddenColumnList id: columnsData
values: Config.dashboard.processColumns.values
model: ProcessesColumns.hiddenColumns }
HiddenColumnItem {}
}
Menu {
id: addMenu
attachTo: addColumnBtn
items: hiddenColumnList.instances
onItemSelected: item => ProcessesColumns.addColumn(item.modelData.id)
} }
} }
@@ -178,26 +152,9 @@ Item {
property int geometryTick: 0 property int geometryTick: 0
property int activeGapIndex: -1 property int activeGapIndex: -1
anchors.fill: rowLayout anchors.fill: columnList
z: 10 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++) Component.onCompleted: Qt.callLater(() => geometryTick++)
Repeater { Repeater {
@@ -571,8 +528,6 @@ Item {
const rightMid = rightItem.x + rightItem.width / 2; const rightMid = rightItem.x + rightItem.width / 2;
if (refX > rightMid) { if (refX > rightMid) {
columnsVisual.items.move(myIndex, myIndex + 1); columnsVisual.items.move(myIndex, myIndex + 1);
resizeOverlay.geometryTick++;
Qt.callLater(() => resizeOverlay.geometryTick++);
return; return;
} }
} }
@@ -583,8 +538,6 @@ Item {
const pinnedAtZero = columnList.itemAtIndex(0)?.modelData?.id === ProcessesColumns.pinnedColumnId; const pinnedAtZero = columnList.itemAtIndex(0)?.modelData?.id === ProcessesColumns.pinnedColumnId;
if (refX < leftMid && !(myIndex === 1 && pinnedAtZero)) { if (refX < leftMid && !(myIndex === 1 && pinnedAtZero)) {
columnsVisual.items.move(myIndex, myIndex - 1); columnsVisual.items.move(myIndex, myIndex - 1);
resizeOverlay.geometryTick++;
Qt.callLater(() => resizeOverlay.geometryTick++);
} }
} }
} }
@@ -7,7 +7,6 @@ import ZShell.Components
import ZShell.Services import ZShell.Services
import qs.Components import qs.Components
import qs.Services import qs.Services
import qs.Helpers
Item { Item {
id: root id: root
-13
View File
@@ -9,11 +9,6 @@ import qs.Services
Item { Item {
id: root id: root
readonly property var desktopEntries: DesktopEntries.applications.values.map(e => ({
icon: e.icon,
command: e.command
}))
implicitWidth: Tokens.sizes.dashboard.processesWidth implicitWidth: Tokens.sizes.dashboard.processesWidth
implicitHeight: Tokens.sizes.dashboard.processesHeight implicitHeight: Tokens.sizes.dashboard.processesHeight
@@ -21,14 +16,6 @@ Item {
service: Processes service: Processes
} }
Connections {
target: DesktopEntries.applications
function onValuesChanged(): void {
Processes.setDesktopEntries(root.desktopEntries);
}
}
Component.onCompleted: Processes.setDesktopEntries(root.desktopEntries)
ProcessesHeader { ProcessesHeader {
id: header id: header
+8 -8
View File
@@ -1,8 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components import ZShell.Components
import ZShell.Services
import ZShell.Config import ZShell.Config
import qs.Helpers
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -14,7 +14,7 @@ CustomRect {
implicitWidth: 300 implicitWidth: 300
radius: Tokens.rounding.largeIncreased radius: Tokens.rounding.largeIncreased
Ref { ServiceRef {
service: NetworkUsage service: NetworkUsage
} }
@@ -129,8 +129,8 @@ CustomRect {
CustomText { CustomText {
color: Colors.palette.m3tertiary color: Colors.palette.m3tertiary
text: { text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); const fmt = UsageFmt.formatBytes(NetworkUsage.downloadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}/s` : "0.0 B/s";
} }
} }
} }
@@ -157,8 +157,8 @@ CustomRect {
CustomText { CustomText {
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
text: { text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); const fmt = UsageFmt.formatBytes(NetworkUsage.uploadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}/s` : "0.0 B/s";
} }
} }
} }
@@ -185,8 +185,8 @@ CustomRect {
CustomText { CustomText {
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
text: { text: {
const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); const down = UsageFmt.formatBytes(NetworkUsage.downloadTotal ?? 0);
const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 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"; return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B";
} }
} }
@@ -34,6 +34,45 @@ PageBase {
onToggled: Config.dashboard.enabled = checked 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 // Behavior
SectionHeader { SectionHeader {
text: qsTr("Behavior") text: qsTr("Behavior")
@@ -58,6 +97,7 @@ PageBase {
text: qsTr("Drag threshold") text: qsTr("Drag threshold")
to: 100 to: 100
value: Config.dashboard.dragThreshold value: Config.dashboard.dragThreshold
last: true
onMoved: v => Config.dashboard.dragThreshold = v onMoved: v => Config.dashboard.dragThreshold = v
} }
+1
View File
@@ -1,5 +1,6 @@
qml_module(ZShell qml_module(ZShell
URI ZShell URI ZShell
INCLUDE_PREFIX core
SOURCES SOURCES
writefile.hpp writefile.cpp writefile.hpp writefile.cpp
appdb.hpp appdb.cpp appdb.hpp appdb.cpp
-4
View File
@@ -64,9 +64,6 @@ AnimDurations* AnimTokens::durations() const {
QEasingCurve AnimTokens::buildCurve(const QList<qreal>& points) { QEasingCurve AnimTokens::buildCurve(const QList<qreal>& points) {
QEasingCurve curve(QEasingCurve::BezierSpline); 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) { for (int i = 0; i + 5 < points.size(); i += 6) {
const QPointF c1(points[i], points[i + 1]); const QPointF c1(points[i], points[i + 1]);
const QPointF c2(points[i + 2], points[i + 3]); const QPointF c2(points[i + 2], points[i + 3]);
@@ -101,7 +98,6 @@ void AnimTokens::rebuildCurves() {
void AnimTokens::bindCurves(AnimCurves* curves) { void AnimTokens::bindCurves(AnimCurves* curves) {
m_curves = curves; m_curves = curves;
// Rebuild when any curve control points change
connect( connect(
curves, &AnimCurves::optionChanged, this, &AnimTokens::rebuildCurves); curves, &AnimCurves::optionChanged, this, &AnimTokens::rebuildCurves);
+1 -1
View File
@@ -11,7 +11,7 @@ class AnimDurations;
class AnimTokens : public QObject { class AnimTokens : public QObject {
Q_OBJECT Q_OBJECT
Q_MOC_INCLUDE("tokens.hpp") // AnimCurves Q_MOC_INCLUDE("tokens.hpp")
Q_MOC_INCLUDE("appearance.hpp") Q_MOC_INCLUDE("appearance.hpp")
QML_ANONYMOUS QML_ANONYMOUS
-10
View File
@@ -6,8 +6,6 @@ namespace ZShell::config {
namespace { namespace {
// Helper: connect all changed signals from a token object to a single valuesChanged signal,
// plus connect the local scaleChanged signal.
template <typename Source, typename Target> template <typename Source, typename Target>
void connectTokenSignals(Source* source, Target* target) { void connectTokenSignals(Source* source, Target* target) {
const auto* meta = source->metaObject(); const auto* meta = source->metaObject();
@@ -30,8 +28,6 @@ void connectTokenSignals(Source* source, Target* target) {
} // namespace } // namespace
// AppearanceRounding
void AppearanceRounding::bindTokens(RoundingTokens* tokens) { void AppearanceRounding::bindTokens(RoundingTokens* tokens) {
m_tokens = tokens; m_tokens = tokens;
connectTokenSignals(tokens, this); connectTokenSignals(tokens, this);
@@ -77,8 +73,6 @@ int AppearanceRounding::full() const {
return m_tokens ? m_tokens->full() : 0; return m_tokens ? m_tokens->full() : 0;
} }
// AppearanceSpacing
void AppearanceSpacing::bindTokens(SpacingTokens* tokens) { void AppearanceSpacing::bindTokens(SpacingTokens* tokens) {
m_tokens = tokens; m_tokens = tokens;
connectTokenSignals(tokens, this); connectTokenSignals(tokens, this);
@@ -120,8 +114,6 @@ int AppearanceSpacing::extraExtraLarge() const {
: 0; : 0;
} }
// AppearancePadding
void AppearancePadding::bindTokens(PaddingTokens* tokens) { void AppearancePadding::bindTokens(PaddingTokens* tokens) {
m_tokens = tokens; m_tokens = tokens;
connectTokenSignals(tokens, this); connectTokenSignals(tokens, this);
@@ -167,8 +159,6 @@ int AppearancePadding::extraExtraLarge() const {
: 0; : 0;
} }
// AnimDurations
void AnimDurations::bindTokens(AnimDurationTokens* tokens) { void AnimDurations::bindTokens(AnimDurationTokens* tokens) {
m_tokens = tokens; m_tokens = tokens;
connectTokenSignals(tokens, this); connectTokenSignals(tokens, this);
-1
View File
@@ -11,7 +11,6 @@ namespace ZShell::config {
using Qt::StringLiterals::operator""_s; using Qt::StringLiterals::operator""_s;
// Forward declare token types from tokens.hpp
class RoundingTokens; class RoundingTokens;
class SpacingTokens; class SpacingTokens;
class PaddingTokens; class PaddingTokens;
+4 -19
View File
@@ -17,24 +17,6 @@ class Performance : public settings::ObjectNode {
CONFIG_PROPERTY(bool, showVram, true) 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 { class ProcessColumnEntry : public settings::ObjectNode {
CONFIG_NODE(ProcessColumnEntry, settings::ObjectNode) CONFIG_NODE(ProcessColumnEntry, settings::ObjectNode)
@@ -51,9 +33,12 @@ class Dashboard : public settings::ObjectNode {
CONFIG_PROPERTY(bool, showOnHover, true) CONFIG_PROPERTY(bool, showOnHover, true)
CONFIG_PROPERTY(int, dragThreshold, 50) CONFIG_PROPERTY(int, dragThreshold, 50)
CONFIG_PROPERTY(int, mediaUpdateInterval, 2000) 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_SUBOBJECT(Performance, performance)
CONFIG_PROPERTY(int, resourceUpdateInterval, 1000) CONFIG_PROPERTY(int, resourceUpdateInterval, 1000)
CONFIG_SUBOBJECT(DashboardSizes, sizes)
CONFIG_LIST( CONFIG_LIST(
ProcessColumnList, ProcessColumnList,
processColumns, processColumns,
+13
View File
@@ -149,6 +149,19 @@ class DashboardTokens : public settings::ObjectNode {
CONFIG_PROPERTY(int, processesHeight, 800) CONFIG_PROPERTY(int, processesHeight, 800)
CONFIG_PROPERTY(int, processesHeaderHeight, 40) CONFIG_PROPERTY(int, processesHeaderHeight, 40)
CONFIG_PROPERTY(int, processesRowHeight, 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 { class SizeTokens : public settings::ObjectNode {
+6
View File
@@ -19,6 +19,7 @@ qml_module(ZShell-services
diskinfo.hpp diskinfo.cpp diskinfo.hpp diskinfo.cpp
storage.hpp storage.cpp storage.hpp storage.cpp
processes.hpp processes.cpp processes.hpp processes.cpp
networkusage.hpp networkusage.cpp
lyriccandidate.hpp lyriccandidate.cpp lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp lyrics.hpp lyrics.cpp
LIBRARIES LIBRARIES
@@ -30,6 +31,11 @@ qml_module(ZShell-services
Sensors::Sensors Sensors::Sensors
ZShell-config ZShell-config
ZShell-settings ZShell-settings
ZShell-internal
ZShell
zshell-util
DEPENDENCIES DEPENDENCIES
ZShell
ZShell.Config ZShell.Config
ZShell.Internal
) )
+153
View File
@@ -0,0 +1,153 @@
#include "networkusage.hpp"
#include <qbytearrayview.h>
#include <qfile.h>
#include <qtypes.h>
#include <array>
#include <charconv>
#include <system_error>
#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<unsigned long long, 9> 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<quint64>(fields[0]);
totalTx += static_cast<quint64>(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<qreal>(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<qreal>(rxDelta);
m_uploadTotal += static_cast<qreal>(txDelta);
if (elapsed > 0.0) {
// Calculate speeds
m_downloadSpeed = static_cast<qreal>(rxDelta) / elapsed;
m_uploadSpeed = static_cast<qreal>(txDelta) / elapsed;
m_downloadBuffer->push(m_downloadSpeed);
m_uploadBuffer->push(m_uploadSpeed);
}
m_prevRx = totalRx;
m_prevTx = totalTx;
emit changed();
}
} // namespace ZShell::services
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <qelapsedtimer.h>
#include <qqmlintegration.h>
#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
+248 -70
View File
@@ -2,8 +2,13 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cerrno>
#include <cstdio>
#include <dirent.h>
#include <fcntl.h>
#include <pwd.h> #include <pwd.h>
#include <signal.h> #include <signal.h>
#include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include <qbytearray.h> #include <qbytearray.h>
@@ -12,6 +17,7 @@
#include <qfile.h> #include <qfile.h>
#include <qfuturewatcher.h> #include <qfuturewatcher.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qthreadpool.h>
#include <qregularexpression.h> #include <qregularexpression.h>
#include <qset.h> #include <qset.h>
#include <QtConcurrent/qtconcurrentrun.h> #include <QtConcurrent/qtconcurrentrun.h>
@@ -53,6 +59,82 @@ template <typename T> int cmpVals(const T& a, const T& b) {
return (a < b) - (a > 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<qsizetype>(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<int>(pid), what);
}
QVector<pid_t> listProcPids() {
QVector<pid_t> pids;
const int fd = ::openat(AT_FDCWD, "/proc", O_RDONLY | O_DIRECTORY);
if (fd < 0) {
return pids;
}
std::array<char, 256 * 1024> buf;
for (;;) {
const ssize_t n = ::getdents64(
fd, reinterpret_cast<ProcDirEnt64*>(buf.data()), buf.size());
if (n <= 0) {
break;
}
for (ssize_t off = 0; off < n;) {
const auto* d =
reinterpret_cast<const ProcDirEnt64*>(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<pid_t>(v));
}
}
off += static_cast<ssize_t>(d->d_reclen);
}
}
::close(fd);
return pids;
}
struct FuzzyText { struct FuzzyText {
QString chars; QString chars;
@@ -460,19 +542,24 @@ void ProcessModel::setDesktopEntryIcons(
} }
QString ProcessModel::resolveProcessExe(pid_t pid) { QString ProcessModel::resolveProcessExe(pid_t pid) {
const QString path = QStringLiteral("/proc/%1/exe").arg(pid); char path[40];
const QString target = QFile::symLinkTarget(path); procPath(path, sizeof(path), pid, "exe");
char buf[4096];
return target; const ssize_t n = ::readlink(path, buf, sizeof(buf) - 1);
if (n < 0) {
return {};
}
buf[n] = '\0';
return QString::fromLocal8Bit(buf, static_cast<int>(n));
} }
std::optional<ProcessModel::RawStat> ProcessModel::readStat(pid_t pid) { std::optional<ProcessModel::RawStat> ProcessModel::readStat(pid_t pid) {
QFile f(QStringLiteral("/proc/%1/stat").arg(pid)); char path[40];
if (!f.open(QIODevice::ReadOnly)) { procPath(path, sizeof(path), pid, "stat");
const QByteArray data = readProcFile(path);
if (data.isEmpty()) {
return std::nullopt; return std::nullopt;
} }
const QByteArray data = f.readAll();
f.close();
const qsizetype openParen = data.indexOf('('); const qsizetype openParen = data.indexOf('(');
const qsizetype closeParen = data.lastIndexOf(')'); const qsizetype closeParen = data.lastIndexOf(')');
@@ -500,12 +587,9 @@ std::optional<ProcessModel::RawStat> ProcessModel::readStat(pid_t pid) {
} }
QString ProcessModel::readCommandLine(pid_t pid, const QString& fallbackComm) { QString ProcessModel::readCommandLine(pid_t pid, const QString& fallbackComm) {
QFile f(QStringLiteral("/proc/%1/cmdline").arg(pid)); char path[40];
if (!f.open(QIODevice::ReadOnly)) { procPath(path, sizeof(path), pid, "cmdline");
return QStringLiteral("[%1]").arg(fallbackComm); QByteArray data = readProcFile(path);
}
QByteArray data = f.readAll();
f.close();
if (data.isEmpty()) { if (data.isEmpty()) {
return QStringLiteral("[%1]").arg(fallbackComm); 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) { ProcessModel::StatusInfo ProcessModel::readStatus(pid_t pid) {
QFile f(QStringLiteral("/proc/%1/status").arg(pid)); char path[40];
if (!f.open(QIODevice::ReadOnly)) { procPath(path, sizeof(path), pid, "status");
return {}; const QByteArray data = readProcFile(path);
}
const QByteArray data = f.readAll();
f.close();
StatusInfo out; StatusInfo out;
qsizetype pos = 0; qsizetype pos = 0;
@@ -554,12 +635,9 @@ ProcessModel::StatusInfo ProcessModel::readStatus(pid_t pid) {
} }
ProcessModel::IoInfo ProcessModel::readIo(pid_t pid) { ProcessModel::IoInfo ProcessModel::readIo(pid_t pid) {
QFile f(QStringLiteral("/proc/%1/io").arg(pid)); char path[40];
if (!f.open(QIODevice::ReadOnly)) { procPath(path, sizeof(path), pid, "io");
return {}; const QByteArray data = readProcFile(path);
}
const QByteArray data = f.readAll();
f.close();
IoInfo out; IoInfo out;
qsizetype pos = 0; qsizetype pos = 0;
@@ -606,12 +684,10 @@ QString ProcessModel::resolveUsername(uid_t uid, QHash<uid_t, QString>& cache) {
} }
std::optional<double> ProcessModel::readUptimeSeconds() { std::optional<double> ProcessModel::readUptimeSeconds() {
QFile f(QStringLiteral("/proc/uptime")); const QByteArray data = readProcFile("/proc/uptime");
if (!f.open(QIODevice::ReadOnly)) { if (data.isEmpty()) {
return std::nullopt; return std::nullopt;
} }
const QByteArray data = f.readAll();
f.close();
const auto fields = splitWhitespace(QByteArrayView(data)); const auto fields = splitWhitespace(QByteArrayView(data));
if (fields.isEmpty()) { if (fields.isEmpty()) {
@@ -623,35 +699,17 @@ std::optional<double> ProcessModel::readUptimeSeconds() {
return ok ? std::make_optional(uptime) : std::nullopt; return ok ? std::make_optional(uptime) : std::nullopt;
} }
ProcessModel::ScanResult ProcessModel::scanProcesses( void ProcessModel::scanChunk(
double uptime, const ScanContext& ctx,
double deltaSecs, const QVector<pid_t>& pids,
QHash<pid_t, int> prevPidToAll, QVector<ProcessEntry>& out,
QVector<ProcessEntry> prevAll, QHash<uid_t, QString>& uidCache) {
QHash<uid_t, QString> uidCache, const QHash<pid_t, int>& prevPidToAll = *ctx.prevPidToAll;
long clockTicksPerSec, const QVector<ProcessEntry>& prevAll = *ctx.prevAll;
QHash<QString, QString> execPathToIcon, const QHash<QString, QString>& execPathToIcon = *ctx.execPathToIcon;
QHash<QString, QString> execNameToIcon, const QHash<QString, QString>& execNameToIcon = *ctx.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<ProcessEntry> fresh;
fresh.reserve(entries.size());
for (const QString& name : entries) {
bool isPid = false;
const pid_t pid = static_cast<pid_t>(name.toLong(&isPid));
if (!isPid || pid <= 0) {
continue;
}
for (pid_t pid : pids) {
const auto statOpt = readStat(pid); const auto statOpt = readStat(pid);
if (!statOpt) { if (!statOpt) {
continue; continue;
@@ -675,7 +733,7 @@ ProcessModel::ScanResult ProcessModel::scanProcesses(
entry.commandLine = readCommandLine(pid, stat.comm); entry.commandLine = readCommandLine(pid, stat.comm);
} }
if (!samePidInstance || desktopEntryIconsChanged) { if (!samePidInstance || ctx.iconsChanged) {
const QString exePath = resolveProcessExe(pid); const QString exePath = resolveProcessExe(pid);
QString iconName; QString iconName;
@@ -711,18 +769,23 @@ ProcessModel::ScanResult ProcessModel::scanProcesses(
entry.uid = status.uid; entry.uid = status.uid;
entry.username = resolveUsername(status.uid, uidCache); 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.ioReadBytes = io.readBytes;
entry.ioWriteBytes = io.writeBytes; entry.ioWriteBytes = io.writeBytes;
entry.elapsedSecs = std::max( entry.elapsedSecs = std::max(
0.0, 0.0,
uptime - (static_cast<double>(stat.starttime) / ctx.uptime - (static_cast<double>(stat.starttime) /
static_cast<double>(clockTicksPerSec))); static_cast<double>(ctx.clockTicksPerSec)));
entry.lastUtime = stat.utime; entry.lastUtime = stat.utime;
entry.lastStime = stat.stime; entry.lastStime = stat.stime;
if (samePidInstance && deltaSecs > 0.0) { if (samePidInstance && ctx.deltaSecs > 0.0) {
const ProcessEntry& prev = prevAll.at(prevIt.value()); const ProcessEntry& prev = prevAll.at(prevIt.value());
const quint64 prevTicks = prev.lastUtime + prev.lastStime; const quint64 prevTicks = prev.lastUtime + prev.lastStime;
const quint64 nowTicks = stat.utime + stat.stime; const quint64 nowTicks = stat.utime + stat.stime;
@@ -730,13 +793,97 @@ ProcessModel::ScanResult ProcessModel::scanProcesses(
nowTicks > prevTicks ? nowTicks - prevTicks : 0; nowTicks > prevTicks ? nowTicks - prevTicks : 0;
entry.cpuPercent = entry.cpuPercent =
(static_cast<double>(deltaTicks) / (static_cast<double>(deltaTicks) /
(deltaSecs * static_cast<double>(clockTicksPerSec))) * (ctx.deltaSecs *
static_cast<double>(ctx.clockTicksPerSec))) *
100.0; 100.0;
} else { } else {
entry.cpuPercent = 0.0; 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<pid_t, int> prevPidToAll,
QVector<ProcessEntry> prevAll,
QHash<uid_t, QString> uidCache,
long clockTicksPerSec,
QHash<QString, QString> execPathToIcon,
QHash<QString, QString> execNameToIcon,
QStringList pathDirs,
bool desktopEntryIconsChanged,
quint64 iconVersion) {
Q_UNUSED(
pathDirs); // kept for future cgroup fallback; sanitizeExec already ran on GUI thread
const QVector<pid_t> 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<ProcessEntry> 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<QVector<pid_t>> chunks(nThreads);
QVector<QVector<ProcessEntry>> outs(nThreads);
QVector<QHash<uid_t, QString>> uidParts(nThreads);
QVector<QFuture<void>> futures;
for (int t = 0; t < nThreads; ++t) {
const qsizetype a = static_cast<qsizetype>(t) * per;
const qsizetype b = qMin<qsizetype>(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<void>& 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; ScanResult result;
@@ -810,6 +957,13 @@ void ProcessModel::onScanFinished() {
for (int i = 0; i < mAll.size(); ++i) { for (int i = 0; i < mAll.size(); ++i) {
mPidToAll.insert(mAll.at(i).pid, 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(); for (auto it = result.newUidNames.constBegin();
it != result.newUidNames.constEnd(); it != result.newUidNames.constEnd();
++it) { ++it) {
@@ -846,11 +1000,35 @@ void ProcessModel::applyView() {
ProcessEntry entry = src; ProcessEntry entry = src;
if (searching) { if (searching) {
int score = fuzzyQueryScore(fuzzyPrepare(src.command), terms); // Normalizing (NFD) + lowering per process per tick is the
if (!src.commandLine.isEmpty() && src.commandLine != src.command) { // expensive part of search; commands are stable for the life of
score = qMax( // a process instance (pid + starttime), so cache the prepared
score, // text. comm can still change without an exec (prctl), so
fuzzyQueryScore(fuzzyPrepare(src.commandLine), terms)); // 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) { if (score < 0) {
continue; continue;
+29
View File
@@ -75,6 +75,16 @@ class ProcessModel : public QAbstractListModel {
void sortAscendingChanged(); void sortAscendingChanged();
private: private:
struct FuzzyCacheEntry {
quint64 starttime = 0;
QString command;
QString commandChars;
QVector<quint8> commandClasses;
QString commandLineChars;
QVector<quint8> commandLineClasses;
bool hasCommandLine = false;
};
struct RawStat { struct RawStat {
pid_t pid = 0; pid_t pid = 0;
pid_t ppid = 0; pid_t ppid = 0;
@@ -116,6 +126,18 @@ class ProcessModel : public QAbstractListModel {
quint64 writeBytes = 0; quint64 writeBytes = 0;
}; };
struct ScanContext {
double uptime = 0.0;
double deltaSecs = -1.0;
const QHash<pid_t, int>* prevPidToAll = nullptr;
const QVector<ProcessEntry>* prevAll = nullptr;
long clockTicksPerSec = 100;
const QHash<QString, QString>* execPathToIcon = nullptr;
const QHash<QString, QString>* execNameToIcon = nullptr;
bool iconsChanged = false;
uid_t selfEuid = 0;
};
struct ScanResult { struct ScanResult {
QVector<ProcessEntry> entries; QVector<ProcessEntry> entries;
QHash<uid_t, QString> newUidNames; QHash<uid_t, QString> newUidNames;
@@ -130,6 +152,12 @@ class ProcessModel : public QAbstractListModel {
static QString resolveUsername(uid_t uid, QHash<uid_t, QString>& cache); static QString resolveUsername(uid_t uid, QHash<uid_t, QString>& cache);
static QString resolveProcessExe(pid_t pid); static QString resolveProcessExe(pid_t pid);
static void scanChunk(
const ScanContext& ctx,
const QVector<pid_t>& pids,
QVector<ProcessEntry>& out,
QHash<uid_t, QString>& uidCache);
static ScanResult scanProcesses( static ScanResult scanProcesses(
double uptime, double uptime,
double deltaSecs, double deltaSecs,
@@ -153,6 +181,7 @@ class ProcessModel : public QAbstractListModel {
QVector<ProcessEntry> mAll; QVector<ProcessEntry> mAll;
QHash<pid_t, int> mPidToAll; QHash<pid_t, int> mPidToAll;
QHash<pid_t, FuzzyCacheEntry> mFuzzyCache;
QVector<ProcessEntry> mProcesses; QVector<ProcessEntry> mProcesses;
QHash<uid_t, QString> mUidToName; QHash<uid_t, QString> mUidToName;
+10 -54
View File
@@ -1,11 +1,7 @@
#include "tickingservice.hpp" #include "tickingservice.hpp"
#include <QFile>
#include <QDir> #include "config/dashboard.hpp"
#include <QJsonDocument> #include "config/rootnodes.hpp"
#include <QJsonObject>
#include <QJsonValue>
#include <QFileSystemWatcher>
#include <QTimer>
namespace ZShell::services { namespace ZShell::services {
@@ -17,53 +13,13 @@ TickingService::TickingService(QObject* parent)
updateTriggered(); updateTriggered();
}); });
QString configPath = auto* dash = ZShell::config::ConfigSingleton::instance()->dashboard();
QDir::homePath() + QStringLiteral("/.config/zshell/config.json"); applyInterval(dash->resourceUpdateInterval());
QObject::connect(
auto reloadConfig = [this, configPath]() { dash,
QFile file(configPath); &ZShell::config::Dashboard::resourceUpdateIntervalChanged,
if (!file.open(QIODevice::ReadOnly)) { this,
return; [this, dash] { applyInterval(dash->resourceUpdateInterval()); });
}
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);
}
} }
int TickingService::updateInterval() const { int TickingService::updateInterval() const {
+2 -1
View File
@@ -1,8 +1,9 @@
#pragma once #pragma once
#include "service.hpp"
#include <qtimer.h> #include <qtimer.h>
#include "service.hpp"
namespace ZShell::services { namespace ZShell::services {
class TickingService : public Service { class TickingService : public Service {
+16
View File
@@ -48,4 +48,20 @@ FormatResult UsageFmt::formatKib(qreal kib, qreal total) const {
return {kib, total, "KiB"}; return {kib, total, "KiB"};
} }
FormatResult UsageFmt::formatBytes(qreal bytes) const {
static constexpr std::array<const char*, 5> 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 } // namespace ZShell::services::usagefmt
+1
View File
@@ -31,6 +31,7 @@ class UsageFmt : public QObject {
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib) const; Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib) const;
Q_INVOKABLE [[nodiscard]] FormatResult formatKib( Q_INVOKABLE [[nodiscard]] FormatResult formatKib(
qreal kib, qreal total) const; qreal kib, qreal total) const;
Q_INVOKABLE [[nodiscard]] FormatResult formatBytes(qreal bytes) const;
}; };
} // namespace ZShell::services::usagefmt } // namespace ZShell::services::usagefmt
+163 -148
View File
@@ -4,190 +4,205 @@
#include <qobject.h> #include <qobject.h>
#include <qvariant.h> #include <qvariant.h>
#include "listnode.hpp" // IWYU pragma: keep #include "listnode.hpp"
#include "schema.hpp" // IWYU pragma: keep #include "schema.hpp"
namespace ZShell::settings { namespace ZShell::settings {
inline QVariantMap vmap(std::initializer_list<std::pair<QString, QVariant>> entries) { inline QVariantMap vmap(
QVariantMap map; std::initializer_list<std::pair<QString, QVariant>> entries) {
for (const auto& [key, value] : entries) QVariantMap map;
map.insert(key, value); for (const auto& [key, value] : entries)
return map; map.insert(key, value);
return map;
} }
template <typename... Ts> inline QList<QMetaType> unionTypes() { template <typename... Ts> inline QList<QMetaType> unionTypes() {
static_assert(sizeof...(Ts) >= 2, "A union needs at least two types"); 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(sizeof...(Ts) <= 4, "A union cannot have more than 4 types"); static_assert(
static_assert((!std::is_same_v<Ts, QVariant> && ...), "A union cannot contain QVariant"); (!std::is_same_v<Ts, QVariant> && ...),
"A union cannot contain QVariant");
return { QMetaType::fromType<Ts>()... }; return {QMetaType::fromType<Ts>()...};
} }
namespace detail { namespace detail {
template <typename T> inline bool compare(const T& a, const T& b) { template <typename T> inline bool compare(const T& a, const T& b) {
return a == b; return a == b;
} }
template <std::floating_point T> bool compare(const T& a, const T& b) { template <std::floating_point T> 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 <std::floating_point T> bool compare(const QList<T>& a, const QList<T>& b) { template <std::floating_point T>
if (a.size() != b.size()) bool compare(const QList<T>& a, const QList<T>& b) {
return false; if (a.size() != b.size()) return false;
for (qsizetype i = 0; i < a.size(); ++i) for (qsizetype i = 0; i < a.size(); ++i)
if (!compare(a.at(i), b.at(i))) if (!compare(a.at(i), b.at(i))) return false;
return false; return true;
return true;
} }
} // namespace detail } // namespace detail
} // namespace ZShell::settings } // namespace ZShell::settings
// Helper macro to prevent splitting initialiser lists
#define DEFAULT_ARG(...) __VA_ARGS__ #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) \
#define SETTINGS_NODE_NO_CTOR(Class, Base) \ Q_OBJECT \
Q_OBJECT \ \
\ public: \
public: \ [[nodiscard]] const ZShell::settings::Schema& schema() const override { \
[[nodiscard]] const ZShell::settings::Schema& schema() const override { \ static const auto schema = ZShell::settings::Schema::build( \
static const auto schema = \ &staticMetaObject, Base::staticMetaObject.propertyCount()); \
ZShell::settings::Schema::build(&staticMetaObject, Base::staticMetaObject.propertyCount()); \ return schema; \
return schema; \ } \
} \ \
\ private: \
private: \ using Self = Class; // For use in the below macros
using Self = Class; // For use in the below macros
#define SETTINGS_NODE(Class, Base) \ #define SETTINGS_NODE(Class, Base) \
SETTINGS_NODE_NO_CTOR(Class, Base) \ SETTINGS_NODE_NO_CTOR(Class, Base) \
QML_ANONYMOUS \ QML_ANONYMOUS \
\ \
public: \ public: \
explicit Class(Class* fallback = nullptr, QObject* parent = nullptr, bool globalOnly = false) \ explicit Class( \
: Base(fallback, parent, globalOnly) {} \ Class* fallback = nullptr, \
\ QObject* parent = nullptr, \
private: bool globalOnly = false) \
: Base(fallback, parent, globalOnly) {} \
\
private:
// Defines a property on a node. #define SETTINGS_PROPERTY_IMPL(Type, name, global, defaultVal, ...) \
#define SETTINGS_PROPERTY_IMPL(Type, name, global, defaultVal, ...) \ Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \
Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ \
\ public: \
public: \ [[nodiscard]] Type name() const { \
[[nodiscard]] Type name() const { \ if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \
if (global || m_globalOnly) \ return m_##name; \
warnGlobalRead(QStringLiteral(#name)); \ } \
return m_##name; \ \
} \ void set_##name(const Type& value) { \
\ if (!true /* TODO: validation */) return; \
void set_##name(const Type& value) { \ \
if (!true /* TODO: validation */) \ if (rejectInvalidWrite(QStringLiteral(#name), value)) \
return; \ return; /* Skip writes of the wrong type */ \
\ \
if (rejectInvalidWrite(QStringLiteral(#name), value)) \ if (rejectGlobalWrite(QStringLiteral(#name))) \
return; /* Skip writes of the wrong type */ \ return; /* Skip writes to global only keys */ \
\ \
if (rejectGlobalWrite(QStringLiteral(#name))) \ const auto needsNotify = \
return; /* Skip writes to global only keys, they should be sent to the global layer */ \ !ZShell::settings::detail::compare(value, m_##name); \
\ m_##name = value; \
const auto needsNotify = !ZShell::settings::detail::compare(value, m_##name); \ if (recordWrite(QStringLiteral(#name), needsNotify)) \
m_##name = value; \ Q_EMIT name##Changed(); \
if (recordWrite(QStringLiteral(#name), needsNotify)) \ } \
Q_EMIT name##Changed(); \ \
} \ Q_SIGNAL void name##Changed(); \
\ \
Q_SIGNAL void name##Changed(); \ private: \
\ Type m_##name = fallbackValue(&Self::m_##name, defaultVal); \
private: \ inline static const bool s_register_##name = \
Type m_##name = fallbackValue(&Self::m_##name, defaultVal); \ (ZShell::settings::Schema::annotate( \
inline static const bool s_register_##name = \ &staticMetaObject, \
(ZShell::settings::Schema::annotate(&staticMetaObject, QStringLiteral(#name), \ QStringLiteral(#name), \
{ .defaultValue = QVariant::fromValue<Type>(defaultVal), .globalOnly = global, __VA_ARGS__ }), \ {.defaultValue = QVariant::fromValue<Type>(defaultVal), \
true); .globalOnly = global, \
__VA_ARGS__}), \
true);
#define SETTINGS_PROPERTY(Type, name, defaultVal, ...) \ #define SETTINGS_PROPERTY(Type, name, defaultVal, ...) \
SETTINGS_PROPERTY_IMPL(Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__) SETTINGS_PROPERTY_IMPL( \
Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__)
// Defines a global property on a node. Shorthand for .globalOnly = true. // Defines a global property on a node. Shorthand for .globalOnly = true.
#define SETTINGS_GLOBAL_PROPERTY(Type, name, defaultVal, ...) \ #define SETTINGS_GLOBAL_PROPERTY(Type, name, defaultVal, ...) \
SETTINGS_PROPERTY_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__) SETTINGS_PROPERTY_IMPL( \
Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)
// Defines a subobject property on a node. Subobject properties are CONSTANT. // Defines a subobject property on a node. Subobject properties are CONSTANT.
#define SETTINGS_SUBOBJECT_IMPL(Type, name, global) \ #define SETTINGS_SUBOBJECT_IMPL(Type, name, global) \
Q_PROPERTY(Type* name READ name CONSTANT) \ Q_PROPERTY(Type* name READ name CONSTANT) \
\ \
public: \ public: \
[[nodiscard]] Type* name() const { \ [[nodiscard]] Type* name() const { \
if (global || m_globalOnly) \ if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \
warnGlobalRead(QStringLiteral(#name)); \ return m_##name; \
return m_##name; \ } \
} \ \
\ private: \
private: \ Type* m_##name = \
Type* m_##name = new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \ new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \
inline static const bool s_register_##name = \ inline static const bool s_register_##name = \
(ZShell::settings::Schema::annotate( \ (ZShell::settings::Schema::annotate( \
&staticMetaObject, QStringLiteral(#name), { .defaultValue = {}, .globalOnly = global }), \ &staticMetaObject, \
true); 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) \
#define SETTINGS_GLOBAL_SUBOBJECT(Type, name) SETTINGS_SUBOBJECT_IMPL(Type, name, true) SETTINGS_SUBOBJECT_IMPL(Type, name, true)
// Defines a list type for use with SETTINGS_LIST. #define SETTINGS_LIST_TYPE(Element, Name) \
#define SETTINGS_LIST_TYPE(Element, Name) \ class Name : public ZShell::settings::ListNode { \
class Name : public ZShell::settings::ListNode { \ Q_OBJECT \
Q_OBJECT \ QML_ANONYMOUS \
QML_ANONYMOUS \ \
\ public: \
public: \ explicit Name( \
explicit Name(Name* fallback = nullptr, QObject* parent = nullptr, bool globalOnly = false) \ Name* fallback = nullptr, \
: ZShell::settings::ListNode(fallback, parent, globalOnly) {} \ QObject* parent = nullptr, \
\ bool globalOnly = false) \
[[nodiscard]] Q_INVOKABLE Element* at(qsizetype index) const { /* Format ugh */ \ : ZShell::settings::ListNode(fallback, parent, globalOnly) {} \
return static_cast<Element*>(elementAt(index)); \ \
} \ [[nodiscard]] Q_INVOKABLE Element* at( \
[[nodiscard]] Q_INVOKABLE Element* insert(const QVariantMap& props, qsizetype index = -1) { \ qsizetype index) const { /* Format ugh */ \
return static_cast<Element*>(insertElement(props, index)); \ return static_cast<Element*>(elementAt(index)); \
} \ } \
\ [[nodiscard]] Q_INVOKABLE Element* insert( \
protected: \ const QVariantMap& props, qsizetype index = -1) { \
[[nodiscard]] ZShell::settings::Node* createElement(ZShell::settings::Node* fallback) override { \ return static_cast<Element*>(insertElement(props, index)); \
return new Element(static_cast<Element*>(fallback), this); \ } \
} \ \
}; protected: \
[[nodiscard]] ZShell::settings::Node* createElement( \
ZShell::settings::Node* fallback) override { \
return new Element(static_cast<Element*>(fallback), this); \
} \
};
// Defines a list property on a node. List properties are CONSTANT. #define SETTINGS_LIST_IMPL(Type, name, global, defaultVal, ...) \
#define SETTINGS_LIST_IMPL(Type, name, global, defaultVal, ...) \ Q_PROPERTY(Type* name READ name CONSTANT) \
Q_PROPERTY(Type* name READ name CONSTANT) \ \
\ public: \
public: \ [[nodiscard]] Type* name() const { \
[[nodiscard]] Type* name() const { \ if (global || m_globalOnly) warnGlobalRead(QStringLiteral(#name)); \
if (global || m_globalOnly) \ return m_##name; \
warnGlobalRead(QStringLiteral(#name)); \ } \
return m_##name; \ \
} \ private: \
\ Type* m_##name = \
private: \ new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \
Type* m_##name = new Type(fallbackValue(&Self::m_##name, nullptr), this, global); \ inline static const bool s_register_##name = \
inline static const bool s_register_##name = \ (ZShell::settings::Schema::annotate( \
(ZShell::settings::Schema::annotate(&staticMetaObject, QStringLiteral(#name), \ &staticMetaObject, \
{ .defaultValue = QVariant::fromValue<QList<QVariantMap>>(defaultVal), \ QStringLiteral(#name), \
.globalOnly = global, \ {.defaultValue = \
__VA_ARGS__ }), \ QVariant::fromValue<QList<QVariantMap>>(defaultVal), \
true); .globalOnly = global, \
__VA_ARGS__}), \
true);
#define SETTINGS_LIST(Type, name, defaultVal, ...) \ #define SETTINGS_LIST(Type, name, defaultVal, ...) \
SETTINGS_LIST_IMPL(Type, name, false, DEFAULT_ARG(defaultVal), __VA_ARGS__) 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, ...) \
#define SETTINGS_GLOBAL_LIST(Type, name, defaultVal, ...) \ SETTINGS_LIST_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)
SETTINGS_LIST_IMPL(Type, name, true, DEFAULT_ARG(defaultVal), __VA_ARGS__)