docs: spec for lid-close hibernate (suspend-then-hibernate)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# 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.**
|
||||
- `RESUME_UUID=$(findmnt -no UUID /)` → root filesystem UUID (currently `60cbc9b9-3631-4f9a-925c-a45e0920eb17`)
|
||||
- `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=UUID=$RESUME_UUID 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]`<br>`HandleLidSwitch=suspend-then-hibernate`<br>`HandleLidSwitchExternalPower=suspend-then-hibernate`<br>`HandleLidSwitchDocked=ignore` |
|
||||
| `sleep/hibernate-delay.conf` | `/etc/systemd/sleep.conf.d/hibernate-delay.conf` | `[Sleep]`<br>`HibernateDelaySec=30min` |
|
||||
|
||||
### `install.sh` additions
|
||||
|
||||
After the existing greetd block:
|
||||
```bash
|
||||
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:
|
||||
|
||||
```kdl
|
||||
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:
|
||||
|
||||
```markdown
|
||||
### 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:
|
||||
|
||||
```bash
|
||||
swapon --show # /swapfile, ≥14 GiB, prio low
|
||||
cat /proc/cmdline # contains resume=UUID=… 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 ~20–30 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).
|
||||
Reference in New Issue
Block a user