summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList
diff options
context:
space:
mode:
authorNippy <nippy@rp1.hu>2026-05-08 19:50:49 +0200
committerNippy <nippy@rp1.hu>2026-05-08 19:50:49 +0200
commit3462bcc46ecf74f576ea4397f16492c16bd75b10 (patch)
treec5ab7aef4498647e057bda8d49c11e5f9dda74c9 /raveos-hyprland-theme/theme-data/dms/Modules/ProcessList
parent56616f693b4f263cbf0d47da43b67424efa6022d (diff)
downloadRaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.tar.gz
RaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.zip
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modules/ProcessList')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/DisksView.qml346
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/PerformanceView.qml304
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessContextMenu.qml341
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessListPopout.qml535
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessesView.qml769
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/SystemView.qml386
6 files changed, 2681 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/DisksView.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/DisksView.qml
new file mode 100644
index 0000000..5a8e688
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/DisksView.qml
@@ -0,0 +1,346 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ function formatSpeed(bytesPerSec) {
+ if (bytesPerSec < 1024)
+ return bytesPerSec.toFixed(0) + " B/s";
+ if (bytesPerSec < 1024 * 1024)
+ return (bytesPerSec / 1024).toFixed(1) + " KB/s";
+ if (bytesPerSec < 1024 * 1024 * 1024)
+ return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s";
+ return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
+ }
+
+ Component.onCompleted: {
+ DgopService.addRef(["disk", "diskmounts"]);
+ }
+
+ Component.onDestruction: {
+ DgopService.removeRef(["disk", "diskmounts"]);
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: Theme.spacingM
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 80
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingXL
+
+ Column {
+ Layout.fillWidth: true
+ spacing: Theme.spacingXS
+
+ Row {
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "storage"
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Disk I/O", "disk io header in system monitor")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingL
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Read:", "disk read label")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: root.formatSpeed(DgopService.diskReadRate)
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: Theme.primary
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Write:", "disk write label")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: root.formatSpeed(DgopService.diskWriteRate)
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: Theme.warning
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ Row {
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "folder"
+ size: Theme.iconSize - 2
+ color: Theme.secondary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Mount Points", "mount points header in system monitor")
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Theme.outlineLight
+ }
+
+ DankListView {
+ id: mountListView
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ spacing: 4
+
+ model: DgopService.diskMounts
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: mountListView.width
+ height: 60
+ radius: Theme.cornerRadius
+ color: mountMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
+
+ readonly property real usedPct: {
+ const pctStr = modelData?.percent ?? "0%";
+ return parseFloat(pctStr.replace("%", "")) / 100;
+ }
+
+ MouseArea {
+ id: mountMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingM
+
+ Column {
+ Layout.fillWidth: true
+ spacing: Theme.spacingXS
+
+ Row {
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: {
+ const mp = modelData?.mount ?? "";
+ if (mp === "/")
+ return "home";
+ if (mp === "/home")
+ return "person";
+ if (mp.includes("boot"))
+ return "memory";
+ if (mp.includes("media") || mp.includes("mnt"))
+ return "usb";
+ return "folder";
+ }
+ size: Theme.iconSize - 4
+ color: Theme.surfaceText
+ opacity: 0.8
+ }
+
+ StyledText {
+ text: modelData?.mount ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: modelData?.device ?? ""
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: "•"
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: modelData?.fstype ?? ""
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ Column {
+ Layout.preferredWidth: 200
+ spacing: Theme.spacingXS
+
+ Rectangle {
+ width: parent.width
+ height: 8
+ radius: 4
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+
+ Rectangle {
+ width: parent.width * Math.min(1, parent.parent.parent.parent.usedPct)
+ height: parent.height
+ radius: 4
+ color: {
+ const pct = parent.parent.parent.parent.usedPct;
+ if (pct > 0.95)
+ return Theme.error;
+ if (pct > 0.85)
+ return Theme.warning;
+ return Theme.primary;
+ }
+
+ Behavior on width {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+ }
+
+ Row {
+ anchors.right: parent.right
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: modelData?.used ?? ""
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: "/"
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: modelData?.size ?? ""
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ StyledText {
+ Layout.preferredWidth: 50
+ text: modelData?.percent ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: {
+ const pct = parent.parent.usedPct;
+ if (pct > 0.95)
+ return Theme.error;
+ if (pct > 0.85)
+ return Theme.warning;
+ return Theme.surfaceText;
+ }
+ horizontalAlignment: Text.AlignRight
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 300
+ height: 80
+ radius: Theme.cornerRadius
+ color: "transparent"
+ visible: DgopService.diskMounts.length === 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "storage"
+ size: 32
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("No mount points found", "empty state in disk mounts list")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/PerformanceView.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/PerformanceView.qml
new file mode 100644
index 0000000..c793d92
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/PerformanceView.qml
@@ -0,0 +1,304 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ readonly property int historySize: 60
+
+ property var cpuHistory: []
+ property var memoryHistory: []
+ property var networkRxHistory: []
+ property var networkTxHistory: []
+ property var diskReadHistory: []
+ property var diskWriteHistory: []
+
+ function formatBytes(bytes) {
+ if (bytes < 1024)
+ return bytes.toFixed(0) + " B/s";
+ if (bytes < 1024 * 1024)
+ return (bytes / 1024).toFixed(1) + " KB/s";
+ if (bytes < 1024 * 1024 * 1024)
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
+ }
+
+ function addToHistory(arr, val) {
+ const newArr = arr.slice();
+ newArr.push(val);
+ if (newArr.length > historySize)
+ newArr.shift();
+ return newArr;
+ }
+
+ function sampleData() {
+ cpuHistory = addToHistory(cpuHistory, DgopService.cpuUsage);
+ memoryHistory = addToHistory(memoryHistory, DgopService.memoryUsage);
+ networkRxHistory = addToHistory(networkRxHistory, DgopService.networkRxRate);
+ networkTxHistory = addToHistory(networkTxHistory, DgopService.networkTxRate);
+ diskReadHistory = addToHistory(diskReadHistory, DgopService.diskReadRate);
+ diskWriteHistory = addToHistory(diskWriteHistory, DgopService.diskWriteRate);
+ }
+
+ Component.onCompleted: {
+ DgopService.addRef(["cpu", "memory", "network", "disk", "diskmounts", "system"]);
+ }
+
+ Component.onDestruction: {
+ DgopService.removeRef(["cpu", "memory", "network", "disk", "diskmounts", "system"]);
+ }
+
+ SystemClock {
+ id: sampleClock
+ precision: SystemClock.Seconds
+ onDateChanged: {
+ if (date.getSeconds() % 1 === 0)
+ root.sampleData();
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: Theme.spacingM
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.preferredHeight: (root.height - Theme.spacingM * 2) / 2
+ spacing: Theme.spacingM
+
+ PerformanceCard {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ title: "CPU"
+ icon: "memory"
+ value: DgopService.cpuUsage.toFixed(1) + "%"
+ subtitle: DgopService.cpuModel || (DgopService.cpuCores + " cores")
+ accentColor: Theme.primary
+ history: root.cpuHistory
+ maxValue: 100
+ showSecondary: false
+ extraInfo: DgopService.cpuTemperature > 0 ? (DgopService.cpuTemperature.toFixed(0) + "°C") : ""
+ extraInfoColor: DgopService.cpuTemperature > 80 ? Theme.error : (DgopService.cpuTemperature > 60 ? Theme.warning : Theme.surfaceVariantText)
+ }
+
+ PerformanceCard {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ title: I18n.tr("Memory")
+ icon: "sd_card"
+ value: DgopService.memoryUsage.toFixed(1) + "%"
+ subtitle: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
+ accentColor: Theme.secondary
+ history: root.memoryHistory
+ maxValue: 100
+ showSecondary: false
+ extraInfo: DgopService.totalSwapKB > 0 ? ("Swap: " + DgopService.formatSystemMemory(DgopService.usedSwapKB)) : ""
+ extraInfoColor: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.preferredHeight: (root.height - Theme.spacingM * 2) / 2
+ spacing: Theme.spacingM
+
+ PerformanceCard {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ title: I18n.tr("Network")
+ icon: "swap_horiz"
+ value: "↓ " + root.formatBytes(DgopService.networkRxRate)
+ subtitle: "↑ " + root.formatBytes(DgopService.networkTxRate)
+ accentColor: Theme.info
+ history: root.networkRxHistory
+ history2: root.networkTxHistory
+ maxValue: 0
+ showSecondary: true
+ extraInfo: ""
+ extraInfoColor: Theme.surfaceVariantText
+ }
+
+ PerformanceCard {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ title: I18n.tr("Disk")
+ icon: "storage"
+ value: "R: " + root.formatBytes(DgopService.diskReadRate)
+ subtitle: "W: " + root.formatBytes(DgopService.diskWriteRate)
+ accentColor: Theme.warning
+ history: root.diskReadHistory
+ history2: root.diskWriteHistory
+ maxValue: 0
+ showSecondary: true
+ extraInfo: {
+ const rootMount = DgopService.diskMounts.find(m => m.mountpoint === "/");
+ if (rootMount) {
+ const usedPct = ((rootMount.used || 0) / Math.max(1, rootMount.total || 1) * 100).toFixed(0);
+ return "/ " + usedPct + "% used";
+ }
+ return "";
+ }
+ extraInfoColor: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ component PerformanceCard: Rectangle {
+ id: card
+
+ property string title: ""
+ property string icon: ""
+ property string value: ""
+ property string subtitle: ""
+ property color accentColor: Theme.primary
+ property var history: []
+ property var history2: null
+ property real maxValue: 100
+ property bool showSecondary: false
+ property string extraInfo: ""
+ property color extraInfoColor: Theme.surfaceVariantText
+
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: Theme.outlineLight
+ border.width: 1
+
+ Canvas {
+ id: graphCanvas
+ anchors.fill: parent
+ anchors.margins: 4
+ renderStrategy: Canvas.Cooperative
+
+ property var hist: card.history
+ property var hist2: card.history2
+
+ onHistChanged: requestPaint()
+ onHist2Changed: requestPaint()
+ onWidthChanged: requestPaint()
+ onHeightChanged: requestPaint()
+
+ onPaint: {
+ const ctx = getContext("2d");
+ ctx.reset();
+ ctx.clearRect(0, 0, width, height);
+
+ if (!hist || hist.length < 2)
+ return;
+
+ let max = card.maxValue;
+ if (max <= 0) {
+ max = 1;
+ for (let k = 0; k < hist.length; k++)
+ max = Math.max(max, hist[k]);
+ if (hist2) {
+ for (let l = 0; l < hist2.length; l++)
+ max = Math.max(max, hist2[l]);
+ }
+ max *= 1.1;
+ }
+
+ const c = card.accentColor;
+ const grad = ctx.createLinearGradient(0, 0, 0, height);
+ grad.addColorStop(0, Qt.rgba(c.r, c.g, c.b, 0.25));
+ grad.addColorStop(1, Qt.rgba(c.r, c.g, c.b, 0.02));
+
+ ctx.fillStyle = grad;
+ ctx.beginPath();
+ ctx.moveTo(0, height);
+ for (let i = 0; i < hist.length; i++) {
+ const x = (width / (root.historySize - 1)) * i;
+ const y = height - (hist[i] / max) * height * 0.8;
+ ctx.lineTo(x, y);
+ }
+ ctx.lineTo((width / (root.historySize - 1)) * (hist.length - 1), height);
+ ctx.closePath();
+ ctx.fill();
+
+ ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.8);
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ for (let j = 0; j < hist.length; j++) {
+ const px = (width / (root.historySize - 1)) * j;
+ const py = height - (hist[j] / max) * height * 0.8;
+ j === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
+ }
+ ctx.stroke();
+
+ if (hist2 && hist2.length >= 2 && card.showSecondary) {
+ ctx.strokeStyle = Qt.rgba(c.r, c.g, c.b, 0.4);
+ ctx.lineWidth = 1.5;
+ ctx.setLineDash([4, 4]);
+ ctx.beginPath();
+ for (let m = 0; m < hist2.length; m++) {
+ const sx = (width / (root.historySize - 1)) * m;
+ const sy = height - (hist2[m] / max) * height * 0.8;
+ m === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
+ }
+ ctx.stroke();
+ ctx.setLineDash([]);
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingXS
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: card.icon
+ size: Theme.iconSize
+ color: card.accentColor
+ }
+
+ StyledText {
+ text: card.title
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ StyledText {
+ text: card.extraInfo
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ color: card.extraInfoColor
+ visible: card.extraInfo.length > 0
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+
+ StyledText {
+ text: card.value
+ font.pixelSize: Theme.fontSizeXLarge
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: card.subtitle
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessContextMenu.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessContextMenu.qml
new file mode 100644
index 0000000..7c48b5a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessContextMenu.qml
@@ -0,0 +1,341 @@
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Popup {
+ id: processContextMenu
+
+ property var processData: null
+ property int selectedIndex: -1
+ property bool keyboardNavigation: false
+ property var parentFocusItem: null
+
+ signal menuClosed
+ signal processKilled
+
+ readonly property var menuItems: [
+ {
+ text: I18n.tr("Copy PID"),
+ icon: "tag",
+ action: copyPid,
+ enabled: true
+ },
+ {
+ text: I18n.tr("Copy Name"),
+ icon: "content_copy",
+ action: copyName,
+ enabled: true
+ },
+ {
+ text: I18n.tr("Copy Full Command"),
+ icon: "code",
+ action: copyFullCommand,
+ enabled: true
+ },
+ {
+ type: "separator"
+ },
+ {
+ text: I18n.tr("Kill Process"),
+ icon: "close",
+ action: killProcess,
+ enabled: true,
+ dangerous: true
+ },
+ {
+ text: I18n.tr("Force Kill (SIGKILL)"),
+ icon: "dangerous",
+ action: forceKillProcess,
+ enabled: processData && processData.pid > 1000,
+ dangerous: true
+ }
+ ]
+
+ readonly property int visibleItemCount: {
+ let count = 0;
+ for (let i = 0; i < menuItems.length; i++) {
+ if (menuItems[i].type !== "separator")
+ count++;
+ }
+ return count;
+ }
+
+ function show(x, y, fromKeyboard) {
+ let finalX = x;
+ let finalY = y;
+
+ if (processContextMenu.parent) {
+ const parentWidth = processContextMenu.parent.width;
+ const parentHeight = processContextMenu.parent.height;
+ const menuWidth = processContextMenu.width;
+ const menuHeight = processContextMenu.height;
+
+ if (finalX + menuWidth > parentWidth)
+ finalX = Math.max(0, parentWidth - menuWidth);
+ if (finalY + menuHeight > parentHeight)
+ finalY = Math.max(0, parentHeight - menuHeight);
+ }
+
+ processContextMenu.x = finalX;
+ processContextMenu.y = finalY;
+ keyboardNavigation = fromKeyboard || false;
+ selectedIndex = fromKeyboard ? 0 : -1;
+ open();
+ }
+
+ function selectNext() {
+ if (visibleItemCount === 0)
+ return;
+ let current = selectedIndex;
+ let next = current;
+ do {
+ next = (next + 1) % menuItems.length;
+ } while (menuItems[next].type === "separator" && next !== current)
+ selectedIndex = next;
+ }
+
+ function selectPrevious() {
+ if (visibleItemCount === 0)
+ return;
+ let current = selectedIndex;
+ let prev = current;
+ do {
+ prev = (prev - 1 + menuItems.length) % menuItems.length;
+ } while (menuItems[prev].type === "separator" && prev !== current)
+ selectedIndex = prev;
+ }
+
+ function activateSelected() {
+ if (selectedIndex < 0 || selectedIndex >= menuItems.length)
+ return;
+ const item = menuItems[selectedIndex];
+ if (item.type === "separator" || !item.enabled)
+ return;
+ item.action();
+ }
+
+ function copyPid() {
+ if (processData)
+ Quickshell.execDetached(["dms", "cl", "copy", processData.pid.toString()]);
+ close();
+ }
+
+ function copyName() {
+ if (processData) {
+ const name = processData.command || "";
+ Quickshell.execDetached(["dms", "cl", "copy", name]);
+ }
+ close();
+ }
+
+ function copyFullCommand() {
+ if (processData) {
+ const fullCmd = processData.fullCommand || processData.command || "";
+ Quickshell.execDetached(["dms", "cl", "copy", fullCmd]);
+ }
+ close();
+ }
+
+ function killProcess() {
+ if (processData)
+ Quickshell.execDetached(["kill", processData.pid.toString()]);
+ processKilled();
+ close();
+ }
+
+ function forceKillProcess() {
+ if (processData)
+ Quickshell.execDetached(["kill", "-9", processData.pid.toString()]);
+ processKilled();
+ close();
+ }
+
+ width: 200
+ height: menuColumn.implicitHeight + Theme.spacingS * 2
+ padding: 0
+ modal: false
+ closePolicy: Popup.CloseOnEscape
+
+ onClosed: {
+ closePolicy = Popup.CloseOnEscape;
+ keyboardNavigation = false;
+ selectedIndex = -1;
+ menuClosed();
+ if (parentFocusItem)
+ Qt.callLater(() => parentFocusItem.forceActiveFocus());
+ }
+
+ onOpened: {
+ outsideClickTimer.start();
+ if (keyboardNavigation)
+ Qt.callLater(() => keyboardHandler.forceActiveFocus());
+ }
+
+ Timer {
+ id: outsideClickTimer
+ interval: 100
+ onTriggered: processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
+ }
+
+ background: Rectangle {
+ color: "transparent"
+ }
+
+ contentItem: Rectangle {
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
+ border.width: BlurService.enabled ? BlurService.borderWidth : 1
+
+ Item {
+ id: keyboardHandler
+ anchors.fill: parent
+ focus: keyboardNavigation
+
+ Keys.onPressed: event => {
+ switch (event.key) {
+ case Qt.Key_Down:
+ case Qt.Key_J:
+ keyboardNavigation = true;
+ selectNext();
+ event.accepted = true;
+ return;
+ case Qt.Key_Up:
+ case Qt.Key_K:
+ keyboardNavigation = true;
+ selectPrevious();
+ event.accepted = true;
+ return;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ case Qt.Key_Space:
+ activateSelected();
+ event.accepted = true;
+ return;
+ case Qt.Key_Escape:
+ case Qt.Key_Left:
+ case Qt.Key_H:
+ close();
+ event.accepted = true;
+ return;
+ }
+ }
+ }
+
+ Column {
+ id: menuColumn
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ spacing: 1
+
+ Repeater {
+ model: menuItems
+
+ Item {
+ width: parent.width
+ height: modelData.type === "separator" ? 5 : 32
+ visible: modelData.type !== "separator" || index > 0
+
+ property int itemVisibleIndex: {
+ let count = 0;
+ for (let i = 0; i < index; i++) {
+ if (menuItems[i].type !== "separator")
+ count++;
+ }
+ return count;
+ }
+
+ Rectangle {
+ visible: modelData.type === "separator"
+ width: parent.width - Theme.spacingS * 2
+ height: 1
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15)
+ }
+
+ Rectangle {
+ id: menuItem
+ visible: modelData.type !== "separator"
+ width: parent.width
+ height: 32
+ radius: Theme.cornerRadius
+ color: {
+ if (!modelData.enabled)
+ return "transparent";
+ const isSelected = keyboardNavigation && selectedIndex === index;
+ if (modelData.dangerous) {
+ if (isSelected)
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
+ return menuItemArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent";
+ }
+ if (isSelected)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
+ return menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
+ }
+ opacity: modelData.enabled ? 1 : 0.5
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: modelData.icon || ""
+ size: 16
+ color: {
+ if (!modelData.enabled)
+ return Theme.surfaceVariantText;
+ const isSelected = keyboardNavigation && selectedIndex === index;
+ if (modelData.dangerous && (menuItemArea.containsMouse || isSelected))
+ return Theme.error;
+ return Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: modelData.text || ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Normal
+ color: {
+ if (!modelData.enabled)
+ return Theme.surfaceVariantText;
+ const isSelected = keyboardNavigation && selectedIndex === index;
+ if (modelData.dangerous && (menuItemArea.containsMouse || isSelected))
+ return Theme.error;
+ return Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankRipple {
+ id: menuItemRipple
+ rippleColor: modelData.dangerous ? Theme.error : Theme.surfaceText
+ cornerRadius: menuItem.radius
+ }
+
+ MouseArea {
+ id: menuItemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: modelData.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: modelData.enabled ?? false
+ onEntered: {
+ keyboardNavigation = false;
+ selectedIndex = index;
+ }
+ onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y)
+ onClicked: modelData.action()
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessListPopout.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessListPopout.qml
new file mode 100644
index 0000000..3adbe07
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessListPopout.qml
@@ -0,0 +1,535 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Common
+import qs.Modules.ProcessList
+import qs.Services
+import qs.Widgets
+
+DankPopout {
+ id: processListPopout
+
+ layerNamespace: "dms:process-list-popout"
+
+ property var parentWidget: null
+ property var triggerScreen: null
+ property string searchText: ""
+ property string expandedPid: ""
+ property string processFilter: "all"
+
+ function hide() {
+ close();
+ if (processContextMenu.visible)
+ processContextMenu.close();
+ }
+
+ function show() {
+ open();
+ }
+
+ popupWidth: Math.round(Theme.fontSizeMedium * 46)
+ popupHeight: Math.round(Theme.fontSizeMedium * 39)
+ triggerWidth: 55
+ positioning: ""
+ screen: triggerScreen
+ shouldBeVisible: false
+
+ onBackgroundClicked: {
+ if (processContextMenu.visible)
+ processContextMenu.close();
+ close();
+ }
+
+ onShouldBeVisibleChanged: {
+ if (!shouldBeVisible) {
+ searchText = "";
+ expandedPid = "";
+ processFilter = "all";
+ }
+ }
+
+ Ref {
+ service: DgopService
+ }
+
+ ProcessContextMenu {
+ id: processContextMenu
+ }
+
+ content: Component {
+ Rectangle {
+ id: processListContent
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ radius: Theme.cornerRadius
+ color: "transparent"
+ clip: true
+ focus: true
+
+ Component.onCompleted: {
+ if (processListPopout.shouldBeVisible)
+ searchField.forceActiveFocus();
+ processContextMenu.parent = processListContent;
+ processContextMenu.parentFocusItem = processListContent;
+ }
+
+ Keys.onPressed: event => {
+ if (processContextMenu.visible)
+ return;
+
+ switch (event.key) {
+ case Qt.Key_Escape:
+ if (processListPopout.searchText.length > 0) {
+ processListPopout.searchText = "";
+ event.accepted = true;
+ return;
+ }
+ if (processesView.keyboardNavigationActive) {
+ processesView.reset();
+ event.accepted = true;
+ return;
+ }
+ processListPopout.close();
+ event.accepted = true;
+ return;
+ case Qt.Key_F:
+ if (event.modifiers & Qt.ControlModifier) {
+ searchField.forceActiveFocus();
+ event.accepted = true;
+ return;
+ }
+ break;
+ }
+
+ processesView.handleKey(event);
+ }
+
+ Connections {
+ target: processListPopout
+ function onShouldBeVisibleChanged() {
+ if (processListPopout.shouldBeVisible) {
+ Qt.callLater(() => searchField.forceActiveFocus());
+ } else {
+ processesView.reset();
+ processFilterGroup.currentIndex = 0;
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingS
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ Row {
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "analytics"
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Processes")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ DankButtonGroup {
+ id: processFilterGroup
+ Layout.minimumWidth: implicitWidth
+ model: [I18n.tr("All"), I18n.tr("User"), I18n.tr("System")]
+ currentIndex: 0
+ checkEnabled: false
+ buttonHeight: Math.round(Theme.fontSizeSmall * 2.4)
+ minButtonWidth: 0
+ buttonPadding: Theme.spacingM
+ textSize: Theme.fontSizeSmall
+ onSelectionChanged: (index, selected) => {
+ if (!selected)
+ return;
+ currentIndex = index;
+ switch (index) {
+ case 0:
+ processListPopout.processFilter = "all";
+ return;
+ case 1:
+ processListPopout.processFilter = "user";
+ return;
+ case 2:
+ processListPopout.processFilter = "system";
+ return;
+ }
+ }
+ }
+
+ DankTextField {
+ id: searchField
+ Layout.fillWidth: true
+ Layout.minimumWidth: Theme.fontSizeMedium * 8
+ Layout.preferredHeight: Theme.fontSizeMedium * 2.5
+ placeholderText: I18n.tr("Search...")
+ leftIconName: "search"
+ showClearButton: true
+ text: processListPopout.searchText
+ onTextChanged: processListPopout.searchText = text
+ ignoreUpDownKeys: true
+ keyForwardTargets: [processListContent]
+ }
+ }
+
+ Item {
+ id: statsContainer
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.max(leftInfo.height, gaugesRow.height) + Theme.spacingS
+
+ function compactMem(kb) {
+ if (kb < 1024 * 1024) {
+ const mb = kb / 1024;
+ return mb >= 100 ? mb.toFixed(0) + " MB" : mb.toFixed(1) + " MB";
+ }
+ const gb = kb / (1024 * 1024);
+ return gb >= 10 ? gb.toFixed(0) + " GB" : gb.toFixed(1) + " GB";
+ }
+
+ readonly property real gaugeSize: Theme.fontSizeMedium * 6.5
+
+ readonly property var enabledGpusWithTemp: {
+ if (!SessionData.enabledGpuPciIds || SessionData.enabledGpuPciIds.length === 0)
+ return [];
+ const result = [];
+ for (const gpu of DgopService.availableGpus) {
+ if (SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 && gpu.temperature > 0)
+ result.push(gpu);
+ }
+ return result;
+ }
+ readonly property bool hasGpu: enabledGpusWithTemp.length > 0
+
+ Row {
+ id: leftInfo
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: Theme.fontSizeMedium * 3
+ height: width
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
+
+ SystemLogo {
+ anchors.centerIn: parent
+ width: parent.width * 0.7
+ height: width
+ colorOverride: Theme.primary
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS / 2
+
+ StyledText {
+ text: DgopService.hostname || "localhost"
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: DgopService.distribution || "Linux"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ Row {
+ spacing: Theme.spacingS
+
+ Row {
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "schedule"
+ size: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: DgopService.shortUptime || "--"
+ font.pixelSize: Theme.fontSizeSmall - 1
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ StyledText {
+ text: "•"
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: DgopService.processCount + " " + I18n.tr("procs", "short for processes")
+ font.pixelSize: Theme.fontSizeSmall - 1
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ Row {
+ id: gaugesRow
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ CircleGauge {
+ width: statsContainer.gaugeSize
+ height: statsContainer.gaugeSize
+ value: DgopService.cpuUsage / 100
+ label: DgopService.cpuUsage.toFixed(0) + "%"
+ sublabel: "CPU"
+ detail: DgopService.cpuTemperature > 0 ? (DgopService.cpuTemperature.toFixed(0) + "°") : ""
+ accentColor: DgopService.cpuUsage > 80 ? Theme.error : (DgopService.cpuUsage > 50 ? Theme.warning : Theme.primary)
+ detailColor: DgopService.cpuTemperature > 85 ? Theme.error : (DgopService.cpuTemperature > 70 ? Theme.warning : Theme.surfaceVariantText)
+ }
+
+ CircleGauge {
+ width: statsContainer.gaugeSize
+ height: statsContainer.gaugeSize
+ value: DgopService.memoryUsage / 100
+ label: statsContainer.compactMem(DgopService.usedMemoryKB)
+ sublabel: I18n.tr("Memory")
+ detail: DgopService.totalSwapKB > 0 ? ("+" + statsContainer.compactMem(DgopService.usedSwapKB)) : ""
+ accentColor: DgopService.memoryUsage > 90 ? Theme.error : (DgopService.memoryUsage > 70 ? Theme.warning : Theme.secondary)
+ }
+
+ CircleGauge {
+ width: statsContainer.gaugeSize
+ height: statsContainer.gaugeSize
+ visible: statsContainer.hasGpu
+
+ readonly property var gpu: statsContainer.enabledGpusWithTemp[0] ?? null
+ readonly property color vendorColor: {
+ const vendor = (gpu?.vendor ?? "").toLowerCase();
+ if (vendor.includes("nvidia"))
+ return Theme.success;
+ if (vendor.includes("amd"))
+ return Theme.error;
+ if (vendor.includes("intel"))
+ return Theme.info;
+ return Theme.info;
+ }
+
+ value: Math.min(1, (gpu?.temperature ?? 0) / 100)
+ label: (gpu?.temperature ?? 0) > 0 ? ((gpu?.temperature ?? 0).toFixed(0) + "°C") : "--"
+ sublabel: "GPU"
+ accentColor: {
+ const temp = gpu?.temperature ?? 0;
+ if (temp > 85)
+ return Theme.error;
+ if (temp > 70)
+ return Theme.warning;
+ return vendorColor;
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ clip: true
+
+ ProcessesView {
+ id: processesView
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ searchText: processListPopout.searchText
+ expandedPid: processListPopout.expandedPid
+ processFilter: processListPopout.processFilter
+ contextMenu: processContextMenu
+ onExpandedPidChanged: processListPopout.expandedPid = expandedPid
+ }
+ }
+ }
+ }
+ }
+
+ component CircleGauge: Item {
+ id: gaugeRoot
+
+ property real value: 0
+ property string label: ""
+ property string sublabel: ""
+ property string detail: ""
+ property color accentColor: Theme.primary
+ property color detailColor: Theme.surfaceVariantText
+
+ readonly property real thickness: Math.max(4, Math.min(width, height) / 15)
+ readonly property real glowExtra: thickness * 1.4
+ readonly property real arcPadding: (thickness + glowExtra) / 2
+
+ readonly property real innerDiameter: width - (arcPadding + thickness + glowExtra) * 2
+ readonly property real maxTextWidth: innerDiameter * 0.9
+ readonly property real baseLabelSize: Math.round(width * 0.18)
+ readonly property real labelSize: Math.round(Math.min(baseLabelSize, maxTextWidth / Math.max(1, label.length * 0.65)))
+ readonly property real sublabelSize: Math.round(Math.min(width * 0.13, maxTextWidth / Math.max(1, sublabel.length * 0.7)))
+ readonly property real detailSize: Math.round(Math.min(width * 0.12, maxTextWidth / Math.max(1, detail.length * 0.65)))
+
+ property real animValue: 0
+
+ onValueChanged: animValue = Math.min(1, Math.max(0, value))
+
+ Behavior on animValue {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ Component.onCompleted: animValue = Math.min(1, Math.max(0, value))
+
+ Canvas {
+ id: glowCanvas
+ anchors.fill: parent
+ onPaint: {
+ const ctx = getContext("2d");
+ ctx.reset();
+ const cx = width / 2;
+ const cy = height / 2;
+ const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
+ const startAngle = -Math.PI * 0.5;
+ const endAngle = Math.PI * 1.5;
+
+ ctx.lineCap = "round";
+
+ if (gaugeRoot.animValue > 0) {
+ const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
+ ctx.beginPath();
+ ctx.arc(cx, cy, radius, startAngle, prog);
+ ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.2);
+ ctx.lineWidth = gaugeRoot.thickness + gaugeRoot.glowExtra;
+ ctx.stroke();
+ }
+ }
+
+ Connections {
+ target: gaugeRoot
+ function onAnimValueChanged() {
+ glowCanvas.requestPaint();
+ }
+ function onAccentColorChanged() {
+ glowCanvas.requestPaint();
+ }
+ function onWidthChanged() {
+ glowCanvas.requestPaint();
+ }
+ function onHeightChanged() {
+ glowCanvas.requestPaint();
+ }
+ }
+
+ Component.onCompleted: requestPaint()
+ }
+
+ Canvas {
+ id: arcCanvas
+ anchors.fill: parent
+ onPaint: {
+ const ctx = getContext("2d");
+ ctx.reset();
+ const cx = width / 2;
+ const cy = height / 2;
+ const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding;
+ const startAngle = -Math.PI * 0.5;
+ const endAngle = Math.PI * 1.5;
+
+ ctx.lineCap = "round";
+
+ ctx.beginPath();
+ ctx.arc(cx, cy, radius, startAngle, endAngle);
+ ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.1);
+ ctx.lineWidth = gaugeRoot.thickness;
+ ctx.stroke();
+
+ if (gaugeRoot.animValue > 0) {
+ const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue;
+ ctx.beginPath();
+ ctx.arc(cx, cy, radius, startAngle, prog);
+ ctx.strokeStyle = gaugeRoot.accentColor;
+ ctx.lineWidth = gaugeRoot.thickness;
+ ctx.stroke();
+ }
+ }
+
+ Connections {
+ target: gaugeRoot
+ function onAnimValueChanged() {
+ arcCanvas.requestPaint();
+ }
+ function onAccentColorChanged() {
+ arcCanvas.requestPaint();
+ }
+ function onWidthChanged() {
+ arcCanvas.requestPaint();
+ }
+ function onHeightChanged() {
+ arcCanvas.requestPaint();
+ }
+ }
+
+ Component.onCompleted: requestPaint()
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: 1
+
+ StyledText {
+ text: gaugeRoot.label
+ font.pixelSize: gaugeRoot.labelSize
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: gaugeRoot.sublabel
+ font.pixelSize: gaugeRoot.sublabelSize
+ font.weight: Font.Medium
+ color: gaugeRoot.accentColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: gaugeRoot.detail
+ font.pixelSize: gaugeRoot.detailSize
+ font.family: SettingsData.monoFontFamily
+ color: gaugeRoot.detailColor
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: gaugeRoot.detail.length > 0
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessesView.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessesView.qml
new file mode 100644
index 0000000..181cf15
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/ProcessesView.qml
@@ -0,0 +1,769 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property string searchText: ""
+ property string expandedPid: ""
+ property var contextMenu: null
+ property string processFilter: "all" // "all", "user", "system"
+
+ property int selectedIndex: -1
+ property bool keyboardNavigationActive: false
+ property int forceRefreshCount: 0
+
+ readonly property bool pauseUpdates: (contextMenu?.visible ?? false) || expandedPid.length > 0
+ readonly property bool shouldUpdate: !pauseUpdates || forceRefreshCount > 0
+ property var cachedProcesses: []
+
+ signal openContextMenuRequested(int index, real x, real y, bool fromKeyboard)
+
+ onFilteredProcessesChanged: {
+ if (!shouldUpdate)
+ return;
+ cachedProcesses = filteredProcesses;
+ if (forceRefreshCount > 0)
+ forceRefreshCount--;
+ }
+
+ onShouldUpdateChanged: {
+ if (shouldUpdate)
+ cachedProcesses = filteredProcesses;
+ }
+
+ readonly property var filteredProcesses: {
+ if (!DgopService.allProcesses || DgopService.allProcesses.length === 0)
+ return [];
+
+ let procs = DgopService.allProcesses.slice();
+
+ if (processFilter === "user") {
+ procs = procs.filter(p => p.username === UserInfoService.username);
+ } else if (processFilter === "system") {
+ procs = procs.filter(p => p.username !== UserInfoService.username);
+ }
+
+ if (searchText.length > 0) {
+ const search = searchText.toLowerCase();
+ procs = procs.filter(p => {
+ const cmd = (p.command || "").toLowerCase();
+ const fullCmd = (p.fullCommand || "").toLowerCase();
+ const pid = p.pid.toString();
+ return cmd.includes(search) || fullCmd.includes(search) || pid.includes(search);
+ });
+ }
+
+ const asc = DgopService.sortAscending;
+ procs.sort((a, b) => {
+ let valueA, valueB, result;
+ switch (DgopService.currentSort) {
+ case "cpu":
+ valueA = a.cpu || 0;
+ valueB = b.cpu || 0;
+ result = valueB - valueA;
+ break;
+ case "memory":
+ valueA = a.memoryKB || 0;
+ valueB = b.memoryKB || 0;
+ result = valueB - valueA;
+ break;
+ case "name":
+ valueA = (a.command || "").toLowerCase();
+ valueB = (b.command || "").toLowerCase();
+ result = valueA.localeCompare(valueB);
+ break;
+ case "pid":
+ valueA = a.pid || 0;
+ valueB = b.pid || 0;
+ result = valueA - valueB;
+ break;
+ default:
+ return 0;
+ }
+ return asc ? -result : result;
+ });
+
+ return procs;
+ }
+
+ function selectNext() {
+ if (cachedProcesses.length === 0)
+ return;
+ keyboardNavigationActive = true;
+ selectedIndex = Math.min(selectedIndex + 1, cachedProcesses.length - 1);
+ ensureVisible();
+ }
+
+ function selectPrevious() {
+ if (cachedProcesses.length === 0)
+ return;
+ keyboardNavigationActive = true;
+ if (selectedIndex <= 0) {
+ selectedIndex = -1;
+ keyboardNavigationActive = false;
+ return;
+ }
+ selectedIndex = selectedIndex - 1;
+ ensureVisible();
+ }
+
+ function selectFirst() {
+ if (cachedProcesses.length === 0)
+ return;
+ keyboardNavigationActive = true;
+ selectedIndex = 0;
+ ensureVisible();
+ }
+
+ function selectLast() {
+ if (cachedProcesses.length === 0)
+ return;
+ keyboardNavigationActive = true;
+ selectedIndex = cachedProcesses.length - 1;
+ ensureVisible();
+ }
+
+ function toggleExpand() {
+ if (selectedIndex < 0 || selectedIndex >= cachedProcesses.length)
+ return;
+ const process = cachedProcesses[selectedIndex];
+ const pidStr = (process?.pid ?? -1).toString();
+ expandedPid = (expandedPid === pidStr) ? "" : pidStr;
+ }
+
+ function openContextMenu() {
+ if (selectedIndex < 0 || selectedIndex >= cachedProcesses.length)
+ return;
+ const delegate = processListView.itemAtIndex(selectedIndex);
+ if (!delegate)
+ return;
+ const process = cachedProcesses[selectedIndex];
+ if (!process || !contextMenu)
+ return;
+ contextMenu.processData = process;
+ const itemPos = delegate.mapToItem(contextMenu.parent, delegate.width / 2, delegate.height / 2);
+ contextMenu.parentFocusItem = root;
+ contextMenu.show(itemPos.x, itemPos.y, true);
+ }
+
+ function reset() {
+ selectedIndex = -1;
+ keyboardNavigationActive = false;
+ expandedPid = "";
+ }
+
+ function forceRefresh(count) {
+ forceRefreshCount = count || 3;
+ }
+
+ function ensureVisible() {
+ if (selectedIndex < 0)
+ return;
+ processListView.positionViewAtIndex(selectedIndex, ListView.Contain);
+ }
+
+ function handleKey(event) {
+ switch (event.key) {
+ case Qt.Key_Down:
+ selectNext();
+ event.accepted = true;
+ return;
+ case Qt.Key_Up:
+ selectPrevious();
+ event.accepted = true;
+ return;
+ case Qt.Key_J:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectNext();
+ event.accepted = true;
+ }
+ return;
+ case Qt.Key_K:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectPrevious();
+ event.accepted = true;
+ }
+ return;
+ case Qt.Key_Home:
+ selectFirst();
+ event.accepted = true;
+ return;
+ case Qt.Key_End:
+ selectLast();
+ event.accepted = true;
+ return;
+ case Qt.Key_Space:
+ if (keyboardNavigationActive) {
+ toggleExpand();
+ event.accepted = true;
+ }
+ return;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (keyboardNavigationActive) {
+ toggleExpand();
+ event.accepted = true;
+ }
+ return;
+ case Qt.Key_Menu:
+ case Qt.Key_F10:
+ if (keyboardNavigationActive && selectedIndex >= 0) {
+ openContextMenu();
+ event.accepted = true;
+ }
+ return;
+ }
+ }
+
+ Component.onCompleted: {
+ DgopService.addRef(["processes", "cpu", "memory", "system"]);
+ cachedProcesses = filteredProcesses;
+ }
+
+ Component.onDestruction: {
+ DgopService.removeRef(["processes", "cpu", "memory", "system"]);
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 36
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: 0
+
+ SortableHeader {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 200
+ text: I18n.tr("Name")
+ sortKey: "name"
+ currentSort: DgopService.currentSort
+ sortAscending: DgopService.sortAscending
+ onClicked: DgopService.toggleSort("name")
+ alignment: Text.AlignLeft
+ }
+
+ SortableHeader {
+ Layout.preferredWidth: 100
+ text: "CPU"
+ sortKey: "cpu"
+ currentSort: DgopService.currentSort
+ sortAscending: DgopService.sortAscending
+ onClicked: DgopService.toggleSort("cpu")
+ }
+
+ SortableHeader {
+ Layout.preferredWidth: 100
+ text: I18n.tr("Memory")
+ sortKey: "memory"
+ currentSort: DgopService.currentSort
+ sortAscending: DgopService.sortAscending
+ onClicked: DgopService.toggleSort("memory")
+ }
+
+ SortableHeader {
+ Layout.preferredWidth: 80
+ text: "PID"
+ sortKey: "pid"
+ currentSort: DgopService.currentSort
+ sortAscending: DgopService.sortAscending
+ onClicked: DgopService.toggleSort("pid")
+ }
+
+ Item {
+ Layout.preferredWidth: 40
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Theme.outlineLight
+ }
+
+ DankListView {
+ id: processListView
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ spacing: 2
+
+ add: root.searchText.length > 0 ? ListViewTransitions.add : null
+ remove: root.searchText.length > 0 ? ListViewTransitions.remove : null
+ displaced: root.searchText.length > 0 ? ListViewTransitions.displaced : null
+ move: root.searchText.length > 0 ? ListViewTransitions.move : null
+
+ model: ScriptModel {
+ values: root.cachedProcesses
+ objectProp: "pid"
+ }
+
+ delegate: ProcessItem {
+ required property var modelData
+ required property int index
+
+ width: processListView.width
+ process: modelData
+ isExpanded: root.expandedPid === (modelData?.pid ?? -1).toString()
+ isSelected: root.keyboardNavigationActive && root.selectedIndex === index
+ contextMenu: root.contextMenu
+ onToggleExpand: {
+ const pidStr = (modelData?.pid ?? -1).toString();
+ root.expandedPid = (root.expandedPid === pidStr) ? "" : pidStr;
+ }
+ onClicked: {
+ root.keyboardNavigationActive = true;
+ root.selectedIndex = index;
+ }
+ onContextMenuRequested: (mouseX, mouseY) => {
+ if (root.contextMenu) {
+ root.contextMenu.processData = modelData;
+ root.contextMenu.parentFocusItem = root;
+ const globalPos = mapToItem(root.contextMenu.parent, mouseX, mouseY);
+ root.contextMenu.show(globalPos.x, globalPos.y, false);
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 300
+ height: 100
+ radius: Theme.cornerRadius
+ color: "transparent"
+ visible: root.cachedProcesses.length === 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: root.searchText.length > 0 ? "search_off" : "hourglass_empty"
+ size: 32
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("No matching processes", "empty state in process list")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ visible: root.searchText.length > 0
+ }
+ }
+ }
+ }
+ }
+
+ component SortableHeader: Item {
+ id: headerItem
+
+ property string text: ""
+ property string sortKey: ""
+ property string currentSort: ""
+ property bool sortAscending: false
+ property int alignment: Text.AlignHCenter
+
+ signal clicked
+
+ readonly property bool isActive: sortKey === currentSort
+
+ height: 36
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.margins: 2
+ radius: Theme.cornerRadius
+ color: headerItem.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (headerMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06) : "transparent")
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: 4
+
+ Item {
+ Layout.fillWidth: headerItem.alignment === Text.AlignLeft
+ visible: headerItem.alignment !== Text.AlignLeft
+ }
+
+ StyledText {
+ text: headerItem.text
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: headerItem.isActive ? Font.Bold : Font.Medium
+ color: headerItem.isActive ? Theme.primary : Theme.surfaceText
+ opacity: headerItem.isActive ? 1 : 0.8
+ }
+
+ DankIcon {
+ name: headerItem.sortAscending ? "arrow_upward" : "arrow_downward"
+ size: Theme.fontSizeSmall
+ color: Theme.primary
+ visible: headerItem.isActive
+ }
+
+ Item {
+ Layout.fillWidth: headerItem.alignment !== Text.AlignLeft
+ visible: headerItem.alignment === Text.AlignLeft
+ }
+ }
+
+ MouseArea {
+ id: headerMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: headerItem.clicked()
+ }
+ }
+
+ component ProcessItem: Rectangle {
+ id: processItemRoot
+
+ property var process: null
+ property bool isExpanded: false
+ property bool isSelected: false
+ property var contextMenu: null
+
+ signal toggleExpand
+ signal clicked
+ signal contextMenuRequested(real mouseX, real mouseY)
+
+ readonly property int processPid: process?.pid ?? 0
+ readonly property real processCpu: process?.cpu ?? 0
+ readonly property int processMemKB: process?.memoryKB ?? 0
+ readonly property string processCmd: process?.command ?? ""
+ readonly property string processFullCmd: process?.fullCommand ?? processCmd
+
+ height: isExpanded ? (44 + expandedRect.height + Theme.spacingXS) : 44
+ radius: Theme.cornerRadius
+ color: {
+ if (isSelected)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
+ return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent";
+ }
+ border.color: {
+ if (isSelected)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
+ return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
+ }
+ border.width: 1
+ clip: true
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+
+ MouseArea {
+ id: processMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ processItemRoot.contextMenuRequested(mouse.x, mouse.y);
+ return;
+ }
+ processItemRoot.clicked();
+ processItemRoot.toggleExpand();
+ }
+ }
+
+ Column {
+ anchors.fill: parent
+ spacing: 0
+
+ Item {
+ width: parent.width
+ height: 44
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: 0
+
+ Item {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 200
+ height: parent.height
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: DgopService.getProcessIcon(processItemRoot.processCmd)
+ size: Theme.iconSize - 4
+ color: {
+ if (processItemRoot.processCpu > 80)
+ return Theme.error;
+ if (processItemRoot.processCpu > 50)
+ return Theme.warning;
+ return Theme.surfaceText;
+ }
+ opacity: 0.8
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: processItemRoot.processCmd
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ width: Math.min(implicitWidth, 280)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ Item {
+ Layout.preferredWidth: 100
+ height: parent.height
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 70
+ height: 24
+ radius: Theme.cornerRadius
+ color: {
+ if (processItemRoot.processCpu > 80)
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
+ if (processItemRoot.processCpu > 50)
+ return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
+ return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ text: DgopService.formatCpuUsage(processItemRoot.processCpu)
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: {
+ if (processItemRoot.processCpu > 80)
+ return Theme.error;
+ if (processItemRoot.processCpu > 50)
+ return Theme.warning;
+ return Theme.surfaceText;
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.preferredWidth: 100
+ height: parent.height
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 70
+ height: 24
+ radius: Theme.cornerRadius
+ color: {
+ if (processItemRoot.processMemKB > 2 * 1024 * 1024)
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15);
+ if (processItemRoot.processMemKB > 1024 * 1024)
+ return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
+ return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06);
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ text: DgopService.formatMemoryUsage(processItemRoot.processMemKB)
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: {
+ if (processItemRoot.processMemKB > 2 * 1024 * 1024)
+ return Theme.error;
+ if (processItemRoot.processMemKB > 1024 * 1024)
+ return Theme.warning;
+ return Theme.surfaceText;
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.preferredWidth: 80
+ height: parent.height
+
+ StyledText {
+ anchors.centerIn: parent
+ text: processItemRoot.processPid > 0 ? processItemRoot.processPid.toString() : ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ Item {
+ Layout.preferredWidth: 40
+ height: parent.height
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: processItemRoot.isExpanded ? "expand_less" : "expand_more"
+ size: Theme.iconSize - 4
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: expandedRect
+ width: parent.width - Theme.spacingM * 2
+ height: processItemRoot.isExpanded ? (expandedContent.implicitHeight + Theme.spacingS * 2) : 0
+ anchors.horizontalCenter: parent.horizontalCenter
+ radius: Theme.cornerRadius - 2
+ color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.6)
+ clip: true
+ visible: processItemRoot.isExpanded
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Column {
+ id: expandedContent
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingXS
+
+ RowLayout {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ StyledText {
+ id: cmdLabel
+ text: I18n.tr("Full Command:", "process detail label")
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.weight: Font.Bold
+ color: Theme.surfaceVariantText
+ Layout.alignment: Qt.AlignVCenter
+ }
+
+ StyledText {
+ id: cmdText
+ text: processItemRoot.processFullCmd
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceText
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
+ elide: Text.ElideMiddle
+ }
+
+ Rectangle {
+ id: copyBtn
+ Layout.preferredWidth: 24
+ Layout.preferredHeight: 24
+ Layout.alignment: Qt.AlignVCenter
+ radius: Theme.cornerRadius - 2
+ color: copyMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "content_copy"
+ size: 14
+ color: copyMouseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: copyMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ Quickshell.execDetached(["dms", "cl", "copy", processItemRoot.processFullCmd]);
+ }
+ }
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingL
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: "PPID:"
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.weight: Font.Bold
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: (processItemRoot.process?.ppid ?? 0) > 0 ? processItemRoot.process.ppid.toString() : "--"
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceText
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: "Mem:"
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.weight: Font.Bold
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: (processItemRoot.process?.memoryPercent ?? 0).toFixed(1) + "%"
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceText
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/SystemView.qml b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/SystemView.qml
new file mode 100644
index 0000000..244129d
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/ProcessList/SystemView.qml
@@ -0,0 +1,386 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ Component.onCompleted: {
+ DgopService.addRef(["system", "cpu"]);
+ }
+
+ Component.onDestruction: {
+ DgopService.removeRef(["system", "cpu"]);
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: Theme.spacingM
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: systemInfoColumn.implicitHeight + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ ColumnLayout {
+ id: systemInfoColumn
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Row {
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "computer"
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("System Information", "system info header in system monitor")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 2
+ rowSpacing: Theme.spacingS
+ columnSpacing: Theme.spacingXL
+
+ InfoRow {
+ label: I18n.tr("Hostname", "system info label")
+ value: DgopService.hostname || "--"
+ }
+ InfoRow {
+ label: I18n.tr("Distribution", "system info label")
+ value: DgopService.distribution || "--"
+ }
+ InfoRow {
+ label: I18n.tr("Kernel", "system info label")
+ value: DgopService.kernelVersion || "--"
+ }
+ InfoRow {
+ label: I18n.tr("Architecture", "system info label")
+ value: DgopService.architecture || "--"
+ }
+ InfoRow {
+ label: I18n.tr("CPU")
+ value: DgopService.cpuModel || ("" + DgopService.cpuCores + " cores")
+ }
+ InfoRow {
+ label: I18n.tr("Uptime")
+ value: DgopService.uptime || "--"
+ }
+ InfoRow {
+ label: I18n.tr("Load Average", "system info label")
+ value: DgopService.loadAverage || "--"
+ }
+ InfoRow {
+ label: I18n.tr("Processes")
+ value: DgopService.processCount > 0 ? DgopService.processCount.toString() : "--"
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Row {
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "developer_board"
+ size: Theme.iconSize
+ color: Theme.secondary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("GPU Monitoring", "gpu section header in system monitor")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Theme.outlineLight
+ }
+
+ DankListView {
+ id: gpuListView
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ spacing: 8
+
+ model: DgopService.availableGpus
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: gpuListView.width
+ height: 80
+ radius: Theme.cornerRadius
+ color: {
+ const vendor = (modelData?.vendor ?? "").toLowerCase();
+ if (vendor.includes("nvidia"))
+ return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.08);
+ if (vendor.includes("amd"))
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
+ if (vendor.includes("intel"))
+ return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.08);
+ return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
+ }
+ border.color: {
+ const vendor = (modelData?.vendor ?? "").toLowerCase();
+ if (vendor.includes("nvidia"))
+ return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2);
+ if (vendor.includes("amd"))
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2);
+ if (vendor.includes("intel"))
+ return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2);
+ return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2);
+ }
+ border.width: 1
+
+ readonly property bool tempEnabled: {
+ const pciId = modelData?.pciId ?? "";
+ if (!pciId)
+ return false;
+ return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(pciId) !== -1 : false;
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "developer_board"
+ size: Theme.iconSize + 4
+ color: {
+ const vendor = (modelData?.vendor ?? "").toLowerCase();
+ if (vendor.includes("nvidia"))
+ return Theme.success;
+ if (vendor.includes("amd"))
+ return Theme.error;
+ if (vendor.includes("intel"))
+ return Theme.info;
+ return Theme.surfaceVariantText;
+ }
+ }
+
+ Column {
+ Layout.fillWidth: true
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: modelData?.displayName ?? I18n.tr("Unknown GPU", "fallback gpu name")
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+
+ Row {
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: modelData?.vendor ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: "•"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: (modelData?.driver ?? "").length > 0
+ }
+
+ StyledText {
+ text: modelData?.driver ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: (modelData?.driver ?? "").length > 0
+ }
+ }
+
+ StyledText {
+ text: modelData?.pciId ?? ""
+ font.pixelSize: Theme.fontSizeSmall - 2
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceVariantText
+ opacity: 0.7
+ }
+ }
+
+ Rectangle {
+ width: 70
+ height: 32
+ radius: Theme.cornerRadius
+ color: parent.parent.tempEnabled ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
+ border.color: tempMouseArea.containsMouse ? Theme.outline : "transparent"
+ border.width: 1
+
+ Row {
+ id: tempRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "thermostat"
+ size: 16
+ color: {
+ if (!parent.parent.parent.parent.tempEnabled)
+ return Theme.surfaceVariantText;
+ const temp = modelData?.temperature ?? 0;
+ if (temp > 85)
+ return Theme.error;
+ if (temp > 70)
+ return Theme.warning;
+ return Theme.surfaceText;
+ }
+ opacity: parent.parent.parent.parent.tempEnabled ? 1 : 0.5
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: {
+ if (!parent.parent.parent.parent.tempEnabled)
+ return I18n.tr("Off");
+ const temp = modelData?.temperature ?? 0;
+ return temp > 0 ? (temp.toFixed(0) + "°C") : "--";
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: parent.parent.parent.parent.tempEnabled ? SettingsData.monoFontFamily : ""
+ font.weight: parent.parent.parent.parent.tempEnabled ? Font.Bold : Font.Normal
+ color: {
+ if (!parent.parent.parent.parent.tempEnabled)
+ return Theme.surfaceVariantText;
+ const temp = modelData?.temperature ?? 0;
+ if (temp > 85)
+ return Theme.error;
+ if (temp > 70)
+ return Theme.warning;
+ return Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: tempMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ const pciId = modelData?.pciId;
+ if (!pciId)
+ return;
+
+ const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : [];
+ const idx = enabledIds.indexOf(pciId);
+ const wasEnabled = idx !== -1;
+
+ if (!wasEnabled) {
+ enabledIds.push(pciId);
+ DgopService.addGpuPciId(pciId);
+ } else {
+ enabledIds.splice(idx, 1);
+ DgopService.removeGpuPciId(pciId);
+ }
+
+ SessionData.setEnabledGpuPciIds(enabledIds);
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 300
+ height: 100
+ radius: Theme.cornerRadius
+ color: "transparent"
+ visible: DgopService.availableGpus.length === 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "developer_board_off"
+ size: 32
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("No GPUs detected", "empty state in gpu list")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ component InfoRow: RowLayout {
+ property string label: ""
+ property string value: ""
+
+ Layout.fillWidth: true
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: label + ":"
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: 100
+ }
+
+ StyledText {
+ text: value
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceText
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ }
+ }
+}