diff --git a/shell/applets/CpuApplet.qml b/shell/applets/CpuApplet.qml index 6b2123e..76b3ab3 100644 --- a/shell/applets/CpuApplet.qml +++ b/shell/applets/CpuApplet.qml @@ -4,6 +4,9 @@ import "../services" as S Column { id: root + required property var cores + required property var coreMaxFreq + required property var coreTypes required property var processes required property color accentColor @@ -13,32 +16,34 @@ Column { onActiveChanged: { if (active && !_coreActive) { _coreActive = true; - S.CpuService.coreConsumers++; + S.SystemStats.coreConsumers++; } else if (!active && _coreActive) { _coreActive = false; - S.CpuService.coreConsumers--; + S.SystemStats.coreConsumers--; } + if (active) + _cpuHistory = []; } Component.onDestruction: if (_coreActive) - S.CpuService.coreConsumers-- + S.SystemStats.coreConsumers-- // Per-core rows Repeater { - model: S.CpuService.cores.length + model: root.cores.length delegate: Item { required property int index width: root.width - readonly property int _u: S.CpuService.cores[index]?.usage ?? 0 - readonly property real _f: S.CpuService.cores[index]?.freq_ghz ?? 0 + readonly property int _u: root.cores[index]?.usage ?? 0 + readonly property real _f: root.cores[index]?.freq_ghz ?? 0 readonly property color _barColor: S.Theme.loadColor(_u) readonly property bool _throttled: { - const maxF = S.CpuService.coreMaxFreq[index] ?? 0; + const maxF = root.coreMaxFreq[index] ?? 0; return maxF > 0 && _f < maxF * 0.85 && _u >= 60; } readonly property bool _isFirstECore: { - const types = S.CpuService.coreTypes; + const types = root.coreTypes; if (!types.length || index >= types.length) return false; if (types[index] !== "Efficiency") @@ -113,7 +118,7 @@ Column { anchors.verticalCenter: parent.verticalCenter width: 32 height: 10 - history: S.CpuService.cores[parent.parent.index]?.history ?? [] + history: root.cores[parent.parent.index]?.history ?? [] strokeColor: parent.parent._barColor colorAt: v => S.Theme.loadColor(v) active: root.active @@ -140,8 +145,8 @@ Column { // Overall CPU utilization InfoRow { label: "Total" - value: S.CpuService.usage + "% @ " + S.CpuService.freqGhz.toFixed(2) + " GHz" - valueColor: S.Theme.loadColor(S.CpuService.usage) + value: S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz" + valueColor: S.Theme.loadColor(S.SystemStats.cpuUsage) } SparklineCanvas { @@ -150,12 +155,23 @@ Column { anchors.right: parent.right anchors.rightMargin: 12 height: 32 - history: S.CpuService.history + history: root._cpuHistory strokeColor: root.accentColor colorAt: v => S.Theme.loadColor(v) active: root.active } + property var _cpuHistory: [] + Connections { + target: S.SystemStats + function onCpuUsageChanged() { + if (!root.active) + return; + const h = root._cpuHistory.concat([S.SystemStats.cpuUsage]); + root._cpuHistory = h.length > 60 ? h.slice(h.length - 60) : h; + } + } + // Process list - hidden on lock screen (exposes running process names) Column { visible: !S.LockService.locked diff --git a/shell/dock/AppletDock.qml b/shell/dock/AppletDock.qml index 7428980..0601279 100644 --- a/shell/dock/AppletDock.qml +++ b/shell/dock/AppletDock.qml @@ -11,7 +11,7 @@ import "../modules" as M PanelWindow { id: root - required property ShellScreen screen + required property var screen visible: _winVisible color: "transparent" @@ -186,6 +186,9 @@ PanelWindow { C.CpuApplet { width: parent.width + cores: S.SystemStats.cpuCores + coreMaxFreq: S.SystemStats.cpuCoreMaxFreq + coreTypes: S.SystemStats.cpuCoreTypes processes: _cpuProcs.processes accentColor: root._accent active: _cpuCard.expanded diff --git a/shell/dock/DockEdgeTrigger.qml b/shell/dock/DockEdgeTrigger.qml index 93cb1a0..6fcb4dc 100644 --- a/shell/dock/DockEdgeTrigger.qml +++ b/shell/dock/DockEdgeTrigger.qml @@ -8,7 +8,7 @@ import "../services" as S PanelWindow { id: root - required property ShellScreen screen + required property var screen visible: !S.DockState.open color: "transparent" diff --git a/shell/modules/BackgroundOverlay.qml b/shell/modules/BackgroundOverlay.qml index 5aeb2c9..af2726f 100644 --- a/shell/modules/BackgroundOverlay.qml +++ b/shell/modules/BackgroundOverlay.qml @@ -8,7 +8,7 @@ import "../services" as S PanelWindow { id: root - required property ShellScreen screen + required property var screen color: "transparent" diff --git a/shell/modules/Bar.qml b/shell/modules/Bar.qml index d5f934c..8f02163 100644 --- a/shell/modules/Bar.qml +++ b/shell/modules/Bar.qml @@ -8,7 +8,7 @@ import "../services" as S PanelWindow { id: bar - required property ShellScreen screen + required property var screen color: "transparent" WlrLayershell.layer: WlrLayer.Bottom diff --git a/shell/modules/CpuModule.qml b/shell/modules/CpuModule.qml index 3fa14d5..5290486 100644 --- a/shell/modules/CpuModule.qml +++ b/shell/modules/CpuModule.qml @@ -8,18 +8,25 @@ M.BarModule { id: root active: S.Modules.cpu.enable spacing: Math.max(1, S.Theme.moduleSpacing - 2) - tooltip: "CPU: " + S.CpuService.usage + "% @ " + S.CpuService.freqGhz.toFixed(2) + " GHz" + tooltip: "CPU: " + S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz" panelNamespace: "nova-cpu" panelContentWidth: 260 panelComponent: Component { C.CpuApplet { width: parent.width + cores: root._cores + coreMaxFreq: root._coreMaxFreq + coreTypes: root._coreTypes processes: root._procs.processes accentColor: root.accentColor active: root._showPanel } } + readonly property var _cores: S.SystemStats.cpuCores + readonly property var _coreMaxFreq: S.SystemStats.cpuCoreMaxFreq + readonly property var _coreTypes: S.SystemStats.cpuCoreTypes + property M.ProcessList _procs: M.ProcessList { sortBy: "cpu" active: root._showPanel @@ -31,7 +38,7 @@ M.BarModule { anchors.verticalCenter: parent.verticalCenter } M.BarLabel { - label: S.CpuService.usage.toString().padStart(2) + "%@" + S.CpuService.freqGhz.toFixed(2) + label: S.SystemStats.cpuUsage.toString().padStart(2) + "%@" + S.SystemStats.cpuFreqGhz.toFixed(2) minText: "99%@9.99" anchors.verticalCenter: parent.verticalCenter } diff --git a/shell/modules/NotifPopup.qml b/shell/modules/NotifPopup.qml index 1d212c3..41ad6d8 100644 --- a/shell/modules/NotifPopup.qml +++ b/shell/modules/NotifPopup.qml @@ -8,7 +8,7 @@ import "../services" as S PanelWindow { id: root - required property ShellScreen screen + required property var screen visible: S.NotifService.popups.length > 0 && !S.NiriIpc.overviewOpen color: "transparent" diff --git a/shell/modules/OverviewBackdrop.qml b/shell/modules/OverviewBackdrop.qml index a17d1ad..6b145a5 100644 --- a/shell/modules/OverviewBackdrop.qml +++ b/shell/modules/OverviewBackdrop.qml @@ -8,7 +8,7 @@ import "../applets" as C PanelWindow { id: root - required property ShellScreen screen + required property var screen color: "transparent" diff --git a/shell/modules/ScreenCapture.qml b/shell/modules/ScreenCapture.qml index 7519aef..c45e68e 100644 --- a/shell/modules/ScreenCapture.qml +++ b/shell/modules/ScreenCapture.qml @@ -6,7 +6,7 @@ import "../services" as S PanelWindow { id: root - required property ShellScreen screen + required property var screen color: "transparent" WlrLayershell.layer: WlrLayer.Background diff --git a/shell/modules/Tooltip.qml b/shell/modules/Tooltip.qml index 4b299d0..d8aa5be 100644 --- a/shell/modules/Tooltip.qml +++ b/shell/modules/Tooltip.qml @@ -7,7 +7,7 @@ import "../services" as S PanelWindow { id: root - required property ShellScreen screen + required property var screen visible: _winVisible color: "transparent" diff --git a/shell/services/CpuService.qml b/shell/services/CpuService.qml deleted file mode 100644 index bb5a8a1..0000000 --- a/shell/services/CpuService.qml +++ /dev/null @@ -1,160 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell.Io -import "." as S - -QtObject { - id: root - - property int usage: 0 - property real freqGhz: 0 - property var cores: [] // [{usage, freq_ghz, history:[]}] - history only while coreConsumers > 0 - property var coreMaxFreq: [] - property var coreTypes: [] - - // Overall CPU history (60 samples) - property var history: [] - - // Per-core data gating - applets increment/decrement - property int coreConsumers: 0 - onCoreConsumersChanged: { - if (coreConsumers > 0) - _coreGraceTimer.stop(); - else - _coreGraceTimer.start(); - } - - property Timer _coreGraceTimer: Timer { - interval: 30000 - onTriggered: root.cores = root.cores.map(() => ({ - "usage": 0, - "freq_ghz": 0, - "history": [] - })) - } - - // nova-stats process (cpu only) - property Process _proc: Process { - running: true - command: { - const ms = S.Modules.statsDaemon.interval; - return ms > 0 ? ["nova-stats", "--types", "cpu", "--interval", ms.toString()] : ["nova-stats", "--types", "cpu"]; - } - stdout: SplitParser { - splitMarker: "\n" - onRead: line => { - try { - const ev = JSON.parse(line); - if (ev.type !== "cpu") - return; - root.usage = ev.usage; - root.freqGhz = ev.freq_ghz; - - const h = root.history.concat([ev.usage]); - root.history = h.length > 60 ? h.slice(h.length - 60) : h; - - if (root.coreConsumers > 0) { - const histLen = 16; - const prev = root.cores; - root.cores = ev.cores.map((c, i) => { - const oldHist = prev[i]?.history ?? []; - const hist = oldHist.concat([c.usage]); - return { - usage: c.usage, - freq_ghz: c.freq_ghz, - history: hist.length > histLen ? hist.slice(hist.length - histLen) : hist - }; - }); - } else if (root.cores.length !== ev.cores.length) { - root.cores = ev.cores.map(c => ({ - "usage": c.usage, - "freq_ghz": c.freq_ghz, - "history": [] - })); - } - } catch (e) {} - } - } - } - - // One-time: per-core max freq - property Process _maxFreqProc: Process { - running: true - command: ["sh", "-c", "ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do f=\"$d/cpufreq/cpuinfo_max_freq\"; [ -f \"$f\" ] && cat \"$f\" || echo 0; done"] - stdout: StdioCollector { - onStreamFinished: { - root.coreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6); - } - } - } - - // One-time: P/E-core topology - property Process _coreTypesProc: Process { - running: true - command: ["sh", "-c", String.raw` - if [ -f /sys/devices/cpu_core/cpus ] && [ -f /sys/devices/cpu_atom/cpus ]; then - core=$(cat /sys/devices/cpu_core/cpus) - atom=$(cat /sys/devices/cpu_atom/cpus) - echo "hybrid:$core:$atom" - exit 0 - fi - ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do - f="$d/topology/core_type" - [ -f "$f" ] && cat "$f" - done - `] - stdout: StdioCollector { - onStreamFinished: { - const out = text.trim(); - if (!out) - return; - if (out.startsWith("hybrid:")) { - const parts = out.split(":"); - const coreRange = parts[1]; - const atomRange = parts[2]; - function expandRange(s) { - const cpus = new Set(); - for (const part of s.split(",")) { - if (part.includes("-")) { - const [a, b] = part.split("-").map(Number); - for (let i = a; i <= b; i++) - cpus.add(i); - } else { - cpus.add(Number(part)); - } - } - return cpus; - } - const pCores = expandRange(coreRange); - const eCores = expandRange(atomRange); - const maxCpu = Math.max(...pCores, ...eCores); - const types = []; - for (let i = 0; i <= maxCpu; i++) - types.push(eCores.has(i) ? "Efficiency" : "Performance"); - root.coreTypes = types; - } else { - const types = out.split("\n").filter(l => l).map(l => l.trim()); - if (types.length > 0) - root.coreTypes = types; - } - } - } - } - - // Fallback: infer P/E from max freq gap - function _inferCoreTypesFromFreq() { - if (coreTypes.length > 0 || coreMaxFreq.length < 2) - return; - const freqs = coreMaxFreq.filter(f => f > 0); - if (!freqs.length) - return; - const maxF = Math.max(...freqs); - const minF = Math.min(...freqs); - if (maxF > 0 && minF > 0 && (maxF - minF) / maxF > 0.15) { - const threshold = (maxF + minF) / 2; - coreTypes = coreMaxFreq.map(f => f >= threshold ? "Performance" : "Efficiency"); - } - } - onCoreMaxFreqChanged: Qt.callLater(_inferCoreTypesFromFreq) -} diff --git a/shell/services/SystemStats.qml b/shell/services/SystemStats.qml index bae2550..b2c67f0 100644 --- a/shell/services/SystemStats.qml +++ b/shell/services/SystemStats.qml @@ -7,6 +7,29 @@ import "." as M QtObject { id: root + // ── CPU ────────────────────────────────────────────────────────────── + property int cpuUsage: 0 + property real cpuFreqGhz: 0 + property var cpuCores: [] // [{usage, freq_ghz, history:[]}] — only rebuilt while coreConsumers > 0 + property int coreConsumers: 0 + onCoreConsumersChanged: { + if (coreConsumers > 0) + _coreGraceTimer.stop(); + else + _coreGraceTimer.start(); + } + + property var _coreGraceTimer: Timer { + interval: 30000 + onTriggered: root.cpuCores = root.cpuCores.map(() => ({ + "usage": 0, + "freq_ghz": 0, + "history": [] + })) + } + property var cpuCoreMaxFreq: [] + property var cpuCoreTypes: [] + // ── Temperature ────────────────────────────────────────────────────── property int tempCelsius: 0 property var tempHistory: [] // 150 samples @ 4s each ≈ 10 min @@ -34,19 +57,42 @@ QtObject { property var diskMounts: [] property int diskRootPct: 0 - // nova-stats stream (mem, temp, gpu - cpu handled by CpuService) + // nova-stats stream (cpu + mem) property var _statsProc: Process { running: true command: { const ms = M.Modules.statsDaemon.interval; - return ms > 0 ? ["nova-stats", "--types", "mem,temp,gpu", "--interval", ms.toString()] : ["nova-stats", "--types", "mem,temp,gpu"]; + return ms > 0 ? ["nova-stats", "--interval", ms.toString()] : ["nova-stats"]; } stdout: SplitParser { splitMarker: "\n" onRead: line => { try { const ev = JSON.parse(line); - if (ev.type === "temp") { + if (ev.type === "cpu") { + root.cpuUsage = ev.usage; + root.cpuFreqGhz = ev.freq_ghz; + if (root.coreConsumers > 0) { + const histLen = 16; + const prev = root.cpuCores; + root.cpuCores = ev.cores.map((c, i) => { + const oldHist = prev[i]?.history ?? []; + const hist = oldHist.concat([c.usage]); + return { + usage: c.usage, + freq_ghz: c.freq_ghz, + history: hist.length > histLen ? hist.slice(hist.length - histLen) : hist + }; + }); + } else if (root.cpuCores.length !== ev.cores.length) { + // Keep count in sync so panel can size correctly before consumers activate + root.cpuCores = ev.cores.map(c => ({ + "usage": c.usage, + "freq_ghz": c.freq_ghz, + "history": [] + })); + } + } else if (ev.type === "temp") { root.tempCelsius = ev.celsius; const th = root.tempHistory.concat([ev.celsius]); root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th; @@ -76,6 +122,91 @@ QtObject { } } + // One-time: per-core max freq (numerically sorted) + property var _maxFreqProc: Process { + running: true + command: ["sh", "-c", "ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do f=\"$d/cpufreq/cpuinfo_max_freq\"; [ -f \"$f\" ] && cat \"$f\" || echo 0; done"] + stdout: StdioCollector { + onStreamFinished: { + root.cpuCoreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6); + } + } + } + + // One-time: P/E-core topology + // Priority: cpu_core/cpu_atom sysfs > topology/core_type > freq-gap heuristic + property var _coreTypesProc: Process { + running: true + command: ["sh", "-c", String.raw` + # Intel hybrid: /sys/devices/cpu_core/cpus and cpu_atom/cpus give CPU ranges + if [ -f /sys/devices/cpu_core/cpus ] && [ -f /sys/devices/cpu_atom/cpus ]; then + core=$(cat /sys/devices/cpu_core/cpus) + atom=$(cat /sys/devices/cpu_atom/cpus) + echo "hybrid:$core:$atom" + exit 0 + fi + # Fallback: per-core topology/core_type + ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do + f="$d/topology/core_type" + [ -f "$f" ] && cat "$f" + done + `] + stdout: StdioCollector { + onStreamFinished: { + const out = text.trim(); + if (!out) + return; + if (out.startsWith("hybrid:")) { + // Parse cpu_core/cpu_atom ranges into per-cpu type array + const parts = out.split(":"); + const coreRange = parts[1]; + const atomRange = parts[2]; + function expandRange(s) { + const cpus = new Set(); + for (const part of s.split(",")) { + if (part.includes("-")) { + const [a, b] = part.split("-").map(Number); + for (let i = a; i <= b; i++) + cpus.add(i); + } else { + cpus.add(Number(part)); + } + } + return cpus; + } + const pCores = expandRange(coreRange); + const eCores = expandRange(atomRange); + const maxCpu = Math.max(...pCores, ...eCores); + const types = []; + for (let i = 0; i <= maxCpu; i++) + types.push(eCores.has(i) ? "Efficiency" : "Performance"); + root.cpuCoreTypes = types; + } else { + // topology/core_type output + const types = out.split("\n").filter(l => l).map(l => l.trim()); + if (types.length > 0) + root.cpuCoreTypes = types; + } + } + } + } + + // Fallback: infer P/E from max freq gap when no sysfs topology is available + function _inferCoreTypesFromFreq() { + if (cpuCoreTypes.length > 0 || cpuCoreMaxFreq.length < 2) + return; + const freqs = cpuCoreMaxFreq.filter(f => f > 0); + if (!freqs.length) + return; + const maxF = Math.max(...freqs); + const minF = Math.min(...freqs); + if (maxF > 0 && minF > 0 && (maxF - minF) / maxF > 0.15) { + const threshold = (maxF + minF) / 2; + cpuCoreTypes = cpuCoreMaxFreq.map(f => f >= threshold ? "Performance" : "Efficiency"); + } + } + onCpuCoreMaxFreqChanged: Qt.callLater(_inferCoreTypesFromFreq) + // Disk via df property var _diskProc: Process { id: diskProc diff --git a/shell/services/qmldir b/shell/services/qmldir index 42a29ec..38e7fd6 100644 --- a/shell/services/qmldir +++ b/shell/services/qmldir @@ -4,7 +4,6 @@ NotifItem 1.0 NotifItem.qml singleton BacklightService 1.0 BacklightService.qml singleton BatteryService 1.0 BatteryService.qml singleton BluetoothService 1.0 BluetoothService.qml -singleton CpuService 1.0 CpuService.qml singleton DockState 1.0 DockState.qml singleton IdleInhibitService 1.0 IdleInhibitService.qml singleton LockService 1.0 LockService.qml diff --git a/stats-daemon/src/main.rs b/stats-daemon/src/main.rs index 582e7f4..d54847d 100644 --- a/stats-daemon/src/main.rs +++ b/stats-daemon/src/main.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use std::io::{self, Write}; use std::thread; use std::time::{Duration, Instant}; @@ -22,69 +21,36 @@ fn parse_interval_ms() -> u64 { 1000 } -fn parse_types() -> HashSet { - let args: Vec = std::env::args().collect(); - let mut i = 1; - while i < args.len() { - if args[i] == "--types" { - if let Some(list) = args.get(i + 1) { - return list.split(',').map(|s| s.trim().to_string()).collect(); - } - } - i += 1; - } - HashSet::new() // empty = all types -} - fn main() { let interval = Duration::from_millis(parse_interval_ms()); - let types = parse_types(); let stdout = io::stdout(); let mut out = io::BufWriter::new(stdout.lock()); let mut prev: Vec = vec![]; let mut freqs: Vec = vec![]; + let mut gpu = gpu::detect_gpu(); let mut tick = 0u64; - let emit_cpu = types.is_empty() || types.contains("cpu"); - let emit_mem = types.is_empty() || types.contains("mem"); - let emit_temp = types.is_empty() || types.contains("temp"); - let emit_graphics = types.is_empty() || types.contains("gpu"); - - let mut gpu = if emit_graphics { - gpu::detect_gpu() - } else { - gpu::GpuBackend::None - }; - loop { let t0 = Instant::now(); - if emit_cpu { - let curr = cpu::read_stat(); - if tick.is_multiple_of(2) { - freqs = cpu::read_core_freqs(); - } - cpu::emit_cpu(&mut out, &prev, &curr, &freqs); - prev = curr; - } - - if emit_mem && tick.is_multiple_of(2) { + let curr = cpu::read_stat(); + if tick.is_multiple_of(2) { + freqs = cpu::read_core_freqs(); mem::emit_mem(&mut out); } + cpu::emit_cpu(&mut out, &prev, &curr, &freqs); + prev = curr; if tick.is_multiple_of(4) { - if emit_temp { - temp::emit_temp(&mut out); - } - if emit_graphics { - let emit = match &gpu { - gpu::GpuBackend::Amd { .. } | gpu::GpuBackend::Intel { .. } => true, - gpu::GpuBackend::Nvidia => tick.is_multiple_of(8), - gpu::GpuBackend::None => false, - }; - if emit { - gpu::emit_gpu(&mut out, &mut gpu); - } + temp::emit_temp(&mut out); + // AMD/Intel read sysfs (instant); NVIDIA shells out so runs less often + let emit = match &gpu { + gpu::GpuBackend::Amd { .. } | gpu::GpuBackend::Intel { .. } => true, + gpu::GpuBackend::Nvidia => tick.is_multiple_of(8), + gpu::GpuBackend::None => false, + }; + if emit { + gpu::emit_gpu(&mut out, &mut gpu); } }