repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/sysinfo.js
Raw// sysinfo.js — pure parsing + issue-detection logic for the system popup.
// NO I/O. Dual-mode: node `require` (tests) + QML import. Same pattern as caldav.js.
// --- clamp a value into 0..1 ---
function clamp01(value) {
var n = Number(value);
if (!isFinite(n)) return 0;
return n < 0 ? 0 : (n > 1 ? 1 : n);
}
// --- /proc/stat first line: idle/total delta -> {usage, next}.
// prev is the previous {idle, total} (null/empty on first call). curLine may be
// the whole file or just the first line; only the leading "cpu " row is used. ---
function cpuDelta(prev, curLine) {
var line = String(curLine == null ? "" : curLine).split(/\r?\n/)[0] || "";
var parts = line.trim().split(/\s+/).slice(1).map(Number);
if (parts.length < 4 || parts.some(function (n) { return !isFinite(n); })) {
return { usage: 0, next: (prev && isFinite(prev.total)) ? prev : { idle: 0, total: 0 } };
}
var idle = parts[3] + (parts[4] || 0);
var total = parts.reduce(function (sum, n) { return sum + n; }, 0);
var next = { idle: idle, total: total };
if (!prev || !isFinite(prev.total)) return { usage: 0, next: next };
var totalDelta = total - prev.total;
var idleDelta = idle - prev.idle;
var usage = totalDelta > 0 ? clamp01(1 - idleDelta / totalDelta) : 0;
return { usage: usage, next: next };
}
// --- /proc/meminfo (kB) -> bytes + usage ratios ---
function parseMeminfo(text) {
var values = {};
var lines = String(text == null ? "" : text).split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
var m = lines[i].match(/^([A-Za-z_()]+):\s+(\d+)/);
if (m) values[m[1]] = Number(m[2]) * 1024;
}
var memTotal = Number(values.MemTotal || 0);
var memAvailable = Number(values.MemAvailable != null ? values.MemAvailable : (values.MemFree || 0));
var swapTotal = Number(values.SwapTotal || 0);
var swapFree = Number(values.SwapFree || 0);
return {
memTotal: memTotal,
memAvailable: memAvailable,
swapTotal: swapTotal,
swapFree: swapFree,
memoryUsage: memTotal > 0 ? clamp01((memTotal - memAvailable) / memTotal) : 0,
swapUsage: swapTotal > 0 ? clamp01((swapTotal - swapFree) / swapTotal) : 0
};
}
// --- /proc/net/dev -> {rx, tx} cumulative bytes, summed over non-lo ifaces.
// After the colon: rx_bytes(0) ... | tx_bytes(8) ... ---
function parseNetDev(text) {
var rx = 0, tx = 0;
var lines = String(text == null ? "" : text).split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
var colon = lines[i].indexOf(":");
if (colon < 0) continue;
var iface = lines[i].slice(0, colon).trim();
if (!iface || iface === "lo") continue;
var f = lines[i].slice(colon + 1).trim().split(/\s+/).map(Number);
if (f.length < 9 || !isFinite(f[0]) || !isFinite(f[8])) continue;
rx += f[0];
tx += f[8];
}
return { rx: rx, tx: tx };
}
// --- /proc/diskstats -> {readBytes, writeBytes} cumulative.
// fields: major minor name reads(3) rmerged(4) sectorsRead(5) ... writes(7) wmerged(8) sectorsWritten(9)
// skip loop*/ram*; bytes = sectors * 512. Sums partitions too (literal). ---
function parseDiskstats(text) {
var readBytes = 0, writeBytes = 0;
var lines = String(text == null ? "" : text).split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
var p = lines[i].trim().split(/\s+/);
if (p.length < 10) continue;
var name = p[2];
if (/^(loop|ram)\d*/.test(name)) continue;
// skip partitions (sda1, nvme0n1p1, mmcblk0p1, vda1) — their I/O is already
// counted in the whole-disk line; summing both ~doubles the reported rate.
if (/^sd[a-z]+\d+$|^nvme\d+n\d+p\d+$|^mmcblk\d+p\d+$|^vd[a-z]+\d+$/.test(name)) continue;
var sread = Number(p[5]), swrite = Number(p[9]);
if (!isFinite(sread) || !isFinite(swrite)) continue;
readBytes += sread * 512;
writeBytes += swrite * 512;
}
return { readBytes: readBytes, writeBytes: writeBytes };
}
// --- generic per-second rate from two cumulative counters. Guards counter
// reset (negative) and non-positive intervals -> 0. ---
function rate(prev, cur, seconds) {
var p = Number(prev), c = Number(cur), s = Number(seconds);
if (!isFinite(p) || !isFinite(c) || !isFinite(s) || s <= 0) return 0;
var d = c - p;
return d > 0 ? d / s : 0;
}
// --- /proc/loadavg -> {one, five, fifteen} ---
function parseLoadavg(text) {
var p = String(text == null ? "" : text).trim().split(/\s+/);
return {
one: Number(p[0]) || 0,
five: Number(p[1]) || 0,
fifteen: Number(p[2]) || 0
};
}
// --- `df -P` (or -B1) -> [{source, target, usedPercent}].
// header + pseudo rows whose capacity column is not a percent are skipped. ---
function parseDf(text) {
var out = [];
var lines = String(text == null ? "" : text).split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var p = line.split(/\s+/);
if (p.length < 6) continue;
var capIdx = -1;
for (var j = 1; j < p.length; j++) {
if (/^\d+%$/.test(p[j])) { capIdx = j; break; }
}
if (capIdx < 0) continue; // header / no percent column
out.push({
source: p[0],
target: p.slice(capIdx + 1).join(" "),
usedPercent: parseInt(p[capIdx], 10)
});
}
return out;
}
// --- `systemctl --failed --no-legend --plain` -> [unit]. First token per line. ---
function parseFailedUnits(text) {
var out = [];
var lines = String(text == null ? "" : text).split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var name = line.split(/\s+/)[0];
if (name && name.indexOf(".") > 0) out.push(name);
}
return out;
}
// --- `ps -eo pid,comm,%cpu,%mem` -> [{pid, name, cpu, mem}]. Header skipped
// (non-numeric pid). ---
function parsePsTop(text) {
var out = [];
var lines = String(text == null ? "" : text).trim().split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
var p = lines[i].trim().split(/\s+/);
if (p.length < 4) continue;
var pid = Number(p[0]);
if (!isFinite(pid)) continue; // header row
out.push({ pid: pid, name: p[1], cpu: Number(p[2]) || 0, mem: Number(p[3]) || 0 });
}
return out;
}
// --- parse a hwmon integer text (millidegrees -> °C) ---
function parseTemp(text) {
var s = String(text == null ? "" : text).trim();
if (s === "") return null;
var v = Number(s);
return isFinite(v) ? v / 1000 : null;
}
// --- parse a hwmon fan integer text (rpm) ---
function parseFan(text) {
var s = String(text == null ? "" : text).trim();
if (s === "") return null;
var v = Number(s);
return isFinite(v) ? v : null;
}
// --- binary formatters (verbatim thresholds from Resources.qml for visual parity) ---
function formatBytes(value) {
var bytes = Math.max(0, Number(value));
if (!isFinite(bytes)) bytes = 0;
if (bytes >= 1.07374e+09) return (bytes / 1073741824).toFixed(1) + " GiB";
if (bytes >= 1.04858e+06) return (bytes / 1048576).toFixed(1) + " MiB";
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + " KiB";
return Math.round(bytes) + " B";
}
function formatRate(value) {
return formatBytes(value) + "/s";
}
// --- ring buffer: append value, cap to last `cap` samples. Returns a new array. ---
function pushRing(buf, value, cap) {
var next = (buf || []).slice();
next.push(value);
var limit = Math.max(0, Number(cap) || 0);
while (next.length > limit) next.shift();
return next;
}
// --- severity ranking for sort: error < warning < info ---
function severityRank(s) {
return s === "error" ? 0 : (s === "warning" ? 1 : 2);
}
// --- detectIssues(state) -> sorted [{id, severity, icon, label, detail, action}].
// A check whose input is absent (null/undefined) is skipped, never fired.
// state fields (downstream contract):
// failedUnits: [unit] | undefined
// cpuTemperatureC: number | null tempHighC: number (default 87)
// degradationReason: string | null (UPower PowerProfiles)
// diskUsage: [{source,target,usedPercent}] | undefined
// memoryUsage: 0..1 | null swapGrowing: bool
// healthPercentage: number | null isLaptopBattery: bool
// loadOne: number | null cores: number
// updateCount: number | null
function detectIssues(state) {
var st = state || {};
var issues = [];
// Failed services -> error
if (st.failedUnits != null && st.failedUnits.length > 0) {
var units = st.failedUnits.slice();
issues.push({
id: "failed-services",
severity: "error",
icon: "",
label: units.length === 1 ? "failed service" : (units.length + " failed services"),
detail: units.join(", "),
action: st.failedScopes ? { kind: "systemctl-status", units: units, scopes: st.failedScopes } : { kind: "systemctl-status", units: units }
});
}
// Thermal -> error if power-profile reports thermal degradation, else
// warning when temp >= high threshold. Skipped if no temp and no signal.
var tempHigh = isFinite(Number(st.tempHighC)) ? Number(st.tempHighC) : 87;
var degradeThermal = !!st.degradationReason &&
/thermal|temperature/i.test(String(st.degradationReason));
var tempHot = st.cpuTemperatureC != null && Number(st.cpuTemperatureC) >= tempHigh;
if (degradeThermal || tempHot) {
issues.push({
id: "thermal",
severity: degradeThermal ? "error" : "warning",
icon: "",
label: degradeThermal ? "thermal throttling" : "cpu running hot",
detail: st.cpuTemperatureC != null ? (Math.round(Number(st.cpuTemperatureC)) + "°C") : "thermal limit",
measurements: { temperatureC: st.cpuTemperatureC, highThresholdC: tempHigh, degradationReason: st.degradationReason || null },
action: { kind: "btm" }
});
}
// Low disk -> warning >= 90, error >= 95. Worst mount per source device.
if (st.diskUsage != null) {
var worst = {};
for (var d = 0; d < st.diskUsage.length; d++) {
var m = st.diskUsage[d];
var pct = Number(m.usedPercent);
if (!isFinite(pct) || pct < 90) continue;
var key = m.source || m.target;
if (!worst[key] || pct > worst[key].usedPercent) {
worst[key] = { usedPercent: pct, target: m.target, source: m.source };
}
}
var keys = Object.keys(worst);
for (var k = 0; k < keys.length; k++) {
var w = worst[keys[k]];
issues.push({
id: "low-disk:" + keys[k],
severity: w.usedPercent >= 95 ? "error" : "warning",
icon: "",
label: "low disk space",
detail: w.target + " " + w.usedPercent + "%",
measurements: { source: w.source, mount: w.target, usedPercent: w.usedPercent },
action: { kind: "open-mount", target: w.target }
});
}
}
// Memory pressure -> warning when mem high AND swap actively growing.
if (st.memoryUsage != null && Number(st.memoryUsage) >= 0.9 && st.swapGrowing === true) {
issues.push({
id: "mem-pressure",
severity: "warning",
icon: "",
label: "memory pressure",
detail: Math.round(Number(st.memoryUsage) * 100) + "% used, swapping",
measurements: { memoryUsage: Number(st.memoryUsage), swapGrowing: st.swapGrowing },
action: { kind: "btm" }
});
}
// Battery health -> warning < 80, error < 60. Skipped if absent / no battery.
if (st.isLaptopBattery !== false && st.healthPercentage != null) {
var health = Number(st.healthPercentage);
if (isFinite(health) && health < 80) {
issues.push({
id: "battery-health",
severity: health < 60 ? "error" : "warning",
icon: "",
label: "battery wear",
detail: Math.round(health) + "% health",
action: null
});
}
}
// High load -> warning when 1-min load exceeds core count.
if (st.loadOne != null && Number(st.cores) > 0 && Number(st.loadOne) > Number(st.cores)) {
issues.push({
id: "high-load",
severity: "warning",
icon: "",
label: "high load",
detail: Number(st.loadOne).toFixed(2) + " on " + Number(st.cores) + " cores",
measurements: { loadOne: Number(st.loadOne), cores: Number(st.cores) },
action: { kind: "btm" }
});
}
// Pending updates -> info.
if (st.updateCount != null && Number(st.updateCount) > 0) {
var count = Number(st.updateCount);
issues.push({
id: "updates",
severity: "info",
icon: "",
label: count === 1 ? "1 update available" : (count + " updates available"),
detail: "pending packages",
action: { kind: "update" }
});
}
issues.sort(function (a, b) { return severityRank(a.severity) - severityRank(b.severity); });
return issues;
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { clamp01: clamp01, cpuDelta: cpuDelta, parseMeminfo: parseMeminfo,
parseNetDev: parseNetDev, parseDiskstats: parseDiskstats, rate: rate,
parseLoadavg: parseLoadavg, parseDf: parseDf, parseFailedUnits: parseFailedUnits,
parsePsTop: parsePsTop, parseTemp: parseTemp, parseFan: parseFan,
formatBytes: formatBytes, formatRate: formatRate, pushRing: pushRing,
detectIssues: detectIssues };
}