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 {
id: root
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
tooltip: "Memory: " + root.percent + "% used"
tooltip: ""
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 {
id: meminfo
@ -19,12 +24,22 @@ M.BarSection {
if (v)
m[k.trim()] = parseInt(v.trim());
});
const total = m.MemTotal;
const avail = m.MemAvailable;
if (total > 0)
root.percent = Math.round(((total - avail) / total) * 100);
const total = m.MemTotal || 0;
const avail = m.MemAvailable || 0;
const buffers = m.Buffers || 0;
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 {
interval: M.Modules.memory.interval || 2000
running: true
@ -32,15 +47,253 @@ M.BarSection {
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 {
icon: "\uEFC5"
anchors.verticalCenter: parent.verticalCenter
TapHandler {
cursorShape: Qt.PointingHandCursor
onTapped: root._pinned = !root._pinned
}
}
M.BarLabel {
label: root.percent + "%"
minText: "100%"
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
IdleInhibitor 1.0 IdleInhibitor.qml
Notifications 1.0 Notifications.qml
singleton ProcessList 1.0 ProcessList.qml
singleton NotifService 1.0 NotifService.qml
NotifItem 1.0 NotifItem.qml
NotifPopup 1.0 NotifPopup.qml