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