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:
@@ -22,7 +22,7 @@ let
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ];
|
||||
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ];
|
||||
|
||||
config = {
|
||||
# The safe half of distro branding: distroName flows into
|
||||
|
||||
@@ -51,5 +51,58 @@
|
||||
`nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the
|
||||
root filesystem is BTRFS with a /.snapshots subvolume (the installer
|
||||
creates one)'';
|
||||
|
||||
power = {
|
||||
enable = lib.mkEnableOption ''
|
||||
active power management. By default ships power-profiles-daemon —
|
||||
the upstream-aligned power-saver/balanced/performance model,
|
||||
switchable from the menu (nomarchy-menu power-profile) and shown
|
||||
in Waybar on laptops — plus thermald and a battery charge limit
|
||||
where applicable. Harmless on desktops (the profile daemon just
|
||||
offers balanced/performance)'' // { default = true; };
|
||||
|
||||
backend = lib.mkOption {
|
||||
type = lib.types.enum [ "ppd" "tlp" ];
|
||||
default = "ppd";
|
||||
description = ''
|
||||
Which daemon governs CPU/platform power. "ppd"
|
||||
(power-profiles-daemon) is the default: a clean three-profile
|
||||
model with the menu switcher and Waybar indicator. "tlp" trades
|
||||
that for TLP's deeper, more aggressive battery tuning, at the
|
||||
cost of the profile switcher (TLP has no profile concept). The
|
||||
two manage the same knobs and are mutually exclusive.
|
||||
'';
|
||||
};
|
||||
|
||||
laptop = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Marks this machine as a laptop, gating battery-only features
|
||||
(the charge limit below). The installer sets it to true when it
|
||||
detects a battery at install time.
|
||||
'';
|
||||
};
|
||||
|
||||
thermal.enable = lib.mkEnableOption ''
|
||||
thermald, Intel's thermal-management daemon, to avoid aggressive
|
||||
throttling under sustained load. Intel-only — the installer
|
||||
enables it when it detects a GenuineIntel CPU. Harmless alongside
|
||||
either backend'';
|
||||
|
||||
batteryChargeLimit = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.ints.between 50 100);
|
||||
default = null;
|
||||
example = 80;
|
||||
description = ''
|
||||
Stop charging at this percentage to extend battery lifespan,
|
||||
where the hardware exposes a charge threshold
|
||||
(/sys/class/power_supply/BAT*/charge_control_end_threshold).
|
||||
null leaves charging at the firmware default. Backend-independent
|
||||
(a small systemd unit writes the sysfs knob at boot), so it
|
||||
works under PPD too; needs nomarchy.system.power.laptop.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
60
modules/nixos/power.nix
Normal file
60
modules/nixos/power.nix
Normal file
@@ -0,0 +1,60 @@
|
||||
# Active power management. One concern, one file: this is the system
|
||||
# side — the power daemon (power-profiles-daemon by default, or TLP),
|
||||
# thermald, and the battery charge limit. The profile *switcher* and the
|
||||
# Waybar *indicator* live home-side (rofi.nix / waybar.nix); they self-
|
||||
# gate on powerprofilesctl being present, so there's no system→home wiring
|
||||
# to keep in sync (the same way the Waybar battery widget auto-hides on
|
||||
# desktops). See the roadmap in README.md.
|
||||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy.system.power;
|
||||
ppd = cfg.backend == "ppd";
|
||||
tlp = cfg.backend == "tlp";
|
||||
chargeLimit = cfg.laptop && cfg.batteryChargeLimit != null;
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
# PPD and TLP both drive CPU/platform power — running both fights.
|
||||
# `backend` selects exactly one; this assertion only fires if a
|
||||
# downstream force-enables the other service directly.
|
||||
assertions = [{
|
||||
assertion = !(config.services.power-profiles-daemon.enable
|
||||
&& config.services.tlp.enable);
|
||||
message = ''
|
||||
nomarchy: power-profiles-daemon and TLP are mutually exclusive.
|
||||
Pick one with nomarchy.system.power.backend ("ppd" or "tlp").
|
||||
'';
|
||||
}];
|
||||
|
||||
# Unprivileged profile switching (the menu/Waybar) goes through
|
||||
# polkit, already enabled distro-wide in default.nix.
|
||||
services.power-profiles-daemon.enable = lib.mkDefault ppd;
|
||||
services.tlp.enable = lib.mkDefault tlp;
|
||||
|
||||
# thermald is Intel-only, so off unless asked (the installer enables
|
||||
# it on a GenuineIntel CPU). Sits happily next to either backend.
|
||||
services.thermald.enable = lib.mkDefault cfg.thermal.enable;
|
||||
|
||||
# Battery charge limit via sysfs. PPD can't cap charge at all, and
|
||||
# TLP's own knob only applies under TLP — so a tiny oneshot writes
|
||||
# the threshold directly, independent of the backend. Boot-time only:
|
||||
# some firmwares reset the threshold on unplug; revisit with a udev
|
||||
# hook if that bites. `-` paths that don't exist are skipped, so this
|
||||
# is a clean no-op on hardware without the control.
|
||||
systemd.services.nomarchy-battery-charge-limit = lib.mkIf chargeLimit {
|
||||
description = "Cap battery charging at ${toString cfg.batteryChargeLimit}%";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
for thresh in /sys/class/power_supply/BAT*/charge_control_end_threshold; do
|
||||
[ -w "$thresh" ] && echo ${toString cfg.batteryChargeLimit} > "$thresh"
|
||||
done
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user