From 034da701a3259d4eebf8375ce4dc52a0c10ae7b0 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Sun, 26 Apr 2026 08:31:19 +0100 Subject: [PATCH] feat(system): add laptop power preset module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `nomarchy.system.laptop.{enable,thermald}` options. `enable` defaults to `formFactor == "laptop"`, so the installer's existing formFactor write auto-flips the preset on without installer changes. The module wires TLP (governors + 75/80 charge thresholds), force-disables power-profiles-daemon (mutually exclusive with TLP), enables upower and thermald (x86_64), adds the brightnessctl udev rule so the existing brightness scripts work without root, and sets a logind lid-switch policy that resolves to suspend-then-hibernate when `hibernation.enable` is on, plain suspend otherwise. Closes the "Form-factor → laptop preset auto-enable" Now item and the "Laptop preset module" Next item from docs/ROADMAP.md in one change. --- core/system/default.nix | 1 + core/system/laptop.nix | 42 +++++++++++++++++++++++++++++++++++++++++ core/system/options.nix | 28 ++++++++++++++++++++++++++- docs/OPTIONS.md | 12 ++++++++++-- docs/ROADMAP.md | 4 ++-- 5 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 core/system/laptop.nix diff --git a/core/system/default.nix b/core/system/default.nix index 3d11d3d..25f0427 100644 --- a/core/system/default.nix +++ b/core/system/default.nix @@ -19,6 +19,7 @@ ./browser.nix # Tier 1 system features (all opt-in via nomarchy.system.*). ./snapper.nix + ./laptop.nix ./hibernate.nix ./containers.nix ./pam.nix diff --git a/core/system/laptop.nix b/core/system/laptop.nix new file mode 100644 index 0000000..54b8e3a --- /dev/null +++ b/core/system/laptop.nix @@ -0,0 +1,42 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.nomarchy.system.laptop; + hib = config.nomarchy.system.hibernation; + lidAction = if hib.enable then "suspend-then-hibernate" else "suspend"; +in +{ + config = lib.mkIf cfg.enable { + services.tlp = { + enable = lib.mkDefault true; + settings = { + CPU_SCALING_GOVERNOR_ON_AC = lib.mkDefault "performance"; + CPU_SCALING_GOVERNOR_ON_BAT = lib.mkDefault "powersave"; + CPU_BOOST_ON_BAT = lib.mkDefault 0; + PLATFORM_PROFILE_ON_AC = lib.mkDefault "balanced"; + PLATFORM_PROFILE_ON_BAT = lib.mkDefault "low-power"; + # Charge thresholds only honored on supported hardware (ThinkPad, + # some ASUS); a harmless warning is logged elsewhere. + START_CHARGE_THRESH_BAT0 = lib.mkDefault 75; + STOP_CHARGE_THRESH_BAT0 = lib.mkDefault 80; + }; + }; + + # TLP and power-profiles-daemon both arbitrate CPU/EPP — NixOS asserts + # mutual exclusion. Opt out of the preset entirely to use PPD instead. + services.power-profiles-daemon.enable = lib.mkForce false; + + services.upower.enable = lib.mkDefault true; + services.thermald.enable = lib.mkDefault cfg.thermald; + + # Backlight write access for the `video` group, so the existing + # nomarchy-brightness-{display,keyboard} scripts run without root. + services.udev.packages = [ pkgs.brightnessctl ]; + + services.logind.settings.Login = { + HandleLidSwitch = lib.mkDefault lidAction; + HandleLidSwitchExternalPower = lib.mkDefault "suspend"; + HandleLidSwitchDocked = lib.mkDefault "ignore"; + }; + }; +} diff --git a/core/system/options.nix b/core/system/options.nix index b49def4..a6f93fa 100644 --- a/core/system/options.nix +++ b/core/system/options.nix @@ -1,4 +1,4 @@ -{ lib, ... }: +{ config, lib, pkgs, ... }: { options.nomarchy.system = { @@ -82,6 +82,32 @@ }; }; + laptop = { + enable = lib.mkOption { + type = lib.types.bool; + default = config.nomarchy.system.formFactor == "laptop"; + defaultText = lib.literalExpression ''config.nomarchy.system.formFactor == "laptop"''; + description = '' + Laptop power preset: TLP (with sane AC/battery governors), + `services.upower`, `services.thermald` (x86_64), a brightnessctl + udev rule, and a logind lid-switch policy. Force-disables + `services.power-profiles-daemon` (mutually exclusive with TLP). + Lid-close defers to `nomarchy.system.hibernation.enable`: + suspend-then-hibernate when on, suspend otherwise. Defaults on + when `formFactor = "laptop"`. + ''; + }; + thermald = lib.mkOption { + type = lib.types.bool; + default = pkgs.stdenv.hostPlatform.isx86_64; + defaultText = lib.literalExpression "pkgs.stdenv.hostPlatform.isx86_64"; + description = '' + Enable `services.thermald` (Intel thermal daemon). Default true on + x86_64. Harmless no-op on AMD; gated off on aarch64. + ''; + }; + }; + containers = { enable = lib.mkEnableOption '' Rootless Podman with Docker compatibility (`docker` → `podman`), diff --git a/docs/OPTIONS.md b/docs/OPTIONS.md index 12e1316..93b933a 100644 --- a/docs/OPTIONS.md +++ b/docs/OPTIONS.md @@ -35,9 +35,9 @@ Defined in `core/system/options.nix`; wired in `core/system/network.nix`. ### `nomarchy.system.formFactor` -`enum [ "laptop" "desktop" ]`, default `"laptop"`. Drives UI affordances and (eventually) lid handling / TLP. The installer auto-detects via `/sys/class/power_supply/BAT*`. The default is `"laptop"` because the battery widget renders empty when no battery is present — safe on a desktop, useful on a laptop. +`enum [ "laptop" "desktop" ]`, default `"laptop"`. Drives UI affordances and the laptop power preset. The installer auto-detects via `/sys/class/power_supply/BAT*`. The default is `"laptop"` because the battery widget renders empty when no battery is present — safe on a desktop, useful on a laptop. -Wired in `features/desktop/waybar/default.nix` (filters the battery widget out on desktop) and `features/scripts/battery-monitor.nix` (skips the timer on desktop). +Wired in `features/desktop/waybar/default.nix` (filters the battery widget out on desktop), `features/scripts/battery-monitor.nix` (skips the timer on desktop), and `nomarchy.system.laptop.enable` (defaults true when this is `"laptop"`). ### `nomarchy.system.theme` @@ -67,6 +67,14 @@ Wired in `features/desktop/waybar/default.nix` (filters the battery widget out o `int`, default `30`. Idle minutes before suspend-then-hibernate fires. +### `nomarchy.system.laptop.enable` + +`bool`, default `nomarchy.system.formFactor == "laptop"`. Laptop power preset: TLP (with sane AC/battery governors and ThinkPad-style 75/80 charge thresholds), `services.upower`, `services.thermald` (gated by `laptop.thermald`), and a brightnessctl udev rule so the existing `nomarchy-brightness-{display,keyboard}` scripts run without root. Force-disables `services.power-profiles-daemon` (mutually exclusive with TLP) — to use PPD instead, set `laptop.enable = false` and wire it yourself. Lid-close action defers to `nomarchy.system.hibernation.enable`: `suspend-then-hibernate` when on, `suspend` otherwise. Charge thresholds are only honored on supported hardware (ThinkPad, some ASUS); harmless warning elsewhere. + +### `nomarchy.system.laptop.thermald` + +`bool`, default `true` on x86_64. Enables `services.thermald` (Intel thermal daemon). Harmless no-op on AMD; gated off on aarch64. + ### `nomarchy.system.containers.enable` `bool`, default `false`. Rootless Podman with Docker compatibility (`docker` → `podman`), plus `podman-compose`, `podman-tui`, and `dive`. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index cb721f9..b7769f3 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -19,11 +19,10 @@ Guardrails (apply when adding anything): ### Now (ready to pick up) -- **Form-factor → laptop preset auto-enable.** When the installer writes `nomarchy.system.formFactor = "laptop"`, also flip on a yet-to-be-built laptop preset (TLP, brightness keys, lid handling). The option exists; the preset module doesn't. +- (empty — pick the top of **Next**.) ### Next (bigger lifts that build on Now) -- **Laptop preset module** (`core/system/laptop.nix`). Gated on `formFactor = "laptop"`. Wires TLP defaults, `services.upower`, `services.thermald` where applicable, brightness key handlers, and a sane logind lid-switch policy that defers to `nomarchy.system.hibernation.enable`. - **Desktop preset module.** CPU governor `performance` by default, no battery widget (already done), ZFS-friendly defaults for users who later add a pool. - **Accessibility preset.** Larger cursor, slower key-repeat defaults, `services.orca`, screen reader keybinding, high-contrast theme variant. - **Gaming preset.** `programs.steam.enable`, `programs.gamemode.enable`, `services.flatpak.enable` with a curated remote, and a Hyprland window-rule to fullscreen Steam-launched apps. @@ -126,6 +125,7 @@ Each PR description should reference the row(s) in `docs/SCRIPTS.md` it closes, (Move items here when they land — keep them brief, link the commit/PR.) +- _2026-04-26_ — Laptop preset module (`core/system/laptop.nix`). New `nomarchy.system.laptop.{enable,thermald}` options; `enable` defaults to `formFactor == "laptop"` so the installer's existing `formFactor` write auto-flips it on. Wires TLP (governors + 75/80 charge thresholds), force-disables `power-profiles-daemon`, enables `upower` and `thermald` (x86_64), adds the brightnessctl udev rule for backlight without root, and sets a logind lid-switch policy that defers to `hibernation.enable`. Closes both the Now item and the largest Next item. - _2026-04-25_ — Software-profile multi-select in the installer. Users can now pick Dev, Gaming, Office, Media, and CLI Utils profiles during install; logic emits corresponding `home.packages` and system toggles into the generated config. - _2026-04-25_ — Pillar 3 Phase B: script & menu audit. Ported/implemented/stubbed ~40 scripts including `nomarchy-version`, `nomarchy-debug`, `nomarchy-reinstall`, `nomarchy-rollback`, `nomarchy-update-firmware`, `nomarchy-pkg-*`, and `nomarchy-theme-*` wrappers. Moved desktop scripts to packaged utility directory. - _2026-04-25_ — Docker & fwupd support. Added `nomarchy.system.virtualization.docker.enable` and `nomarchy.hardware.fwupd` options. Wires system services and adds `docker-compose` and `fwupdmgr` to PATH.