add ProcessList singleton, memory hover panel with breakdown + top processes

This commit is contained in:
Damocles 2026-04-14 01:00:08 +02:00
parent edcc78483c
commit 7e0021853f
3 changed files with 309 additions and 7 deletions

View file

@ -5,9 +5,14 @@ import "." as M
M.BarSection { M.BarSection {
id: root id: root
spacing: Math.max(1, M.Theme.moduleSpacing - 2) spacing: Math.max(1, M.Theme.moduleSpacing - 2)
tooltip: "Memory: " + root.percent + "% used" tooltip: ""
property int percent: 0 property int percent: 0
property real usedGb: 0
property real totalGb: 0
property real availGb: 0
property real cachedGb: 0
property real buffersGb: 0
FileView { FileView {
id: meminfo id: meminfo
@ -19,12 +24,22 @@ M.BarSection {
if (v) if (v)
m[k.trim()] = parseInt(v.trim()); m[k.trim()] = parseInt(v.trim());
}); });
const total = m.MemTotal; const total = m.MemTotal || 0;
const avail = m.MemAvailable; const avail = m.MemAvailable || 0;
if (total > 0) const buffers = m.Buffers || 0;
root.percent = Math.round(((total - avail) / total) * 100); const cached = (m.Cached || 0) + (m.SReclaimable || 0);
const used = total - avail;
if (total > 0) {
root.percent = Math.round(used / total * 100);
root.usedGb = used / 1048576;
root.totalGb = total / 1048576;
root.availGb = avail / 1048576;
root.cachedGb = cached / 1048576;
root.buffersGb = buffers / 1048576;
}
} }
} }
Timer { Timer {
interval: M.Modules.memory.interval || 2000 interval: M.Modules.memory.interval || 2000
running: true running: true
@ -32,15 +47,253 @@ M.BarSection {
onTriggered: meminfo.reload() onTriggered: meminfo.reload()
} }
function _fmt(gb) {
return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G";
}
property bool _pinned: false
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
readonly property bool _showPanel: _anyHover || _pinned
on_AnyHoverChanged: {
if (_anyHover)
_unpinTimer.stop();
else if (_pinned)
_unpinTimer.start();
}
Timer {
id: _unpinTimer
interval: 500
onTriggered: root._pinned = false
}
M.BarIcon { M.BarIcon {
icon: "\uEFC5" icon: "\uEFC5"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
cursorShape: Qt.PointingHandCursor
onTapped: root._pinned = !root._pinned
}
} }
M.BarLabel { M.BarLabel {
label: root.percent + "%" label: root.percent + "%"
minText: "100%" minText: "100%"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
TapHandler {
cursorShape: Qt.PointingHandCursor
onTapped: root._pinned = !root._pinned
}
}
M.HoverPanel {
id: hoverPanel
showPanel: root._showPanel
screen: QsWindow.window?.screen ?? null
anchorItem: root
accentColor: root.accentColor
panelNamespace: "nova-memory"
contentWidth: 240
// Header
Item {
width: parent.width
height: 28
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Memory"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 1
font.family: M.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._fmt(root.usedGb) + " / " + root._fmt(root.totalGb)
color: root.accentColor
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
font.bold: true
}
}
// Usage bar
Item {
width: parent.width
height: 14
Item {
id: memBar
anchors.left: parent.left
anchors.leftMargin: 12
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 6
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 3
}
// Cached (base0D, behind used)
Rectangle {
width: parent.width * Math.min(1, (root.usedGb + root.cachedGb) / Math.max(root.totalGb, 0.001))
height: parent.height
color: M.Theme.base0D
opacity: 0.4
radius: 3
Behavior on width {
NumberAnimation {
duration: 200
}
}
}
// Used (accentColor, on top)
Rectangle {
width: parent.width * Math.min(1, root.usedGb / Math.max(root.totalGb, 0.001))
height: parent.height
color: root.accentColor
radius: 3
Behavior on width {
NumberAnimation {
duration: 200
}
}
}
}
}
// Breakdown rows
Item {
width: parent.width
height: 18
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Used"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._fmt(root.usedGb)
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
}
Item {
width: parent.width
height: 18
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Cached"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._fmt(root.cachedGb)
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
}
Item {
width: parent.width
height: 18
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Available"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root._fmt(root.availGb)
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
}
}
// Process list separator
Rectangle {
width: parent.width - 16
height: 1
anchors.horizontalCenter: parent.horizontalCenter
color: M.Theme.base03
}
// Top processes by memory
Repeater {
model: M.ProcessList.byMem
delegate: Item {
required property var modelData
width: hoverPanel.contentWidth
height: 20
Text {
id: procCmd
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: modelData.cmd
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
elide: Text.ElideRight
width: parent.width - 80
}
Text {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: modelData.mem.toFixed(1) + "%"
color: M.Theme.base04
font.pixelSize: M.Theme.fontSize - 2
font.family: M.Theme.fontFamily
width: 36
horizontalAlignment: Text.AlignRight
}
}
}
Item {
width: 1
height: 4
}
} }
} }

48
modules/ProcessList.qml Normal file
View file

@ -0,0 +1,48 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import "." as M
QtObject {
id: root
property var byCpu: []
property var byMem: []
property int maxItems: 8
property Process _proc: Process {
id: proc
running: true
command: ["sh", "-c", "ps aux --sort=-%cpu 2>/dev/null | awk 'NR>1 && NR<=50 {cmd=$11; for(i=12;i<=NF&&i<=13;i++) cmd=cmd\" \"$i; print $1\"|\"$2\"|\"$3\"|\"$4\"|\"cmd}'"]
stdout: StdioCollector {
onStreamFinished: {
const rows = [];
for (const line of text.trim().split("\n")) {
if (!line)
continue;
const p = line.split("|");
if (p.length < 5)
continue;
const cmd = p[4].replace(/^.*\//, "");
rows.push({
"user": p[0],
"pid": parseInt(p[1]),
"cpu": parseFloat(p[2]),
"mem": parseFloat(p[3]),
"cmd": cmd || p[4]
});
}
root.byCpu = rows.slice().sort((a, b) => b.cpu - a.cpu).slice(0, root.maxItems);
root.byMem = rows.slice().sort((a, b) => b.mem - a.mem).slice(0, root.maxItems);
}
}
}
property Timer _timer: Timer {
interval: 2000
running: true
repeat: true
onTriggered: proc.running = true
}
}

View file

@ -32,6 +32,7 @@ Weather 1.0 Weather.qml
PowerProfile 1.0 PowerProfile.qml PowerProfile 1.0 PowerProfile.qml
IdleInhibitor 1.0 IdleInhibitor.qml IdleInhibitor 1.0 IdleInhibitor.qml
Notifications 1.0 Notifications.qml Notifications 1.0 Notifications.qml
singleton ProcessList 1.0 ProcessList.qml
singleton NotifService 1.0 NotifService.qml singleton NotifService 1.0 NotifService.qml
NotifItem 1.0 NotifItem.qml NotifItem 1.0 NotifItem.qml
NotifPopup 1.0 NotifPopup.qml NotifPopup 1.0 NotifPopup.qml