From 0ed5e22fc6d9293d8671b11319b0971667cade68 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Thu, 24 Sep 2026 12:37:35 -0400 Subject: [PATCH] Add tabs, a performance page and a mixer button to Quick Settings --- default/hypr/apps/system.lua | 2 +- docs/widgets.md | 19 ++- .../sysmon/Metric.qml => Ui/MetricBar.qml} | 0 shell/Ui/SystemStats.qml | 120 +++++++++++++++ shell/Ui/qmldir | 2 + shell/plugins/panels/audio/Panel.qml | 22 ++- .../panels/quick-settings/PerformanceTab.qml | 137 +++++++++++++++++ .../panels/quick-settings/QuickSettings.qml | 142 ++++++++++++------ .../panels/quick-settings/SoundTab.qml | 83 ++++++++++ .../plugins/panels/quick-settings/TabBar.qml | 28 ++++ shell/plugins/panels/sysmon/SysMonitor.qml | 123 ++------------- 11 files changed, 520 insertions(+), 158 deletions(-) rename shell/{plugins/panels/sysmon/Metric.qml => Ui/MetricBar.qml} (100%) create mode 100644 shell/Ui/SystemStats.qml create mode 100644 shell/plugins/panels/quick-settings/PerformanceTab.qml create mode 100644 shell/plugins/panels/quick-settings/SoundTab.qml create mode 100644 shell/plugins/panels/quick-settings/TabBar.qml diff --git a/default/hypr/apps/system.lua b/default/hypr/apps/system.lua index 3f3cf2c..fd5a809 100644 --- a/default/hypr/apps/system.lua +++ b/default/hypr/apps/system.lua @@ -4,7 +4,7 @@ o.window({ tag = "floating-window" }, { center = true }) o.window({ tag = "floating-window" }, { size = { 875, 600 } }) o.window( - "(org.blob.btop|org.blob.terminal|org.blob.bash|org.codeberg.dnkl.foot|org.gnome.NautilusPreviewer|org.gnome.Evince|Blob|About|TUI.float|imv|mpv)", + "(org.blob.btop|org.blob.terminal|org.blob.bash|org.codeberg.dnkl.foot|org.gnome.NautilusPreviewer|org.gnome.Evince|Blob|About|TUI.float|imv|mpv|org.pulseaudio.pavucontrol)", { tag = "+floating-window", } diff --git a/docs/widgets.md b/docs/widgets.md index 678df45..7387dd3 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -9,8 +9,8 @@ autostarted. | Was | Is | Note | | --- | --- | --- | -| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 9 files, none over 250 lines | -| `SysMonitor.tsx` (142) | `shell/plugins/panels/sysmon/` | CPU, memory, disk, temperature | +| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 13 files, none over 350 lines, with four tabs | +| `SysMonitor.tsx` (142) | `shell/plugins/panels/sysmon/` | CPU, memory, disk, temperature; the probes and bars are `Ui/SystemStats.qml` and `Ui/MetricBar.qml`, shared with the Performance tab | | `Notifications.tsx` (160) | `shell/plugins/notification-center/` | reads the notification service's own history files | | `WallPicker.tsx` (106) | the existing image picker | `blob-wallpaper-set --menu` now calls `blob-menu-images`, so wallpapers get the image grid instead of a text list | | `ThemePicker.tsx` (178) | the menu and `blob-theme-switcher` | the shell already had a theme switcher; a second picker was duplicate surface | @@ -33,6 +33,21 @@ The CSS was written against pywal's `@color0`, `@color4` and `@color5` aliases. falling back to `accent` for themes that define neither. `color0` is `background`, which Color already had. +## Quick settings tabs + +Under the header, a tab bar switches the card between four pages. Each page +only polls the state it shows, so an open card on the General tab is not +running `pamixer` and `playerctl` every second. + +| Tab | Holds | +| --- | --- | +| General | the calendar and the brightness slider | +| Sound & Music | output and microphone volume (click the icon to mute), the media row with its controls, and buttons for the audio panel (picking devices) and the pavucontrol mixer | +| Performance | CPU, memory, disk and temperature bars, the load average, the power profile (Power saver, Balanced, Performance, whichever the machine offers), and a button that opens btop | +| Quick Settings | the toggle tiles: Wi-Fi, Bluetooth, Wallpaper, Silence, Night Light, Record, Stay Awake, Pick Color, Theme, Tablet Lock | + +The weather card beside it is not part of the tabs and stays visible. + ## Opening them | Panel | Opened by | diff --git a/shell/plugins/panels/sysmon/Metric.qml b/shell/Ui/MetricBar.qml similarity index 100% rename from shell/plugins/panels/sysmon/Metric.qml rename to shell/Ui/MetricBar.qml diff --git a/shell/Ui/SystemStats.qml b/shell/Ui/SystemStats.qml new file mode 100644 index 0000000..461c3a9 --- /dev/null +++ b/shell/Ui/SystemStats.qml @@ -0,0 +1,120 @@ +import QtQuick +import Quickshell.Io + +Item { + id: root + + property bool polling: false + + property int cpuPercent: 0 + property int memoryPercent: 0 + property string memoryDetail: "" + property int diskPercent: 0 + property string diskDetail: "" + property int temperature: 0 + property string loadAverage: "" + + property real previousIdle: 0 + property real previousTotal: 0 + + function readCpu(raw) { + var values = String(raw).trim().split(/\s+/).slice(1).map(Number) + var idle = (values[3] || 0) + (values[4] || 0) + var total = 0 + for (var i = 0; i < values.length; i++) total += values[i] || 0 + var idleDelta = idle - root.previousIdle + var totalDelta = total - root.previousTotal + root.previousIdle = idle + root.previousTotal = total + if (totalDelta <= 0) return + root.cpuPercent = Math.round((1 - idleDelta / totalDelta) * 100) + } + + function readMemory(raw) { + var parts = String(raw).trim().split(/\s+/).map(Number) + var used = parts[0] || 0 + var total = parts[1] || 0 + if (!total) { + root.memoryPercent = 0 + root.memoryDetail = "" + return + } + root.memoryPercent = Math.round(used / total * 100) + root.memoryDetail = (used / 1024).toFixed(1) + "G / " + (total / 1024).toFixed(1) + "G" + } + + function readDisk(raw) { + var parts = String(raw).trim().split(/\s+/) + root.diskPercent = Number(String(parts[0] || "").replace("%", "")) || 0 + root.diskDetail = (parts[1] || "0") + " / " + (parts[2] || "0") + } + + function readLoad(raw) { + var parts = String(raw).trim().split(/\s+/) + root.loadAverage = parts.length >= 3 ? parts.slice(0, 3).join(" ") : "" + } + + function refreshFast() { + if (!cpuProbe.running) cpuProbe.running = true + if (!memoryProbe.running) memoryProbe.running = true + if (!temperatureProbe.running) temperatureProbe.running = true + if (!loadProbe.running) loadProbe.running = true + } + + function refreshSlow() { + if (!diskProbe.running) diskProbe.running = true + } + + function refresh() { + refreshFast() + refreshSlow() + } + + Process { + id: cpuProbe + command: ["bash", "-c", "grep '^cpu ' /proc/stat"] + stdout: StdioCollector { onStreamFinished: root.readCpu(text) } + } + + Process { + id: memoryProbe + command: ["bash", "-c", "free -m | awk '/^Mem:/ {print $3\" \"$2}'"] + stdout: StdioCollector { onStreamFinished: root.readMemory(text) } + } + + Process { + id: diskProbe + command: ["bash", "-c", "df -h --output=pcent,used,size / | tail -1"] + stdout: StdioCollector { onStreamFinished: root.readDisk(text) } + } + + Process { + id: temperatureProbe + command: ["bash", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"] + stdout: StdioCollector { + onStreamFinished: root.temperature = Math.round(Number(String(text).trim()) / 1000) || 0 + } + } + + Process { + id: loadProbe + command: ["bash", "-c", "cat /proc/loadavg"] + stdout: StdioCollector { onStreamFinished: root.readLoad(text) } + } + + Timer { + interval: 2000 + running: root.polling + repeat: true + onTriggered: root.refreshFast() + } + + Timer { + interval: 30000 + running: root.polling + repeat: true + onTriggered: root.refreshSlow() + } + + onPollingChanged: if (root.polling) root.refresh() +} diff --git a/shell/Ui/qmldir b/shell/Ui/qmldir index 8b3457c..7a466cc 100644 --- a/shell/Ui/qmldir +++ b/shell/Ui/qmldir @@ -42,3 +42,5 @@ CalendarModel 1.0 CalendarModel.js CalendarView 1.0 CalendarView.qml CalendarDays 1.0 CalendarDays.qml CalendarMonthPicker 1.0 CalendarMonthPicker.qml +MetricBar 1.0 MetricBar.qml +SystemStats 1.0 SystemStats.qml diff --git a/shell/plugins/panels/audio/Panel.qml b/shell/plugins/panels/audio/Panel.qml index 8554936..54c905d 100644 --- a/shell/plugins/panels/audio/Panel.qml +++ b/shell/plugins/panels/audio/Panel.qml @@ -454,6 +454,11 @@ Panel { // The hero switch is the whole panel's on/off, so it carries both channels // at once. It reads as on while anything is still audible, which keeps // muting a single channel from the row below flipping the master switch. + function openMixer() { + root.close() + Quickshell.execDetached(["uwsm-app", "--", "pavucontrol"]) + } + function toggleAllMuted() { var mute = anyAudible if (hasOutput) volumeSink.audio.muted = mute @@ -669,6 +674,7 @@ Panel { onCloseRequested: root.close() onTabRequested: function(direction) { root.switchPanel(direction) } onTextKey: function(t) { + if (t === "x" || t === "X") { root.openMixer(); return } // 'm' mutes whatever the cursor is on: focused section's slider // for output/input, the focused stream for streams. if (t === "m" || t === "M") { @@ -741,12 +747,24 @@ Panel { } } + PanelActionButton { + id: mixerButton + anchors.right: powerSwitch.left + anchors.rightMargin: Style.space(8) + anchors.verticalCenter: parent.verticalCenter + iconText: "" + tooltipText: "Mixer (x)" + foreground: root.bar.foreground + fontFamily: root.bar.fontFamily + onClicked: root.openMixer() + } + Column { id: heroLabels anchors.left: heroIcon.right anchors.leftMargin: Style.space(14) - anchors.right: parent.right - anchors.rightMargin: powerSwitch.width + Style.space(12) + anchors.right: mixerButton.left + anchors.rightMargin: Style.space(12) anchors.verticalCenter: parent.verticalCenter spacing: Style.space(2) diff --git a/shell/plugins/panels/quick-settings/PerformanceTab.qml b/shell/plugins/panels/quick-settings/PerformanceTab.qml new file mode 100644 index 0000000..5574965 --- /dev/null +++ b/shell/plugins/panels/quick-settings/PerformanceTab.qml @@ -0,0 +1,137 @@ +import QtQuick +import Quickshell.Io +import qs.Commons +import qs.Ui + +Column { + id: root + + property bool polling: false + + signal commandRequested(string command) + signal tuiRequested(string command) + + readonly property var profileLabels: ({ + "power-saver": "Power saver", + "balanced": "Balanced", + "performance": "Performance" + }) + property var profiles: [] + property string activeProfile: "" + + spacing: Style.spaceReal(12) + + function readProfiles(raw) { + var available = [] + var active = "" + var lines = String(raw).trim().split("\n") + for (var i = 0; i < lines.length; i++) { + var fields = lines[i].split("\t") + if (!fields[0]) continue + available.push({ value: fields[0], label: root.profileLabels[fields[0]] || fields[0] }) + if (fields[1] === "1") active = fields[0] + } + root.profiles = available + root.activeProfile = active + } + + function refreshProfiles() { + if (!profileProbe.running) profileProbe.running = true + } + + function setProfile(profile) { + root.activeProfile = profile + root.commandRequested("blob-power-set autodetect " + profile) + profileSettleTimer.restart() + } + + onPollingChanged: if (root.polling) root.refreshProfiles() + + SystemStats { + id: stats + polling: root.polling + } + + Process { + id: profileProbe + command: ["blob-power-list", "--active-state"] + stdout: StdioCollector { onStreamFinished: root.readProfiles(text) } + } + + Timer { + id: profileSettleTimer + interval: 500 + onTriggered: root.refreshProfiles() + } + + MetricBar { + width: parent.width + icon: "󰍛" + name: "CPU" + percent: stats.cpuPercent + detail: stats.cpuPercent + "%" + } + + MetricBar { + width: parent.width + icon: "󰘚" + name: "Memory" + percent: stats.memoryPercent + detail: stats.memoryDetail + } + + MetricBar { + width: parent.width + icon: "󰋊" + name: "Disk" + percent: stats.diskPercent + detail: stats.diskDetail + } + + MetricBar { + width: parent.width + icon: "" + name: "Temperature" + percent: stats.temperature + detail: stats.temperature + "°C" + } + + Text { + width: parent.width + visible: stats.loadAverage !== "" + text: "Load average " + stats.loadAverage + textFormat: Text.PlainText + color: Util.alpha(Color.foreground, 0.7) + font.family: Style.font.family + font.pixelSize: Style.font.bodySmall + } + + Row { + width: parent.width + visible: root.profiles.length > 0 + spacing: Style.spaceReal(6) + + Repeater { + model: root.profiles + + Button { + required property var modelData + width: (parent.width - parent.spacing * (root.profiles.length - 1)) / root.profiles.length + text: modelData.label + selected: modelData.value === root.activeProfile + bordered: true + fontSize: Style.font.bodySmall + onClicked: root.setProfile(modelData.value) + } + } + } + + Button { + width: parent.width + text: "Open btop" + iconText: "󰍛" + bordered: true + fontSize: Style.font.bodySmall + onClicked: root.tuiRequested("btop") + } +} diff --git a/shell/plugins/panels/quick-settings/QuickSettings.qml b/shell/plugins/panels/quick-settings/QuickSettings.qml index 5df5738..e5462c4 100644 --- a/shell/plugins/panels/quick-settings/QuickSettings.qml +++ b/shell/plugins/panels/quick-settings/QuickSettings.qml @@ -15,6 +15,18 @@ Item { readonly property string pluginId: (manifest && manifest.id) || "blob.quick-settings" + readonly property var tabs: [ + { id: "general", label: "General", icon: "" }, + { id: "sound", label: "Sound & Music", icon: "" }, + { id: "performance", label: "Performance", icon: "" }, + { id: "quick", label: "Quick Settings", icon: "" } + ] + property string currentTab: "general" + readonly property bool generalVisible: opened && currentTab === "general" + readonly property bool soundVisible: opened && currentTab === "sound" + readonly property bool performanceVisible: opened && currentTab === "performance" + readonly property bool quickVisible: opened && currentTab === "quick" + readonly property int barInset: { var bar = root.shell ? root.shell.bar : null if (!bar || bar.barHidden === true) return 0 @@ -60,6 +72,8 @@ Item { recordingFlag.refresh() volumeValue.refresh() mutedFlag.refresh() + micVolumeValue.refresh() + micMutedFlag.refresh() brightnessValue.refresh() mediaValue.refresh() } @@ -77,57 +91,71 @@ Item { PolledFlag { id: bluetoothFlag - polling: root.opened + polling: root.quickVisible command: "bluetoothctl show 2>/dev/null | grep -q 'Powered: yes' && echo on || echo off" } PolledFlag { id: dndFlag - polling: root.opened + polling: root.quickVisible command: "blob-shell notifications dndState 2>/dev/null" } PolledFlag { id: nightLightFlag - polling: root.opened + polling: root.quickVisible command: "blob-toggle-nightlight --status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off" } PolledFlag { id: stayAwakeFlag - polling: root.opened + polling: root.quickVisible command: "blob-toggle-idle status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off" } PolledFlag { id: tabletFlag - polling: root.opened + polling: root.quickVisible command: "blob-toggle-tablet status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off" } PolledFlag { id: recordingFlag - polling: root.opened + polling: root.quickVisible command: "pgrep -f '^gpu-screen-recorder' >/dev/null && echo on || echo off" } PolledFlag { id: mutedFlag - polling: root.opened + polling: root.soundVisible intervalMs: 1000 command: "pamixer --get-mute 2>/dev/null | grep -qx true && echo on || echo off" } PolledText { id: volumeValue - polling: root.opened + polling: root.soundVisible intervalMs: 1000 command: "pamixer --get-volume 2>/dev/null || echo 0" } + PolledFlag { + id: micMutedFlag + polling: root.soundVisible + intervalMs: 1000 + command: "pamixer --default-source --get-mute 2>/dev/null | grep -qx true && echo on || echo off" + } + + PolledText { + id: micVolumeValue + polling: root.soundVisible + intervalMs: 1000 + command: "pamixer --default-source --get-volume 2>/dev/null || echo 0" + } + PolledText { id: brightnessValue - polling: root.opened + polling: root.generalVisible intervalMs: 2000 command: "brightnessctl -m 2>/dev/null | cut -d, -f4 | tr -d '%' || echo 0" } @@ -148,7 +176,7 @@ Item { PolledText { id: mediaValue - polling: root.opened + polling: root.soundVisible intervalMs: 1000 command: "playerctl metadata -f '{{title}}|||{{artist}}|||{{status}}' 2>/dev/null || echo 'No Media||||||Stopped'" } @@ -221,13 +249,70 @@ Item { onCloseRequested: root.dismiss() } - Calendar { + TabBar { width: parent.width - polling: root.opened + tabs: root.tabs + currentTab: root.currentTab + onTabRequested: function(tabId) { root.currentTab = tabId } + } + + Column { + width: parent.width + visible: root.currentTab === "general" + spacing: Style.spaceReal(12) + + Calendar { + width: parent.width + polling: root.generalVisible + } + + SliderRow { + width: parent.width + icon: "\uf185" + value: Number(brightnessValue.value) || 0 + onValueRequested: function(next) { + root.run("brightnessctl set " + next + "%") + brightnessValue.value = String(next) + } + } + } + + SoundTab { + width: parent.width + visible: root.currentTab === "sound" + outputVolume: Number(volumeValue.value) || 0 + outputMuted: mutedFlag.value + inputVolume: Number(micVolumeValue.value) || 0 + inputMuted: micMutedFlag.value + mediaParts: root.mediaParts + onCommandRequested: function(command) { root.toggleAndRefresh(command) } + onOutputVolumeRequested: function(next) { + root.run("pamixer --set-volume " + next) + volumeValue.value = String(next) + } + onInputVolumeRequested: function(next) { + root.run("pamixer --default-source --set-volume " + next) + micVolumeValue.value = String(next) + } + onMediaControlRequested: function(action) { + root.run("playerctl " + action) + stateSettleTimer.restart() + } + onPanelRequested: function(pluginId) { root.openPanel(pluginId) } + onAppRequested: function(command) { root.runAndDismiss("uwsm-app -- " + command) } + } + + PerformanceTab { + width: parent.width + visible: root.currentTab === "performance" + polling: root.performanceVisible + onCommandRequested: function(command) { root.run(command) } + onTuiRequested: function(command) { root.runAndDismiss("blob-launch-or-focus-tui " + command) } } Tiles { width: parent.width + visible: root.currentTab === "quick" bluetoothOn: bluetoothFlag.value doNotDisturb: dndFlag.value nightLight: nightLightFlag.value @@ -238,39 +323,6 @@ Item { onPanelRequested: function(pluginId) { root.openPanel(pluginId) } onToggleRequested: function(command) { root.toggleAndRefresh(command) } } - - SliderRow { - width: parent.width - icon: mutedFlag.value ? "\uf026" : "\uf028" - dimmed: mutedFlag.value - value: Number(volumeValue.value) || 0 - onIconActivated: root.toggleAndRefresh("pamixer --toggle-mute") - onValueRequested: function(next) { - root.run("pamixer --set-volume " + next) - volumeValue.value = String(next) - } - } - - SliderRow { - width: parent.width - icon: "\uf185" - value: Number(brightnessValue.value) || 0 - onValueRequested: function(next) { - root.run("brightnessctl set " + next + "%") - brightnessValue.value = String(next) - } - } - - MediaRow { - width: parent.width - title: root.mediaParts.length > 0 ? root.mediaParts[0] : "" - artist: root.mediaParts.length > 1 ? root.mediaParts[1] : "" - status: root.mediaParts.length > 2 ? root.mediaParts[2] : "Stopped" - onControlRequested: function(action) { - root.run("playerctl " + action) - stateSettleTimer.restart() - } - } } } diff --git a/shell/plugins/panels/quick-settings/SoundTab.qml b/shell/plugins/panels/quick-settings/SoundTab.qml new file mode 100644 index 0000000..b92d6a1 --- /dev/null +++ b/shell/plugins/panels/quick-settings/SoundTab.qml @@ -0,0 +1,83 @@ +import QtQuick +import qs.Commons +import qs.Ui + +Column { + id: root + + property int outputVolume: 0 + property bool outputMuted: false + property int inputVolume: 0 + property bool inputMuted: false + property var mediaParts: [] + + signal commandRequested(string command) + signal outputVolumeRequested(int value) + signal inputVolumeRequested(int value) + signal mediaControlRequested(string action) + signal panelRequested(string pluginId) + signal appRequested(string command) + + spacing: Style.spaceReal(12) + + SliderRow { + width: parent.width + icon: root.outputMuted ? "" : "" + dimmed: root.outputMuted + value: root.outputVolume + onIconActivated: root.commandRequested("pamixer --toggle-mute") + onValueRequested: function(next) { root.outputVolumeRequested(next) } + } + + SliderRow { + width: parent.width + icon: root.inputMuted ? "" : "" + dimmed: root.inputMuted + value: root.inputVolume + onIconActivated: root.commandRequested("pamixer --default-source --toggle-mute") + onValueRequested: function(next) { root.inputVolumeRequested(next) } + } + + MediaRow { + id: mediaRow + width: parent.width + title: root.mediaParts.length > 0 ? root.mediaParts[0] : "" + artist: root.mediaParts.length > 1 ? root.mediaParts[1] : "" + status: root.mediaParts.length > 2 ? root.mediaParts[2] : "Stopped" + onControlRequested: function(action) { root.mediaControlRequested(action) } + } + + Text { + width: parent.width + visible: !mediaRow.visible + horizontalAlignment: Text.AlignHCenter + text: "Nothing playing" + textFormat: Text.PlainText + color: Util.alpha(Color.foreground, 0.6) + font.family: Style.font.family + font.pixelSize: Style.font.bodySmall + } + + Row { + width: parent.width + spacing: Style.spaceReal(6) + + Button { + width: (parent.width - parent.spacing) / 2 + text: "Audio devices" + iconText: "" + bordered: true + fontSize: Style.font.bodySmall + onClicked: root.panelRequested("blob.audio") + } + + Button { + width: (parent.width - parent.spacing) / 2 + text: "Mixer" + iconText: "" + bordered: true + fontSize: Style.font.bodySmall + onClicked: root.appRequested("pavucontrol") + } + } +} diff --git a/shell/plugins/panels/quick-settings/TabBar.qml b/shell/plugins/panels/quick-settings/TabBar.qml new file mode 100644 index 0000000..41ed1ea --- /dev/null +++ b/shell/plugins/panels/quick-settings/TabBar.qml @@ -0,0 +1,28 @@ +import QtQuick +import qs.Commons +import qs.Ui + +Row { + id: root + + property var tabs: [] + property string currentTab: "" + signal tabRequested(string tabId) + + spacing: Style.spaceReal(6) + + Repeater { + model: root.tabs + + Button { + required property var modelData + width: (root.width - root.spacing * (root.tabs.length - 1)) / root.tabs.length + text: modelData.label + iconText: modelData.icon + selected: modelData.id === root.currentTab + bordered: true + fontSize: Style.font.bodySmall + onClicked: root.tabRequested(modelData.id) + } + } +} diff --git a/shell/plugins/panels/sysmon/SysMonitor.qml b/shell/plugins/panels/sysmon/SysMonitor.qml index abcfa72..60ead88 100644 --- a/shell/plugins/panels/sysmon/SysMonitor.qml +++ b/shell/plugins/panels/sysmon/SysMonitor.qml @@ -14,16 +14,6 @@ Item { readonly property string pluginId: (manifest && manifest.id) || "blob.sysmon" - property int cpuPercent: 0 - property int memoryPercent: 0 - property string memoryDetail: "" - property int diskPercent: 0 - property string diskDetail: "" - property int temperature: 0 - - property real previousIdle: 0 - property real previousTotal: 0 - function open(payloadJson) { root.opened = true } @@ -38,92 +28,9 @@ Item { root.shell.hide(root.pluginId) } - function readCpu(raw) { - var values = String(raw).trim().split(/\s+/).slice(1).map(Number) - var idle = (values[3] || 0) + (values[4] || 0) - var total = 0 - for (var i = 0; i < values.length; i++) total += values[i] || 0 - var idleDelta = idle - root.previousIdle - var totalDelta = total - root.previousTotal - root.previousIdle = idle - root.previousTotal = total - if (totalDelta <= 0) return - root.cpuPercent = Math.round((1 - idleDelta / totalDelta) * 100) - } - - function readMemory(raw) { - var parts = String(raw).trim().split(/\s+/).map(Number) - var used = parts[0] || 0 - var total = parts[1] || 0 - if (!total) { - root.memoryPercent = 0 - root.memoryDetail = "" - return - } - root.memoryPercent = Math.round(used / total * 100) - root.memoryDetail = (used / 1024).toFixed(1) + "G / " + (total / 1024).toFixed(1) + "G" - } - - function readDisk(raw) { - var parts = String(raw).trim().split(/\s+/) - root.diskPercent = Number(String(parts[0] || "").replace("%", "")) || 0 - root.diskDetail = (parts[1] || "0") + " / " + (parts[2] || "0") - } - - Process { - id: cpuProbe - command: ["bash", "-c", "grep '^cpu ' /proc/stat"] - stdout: StdioCollector { onStreamFinished: root.readCpu(text) } - } - - Process { - id: memoryProbe - command: ["bash", "-c", "free -m | awk '/^Mem:/ {print $3\" \"$2}'"] - stdout: StdioCollector { onStreamFinished: root.readMemory(text) } - } - - Process { - id: diskProbe - command: ["bash", "-c", "df -h --output=pcent,used,size / | tail -1"] - stdout: StdioCollector { onStreamFinished: root.readDisk(text) } - } - - Process { - id: temperatureProbe - command: ["bash", "-c", "cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0"] - stdout: StdioCollector { - onStreamFinished: root.temperature = Math.round(Number(String(text).trim()) / 1000) || 0 - } - } - - function refreshFast() { - if (!cpuProbe.running) cpuProbe.running = true - if (!memoryProbe.running) memoryProbe.running = true - if (!temperatureProbe.running) temperatureProbe.running = true - } - - function refreshSlow() { - if (!diskProbe.running) diskProbe.running = true - } - - Timer { - interval: 2000 - running: root.opened - repeat: true - onTriggered: root.refreshFast() - } - - Timer { - interval: 30000 - running: root.opened - repeat: true - onTriggered: root.refreshSlow() - } - - onOpenedChanged: { - if (!root.opened) return - root.refreshFast() - root.refreshSlow() + SystemStats { + id: stats + polling: root.opened } PanelWindow { @@ -158,36 +65,36 @@ Item { font.bold: true } - Metric { + MetricBar { width: parent.width icon: "󰍛" name: "CPU" - percent: root.cpuPercent - detail: root.cpuPercent + "%" + percent: stats.cpuPercent + detail: stats.cpuPercent + "%" } - Metric { + MetricBar { width: parent.width icon: "󰘚" name: "Memory" - percent: root.memoryPercent - detail: root.memoryDetail + percent: stats.memoryPercent + detail: stats.memoryDetail } - Metric { + MetricBar { width: parent.width icon: "󰋊" name: "Disk" - percent: root.diskPercent - detail: root.diskDetail + percent: stats.diskPercent + detail: stats.diskDetail } - Metric { + MetricBar { width: parent.width icon: "" name: "Temperature" - percent: root.temperature - detail: root.temperature + "°C" + percent: stats.temperature + detail: stats.temperature + "°C" } } }