dashboard/performance: new design, configurable, controlcenter support (#975)
* feat(dashboard): add configurable performance resources - Add config options to show/hide Battery, GPU, CPU, Memory, Storage - Make dashboard responsive based on number of visible resources - Scale resource sizes and spacing dynamically for 3, 4, or 5 items - Battery shows charge status and time remaining/to full - Each resource can be individually toggled via config * fix(dashboard): add dynamic right margin for last visible resource Ensures the rightmost resource always has proper margin to prevent content from being cut off at the edge * fix(performance): comment out duplicated value2 properties for memory and storage resources * controlcenter: add settings for dashboard * feat: handle readonly properties and re-usable codes * Feature/performance tab rework (#5) * dashboard/performance: rework tab with card-based grid layout - Replace circular arc meters with card-based grid layout - CPU/GPU cards show hardware name, usage and temperature with horizontal bars - Memory card with 3/4 arc indicator and used/total at bottom - Storage card shows physical disks from lsblk with aggregated partition usage - Add cpuName, gpuName, cpuFreq, cpuMaxFreq, disks properties to SystemUsage - Clean hardware names (remove Intel/AMD/NVIDIA prefixes, TM/R symbols) * dashboard/performance: new hero card design * dashboard/performance: update storage indicators to be reponsive to the physical disks count * dashboard/performance: fix the overlay bounding issue * dashboard/perfromance: refactor code * dashboard/performance: add battery gauge * dashboard/performance: correct battery icon * dashboard/performance: configurable battery * dashboard/performance: update layout * dashboard/performance: move the "Usage" text on top and smaller the font size * dashboard/performance: add a lot of configurations * dashboard/performance: add network metrics * fix: issue with hot reload * chore: update default vaule for mainValueSpacing to 0 * chore: group settings into collapasible sections * chore: making GPU & Battery toggle not showing if not found * chore: fix network widget spacing & text * chore: remove old disk bars configs, add update interval * chore: remove old & unused value, functions * chore: network graph update smoothly when data points change * chore: refactor settings - de-flood settings, most of the font & size setting now follow the global Appearance config - Most of sliders are not needed anymore, only keep the update interval slider - clean up * chore: remove readonly properties from the controlcenter/dashboard. * chore: minor fix * fix: fix warning about onPercChange() * fix: network metrics negative number * fix: add minimal height & width, placeholder for none toggled * fix: network graph move smoothly (#6) * fix: network graph move smoothly * clean up * fix: graph animation even more smooth * fix: padding issue * chore: network icons short description * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
This commit is contained in:
parent
69adb92087
commit
46174d1934
16 changed files with 1683 additions and 210 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@
|
|||
/.qmlls.ini
|
||||
build/
|
||||
.cache/
|
||||
logs
|
||||
|
|
@ -258,8 +258,16 @@ Singleton {
|
|||
return {
|
||||
enabled: dashboard.enabled,
|
||||
showOnHover: dashboard.showOnHover,
|
||||
mediaUpdateInterval: dashboard.mediaUpdateInterval,
|
||||
updateInterval: dashboard.updateInterval,
|
||||
dragThreshold: dashboard.dragThreshold,
|
||||
performance: {
|
||||
showBattery: dashboard.performance.showBattery,
|
||||
showGpu: dashboard.performance.showGpu,
|
||||
showCpu: dashboard.performance.showCpu,
|
||||
showMemory: dashboard.performance.showMemory,
|
||||
showStorage: dashboard.performance.showStorage,
|
||||
showNetwork: dashboard.performance.showNetwork
|
||||
},
|
||||
sizes: {
|
||||
tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight,
|
||||
tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,19 @@ JsonObject {
|
|||
property bool enabled: true
|
||||
property bool showOnHover: true
|
||||
property int mediaUpdateInterval: 500
|
||||
property int resourceUpdateInterval: 1000
|
||||
property int dragThreshold: 50
|
||||
property Sizes sizes: Sizes {}
|
||||
property Performance performance: Performance {}
|
||||
|
||||
component Performance: JsonObject {
|
||||
property bool showBattery: true
|
||||
property bool showGpu: true
|
||||
property bool showCpu: true
|
||||
property bool showMemory: true
|
||||
property bool showStorage: true
|
||||
property bool showNetwork: true
|
||||
}
|
||||
|
||||
component Sizes: JsonObject {
|
||||
readonly property int tabIndicatorHeight: 3
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ QtObject {
|
|||
readonly property string label: "launcher"
|
||||
readonly property string icon: "apps"
|
||||
readonly property string component: "launcher/LauncherPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "dashboard"
|
||||
readonly property string label: "dashboard"
|
||||
readonly property string icon: "dashboard"
|
||||
readonly property string component: "dashboard/DashboardPane.qml"
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import "audio"
|
|||
import "appearance"
|
||||
import "taskbar"
|
||||
import "launcher"
|
||||
import "dashboard"
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
|
|
|
|||
67
modules/controlcenter/components/ReadonlySlider.qml
Normal file
67
modules/controlcenter/components/ReadonlySlider.qml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
property real from: 0
|
||||
property real to: 100
|
||||
property string suffix: ""
|
||||
property bool readonly: false
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
visible: root.readonly
|
||||
text: "lock"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1)
|
||||
opacity: root.readonly ? 0.5 : 1.0
|
||||
|
||||
StyledRect {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * ((root.value - root.from) / (root.to - root.from))
|
||||
radius: parent.radius
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
}
|
||||
123
modules/controlcenter/dashboard/DashboardPane.qml
Normal file
123
modules/controlcenter/dashboard/DashboardPane.qml
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
// General Settings
|
||||
property bool enabled: Config.dashboard.enabled ?? true
|
||||
property bool showOnHover: Config.dashboard.showOnHover ?? true
|
||||
property int updateInterval: Config.dashboard.updateInterval ?? 1000
|
||||
property int dragThreshold: Config.dashboard.dragThreshold ?? 50
|
||||
|
||||
// Performance Resources
|
||||
property bool showBattery: Config.dashboard.performance.showBattery ?? false
|
||||
property bool showGpu: Config.dashboard.performance.showGpu ?? true
|
||||
property bool showCpu: Config.dashboard.performance.showCpu ?? true
|
||||
property bool showMemory: Config.dashboard.performance.showMemory ?? true
|
||||
property bool showStorage: Config.dashboard.performance.showStorage ?? true
|
||||
property bool showNetwork: Config.dashboard.performance.showNetwork ?? true
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function saveConfig() {
|
||||
Config.dashboard.enabled = root.enabled;
|
||||
Config.dashboard.showOnHover = root.showOnHover;
|
||||
Config.dashboard.updateInterval = root.updateInterval;
|
||||
Config.dashboard.dragThreshold = root.dragThreshold;
|
||||
Config.dashboard.performance.showBattery = root.showBattery;
|
||||
Config.dashboard.performance.showGpu = root.showGpu;
|
||||
Config.dashboard.performance.showCpu = root.showCpu;
|
||||
Config.dashboard.performance.showMemory = root.showMemory;
|
||||
Config.dashboard.performance.showStorage = root.showStorage;
|
||||
Config.dashboard.performance.showNetwork = root.showNetwork;
|
||||
// Note: sizes properties are readonly and cannot be modified
|
||||
Config.save();
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: dashboardClippingRect
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
radius: dashboardBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: dashboardLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
|
||||
sourceComponent: dashboardContentComponent
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: dashboardBorder
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dashboardContentComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: dashboardFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: dashboardLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: dashboardFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: dashboardLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Dashboard")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
// General Settings Section
|
||||
GeneralSection {
|
||||
rootItem: root
|
||||
}
|
||||
|
||||
// Performance Resources Section
|
||||
PerformanceSection {
|
||||
rootItem: root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
modules/controlcenter/dashboard/GeneralSection.qml
Normal file
81
modules/controlcenter/dashboard/GeneralSection.qml
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
SectionContainer {
|
||||
id: root
|
||||
|
||||
required property var rootItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("General Settings")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: root.rootItem.enabled
|
||||
onToggled: checked => {
|
||||
root.rootItem.enabled = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: root.rootItem.showOnHover
|
||||
onToggled: checked => {
|
||||
root.rootItem.showOnHover = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Update interval")
|
||||
value: root.rootItem.updateInterval
|
||||
from: 100
|
||||
to: 10000
|
||||
stepSize: 100
|
||||
suffix: "ms"
|
||||
validator: IntValidator { bottom: 100; top: 10000 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
root.rootItem.updateInterval = Math.round(newValue);
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Drag threshold")
|
||||
value: root.rootItem.dragThreshold
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "px"
|
||||
validator: IntValidator { bottom: 0; top: 100 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
root.rootItem.dragThreshold = Math.round(newValue);
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
modules/controlcenter/dashboard/PerformanceSection.qml
Normal file
85
modules/controlcenter/dashboard/PerformanceSection.qml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import ".."
|
||||
import "../components"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.UPower
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SectionContainer {
|
||||
id: root
|
||||
|
||||
required property var rootItem
|
||||
// GPU toggle is hidden when gpuType is "NONE" (no GPU data available)
|
||||
readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE"
|
||||
// Battery toggle is hidden when no laptop battery is present
|
||||
readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery
|
||||
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Performance Resources")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root.rootItem
|
||||
options: {
|
||||
let opts = [];
|
||||
if (root.batteryAvailable)
|
||||
opts.push({
|
||||
"label": qsTr("Battery"),
|
||||
"propertyName": "showBattery",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showBattery = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
if (root.gpuAvailable)
|
||||
opts.push({
|
||||
"label": qsTr("GPU"),
|
||||
"propertyName": "showGpu",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showGpu = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
opts.push({
|
||||
"label": qsTr("CPU"),
|
||||
"propertyName": "showCpu",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showCpu = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Memory"),
|
||||
"propertyName": "showMemory",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showMemory = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Storage"),
|
||||
"propertyName": "showStorage",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showStorage = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Network"),
|
||||
"propertyName": "showNetwork",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showNetwork = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.utils
|
||||
import qs.config
|
||||
import Caelestia.Services
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
|
@ -142,7 +140,7 @@ Item {
|
|||
|
||||
anchors.fill: parent
|
||||
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: width
|
||||
|
|
@ -324,7 +322,7 @@ Item {
|
|||
|
||||
disabled: !Players.list.length
|
||||
active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null
|
||||
menu.onItemSelected: item => Players.manualActive = item.modelData
|
||||
menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData
|
||||
|
||||
menuItems: playerList.instances
|
||||
fallbackIcon: "music_off"
|
||||
|
|
@ -341,13 +339,7 @@ Item {
|
|||
|
||||
model: Players.list
|
||||
|
||||
MenuItem {
|
||||
required property MprisPlayer modelData
|
||||
|
||||
icon: modelData === Players.active ? "check" : ""
|
||||
text: Players.getIdentity(modelData)
|
||||
activeIcon: "animated_images"
|
||||
}
|
||||
PlayerItem {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -380,13 +372,21 @@ Item {
|
|||
height: visualiser.height * 0.75
|
||||
|
||||
playing: Players.active?.isPlaying ?? false
|
||||
speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment
|
||||
speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type
|
||||
source: Paths.absolutePath(Config.paths.mediaGif)
|
||||
asynchronous: true
|
||||
fillMode: AnimatedImage.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
|
||||
component PlayerItem: MenuItem {
|
||||
required property MprisPlayer modelData
|
||||
|
||||
icon: modelData === Players.active ? "check" : ""
|
||||
text: Players.getIdentity(modelData)
|
||||
activeIcon: "animated_images"
|
||||
}
|
||||
|
||||
component PlayerControl: IconButton {
|
||||
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)
|
||||
radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -60,10 +60,10 @@ Item {
|
|||
id: indicator
|
||||
|
||||
anchors.top: bar.bottom
|
||||
anchors.topMargin: Config.dashboard.sizes.tabIndicatorSpacing
|
||||
anchors.topMargin: 5
|
||||
|
||||
implicitWidth: bar.currentItem.implicitWidth
|
||||
implicitHeight: Config.dashboard.sizes.tabIndicatorHeight
|
||||
implicitHeight: 3
|
||||
|
||||
x: {
|
||||
const tab = bar.currentItem;
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ Item {
|
|||
|
||||
anchors.fill: parent
|
||||
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: width
|
||||
|
|
|
|||
233
services/NetworkUsage.qml
Normal file
233
services/NetworkUsage.qml
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int refCount: 0
|
||||
|
||||
// Current speeds in bytes per second
|
||||
readonly property real downloadSpeed: _downloadSpeed
|
||||
readonly property real uploadSpeed: _uploadSpeed
|
||||
|
||||
// Total bytes transferred since tracking started
|
||||
readonly property real downloadTotal: _downloadTotal
|
||||
readonly property real uploadTotal: _uploadTotal
|
||||
|
||||
// History of speeds for sparkline (most recent at end)
|
||||
readonly property var downloadHistory: _downloadHistory
|
||||
readonly property var uploadHistory: _uploadHistory
|
||||
readonly property int historyLength: 30
|
||||
|
||||
// Private properties
|
||||
property real _downloadSpeed: 0
|
||||
property real _uploadSpeed: 0
|
||||
property real _downloadTotal: 0
|
||||
property real _uploadTotal: 0
|
||||
property var _downloadHistory: []
|
||||
property var _uploadHistory: []
|
||||
|
||||
// Previous readings for calculating speed
|
||||
property real _prevRxBytes: 0
|
||||
property real _prevTxBytes: 0
|
||||
property real _prevTimestamp: 0
|
||||
|
||||
// Initial readings for calculating totals
|
||||
property real _initialRxBytes: 0
|
||||
property real _initialTxBytes: 0
|
||||
property bool _initialized: false
|
||||
|
||||
function formatBytes(bytes: real): var {
|
||||
// Handle negative or invalid values
|
||||
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
|
||||
return {
|
||||
value: 0,
|
||||
unit: "B/s"
|
||||
};
|
||||
}
|
||||
|
||||
if (bytes < 1024) {
|
||||
return {
|
||||
value: bytes,
|
||||
unit: "B/s"
|
||||
};
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return {
|
||||
value: bytes / 1024,
|
||||
unit: "KB/s"
|
||||
};
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return {
|
||||
value: bytes / (1024 * 1024),
|
||||
unit: "MB/s"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
value: bytes / (1024 * 1024 * 1024),
|
||||
unit: "GB/s"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytesTotal(bytes: real): var {
|
||||
// Handle negative or invalid values
|
||||
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
|
||||
return {
|
||||
value: 0,
|
||||
unit: "B"
|
||||
};
|
||||
}
|
||||
|
||||
if (bytes < 1024) {
|
||||
return {
|
||||
value: bytes,
|
||||
unit: "B"
|
||||
};
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return {
|
||||
value: bytes / 1024,
|
||||
unit: "KB"
|
||||
};
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return {
|
||||
value: bytes / (1024 * 1024),
|
||||
unit: "MB"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
value: bytes / (1024 * 1024 * 1024),
|
||||
unit: "GB"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseNetDev(content: string): var {
|
||||
const lines = content.split("\n");
|
||||
let totalRx = 0;
|
||||
let totalTx = 0;
|
||||
|
||||
for (let i = 2; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line)
|
||||
continue;
|
||||
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 10)
|
||||
continue;
|
||||
|
||||
const iface = parts[0].replace(":", "");
|
||||
// Skip loopback interface
|
||||
if (iface === "lo")
|
||||
continue;
|
||||
|
||||
const rxBytes = parseFloat(parts[1]) || 0;
|
||||
const txBytes = parseFloat(parts[9]) || 0;
|
||||
|
||||
totalRx += rxBytes;
|
||||
totalTx += txBytes;
|
||||
}
|
||||
|
||||
return {
|
||||
rx: totalRx,
|
||||
tx: totalTx
|
||||
};
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: netDevFile
|
||||
path: "/proc/net/dev"
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval
|
||||
running: root.refCount > 0
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
netDevFile.reload();
|
||||
const content = netDevFile.text();
|
||||
if (!content)
|
||||
return;
|
||||
|
||||
const data = root.parseNetDev(content);
|
||||
const now = Date.now();
|
||||
|
||||
if (!root._initialized) {
|
||||
root._initialRxBytes = data.rx;
|
||||
root._initialTxBytes = data.tx;
|
||||
root._prevRxBytes = data.rx;
|
||||
root._prevTxBytes = data.tx;
|
||||
root._prevTimestamp = now;
|
||||
root._initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const timeDelta = (now - root._prevTimestamp) / 1000; // seconds
|
||||
if (timeDelta > 0) {
|
||||
// Calculate byte deltas
|
||||
let rxDelta = data.rx - root._prevRxBytes;
|
||||
let txDelta = data.tx - root._prevTxBytes;
|
||||
|
||||
// Handle counter overflow (when counters wrap around from max to 0)
|
||||
// This happens when counters exceed 32-bit or 64-bit limits
|
||||
if (rxDelta < 0) {
|
||||
// Counter wrapped around - assume 64-bit counter
|
||||
rxDelta += Math.pow(2, 64);
|
||||
}
|
||||
if (txDelta < 0) {
|
||||
txDelta += Math.pow(2, 64);
|
||||
}
|
||||
|
||||
// Calculate speeds
|
||||
root._downloadSpeed = rxDelta / timeDelta;
|
||||
root._uploadSpeed = txDelta / timeDelta;
|
||||
|
||||
const maxHistory = root.historyLength + 1;
|
||||
|
||||
if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) {
|
||||
let newDownHist = root._downloadHistory.slice();
|
||||
newDownHist.push(root._downloadSpeed);
|
||||
if (newDownHist.length > maxHistory) {
|
||||
newDownHist.shift();
|
||||
}
|
||||
root._downloadHistory = newDownHist;
|
||||
}
|
||||
|
||||
if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) {
|
||||
let newUpHist = root._uploadHistory.slice();
|
||||
newUpHist.push(root._uploadSpeed);
|
||||
if (newUpHist.length > maxHistory) {
|
||||
newUpHist.shift();
|
||||
}
|
||||
root._uploadHistory = newUpHist;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate totals with overflow handling
|
||||
let downTotal = data.rx - root._initialRxBytes;
|
||||
let upTotal = data.tx - root._initialTxBytes;
|
||||
|
||||
// Handle counter overflow for totals
|
||||
if (downTotal < 0) {
|
||||
downTotal += Math.pow(2, 64);
|
||||
}
|
||||
if (upTotal < 0) {
|
||||
upTotal += Math.pow(2, 64);
|
||||
}
|
||||
|
||||
root._downloadTotal = downTotal;
|
||||
root._uploadTotal = upTotal;
|
||||
|
||||
root._prevRxBytes = data.rx;
|
||||
root._prevTxBytes = data.tx;
|
||||
root._prevTimestamp = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,24 +8,50 @@ import QtQuick
|
|||
Singleton {
|
||||
id: root
|
||||
|
||||
// CPU properties
|
||||
property string cpuName: ""
|
||||
property real cpuPerc
|
||||
property real cpuTemp
|
||||
|
||||
// GPU properties
|
||||
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
|
||||
property string autoGpuType: "NONE"
|
||||
property string gpuName: ""
|
||||
property real gpuPerc
|
||||
property real gpuTemp
|
||||
|
||||
// Memory properties
|
||||
property real memUsed
|
||||
property real memTotal
|
||||
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
|
||||
property real storageUsed
|
||||
property real storageTotal
|
||||
property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0
|
||||
|
||||
// Storage properties (aggregated)
|
||||
readonly property real storagePerc: {
|
||||
let totalUsed = 0;
|
||||
let totalSize = 0;
|
||||
for (const disk of disks) {
|
||||
totalUsed += disk.used;
|
||||
totalSize += disk.total;
|
||||
}
|
||||
return totalSize > 0 ? totalUsed / totalSize : 0;
|
||||
}
|
||||
|
||||
// Individual disks: Array of { mount, used, total, free, perc }
|
||||
property var disks: []
|
||||
|
||||
property real lastCpuIdle
|
||||
property real lastCpuTotal
|
||||
|
||||
property int refCount
|
||||
|
||||
function cleanCpuName(name: string): string {
|
||||
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function cleanGpuName(name: string): string {
|
||||
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function formatKib(kib: real): var {
|
||||
const mib = 1024;
|
||||
const gib = 1024 ** 2;
|
||||
|
|
@ -54,7 +80,7 @@ Singleton {
|
|||
|
||||
Timer {
|
||||
running: root.refCount > 0
|
||||
interval: 3000
|
||||
interval: Config.dashboard.resourceUpdateInterval
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
|
|
@ -66,6 +92,18 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
// One-time CPU info detection (name)
|
||||
FileView {
|
||||
id: cpuinfoInit
|
||||
|
||||
path: "/proc/cpuinfo"
|
||||
onLoaded: {
|
||||
const nameMatch = text().match(/model name\s*:\s*(.+)/);
|
||||
if (nameMatch)
|
||||
root.cpuName = root.cleanCpuName(nameMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: stat
|
||||
|
||||
|
|
@ -101,41 +139,120 @@ Singleton {
|
|||
Process {
|
||||
id: storage
|
||||
|
||||
command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"]
|
||||
// Get physical disks with aggregated usage from their partitions
|
||||
// lsblk outputs: NAME SIZE TYPE FSUSED FSSIZE in bytes
|
||||
command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const deviceMap = new Map();
|
||||
const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
|
||||
const lines = text.trim().split("\n");
|
||||
|
||||
for (const line of text.trim().split("\n")) {
|
||||
for (const line of lines) {
|
||||
if (line.trim() === "")
|
||||
continue;
|
||||
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 3) {
|
||||
const device = parts[0];
|
||||
const used = parseInt(parts[1], 10) || 0;
|
||||
const avail = parseInt(parts[2], 10) || 0;
|
||||
// Parse KEY="VALUE" format
|
||||
const nameMatch = line.match(/NAME="([^"]+)"/);
|
||||
const sizeMatch = line.match(/SIZE="([^"]+)"/);
|
||||
const typeMatch = line.match(/TYPE="([^"]+)"/);
|
||||
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
|
||||
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
|
||||
|
||||
// Only keep the entry with the largest total space for each device
|
||||
if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) {
|
||||
deviceMap.set(device, {
|
||||
used: used,
|
||||
avail: avail
|
||||
});
|
||||
if (!nameMatch || !typeMatch)
|
||||
continue;
|
||||
|
||||
const name = nameMatch[1];
|
||||
const type = typeMatch[1];
|
||||
const size = parseInt(sizeMatch?.[1] || "0", 10);
|
||||
const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
|
||||
const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
|
||||
|
||||
if (type === "disk") {
|
||||
// Skip zram (swap) devices
|
||||
if (name.startsWith("zram"))
|
||||
continue;
|
||||
|
||||
// Initialize disk entry
|
||||
if (!diskMap[name]) {
|
||||
diskMap[name] = {
|
||||
name: name,
|
||||
totalSize: size,
|
||||
used: 0,
|
||||
fsTotal: 0
|
||||
};
|
||||
}
|
||||
} else if (type === "part") {
|
||||
// Find parent disk (remove trailing numbers/p+numbers)
|
||||
let parentDisk = name.replace(/p?\d+$/, "");
|
||||
// For nvme devices like nvme0n1p1, parent is nvme0n1
|
||||
if (name.match(/nvme\d+n\d+p\d+/))
|
||||
parentDisk = name.replace(/p\d+$/, "");
|
||||
|
||||
// Aggregate partition usage to parent disk
|
||||
if (diskMap[parentDisk]) {
|
||||
diskMap[parentDisk].used += fsused;
|
||||
diskMap[parentDisk].fsTotal += fssize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to sorted array
|
||||
const diskList = [];
|
||||
let totalUsed = 0;
|
||||
let totalAvail = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
for (const [device, stats] of deviceMap) {
|
||||
totalUsed += stats.used;
|
||||
totalAvail += stats.avail;
|
||||
for (const diskName of Object.keys(diskMap).sort()) {
|
||||
const disk = diskMap[diskName];
|
||||
// Use filesystem total if available, otherwise use disk size
|
||||
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
|
||||
const used = disk.used;
|
||||
const perc = total > 0 ? used / total : 0;
|
||||
|
||||
// Convert bytes to KiB for consistency with formatKib
|
||||
diskList.push({
|
||||
mount: disk.name // Using 'mount' property for compatibility
|
||||
,
|
||||
used: used / 1024,
|
||||
total: total / 1024,
|
||||
free: (total - used) / 1024,
|
||||
perc: perc
|
||||
});
|
||||
|
||||
totalUsed += used;
|
||||
totalSize += total;
|
||||
}
|
||||
|
||||
root.storageUsed = totalUsed;
|
||||
root.storageTotal = totalUsed + totalAvail;
|
||||
root.disks = diskList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GPU name detection (one-time)
|
||||
Process {
|
||||
id: gpuNameDetect
|
||||
|
||||
running: true
|
||||
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const output = text.trim();
|
||||
if (!output)
|
||||
return;
|
||||
|
||||
// Check if it's from nvidia-smi (clean GPU name)
|
||||
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
|
||||
root.gpuName = root.cleanGpuName(output);
|
||||
} else {
|
||||
// Parse lspci output: extract name from brackets or after colon
|
||||
const bracketMatch = output.match(/\[([^\]]+)\]/);
|
||||
if (bracketMatch) {
|
||||
root.gpuName = root.cleanGpuName(bracketMatch[1]);
|
||||
} else {
|
||||
const colonMatch = output.match(/:\s*(.+)/);
|
||||
if (colonMatch)
|
||||
root.gpuName = root.cleanGpuName(colonMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue