Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/SystemPanel.qml

Raw
pragma ComponentBehavior: Bound
import QtQuick
import "sysinfo.js" as Sys

Item {
    id: root

    required property Theme theme
    readonly property Flickable scrollView: contentScroll
    required property Resources resourceService
    implicitHeight: contentColumn.implicitHeight
    height: parent ? parent.height : implicitHeight

    // service handle; identity facts and the live issue ARRAY come straight
    // off Resources.qml (no per-frame JSON parse).
    readonly property var rs: root.resourceService
    readonly property var identity: rs ? rs.identity : ({})
    // touch the inputs so the band re-evaluates when the slow/rare loops
    // mutate them; issues() itself is a plain function call.
    property var issueList: []
    readonly property var currentIssues: {
        if (!rs)
            return [];

        rs.failedUnits;
        rs.incidents;
        rs.cpuTemperatureC;
        rs.diskUsage;
        rs.updateCount;
        rs.memoryUsage;
        rs.swapGrowing;
        rs.loadOne;
        rs.healthPercentage;
        return rs.issues();
    }

    onCurrentIssuesChanged: {
        now = Date.now();
        if (JSON.stringify(currentIssues) !== JSON.stringify(issueList))
            issueList = currentIssues;
    }
    property real now: Date.now()
    Timer {
        interval: 30000
        running: true
        repeat: true
        onTriggered: root.now = Date.now()
    }

    // popoutOpen is a WRITE contract: it gates the slow + rare loops.
    Component.onCompleted: {
        if (rs)
            rs.popoutOpen = true;
    }
    Component.onDestruction: {
        if (rs)
            rs.popoutOpen = false;
    }

    Flickable {
        id: contentScroll

        anchors.fill: parent
        contentHeight: contentColumn.implicitHeight
        clip: true
        boundsBehavior: Flickable.StopAtBounds

        function revealItem(item) {
            const point = item.mapToItem(contentColumn, 0, 0);
            if (point.y < contentY)
                contentY = point.y;
            else if (point.y + item.height > contentY + height)
                contentY = Math.min(point.y + item.height - height, Math.max(0, contentHeight - height));
        }

        Column {
            id: contentColumn

            width: contentScroll.width
            spacing: 10

            // ---- two rounded cards: identity | live, split on the golden ratio
            // (live gets the larger ~0.618 share, that's where the live data lives) ----
            Row {
                id: cardRow

                readonly property real avail: Math.max(1, width - spacing)
                readonly property real machineW: avail * 0.382
                readonly property real liveW: avail * 0.618

                width: parent.width
                height: 292
                spacing: 10

                // ===== MACHINE IDENTITY =====
                PopupCard {
                    theme: root.theme
                    padding: 0
                    width: cardRow.machineW
                    height: cardRow.height

                    Column {
                        anchors.fill: parent
                        anchors.margins: 14
                        spacing: 6

                        Row {
                            spacing: 8

                            Text {
                                text: ""
                                color: root.theme ? root.theme.ui.accent : "white"
                                font.family: "Symbols Nerd Font Mono"
                                font.pixelSize: 15
                            }

                            NuguText {
                                theme: root.theme
                                variant: "title"
                                text: "machine"
                            }
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            value: root.identity.host || "—"
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            value: root.identity.model || "—"
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            value: root.identity.distro || "—"
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            value: root.identity.kernel || "—"
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            value: root.identity.wm || "—"
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            value: root.identity.packages ? `${root.identity.packages} packages` : "—"
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            value: root.identity.shell || "—"
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            value: root.identity.uptime || "—"
                        }

                        IdentityRow {
                            theme: root.theme
                            glyph: ""
                            // battery row hidden entirely when there is no laptop battery
                            visible: root.rs && root.rs.isLaptopBattery
                            value: {
                                if (!root.rs)
                                    return "—";

                                const p = root.rs.batteryPercent;
                                const st = root.rs.batteryStatus;
                                const h = root.rs.healthPercentage;
                                let s = (p === null || p === undefined) ? "battery n/a" : `battery ${p}%`;
                                if (st)
                                    s += ` · ${st}`;
                                if (h !== null && h !== undefined)
                                    s += ` · health ${Math.round(Number(h))}%`;
                                return s;
                            }
                        }
                    }
                }

                // ===== LIVE STATS =====
                PopupCard {
                    theme: root.theme
                    padding: 0
                    width: cardRow.liveW
                    height: cardRow.height

                    Column {
                        anchors.fill: parent
                        anchors.margins: 14
                        spacing: 8

                        Row {
                            spacing: 8

                            Text {
                                text: ""
                                color: root.theme ? root.theme.ui.accent : "white"
                                font.family: "Symbols Nerd Font Mono"
                                font.pixelSize: 15
                            }

                            NuguText {
                                theme: root.theme
                                variant: "title"
                                text: "live"
                            }
                        }

                        MetricRow {
                            theme: root.theme
                            label: "cpu"
                            value: root.rs ? `${Math.round(root.rs.cpuUsage * 100)}%` : "—"
                            fraction: root.rs ? root.rs.cpuUsage : 0
                            barColor: root.theme ? root.theme.ui.accent : "#d86dd8"
                            ring: root.rs ? root.rs.cpuRing : []
                            sparkScaleMax: 1
                        }

                        MetricRow {
                            theme: root.theme
                            label: "mem"
                            value: root.rs ? `${Math.round(root.rs.memoryUsage * 100)}% · ${Sys.formatBytes(Math.max(0, root.rs.memTotal - root.rs.memAvailable))}` : "—"
                            fraction: root.rs ? root.rs.memoryUsage : 0
                            barColor: root.theme ? root.theme.ui.focus : "#9aabae"
                            ring: root.rs ? root.rs.memRing : []
                            sparkScaleMax: 1
                        }

                        MetricRow {
                            theme: root.theme
                            label: "swap"
                            value: {
                                if (!root.rs || root.rs.swapTotal <= 0)
                                    return "off";

                                return `${Math.round(root.rs.swapUsage * 100)}% · ${Sys.formatBytes(Math.max(0, root.rs.swapTotal - root.rs.swapFree))}`;
                            }
                            fraction: root.rs ? root.rs.swapUsage : 0
                            barColor: root.theme ? root.theme.warning : "#f0ee69"
                            showSpark: false
                            ring: []
                        }

                        MetricRow {
                            theme: root.theme
                            label: "temp"
                            value: {
                                if (!root.rs)
                                    return "n/a";

                                const t = root.rs.cpuTemperatureC;
                                const f = root.rs.fanRpm;
                                const ts = (t === null || t === undefined) ? "n/a" : `${Math.round(Number(t))}°C`;
                                const fs = (f === null || f === undefined) ? "" : ` · ${Math.round(Number(f))} rpm`;
                                return ts + fs;
                            }
                            fraction: {
                                if (!root.rs)
                                    return 0;

                                const t = root.rs.cpuTemperatureC;
                                if (t === null || t === undefined)
                                    return 0;

                                return Math.max(0, Math.min(1, Number(t) / Math.max(1, root.rs.tempHighC)));
                            }
                            barColor: root.theme ? root.theme.error : "#ec6376"
                            // sparkline hidden when the sensor is unavailable (null)
                            showSpark: root.rs && root.rs.cpuTemperatureC !== null
                            ring: root.rs ? root.rs.tempRing : []
                            sparkScaleMax: root.rs ? Math.max(1, root.rs.tempHighC) : 1
                        }

                        MetricRow {
                            theme: root.theme
                            label: "net"
                            value: root.rs ? `↓ ${Sys.formatRate(root.rs.networkRxRate)}  ↑ ${Sys.formatRate(root.rs.networkTxRate)}` : "—"
                            // net/disk have no natural ceiling: scale against the ring's own max
                            fraction: {
                                if (!root.rs)
                                    return 0;

                                const ring = root.rs.netRing || [];
                                let m = 0;
                                for (let i = 0; i < ring.length; i++)
                                    m = Math.max(m, Number(ring[i]) || 0);
                                const cur = root.rs.networkRxRate + root.rs.networkTxRate;
                                return m > 0 ? Math.min(1, cur / m) : 0;
                            }
                            // directional spark: ↑ upload (accent) above the line,
                            // ↓ download (info) below; fill-bar stays combined.
                            barColor: root.theme ? root.theme.ui.accent : "#d86dd8"
                            splitSpark: true
                            ring: root.rs ? root.rs.netTxRing : []
                            ringDown: root.rs ? root.rs.netRxRing : []
                            barColorDown: root.theme ? root.theme.info : "#3ec5e3"
                        }

                        MetricRow {
                            theme: root.theme
                            label: "disk"
                            value: root.rs ? `R ${Sys.formatRate(root.rs.diskReadRate)}  W ${Sys.formatRate(root.rs.diskWriteRate)}` : "—"
                            fraction: {
                                if (!root.rs)
                                    return 0;

                                const ring = root.rs.diskRing || [];
                                let m = 0;
                                for (let i = 0; i < ring.length; i++)
                                    m = Math.max(m, Number(ring[i]) || 0);
                                const cur = root.rs.diskReadRate + root.rs.diskWriteRate;
                                return m > 0 ? Math.min(1, cur / m) : 0;
                            }
                            // directional spark: R read (accent) above the line,
                            // W write (info) below; fill-bar stays combined.
                            barColor: root.theme ? root.theme.ui.accent : "#d86dd8"
                            splitSpark: true
                            ring: root.rs ? root.rs.diskReadRing : []
                            ringDown: root.rs ? root.rs.diskWriteRing : []
                            barColorDown: root.theme ? root.theme.info : "#3ec5e3"
                        }

                        NuguText {
                            theme: root.theme
                            variant: "meta"
                            text: root.rs ? `load ${root.rs.loadOne.toFixed(2)} · ${root.rs.loadFive.toFixed(2)} · ${root.rs.loadFifteen.toFixed(2)}  (${root.rs.cores} cores)` : "load —"
                        }
                    }
                }
            }

            // ---- compact top-processes row (chips) ----
            Flow {
                width: parent.width
                spacing: 8

                Repeater {
                    model: root.rs ? root.rs.processes : []

                    delegate: PopupBadge {
                        id: procChip

                        required property var modelData

                        theme: root.theme
                        text: `${String(modelData.name || "?")} · ${Number(modelData.cpu || 0).toFixed(0)}%`
                        role: "normal"
                    }
                }
            }

            // Current conditions and historical events have different meanings.
            Repeater {
                model: [false, true]
                delegate: Column {
                    id: issueGroup
                    required property bool modelData
                    readonly property var entries: root.issueList.filter(issue => !!issue.context === modelData)
                    width: contentColumn.width
                    spacing: 8

                    Row {
                        width: parent.width
                        spacing: 12
                        NuguText {
                            theme: root.theme
                            variant: "title"
                            text: issueGroup.modelData ? "incident history" : "needs attention"
                        }
                        NuguText {
                            theme: root.theme
                            variant: "meta"
                            color: root.theme.ui.minor
                            text: issueGroup.modelData ? issueGroup.entries.reduce((count, issue) => count + (issue.events ? issue.events.length : 1), 0) + " recorded" : issueGroup.entries.length + (issueGroup.entries.length === 1 ? " problem" : " problems")
                        }
                        PopupButton {
                            visible: issueGroup.modelData
                            theme: root.theme
                            icon: ""
                            showText: false
                            width: 24
                            height: 24
                            accessibleName: "incident coverage"
                            tooltip: "recorded crashes and oom kills · current boot · last 24 hours · latest 100\nnot every app exit produces a record"
                        }
                    }
                    NuguText {
                        visible: issueGroup.entries.length === 0 || issueGroup.modelData && root.rs && root.rs.incidentStatus !== "ready"
                        theme: root.theme
                        width: parent.width
                        wrapMode: Text.Wrap
                        variant: "meta"
                        text: issueGroup.modelData ? (root.rs && root.rs.incidentStatus === "ready" ? "no recent crashes or oom kills recorded" : "incident history unavailable or incomplete") : "disk · memory · battery · updates ok"
                    }
                    Repeater {
                        model: issueGroup.entries
                        delegate: SystemIssueRow {
                            id: issueRow
                            required property var modelData
                            theme: root.theme
                            width: issueGroup.width
                            issue: modelData
                            now: root.now
                            pending: root.rs && root.rs.agentPending && root.rs.agentIssueId === modelData.id
                            launchBusy: root.rs && root.rs.agentPending
                            launchError: root.rs && root.rs.agentIssueId === modelData.id ? root.rs.agentError : ""
                            onFocusEntered: Qt.callLater(contentScroll.revealItem, issueRow)
                            onOpenRequested: if (root.rs)
                                root.rs.runIssueAction(modelData.id)
                            onAgentRequested: if (root.rs)
                                root.rs.runIssueAgent(modelData.id)
                        }
                    }
                }
            }
        }
    }
}