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
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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__)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user