feat(battery): low-battery notifications at the bar's 25/10% thresholds (item 13 slice)
All checks were successful
Check / eval (push) Successful in 3m4s
All checks were successful
Check / eval (push) Successful in 3m4s
The bar colored the battery at 25/10% but nothing notified. New shellcheck-gated watcher (pkgs/nomarchy-battery-notify, overlay) polls the same sysfs the bar reads — type=Battery, scope!=Device — and fires one toast per downward crossing (normal at 25%, critical at 10%; swaync keeps critical up until dismissed), re-armed by charging. Self-gates on battery presence (the powerprofile pattern), so desktops get a silent no-op. HM user unit behind nomarchy.batteryNotify.enable (default on) with libnotify on the unit PATH; notify-send resolves from PATH so the VM check can shim it. poweralertd rejected: its UPower thresholds (20/5) wouldn't match the bar, and it toasts every plug/unplug. Verified: V0 (flake check, bash -n); V1 (template-home builds, unit in the generation); V2 GREEN (new checks.battery-notify, test_power fake battery: self-gate, silence at 80%, one toast per crossing, critical urgency, charging re-arms). Remains: V3 — real swaync toast on a draining laptop (HARDWARE-QUEUE). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
63
flake.nix
63
flake.nix
@@ -84,6 +84,7 @@
|
||||
themesDir = ./themes;
|
||||
};
|
||||
nomarchy-doctor = final.callPackage ./pkgs/nomarchy-doctor { };
|
||||
nomarchy-battery-notify = final.callPackage ./pkgs/nomarchy-battery-notify { };
|
||||
};
|
||||
|
||||
nixosModules.nomarchy = {
|
||||
@@ -331,6 +332,68 @@
|
||||
'';
|
||||
};
|
||||
|
||||
# The low-battery watcher's crossing logic: one toast per downward
|
||||
# crossing (normal at 25%, critical at 10%), re-armed by charging.
|
||||
# test_power fakes a battery whose capacity/status we script;
|
||||
# notify-send is shimmed to a log via PATH (the package resolves
|
||||
# it from PATH for exactly this reason). What stays on-hardware:
|
||||
# the real toast rendering through swaync in a session.
|
||||
battery-notify = pkgs.testers.runNixOSTest {
|
||||
name = "nomarchy-battery-notify";
|
||||
nodes.machine = { ... }: {
|
||||
boot.kernelModules = [ "test_power" ]; # fake battery + Mains
|
||||
environment.systemPackages = [ pkgs.nomarchy-battery-notify ];
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# notify-send shim: log the calls instead of needing a session bus.
|
||||
machine.succeed(
|
||||
"mkdir -p /shim && printf '#!/bin/sh\necho \"$*\" >> /tmp/notifications\n'"
|
||||
" > /shim/notify-send && chmod +x /shim/notify-send"
|
||||
)
|
||||
|
||||
# Self-gate: with the fake battery gone, the watcher exits 0 at once.
|
||||
machine.succeed("modprobe -r test_power")
|
||||
machine.succeed("timeout 5 nomarchy-battery-notify 1")
|
||||
machine.succeed("modprobe test_power")
|
||||
|
||||
# Healthy level while discharging: silence.
|
||||
machine.succeed("echo discharging > /sys/module/test_power/parameters/battery_status")
|
||||
machine.succeed("echo 80 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.succeed(
|
||||
"systemd-run --unit=batwatch --setenv=PATH=/shim:/run/current-system/sw/bin"
|
||||
" /run/current-system/sw/bin/nomarchy-battery-notify 1"
|
||||
)
|
||||
machine.sleep(3)
|
||||
machine.fail("test -s /tmp/notifications")
|
||||
|
||||
# Crossing 25% fires the low toast exactly once, not per poll.
|
||||
machine.succeed("echo 20 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.wait_until_succeeds("grep -q 'Battery low' /tmp/notifications")
|
||||
machine.succeed("echo 15 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.sleep(3)
|
||||
assert machine.succeed("grep -c 'Battery low' /tmp/notifications").strip() == "1"
|
||||
|
||||
# Crossing 10% escalates to a critical-urgency toast, once.
|
||||
machine.succeed("echo 5 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.wait_until_succeeds("grep -q 'Battery critical' /tmp/notifications")
|
||||
assert "critical" in machine.succeed("grep 'Battery critical' /tmp/notifications")
|
||||
machine.sleep(3)
|
||||
assert machine.succeed("grep -c 'Battery critical' /tmp/notifications").strip() == "1"
|
||||
|
||||
# Back on the charger → re-armed: the next drain notifies again.
|
||||
machine.succeed("echo charging > /sys/module/test_power/parameters/battery_status")
|
||||
machine.succeed("echo 80 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.sleep(3)
|
||||
machine.succeed("echo discharging > /sys/module/test_power/parameters/battery_status")
|
||||
machine.succeed("echo 20 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.wait_until_succeeds(
|
||||
"test \"$(grep -c 'Battery low' /tmp/notifications)\" = 2"
|
||||
)
|
||||
'';
|
||||
};
|
||||
|
||||
# Memory-pressure protection (oom.nix): earlyoom must kill a
|
||||
# runaway allocator BEFORE the kernel thrash point — and must not
|
||||
# take anything else with it. A bystander unit survives the kill,
|
||||
|
||||
Reference in New Issue
Block a user