Luigit
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 };
}