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