forked from Shinonome/dots-hyprland
overlay: add resource monitor widget
This commit is contained in:
@@ -411,6 +411,7 @@ Singleton {
|
|||||||
|
|
||||||
property JsonObject resources: JsonObject {
|
property JsonObject resources: JsonObject {
|
||||||
property int updateInterval: 3000
|
property int updateInterval: 3000
|
||||||
|
property int historyLength: 60
|
||||||
}
|
}
|
||||||
|
|
||||||
property JsonObject musicRecognition: JsonObject {
|
property JsonObject musicRecognition: JsonObject {
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ Singleton {
|
|||||||
property real x: 100
|
property real x: 100
|
||||||
property real y: 130
|
property real y: 130
|
||||||
}
|
}
|
||||||
|
property JsonObject resources: JsonObject {
|
||||||
|
property bool pinned: false
|
||||||
|
property bool clickthrough: true
|
||||||
|
property real x: 1000
|
||||||
|
property real y: 320
|
||||||
|
property int tabIndex: 0
|
||||||
|
}
|
||||||
property JsonObject volumeMixer: JsonObject {
|
property JsonObject volumeMixer: JsonObject {
|
||||||
property bool pinned: false
|
property bool pinned: false
|
||||||
property bool clickthrough: false
|
property bool clickthrough: false
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.functions
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Simple one value line graph
|
||||||
|
*/
|
||||||
|
Canvas {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
enum Alignment { Left, Right }
|
||||||
|
|
||||||
|
required property list<real> values
|
||||||
|
property int points: values.length
|
||||||
|
property color color: Appearance.colors.colPrimary
|
||||||
|
property real fillOpacity: 0.5
|
||||||
|
property var alignment: Graph.Alignment.Left
|
||||||
|
|
||||||
|
onValuesChanged: root.requestPaint()
|
||||||
|
onPaint: {
|
||||||
|
var ctx = getContext("2d")
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
if (!root.values || root.values.length < 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
var n = root.points
|
||||||
|
var dx = width / (n - 1)
|
||||||
|
ctx.strokeStyle = root.color
|
||||||
|
ctx.fillStyle = ColorUtils.transparentize(root.color, 1 - root.fillOpacity)
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.beginPath()
|
||||||
|
for (var i = 0; i < n; ++i) {
|
||||||
|
var valueIndex = (root.alignment === Graph.Alignment.Right) ? root.values.length - n + i : i
|
||||||
|
if (valueIndex < 0 || valueIndex >= root.values.length) {
|
||||||
|
continue; // No data for this point
|
||||||
|
}
|
||||||
|
var x = i * dx
|
||||||
|
var norm = root.values[valueIndex] // already in 0-1 range
|
||||||
|
var y = height - norm * height
|
||||||
|
if (valueIndex === 0) {
|
||||||
|
ctx.moveTo(x, height)
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.lineTo(width, height)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,10 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property list<var> availableWidgets: [
|
readonly property list<var> availableWidgets: [
|
||||||
{ identifier: "crosshair", materialSymbol: "point_scan" },
|
|
||||||
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
|
|
||||||
{ identifier: "recorder", materialSymbol: "screen_record" },
|
{ identifier: "recorder", materialSymbol: "screen_record" },
|
||||||
|
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
|
||||||
|
{ identifier: "crosshair", materialSymbol: "point_scan" },
|
||||||
|
{ identifier: "resources", materialSymbol: "browse_activity" }
|
||||||
]
|
]
|
||||||
|
|
||||||
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
|
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Quickshell.Bluetooth
|
|||||||
import qs.modules.overlay.crosshair
|
import qs.modules.overlay.crosshair
|
||||||
import qs.modules.overlay.volumeMixer
|
import qs.modules.overlay.volumeMixer
|
||||||
import qs.modules.overlay.recorder
|
import qs.modules.overlay.recorder
|
||||||
|
import qs.modules.overlay.resources
|
||||||
|
|
||||||
DelegateChooser {
|
DelegateChooser {
|
||||||
id: root
|
id: root
|
||||||
@@ -17,4 +18,5 @@ DelegateChooser {
|
|||||||
DelegateChoice { roleValue: "crosshair"; Crosshair {} }
|
DelegateChoice { roleValue: "crosshair"; Crosshair {} }
|
||||||
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
|
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
|
||||||
DelegateChoice { roleValue: "recorder"; Recorder {} }
|
DelegateChoice { roleValue: "recorder"; Recorder {} }
|
||||||
|
DelegateChoice { roleValue: "resources"; Resources {} }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Hyprland
|
|
||||||
import qs
|
import qs
|
||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
import qs.modules.common.widgets
|
import qs.modules.common.widgets
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Qt.labs.synchronizer
|
||||||
|
import qs
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.modules.overlay
|
||||||
|
|
||||||
|
StyledOverlayWidget {
|
||||||
|
id: root
|
||||||
|
property list<var> resources: [
|
||||||
|
{
|
||||||
|
"icon": "planner_review",
|
||||||
|
"name": Translation.tr("CPU"),
|
||||||
|
"history": ResourceUsage.cpuUsageHistory,
|
||||||
|
"maxAvailableString": ResourceUsage.maxAvailableCpuString
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "memory",
|
||||||
|
"name": Translation.tr("RAM"),
|
||||||
|
"history": ResourceUsage.memoryUsageHistory,
|
||||||
|
"maxAvailableString": ResourceUsage.maxAvailableMemoryString
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "swap_horiz",
|
||||||
|
"name": Translation.tr("Swap"),
|
||||||
|
"history": ResourceUsage.swapUsageHistory,
|
||||||
|
"maxAvailableString": ResourceUsage.maxAvailableSwapString
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
contentItem: Rectangle {
|
||||||
|
id: contentItem
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: Appearance.m3colors.m3surfaceContainer
|
||||||
|
property real padding: 4
|
||||||
|
implicitWidth: 350
|
||||||
|
implicitHeight: 200
|
||||||
|
// implicitHeight: contentColumn.implicitHeight + padding * 2
|
||||||
|
ColumnLayout {
|
||||||
|
id: contentColumn
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
margins: parent.padding
|
||||||
|
}
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
SecondaryTabBar {
|
||||||
|
id: tabBar
|
||||||
|
|
||||||
|
currentIndex: Persistent.states.overlay.resources.tabIndex
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
Persistent.states.overlay.resources.tabIndex = tabBar.currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.resources.length
|
||||||
|
delegate: SecondaryTabButton {
|
||||||
|
required property int index
|
||||||
|
property var modelData: root.resources[index]
|
||||||
|
buttonIcon: modelData.icon
|
||||||
|
buttonText: modelData.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceSummary {
|
||||||
|
Layout.margins: 8
|
||||||
|
history: root.resources[tabBar.currentIndex]?.history ?? []
|
||||||
|
maxAvailableString: root.resources[tabBar.currentIndex]?.maxAvailableString ?? "--"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ResourceSummary: RowLayout {
|
||||||
|
id: resourceSummary
|
||||||
|
required property list<real> history
|
||||||
|
required property string maxAvailableString
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 2
|
||||||
|
StyledText {
|
||||||
|
text: (resourceSummary.history[resourceSummary.history.length - 1] * 100).toFixed(1) + "%"
|
||||||
|
font {
|
||||||
|
family: Appearance.font.family.numbers
|
||||||
|
variableAxes: Appearance.font.variableAxes.numbers
|
||||||
|
pixelSize: Appearance.font.pixelSize.huge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
text: Translation.tr("of %1").arg(resourceSummary.maxAvailableString)
|
||||||
|
font {
|
||||||
|
// family: Appearance.font.family.numbers
|
||||||
|
// variableAxes: Appearance.font.variableAxes.numbers
|
||||||
|
pixelSize: Appearance.font.pixelSize.smallie
|
||||||
|
}
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
id: graphBg
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
color: Appearance.colors.colSecondaryContainer
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: OpacityMask {
|
||||||
|
maskSource: Rectangle {
|
||||||
|
width: graphBg.width
|
||||||
|
height: graphBg.height
|
||||||
|
radius: graphBg.radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Graph {
|
||||||
|
anchors.fill: parent
|
||||||
|
values: root.resources[tabBar.currentIndex]?.history ?? []
|
||||||
|
points: ResourceUsage.historyLength
|
||||||
|
alignment: Graph.Alignment.Right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,17 +10,55 @@ import Quickshell.Io
|
|||||||
* Simple polled resource usage service with RAM, Swap, and CPU usage.
|
* Simple polled resource usage service with RAM, Swap, and CPU usage.
|
||||||
*/
|
*/
|
||||||
Singleton {
|
Singleton {
|
||||||
property double memoryTotal: 1
|
id: root
|
||||||
property double memoryFree: 1
|
property real memoryTotal: 1
|
||||||
property double memoryUsed: memoryTotal - memoryFree
|
property real memoryFree: 0
|
||||||
property double memoryUsedPercentage: memoryUsed / memoryTotal
|
property real memoryUsed: memoryTotal - memoryFree
|
||||||
property double swapTotal: 1
|
property real memoryUsedPercentage: memoryUsed / memoryTotal
|
||||||
property double swapFree: 1
|
property real swapTotal: 1
|
||||||
property double swapUsed: swapTotal - swapFree
|
property real swapFree: 0
|
||||||
property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0
|
property real swapUsed: swapTotal - swapFree
|
||||||
property double cpuUsage: 0
|
property real swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0
|
||||||
|
property real cpuUsage: 0
|
||||||
property var previousCpuStats
|
property var previousCpuStats
|
||||||
|
|
||||||
|
property string maxAvailableMemoryString: kbToGbString(ResourceUsage.memoryTotal)
|
||||||
|
property string maxAvailableSwapString: kbToGbString(ResourceUsage.swapTotal)
|
||||||
|
property string maxAvailableCpuString: "--"
|
||||||
|
|
||||||
|
readonly property int historyLength: Config?.options.resources.historyLength ?? 60
|
||||||
|
property list<real> cpuUsageHistory: []
|
||||||
|
property list<real> memoryUsageHistory: []
|
||||||
|
property list<real> swapUsageHistory: []
|
||||||
|
|
||||||
|
function kbToGbString(kb) {
|
||||||
|
return (kb / (1024 * 1024)).toFixed(1) + " GB";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMemoryUsageHistory() {
|
||||||
|
memoryUsageHistory = [...memoryUsageHistory, memoryUsedPercentage]
|
||||||
|
if (memoryUsageHistory.length > historyLength) {
|
||||||
|
memoryUsageHistory.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateSwapUsageHistory() {
|
||||||
|
swapUsageHistory = [...swapUsageHistory, swapUsedPercentage]
|
||||||
|
if (swapUsageHistory.length > historyLength) {
|
||||||
|
swapUsageHistory.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateCpuUsageHistory() {
|
||||||
|
cpuUsageHistory = [...cpuUsageHistory, cpuUsage]
|
||||||
|
if (cpuUsageHistory.length > historyLength) {
|
||||||
|
cpuUsageHistory.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateHistories() {
|
||||||
|
updateMemoryUsageHistory()
|
||||||
|
updateSwapUsageHistory()
|
||||||
|
updateCpuUsageHistory()
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 1
|
interval: 1
|
||||||
running: true
|
running: true
|
||||||
@@ -29,6 +67,7 @@ Singleton {
|
|||||||
// Reload files
|
// Reload files
|
||||||
fileMeminfo.reload()
|
fileMeminfo.reload()
|
||||||
fileStat.reload()
|
fileStat.reload()
|
||||||
|
fileCpuinfo.reload()
|
||||||
|
|
||||||
// Parse memory and swap usage
|
// Parse memory and swap usage
|
||||||
const textMeminfo = fileMeminfo.text()
|
const textMeminfo = fileMeminfo.text()
|
||||||
@@ -53,10 +92,36 @@ Singleton {
|
|||||||
|
|
||||||
previousCpuStats = { total, idle }
|
previousCpuStats = { total, idle }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse max CPU frequency
|
||||||
|
const textCpuinfo = fileCpuinfo.text()
|
||||||
|
// Try to find 'cpu max MHz', fallback to highest 'cpu MHz'
|
||||||
|
let maxMHz = 0
|
||||||
|
let match
|
||||||
|
// Try cpu max MHz (modern kernels)
|
||||||
|
match = textCpuinfo.match(/cpu max MHz\s*:\s*([\d.]+)/)
|
||||||
|
if (match) {
|
||||||
|
maxMHz = Number(match[1])
|
||||||
|
} else {
|
||||||
|
// Fallback: find all cpu MHz lines and take the max
|
||||||
|
let mhzRegex = /cpu MHz\s*:\s*([\d.]+)/g
|
||||||
|
let mhzMatch
|
||||||
|
let mhzList = []
|
||||||
|
while ((mhzMatch = mhzRegex.exec(textCpuinfo)) !== null) {
|
||||||
|
mhzList.push(Number(mhzMatch[1]))
|
||||||
|
}
|
||||||
|
if (mhzList.length > 0) {
|
||||||
|
maxMHz = Math.max.apply(null, mhzList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.maxAvailableCpuString = maxMHz > 0 ? (maxMHz / 1000).toFixed(1) + "GHz" : "--"
|
||||||
|
|
||||||
|
root.updateHistories()
|
||||||
interval = Config.options?.resources?.updateInterval ?? 3000
|
interval = Config.options?.resources?.updateInterval ?? 3000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView { id: fileMeminfo; path: "/proc/meminfo" }
|
FileView { id: fileMeminfo; path: "/proc/meminfo" }
|
||||||
FileView { id: fileStat; path: "/proc/stat" }
|
FileView { id: fileStat; path: "/proc/stat" }
|
||||||
|
FileView { id: fileCpuinfo; path: "/proc/cpuinfo" }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user