refactor: add SystemStats singleton + nova-stats daemon for cpu/mem polling
This commit is contained in:
parent
71a843e0f3
commit
136ff53cb5
13 changed files with 371 additions and 196 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
result
|
||||||
|
result-*
|
||||||
23
flake.nix
23
flake.nix
|
|
@ -32,6 +32,7 @@
|
||||||
projectRootFile = "flake.nix";
|
projectRootFile = "flake.nix";
|
||||||
programs.nixfmt.enable = true;
|
programs.nixfmt.enable = true;
|
||||||
programs.qmlformat.enable = true;
|
programs.qmlformat.enable = true;
|
||||||
|
programs.rustfmt.enable = true;
|
||||||
};
|
};
|
||||||
forAllSystems =
|
forAllSystems =
|
||||||
fn:
|
fn:
|
||||||
|
|
@ -53,9 +54,14 @@
|
||||||
withX11 = false;
|
withX11 = false;
|
||||||
withI3 = false;
|
withI3 = false;
|
||||||
};
|
};
|
||||||
|
nova-stats = pkgs.callPackage ./nix/stats-daemon.nix { };
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
nova-shell = pkgs.callPackage ./nix/package.nix { quickshell = qs; };
|
inherit nova-stats;
|
||||||
|
nova-shell = pkgs.callPackage ./nix/package.nix {
|
||||||
|
quickshell = qs;
|
||||||
|
inherit nova-stats;
|
||||||
|
};
|
||||||
nova-shell-cli = pkgs.runCommand "nova-shell-cli" { nativeBuildInputs = [ pkgs.makeWrapper ]; } ''
|
nova-shell-cli = pkgs.runCommand "nova-shell-cli" { nativeBuildInputs = [ pkgs.makeWrapper ]; } ''
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
makeWrapper ${qs}/bin/quickshell $out/bin/nova-shell \
|
makeWrapper ${qs}/bin/quickshell $out/bin/nova-shell \
|
||||||
|
|
@ -65,6 +71,21 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
devShells = forAllSystems (
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
cargo
|
||||||
|
rustc
|
||||||
|
rust-analyzer
|
||||||
|
clippy
|
||||||
|
rustfmt
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
checks = forAllSystems (
|
checks = forAllSystems (
|
||||||
{ pkgs, treefmt-eval }:
|
{ pkgs, treefmt-eval }:
|
||||||
{
|
{
|
||||||
|
|
|
||||||
119
modules/Cpu.qml
119
modules/Cpu.qml
|
|
@ -1,5 +1,4 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Io
|
|
||||||
import "." as M
|
import "." as M
|
||||||
|
|
||||||
M.BarSection {
|
M.BarSection {
|
||||||
|
|
@ -7,7 +6,7 @@ M.BarSection {
|
||||||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||||
tooltip: ""
|
tooltip: ""
|
||||||
|
|
||||||
property int usage: 0
|
property int usage: M.SystemStats.cpuUsage
|
||||||
Behavior on usage {
|
Behavior on usage {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 400
|
duration: 400
|
||||||
|
|
@ -15,7 +14,7 @@ M.BarSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property real freqGhz: 0
|
property real freqGhz: M.SystemStats.cpuFreqGhz
|
||||||
Behavior on freqGhz {
|
Behavior on freqGhz {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 400
|
duration: 400
|
||||||
|
|
@ -23,115 +22,11 @@ M.BarSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var _prev: null
|
readonly property var _coreUsage: M.SystemStats.cpuCoreUsage
|
||||||
property var _corePrev: []
|
readonly property var _coreFreq: M.SystemStats.cpuCoreFreq
|
||||||
property var _coreUsage: []
|
readonly property var _coreHistory: M.SystemStats.cpuCoreHistory
|
||||||
property var _coreFreq: []
|
readonly property var _coreMaxFreq: M.SystemStats.cpuCoreMaxFreq
|
||||||
property var _coreHistory: [] // array of arrays, last 16 samples per core
|
readonly property var _coreTypes: M.SystemStats.cpuCoreTypes
|
||||||
property var _coreMaxFreq: [] // max freq in GHz per core, from cpufreq
|
|
||||||
property var _coreTypes: [] // "Performance" or "Efficiency" per core
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: stat
|
|
||||||
path: "/proc/stat"
|
|
||||||
onLoaded: {
|
|
||||||
const lines = text().split("\n");
|
|
||||||
|
|
||||||
// Aggregate
|
|
||||||
const agg = lines[0].trim().split(/\s+/).slice(1).map(Number);
|
|
||||||
const idle = agg[3] + agg[4];
|
|
||||||
const total = agg.reduce((a, b) => a + b, 0);
|
|
||||||
if (root._prev) {
|
|
||||||
const dIdle = idle - root._prev.idle;
|
|
||||||
const dTotal = total - root._prev.total;
|
|
||||||
if (dTotal > 0)
|
|
||||||
root.usage = Math.round((1 - dIdle / dTotal) * 100);
|
|
||||||
}
|
|
||||||
root._prev = {
|
|
||||||
idle,
|
|
||||||
total
|
|
||||||
};
|
|
||||||
|
|
||||||
// Per-core
|
|
||||||
const coreLines = lines.filter(l => /^cpu\d+\s/.test(l));
|
|
||||||
const newUsage = [];
|
|
||||||
const newPrev = root._corePrev.length === coreLines.length ? root._corePrev : Array(coreLines.length).fill(null);
|
|
||||||
for (let i = 0; i < coreLines.length; i++) {
|
|
||||||
const f = coreLines[i].trim().split(/\s+/).slice(1).map(Number);
|
|
||||||
const ci = f[3] + f[4];
|
|
||||||
const ct = f.reduce((a, b) => a + b, 0);
|
|
||||||
if (newPrev[i]) {
|
|
||||||
const di = ci - newPrev[i].idle;
|
|
||||||
const dt = ct - newPrev[i].total;
|
|
||||||
newUsage.push(dt > 0 ? Math.round((1 - di / dt) * 100) : 0);
|
|
||||||
} else {
|
|
||||||
newUsage.push(0);
|
|
||||||
}
|
|
||||||
newPrev[i] = {
|
|
||||||
idle: ci,
|
|
||||||
total: ct
|
|
||||||
};
|
|
||||||
}
|
|
||||||
root._coreUsage = newUsage;
|
|
||||||
root._corePrev = newPrev;
|
|
||||||
|
|
||||||
// Update sparkline history
|
|
||||||
const histLen = 16;
|
|
||||||
const oldH = root._coreHistory;
|
|
||||||
const newH = [];
|
|
||||||
for (let i = 0; i < newUsage.length; i++) {
|
|
||||||
const prev = i < oldH.length ? oldH[i] : [];
|
|
||||||
const next = prev.concat([newUsage[i]]);
|
|
||||||
newH.push(next.length > histLen ? next.slice(next.length - histLen) : next);
|
|
||||||
}
|
|
||||||
root._coreHistory = newH;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: cpuinfo
|
|
||||||
path: "/proc/cpuinfo"
|
|
||||||
onLoaded: {
|
|
||||||
const lines = text().split("\n").filter(l => l.startsWith("cpu MHz"));
|
|
||||||
if (lines.length === 0)
|
|
||||||
return;
|
|
||||||
const freqs = lines.map(l => parseFloat(l.split(":")[1]) / 1000);
|
|
||||||
root.freqGhz = freqs.reduce((a, b) => a + b, 0) / freqs.length;
|
|
||||||
root._coreFreq = freqs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read per-core max freq once at init
|
|
||||||
Process {
|
|
||||||
running: true
|
|
||||||
command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root._coreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read P/E-core topology once at init
|
|
||||||
Process {
|
|
||||||
running: true
|
|
||||||
command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root._coreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: M.Modules.cpu.interval || 1000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
stat.reload();
|
|
||||||
cpuinfo.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool _pinned: false
|
property bool _pinned: false
|
||||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Io
|
|
||||||
import "." as M
|
import "." as M
|
||||||
|
|
||||||
M.BarSection {
|
M.BarSection {
|
||||||
|
|
@ -7,46 +6,8 @@ M.BarSection {
|
||||||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||||
tooltip: ""
|
tooltip: ""
|
||||||
|
|
||||||
property var _mounts: []
|
property var _mounts: M.SystemStats.diskMounts
|
||||||
property int _rootPct: 0
|
property int _rootPct: M.SystemStats.diskRootPct
|
||||||
|
|
||||||
Process {
|
|
||||||
id: proc
|
|
||||||
running: true
|
|
||||||
command: ["sh", "-c", "df -x tmpfs -x devtmpfs -x squashfs -x efivarfs -x overlay -B1 --output=target,size,used 2>/dev/null | awk 'NR>1 && $2+0>0 {print $1\"|\"$2\"|\"$3}'"]
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const lines = text.trim().split("\n").filter(l => l);
|
|
||||||
const mounts = [];
|
|
||||||
for (const line of lines) {
|
|
||||||
const parts = line.split("|");
|
|
||||||
if (parts.length < 3)
|
|
||||||
continue;
|
|
||||||
const total = parseInt(parts[1]);
|
|
||||||
const used = parseInt(parts[2]);
|
|
||||||
if (total <= 0)
|
|
||||||
continue;
|
|
||||||
mounts.push({
|
|
||||||
"target": parts[0],
|
|
||||||
"pct": Math.round(used / total * 100),
|
|
||||||
"usedBytes": used,
|
|
||||||
"totalBytes": total
|
|
||||||
});
|
|
||||||
}
|
|
||||||
root._mounts = mounts;
|
|
||||||
const rm = mounts.find(m => m.target === "/");
|
|
||||||
if (rm)
|
|
||||||
root._rootPct = rm.pct;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: M.Modules.disk.interval || 30000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: proc.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function _fmt(bytes) {
|
function _fmt(bytes) {
|
||||||
if (bytes >= 1e12)
|
if (bytes >= 1e12)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Io
|
|
||||||
import "." as M
|
import "." as M
|
||||||
|
|
||||||
M.BarSection {
|
M.BarSection {
|
||||||
|
|
@ -7,45 +6,12 @@ M.BarSection {
|
||||||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||||
tooltip: ""
|
tooltip: ""
|
||||||
|
|
||||||
property int percent: 0
|
property int percent: M.SystemStats.memPercent
|
||||||
property real usedGb: 0
|
property real usedGb: M.SystemStats.memUsedGb
|
||||||
property real totalGb: 0
|
property real totalGb: M.SystemStats.memTotalGb
|
||||||
property real availGb: 0
|
property real availGb: M.SystemStats.memAvailGb
|
||||||
property real cachedGb: 0
|
property real cachedGb: M.SystemStats.memCachedGb
|
||||||
property real buffersGb: 0
|
property real buffersGb: M.SystemStats.memBuffersGb
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: meminfo
|
|
||||||
path: "/proc/meminfo"
|
|
||||||
onLoaded: {
|
|
||||||
const m = {};
|
|
||||||
text().split("\n").forEach(l => {
|
|
||||||
const [k, v] = l.split(":");
|
|
||||||
if (v)
|
|
||||||
m[k.trim()] = parseInt(v.trim());
|
|
||||||
});
|
|
||||||
const total = m.MemTotal || 0;
|
|
||||||
const avail = m.MemAvailable || 0;
|
|
||||||
const buffers = m.Buffers || 0;
|
|
||||||
const cached = (m.Cached || 0) + (m.SReclaimable || 0);
|
|
||||||
const used = total - avail;
|
|
||||||
if (total > 0) {
|
|
||||||
root.percent = Math.round(used / total * 100);
|
|
||||||
root.usedGb = used / 1048576;
|
|
||||||
root.totalGb = total / 1048576;
|
|
||||||
root.availGb = avail / 1048576;
|
|
||||||
root.cachedGb = cached / 1048576;
|
|
||||||
root.buffersGb = buffers / 1048576;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: M.Modules.memory.interval || 2000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: meminfo.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
function _fmt(gb) {
|
function _fmt(gb) {
|
||||||
return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G";
|
return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G";
|
||||||
|
|
|
||||||
127
modules/SystemStats.qml
Normal file
127
modules/SystemStats.qml
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// ── CPU ──────────────────────────────────────────────────────────────
|
||||||
|
property int cpuUsage: 0
|
||||||
|
property real cpuFreqGhz: 0
|
||||||
|
property var cpuCoreUsage: []
|
||||||
|
property var cpuCoreFreq: []
|
||||||
|
property var cpuCoreHistory: []
|
||||||
|
property var cpuCoreMaxFreq: []
|
||||||
|
property var cpuCoreTypes: []
|
||||||
|
|
||||||
|
// ── Memory ───────────────────────────────────────────────────────────
|
||||||
|
property int memPercent: 0
|
||||||
|
property real memUsedGb: 0
|
||||||
|
property real memTotalGb: 0
|
||||||
|
property real memAvailGb: 0
|
||||||
|
property real memCachedGb: 0
|
||||||
|
property real memBuffersGb: 0
|
||||||
|
|
||||||
|
// ── Disk ─────────────────────────────────────────────────────────────
|
||||||
|
property var diskMounts: []
|
||||||
|
property int diskRootPct: 0
|
||||||
|
|
||||||
|
// nova-stats stream (cpu + mem)
|
||||||
|
property var _statsProc: Process {
|
||||||
|
running: true
|
||||||
|
command: ["nova-stats"]
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: line => {
|
||||||
|
try {
|
||||||
|
const ev = JSON.parse(line);
|
||||||
|
if (ev.type === "cpu") {
|
||||||
|
root.cpuUsage = ev.usage;
|
||||||
|
root.cpuFreqGhz = ev.freq_ghz;
|
||||||
|
root.cpuCoreUsage = ev.core_usage;
|
||||||
|
root.cpuCoreFreq = ev.core_freq_ghz;
|
||||||
|
const histLen = 16;
|
||||||
|
const oldH = root.cpuCoreHistory;
|
||||||
|
const newH = [];
|
||||||
|
for (let i = 0; i < ev.core_usage.length; i++) {
|
||||||
|
const prev = i < oldH.length ? oldH[i] : [];
|
||||||
|
const next = prev.concat([ev.core_usage[i]]);
|
||||||
|
newH.push(next.length > histLen ? next.slice(next.length - histLen) : next);
|
||||||
|
}
|
||||||
|
root.cpuCoreHistory = newH;
|
||||||
|
} else if (ev.type === "mem") {
|
||||||
|
root.memPercent = ev.percent;
|
||||||
|
root.memUsedGb = ev.used_gb;
|
||||||
|
root.memTotalGb = ev.total_gb;
|
||||||
|
root.memAvailGb = ev.avail_gb;
|
||||||
|
root.memCachedGb = ev.cached_gb;
|
||||||
|
root.memBuffersGb = ev.buffers_gb;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-time: per-core max freq
|
||||||
|
property var _maxFreqProc: Process {
|
||||||
|
running: true
|
||||||
|
command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.cpuCoreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-time: P/E-core topology
|
||||||
|
property var _coreTypesProc: Process {
|
||||||
|
running: true
|
||||||
|
command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.cpuCoreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk via df
|
||||||
|
property var _diskProc: Process {
|
||||||
|
id: diskProc
|
||||||
|
running: true
|
||||||
|
command: ["sh", "-c", "df -x tmpfs -x devtmpfs -x squashfs -x efivarfs -x overlay -B1 --output=target,size,used 2>/dev/null | awk 'NR>1 && $2+0>0 {print $1\"|\"$2\"|\"$3}'"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const lines = text.trim().split("\n").filter(l => l);
|
||||||
|
const mounts = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split("|");
|
||||||
|
if (parts.length < 3)
|
||||||
|
continue;
|
||||||
|
const total = parseInt(parts[1]);
|
||||||
|
const used = parseInt(parts[2]);
|
||||||
|
if (total <= 0)
|
||||||
|
continue;
|
||||||
|
mounts.push({
|
||||||
|
"target": parts[0],
|
||||||
|
"pct": Math.round(used / total * 100),
|
||||||
|
"usedBytes": used,
|
||||||
|
"totalBytes": total
|
||||||
|
});
|
||||||
|
}
|
||||||
|
root.diskMounts = mounts;
|
||||||
|
const rm = mounts.find(m => m.target === "/");
|
||||||
|
if (rm)
|
||||||
|
root.diskRootPct = rm.pct;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var _diskTimer: Timer {
|
||||||
|
interval: M.Modules.disk.interval || 30000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: diskProc.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,7 @@ PowerProfile 1.0 PowerProfile.qml
|
||||||
IdleInhibitor 1.0 IdleInhibitor.qml
|
IdleInhibitor 1.0 IdleInhibitor.qml
|
||||||
Notifications 1.0 Notifications.qml
|
Notifications 1.0 Notifications.qml
|
||||||
singleton NiriIpc 1.0 NiriIpc.qml
|
singleton NiriIpc 1.0 NiriIpc.qml
|
||||||
|
singleton SystemStats 1.0 SystemStats.qml
|
||||||
singleton ProcessList 1.0 ProcessList.qml
|
singleton ProcessList 1.0 ProcessList.qml
|
||||||
singleton NotifService 1.0 NotifService.qml
|
singleton NotifService 1.0 NotifService.qml
|
||||||
NotifItem 1.0 NotifItem.qml
|
NotifItem 1.0 NotifItem.qml
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,8 @@ in
|
||||||
pkgs.nerd-fonts.symbols-only
|
pkgs.nerd-fonts.symbols-only
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.modules.weather.enable pkgs.wttrbar
|
++ lib.optional cfg.modules.weather.enable pkgs.wttrbar
|
||||||
++ lib.optional cfg.modules.mpris.enable pkgs.cava;
|
++ lib.optional cfg.modules.mpris.enable pkgs.cava
|
||||||
|
++ lib.optional cfg.modules.disk.enable pkgs.coreutils;
|
||||||
|
|
||||||
xdg.configFile."nova-shell/modules.json".source =
|
xdg.configFile."nova-shell/modules.json".source =
|
||||||
(pkgs.formats.json { }).generate "nova-shell-modules.json"
|
(pkgs.formats.json { }).generate "nova-shell-modules.json"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
makeWrapper,
|
makeWrapper,
|
||||||
quickshell,
|
quickshell,
|
||||||
qt6,
|
qt6,
|
||||||
|
nova-stats,
|
||||||
}:
|
}:
|
||||||
stdenvNoCC.mkDerivation {
|
stdenvNoCC.mkDerivation {
|
||||||
pname = "nova-shell";
|
pname = "nova-shell";
|
||||||
|
|
@ -32,7 +33,8 @@ stdenvNoCC.mkDerivation {
|
||||||
|
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
makeWrapper ${lib.getExe quickshell} $out/bin/nova-shell \
|
makeWrapper ${lib.getExe quickshell} $out/bin/nova-shell \
|
||||||
--add-flags "-p $out/share/nova-shell/shell.qml"
|
--add-flags "-p $out/share/nova-shell/shell.qml" \
|
||||||
|
--prefix PATH : ${lib.makeBinPath [ nova-stats ]}
|
||||||
|
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
|
||||||
12
nix/stats-daemon.nix
Normal file
12
nix/stats-daemon.nix
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{ lib, rustPlatform }:
|
||||||
|
rustPlatform.buildRustPackage {
|
||||||
|
pname = "nova-stats";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = lib.cleanSource ../stats-daemon;
|
||||||
|
cargoLock.lockFile = ../stats-daemon/Cargo.lock;
|
||||||
|
meta = {
|
||||||
|
description = "System stats daemon for nova-shell";
|
||||||
|
mainProgram = "nova-stats";
|
||||||
|
platforms = lib.platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
||||||
7
stats-daemon/Cargo.lock
generated
Normal file
7
stats-daemon/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nova-stats"
|
||||||
|
version = "0.1.0"
|
||||||
8
stats-daemon/Cargo.toml
Normal file
8
stats-daemon/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "nova-stats"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "nova-stats"
|
||||||
|
path = "src/main.rs"
|
||||||
172
stats-daemon/src/main.rs
Normal file
172
stats-daemon/src/main.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
struct Sample {
|
||||||
|
idle: u64,
|
||||||
|
total: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_stat() -> Vec<Sample> {
|
||||||
|
fs::read_to_string("/proc/stat")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.lines()
|
||||||
|
.filter(|l| l.starts_with("cpu"))
|
||||||
|
.map(|l| {
|
||||||
|
let vals: Vec<u64> = l
|
||||||
|
.split_whitespace()
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect();
|
||||||
|
let idle = vals.get(3).copied().unwrap_or(0) + vals.get(4).copied().unwrap_or(0);
|
||||||
|
let total = vals.iter().sum();
|
||||||
|
Sample { idle, total }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pct(prev: &Sample, curr: &Sample) -> u32 {
|
||||||
|
let dt = curr.total.saturating_sub(prev.total);
|
||||||
|
let di = curr.idle.saturating_sub(prev.idle);
|
||||||
|
if dt == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
(dt.saturating_sub(di) * 100 / dt) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_core_freqs() -> Vec<f64> {
|
||||||
|
let mut freqs = Vec::new();
|
||||||
|
for i in 0.. {
|
||||||
|
let path = format!("/sys/devices/system/cpu/cpu{i}/cpufreq/scaling_cur_freq");
|
||||||
|
match fs::read_to_string(&path) {
|
||||||
|
Ok(s) => match s.trim().parse::<u64>() {
|
||||||
|
Ok(khz) => freqs.push(khz as f64 / 1_000_000.0),
|
||||||
|
Err(_) => break,
|
||||||
|
},
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
freqs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_cpu(out: &mut impl Write, prev: &[Sample], curr: &[Sample], freqs: &[f64]) {
|
||||||
|
if curr.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let usage = prev
|
||||||
|
.first()
|
||||||
|
.zip(curr.first())
|
||||||
|
.map(|(p, c)| pct(p, c))
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let core_usage: Vec<u32> = prev
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.zip(curr.iter().skip(1))
|
||||||
|
.map(|(p, c)| pct(p, c))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let avg_freq = if freqs.is_empty() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
freqs.iter().sum::<f64>() / freqs.len() as f64
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = write!(
|
||||||
|
out,
|
||||||
|
"{{\"type\":\"cpu\",\"usage\":{usage},\"freq_ghz\":{avg_freq:.3},\"core_usage\":["
|
||||||
|
);
|
||||||
|
for (i, u) in core_usage.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
let _ = write!(out, ",");
|
||||||
|
}
|
||||||
|
let _ = write!(out, "{u}");
|
||||||
|
}
|
||||||
|
let _ = write!(out, "],\"core_freq_ghz\":[");
|
||||||
|
for (i, f) in freqs.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
let _ = write!(out, ",");
|
||||||
|
}
|
||||||
|
let _ = write!(out, "{f:.3}");
|
||||||
|
}
|
||||||
|
let _ = writeln!(out, "]}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_mem(out: &mut impl Write) {
|
||||||
|
let content = fs::read_to_string("/proc/meminfo").unwrap_or_default();
|
||||||
|
let mut total = 0u64;
|
||||||
|
let mut avail = 0u64;
|
||||||
|
let mut buffers = 0u64;
|
||||||
|
let mut cached = 0u64;
|
||||||
|
let mut sreclaimable = 0u64;
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let mut parts = line.splitn(2, ':');
|
||||||
|
let key = parts.next().unwrap_or("").trim();
|
||||||
|
let val: u64 = parts
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(0);
|
||||||
|
match key {
|
||||||
|
"MemTotal" => total = val,
|
||||||
|
"MemAvailable" => avail = val,
|
||||||
|
"Buffers" => buffers = val,
|
||||||
|
"Cached" => cached = val,
|
||||||
|
"SReclaimable" => sreclaimable = val,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if total == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let used = total.saturating_sub(avail);
|
||||||
|
let cached_total = cached + sreclaimable;
|
||||||
|
let percent = used * 100 / total;
|
||||||
|
let gb = |kb: u64| kb as f64 / 1_048_576.0;
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"{{\"type\":\"mem\",\"percent\":{percent},\"used_gb\":{:.3},\"total_gb\":{:.3},\"avail_gb\":{:.3},\"cached_gb\":{:.3},\"buffers_gb\":{:.3}}}",
|
||||||
|
gb(used),
|
||||||
|
gb(total),
|
||||||
|
gb(avail),
|
||||||
|
gb(cached_total),
|
||||||
|
gb(buffers),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut out = io::BufWriter::new(stdout.lock());
|
||||||
|
let mut prev: Vec<Sample> = vec![];
|
||||||
|
let mut tick = 0u64;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
|
||||||
|
let curr = read_stat();
|
||||||
|
let freqs = read_core_freqs();
|
||||||
|
emit_cpu(&mut out, &prev, &curr, &freqs);
|
||||||
|
prev = curr;
|
||||||
|
|
||||||
|
if tick % 2 == 0 {
|
||||||
|
emit_mem(&mut out);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = out.flush();
|
||||||
|
tick += 1;
|
||||||
|
|
||||||
|
let elapsed = t0.elapsed();
|
||||||
|
if elapsed < Duration::from_secs(1) {
|
||||||
|
thread::sleep(Duration::from_secs(1) - elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue