Files
dotfiles/docs/superpowers/specs/2026-05-10-lid-hibernate-design.md
funman300 e3a6986f81 docs: align lid-hibernate spec with PARTUUID-based resume
Spec said `findmnt -no UUID /` and `resume=UUID=` but the script
correctly uses PARTUUID throughout (matches the system's existing
root=PARTUUID= convention).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:18:25 -07:00

7.5 KiB
Raw Permalink Blame History

Lid-Close Hibernate Design

Date: 2026-05-10 Status: Approved

Summary

Make the laptop sleep more aggressively to save battery: close the lid (on AC or battery) → suspend immediately → hibernate after 30 min if not reopened. Same path applies to swayidle's existing 30-min idle timer. Reliably reaches 0% battery drain within ~60 min of stopping use, while keeping fast resume for short interruptions.

Requires one-time bootstrap to enable hibernation (swap file, kernel resume params, mkinitcpio hook, UKI regen, reboot). Day-to-day config lives in tracked dotfiles.

Current Behaviour

Lid handling (/etc/systemd/logind.conf.d/lid.conf, not currently tracked in repo):

HandleLidSwitch=suspend
HandleLidSwitchExternalPower=lock
HandleLidSwitchDocked=ignore

On battery: lid close → suspend (RAM stays powered, slow drain). On AC: lid close → just locks (laptop runs at full power overnight).

swayidle (niri/config.kdl line 39):

spawn-at-startup "swayidle" "-w" "timeout" "300" "niri msg action power-off-monitors" \
                                  "timeout" "600" "gtklock" "-d" \
                                  "timeout" "1800" "systemctl suspend" \
                                  "before-sleep" "gtklock" "-d"

After 30 min idle, calls systemctl suspend (not hibernate, no auto-progression).

Hibernation prerequisites are NOT met: only zram swap (4 GiB, RAM-backed, can't persist), no resume= kernel param, no resume mkinitcpio hook. systemctl hibernate would fail.

Target Behaviour

Trigger Immediate action After 30 min suspended Result
Lid close (battery or AC) suspend hibernate 0% drain
Idle 30 min suspend hibernate 0% drain
Lid close while docked ignore desktop stays awake
systemctl hibernate hibernate immediately 0% drain, manual

HibernateDelaySec=30min controls the suspend → hibernate transition. Tunable in one tracked file.

Implementation

Hibernation prerequisites (one-time bootstrap)

scripts/enable-hibernation.sh — idempotent script, requires sudo, run once, reboot afterward:

  1. Swap file. btrfs filesystem mkswapfile --size 16g /swapfile (handles NOCOW, alignment, mkswap in one). Skip if /swapfile exists with size ≥14 GiB.
  2. Activate. swapon /swapfile. Append /swapfile none swap defaults 0 0 to /etc/fstab if absent.
  3. Compute resume params.
    • ROOT_PARTUUID=$(findmnt -no PARTUUID /) → root partition's GPT PARTUUID (matches the existing root=PARTUUID=… convention in /etc/kernel/cmdline)
    • RESUME_OFFSET=$(btrfs inspect-internal map-swapfile -r /swapfile) → physical offset of swap file
  4. mkinitcpio hook. Edit /etc/mkinitcpio.conf HOOKS array to insert resume between block and filesystems. sed guarded so a re-run is a no-op.
  5. Kernel cmdline. Append resume=PARTUUID=$ROOT_PARTUUID resume_offset=$RESUME_OFFSET to /etc/kernel/cmdline if not already present.
  6. Regenerate UKI. mkinitcpio -P → produces /boot/EFI/Linux/arch-linux-zen.efi with new HOOKS + cmdline baked in.
  7. Reboot. Print explicit instruction; do not auto-reboot.

Why a separate script and not install.sh: hibernation enablement is invasive (modifies /etc/mkinitcpio.conf, /etc/kernel/cmdline, allocates 16 GiB on disk, regenerates the UKI). install.sh runs frequently during dotfiles iteration; this should run once.

Tracked dotfiles (deployed by install.sh)

New file Destination Contents
logind/lid.conf /etc/systemd/logind.conf.d/lid.conf [Login]
HandleLidSwitch=suspend-then-hibernate
HandleLidSwitchExternalPower=suspend-then-hibernate
HandleLidSwitchDocked=ignore
sleep/hibernate-delay.conf /etc/systemd/sleep.conf.d/hibernate-delay.conf [Sleep]
HibernateDelaySec=30min

install.sh additions

After the existing greetd block:

sudo install -Dm644 "$(pwd)/logind/lid.conf" /etc/systemd/logind.conf.d/lid.conf
sudo install -Dm644 "$(pwd)/sleep/hibernate-delay.conf" /etc/systemd/sleep.conf.d/hibernate-delay.conf

install -Dm644 creates intermediate directories if missing and sets correct mode (consistent with system file expectations).

niri/config.kdl swayidle tweak

Line 39: change "systemctl suspend" to "systemctl suspend-then-hibernate". The full line becomes:

spawn-at-startup "swayidle" "-w" "timeout" "300" "niri msg action power-off-monitors" "timeout" "600" "gtklock" "-d" "timeout" "1800" "systemctl suspend-then-hibernate" "before-sleep" "gtklock" "-d"

This makes the 30-min idle path use the same logind/sleep configuration as the lid path. After this edit, idle suspends and lid suspends are identical paths — both transition to hibernation after 30 min suspended.

README.md setup section

Add a paragraph after the existing install.sh instructions:

### One-time hibernation enablement

Hibernation requires a persistent swap file, kernel resume parameters, and a regenerated UKI. Run once, then reboot:

    sudo bash scripts/enable-hibernation.sh

After reboot, `systemctl hibernate` will work, and lid close / 30-min idle will suspend-then-hibernate per the tracked logind config.

Verification

After running scripts/enable-hibernation.sh and rebooting:

swapon --show          # /swapfile, ≥14 GiB, prio low
cat /proc/cmdline      # contains resume=PARTUUID=… resume_offset=…
systemctl status systemd-hibernate.service

Behavioural checks:

  1. systemctl hibernate — laptop fully powers off, then resumes to gtklock on next press of power button. (Resume takes ~2030 s.)
  2. systemctl suspend-then-hibernate — laptop suspends instantly. After 30 min, kernel transitions to hibernation. (Don't have to wait for a real test — journalctl -u systemd-suspend-then-hibernate.service shows the timer firing.)
  3. Close lid on battery → suspends. Reopen within 30 min → instant resume. Reopen after 30 min → ~30 s resume from hibernation.
  4. Close lid on AC → same as battery (the asymmetric "AC just locks" behavior is gone).
  5. Leave laptop idle 30 min → swayidle calls systemctl suspend-then-hibernate, same path as lid.

Files Touched

File Action
logind/lid.conf created
sleep/hibernate-delay.conf created
scripts/enable-hibernation.sh created
install.sh append two sudo install lines
niri/config.kdl one-line edit on line 39 (swayidle action)
README.md append a "One-time hibernation enablement" subsection

Out of Scope

  • Snapshot-aware swap subvolume. No snapshot tool (snapper, btrbk) is installed, so a flat /swapfile on the root subvolume is safe. If snapshots are added later, the swap file needs to be moved to a dedicated @swap subvolume — a future migration, not part of this work.
  • Encrypted hibernation image. Root is not LUKS, so the hibernation image lands unencrypted on /swapfile. If full-disk encryption is added later, the swap file is automatically encrypted along with the rest of root.
  • GUI tools (HandleSuspendKey, HandlePowerKey, etc.) — unchanged. Only Lid* keys are touched.
  • HibernateDelaySec UI exposure. Tunable in the tracked file; no script or env var to flip it without editing.
  • Auto-reboot from enable-hibernation.sh. The script prints a reboot instruction and exits. Rebooting is a user action.
  • Removing zram swap. zram stays as a fast in-memory compressed swap layer; hibernation uses /swapfile (lower priority is fine — kernel picks higher-priority zram first under memory pressure).