overlay: add resource monitor widget

This commit is contained in:
end-4
2025-11-07 12:56:44 +01:00
parent e5e85db75d
commit a27a6deddf
8 changed files with 272 additions and 12 deletions
@@ -411,6 +411,7 @@ Singleton {
property JsonObject resources: JsonObject {
property int updateInterval: 3000
property int historyLength: 60
}
property JsonObject musicRecognition: JsonObject {
@@ -93,6 +93,13 @@ Singleton {
property real x: 100
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 bool pinned: 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
readonly property list<var> availableWidgets: [
{ identifier: "crosshair", materialSymbol: "point_scan" },
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
{ 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
@@ -9,6 +9,7 @@ import Quickshell.Bluetooth
import qs.modules.overlay.crosshair
import qs.modules.overlay.volumeMixer
import qs.modules.overlay.recorder
import qs.modules.overlay.resources
DelegateChooser {
id: root
@@ -17,4 +18,5 @@ DelegateChooser {
DelegateChoice { roleValue: "crosshair"; Crosshair {} }
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
DelegateChoice { roleValue: "recorder"; Recorder {} }
DelegateChoice { roleValue: "resources"; Resources {} }
}
@@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import qs
import qs.modules.common
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.
*/
Singleton {
property double memoryTotal: 1
property double memoryFree: 1
property double memoryUsed: memoryTotal - memoryFree
property double memoryUsedPercentage: memoryUsed / memoryTotal
property double swapTotal: 1
property double swapFree: 1
property double swapUsed: swapTotal - swapFree
property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0
property double cpuUsage: 0
id: root
property real memoryTotal: 1
property real memoryFree: 0
property real memoryUsed: memoryTotal - memoryFree
property real memoryUsedPercentage: memoryUsed / memoryTotal
property real swapTotal: 1
property real swapFree: 0
property real swapUsed: swapTotal - swapFree
property real swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0
property real cpuUsage: 0
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 {
interval: 1
running: true
@@ -29,6 +67,7 @@ Singleton {
// Reload files
fileMeminfo.reload()
fileStat.reload()
fileCpuinfo.reload()
// Parse memory and swap usage
const textMeminfo = fileMeminfo.text()
@@ -53,10 +92,36 @@ Singleton {
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
}
}
FileView { id: fileMeminfo; path: "/proc/meminfo" }
FileView { id: fileStat; path: "/proc/stat" }
FileView { id: fileCpuinfo; path: "/proc/cpuinfo" }
}