fix(power): #60 name-agnostic system-battery scan
All checks were successful
Check / eval (push) Successful in 2m53s

Replace BAT* globs with type=Battery + !scope=Device (same filter as
nomarchy-battery-notify) in charge-limit oneshot, menu gates, Waybar
power-profile self-gate, doctor, and install chassis detection. CMB0-
style names get charge-cap and laptop modules consistently.

V3 pending: confirm on non-BAT* hardware (HARDWARE-QUEUE). Close #60.

Verified: V1 (option-docs, hardware-db-modules, battery-charge-limit VM).
This commit is contained in:
Bernardo Magri
2026-07-10 08:32:24 +01:00
parent 09388196a9
commit c962d07841
9 changed files with 80 additions and 29 deletions

View File

@@ -95,15 +95,6 @@ the WiFi card.” Does **not** rewrite the flake unless a later `--apply`
is explicitly designed. Cost: extract script + package + man/README is explicitly designed. Cost: extract script + package + man/README
pointer; V0V1. pointer; V0V1.
### 60. Battery / powerprofile: name-agnostic battery scan
`power.nix` oneshot and menu/waybar globs use
`/sys/class/power_supply/BAT*`. `nomarchy-battery-notify` correctly
treats any system battery (`type=Battery`, not `scope=Device`). Machines
with `CMB0` (and similar) get notify but not charge-cap / inconsistent
gating. Fix: name-agnostic scan aligned with battery-notify. Cost:
small multi-file; `[blocked:hw]` only to confirm on non-BAT* hardware —
logic still ships at V1.
### 63. Generated Waybar: identity-bar affordances (powermenu + logo) ### 63. Generated Waybar: identity-bar affordances (powermenu + logo)
Whole-swap bars ship `custom/powermenu` and a left logo → main menu; the Whole-swap bars ship `custom/powermenu` and a left logo → main menu; the
generated `waybar.nix` bar has neither. Optional parity add for generated `waybar.nix` bar has neither. Optional parity add for
@@ -151,7 +142,7 @@ Still PROPOSED for later triage: first-boot tips (partially shipped via
Custom…) writing `settings.power.batteryChargeLimit` — but that's a Custom…) writing `settings.power.batteryChargeLimit` — but that's a
baked NixOS option, so it only lands on the next `sys-rebuild`. To make baked NixOS option, so it only lands on the next `sys-rebuild`. To make
it instant (no rebuild): add a udev rule making it instant (no rebuild): add a udev rule making
`/sys/class/power_supply/BAT*/charge_control_end_threshold` system-battery `charge_control_end_threshold` nodes
group-writable (`MODE`/`GROUP`) so the menu can `echo N >` it live group-writable (`MODE`/`GROUP`) so the menu can `echo N >` it live
alongside persisting state (the sysfs oneshot in power.nix still owns alongside persisting state (the sysfs oneshot in power.nix still owns
boot + AC-replug re-apply), and/or surface a rofi-menu quick row or a boot + AC-replug re-apply), and/or surface a rofi-menu quick row or a
@@ -181,7 +172,8 @@ Still PROPOSED for later triage: first-boot tips (partially shipped via
_(Waybar doctor indicator, Control Center appearance no-ops, summer-day _(Waybar doctor indicator, Control Center appearance no-ops, summer-day
scroll → #51; neon-glass broken waybar → **fixed** 2026-07-09 by quarantine scroll → #51; neon-glass broken waybar → **fixed** 2026-07-09 by quarantine
(removed `waybar.css` → generated bar; render-confirmed).)_ (removed `waybar.css` → generated bar; render-confirmed).)_
_(Battery charge-limit / powerprofile BAT*-only → **#60**.)_ _(Battery charge-limit / powerprofile BAT*-only → **#60** ✓ 2026-07-10;
V3 pending on non-BAT* hardware.)_
### Docs / option-surface drift (codebase exam 2026-07-09) ### Docs / option-surface drift (codebase exam 2026-07-09)
@@ -456,7 +448,7 @@ NVIDIA depth) is still mostly CLI. Items below are product work on top
of that doc — design notes live there (§4§10).* of that doc — design notes live there (§4§10).*
_(`nomarchy-detect-hw` CLI → **#58**; Fingerprint menu → **#55**; _(`nomarchy-detect-hw` CLI → **#58**; Fingerprint menu → **#55**;
Installer NVIDIA commented guidance → **#59**.)_ Installer NVIDIA commented guidance → **#59** ✓ 2026-07-10.)_
_(hardware-db ∈ nixos-hardware → #49; usbutils on install PATH → #50; _(hardware-db ∈ nixos-hardware → #49; usbutils on install PATH → #50;
i2c README table → #48.)_ i2c README table → #48.)_

View File

@@ -8,6 +8,12 @@ the failure as a NOW bug in BACKLOG.md. Machines: the **AMD dev box**
QA machine), the **T14s** (webcam case). QA machine), the **T14s** (webcam case).
## Any machine (dev box is fine) ## Any machine (dev box is fine)
- [ ] **#60 non-BAT* battery name (if available)** — on a machine whose
system battery is **not** named `BAT*` (e.g. `CMB0`): confirm
charge-limit oneshot writes the threshold, System Battery limit
and Power profile rows appear, Waybar power-profile module shows,
doctor charge-limit section runs. On `BAT0`-only machines this is
a no-op (already covered by existing charge-limit re-apply item).
- [ ] **btop theme fidelity (#52 residual)** — softGL theme-shot cannot - [ ] **btop theme fidelity (#52 residual)** — softGL theme-shot cannot
open Ghostty, so the hand `btop.theme` assets were only guest-file open Ghostty, so the hand `btop.theme` assets were only guest-file
asserted (main_bg / inactive_fg keys) + desktop/bar rendered for asserted (main_bg / inactive_fg keys) + desktop/bar rendered for

View File

@@ -17,6 +17,19 @@ Template:
--- ---
## 2026-07-10 — #60 name-agnostic system-battery scan
- **Task:** NEXT #60 — align charge-limit / menu / waybar / doctor /
installer chassis with battery-notifys type=Battery + !scope=Device
filter (BAT* globs miss CMB0).
- **Did:** power.nix oneshot, rofi helpers (`has_system_battery` /
`has_charge_threshold`), waybar powerprofile gate, doctor loop,
hardware-db chassis, options.nix description. HARDWARE-QUEUE entry
for non-BAT* confirm. Closed #60.
- **Verified:** **V1**`checks.option-docs`, `hardware-db-modules`,
`battery-charge-limit` (VM) green; no remaining `BAT*` globs in code
paths. **V3 pending:** non-BAT* name on real hardware (queue entry).
- **Next suggestion:** #63 generated Waybar powermenu + logo.
## 2026-07-10 — #59 NVIDIA commented system.nix guidance ## 2026-07-10 — #59 NVIDIA commented system.nix guidance
- **Task:** NEXT #59 — when `common-gpu-nvidia` is selected, emit - **Task:** NEXT #59 — when `common-gpu-nvidia` is selected, emit
commented PRIME/power/open-module guidance in installer-generated commented PRIME/power/open-module guidance in installer-generated

View File

@@ -174,6 +174,28 @@ let
# icon set (Papirus) via rofi's per-row icon protocol see row() below. # icon set (Papirus) via rofi's per-row icon protocol see row() below.
# `nomarchy-menu` with no argument shows the module picker. # `nomarchy-menu` with no argument shows the module picker.
# System batteries: type=Battery, not scope=Device same filter as
# nomarchy-battery-notify. Name-agnostic (BAT0, CMB0, test_battery)
# so charge-limit / power-profile gates match notify (BACKLOG #60).
has_system_battery() {
local d
for d in /sys/class/power_supply/*/; do
[ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
[ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
return 0
done
return 1
}
has_charge_threshold() {
local d
for d in /sys/class/power_supply/*/; do
[ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
[ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
[ -e "$d/charge_control_end_threshold" ] && return 0
done
return 1
}
urlencode() { urlencode() {
local s="$1" out="" ch i local s="$1" out="" ch i
for ((i = 0; i < ''${#s}; i++)); do for ((i = 0; i < ''${#s}; i++)); do
@@ -264,9 +286,8 @@ let
# rows drive powerprofilesctl like the power-profile picker, charge # rows drive powerprofilesctl like the power-profile picker, charge
# rows drive the same theme-sync writer as batterylimit. Each half # rows drive the same theme-sync writer as batterylimit. Each half
# self-gates on its hardware, so a desktop with neither just toasts. # self-gates on its hardware, so a desktop with neither just toasts.
bats=(/sys/class/power_supply/BAT*)
haveppd=false; command -v powerprofilesctl >/dev/null 2>&1 && haveppd=true haveppd=false; command -v powerprofilesctl >/dev/null 2>&1 && haveppd=true
havecharge=false; [ -e "''${bats[0]}/charge_control_end_threshold" ] && havecharge=true havecharge=false; has_charge_threshold && havecharge=true
if ! $haveppd && ! $havecharge; then if ! $haveppd && ! $havecharge; then
notify-send "Power" "No power-profile or charge-limit control on this machine." notify-send "Power" "No power-profile or charge-limit control on this machine."
exit 0 exit 0
@@ -755,7 +776,6 @@ ${themeRows}
# System submenu connectivity and machine state. Snapshots and the # System submenu connectivity and machine state. Snapshots and the
# power-profile picker self-gate (BTRFS+snapper / laptop on ppd), so # power-profile picker self-gate (BTRFS+snapper / laptop on ppd), so
# the submenu shrinks to match the hardware, like the Waybar modules. # the submenu shrinks to match the hardware, like the Waybar modules.
bats=(/sys/class/power_supply/BAT*)
choice=$( { choice=$( {
row "Network${menuHint "network"}" network-wireless row "Network${menuHint "network"}" network-wireless
row "VPN" network-vpn row "VPN" network-vpn
@@ -783,13 +803,12 @@ ${themeRows}
# on VMs/headless via services.fwupd.enable=false), like Printers. # on VMs/headless via services.fwupd.enable=false), like Printers.
command -v fwupdmgr >/dev/null 2>&1 && row "Firmware" firmware-manager command -v fwupdmgr >/dev/null 2>&1 && row "Firmware" firmware-manager
command -v nomarchy-control-center >/dev/null 2>&1 && row "Control Center" preferences-desktop command -v nomarchy-control-center >/dev/null 2>&1 && row "Control Center" preferences-desktop
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then if has_system_battery && command -v powerprofilesctl >/dev/null 2>&1; then
row "Power profile" preferences-system-power row "Power profile" preferences-system-power
fi fi
# Self-gated on the charge-threshold sysfs node (laptops that # Self-gated on the charge-threshold sysfs node (laptops that
# expose it), like Power profile above. # expose it), like Power profile above.
[ -e "''${bats[0]}/charge_control_end_threshold" ] \ has_charge_threshold && row "Battery limit" battery-080
&& row "Battery limit" battery-080
back back
} | rofi -dmenu -show-icons -markup-rows -p System) || exit 0 } | rofi -dmenu -show-icons -markup-rows -p System) || exit 0
case "$choice" in case "$choice" in
@@ -820,8 +839,7 @@ ${themeRows}
# it lands on the next sys-rebuild the sysfs oneshot in power.nix # it lands on the next sys-rebuild the sysfs oneshot in power.nix
# owns applying it (boot + AC-replug). Self-gated in System; guard # owns applying it (boot + AC-replug). Self-gated in System; guard
# here too in case it's invoked directly. # here too in case it's invoked directly.
bats=(/sys/class/power_supply/BAT*) has_charge_threshold \
[ -e "''${bats[0]}/charge_control_end_threshold" ] \
|| { notify-send "Battery limit" "No charge-threshold control on this machine."; exit 0; } || { notify-send "Battery limit" "No charge-threshold control on this machine."; exit 0; }
cur=$(nomarchy-theme-sync get settings.power.batteryChargeLimit 2>/dev/null) cur=$(nomarchy-theme-sync get settings.power.batteryChargeLimit 2>/dev/null)
sel=$( { sel=$( {

View File

@@ -60,7 +60,14 @@ let
''; '';
powerProfileStatus = pkgs.writeShellScriptBin "nomarchy-powerprofile-status" '' powerProfileStatus = pkgs.writeShellScriptBin "nomarchy-powerprofile-status" ''
case "$(ls /sys/class/power_supply/ 2>/dev/null)" in *BAT*) ;; *) exit 0 ;; esac # Name-agnostic system battery (BAT0, CMB0, ) BACKLOG #60.
has_bat=
for d in /sys/class/power_supply/*/; do
[ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
[ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
has_bat=1; break
done
[ -n "$has_bat" ] || exit 0
command -v powerprofilesctl >/dev/null 2>&1 || exit 0 command -v powerprofilesctl >/dev/null 2>&1 || exit 0
prof=$(powerprofilesctl get 2>/dev/null) || exit 0 prof=$(powerprofilesctl get 2>/dev/null) || exit 0
case "$prof" in case "$prof" in

View File

@@ -106,8 +106,9 @@
example = 80; example = 80;
description = '' description = ''
Stop charging at this percentage to extend battery lifespan, Stop charging at this percentage to extend battery lifespan,
where the hardware exposes a charge threshold where the hardware exposes charge_control_end_threshold on a
(/sys/class/power_supply/BAT*/charge_control_end_threshold). system battery (type=Battery under /sys/class/power_supply;
name-agnostic BAT0, CMB0, ).
null leaves charging at the firmware default. Backend-independent null leaves charging at the firmware default. Backend-independent
(a small systemd unit writes the sysfs knob, re-applied on AC (a small systemd unit writes the sysfs knob, re-applied on AC
state changes), so it works under PPD too; needs state changes), so it works under PPD too; needs

View File

@@ -50,7 +50,12 @@ in
RemainAfterExit = true; RemainAfterExit = true;
}; };
script = '' script = ''
for thresh in /sys/class/power_supply/BAT*/charge_control_end_threshold; do # Name-agnostic (BAT0, CMB0, ): same type/scope filter as
# nomarchy-battery-notify BACKLOG #60.
for d in /sys/class/power_supply/*/; do
[ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
[ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
thresh="$d/charge_control_end_threshold"
[ -w "$thresh" ] && echo ${toString cfg.batteryChargeLimit} > "$thresh" [ -w "$thresh" ] && echo ${toString cfg.batteryChargeLimit} > "$thresh"
done done
exit 0 exit 0

View File

@@ -232,8 +232,11 @@ else
fi fi
# ── laptop battery charge threshold (only when a limit is set) ─────── # ── laptop battery charge threshold (only when a limit is set) ───────
# Name-agnostic (BAT0, CMB0, …) — same type/scope filter as notify (#60).
bat_seen=0; bat_limited=0 bat_seen=0; bat_limited=0
for bat in /sys/class/power_supply/BAT*; do for bat in /sys/class/power_supply/*/; do
[ "$(cat "$bat/type" 2>/dev/null)" = Battery ] || continue
[ "$(cat "$bat/scope" 2>/dev/null || echo System)" = Device ] && continue
f="$bat/charge_control_end_threshold" f="$bat/charge_control_end_threshold"
[ -r "$f" ] || continue [ -r "$f" ] || continue
bat_seen=1 bat_seen=1

View File

@@ -227,10 +227,16 @@ nomarchy_detect_hw() {
esac esac
fi fi
# Chassis (glob test, not compgen — nixpkgs' non-interactive bash # Chassis: any system battery (type=Battery, not scope=Device) —
# is built without the completion builtins) # name-agnostic BAT0/CMB0/… (BACKLOG #60). Not a bare BAT* glob.
local bats=(/sys/class/power_supply/BAT*) local has_bat=0 d
if [[ -e "${bats[0]}" ]]; then for d in /sys/class/power_supply/*/; do
[[ "$(cat "$d/type" 2>/dev/null)" == Battery ]] || continue
[[ "$(cat "$d/scope" 2>/dev/null || echo System)" == Device ]] && continue
has_bat=1
break
done
if (( has_bat )); then
echo "MODULE common-pc-laptop" echo "MODULE common-pc-laptop"
echo "DETAIL chassis: laptop (battery present)" echo "DETAIL chassis: laptop (battery present)"
else else