pragma Singleton

// ─────────────────────────────────────────────────────────────────────────────
// System vitals — CPU, memory and GPU utilisation.
//
// Replaces the GNOME Vitals extension, which showed exactly these three in
// exactly this order. Everything comes from procfs/sysfs, so there are no
// subprocesses: one timer kicks three async re-reads of very small files and
// the parsing happens in the `loaded` handlers.
// ─────────────────────────────────────────────────────────────────────────────

import Quickshell
import Quickshell.Io
import QtQuick
import qs.config

Singleton {
    id: root

    // All three are percentages, 0-100.
    property real cpu: 0
    property real memory: 0
    property real gpu: 0

    // False when Settings.gpuBusyPath is missing or unreadable (no amdgpu, or a
    // different card index). The widget hides the GPU field rather than
    // reporting a permanent 0%.
    property bool gpuAvailable: false

    // /proc/stat is cumulative since boot, so utilisation is only meaningful as
    // a delta between two samples. These hold the previous one.
    property real _prevTotal: 0
    property real _prevIdle: 0

    Timer {
        interval: Settings.vitalsIntervalMs
        running: true
        repeat: true
        triggeredOnStart: true
        onTriggered: {
            cpuFile.reload();
            memFile.reload();
            if (Settings.showGpu)
                gpuFile.reload();
        }
    }

    // ── CPU ─────────────────────────────────────────────────────────────────
    FileView {
        id: cpuFile
        path: "/proc/stat"
        printErrors: false
        onLoaded: root._parseCpu(text())
    }

    // ── Memory ──────────────────────────────────────────────────────────────
    FileView {
        id: memFile
        path: "/proc/meminfo"
        printErrors: false
        onLoaded: root._parseMemory(text())
    }

    // ── GPU ─────────────────────────────────────────────────────────────────
    // amdgpu exposes a bare integer 0-100 here.
    FileView {
        id: gpuFile
        path: Settings.gpuBusyPath
        printErrors: false
        onLoaded: root._parseGpu(text())
        onLoadFailed: root.gpuAvailable = false
    }

    function _parseCpu(text: string): void {
        if (!text)
            return;

        // First line is the aggregate: "cpu  user nice system idle iowait ..."
        const fields = text.slice(0, text.indexOf("\n")).split(/\s+/).slice(1).map(parseFloat).filter(n => !isNaN(n));
        if (fields.length < 5)
            return;

        // idle + iowait — the kernel counts both as "not doing work".
        const idle = fields[3] + fields[4];
        let total = 0;
        for (let i = 0; i < fields.length; i++)
            total += fields[i];

        const dTotal = total - root._prevTotal;
        const dIdle = idle - root._prevIdle;
        root._prevTotal = total;
        root._prevIdle = idle;

        // The very first sample has no predecessor; reporting it would show the
        // since-boot average, which is never what you want.
        if (dTotal <= 0)
            return;

        root.cpu = Math.max(0, Math.min(100, (1 - dIdle / dTotal) * 100));
    }

    function _parseMemory(text: string): void {
        // MemAvailable is the kernel's own estimate of what a new allocation
        // could get; a much better "used" basis than MemFree.
        const total = root._meminfoField(text, "MemTotal");
        const available = root._meminfoField(text, "MemAvailable");
        if (total <= 0 || available < 0)
            return;

        root.memory = Math.max(0, Math.min(100, (1 - available / total) * 100));
    }

    function _meminfoField(text: string, key: string): real {
        const match = text.match(new RegExp("^" + key + ":\\s+(\\d+)", "m"));
        return match ? parseFloat(match[1]) : -1;
    }

    function _parseGpu(text: string): void {
        const value = parseInt(text, 10);
        if (isNaN(value)) {
            root.gpuAvailable = false;
            return;
        }

        root.gpuAvailable = true;
        root.gpu = Math.max(0, Math.min(100, value));
    }
}
