From 76ac036f85cd3b6b82036b0a00149a1ef6e9e209 Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 16 Jan 2026 20:20:03 -0500 Subject: [PATCH] system monitor: overhaul popout and app with new design --- quickshell/Modals/ProcessListModal.qml | 459 ++++++++----- quickshell/Modules/ProcessList/DisksView.qml | 346 ++++++++++ .../Modules/ProcessList/PerformanceTab.qml | 451 ------------- .../Modules/ProcessList/PerformanceView.qml | 304 +++++++++ .../ProcessList/ProcessContextMenu.qml | 264 ++++---- .../Modules/ProcessList/ProcessListItem.qml | 200 ------ .../Modules/ProcessList/ProcessListPopout.qml | 417 +++++++++++- .../Modules/ProcessList/ProcessListView.qml | 264 -------- .../Modules/ProcessList/ProcessesTab.qml | 28 - .../Modules/ProcessList/ProcessesView.qml | 607 ++++++++++++++++++ .../Modules/ProcessList/SystemOverview.qml | 435 ------------- quickshell/Modules/ProcessList/SystemTab.qml | 591 ----------------- quickshell/Modules/ProcessList/SystemView.qml | 386 +++++++++++ quickshell/Services/DgopService.qml | 49 +- 14 files changed, 2460 insertions(+), 2341 deletions(-) create mode 100644 quickshell/Modules/ProcessList/DisksView.qml delete mode 100644 quickshell/Modules/ProcessList/PerformanceTab.qml create mode 100644 quickshell/Modules/ProcessList/PerformanceView.qml delete mode 100644 quickshell/Modules/ProcessList/ProcessListItem.qml delete mode 100644 quickshell/Modules/ProcessList/ProcessListView.qml delete mode 100644 quickshell/Modules/ProcessList/ProcessesTab.qml create mode 100644 quickshell/Modules/ProcessList/ProcessesView.qml delete mode 100644 quickshell/Modules/ProcessList/SystemOverview.qml delete mode 100644 quickshell/Modules/ProcessList/SystemTab.qml create mode 100644 quickshell/Modules/ProcessList/SystemView.qml diff --git a/quickshell/Modals/ProcessListModal.qml b/quickshell/Modals/ProcessListModal.qml index 8dbb35ea..1e8d9f6f 100644 --- a/quickshell/Modals/ProcessListModal.qml +++ b/quickshell/Modals/ProcessListModal.qml @@ -11,7 +11,8 @@ FloatingWindow { id: processListModal property int currentTab: 0 - property var tabNames: ["Processes", "Performance", "System"] + property string searchText: "" + property string expandedPid: "" property bool shouldHaveFocus: visible property alias shouldBeVisible: processListModal.visible @@ -27,9 +28,8 @@ FloatingWindow { function hide() { visible = false; - if (processContextMenu.visible) { + if (processContextMenu.visible) processContextMenu.close(); - } } function toggle() { @@ -61,46 +61,39 @@ FloatingWindow { show(); } + 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"; + } + objectName: "processListModal" title: I18n.tr("System Monitor", "sysmon window title") - minimumSize: Qt.size(650, 400) - implicitWidth: 900 - implicitHeight: 680 + minimumSize: Qt.size(750, 550) + implicitWidth: 1000 + implicitHeight: 720 color: Theme.surfaceContainer visible: false onVisibleChanged: { if (!visible) { closingModal(); + searchText = ""; + expandedPid = ""; + DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]); } else { + DgopService.addRef(["cpu", "memory", "network", "disk", "system"]); Qt.callLater(() => { - if (contentFocusScope) { + if (contentFocusScope) contentFocusScope.forceActiveFocus(); - } }); } } - Component { - id: processesTabComponent - - ProcessesTab { - contextMenu: processContextMenu - } - } - - Component { - id: performanceTabComponent - - PerformanceTab {} - } - - Component { - id: systemTabComponent - - SystemTab {} - } - ProcessContextMenu { id: processContextMenu } @@ -128,6 +121,26 @@ FloatingWindow { currentTab = 2; event.accepted = true; return; + case Qt.Key_4: + currentTab = 3; + event.accepted = true; + return; + case Qt.Key_Escape: + if (searchText.length > 0) { + searchText = ""; + event.accepted = true; + return; + } + hide(); + event.accepted = true; + return; + case Qt.Key_F: + if (event.modifiers & Qt.ControlModifier) { + searchField.forceActiveFocus(); + event.accepted = true; + return; + } + break; } } @@ -161,7 +174,7 @@ FloatingWindow { } StyledText { - text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.") + text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.", "dgop unavailable error message") font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText anchors.horizontalCenter: parent.horizontalCenter @@ -171,14 +184,14 @@ FloatingWindow { } } - Column { + ColumnLayout { anchors.fill: parent spacing: 0 visible: DgopService.dgopAvailable Item { - width: parent.width - height: 48 + Layout.fillWidth: true + Layout.preferredHeight: 48 MouseArea { anchors.fill: parent @@ -233,166 +246,276 @@ FloatingWindow { } } - Item { - width: parent.width - height: parent.height - 48 + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 52 + Layout.leftMargin: Theme.spacingL + Layout.rightMargin: Theme.spacingL + spacing: Theme.spacingL - ColumnLayout { - anchors.fill: parent - anchors.leftMargin: Theme.spacingL - anchors.rightMargin: Theme.spacingL - anchors.bottomMargin: Theme.spacingL - anchors.topMargin: 0 - spacing: Theme.spacingL + Row { + spacing: 2 - Rectangle { - Layout.fillWidth: true - height: 52 - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - radius: Theme.cornerRadius - border.color: Theme.outlineLight - border.width: 1 + Repeater { + model: [ + { + text: I18n.tr("Processes"), + icon: "list_alt" + }, + { + text: I18n.tr("Performance"), + icon: "analytics" + }, + { + text: I18n.tr("Disks"), + icon: "storage" + }, + { + text: I18n.tr("System"), + icon: "computer" + } + ] - Row { - anchors.fill: parent - anchors.margins: 4 - spacing: 2 + Rectangle { + width: 120 + height: 44 + radius: Theme.cornerRadius + color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent") + border.color: currentTab === index ? Theme.primary : "transparent" + border.width: currentTab === index ? 1 : 0 - Repeater { - model: tabNames + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS - Rectangle { - width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length - height: 44 - radius: Theme.cornerRadius - color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent") - border.color: currentTab === index ? Theme.primary : "transparent" - border.width: currentTab === index ? 1 : 0 + DankIcon { + name: modelData.icon + size: Theme.iconSize - 2 + color: currentTab === index ? Theme.primary : Theme.surfaceText + opacity: currentTab === index ? 1 : 0.7 + anchors.verticalCenter: parent.verticalCenter + } - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS + StyledText { + text: modelData.text + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: currentTab === index ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } - DankIcon { - name: { - const tabIcons = ["list_alt", "analytics", "settings"]; - return tabIcons[index] || "tab"; - } - size: Theme.iconSize - 2 - color: currentTab === index ? Theme.primary : Theme.surfaceText - opacity: currentTab === index ? 1 : 0.7 - anchors.verticalCenter: parent.verticalCenter + MouseArea { + id: tabMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: currentTab = index + } - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - } - } - - StyledText { - text: modelData - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Medium - color: currentTab === index ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: -1 - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - } - } - } - - MouseArea { - id: tabMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: () => { - currentTab = index; - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - } - - Behavior on border.color { - ColorAnimation { - duration: Theme.shortDuration - } - } + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration } } } } + } - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: Theme.outlineLight - border.width: 1 + Item { + Layout.fillWidth: true + } - Loader { - id: processesTab + DankTextField { + id: searchField + Layout.preferredWidth: 250 + Layout.preferredHeight: 40 + placeholderText: I18n.tr("Search processes...", "process search placeholder") + leftIconName: "search" + showClearButton: true + text: searchText + visible: currentTab === 0 + onTextChanged: searchText = text + } + } - anchors.fill: parent - anchors.margins: Theme.spacingS - active: processListModal.visible && currentTab === 0 - visible: currentTab === 0 - opacity: currentTab === 0 ? 1 : 0 - sourceComponent: processesTabComponent + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: Theme.spacingL + Layout.topMargin: Theme.spacingM + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: Theme.outlineLight + border.width: 1 + clip: true - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } + Loader { + id: processesTabLoader + anchors.fill: parent + anchors.margins: Theme.spacingS + active: processListModal.visible && currentTab === 0 + visible: currentTab === 0 + sourceComponent: ProcessesView { + searchText: processListModal.searchText + expandedPid: processListModal.expandedPid + contextMenu: processContextMenu + onExpandedPidChanged: processListModal.expandedPid = expandedPid + } + } + + Loader { + id: performanceTabLoader + anchors.fill: parent + anchors.margins: Theme.spacingS + active: processListModal.visible && currentTab === 1 + visible: currentTab === 1 + sourceComponent: PerformanceView {} + } + + Loader { + id: disksTabLoader + anchors.fill: parent + anchors.margins: Theme.spacingS + active: processListModal.visible && currentTab === 2 + visible: currentTab === 2 + sourceComponent: DisksView {} + } + + Loader { + id: systemTabLoader + anchors.fill: parent + anchors.margins: Theme.spacingS + active: processListModal.visible && currentTab === 3 + visible: currentTab === 3 + sourceComponent: SystemView {} + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 32 + Layout.leftMargin: Theme.spacingL + Layout.rightMargin: Theme.spacingL + Layout.bottomMargin: Theme.spacingM + color: "transparent" + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingL + + Row { + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Processes:", "process count label in footer") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText } - Loader { - id: performanceTab + StyledText { + text: DgopService.processCount.toString() + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + } - anchors.fill: parent - anchors.margins: Theme.spacingS - active: processListModal.visible && currentTab === 1 - visible: currentTab === 1 - opacity: currentTab === 1 ? 1 : 0 - sourceComponent: performanceTabComponent + Row { + spacing: Theme.spacingXS - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } + StyledText { + text: I18n.tr("Uptime:", "uptime label in footer") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText } - Loader { - id: systemTab + StyledText { + text: DgopService.shortUptime || "--" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + } + } - anchors.fill: parent - anchors.margins: Theme.spacingS - active: processListModal.visible && currentTab === 2 - visible: currentTab === 2 - opacity: currentTab === 2 ? 1 : 0 - sourceComponent: systemTabComponent + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingL - Behavior on opacity { - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } + Row { + spacing: Theme.spacingXS + + DankIcon { + name: "swap_horiz" + size: 14 + color: Theme.info + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate) + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + } + } + + Row { + spacing: Theme.spacingXS + + DankIcon { + name: "storage" + size: 14 + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: "↓" + formatBytes(DgopService.diskReadRate) + " ↑" + formatBytes(DgopService.diskWriteRate) + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + } + } + + Row { + spacing: Theme.spacingXS + + DankIcon { + name: "memory" + size: 14 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: DgopService.cpuUsage.toFixed(1) + "%" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: DgopService.cpuUsage > 80 ? Theme.error : Theme.surfaceText + } + } + + Row { + spacing: Theme.spacingXS + + DankIcon { + name: "sd_card" + size: 14 + color: Theme.secondary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB) + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: DgopService.memoryUsage > 90 ? Theme.error : Theme.surfaceText } } } diff --git a/quickshell/Modules/ProcessList/DisksView.qml b/quickshell/Modules/ProcessList/DisksView.qml new file mode 100644 index 00000000..5a8e6889 --- /dev/null +++ b/quickshell/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/quickshell/Modules/ProcessList/PerformanceTab.qml b/quickshell/Modules/ProcessList/PerformanceTab.qml deleted file mode 100644 index 67fc0078..00000000 --- a/quickshell/Modules/ProcessList/PerformanceTab.qml +++ /dev/null @@ -1,451 +0,0 @@ -import QtQuick -import qs.Common -import qs.Services -import qs.Widgets - -Column { - function formatNetworkSpeed(bytesPerSec) { - if (bytesPerSec < 1024) { - return bytesPerSec.toFixed(0) + " B/s"; - } else if (bytesPerSec < 1024 * 1024) { - return (bytesPerSec / 1024).toFixed(1) + " KB/s"; - } else if (bytesPerSec < 1024 * 1024 * 1024) { - return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"; - } else { - return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"; - } - } - - function formatDiskSpeed(bytesPerSec) { - if (bytesPerSec < 1024 * 1024) { - return (bytesPerSec / 1024).toFixed(1) + " KB/s"; - } else if (bytesPerSec < 1024 * 1024 * 1024) { - return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"; - } else { - return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"; - } - } - - anchors.fill: parent - spacing: Theme.spacingM - Component.onCompleted: { - DgopService.addRef(["cpu", "memory", "network", "disk"]); - } - Component.onDestruction: { - DgopService.removeRef(["cpu", "memory", "network", "disk"]); - } - - Rectangle { - width: parent.width - height: 200 - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) - border.width: 1 - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingM - spacing: Theme.spacingS - - Row { - width: parent.width - height: 32 - spacing: Theme.spacingM - - StyledText { - text: "CPU" - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Bold - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - width: 80 - height: 24 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: `${DgopService.cpuUsage.toFixed(1)}%` - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Bold - color: Theme.primary - anchors.centerIn: parent - } - } - - Item { - width: parent.width - 280 - height: 1 - } - - StyledText { - text: `${DgopService.cpuCores} cores` - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - anchors.verticalCenter: parent.verticalCenter - } - } - - DankFlickable { - clip: true - width: parent.width - height: parent.height - 40 - contentHeight: coreUsageColumn.implicitHeight - - Column { - id: coreUsageColumn - - width: parent.width - spacing: 6 - - Repeater { - model: DgopService.perCoreCpuUsage - - Row { - width: parent.width - height: 20 - spacing: Theme.spacingS - - StyledText { - text: `C${index}` - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: 24 - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - width: parent.width - 80 - height: 6 - radius: 3 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - anchors.verticalCenter: parent.verticalCenter - - Rectangle { - width: parent.width * Math.min(1, modelData / 100) - height: parent.height - radius: parent.radius - color: { - const usage = modelData; - if (usage > 80) { - return Theme.error; - } - if (usage > 60) { - return Theme.warning; - } - return Theme.primary; - } - - Behavior on width { - NumberAnimation { - duration: Theme.shortDuration - } - } - } - } - - StyledText { - text: modelData ? `${modelData.toFixed(0)}%` : "0%" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.surfaceText - width: 32 - horizontalAlignment: Text.AlignRight - anchors.verticalCenter: parent.verticalCenter - } - } - } - } - } - } - } - - Row { - width: parent.width - height: 80 - spacing: Theme.spacingM - - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: 80 - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) - border.width: 1 - - Row { - anchors.centerIn: parent - spacing: Theme.spacingM - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: 4 - - StyledText { - text: I18n.tr("Memory") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Bold - color: Theme.surfaceText - } - - StyledText { - text: `${DgopService.formatSystemMemory(DgopService.usedMemoryKB)} / ${DgopService.formatSystemMemory(DgopService.totalMemoryKB)}` - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: 4 - width: 120 - - Rectangle { - width: parent.width - height: 16 - radius: 8 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - - Rectangle { - width: DgopService.totalMemoryKB > 0 ? parent.width * (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0 - height: parent.height - radius: parent.radius - color: { - const usage = DgopService.totalMemoryKB > 0 ? (DgopService.usedMemoryKB / DgopService.totalMemoryKB) : 0; - if (usage > 0.9) { - return Theme.error; - } - if (usage > 0.7) { - return Theme.warning; - } - return Theme.secondary; - } - - Behavior on width { - NumberAnimation { - duration: Theme.mediumDuration - } - } - } - } - - StyledText { - text: DgopService.totalMemoryKB > 0 ? `${((DgopService.usedMemoryKB / DgopService.totalMemoryKB) * 100).toFixed(1)}% used` : "No data" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Bold - color: Theme.surfaceText - } - } - } - } - - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: 80 - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) - border.width: 1 - - Row { - anchors.centerIn: parent - spacing: Theme.spacingM - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: 4 - - StyledText { - text: I18n.tr("Swap") - font.pixelSize: Theme.fontSizeLarge - font.weight: Font.Bold - color: Theme.surfaceText - } - - StyledText { - text: DgopService.totalSwapKB > 0 ? `${DgopService.formatSystemMemory(DgopService.usedSwapKB)} / ${DgopService.formatSystemMemory(DgopService.totalSwapKB)}` : "No swap" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: 4 - width: 120 - - Rectangle { - width: parent.width - height: 16 - radius: 8 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - - Rectangle { - width: DgopService.totalSwapKB > 0 ? parent.width * (DgopService.usedSwapKB / DgopService.totalSwapKB) : 0 - height: parent.height - radius: parent.radius - color: { - if (!DgopService.totalSwapKB) { - return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3); - } - const usage = DgopService.usedSwapKB / DgopService.totalSwapKB; - if (usage > 0.9) { - return Theme.error; - } - if (usage > 0.7) { - return Theme.warning; - } - return Theme.info; - } - - Behavior on width { - NumberAnimation { - duration: Theme.mediumDuration - } - } - } - } - - StyledText { - text: DgopService.totalSwapKB > 0 ? `${((DgopService.usedSwapKB / DgopService.totalSwapKB) * 100).toFixed(1)}% used` : "N/A" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Bold - color: Theme.surfaceText - } - } - } - } - } - - Row { - width: parent.width - height: 80 - spacing: Theme.spacingM - - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: 80 - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) - border.width: 1 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: I18n.tr("Network") - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Bold - color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } - - Row { - spacing: Theme.spacingS - anchors.horizontalCenter: parent.horizontalCenter - - Row { - spacing: 4 - - StyledText { - text: "↓" - font.pixelSize: Theme.fontSizeSmall - color: Theme.info - } - - StyledText { - text: DgopService.networkRxRate > 0 ? formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Bold - color: Theme.surfaceText - } - } - - Row { - spacing: 4 - - StyledText { - text: "↑" - font.pixelSize: Theme.fontSizeSmall - color: Theme.error - } - - StyledText { - text: DgopService.networkTxRate > 0 ? formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Bold - color: Theme.surfaceText - } - } - } - } - } - - Rectangle { - width: (parent.width - Theme.spacingM) / 2 - height: 80 - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) - border.width: 1 - - Column { - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - text: I18n.tr("Disk") - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Bold - color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } - - Row { - spacing: Theme.spacingS - anchors.horizontalCenter: parent.horizontalCenter - - Row { - spacing: 4 - - StyledText { - text: "R" - font.pixelSize: Theme.fontSizeSmall - color: Theme.primary - } - - StyledText { - text: formatDiskSpeed(DgopService.diskReadRate) - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Bold - color: Theme.surfaceText - } - } - - Row { - spacing: 4 - - StyledText { - text: "W" - font.pixelSize: Theme.fontSizeSmall - color: Theme.warning - } - - StyledText { - text: formatDiskSpeed(DgopService.diskWriteRate) - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Bold - color: Theme.surfaceText - } - } - } - } - } - } -} diff --git a/quickshell/Modules/ProcessList/PerformanceView.qml b/quickshell/Modules/ProcessList/PerformanceView.qml new file mode 100644 index 00000000..c793d92f --- /dev/null +++ b/quickshell/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/quickshell/Modules/ProcessList/ProcessContextMenu.qml b/quickshell/Modules/ProcessList/ProcessContextMenu.qml index 29153366..210c698e 100644 --- a/quickshell/Modules/ProcessList/ProcessContextMenu.qml +++ b/quickshell/Modules/ProcessList/ProcessContextMenu.qml @@ -19,13 +19,10 @@ Popup { const menuWidth = processContextMenu.width; const menuHeight = processContextMenu.height; - if (finalX + menuWidth > parentWidth) { + if (finalX + menuWidth > parentWidth) finalX = Math.max(0, parentWidth - menuWidth); - } - - if (finalY + menuHeight > parentHeight) { + if (finalY + menuHeight > parentHeight) finalY = Math.max(0, parentHeight - menuHeight); - } } processContextMenu.x = finalX; @@ -33,25 +30,19 @@ Popup { open(); } - width: 180 + width: 200 height: menuColumn.implicitHeight + Theme.spacingS * 2 padding: 0 modal: false closePolicy: Popup.CloseOnEscape - onClosed: { - closePolicy = Popup.CloseOnEscape; - } - onOpened: { - outsideClickTimer.start(); - } + + onClosed: closePolicy = Popup.CloseOnEscape + onOpened: outsideClickTimer.start() Timer { id: outsideClickTimer - interval: 100 - onTriggered: { - processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside; - } + onTriggered: processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside } background: Rectangle { @@ -59,8 +50,6 @@ Popup { } contentItem: Rectangle { - id: menuContent - color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) radius: Theme.cornerRadius border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) @@ -68,157 +57,140 @@ Popup { Column { id: menuColumn - anchors.fill: parent anchors.margins: Theme.spacingS spacing: 1 - Rectangle { - width: parent.width - height: 28 - radius: Theme.cornerRadius - color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" - - StyledText { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - text: I18n.tr("Copy PID") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Normal - } - - MouseArea { - id: copyPidArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (processContextMenu.processData) { - Quickshell.execDetached(["dms", "cl", "copy", processContextMenu.processData.pid.toString()]); - } - processContextMenu.close(); - } + MenuItem { + text: I18n.tr("Copy PID") + iconName: "tag" + onClicked: { + if (processContextMenu.processData) + Quickshell.execDetached(["dms", "cl", "copy", processContextMenu.processData.pid.toString()]); + processContextMenu.close(); } } - Rectangle { - width: parent.width - height: 28 - radius: Theme.cornerRadius - color: copyNameArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" - - StyledText { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - text: I18n.tr("Copy Process Name") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceText - font.weight: Font.Normal - } - - MouseArea { - id: copyNameArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (processContextMenu.processData) { - const processName = processContextMenu.processData.displayName || processContextMenu.processData.command; - Quickshell.execDetached(["dms", "cl", "copy", processName]); - } - processContextMenu.close(); + MenuItem { + text: I18n.tr("Copy Name") + iconName: "content_copy" + onClicked: { + if (processContextMenu.processData) { + const name = processContextMenu.processData.command || ""; + Quickshell.execDetached(["dms", "cl", "copy", name]); } + processContextMenu.close(); + } + } + + MenuItem { + text: I18n.tr("Copy Full Command") + iconName: "code" + onClicked: { + if (processContextMenu.processData) { + const fullCmd = processContextMenu.processData.fullCommand || processContextMenu.processData.command || ""; + Quickshell.execDetached(["dms", "cl", "copy", fullCmd]); + } + processContextMenu.close(); } } Rectangle { width: parent.width - Theme.spacingS * 2 - height: 5 + height: 1 anchors.horizontalCenter: parent.horizontalCenter - color: "transparent" - - Rectangle { - anchors.centerIn: parent - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - } + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15) } - Rectangle { - width: parent.width - height: 28 - radius: Theme.cornerRadius - color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + MenuItem { + text: I18n.tr("Kill Process") + iconName: "close" + dangerous: true enabled: processContextMenu.processData - opacity: enabled ? 1 : 0.5 - - StyledText { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - text: I18n.tr("Kill Process") - font.pixelSize: Theme.fontSizeSmall - color: parent.enabled ? (killArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) - font.weight: Font.Normal - } - - MouseArea { - id: killArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: parent.enabled - onClicked: { - if (processContextMenu.processData) { - Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]); - } - - processContextMenu.close(); - } + onClicked: { + if (processContextMenu.processData) + Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]); + processContextMenu.close(); } } - Rectangle { - width: parent.width - height: 28 - radius: Theme.cornerRadius - color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + MenuItem { + text: I18n.tr("Force Kill (SIGKILL)") + iconName: "dangerous" + dangerous: true enabled: processContextMenu.processData && processContextMenu.processData.pid > 1000 - opacity: enabled ? 1 : 0.5 - - StyledText { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - text: I18n.tr("Force Kill Process") - font.pixelSize: Theme.fontSizeSmall - color: parent.enabled ? (forceKillArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) - font.weight: Font.Normal - } - - MouseArea { - id: forceKillArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - enabled: parent.enabled - onClicked: { - if (processContextMenu.processData) { - Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]); - } - - processContextMenu.close(); - } + onClicked: { + if (processContextMenu.processData) + Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]); + processContextMenu.close(); } } } } + + component MenuItem: Rectangle { + id: menuItem + + property string text: "" + property string iconName: "" + property bool dangerous: false + property bool enabled: true + + signal clicked + + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: { + if (!enabled) + return "transparent"; + if (dangerous) + return menuItemArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"; + return menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"; + } + opacity: enabled ? 1 : 0.5 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: menuItem.iconName + size: 16 + color: { + if (!menuItem.enabled) + return Theme.surfaceVariantText; + if (menuItem.dangerous && menuItemArea.containsMouse) + return Theme.error; + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: menuItem.text + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + color: { + if (!menuItem.enabled) + return Theme.surfaceVariantText; + if (menuItem.dangerous && menuItemArea.containsMouse) + return Theme.error; + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: menuItemArea + anchors.fill: parent + hoverEnabled: true + cursorShape: menuItem.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: menuItem.enabled + onClicked: menuItem.clicked() + } + } } diff --git a/quickshell/Modules/ProcessList/ProcessListItem.qml b/quickshell/Modules/ProcessList/ProcessListItem.qml deleted file mode 100644 index 8b86b76e..00000000 --- a/quickshell/Modules/ProcessList/ProcessListItem.qml +++ /dev/null @@ -1,200 +0,0 @@ -import QtQuick -import qs.Common -import qs.Services -import qs.Widgets - -Rectangle { - id: processItem - - property var process: null - property var contextMenu: null - - width: parent ? parent.width : 0 - height: 40 - radius: Theme.cornerRadius - color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.primary, 0) - border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0) - border.width: 1 - - MouseArea { - id: processMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: (mouse) => { - if (mouse.button === Qt.RightButton) { - if (process && process.pid > 0 && contextMenu) { - contextMenu.processData = process; - const globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y); - const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos; - contextMenu.show(localPos.x, localPos.y); - } - } - } - onPressAndHold: { - if (process && process.pid > 0 && contextMenu) { - contextMenu.processData = process; - const globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2); - contextMenu.show(globalPos.x, globalPos.y); - } - } - } - - Item { - anchors.fill: parent - anchors.margins: 8 - - DankIcon { - id: processIcon - - name: DgopService.getProcessIcon(process ? process.command : "") - size: Theme.iconSize - 4 - color: { - if (process && process.cpu > 80) { - return Theme.error; - } - return Theme.surfaceText; - } - opacity: 0.8 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: process ? process.displayName : "" - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Medium - color: Theme.surfaceText - width: 250 - elide: Text.ElideRight - anchors.left: processIcon.right - anchors.leftMargin: 8 - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - id: cpuBadge - - width: 80 - height: 20 - radius: Theme.cornerRadius - color: { - if (process && process.cpu > 80) { - return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); - } - return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08); - } - anchors.right: parent.right - anchors.rightMargin: 194 - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: DgopService.formatCpuUsage(process ? process.cpu : 0) - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: { - if (process && process.cpu > 80) { - return Theme.error; - } - return Theme.surfaceText; - } - anchors.centerIn: parent - } - - } - - Rectangle { - id: memoryBadge - - width: 80 - height: 20 - radius: Theme.cornerRadius - color: { - if (process && process.memoryKB > 1024 * 1024) { - return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); - } - return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08); - } - anchors.right: parent.right - anchors.rightMargin: 102 - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: DgopService.formatMemoryUsage(process ? process.memoryKB : 0) - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: { - if (process && process.memoryKB > 1024 * 1024) { - return Theme.error; - } - return Theme.surfaceText; - } - anchors.centerIn: parent - } - - } - - StyledText { - text: process ? process.pid.toString() : "" - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - opacity: 0.7 - width: 50 - horizontalAlignment: Text.AlignRight - anchors.right: parent.right - anchors.rightMargin: 40 - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - id: menuButton - - width: 28 - height: 28 - radius: Theme.cornerRadius - color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0) - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - DankIcon { - name: "more_vert" - size: Theme.iconSize - 2 - color: Theme.surfaceText - opacity: 0.6 - anchors.centerIn: parent - } - - MouseArea { - id: menuButtonArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (process && process.pid > 0 && contextMenu) { - contextMenu.processData = process; - const globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height); - const localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos; - contextMenu.show(localPos.x, localPos.y); - } - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - - } - -} diff --git a/quickshell/Modules/ProcessList/ProcessListPopout.qml b/quickshell/Modules/ProcessList/ProcessListPopout.qml index 0136f038..21758151 100644 --- a/quickshell/Modules/ProcessList/ProcessListPopout.qml +++ b/quickshell/Modules/ProcessList/ProcessListPopout.qml @@ -12,32 +12,39 @@ DankPopout { property var parentWidget: null property var triggerScreen: null + property string searchText: "" + property string expandedPid: "" function hide() { close(); - if (processContextMenu.visible) { + if (processContextMenu.visible) processContextMenu.close(); - } } function show() { open(); } - popupWidth: 600 - popupHeight: 600 + popupWidth: 650 + popupHeight: 550 triggerWidth: 55 positioning: "" screen: triggerScreen shouldBeVisible: false onBackgroundClicked: { - if (processContextMenu.visible) { + if (processContextMenu.visible) processContextMenu.close(); - } close(); } + onShouldBeVisibleChanged: { + if (!shouldBeVisible) { + searchText = ""; + expandedPid = ""; + } + } + Ref { service: DgopService } @@ -55,55 +62,247 @@ DankPopout { radius: Theme.cornerRadius color: "transparent" - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 0 clip: true - antialiasing: true - smooth: true focus: true + Component.onCompleted: { - if (processListPopout.shouldBeVisible) { + if (processListPopout.shouldBeVisible) forceActiveFocus(); - } processContextMenu.parent = processListContent; } + Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { + switch (event.key) { + case Qt.Key_Escape: + if (processListPopout.searchText.length > 0) { + processListPopout.searchText = ""; + 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; } } Connections { + target: processListPopout function onShouldBeVisibleChanged() { if (processListPopout.shouldBeVisible) { - Qt.callLater(() => { - processListContent.forceActiveFocus(); - }); + Qt.callLater(() => processListContent.forceActiveFocus()); } } - - target: processListPopout } ColumnLayout { anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingL + anchors.margins: Theme.spacingS + spacing: Theme.spacingS - Rectangle { + RowLayout { Layout.fillWidth: true - height: systemOverview.height + Theme.spacingM * 2 - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 0 + spacing: Theme.spacingM - SystemOverview { - id: systemOverview + Row { + spacing: Theme.spacingS - anchors.centerIn: parent - width: parent.width - Theme.spacingM * 2 + 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 + } + + DankTextField { + id: searchField + Layout.preferredWidth: Theme.fontSizeMedium * 14 + Layout.preferredHeight: Theme.fontSizeMedium * 2.5 + placeholderText: I18n.tr("Search...") + leftIconName: "search" + showClearButton: true + text: processListPopout.searchText + onTextChanged: processListPopout.searchText = text + } + } + + 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 + + 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; + } + } } } @@ -112,16 +311,168 @@ DankPopout { Layout.fillHeight: true radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) - border.width: 0 + clip: true - ProcessListView { + ProcessesView { anchors.fill: parent anchors.margins: Theme.spacingS + searchText: processListPopout.searchText + expandedPid: processListPopout.expandedPid 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(3, Math.min(width, height) / 17) + readonly property real glowExtra: thickness * 1.6 + readonly property real arcPadding: thickness / 1.5 + + 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: 0 + + StyledText { + text: gaugeRoot.label + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: gaugeRoot.sublabel + font.pixelSize: Theme.fontSizeSmall - 2 + color: gaugeRoot.accentColor + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: gaugeRoot.detail + font.pixelSize: Theme.fontSizeSmall - 3 + font.family: SettingsData.monoFontFamily + color: gaugeRoot.detailColor + anchors.horizontalCenter: parent.horizontalCenter + visible: gaugeRoot.detail.length > 0 + } + } + } } diff --git a/quickshell/Modules/ProcessList/ProcessListView.qml b/quickshell/Modules/ProcessList/ProcessListView.qml deleted file mode 100644 index 50551b9f..00000000 --- a/quickshell/Modules/ProcessList/ProcessListView.qml +++ /dev/null @@ -1,264 +0,0 @@ -import QtQuick -import QtQuick.Controls -import Quickshell -import qs.Common -import qs.Services -import qs.Widgets - -Column { - id: root - - property var contextMenu: null - - Component.onCompleted: { - DgopService.addRef(["processes"]); - } - Component.onDestruction: { - DgopService.removeRef(["processes"]); - } - - Item { - id: columnHeaders - - width: parent.width - anchors.leftMargin: 8 - height: 24 - - Rectangle { - width: 60 - height: 20 - color: { - if (DgopService.currentSort === "name") { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); - } - return processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0); - } - radius: Theme.cornerRadius - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: I18n.tr("Process") - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: DgopService.currentSort === "name" ? Font.Bold : Font.Medium - color: Theme.surfaceText - opacity: DgopService.currentSort === "name" ? 1 : 0.7 - anchors.centerIn: parent - } - - MouseArea { - id: processHeaderArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - DgopService.setSortBy("name"); - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - - Rectangle { - width: 80 - height: 20 - color: { - if (DgopService.currentSort === "cpu") { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); - } - return cpuHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0); - } - radius: Theme.cornerRadius - anchors.right: parent.right - anchors.rightMargin: 200 - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: "CPU" - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: DgopService.currentSort === "cpu" ? Font.Bold : Font.Medium - color: Theme.surfaceText - opacity: DgopService.currentSort === "cpu" ? 1 : 0.7 - anchors.centerIn: parent - } - - MouseArea { - id: cpuHeaderArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - DgopService.setSortBy("cpu"); - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - - Rectangle { - width: 80 - height: 20 - color: { - if (DgopService.currentSort === "memory") { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); - } - return memoryHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0); - } - radius: Theme.cornerRadius - anchors.right: parent.right - anchors.rightMargin: 112 - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: "RAM" - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: DgopService.currentSort === "memory" ? Font.Bold : Font.Medium - color: Theme.surfaceText - opacity: DgopService.currentSort === "memory" ? 1 : 0.7 - anchors.centerIn: parent - } - - MouseArea { - id: memoryHeaderArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - DgopService.setSortBy("memory"); - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - - Rectangle { - width: 50 - height: 20 - color: { - if (DgopService.currentSort === "pid") { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); - } - return pidHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0); - } - radius: Theme.cornerRadius - anchors.right: parent.right - anchors.rightMargin: 53 - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: "PID" - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: DgopService.currentSort === "pid" ? Font.Bold : Font.Medium - color: Theme.surfaceText - opacity: DgopService.currentSort === "pid" ? 1 : 0.7 - horizontalAlignment: Text.AlignHCenter - anchors.centerIn: parent - } - - MouseArea { - id: pidHeaderArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - DgopService.setSortBy("pid"); - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - - Rectangle { - width: 28 - height: 28 - radius: Theme.cornerRadius - color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : Theme.withAlpha(Theme.surfaceText, 0) - anchors.right: parent.right - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: DgopService.sortDescending ? "↓" : "↑" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.centerIn: parent - } - - MouseArea { - // TODO: Re-implement sort order toggle - - id: sortOrderArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - - } - - DankListView { - id: processListView - - property string keyRoleName: "pid" - - width: parent.width - height: parent.height - columnHeaders.height - clip: true - spacing: 4 - model: ScriptModel { - values: DgopService.processes - objectProp: "pid" - } - - delegate: ProcessListItem { - process: modelData - contextMenu: root.contextMenu - } - - } - -} diff --git a/quickshell/Modules/ProcessList/ProcessesTab.qml b/quickshell/Modules/ProcessList/ProcessesTab.qml deleted file mode 100644 index 987dd90a..00000000 --- a/quickshell/Modules/ProcessList/ProcessesTab.qml +++ /dev/null @@ -1,28 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import qs.Common -import qs.Modules.ProcessList -import qs.Services - -ColumnLayout { - id: processesTab - - property var contextMenu: null - - anchors.fill: parent - spacing: Theme.spacingM - - SystemOverview { - Layout.fillWidth: true - } - - ProcessListView { - Layout.fillWidth: true - Layout.fillHeight: true - contextMenu: processesTab.contextMenu - } - - ProcessContextMenu { - id: localContextMenu - } -} diff --git a/quickshell/Modules/ProcessList/ProcessesView.qml b/quickshell/Modules/ProcessList/ProcessesView.qml new file mode 100644 index 00000000..d0ef346e --- /dev/null +++ b/quickshell/Modules/ProcessList/ProcessesView.qml @@ -0,0 +1,607 @@ +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 bool hoveringExpandedItem: false + + readonly property bool pauseUpdates: hoveringExpandedItem || (contextMenu?.visible ?? false) + property var cachedProcesses: [] + + onFilteredProcessesChanged: { + if (!pauseUpdates) + cachedProcesses = filteredProcesses; + } + + onPauseUpdatesChanged: { + if (!pauseUpdates) + cachedProcesses = filteredProcesses; + } + + readonly property var filteredProcesses: { + if (!DgopService.allProcesses || DgopService.allProcesses.length === 0) + return []; + + let procs = DgopService.allProcesses.slice(); + + 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; + } + + 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.setSortBy("name") + alignment: Text.AlignLeft + } + + SortableHeader { + Layout.preferredWidth: 100 + text: "CPU" + sortKey: "cpu" + currentSort: DgopService.currentSort + sortAscending: DgopService.sortAscending + onClicked: DgopService.setSortBy("cpu") + } + + SortableHeader { + Layout.preferredWidth: 100 + text: I18n.tr("Memory") + sortKey: "memory" + currentSort: DgopService.currentSort + sortAscending: DgopService.sortAscending + onClicked: DgopService.setSortBy("memory") + } + + SortableHeader { + Layout.preferredWidth: 80 + text: "PID" + sortKey: "pid" + currentSort: DgopService.currentSort + sortAscending: DgopService.sortAscending + onClicked: DgopService.setSortBy("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 + + model: ScriptModel { + values: root.cachedProcesses + objectProp: "pid" + } + + delegate: ProcessItem { + required property var modelData + + width: processListView.width + process: modelData + isExpanded: root.expandedPid === (modelData?.pid ?? -1).toString() + contextMenu: root.contextMenu + onToggleExpand: { + const pidStr = (modelData?.pid ?? -1).toString(); + root.expandedPid = (root.expandedPid === pidStr) ? "" : pidStr; + } + onHoveringExpandedChanged: { + if (hoveringExpanded) + root.hoveringExpandedItem = true; + else + Qt.callLater(() => { + root.hoveringExpandedItem = 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 var contextMenu: null + readonly property bool hoveringExpanded: (isExpanded && processMouseArea.containsMouse) || copyMouseArea.containsMouse + + signal toggleExpand + + 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: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent" + border.color: 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) { + if (processItemRoot.processPid > 0 && processItemRoot.contextMenu) { + processItemRoot.contextMenu.processData = processItemRoot.process; + const globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y); + const localPos = processItemRoot.contextMenu.parent ? processItemRoot.contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos; + processItemRoot.contextMenu.show(localPos.x, localPos.y); + } + return; + } + 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/quickshell/Modules/ProcessList/SystemOverview.qml b/quickshell/Modules/ProcessList/SystemOverview.qml deleted file mode 100644 index 72e460c4..00000000 --- a/quickshell/Modules/ProcessList/SystemOverview.qml +++ /dev/null @@ -1,435 +0,0 @@ -import QtQuick -import QtQuick.Controls -import qs.Common -import qs.Services -import qs.Widgets - -Row { - width: parent.width - spacing: Theme.spacingM - Component.onCompleted: { - DgopService.addRef(["cpu", "memory", "system"]); - } - Component.onDestruction: { - DgopService.removeRef(["cpu", "memory", "system"]); - } - - Rectangle { - width: (parent.width - Theme.spacingM * 2) / 3 - height: 80 - radius: Theme.cornerRadius - color: { - if (DgopService.sortBy === "cpu") { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16); - } else if (cpuCardMouseArea.containsMouse) { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); - } else { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08); - } - } - border.color: DgopService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) - border.width: DgopService.sortBy === "cpu" ? 2 : 1 - - MouseArea { - id: cpuCardMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - DgopService.setSortBy("cpu"); - } - } - - Column { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: 2 - - StyledText { - text: I18n.tr("CPU") - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: DgopService.sortBy === "cpu" ? Theme.primary : Theme.secondary - opacity: DgopService.sortBy === "cpu" ? 1 : 0.8 - } - - Row { - spacing: Theme.spacingS - - StyledText { - text: { - if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null) { - return "--%"; - } - return DgopService.cpuUsage.toFixed(1) + "%"; - } - font.pixelSize: Theme.fontSizeLarge - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - width: 1 - height: 20 - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: { - if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature <= 0) { - return "--°"; - } - return Math.round(DgopService.cpuTemperature) + "°"; - } - font.pixelSize: Theme.fontSizeMedium - font.family: SettingsData.monoFontFamily - font.weight: Font.Medium - color: { - if (DgopService.cpuTemperature > 80) { - return Theme.error; - } - if (DgopService.cpuTemperature > 60) { - return Theme.warning; - } - return Theme.surfaceText; - } - anchors.verticalCenter: parent.verticalCenter - } - - } - - StyledText { - text: `${DgopService.cpuCores} cores` - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - opacity: 0.7 - } - - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - Behavior on border.color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - - Rectangle { - width: (parent.width - Theme.spacingM * 2) / 3 - height: 80 - radius: Theme.cornerRadius - color: { - if (DgopService.sortBy === "memory") { - return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16); - } else if (memoryCardMouseArea.containsMouse) { - return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12); - } else { - return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08); - } - } - border.color: DgopService.sortBy === "memory" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.2) - border.width: DgopService.sortBy === "memory" ? 2 : 1 - - MouseArea { - id: memoryCardMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - DgopService.setSortBy("memory"); - } - } - - Column { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: 2 - - StyledText { - text: I18n.tr("Memory") - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: DgopService.sortBy === "memory" ? Theme.primary : Theme.secondary - opacity: DgopService.sortBy === "memory" ? 1 : 0.8 - } - - Row { - spacing: Theme.spacingS - - StyledText { - text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) - font.pixelSize: Theme.fontSizeLarge - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - width: 1 - height: 20 - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) - anchors.verticalCenter: parent.verticalCenter - visible: DgopService.totalSwapKB > 0 - } - - StyledText { - text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(DgopService.usedSwapKB) : "" - font.pixelSize: Theme.fontSizeMedium - font.family: SettingsData.monoFontFamily - font.weight: Font.Medium - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - visible: DgopService.totalSwapKB > 0 - } - - } - - StyledText { - text: { - if (DgopService.totalSwapKB > 0) { - return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB) + " + swap"; - } - return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB); - } - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - opacity: 0.7 - } - - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - Behavior on border.color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - - Rectangle { - width: (parent.width - Theme.spacingM * 2) / 3 - height: 80 - radius: Theme.cornerRadius - color: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { - return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16); - } else { - return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); - } - } - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - const vendor = gpu.vendor.toLowerCase(); - if (vendor.includes("nvidia")) { - if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { - return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2); - } else { - return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12); - } - } else if (vendor.includes("amd")) { - if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { - return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2); - } else { - return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); - } - } else if (vendor.includes("intel")) { - if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { - return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2); - } else { - return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.12); - } - } - if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { - return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16); - } else { - return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); - } - } - border.color: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2); - } - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - const vendor = gpu.vendor.toLowerCase(); - if (vendor.includes("nvidia")) { - return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3); - } else if (vendor.includes("amd")) { - return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3); - } else if (vendor.includes("intel")) { - return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3); - } - return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2); - } - border.width: 1 - - MouseArea { - id: gpuCardMouseArea - - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton - cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: (mouse) => { - if (mouse.button === Qt.LeftButton) { - if (DgopService.availableGpus.length > 1) { - const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length; - SessionData.setSelectedGpuIndex(nextIndex); - } - } else if (mouse.button === Qt.RightButton) { - gpuContextMenu.popup(); - } - } - } - - Column { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: 2 - - StyledText { - text: I18n.tr("GPU") - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.secondary - opacity: 0.8 - } - - StyledText { - text: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return "No GPU"; - } - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - // Check if temperature monitoring is enabled for this GPU - const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; - const temp = gpu.temperature; - const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0; - if (hasTemp) { - return Math.round(temp) + "°"; - } else { - return gpu.vendor; - } - } - font.pixelSize: Theme.fontSizeLarge - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return Theme.surfaceText; - } - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; - const temp = gpu.temperature || 0; - if (tempEnabled && temp > 80) { - return Theme.error; - } - if (tempEnabled && temp > 60) { - return Theme.warning; - } - return Theme.surfaceText; - } - } - - StyledText { - text: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return "No GPUs detected"; - } - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - const tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; - const temp = gpu.temperature; - const hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0; - if (hasTemp) { - return gpu.vendor + " " + gpu.displayName; - } else { - return gpu.displayName; - } - } - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - opacity: 0.7 - width: parent.parent.width - Theme.spacingM * 2 - elide: Text.ElideRight - maximumLineCount: 1 - } - - } - - Menu { - id: gpuContextMenu - - MenuItem { - text: I18n.tr("Enable GPU Temperature") - checkable: true - checked: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return false; - } - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - if (!gpu.pciId) { - return false; - } - return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 : false; - } - onTriggered: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return; - } - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - if (!gpu.pciId) { - return; - } - const enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : []; - const index = enabledIds.indexOf(gpu.pciId); - if (checked && index === -1) { - enabledIds.push(gpu.pciId); - DgopService.addGpuPciId(gpu.pciId); - } else if (!checked && index !== -1) { - enabledIds.splice(index, 1); - DgopService.removeGpuPciId(gpu.pciId); - } - SessionData.setEnabledGpuPciIds(enabledIds); - } - } - - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - - } - - } - -} diff --git a/quickshell/Modules/ProcessList/SystemTab.qml b/quickshell/Modules/ProcessList/SystemTab.qml deleted file mode 100644 index 56fe700e..00000000 --- a/quickshell/Modules/ProcessList/SystemTab.qml +++ /dev/null @@ -1,591 +0,0 @@ -import QtQuick -import qs.Common -import qs.Services -import qs.Widgets - -DankFlickable { - anchors.fill: parent - contentHeight: systemColumn.implicitHeight - clip: true - Component.onCompleted: { - DgopService.addRef(["system", "diskmounts"]); - } - Component.onDestruction: { - DgopService.removeRef(["system", "diskmounts"]); - } - - Column { - id: systemColumn - - width: parent.width - spacing: Theme.spacingM - - Rectangle { - width: parent.width - height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.width: 0 - - Column { - id: systemInfoColumn - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacingL - spacing: Theme.spacingL - - Row { - width: parent.width - spacing: Theme.spacingL - - SystemLogo { - width: 80 - height: 80 - } - - Column { - width: parent.width - 80 - Theme.spacingL - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS - - StyledText { - text: DgopService.hostname - font.pixelSize: Theme.fontSizeXLarge - font.family: SettingsData.monoFontFamily - font.weight: Font.Light - color: Theme.surfaceText - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: `${DgopService.distribution} • ${DgopService.architecture} • ${DgopService.kernelVersion}` - font.pixelSize: Theme.fontSizeMedium - font.family: SettingsData.monoFontFamily - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: `${DgopService.uptime} • Boot: ${DgopService.bootTime}` - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: `Load: ${DgopService.loadAverage} • ${DgopService.processCount} processes, ${DgopService.threadCount} threads` - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) - verticalAlignment: Text.AlignVCenter - } - } - } - - Rectangle { - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - } - - Row { - width: parent.width - spacing: Theme.spacingXL - - Rectangle { - width: (parent.width - Theme.spacingXL) / 2 - height: hardwareColumn.implicitHeight + Theme.spacingL - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.width: 1 - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) - - Column { - id: hardwareColumn - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacingM - spacing: Theme.spacingXS - - Row { - width: parent.width - spacing: Theme.spacingS - - DankIcon { - name: "memory" - size: Theme.iconSizeSmall - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: I18n.tr("System") - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - } - - StyledText { - text: DgopService.cpuModel - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - elide: Text.ElideRight - wrapMode: Text.NoWrap - maximumLineCount: 1 - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: DgopService.motherboard - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) - width: parent.width - elide: Text.ElideRight - wrapMode: Text.NoWrap - maximumLineCount: 1 - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: `BIOS ${DgopService.biosVersion}` - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - width: parent.width - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: `${DgopService.formatSystemMemory(DgopService.totalMemoryKB)} RAM` - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) - width: parent.width - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - } - } - - Rectangle { - width: (parent.width - Theme.spacingXL) / 2 - height: gpuColumn.implicitHeight + Theme.spacingL - radius: Theme.cornerRadius - color: { - const baseColor = Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); - const hoverColor = Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, Theme.popupTransparency * 1.5); - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor; - } - - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - const vendor = gpu.fullName.split(' ')[0].toLowerCase(); - let tintColor; - if (vendor.includes("nvidia")) { - tintColor = Theme.success; - } else if (vendor.includes("amd")) { - tintColor = Theme.error; - } else if (vendor.includes("intel")) { - tintColor = Theme.info; - } else { - return gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1 ? hoverColor : baseColor; - } - if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) { - return Qt.rgba((hoverColor.r + tintColor.r * 0.1) / 1.1, (hoverColor.g + tintColor.g * 0.1) / 1.1, (hoverColor.b + tintColor.b * 0.1) / 1.1, 0.6); - } else { - return Qt.rgba((baseColor.r + tintColor.r * 0.08) / 1.08, (baseColor.g + tintColor.g * 0.08) / 1.08, (baseColor.b + tintColor.b * 0.08) / 1.08, 0.4); - } - } - border.width: 1 - border.color: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1); - } - - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - const vendor = gpu.fullName.split(' ')[0].toLowerCase(); - if (vendor.includes("nvidia")) { - return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3); - } else if (vendor.includes("amd")) { - return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3); - } else if (vendor.includes("intel")) { - return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3); - } - return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1); - } - - MouseArea { - id: gpuCardMouseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: { - if (DgopService.availableGpus.length > 1) { - const nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length; - SessionData.setSelectedGpuIndex(nextIndex); - } - } - } - - Column { - id: gpuColumn - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacingM - spacing: Theme.spacingXS - - Row { - width: parent.width - spacing: Theme.spacingS - - DankIcon { - name: "auto_awesome_mosaic" - size: Theme.iconSizeSmall - color: Theme.secondary - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "GPU" - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.secondary - anchors.verticalCenter: parent.verticalCenter - } - } - - StyledText { - text: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return "No GPUs detected"; - } - - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - return gpu.fullName; - } - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Medium - color: Theme.surfaceText - width: parent.width - elide: Text.ElideRight - maximumLineCount: 1 - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return "Device: N/A"; - } - - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - return `Device: ${gpu.pciId}`; - } - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) - width: parent.width - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - textFormat: Text.RichText - } - - StyledText { - text: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return "Driver: N/A"; - } - - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - return `Driver: ${gpu.driver}`; - } - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) - width: parent.width - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return "Temp: --°"; - } - - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - const temp = gpu.temperature; - return `Temp: ${(temp === undefined || temp === null || temp === 0) ? '--°' : `${Math.round(temp)}°C`}`; - } - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: { - if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { - return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7); - } - - const gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; - const temp = gpu.temperature || 0; - if (temp > 80) { - return Theme.error; - } - - if (temp > 60) { - return Theme.warning; - } - - return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7); - } - width: parent.width - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - } - } - } - } - } - } - - Rectangle { - width: parent.width - height: storageColumn.implicitHeight + 2 * Theme.spacingL - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.width: 0 - - Column { - id: storageColumn - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacingL - spacing: Theme.spacingS - - Row { - width: parent.width - spacing: Theme.spacingS - - DankIcon { - name: "storage" - size: Theme.iconSize - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: I18n.tr("Storage & Disks") - font.pixelSize: Theme.fontSizeLarge - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - } - - Column { - width: parent.width - spacing: 2 - - Row { - width: parent.width - height: 24 - spacing: Theme.spacingS - - StyledText { - text: I18n.tr("Device") - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - width: parent.width * 0.25 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: I18n.tr("Mount") - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - width: parent.width * 0.2 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: I18n.tr("Size") - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - width: parent.width * 0.15 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: I18n.tr("Used") - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - width: parent.width * 0.15 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: I18n.tr("Available") - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - width: parent.width * 0.15 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: I18n.tr("Use%") - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - font.weight: Font.Bold - color: Theme.surfaceText - width: parent.width * 0.1 - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - } - - Repeater { - id: diskMountRepeater - - model: DgopService.diskMounts - - Rectangle { - width: parent.width - height: 24 - radius: Theme.cornerRadius - color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent" - - MouseArea { - id: diskMouseArea - - anchors.fill: parent - hoverEnabled: true - } - - Row { - anchors.fill: parent - spacing: Theme.spacingS - - StyledText { - text: modelData.device - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - width: parent.width * 0.25 - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: modelData.mount - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - width: parent.width * 0.2 - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: modelData.size - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - width: parent.width * 0.15 - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: modelData.used - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - width: parent.width * 0.15 - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: modelData.avail - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: Theme.surfaceText - width: parent.width * 0.15 - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - - StyledText { - text: modelData.percent - font.pixelSize: Theme.fontSizeSmall - font.family: SettingsData.monoFontFamily - color: { - const percent = parseInt(modelData.percent); - if (percent > 90) { - return Theme.error; - } - - if (percent > 75) { - return Theme.warning; - } - - return Theme.surfaceText; - } - width: parent.width * 0.1 - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter - } - } - } - } - } - } - } - } -} diff --git a/quickshell/Modules/ProcessList/SystemView.qml b/quickshell/Modules/ProcessList/SystemView.qml new file mode 100644 index 00000000..244129d8 --- /dev/null +++ b/quickshell/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 + } + } +} diff --git a/quickshell/Services/DgopService.qml b/quickshell/Services/DgopService.qml index 273d4a95..04d3236a 100644 --- a/quickshell/Services/DgopService.qml +++ b/quickshell/Services/DgopService.qml @@ -21,6 +21,7 @@ Singleton { property int processLimit: 20 property string processSort: "cpu" property bool noCpu: false + property int dgopProcessPid: 0 // Cursor data for accurate CPU calculations property string cpuCursor: "" @@ -59,6 +60,7 @@ Singleton { property var processes: [] property var allProcesses: [] property string currentSort: "cpu" + property bool sortAscending: false property var availableGpus: [] property string kernelVersion: "" @@ -93,10 +95,8 @@ Singleton { if (modules) { const modulesToAdd = Array.isArray(modules) ? modules : [modules]; for (const module of modulesToAdd) { - // Increment reference count for this module const currentCount = moduleRefCounts[module] || 0; moduleRefCounts[module] = currentCount + 1; - console.log("Adding ref for module:", module, "count:", moduleRefCounts[module]); // Add to enabled modules if not already there if (enabledModules.indexOf(module) === -1) { @@ -126,17 +126,13 @@ Singleton { for (const module of modulesToRemove) { const currentCount = moduleRefCounts[module] || 0; if (currentCount > 1) { - // Decrement reference count moduleRefCounts[module] = currentCount - 1; - console.log("Removing ref for module:", module, "count:", moduleRefCounts[module]); } else if (currentCount === 1) { - // Remove completely when count reaches 0 delete moduleRefCounts[module]; const index = enabledModules.indexOf(module); if (index > -1) { enabledModules.splice(index, 1); modulesChanged = true; - console.log("Disabling module:", module, "(no more refs)"); } } } @@ -171,17 +167,13 @@ Singleton { gpuPciIds = gpuPciIds.concat([pciId]); } - console.log("Adding GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]); - // Force property change notification gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts); } function removeGpuPciId(pciId) { const currentCount = gpuPciIdRefCounts[pciId] || 0; if (currentCount > 1) { - // Decrement reference count gpuPciIdRefCounts[pciId] = currentCount - 1; - console.log("Removing GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId]); } else if (currentCount === 1) { // Remove completely when count reaches 0 delete gpuPciIdRefCounts[pciId]; @@ -203,8 +195,6 @@ Singleton { } availableGpus = updatedGpus; } - - console.log("Removing GPU PCI ID completely:", pciId); } // Force property change notification @@ -389,8 +379,12 @@ Singleton { if (data.processes && Array.isArray(data.processes)) { const newProcesses = []; processSampleCount++; + const ourPid = dgopProcessPid; for (const proc of data.processes) { + if (ourPid > 0 && proc.pid === ourPid) + continue; + const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0; newProcesses.push({ @@ -575,41 +569,49 @@ Singleton { } function setSortBy(newSortBy) { - if (newSortBy !== currentSort) { + if (newSortBy === currentSort) { + sortAscending = !sortAscending; + } else { currentSort = newSortBy; - applySorting(); + sortAscending = false; } + applySorting(); } function applySorting() { - if (!allProcesses || allProcesses.length === 0) { + if (!allProcesses || allProcesses.length === 0) return; - } + const asc = sortAscending; const sorted = allProcesses.slice(); sorted.sort((a, b) => { - let valueA, valueB; + let valueA, valueB, result; switch (currentSort) { case "cpu": valueA = a.cpu || 0; valueB = b.cpu || 0; - return valueB - valueA; + result = valueB - valueA; + break; case "memory": valueA = a.memoryKB || 0; valueB = b.memoryKB || 0; - return valueB - valueA; + result = valueB - valueA; + break; case "name": valueA = (a.command || "").toLowerCase(); valueB = (b.command || "").toLowerCase(); - return valueA.localeCompare(valueB); + result = valueA.localeCompare(valueB); + break; case "pid": valueA = a.pid || 0; valueB = b.pid || 0; - return valueA - valueB; + result = valueA - valueB; + break; default: return 0; } + return asc ? -result : result; }); processes = sorted.slice(0, processLimit); @@ -628,10 +630,7 @@ Singleton { id: dgopProcess command: root.buildDgopCommand() running: false - onCommandChanged: - - //console.log("DgopService command:", JSON.stringify(command)) - {} + onStarted: dgopProcessPid = processId ?? 0 onExited: exitCode => { if (exitCode !== 0) { console.warn("Dgop process failed with exit code:", exitCode);