Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/Power.qml

Raw
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import "power.js" as Power

// Power service: battery/device status from UPower (native) + session actions.
// Pure shaping and the action routing table live in power.js; this file owns
// only what must touch native objects: reading UPower devices and executing
// the chosen action (lock / niri / systemctl). DI per shell.qml — no singletons
// declared here.
Item {
    id: root

    property Lock lockService
    property Niri niriService
    property string pendingConfirmation: ""
    property var lastAction: ({})

    // --- Battery / devices (native UPower) ---------------------------------

    function nativeBattery() {
        const device = UPower.displayDevice;
        if (!device || !device.ready)
            return {
                "id": "display",
                "percent": 0,
                "state": "unknown",
                "timeEstimate": "unknown",
                "health": -1
            };

        const state = Power.cleanState(UPowerDeviceState.toString(device.state || UPowerDeviceState.Unknown));
        return {
            "id": String(device.nativePath || "display"),
            "percent": Power.normalizePercent(device.percentage),
            "state": state,
            "timeEstimate": Power.timeEstimateLabel(state, device.timeToFull, device.timeToEmpty),
            "health": device.healthSupported ? Math.round(Number(device.healthPercentage || 0)) : -1
        };
    }

    function batteryObject() {
        const b = nativeBattery();
        b.present = Power.hasMainBattery(b);
        b.role = Power.batteryRole(b.percent);
        return b;
    }

    function externalDevices() {
        const rows = [];
        const devices = UPower.devices.values || [];
        for (let i = 0; i < devices.length; i++) {
            const device = devices[i];
            if (!device || device.isLaptopBattery || !device.isPresent)
                continue;

            rows.push({
                "id": String(device.nativePath || device.model || `device-${i}`),
                "name": String(device.model || UPowerDeviceType.toString(device.type)),
                "type": String(UPowerDeviceType.toString(device.type)).toLowerCase(),
                "percent": Power.normalizePercent(device.percentage),
                "state": Power.cleanState(UPowerDeviceState.toString(device.state || UPowerDeviceState.Unknown))
            });
        }
        return rows;
    }

    // lowPowerMode() drives global animation throttling (shell.qml binds
    // nuguTheme.lowPowerMode to it). Keep it lean — it is read every frame.
    function lowPowerMode() {
        return Power.isLowPower(nativeBattery().state);
    }

    // --- State (IPC contract) ----------------------------------------------

    function stateObject() {
        const battery = batteryObject();
        return {
            "battery": battery,
            "externalDevices": externalDevices(),
            "sessionActions": Power.SESSION_ACTIONS,
            "destructiveActions": Power.DESTRUCTIVE_ACTIONS,
            "compact": {
                "label": "Power",
                "role": battery.role,
                "charging": battery.state === "charging"
            },
            "pendingConfirmation": pendingConfirmation,
            "lastAction": lastAction
        };
    }

    function stateJson() {
        return JSON.stringify(stateObject());
    }

    // --- Session actions ----------------------------------------------------

    // Dispatch a resolved action plan (from power.js) to the native sink.
    function runPlan(plan) {
        if (plan.via === "lock" && lockService)
            lockService.request();
        else if (plan.via === "niri" && niriService)
            niriService.action(plan.args);
        else if (plan.via === "exec")
            Quickshell.execDetached(plan.argv);
    }

    function sessionAction(kind) {
        const action = String(kind);
        if (Power.requiresConfirmation(action)) {
            pendingConfirmation = action;
            lastAction = {
                "kind": action,
                "confirmed": false
            };
        } else {
            pendingConfirmation = "";
            runPlan(Power.actionPlan(action));
            lastAction = {
                "kind": action,
                "confirmed": true
            };
        }
        return stateJson();
    }

    function confirmAction(kind) {
        const action = String(kind);
        const confirmed = pendingConfirmation === action && Power.requiresConfirmation(action);
        pendingConfirmation = "";
        if (confirmed)
            runPlan(Power.actionPlan(action));

        lastAction = {
            "kind": action,
            "confirmed": confirmed
        };
        return stateJson();
    }

    function cancelAction() {
        pendingConfirmation = "";
        lastAction = {
            "kind": "cancel"
        };
        return stateJson();
    }

    // selfTest() — exercises the pure routing/shaping without touching native
    // state or executing anything. Gate-checks power.js via IPC.
    function selfTest() {
        const cases = [];
        function check(name, got, want) {
            cases.push({
                "name": name,
                "pass": JSON.stringify(got) === JSON.stringify(want),
                "got": got
            });
        }

        check("poweroff routes to systemctl", Power.actionPlan("poweroff"), {
            "via": "exec",
            "argv": ["systemctl", "poweroff"]
        });
        check("logout routes to niri quit", Power.actionPlan("logout"), {
            "via": "niri",
            "args": ["quit", "--skip-confirmation"]
        });
        check("lock routes to lockService", Power.actionPlan("lock"), {
            "via": "lock"
        });
        check("unknown is no-op", Power.actionPlan("bogus"), {
            "via": "none"
        });
        check("poweroff confirms", Power.requiresConfirmation("poweroff"), true);
        check("suspend immediate", Power.requiresConfirmation("suspend"), false);
        check("batteryRole critical", Power.batteryRole(8), "error");
        check("normalizePercent fraction", Power.normalizePercent(0.5), 50);
        check("low power on discharge", Power.isLowPower("Discharging"), true);

        const failed = cases.filter(c => !c.pass);
        return JSON.stringify({
            "ok": failed.length === 0,
            "total": cases.length,
            "failed": failed
        });
    }
}