feat(power): suspend-then-hibernate 1h on battery (#115)
All checks were successful
Check / eval (push) Successful in 6m16s

Human decision: 1h delay, battery only, Preferences enable/disable.
Wire systemd suspend-then-hibernate + HibernateDelaySec=1h +
HibernateOnACPower=false when settings.power.suspendThenHibernate is on
and boot.resumeDevice is set. Lid: s2h undocked, plain suspend on AC.
nomarchy-suspend for hypridle + Power menu (live state). LUKS unlock
before encrypted hibernate so s2h→disk is not double-password.
state-bridges assert the policy; HARDWARE-QUEUE #115 + suggested order.

V1: nix flake check --no-build. V3: bag-carry on hardware.
This commit is contained in:
2026-07-15 12:13:23 +01:00
parent e9dd3d14c5
commit 52d1581131
13 changed files with 304 additions and 51 deletions

View File

@@ -90,6 +90,7 @@
nomarchy-what-changed = final.callPackage ./pkgs/nomarchy-what-changed { };
nomarchy-detect-hw = final.callPackage ./pkgs/nomarchy-detect-hw { };
nomarchy-airplane = final.callPackage ./pkgs/nomarchy-airplane { };
nomarchy-suspend = final.callPackage ./pkgs/nomarchy-suspend { };
nomarchy-gen-prune = final.callPackage ./pkgs/nomarchy-gen-prune { };
# pull / rebuild / home (+ legacy aliases) — also on HM so a home
# switch can refresh a broken system-package nomarchy-pull.
@@ -171,6 +172,11 @@
settings.bluetooth.enable = false; # default true → flipped
settings.printing.enable = true; # default false → flipped
settings.greeter.autoLogin = "me";
settings.power.suspendThenHibernate = false; # default true → off
};
# #115: s2h on needs a resume device or logind never gets the policy.
s2hOnState = stateFileOf "state-s2h-on.json" {
settings.power.suspendThenHibernate = true;
};
in
{
@@ -206,7 +212,17 @@
nomarchy.system.bluetooth.enable = true;
nomarchy.services.printing.enable = false;
nomarchy.system.greeter.autoLogin = null;
nomarchy.system.power.suspendThenHibernate = true;
};
# #115: resume device required for s2h logind policy.
s2hOn = configWith s2hOnState {
boot.resumeDevice = "/dev/disk/by-uuid/00000000-0000-0000-0000-0000000000aa";
};
s2hOff = configWith flippedState {
boot.resumeDevice = "/dev/disk/by-uuid/00000000-0000-0000-0000-0000000000aa";
};
# Toggle true but no resume → must NOT claim s2h lid policy.
s2hNoResume = configWith s2hOnState { };
cases = [
{ n = "absent key leaves bluetooth at its default (true)";
@@ -231,6 +247,26 @@
ok = pinned.services.printing.enable == false; }
{ n = "hand-set autoLogin outranks the state";
ok = !(pinned.services.greetd.settings ? initial_session); }
{ n = "s2h on + resume HandleLidSwitch=suspend-then-hibernate";
ok = (s2hOn.services.logind.settings.Login.HandleLidSwitch or null)
== "suspend-then-hibernate"; }
{ n = "s2h on + resume HibernateDelaySec=1h";
ok = (s2hOn.systemd.sleep.settings.Sleep.HibernateDelaySec or null)
== "1h"; }
{ n = "s2h on + resume HibernateOnACPower=false";
ok = (s2hOn.systemd.sleep.settings.Sleep.HibernateOnACPower or null)
== false; }
{ n = "s2h on + resume ExternalPower lid = suspend";
ok = (s2hOn.services.logind.settings.Login.HandleLidSwitchExternalPower or null)
== "suspend"; }
{ n = "s2h off + resume lid not forced to s2h";
ok = (s2hOff.services.logind.settings.Login.HandleLidSwitch or null)
!= "suspend-then-hibernate"; }
{ n = "s2h on without resume no s2h lid policy";
ok = (s2hNoResume.services.logind.settings.Login.HandleLidSwitch or null)
!= "suspend-then-hibernate"; }
{ n = "hand-set s2h=true outranks state false";
ok = pinned.nomarchy.system.power.suspendThenHibernate == true; }
];
failed = builtins.filter (c: !c.ok) cases;
in