From 7e0021853f5371e801252f7448e1e3ea494315a0 Mon Sep 17 00:00:00 2001 From: Damocles Date: Tue, 14 Apr 2026 01:00:08 +0200 Subject: [PATCH] add ProcessList singleton, memory hover panel with breakdown + top processes --- modules/Memory.qml | 267 ++++++++++++++++++++++++++++++++++++++-- modules/ProcessList.qml | 48 ++++++++ modules/qmldir | 1 + 3 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 modules/ProcessList.qml diff --git a/modules/Memory.qml b/modules/Memory.qml index d86c0ce..2c83502 100644 --- a/modules/Memory.qml +++ b/modules/Memory.qml @@ -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 + } } } diff --git a/modules/ProcessList.qml b/modules/ProcessList.qml new file mode 100644 index 0000000..b5bfbb9 --- /dev/null +++ b/modules/ProcessList.qml @@ -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 + } +} diff --git a/modules/qmldir b/modules/qmldir index 076eec8..aa9d8dd 100644 --- a/modules/qmldir +++ b/modules/qmldir @@ -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