feat(power): laptop power management (power-profiles-daemon + TLP)

Adds a `nomarchy.system.power.*` surface (modules/nixos/power.nix),
replacing the bare `services.upower` (Waybar reporting only) baseline:

- power-profiles-daemon ships by default; switched through polkit (no
  root prompt). TLP is the opt-in `backend = "tlp"` for deeper battery
  tuning — the two are mutually exclusive (asserted).
- thermald behind `power.thermal.enable` (Intel-only).
- `power.batteryChargeLimit` (e.g. 80): a backend-independent sysfs
  oneshot writes charge_control_end_threshold; gated on `power.laptop`.

Switcher + indicator live home-side and self-gate on a battery being
present + powerprofilesctl running, so they hide on desktops and under
TLP — no system->home wiring (like the Waybar battery widget):

- rofi.nix: `nomarchy-menu power-profile` module + a picker entry shown
  only on laptops with PPD.
- waybar.nix: first `custom/*` module — click cycles the profile.

Installer writes `power.laptop = true` (battery probe) and
`power.thermal.enable` (GenuineIntel) into the generated system.nix, and
scaffolds the charge limit commented-out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 17:35:42 +01:00
parent 8853f6ae49
commit 00f5311499
7 changed files with 233 additions and 37 deletions

View File

@@ -7,11 +7,39 @@
# <themesDir>/<slug>/waybar.css (and optionally waybar.jsonc) which
# replace the generated style/layout entirely. Probed at eval time —
# pure, it's flake source.
{ config, lib, ... }:
{ config, lib, pkgs, ... }:
let
t = config.nomarchy.theme;
# Power-profile indicator (power-profiles-daemon). Both scripts self-
# gate: they exit silently unless this is a laptop (a battery is
# present) running PPD, so the module hides itself on desktops and
# under the TLP backend — no system→home coupling. Speedometer glyphs
# (slow/medium/fast) map to power-saver/balanced/performance.
powerProfileStatus = pkgs.writeShellScript "nomarchy-powerprofile-status" ''
case "$(ls /sys/class/power_supply/ 2>/dev/null)" in *BAT*) ;; *) exit 0 ;; esac
command -v powerprofilesctl >/dev/null 2>&1 || exit 0
prof=$(powerprofilesctl get 2>/dev/null) || exit 0
case "$prof" in
power-saver) icon="󰾆" ;;
performance) icon="󰓅" ;;
*) icon="󰾅" ;;
esac
printf '{"text":"%s","tooltip":"Power profile: %s","class":"%s"}\n' "$icon" "$prof" "$prof"
'';
# Cycle to the next profile this machine actually offers (wraps around).
powerProfileCycle = pkgs.writeShellScript "nomarchy-powerprofile-cycle" ''
command -v powerprofilesctl >/dev/null 2>&1 || exit 0
cur=$(powerprofilesctl get 2>/dev/null) || exit 0
avail=$(powerprofilesctl list 2>/dev/null | sed -nE 's/^[* ] ([a-z-]+):$/\1/p')
next=$(printf '%s\n' "$avail" | awk -v cur="$cur" '
{ a[NR] = $0 }
END { for (i = 1; i <= NR; i++) if (a[i] == cur) { print a[i % NR + 1]; f = 1 }
if (!f && NR) print a[1] }')
[ -n "$next" ] && powerprofilesctl set "$next"
'';
# Per-theme override probe.
assetDir = config.nomarchy.themesDir + "/${t.slug}";
styleOverride = assetDir + "/waybar.css";
@@ -38,7 +66,7 @@ let
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
modules-center = [ "clock" ];
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "battery" ];
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" "battery" ];
"hyprland/workspaces" = {
format = "{icon}";
@@ -83,6 +111,13 @@ let
format-icons = [ "󰁺" "󰁼" "󰁾" "󰂀" "󰁹" ];
};
"custom/powerprofile" = {
exec = "${powerProfileStatus}";
return-type = "json";
interval = 5;
on-click = "${powerProfileCycle}";
};
tray.spacing = 8;
};
@@ -129,7 +164,7 @@ let
font-weight: bold;
}
#tray, #pulseaudio, #network, #cpu, #memory, #battery {
#tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #battery {
color: @subtext;
padding: 0 10px;
}