diff --git a/README.md b/README.md index 43aae663..0900c707 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Specifically created for [Niri](https://github.com/YaLTeR/niri). ```bash # Arch -paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava +paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard # Some dependencies are optional # - cava for audio visualizer, without it music will just randomly visualize diff --git a/Services/ProcessMonitorService.qml b/Services/ProcessMonitorService.qml new file mode 100644 index 00000000..c2251230 --- /dev/null +++ b/Services/ProcessMonitorService.qml @@ -0,0 +1,173 @@ +import QtQuick +import Quickshell +import Quickshell.Io +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + + // Process list properties + property var processes: [] + property bool isUpdating: false + property int processUpdateInterval: 3000 + + // Sorting options + property string sortBy: "cpu" // "cpu", "memory", "name", "pid" + property bool sortDescending: true + property int maxProcesses: 20 + + Component.onCompleted: { + console.log("ProcessMonitorService: Starting initialization...") + updateProcessList() + console.log("ProcessMonitorService: Initialization complete") + } + + // Process monitoring with ps command + Process { + id: processListProcess + command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + const lines = text.trim().split('\n') + const newProcesses = [] + + // Skip header line + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim() + if (!line) continue + + // Parse ps output: PID PPID %CPU %MEM COMMAND CMD + const parts = line.split(/\s+/) + if (parts.length >= 6) { + const pid = parseInt(parts[0]) + const ppid = parseInt(parts[1]) + const cpu = parseFloat(parts[2]) + const memory = parseFloat(parts[3]) + const command = parts[4] + const fullCmd = parts.slice(5).join(' ') + + newProcesses.push({ + pid: pid, + ppid: ppid, + cpu: cpu, + memory: memory, + command: command, + fullCommand: fullCmd, + displayName: command.length > 15 ? command.substring(0, 15) + "..." : command + }) + } + } + + root.processes = newProcesses + root.isUpdating = false + } + } + } + + onExited: (exitCode) => { + root.isUpdating = false + if (exitCode !== 0) { + console.warn("Process list check failed with exit code:", exitCode) + } + } + } + + // Process monitoring timer + Timer { + id: processTimer + interval: root.processUpdateInterval + running: true + repeat: true + + onTriggered: { + updateProcessList() + } + } + + // Public functions + function updateProcessList() { + if (!root.isUpdating) { + root.isUpdating = true + + // Update sort command based on current sort option + let sortOption = "" + switch (root.sortBy) { + case "cpu": + sortOption = sortDescending ? "--sort=-pcpu" : "--sort=+pcpu" + break + case "memory": + sortOption = sortDescending ? "--sort=-pmem" : "--sort=+pmem" + break + case "name": + sortOption = sortDescending ? "--sort=-comm" : "--sort=+comm" + break + case "pid": + sortOption = sortDescending ? "--sort=-pid" : "--sort=+pid" + break + default: + sortOption = "--sort=-pcpu" + } + + processListProcess.command = ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,comm,cmd " + sortOption + " | head -" + (root.maxProcesses + 1)] + processListProcess.running = true + } + } + + function setSortBy(newSortBy) { + if (newSortBy !== root.sortBy) { + root.sortBy = newSortBy + updateProcessList() + } + } + + function toggleSortOrder() { + root.sortDescending = !root.sortDescending + updateProcessList() + } + + function killProcess(pid) { + if (pid > 0) { + const killCmd = ["bash", "-c", "kill " + pid] + const killProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ${JSON.stringify(killCmd)} + running: true + onExited: (exitCode) => { + if (exitCode === 0) { + console.log("Process killed successfully:", ${pid}) + } else { + console.warn("Failed to kill process:", ${pid}, "exit code:", exitCode) + } + destroy() + } + } + `, root) + } + } + + function getProcessIcon(command) { + // Return appropriate Material Design icon for common processes + const cmd = command.toLowerCase() + if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web" + if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) return "code" + if (cmd.includes("terminal") || cmd.includes("bash") || cmd.includes("zsh")) return "terminal" + if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) return "music_note" + if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) return "play_circle" + if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings" + return "memory" // Default process icon + } + + function formatCpuUsage(cpu) { + return cpu.toFixed(1) + "%" + } + + function formatMemoryUsage(memory) { + return memory.toFixed(1) + "%" + } +} \ No newline at end of file diff --git a/Services/SystemMonitorService.qml b/Services/SystemMonitorService.qml index 848d92cc..f5b2f4c0 100644 --- a/Services/SystemMonitorService.qml +++ b/Services/SystemMonitorService.qml @@ -13,6 +13,9 @@ Singleton { property string cpuModel: "" property real cpuFrequency: 0.0 + // Previous CPU stats for accurate calculation + property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0] + // Memory properties property real memoryUsage: 0.0 property real totalMemory: 0.0 @@ -26,8 +29,8 @@ Singleton { property real cpuTemperature: 0.0 // Update intervals - property int cpuUpdateInterval: 2000 - property int memoryUpdateInterval: 3000 + property int cpuUpdateInterval: 1000 + property int memoryUpdateInterval: 2000 property int temperatureUpdateInterval: 5000 Component.onCompleted: { @@ -63,16 +66,33 @@ Singleton { } } - // CPU usage monitoring + // CPU usage monitoring with accurate calculation Process { id: cpuUsageProcess - command: ["bash", "-c", "grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$3+$4+$5)} END {printf \"%.1f\", usage}'"] + command: ["bash", "-c", "head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}'"] running: false stdout: StdioCollector { onStreamFinished: { if (text.trim()) { - root.cpuUsage = parseFloat(text.trim()) + const stats = text.trim().split(" ").map(x => parseInt(x)) + if (root.prevCpuStats[0] > 0) { + // Calculate differences + let diffs = [] + for (let i = 0; i < 8; i++) { + diffs[i] = stats[i] - root.prevCpuStats[i] + } + + // Calculate total and idle time + const totalTime = diffs.reduce((a, b) => a + b, 0) + const idleTime = diffs[3] + diffs[4] // idle + iowait + + // CPU usage percentage + if (totalTime > 0) { + root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100)) + } + } + root.prevCpuStats = stats } } } diff --git a/Services/WeatherService.qml b/Services/WeatherService.qml index 13b880a8..0d8cf0fa 100644 --- a/Services/WeatherService.qml +++ b/Services/WeatherService.qml @@ -9,6 +9,7 @@ Singleton { property var weather: ({ available: false, + loading: true, temp: 0, tempF: 0, city: "", @@ -104,6 +105,7 @@ Singleton { root.weather = { available: true, + loading: false, temp: Number(current.temp_C) || 0, tempF: Number(current.temp_F) || 0, city: location.areaName?.[0]?.value || "Unknown", @@ -122,6 +124,7 @@ Singleton { } catch (e) { console.warn("Failed to parse weather data:", e.message) root.weather.available = false + root.weather.loading = false } } } @@ -130,6 +133,7 @@ Singleton { if (exitCode !== 0) { console.warn("Weather fetch failed with exit code:", exitCode) root.weather.available = false + root.weather.loading = false } } } diff --git a/Services/qmldir b/Services/qmldir index 48dddfcd..264ba80c 100644 --- a/Services/qmldir +++ b/Services/qmldir @@ -8,6 +8,7 @@ singleton BluetoothService 1.0 BluetoothService.qml singleton BrightnessService 1.0 BrightnessService.qml singleton BatteryService 1.0 BatteryService.qml singleton SystemMonitorService 1.0 SystemMonitorService.qml +singleton ProcessMonitorService 1.0 ProcessMonitorService.qml singleton AppSearchService 1.0 AppSearchService.qml singleton LauncherService 1.0 LauncherService.qml singleton NiriWorkspaceService 1.0 NiriWorkspaceService.qml diff --git a/Widgets/CenterCommandCenter/WeatherWidget.qml b/Widgets/CenterCommandCenter/WeatherWidget.qml index e6e6a00d..912a59ae 100644 --- a/Widgets/CenterCommandCenter/WeatherWidget.qml +++ b/Widgets/CenterCommandCenter/WeatherWidget.qml @@ -41,7 +41,7 @@ Rectangle { Column { anchors.centerIn: parent spacing: theme.spacingS - visible: !weather || !weather.available + visible: !weather || !weather.available || weather.temp === 0 Text { text: "cloud_off" @@ -63,7 +63,7 @@ Rectangle { Row { anchors.fill: parent spacing: theme.spacingL - visible: weather + visible: weather && weather.available && weather.temp !== 0 // Weather icon Text { @@ -108,7 +108,7 @@ Rectangle { columns: 2 spacing: theme.spacingM anchors.horizontalCenter: parent.horizontalCenter - visible: weather !== null + visible: weather && weather.available && weather.temp !== 0 Row { spacing: theme.spacingXS diff --git a/Widgets/CpuMonitorWidget.qml b/Widgets/CpuMonitorWidget.qml index db210460..16c91ba1 100644 --- a/Widgets/CpuMonitorWidget.qml +++ b/Widgets/CpuMonitorWidget.qml @@ -2,12 +2,14 @@ import QtQuick import QtQuick.Controls import "../Common" import "../Services" +import "." Rectangle { id: cpuWidget property bool showPercentage: true property bool showIcon: true + property var processDropdown: null width: 55 height: 30 @@ -24,9 +26,13 @@ Rectangle { id: cpuArea anchors.fill: parent hoverEnabled: true + cursorShape: Qt.PointingHandCursor onClicked: { - // CPU widget clicked + if (processDropdown) { + ProcessMonitorService.setSortBy("cpu") + processDropdown.toggle() + } } } diff --git a/Widgets/ProcessListDropdown.qml b/Widgets/ProcessListDropdown.qml new file mode 100644 index 00000000..e7a7666c --- /dev/null +++ b/Widgets/ProcessListDropdown.qml @@ -0,0 +1,729 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Io +import "../Common" +import "../Services" + +PanelWindow { + id: processDropdown + + property bool isVisible: false + property var parentWidget: null + + visible: isVisible + + implicitWidth: 500 + implicitHeight: 500 + + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + // Click outside to close + MouseArea { + anchors.fill: parent + onClicked: processDropdown.hide() + } + + Rectangle { + id: dropdownContent + width: Math.min(500, parent.width - Theme.spacingL * 2) + height: Math.min(500, parent.height - Theme.barHeight - Theme.spacingS * 2) + x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL) + y: Theme.barHeight + Theme.spacingXS + + radius: Theme.cornerRadiusLarge + color: Theme.surfaceContainer + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + clip: true + + opacity: processDropdown.isVisible ? 1.0 : 0.0 + scale: processDropdown.isVisible ? 1.0 : 0.85 + + // Add shadow effect + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 8 + shadowBlur: 1.0 + shadowColor: Qt.rgba(0, 0, 0, 0.15) + shadowOpacity: processDropdown.isVisible ? 0.15 : 0 + } + + // Smooth animations + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + // Click inside dropdown - consume the event + MouseArea { + anchors.fill: parent + onClicked: { + // Consume clicks inside dropdown to prevent closing + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + // Header + Column { + Layout.fillWidth: true + spacing: Theme.spacingM + + Row { + width: parent.width + height: 32 + + Text { + id: processTitle + text: "System Processes" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Medium + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - processTitle.width - sortControls.width - Theme.spacingM + height: 1 + } + + // Sort controls + Row { + id: sortControls + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: cpuButton.width + ramButton.width + Theme.spacingXS + height: 28 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + + Row { + anchors.centerIn: parent + spacing: 0 + + Button { + id: cpuButton + text: "CPU" + flat: true + checkable: true + checked: ProcessMonitorService.sortBy === "cpu" + onClicked: ProcessMonitorService.setSortBy("cpu") + font.pixelSize: Theme.fontSizeSmall + hoverEnabled: true + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: parent.clicked() + } + + contentItem: Text { + text: parent.text + font: parent.font + color: parent.checked ? Theme.primary : Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.checked ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + radius: Theme.cornerRadius + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + + Button { + id: ramButton + text: "RAM" + flat: true + checkable: true + checked: ProcessMonitorService.sortBy === "memory" + onClicked: ProcessMonitorService.setSortBy("memory") + font.pixelSize: Theme.fontSizeSmall + hoverEnabled: true + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: parent.clicked() + } + + contentItem: Text { + text: parent.text + font: parent.font + color: parent.checked ? Theme.primary : Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.checked ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + radius: Theme.cornerRadius + + 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) : + "transparent" + + Text { + text: ProcessMonitorService.sortDescending ? "↓" : "↑" + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + anchors.centerIn: parent + } + + MouseArea { + id: sortOrderArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: ProcessMonitorService.toggleSortOrder() + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + } + } + } + + // Separator + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + } + } + + // Headers + Row { + id: columnHeaders + Layout.fillWidth: true + + Text { + text: "Process" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + opacity: 0.7 + width: 180 + } + + Text { + text: "CPU" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + opacity: 0.7 + width: 60 + horizontalAlignment: Text.AlignRight + } + + Text { + text: "RAM" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + opacity: 0.7 + width: 60 + horizontalAlignment: Text.AlignRight + } + + Text { + text: "PID" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + opacity: 0.7 + width: 60 + horizontalAlignment: Text.AlignRight + } + } + + // Process list + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 200 + clip: true + + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ListView { + id: processListView + anchors.fill: parent + model: ProcessMonitorService.processes + spacing: 2 + + delegate: Rectangle { + width: processListView.width - 16 + height: 36 + radius: Theme.cornerRadius + color: processMouseArea.containsMouse ? + Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : + "transparent" + + MouseArea { + id: processMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) { + if (modelData && modelData.pid > 0) { + processContextMenuWindow.processData = modelData + let globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y) + processContextMenuWindow.show(globalPos.x, globalPos.y) + } + } + } + + onPressAndHold: { + // Context menu for kill process etc + if (modelData && modelData.pid > 0) { + processContextMenuWindow.processData = modelData + let globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2) + processContextMenuWindow.show(globalPos.x, globalPos.y) + } + } + } + + Row { + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + width: parent.width - 16 + + // Process icon + Text { + text: ProcessMonitorService.getProcessIcon(modelData ? modelData.command : "") + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 4 + color: { + if (modelData && modelData.cpu > 80) return Theme.error + if (modelData && modelData.cpu > 50) return Theme.warning + return Theme.surfaceText + } + opacity: 0.8 + anchors.verticalCenter: parent.verticalCenter + } + + // Process name + Text { + text: modelData ? modelData.displayName : "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + width: 150 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + + Item { width: parent.width - 280 } + + // CPU usage + Text { + text: ProcessMonitorService.formatCpuUsage(modelData ? modelData.cpu : 0) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: { + if (modelData && modelData.cpu > 80) return Theme.error + if (modelData && modelData.cpu > 50) return Theme.warning + return Theme.surfaceText + } + width: 60 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + + // Memory usage + Text { + text: ProcessMonitorService.formatMemoryUsage(modelData ? modelData.memory : 0) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: { + if (modelData && modelData.memory > 10) return Theme.error + if (modelData && modelData.memory > 5) return Theme.warning + return Theme.surfaceText + } + width: 60 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + + // PID + Text { + text: modelData ? modelData.pid.toString() : "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + opacity: 0.7 + width: 60 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + } + } + + // Styled context menu for process actions - positioned in global coordinates + PanelWindow { + id: processContextMenuWindow + property var processData: null + property bool menuVisible: false + + visible: menuVisible + color: "transparent" + + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + anchors { + top: true + left: true + right: true + bottom: true + } + + Rectangle { + id: processContextMenu + width: 180 + height: menuColumn.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadiusLarge + color: Theme.surfaceContainer + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) + border.width: 1 + + // Material 3 drop shadow + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: parent.z - 1 + } + + // Material 3 animations + opacity: menuVisible ? 1.0 : 0.0 + scale: menuVisible ? 1.0 : 0.85 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Column { + id: menuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 1 + + // Copy PID + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadiusSmall + color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Text { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: "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 (processContextMenuWindow.processData) { + copyPidProcess.command = ["wl-copy", processContextMenuWindow.processData.pid.toString()] + copyPidProcess.running = true + } + processContextMenuWindow.hide() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Copy Process Name + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadiusSmall + color: copyNameArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Text { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: "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 (processContextMenuWindow.processData) { + let processName = processContextMenuWindow.processData.displayName || processContextMenuWindow.processData.command + copyNameProcess.command = ["wl-copy", processName] + copyNameProcess.running = true + } + processContextMenuWindow.hide() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Separator + Rectangle { + width: parent.width - Theme.spacingS * 2 + height: 5 + 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) + } + } + + // Kill Process + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadiusSmall + color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + enabled: processContextMenuWindow.processData && processContextMenuWindow.processData.pid > 1000 + opacity: enabled ? 1.0 : 0.5 + + Text { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: "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 (processContextMenuWindow.processData) { + killProcess.command = ["kill", processContextMenuWindow.processData.pid.toString()] + killProcess.running = true + } + processContextMenuWindow.hide() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + // Force Kill Process + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadiusSmall + color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + enabled: processContextMenuWindow.processData && processContextMenuWindow.processData.pid > 1000 + opacity: enabled ? 1.0 : 0.5 + + Text { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: "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 (processContextMenuWindow.processData) { + forceKillProcess.command = ["kill", "-9", processContextMenuWindow.processData.pid.toString()] + forceKillProcess.running = true + } + processContextMenuWindow.hide() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + } + + function show(x, y) { + processContextMenu.x = x + processContextMenu.y = y + processContextMenuWindow.menuVisible = true + } + + function hide() { + processContextMenuWindow.menuVisible = false + } + + // Click outside to close + MouseArea { + anchors.fill: parent + z: -1 + onClicked: { + processContextMenuWindow.menuVisible = false + } + } + + // Process objects for commands + Process { + id: copyPidProcess + running: false + } + + Process { + id: copyNameProcess + running: false + } + + Process { + id: killProcess + running: false + } + + Process { + id: forceKillProcess + running: false + } + } + + // Close dropdown when clicking outside + function hide() { + isVisible = false + } + + function show() { + isVisible = true + ProcessMonitorService.updateProcessList() + } + + function toggle() { + if (isVisible) { + hide() + } else { + show() + } + } +} \ No newline at end of file diff --git a/Widgets/RamMonitorWidget.qml b/Widgets/RamMonitorWidget.qml index 2806c437..1d1f3667 100644 --- a/Widgets/RamMonitorWidget.qml +++ b/Widgets/RamMonitorWidget.qml @@ -2,12 +2,14 @@ import QtQuick import QtQuick.Controls import "../Common" import "../Services" +import "." Rectangle { id: ramWidget property bool showPercentage: true property bool showIcon: true + property var processDropdown: null width: 55 height: 30 @@ -24,9 +26,13 @@ Rectangle { id: ramArea anchors.fill: parent hoverEnabled: true + cursorShape: Qt.PointingHandCursor onClicked: { - // RAM widget clicked + if (processDropdown) { + ProcessMonitorService.setSortBy("memory") + processDropdown.toggle() + } } } @@ -57,5 +63,4 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter } } - } diff --git a/Widgets/TopBar/TopBar.qml b/Widgets/TopBar/TopBar.qml index 3bb05531..1940491b 100644 --- a/Widgets/TopBar/TopBar.qml +++ b/Widgets/TopBar/TopBar.qml @@ -50,6 +50,9 @@ PanelWindow { // Notification properties property int notificationCount: 0 + // Process dropdown reference + property var processDropdown: null + // Clipboard properties signal clipboardRequested() @@ -139,6 +142,7 @@ PanelWindow { anchors.rightMargin: Theme.spacingM anchors.topMargin: Theme.spacingXS anchors.bottomMargin: Theme.spacingXS + clip: true Row { id: leftSection @@ -203,7 +207,7 @@ PanelWindow { weatherCode: topBar.weatherCode weatherTemp: topBar.weatherTemp weatherTempF: topBar.weatherTempF - visible: Prefs.showWeather + visible: Prefs.showWeather && topBar.weatherAvailable && topBar.weatherTemp > 0 && topBar.weatherTempF > 0 onClicked: { if (topBar.shellRoot) { @@ -281,11 +285,13 @@ PanelWindow { CpuMonitorWidget { anchors.verticalCenter: parent.verticalCenter visible: Prefs.showSystemResources + processDropdown: topBar.processDropdown } RamMonitorWidget { anchors.verticalCenter: parent.verticalCenter visible: Prefs.showSystemResources + processDropdown: topBar.processDropdown } NotificationCenterButton { diff --git a/Widgets/TopBar/WeatherWidget.qml b/Widgets/TopBar/WeatherWidget.qml index 95187575..be6991a0 100644 --- a/Widgets/TopBar/WeatherWidget.qml +++ b/Widgets/TopBar/WeatherWidget.qml @@ -12,8 +12,8 @@ Rectangle { signal clicked() - visible: weatherAvailable - width: weatherAvailable ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0 + // Visibility is now controlled by TopBar.qml + width: visible ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0 height: 30 radius: Theme.cornerRadius color: weatherArea.containsMouse ? diff --git a/Widgets/qmldir b/Widgets/qmldir index 820633e9..bf46d21e 100644 --- a/Widgets/qmldir +++ b/Widgets/qmldir @@ -14,6 +14,7 @@ PowerConfirmDialog 1.0 PowerConfirmDialog.qml ThemePicker 1.0 ThemePicker.qml CpuMonitorWidget 1.0 CpuMonitorWidget.qml RamMonitorWidget 1.0 RamMonitorWidget.qml +ProcessListDropdown 1.0 ProcessListDropdown.qml SpotlightLauncher 1.0 SpotlightLauncher.qml SettingsPopup 1.0 SettingsPopup.qml SettingsSection 1.0 SettingsSection.qml diff --git a/shell.qml b/shell.qml index b21f1ee1..053653fb 100644 --- a/shell.qml +++ b/shell.qml @@ -304,6 +304,7 @@ ShellRoot { bluetoothEnabled: root.bluetoothEnabled shellRoot: root notificationCount: notificationHistory.count + processDropdown: processListDropdown // Connect tray menu properties showTrayMenu: root.showTrayMenu @@ -340,6 +341,10 @@ ShellRoot { PowerMenuPopup {} PowerConfirmDialog {} + ProcessListDropdown { + id: processListDropdown + } + SettingsPopup { id: settingsPopup settingsVisible: root.settingsVisible