From c962d07841ed56cd58c0c8a7e87516a280361530 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Fri, 10 Jul 2026 08:32:24 +0100 Subject: [PATCH] fix(power): #60 name-agnostic system-battery scan 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). --- agent/BACKLOG.md | 16 +++--------- agent/HARDWARE-QUEUE.md | 6 +++++ agent/JOURNAL.md | 13 ++++++++++ modules/home/rofi.nix | 34 +++++++++++++++++++------ modules/home/waybar.nix | 9 ++++++- modules/nixos/options.nix | 5 ++-- modules/nixos/power.nix | 7 ++++- pkgs/nomarchy-doctor/nomarchy-doctor.sh | 5 +++- pkgs/nomarchy-install/hardware-db.sh | 14 +++++++--- 9 files changed, 80 insertions(+), 29 deletions(-) diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index aa6eabb..db887bc 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -95,15 +95,6 @@ the Wi‑Fi card.” Does **not** rewrite the flake unless a later `--apply` is explicitly designed. Cost: extract script + package + man/README pointer; V0–V1. -### 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) Whole-swap bars ship `custom/powermenu` and a left logo → main menu; the 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 baked NixOS option, so it only lands on the next `sys-rebuild`. To make 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 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 @@ -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 scroll → #51; neon-glass broken waybar → **fixed** 2026-07-09 by quarantine (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) @@ -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).* _(`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; i2c README table → #48.)_ diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index 6eb308d..872cd2c 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -8,6 +8,12 @@ the failure as a NOW bug in BACKLOG.md. Machines: the **AMD dev box** QA machine), the **T14s** (webcam case). ## 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 open Ghostty, so the hand `btop.theme` assets were only guest-file asserted (main_bg / inactive_fg keys) + desktop/bar rendered for diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index ff34492..9dd4a78 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -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-notify’s 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 - **Task:** NEXT #59 — when `common-gpu-nvidia` is selected, emit commented PRIME/power/open-module guidance in installer-generated diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index 7691335..401421b 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -174,6 +174,28 @@ let # icon set (Papirus) via rofi's per-row icon protocol — see row() below. # `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() { local s="$1" out="" ch i for ((i = 0; i < ''${#s}; i++)); do @@ -264,9 +286,8 @@ let # rows drive powerprofilesctl like the power-profile picker, charge # rows drive the same theme-sync writer as batterylimit. Each half # 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 - havecharge=false; [ -e "''${bats[0]}/charge_control_end_threshold" ] && havecharge=true + havecharge=false; has_charge_threshold && havecharge=true if ! $haveppd && ! $havecharge; then notify-send "Power" "No power-profile or charge-limit control on this machine." exit 0 @@ -755,7 +776,6 @@ ${themeRows} # System submenu — connectivity and machine state. Snapshots and the # power-profile picker self-gate (BTRFS+snapper / laptop on ppd), so # the submenu shrinks to match the hardware, like the Waybar modules. - bats=(/sys/class/power_supply/BAT*) choice=$( { row "Network${menuHint "network"}" network-wireless row "VPN" network-vpn @@ -783,13 +803,12 @@ ${themeRows} # on VMs/headless via services.fwupd.enable=false), like Printers. 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 - 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 fi # Self-gated on the charge-threshold sysfs node (laptops that # expose it), like Power profile above. - [ -e "''${bats[0]}/charge_control_end_threshold" ] \ - && row "Battery limit" battery-080 + has_charge_threshold && row "Battery limit" battery-080 back } | rofi -dmenu -show-icons -markup-rows -p System) || exit 0 case "$choice" in @@ -820,8 +839,7 @@ ${themeRows} # it lands on the next sys-rebuild — the sysfs oneshot in power.nix # owns applying it (boot + AC-replug). Self-gated in System; guard # here too in case it's invoked directly. - bats=(/sys/class/power_supply/BAT*) - [ -e "''${bats[0]}/charge_control_end_threshold" ] \ + has_charge_threshold \ || { notify-send "Battery limit" "No charge-threshold control on this machine."; exit 0; } cur=$(nomarchy-theme-sync get settings.power.batteryChargeLimit 2>/dev/null) sel=$( { diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index f667834..6373c0c 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -60,7 +60,14 @@ let ''; 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 prof=$(powerprofilesctl get 2>/dev/null) || exit 0 case "$prof" in diff --git a/modules/nixos/options.nix b/modules/nixos/options.nix index 0b4ba45..fe3066f 100644 --- a/modules/nixos/options.nix +++ b/modules/nixos/options.nix @@ -106,8 +106,9 @@ 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). + where the hardware exposes charge_control_end_threshold on a + system battery (type=Battery under /sys/class/power_supply; + name-agnostic — BAT0, CMB0, …). null leaves charging at the firmware default. Backend-independent (a small systemd unit writes the sysfs knob, re-applied on AC state changes), so it works under PPD too; needs diff --git a/modules/nixos/power.nix b/modules/nixos/power.nix index 7499790..9fcd1e7 100644 --- a/modules/nixos/power.nix +++ b/modules/nixos/power.nix @@ -50,7 +50,12 @@ in RemainAfterExit = true; }; 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" done exit 0 diff --git a/pkgs/nomarchy-doctor/nomarchy-doctor.sh b/pkgs/nomarchy-doctor/nomarchy-doctor.sh index 00f42ba..4232f23 100644 --- a/pkgs/nomarchy-doctor/nomarchy-doctor.sh +++ b/pkgs/nomarchy-doctor/nomarchy-doctor.sh @@ -232,8 +232,11 @@ else fi # ── 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 -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" [ -r "$f" ] || continue bat_seen=1 diff --git a/pkgs/nomarchy-install/hardware-db.sh b/pkgs/nomarchy-install/hardware-db.sh index a09fa85..5946387 100644 --- a/pkgs/nomarchy-install/hardware-db.sh +++ b/pkgs/nomarchy-install/hardware-db.sh @@ -227,10 +227,16 @@ nomarchy_detect_hw() { esac fi - # Chassis (glob test, not compgen — nixpkgs' non-interactive bash - # is built without the completion builtins) - local bats=(/sys/class/power_supply/BAT*) - if [[ -e "${bats[0]}" ]]; then + # Chassis: any system battery (type=Battery, not scope=Device) — + # name-agnostic BAT0/CMB0/… (BACKLOG #60). Not a bare BAT* glob. + local has_bat=0 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 + has_bat=1 + break + done + if (( has_bat )); then echo "MODULE common-pc-laptop" echo "DETAIL chassis: laptop (battery present)" else