Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/power.js

Raw
// power.js — pure power/session shaping + action routing. NO I/O, no UPower.
// Dual-mode: node `require` (tests) + QML import ("power.js" as Power).
// Same pattern as audio.js / brightness.js / network.js. No `.pragma library`.
//
// The native side (Power.qml) owns UPower devices and the actual exec; this
// module owns the plain-data transforms and — crucially — actionPlan(), the
// single routing table that says HOW each session action is carried out. That
// table is pure, so the dangerous part (which argv runs for "poweroff") is
// unit-tested instead of buried in QML.

// --- Battery / device shaping ----------------------------------------------

// normalizePercent(value) — UPower percentage may arrive 0..1 or 0..100.
function normalizePercent(value) {
    var n = Number(value || 0);
    if (n > 0 && n <= 1) return Math.round(n * 100);
    return Math.round(n);
}

// minutesToLabel(seconds) — "unknown" / "45m" / "2h" / "1h 20m".
function minutesToLabel(seconds) {
    var value = Math.max(0, Math.round(Number(seconds || 0) / 60));
    if (value <= 0) return "unknown";
    var hours = Math.floor(value / 60);
    var minutes = value % 60;
    if (hours <= 0) return minutes + "m";
    if (minutes <= 0) return hours + "h";
    return hours + "h " + minutes + "m";
}

// cleanState(raw) — collapse a UPowerDeviceState.toString() value to a compact
// token: "Fully charged" -> "full", "Pending charge" -> "pendingcharge".
function cleanState(raw) {
    return String(raw == null ? "unknown" : raw)
        .toLowerCase().replace(/\s+/g, "").replace("fullycharged", "full");
}

// batteryRole(percent) — theme token by charge level.
function batteryRole(percent) {
    var p = Number(percent || 0);
    if (p <= 10) return "error";
    if (p <= 25) return "warning";
    return "ui.accent";
}

// timeEstimateLabel(state, secsToFull, secsToEmpty) — charging counts up to
// full, otherwise down to empty.
function timeEstimateLabel(state, secsToFull, secsToEmpty) {
    return cleanState(state) === "charging"
        ? minutesToLabel(secsToFull)
        : minutesToLabel(secsToEmpty);
}

function hasMainBattery(battery) {
    var b = battery || {};
    var state = cleanState(b.state);
    return Number(b.percent || 0) > 0 || state !== "unknown" || String(b.timeEstimate || "unknown") !== "unknown" || Number(b.health || -1) >= 0;
}

// isLowPower(state) — running on battery (drives global animation throttling).
function isLowPower(state) {
    var s = cleanState(state);
    return s.indexOf("discharging") >= 0 || s.indexOf("pendingdischarge") >= 0;
}

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

var SESSION_ACTIONS = ["lock", "suspend", "monitor-off", "monitor-on", "logout", "reboot", "poweroff"];
var DESTRUCTIVE_ACTIONS = ["logout", "reboot", "poweroff"];

// requiresConfirmation(action) — irreversible session actions are two-step.
// suspend is recoverable, so it fires immediately.
function requiresConfirmation(action) {
    return DESTRUCTIVE_ACTIONS.indexOf(String(action)) >= 0;
}

// actionPlan(action) — the routing table. Returns how Power.qml executes it:
//   { via: "lock" }                       -> lockService.request()
//   { via: "niri", args: [...] }          -> niriService.action(args)
//   { via: "exec", argv: [...] }          -> Quickshell.execDetached(argv)
//   { via: "none" }                       -> unknown action, do nothing
// argv/args are discrete tokens (never a shell string) — no injection surface.
function actionPlan(action) {
    switch (String(action)) {
    case "lock":        return { via: "lock" };
    case "monitor-off": return { via: "niri", args: ["power-off-monitors"] };
    case "monitor-on":  return { via: "niri", args: ["power-on-monitors"] };
    // niri owns the session; quit ends it. --skip-confirmation because the
    // popup already two-step confirms (avoids niri's own Enter prompt).
    case "logout":      return { via: "niri", args: ["quit", "--skip-confirmation"] };
    case "suspend":     return { via: "exec", argv: ["systemctl", "suspend"] };
    case "reboot":      return { via: "exec", argv: ["systemctl", "reboot"] };
    case "poweroff":    return { via: "exec", argv: ["systemctl", "poweroff"] };
    default:            return { via: "none" };
    }
}

// actionLabel(action) — short grid label.
function actionLabel(action) {
    if (action === "monitor-off") return "mon off";
    if (action === "monitor-on") return "mon on";
    if (action === "poweroff") return "power";
    return String(action);
}

if (typeof module !== 'undefined' && module.exports) {
    module.exports = {
        normalizePercent: normalizePercent,
        minutesToLabel: minutesToLabel,
        cleanState: cleanState,
        batteryRole: batteryRole,
        timeEstimateLabel: timeEstimateLabel,
        hasMainBattery: hasMainBattery,
        isLowPower: isLowPower,
        SESSION_ACTIONS: SESSION_ACTIONS,
        DESTRUCTIVE_ACTIONS: DESTRUCTIVE_ACTIONS,
        requiresConfirmation: requiresConfirmation,
        actionPlan: actionPlan,
        actionLabel: actionLabel
    };
}