mirror of
https://github.com/end-4/dots-hyprland.git
synced 2026-06-05 23:09:26 -05:00
overlay: add resource monitor widget
This commit is contained in:
@@ -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" }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user