diff options
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList')
6 files changed, 2681 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/DisksView.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/DisksView.qml new file mode 100644 index 0000000..5a8e688 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/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/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/PerformanceView.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/PerformanceView.qml new file mode 100644 index 0000000..c793d92 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/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/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessContextMenu.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessContextMenu.qml new file mode 100644 index 0000000..7c48b5a --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessContextMenu.qml @@ -0,0 +1,341 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Popup { + id: processContextMenu + + property var processData: null + property int selectedIndex: -1 + property bool keyboardNavigation: false + property var parentFocusItem: null + + signal menuClosed + signal processKilled + + readonly property var menuItems: [ + { + text: I18n.tr("Copy PID"), + icon: "tag", + action: copyPid, + enabled: true + }, + { + text: I18n.tr("Copy Name"), + icon: "content_copy", + action: copyName, + enabled: true + }, + { + text: I18n.tr("Copy Full Command"), + icon: "code", + action: copyFullCommand, + enabled: true + }, + { + type: "separator" + }, + { + text: I18n.tr("Kill Process"), + icon: "close", + action: killProcess, + enabled: true, + dangerous: true + }, + { + text: I18n.tr("Force Kill (SIGKILL)"), + icon: "dangerous", + action: forceKillProcess, + enabled: processData && processData.pid > 1000, + dangerous: true + } + ] + + readonly property int visibleItemCount: { + let count = 0; + for (let i = 0; i < menuItems.length; i++) { + if (menuItems[i].type !== "separator") + count++; + } + return count; + } + + function show(x, y, fromKeyboard) { + let finalX = x; + let finalY = y; + + if (processContextMenu.parent) { + const parentWidth = processContextMenu.parent.width; + const parentHeight = processContextMenu.parent.height; + const menuWidth = processContextMenu.width; + const menuHeight = processContextMenu.height; + + if (finalX + menuWidth > parentWidth) + finalX = Math.max(0, parentWidth - menuWidth); + if (finalY + menuHeight > parentHeight) + finalY = Math.max(0, parentHeight - menuHeight); + } + + processContextMenu.x = finalX; + processContextMenu.y = finalY; + keyboardNavigation = fromKeyboard || false; + selectedIndex = fromKeyboard ? 0 : -1; + open(); + } + + function selectNext() { + if (visibleItemCount === 0) + return; + let current = selectedIndex; + let next = current; + do { + next = (next + 1) % menuItems.length; + } while (menuItems[next].type === "separator" && next !== current) + selectedIndex = next; + } + + function selectPrevious() { + if (visibleItemCount === 0) + return; + let current = selectedIndex; + let prev = current; + do { + prev = (prev - 1 + menuItems.length) % menuItems.length; + } while (menuItems[prev].type === "separator" && prev !== current) + selectedIndex = prev; + } + + function activateSelected() { + if (selectedIndex < 0 || selectedIndex >= menuItems.length) + return; + const item = menuItems[selectedIndex]; + if (item.type === "separator" || !item.enabled) + return; + item.action(); + } + + function copyPid() { + if (processData) + Quickshell.execDetached(["dms", "cl", "copy", processData.pid.toString()]); + close(); + } + + function copyName() { + if (processData) { + const name = processData.command || ""; + Quickshell.execDetached(["dms", "cl", "copy", name]); + } + close(); + } + + function copyFullCommand() { + if (processData) { + const fullCmd = processData.fullCommand || processData.command || ""; + Quickshell.execDetached(["dms", "cl", "copy", fullCmd]); + } + close(); + } + + function killProcess() { + if (processData) + Quickshell.execDetached(["kill", processData.pid.toString()]); + processKilled(); + close(); + } + + function forceKillProcess() { + if (processData) + Quickshell.execDetached(["kill", "-9", processData.pid.toString()]); + processKilled(); + close(); + } + + width: 200 + height: menuColumn.implicitHeight + Theme.spacingS * 2 + padding: 0 + modal: false + closePolicy: Popup.CloseOnEscape + + onClosed: { + closePolicy = Popup.CloseOnEscape; + keyboardNavigation = false; + selectedIndex = -1; + menuClosed(); + if (parentFocusItem) + Qt.callLater(() => parentFocusItem.forceActiveFocus()); + } + + onOpened: { + outsideClickTimer.start(); + if (keyboardNavigation) + Qt.callLater(() => keyboardHandler.forceActiveFocus()); + } + + Timer { + id: outsideClickTimer + interval: 100 + onTriggered: processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside + } + + background: Rectangle { + color: "transparent" + } + + contentItem: Rectangle { + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: BlurService.enabled ? BlurService.borderWidth : 1 + + Item { + id: keyboardHandler + anchors.fill: parent + focus: keyboardNavigation + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Down: + case Qt.Key_J: + keyboardNavigation = true; + selectNext(); + event.accepted = true; + return; + case Qt.Key_Up: + case Qt.Key_K: + keyboardNavigation = true; + selectPrevious(); + event.accepted = true; + return; + case Qt.Key_Return: + case Qt.Key_Enter: + case Qt.Key_Space: + activateSelected(); + event.accepted = true; + return; + case Qt.Key_Escape: + case Qt.Key_Left: + case Qt.Key_H: + close(); + event.accepted = true; + return; + } + } + } + + Column { + id: menuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 1 + + Repeater { + model: menuItems + + Item { + width: parent.width + height: modelData.type === "separator" ? 5 : 32 + visible: modelData.type !== "separator" || index > 0 + + property int itemVisibleIndex: { + let count = 0; + for (let i = 0; i < index; i++) { + if (menuItems[i].type !== "separator") + count++; + } + return count; + } + + Rectangle { + visible: modelData.type === "separator" + width: parent.width - Theme.spacingS * 2 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.15) + } + + Rectangle { + id: menuItem + visible: modelData.type !== "separator" + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: { + if (!modelData.enabled) + return "transparent"; + const isSelected = keyboardNavigation && selectedIndex === index; + if (modelData.dangerous) { + if (isSelected) + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2); + return menuItemArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"; + } + if (isSelected) + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2); + return menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"; + } + opacity: modelData.enabled ? 1 : 0.5 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: modelData.icon || "" + size: 16 + color: { + if (!modelData.enabled) + return Theme.surfaceVariantText; + const isSelected = keyboardNavigation && selectedIndex === index; + if (modelData.dangerous && (menuItemArea.containsMouse || isSelected)) + return Theme.error; + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: modelData.text || "" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + color: { + if (!modelData.enabled) + return Theme.surfaceVariantText; + const isSelected = keyboardNavigation && selectedIndex === index; + if (modelData.dangerous && (menuItemArea.containsMouse || isSelected)) + return Theme.error; + return Theme.surfaceText; + } + anchors.verticalCenter: parent.verticalCenter + } + } + + DankRipple { + id: menuItemRipple + rippleColor: modelData.dangerous ? Theme.error : Theme.surfaceText + cornerRadius: menuItem.radius + } + + MouseArea { + id: menuItemArea + anchors.fill: parent + hoverEnabled: true + cursorShape: modelData.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: modelData.enabled ?? false + onEntered: { + keyboardNavigation = false; + selectedIndex = index; + } + onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y) + onClicked: modelData.action() + } + } + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessListPopout.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessListPopout.qml new file mode 100644 index 0000000..3adbe07 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessListPopout.qml @@ -0,0 +1,535 @@ +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Modules.ProcessList +import qs.Services +import qs.Widgets + +DankPopout { + id: processListPopout + + layerNamespace: "dms:process-list-popout" + + property var parentWidget: null + property var triggerScreen: null + property string searchText: "" + property string expandedPid: "" + property string processFilter: "all" + + function hide() { + close(); + if (processContextMenu.visible) + processContextMenu.close(); + } + + function show() { + open(); + } + + popupWidth: Math.round(Theme.fontSizeMedium * 46) + popupHeight: Math.round(Theme.fontSizeMedium * 39) + triggerWidth: 55 + positioning: "" + screen: triggerScreen + shouldBeVisible: false + + onBackgroundClicked: { + if (processContextMenu.visible) + processContextMenu.close(); + close(); + } + + onShouldBeVisibleChanged: { + if (!shouldBeVisible) { + searchText = ""; + expandedPid = ""; + processFilter = "all"; + } + } + + Ref { + service: DgopService + } + + ProcessContextMenu { + id: processContextMenu + } + + content: Component { + Rectangle { + id: processListContent + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + radius: Theme.cornerRadius + color: "transparent" + clip: true + focus: true + + Component.onCompleted: { + if (processListPopout.shouldBeVisible) + searchField.forceActiveFocus(); + processContextMenu.parent = processListContent; + processContextMenu.parentFocusItem = processListContent; + } + + Keys.onPressed: event => { + if (processContextMenu.visible) + return; + + switch (event.key) { + case Qt.Key_Escape: + if (processListPopout.searchText.length > 0) { + processListPopout.searchText = ""; + event.accepted = true; + return; + } + if (processesView.keyboardNavigationActive) { + processesView.reset(); + event.accepted = true; + return; + } + processListPopout.close(); + event.accepted = true; + return; + case Qt.Key_F: + if (event.modifiers & Qt.ControlModifier) { + searchField.forceActiveFocus(); + event.accepted = true; + return; + } + break; + } + + processesView.handleKey(event); + } + + Connections { + target: processListPopout + function onShouldBeVisibleChanged() { + if (processListPopout.shouldBeVisible) { + Qt.callLater(() => searchField.forceActiveFocus()); + } else { + processesView.reset(); + processFilterGroup.currentIndex = 0; + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: Theme.spacingS + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacingM + + Row { + spacing: Theme.spacingS + + DankIcon { + name: "analytics" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Processes") + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + Item { + Layout.fillWidth: true + } + + DankButtonGroup { + id: processFilterGroup + Layout.minimumWidth: implicitWidth + model: [I18n.tr("All"), I18n.tr("User"), I18n.tr("System")] + currentIndex: 0 + checkEnabled: false + buttonHeight: Math.round(Theme.fontSizeSmall * 2.4) + minButtonWidth: 0 + buttonPadding: Theme.spacingM + textSize: Theme.fontSizeSmall + onSelectionChanged: (index, selected) => { + if (!selected) + return; + currentIndex = index; + switch (index) { + case 0: + processListPopout.processFilter = "all"; + return; + case 1: + processListPopout.processFilter = "user"; + return; + case 2: + processListPopout.processFilter = "system"; + return; + } + } + } + + DankTextField { + id: searchField + Layout.fillWidth: true + Layout.minimumWidth: Theme.fontSizeMedium * 8 + Layout.preferredHeight: Theme.fontSizeMedium * 2.5 + placeholderText: I18n.tr("Search...") + leftIconName: "search" + showClearButton: true + text: processListPopout.searchText + onTextChanged: processListPopout.searchText = text + ignoreUpDownKeys: true + keyForwardTargets: [processListContent] + } + } + + Item { + id: statsContainer + Layout.fillWidth: true + Layout.preferredHeight: Math.max(leftInfo.height, gaugesRow.height) + Theme.spacingS + + function compactMem(kb) { + if (kb < 1024 * 1024) { + const mb = kb / 1024; + return mb >= 100 ? mb.toFixed(0) + " MB" : mb.toFixed(1) + " MB"; + } + const gb = kb / (1024 * 1024); + return gb >= 10 ? gb.toFixed(0) + " GB" : gb.toFixed(1) + " GB"; + } + + readonly property real gaugeSize: Theme.fontSizeMedium * 6.5 + + readonly property var enabledGpusWithTemp: { + if (!SessionData.enabledGpuPciIds || SessionData.enabledGpuPciIds.length === 0) + return []; + const result = []; + for (const gpu of DgopService.availableGpus) { + if (SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 && gpu.temperature > 0) + result.push(gpu); + } + return result; + } + readonly property bool hasGpu: enabledGpusWithTemp.length > 0 + + Row { + id: leftInfo + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Rectangle { + width: Theme.fontSizeMedium * 3 + height: width + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + + SystemLogo { + anchors.centerIn: parent + width: parent.width * 0.7 + height: width + colorOverride: Theme.primary + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS / 2 + + StyledText { + text: DgopService.hostname || "localhost" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Bold + color: Theme.surfaceText + } + + StyledText { + text: DgopService.distribution || "Linux" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + Row { + spacing: Theme.spacingS + + Row { + spacing: Theme.spacingXS + + DankIcon { + name: "schedule" + size: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: DgopService.shortUptime || "--" + font.pixelSize: Theme.fontSizeSmall - 1 + font.family: SettingsData.monoFontFamily + color: Theme.surfaceVariantText + } + } + + StyledText { + text: "•" + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + } + + StyledText { + text: DgopService.processCount + " " + I18n.tr("procs", "short for processes") + font.pixelSize: Theme.fontSizeSmall - 1 + font.family: SettingsData.monoFontFamily + color: Theme.surfaceVariantText + } + } + } + } + + Row { + id: gaugesRow + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + CircleGauge { + width: statsContainer.gaugeSize + height: statsContainer.gaugeSize + value: DgopService.cpuUsage / 100 + label: DgopService.cpuUsage.toFixed(0) + "%" + sublabel: "CPU" + detail: DgopService.cpuTemperature > 0 ? (DgopService.cpuTemperature.toFixed(0) + "°") : "" + accentColor: DgopService.cpuUsage > 80 ? Theme.error : (DgopService.cpuUsage > 50 ? Theme.warning : Theme.primary) + detailColor: DgopService.cpuTemperature > 85 ? Theme.error : (DgopService.cpuTemperature > 70 ? Theme.warning : Theme.surfaceVariantText) + } + + CircleGauge { + width: statsContainer.gaugeSize + height: statsContainer.gaugeSize + value: DgopService.memoryUsage / 100 + label: statsContainer.compactMem(DgopService.usedMemoryKB) + sublabel: I18n.tr("Memory") + detail: DgopService.totalSwapKB > 0 ? ("+" + statsContainer.compactMem(DgopService.usedSwapKB)) : "" + accentColor: DgopService.memoryUsage > 90 ? Theme.error : (DgopService.memoryUsage > 70 ? Theme.warning : Theme.secondary) + } + + CircleGauge { + width: statsContainer.gaugeSize + height: statsContainer.gaugeSize + visible: statsContainer.hasGpu + + readonly property var gpu: statsContainer.enabledGpusWithTemp[0] ?? null + readonly property color vendorColor: { + const vendor = (gpu?.vendor ?? "").toLowerCase(); + if (vendor.includes("nvidia")) + return Theme.success; + if (vendor.includes("amd")) + return Theme.error; + if (vendor.includes("intel")) + return Theme.info; + return Theme.info; + } + + value: Math.min(1, (gpu?.temperature ?? 0) / 100) + label: (gpu?.temperature ?? 0) > 0 ? ((gpu?.temperature ?? 0).toFixed(0) + "°C") : "--" + sublabel: "GPU" + accentColor: { + const temp = gpu?.temperature ?? 0; + if (temp > 85) + return Theme.error; + if (temp > 70) + return Theme.warning; + return vendorColor; + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + clip: true + + ProcessesView { + id: processesView + anchors.fill: parent + anchors.margins: Theme.spacingS + searchText: processListPopout.searchText + expandedPid: processListPopout.expandedPid + processFilter: processListPopout.processFilter + contextMenu: processContextMenu + onExpandedPidChanged: processListPopout.expandedPid = expandedPid + } + } + } + } + } + + component CircleGauge: Item { + id: gaugeRoot + + property real value: 0 + property string label: "" + property string sublabel: "" + property string detail: "" + property color accentColor: Theme.primary + property color detailColor: Theme.surfaceVariantText + + readonly property real thickness: Math.max(4, Math.min(width, height) / 15) + readonly property real glowExtra: thickness * 1.4 + readonly property real arcPadding: (thickness + glowExtra) / 2 + + readonly property real innerDiameter: width - (arcPadding + thickness + glowExtra) * 2 + readonly property real maxTextWidth: innerDiameter * 0.9 + readonly property real baseLabelSize: Math.round(width * 0.18) + readonly property real labelSize: Math.round(Math.min(baseLabelSize, maxTextWidth / Math.max(1, label.length * 0.65))) + readonly property real sublabelSize: Math.round(Math.min(width * 0.13, maxTextWidth / Math.max(1, sublabel.length * 0.7))) + readonly property real detailSize: Math.round(Math.min(width * 0.12, maxTextWidth / Math.max(1, detail.length * 0.65))) + + property real animValue: 0 + + onValueChanged: animValue = Math.min(1, Math.max(0, value)) + + Behavior on animValue { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Easing.OutCubic + } + } + + Component.onCompleted: animValue = Math.min(1, Math.max(0, value)) + + Canvas { + id: glowCanvas + anchors.fill: parent + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + const cx = width / 2; + const cy = height / 2; + const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding; + const startAngle = -Math.PI * 0.5; + const endAngle = Math.PI * 1.5; + + ctx.lineCap = "round"; + + if (gaugeRoot.animValue > 0) { + const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue; + ctx.beginPath(); + ctx.arc(cx, cy, radius, startAngle, prog); + ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.2); + ctx.lineWidth = gaugeRoot.thickness + gaugeRoot.glowExtra; + ctx.stroke(); + } + } + + Connections { + target: gaugeRoot + function onAnimValueChanged() { + glowCanvas.requestPaint(); + } + function onAccentColorChanged() { + glowCanvas.requestPaint(); + } + function onWidthChanged() { + glowCanvas.requestPaint(); + } + function onHeightChanged() { + glowCanvas.requestPaint(); + } + } + + Component.onCompleted: requestPaint() + } + + Canvas { + id: arcCanvas + anchors.fill: parent + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + const cx = width / 2; + const cy = height / 2; + const radius = (Math.min(width, height) / 2) - gaugeRoot.arcPadding; + const startAngle = -Math.PI * 0.5; + const endAngle = Math.PI * 1.5; + + ctx.lineCap = "round"; + + ctx.beginPath(); + ctx.arc(cx, cy, radius, startAngle, endAngle); + ctx.strokeStyle = Qt.rgba(gaugeRoot.accentColor.r, gaugeRoot.accentColor.g, gaugeRoot.accentColor.b, 0.1); + ctx.lineWidth = gaugeRoot.thickness; + ctx.stroke(); + + if (gaugeRoot.animValue > 0) { + const prog = startAngle + (endAngle - startAngle) * gaugeRoot.animValue; + ctx.beginPath(); + ctx.arc(cx, cy, radius, startAngle, prog); + ctx.strokeStyle = gaugeRoot.accentColor; + ctx.lineWidth = gaugeRoot.thickness; + ctx.stroke(); + } + } + + Connections { + target: gaugeRoot + function onAnimValueChanged() { + arcCanvas.requestPaint(); + } + function onAccentColorChanged() { + arcCanvas.requestPaint(); + } + function onWidthChanged() { + arcCanvas.requestPaint(); + } + function onHeightChanged() { + arcCanvas.requestPaint(); + } + } + + Component.onCompleted: requestPaint() + } + + Column { + anchors.centerIn: parent + spacing: 1 + + StyledText { + text: gaugeRoot.label + font.pixelSize: gaugeRoot.labelSize + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: gaugeRoot.sublabel + font.pixelSize: gaugeRoot.sublabelSize + font.weight: Font.Medium + color: gaugeRoot.accentColor + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: gaugeRoot.detail + font.pixelSize: gaugeRoot.detailSize + font.family: SettingsData.monoFontFamily + color: gaugeRoot.detailColor + anchors.horizontalCenter: parent.horizontalCenter + visible: gaugeRoot.detail.length > 0 + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessesView.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessesView.qml new file mode 100644 index 0000000..181cf15 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/ProcessesView.qml @@ -0,0 +1,769 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property string searchText: "" + property string expandedPid: "" + property var contextMenu: null + property string processFilter: "all" // "all", "user", "system" + + property int selectedIndex: -1 + property bool keyboardNavigationActive: false + property int forceRefreshCount: 0 + + readonly property bool pauseUpdates: (contextMenu?.visible ?? false) || expandedPid.length > 0 + readonly property bool shouldUpdate: !pauseUpdates || forceRefreshCount > 0 + property var cachedProcesses: [] + + signal openContextMenuRequested(int index, real x, real y, bool fromKeyboard) + + onFilteredProcessesChanged: { + if (!shouldUpdate) + return; + cachedProcesses = filteredProcesses; + if (forceRefreshCount > 0) + forceRefreshCount--; + } + + onShouldUpdateChanged: { + if (shouldUpdate) + cachedProcesses = filteredProcesses; + } + + readonly property var filteredProcesses: { + if (!DgopService.allProcesses || DgopService.allProcesses.length === 0) + return []; + + let procs = DgopService.allProcesses.slice(); + + if (processFilter === "user") { + procs = procs.filter(p => p.username === UserInfoService.username); + } else if (processFilter === "system") { + procs = procs.filter(p => p.username !== UserInfoService.username); + } + + if (searchText.length > 0) { + const search = searchText.toLowerCase(); + procs = procs.filter(p => { + const cmd = (p.command || "").toLowerCase(); + const fullCmd = (p.fullCommand || "").toLowerCase(); + const pid = p.pid.toString(); + return cmd.includes(search) || fullCmd.includes(search) || pid.includes(search); + }); + } + + const asc = DgopService.sortAscending; + procs.sort((a, b) => { + let valueA, valueB, result; + switch (DgopService.currentSort) { + case "cpu": + valueA = a.cpu || 0; + valueB = b.cpu || 0; + result = valueB - valueA; + break; + case "memory": + valueA = a.memoryKB || 0; + valueB = b.memoryKB || 0; + result = valueB - valueA; + break; + case "name": + valueA = (a.command || "").toLowerCase(); + valueB = (b.command || "").toLowerCase(); + result = valueA.localeCompare(valueB); + break; + case "pid": + valueA = a.pid || 0; + valueB = b.pid || 0; + result = valueA - valueB; + break; + default: + return 0; + } + return asc ? -result : result; + }); + + return procs; + } + + function selectNext() { + if (cachedProcesses.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = Math.min(selectedIndex + 1, cachedProcesses.length - 1); + ensureVisible(); + } + + function selectPrevious() { + if (cachedProcesses.length === 0) + return; + keyboardNavigationActive = true; + if (selectedIndex <= 0) { + selectedIndex = -1; + keyboardNavigationActive = false; + return; + } + selectedIndex = selectedIndex - 1; + ensureVisible(); + } + + function selectFirst() { + if (cachedProcesses.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = 0; + ensureVisible(); + } + + function selectLast() { + if (cachedProcesses.length === 0) + return; + keyboardNavigationActive = true; + selectedIndex = cachedProcesses.length - 1; + ensureVisible(); + } + + function toggleExpand() { + if (selectedIndex < 0 || selectedIndex >= cachedProcesses.length) + return; + const process = cachedProcesses[selectedIndex]; + const pidStr = (process?.pid ?? -1).toString(); + expandedPid = (expandedPid === pidStr) ? "" : pidStr; + } + + function openContextMenu() { + if (selectedIndex < 0 || selectedIndex >= cachedProcesses.length) + return; + const delegate = processListView.itemAtIndex(selectedIndex); + if (!delegate) + return; + const process = cachedProcesses[selectedIndex]; + if (!process || !contextMenu) + return; + contextMenu.processData = process; + const itemPos = delegate.mapToItem(contextMenu.parent, delegate.width / 2, delegate.height / 2); + contextMenu.parentFocusItem = root; + contextMenu.show(itemPos.x, itemPos.y, true); + } + + function reset() { + selectedIndex = -1; + keyboardNavigationActive = false; + expandedPid = ""; + } + + function forceRefresh(count) { + forceRefreshCount = count || 3; + } + + function ensureVisible() { + if (selectedIndex < 0) + return; + processListView.positionViewAtIndex(selectedIndex, ListView.Contain); + } + + function handleKey(event) { + switch (event.key) { + case Qt.Key_Down: + selectNext(); + event.accepted = true; + return; + case Qt.Key_Up: + selectPrevious(); + event.accepted = true; + return; + case Qt.Key_J: + if (event.modifiers & Qt.ControlModifier) { + selectNext(); + event.accepted = true; + } + return; + case Qt.Key_K: + if (event.modifiers & Qt.ControlModifier) { + selectPrevious(); + event.accepted = true; + } + return; + case Qt.Key_Home: + selectFirst(); + event.accepted = true; + return; + case Qt.Key_End: + selectLast(); + event.accepted = true; + return; + case Qt.Key_Space: + if (keyboardNavigationActive) { + toggleExpand(); + event.accepted = true; + } + return; + case Qt.Key_Return: + case Qt.Key_Enter: + if (keyboardNavigationActive) { + toggleExpand(); + event.accepted = true; + } + return; + case Qt.Key_Menu: + case Qt.Key_F10: + if (keyboardNavigationActive && selectedIndex >= 0) { + openContextMenu(); + event.accepted = true; + } + return; + } + } + + Component.onCompleted: { + DgopService.addRef(["processes", "cpu", "memory", "system"]); + cachedProcesses = filteredProcesses; + } + + Component.onDestruction: { + DgopService.removeRef(["processes", "cpu", "memory", "system"]); + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 36 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: 0 + + SortableHeader { + Layout.fillWidth: true + Layout.minimumWidth: 200 + text: I18n.tr("Name") + sortKey: "name" + currentSort: DgopService.currentSort + sortAscending: DgopService.sortAscending + onClicked: DgopService.toggleSort("name") + alignment: Text.AlignLeft + } + + SortableHeader { + Layout.preferredWidth: 100 + text: "CPU" + sortKey: "cpu" + currentSort: DgopService.currentSort + sortAscending: DgopService.sortAscending + onClicked: DgopService.toggleSort("cpu") + } + + SortableHeader { + Layout.preferredWidth: 100 + text: I18n.tr("Memory") + sortKey: "memory" + currentSort: DgopService.currentSort + sortAscending: DgopService.sortAscending + onClicked: DgopService.toggleSort("memory") + } + + SortableHeader { + Layout.preferredWidth: 80 + text: "PID" + sortKey: "pid" + currentSort: DgopService.currentSort + sortAscending: DgopService.sortAscending + onClicked: DgopService.toggleSort("pid") + } + + Item { + Layout.preferredWidth: 40 + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Theme.outlineLight + } + + DankListView { + id: processListView + + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 2 + + add: root.searchText.length > 0 ? ListViewTransitions.add : null + remove: root.searchText.length > 0 ? ListViewTransitions.remove : null + displaced: root.searchText.length > 0 ? ListViewTransitions.displaced : null + move: root.searchText.length > 0 ? ListViewTransitions.move : null + + model: ScriptModel { + values: root.cachedProcesses + objectProp: "pid" + } + + delegate: ProcessItem { + required property var modelData + required property int index + + width: processListView.width + process: modelData + isExpanded: root.expandedPid === (modelData?.pid ?? -1).toString() + isSelected: root.keyboardNavigationActive && root.selectedIndex === index + contextMenu: root.contextMenu + onToggleExpand: { + const pidStr = (modelData?.pid ?? -1).toString(); + root.expandedPid = (root.expandedPid === pidStr) ? "" : pidStr; + } + onClicked: { + root.keyboardNavigationActive = true; + root.selectedIndex = index; + } + onContextMenuRequested: (mouseX, mouseY) => { + if (root.contextMenu) { + root.contextMenu.processData = modelData; + root.contextMenu.parentFocusItem = root; + const globalPos = mapToItem(root.contextMenu.parent, mouseX, mouseY); + root.contextMenu.show(globalPos.x, globalPos.y, false); + } + } + } + + Rectangle { + anchors.centerIn: parent + width: 300 + height: 100 + radius: Theme.cornerRadius + color: "transparent" + visible: root.cachedProcesses.length === 0 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingM + + DankIcon { + name: root.searchText.length > 0 ? "search_off" : "hourglass_empty" + size: 32 + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: I18n.tr("No matching processes", "empty state in process list") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceVariantText + anchors.horizontalCenter: parent.horizontalCenter + visible: root.searchText.length > 0 + } + } + } + } + } + + component SortableHeader: Item { + id: headerItem + + property string text: "" + property string sortKey: "" + property string currentSort: "" + property bool sortAscending: false + property int alignment: Text.AlignHCenter + + signal clicked + + readonly property bool isActive: sortKey === currentSort + + height: 36 + + Rectangle { + anchors.fill: parent + anchors.margins: 2 + radius: Theme.cornerRadius + color: headerItem.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (headerMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06) : "transparent") + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: 4 + + Item { + Layout.fillWidth: headerItem.alignment === Text.AlignLeft + visible: headerItem.alignment !== Text.AlignLeft + } + + StyledText { + text: headerItem.text + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: headerItem.isActive ? Font.Bold : Font.Medium + color: headerItem.isActive ? Theme.primary : Theme.surfaceText + opacity: headerItem.isActive ? 1 : 0.8 + } + + DankIcon { + name: headerItem.sortAscending ? "arrow_upward" : "arrow_downward" + size: Theme.fontSizeSmall + color: Theme.primary + visible: headerItem.isActive + } + + Item { + Layout.fillWidth: headerItem.alignment !== Text.AlignLeft + visible: headerItem.alignment === Text.AlignLeft + } + } + + MouseArea { + id: headerMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: headerItem.clicked() + } + } + + component ProcessItem: Rectangle { + id: processItemRoot + + property var process: null + property bool isExpanded: false + property bool isSelected: false + property var contextMenu: null + + signal toggleExpand + signal clicked + signal contextMenuRequested(real mouseX, real mouseY) + + readonly property int processPid: process?.pid ?? 0 + readonly property real processCpu: process?.cpu ?? 0 + readonly property int processMemKB: process?.memoryKB ?? 0 + readonly property string processCmd: process?.command ?? "" + readonly property string processFullCmd: process?.fullCommand ?? processCmd + + height: isExpanded ? (44 + expandedRect.height + Theme.spacingXS) : 44 + radius: Theme.cornerRadius + color: { + if (isSelected) + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15); + return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"; + } + border.color: { + if (isSelected) + return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3); + return processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"; + } + border.width: 1 + clip: true + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + } + + MouseArea { + id: processMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + processItemRoot.contextMenuRequested(mouse.x, mouse.y); + return; + } + processItemRoot.clicked(); + processItemRoot.toggleExpand(); + } + } + + Column { + anchors.fill: parent + spacing: 0 + + Item { + width: parent.width + height: 44 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Theme.spacingS + anchors.rightMargin: Theme.spacingS + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.minimumWidth: 200 + height: parent.height + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: DgopService.getProcessIcon(processItemRoot.processCmd) + size: Theme.iconSize - 4 + color: { + if (processItemRoot.processCpu > 80) + return Theme.error; + if (processItemRoot.processCpu > 50) + return Theme.warning; + return Theme.surfaceText; + } + opacity: 0.8 + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: processItemRoot.processCmd + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + width: Math.min(implicitWidth, 280) + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Item { + Layout.preferredWidth: 100 + height: parent.height + + Rectangle { + anchors.centerIn: parent + width: 70 + height: 24 + radius: Theme.cornerRadius + color: { + if (processItemRoot.processCpu > 80) + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15); + if (processItemRoot.processCpu > 50) + return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12); + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06); + } + + StyledText { + anchors.centerIn: parent + text: DgopService.formatCpuUsage(processItemRoot.processCpu) + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: { + if (processItemRoot.processCpu > 80) + return Theme.error; + if (processItemRoot.processCpu > 50) + return Theme.warning; + return Theme.surfaceText; + } + } + } + } + + Item { + Layout.preferredWidth: 100 + height: parent.height + + Rectangle { + anchors.centerIn: parent + width: 70 + height: 24 + radius: Theme.cornerRadius + color: { + if (processItemRoot.processMemKB > 2 * 1024 * 1024) + return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.15); + if (processItemRoot.processMemKB > 1024 * 1024) + return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12); + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.06); + } + + StyledText { + anchors.centerIn: parent + text: DgopService.formatMemoryUsage(processItemRoot.processMemKB) + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + font.weight: Font.Bold + color: { + if (processItemRoot.processMemKB > 2 * 1024 * 1024) + return Theme.error; + if (processItemRoot.processMemKB > 1024 * 1024) + return Theme.warning; + return Theme.surfaceText; + } + } + } + } + + Item { + Layout.preferredWidth: 80 + height: parent.height + + StyledText { + anchors.centerIn: parent + text: processItemRoot.processPid > 0 ? processItemRoot.processPid.toString() : "" + font.pixelSize: Theme.fontSizeSmall + font.family: SettingsData.monoFontFamily + color: Theme.surfaceVariantText + } + } + + Item { + Layout.preferredWidth: 40 + height: parent.height + + DankIcon { + anchors.centerIn: parent + name: processItemRoot.isExpanded ? "expand_less" : "expand_more" + size: Theme.iconSize - 4 + color: Theme.surfaceVariantText + } + } + } + } + + Rectangle { + id: expandedRect + width: parent.width - Theme.spacingM * 2 + height: processItemRoot.isExpanded ? (expandedContent.implicitHeight + Theme.spacingS * 2) : 0 + anchors.horizontalCenter: parent.horizontalCenter + radius: Theme.cornerRadius - 2 + color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.6) + clip: true + visible: processItemRoot.isExpanded + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Column { + id: expandedContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingS + spacing: Theme.spacingXS + + RowLayout { + width: parent.width + spacing: Theme.spacingS + + StyledText { + id: cmdLabel + text: I18n.tr("Full Command:", "process detail label") + font.pixelSize: Theme.fontSizeSmall - 2 + font.weight: Font.Bold + color: Theme.surfaceVariantText + Layout.alignment: Qt.AlignVCenter + } + + StyledText { + id: cmdText + text: processItemRoot.processFullCmd + font.pixelSize: Theme.fontSizeSmall - 2 + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + elide: Text.ElideMiddle + } + + Rectangle { + id: copyBtn + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignVCenter + radius: Theme.cornerRadius - 2 + color: copyMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "content_copy" + size: 14 + color: copyMouseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText + } + + MouseArea { + id: copyMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Quickshell.execDetached(["dms", "cl", "copy", processItemRoot.processFullCmd]); + } + } + } + } + + Row { + spacing: Theme.spacingL + + Row { + spacing: Theme.spacingXS + + StyledText { + text: "PPID:" + font.pixelSize: Theme.fontSizeSmall - 2 + font.weight: Font.Bold + color: Theme.surfaceVariantText + } + + StyledText { + text: (processItemRoot.process?.ppid ?? 0) > 0 ? processItemRoot.process.ppid.toString() : "--" + font.pixelSize: Theme.fontSizeSmall - 2 + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + } + } + + Row { + spacing: Theme.spacingXS + + StyledText { + text: "Mem:" + font.pixelSize: Theme.fontSizeSmall - 2 + font.weight: Font.Bold + color: Theme.surfaceVariantText + } + + StyledText { + text: (processItemRoot.process?.memoryPercent ?? 0).toFixed(1) + "%" + font.pixelSize: Theme.fontSizeSmall - 2 + font.family: SettingsData.monoFontFamily + color: Theme.surfaceText + } + } + } + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/SystemView.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/ProcessList/SystemView.qml new file mode 100644 index 0000000..244129d --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/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 + } + } +} |