Files
dotfiles/docs/superpowers/plans/2026-05-10-lid-hibernate.md
T
funman300 4e69e155d7 docs: implementation plan for lid-close hibernate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:05:26 -07:00

402 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lid-Close Hibernate Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make lid close (battery or AC) and 30-min idle both reach hibernation via the kernel's `suspend-then-hibernate` mode, after first enabling hibernation on the system.
**Architecture:** Two layers. (1) A one-time, idempotent bootstrap script (`scripts/enable-hibernation.sh`) that creates a 16 GiB btrfs swap file, adds the `resume` mkinitcpio hook, appends `resume=` + `resume_offset=` to the kernel cmdline, and regenerates the UKI. Reboot required after. (2) Tracked dotfiles (logind drop-in, sleep drop-in, niri swayidle line) deployed by `install.sh` as usual — no reboot needed for these.
**Tech Stack:** bash, systemd-logind, systemd-sleep, mkinitcpio, btrfs, ukify (via `mkinitcpio -P`), niri.
**Spec:** `docs/superpowers/specs/2026-05-10-lid-hibernate-design.md`
**Verification model:** Bash + system commands. Subagents can `bash -n` shell scripts and read INI files but cannot run sudo or reboot. Each task ends with a syntax check + a list of one-line user-side post-deploy verifications the controller will run.
---
## File Structure
| File | Role |
|---|---|
| `scripts/enable-hibernation.sh` | One-time idempotent bootstrap. Creates swap, edits /etc/mkinitcpio.conf, /etc/kernel/cmdline, regenerates UKI. |
| `logind/lid.conf` | Tracked drop-in for `/etc/systemd/logind.conf.d/` — sets all three lid handlers to `suspend-then-hibernate` (or `ignore` for docked). |
| `sleep/hibernate-delay.conf` | Tracked drop-in for `/etc/systemd/sleep.conf.d/` — sets `HibernateDelaySec=30min`. |
| `install.sh` | Modified: adds two `sudo install -Dm644` lines to deploy the drop-ins. |
| `niri/config.kdl` | Modified: line 39 swayidle action `systemctl suspend``systemctl suspend-then-hibernate`. |
| `README.md` | Modified: appended subsection "One-time hibernation enablement" with the run-once instruction. |
---
## Task 1: Bootstrap script
After this task, `sudo bash scripts/enable-hibernation.sh` enables hibernation end-to-end. Re-runs are no-ops. The script does NOT auto-reboot — it prints instructions.
**Files:**
- Create: `scripts/enable-hibernation.sh`
- [ ] **Step 1: Create the script**
Write `scripts/enable-hibernation.sh` with the following exact content:
```bash
#!/bin/bash
# enable-hibernation.sh — one-time setup for laptop hibernation on btrfs.
# Idempotent: re-runs are no-ops once everything is in place. Run with sudo.
# After successful run, reboot before testing `systemctl hibernate`.
set -euo pipefail
if [ "$EUID" -ne 0 ]; then
echo "Run as root: sudo bash $0" >&2
exit 1
fi
SWAPFILE=/swapfile
SWAPSIZE=16g
ROOT_PARTUUID=$(findmnt -no PARTUUID /)
echo "==> Step 1/6: ensure swap file exists at $SWAPFILE (size $SWAPSIZE)"
if [ ! -f "$SWAPFILE" ]; then
btrfs filesystem mkswapfile --size "$SWAPSIZE" "$SWAPFILE"
echo " created $SWAPFILE"
else
actual_bytes=$(stat -c %s "$SWAPFILE")
min_bytes=$((14 * 1024 * 1024 * 1024))
if [ "$actual_bytes" -lt "$min_bytes" ]; then
echo "ERROR: $SWAPFILE exists but is smaller than 14 GiB (got $actual_bytes bytes)." >&2
echo " Remove it manually and re-run." >&2
exit 1
fi
echo " already exists ($((actual_bytes / 1024 / 1024 / 1024)) GiB) — skipping creation"
fi
echo "==> Step 2/6: activate swap and persist in fstab"
if ! swapon --show | grep -q "^$SWAPFILE "; then
swapon "$SWAPFILE"
echo " swapon $SWAPFILE"
else
echo " already active — skipping swapon"
fi
if ! grep -qE "^$SWAPFILE\s" /etc/fstab; then
printf '%s\tnone\tswap\tdefaults\t0 0\n' "$SWAPFILE" >> /etc/fstab
echo " appended to /etc/fstab"
else
echo " /etc/fstab already references $SWAPFILE — skipping"
fi
echo "==> Step 3/6: compute resume params"
RESUME_OFFSET=$(btrfs inspect-internal map-swapfile -r "$SWAPFILE")
echo " resume=PARTUUID=$ROOT_PARTUUID"
echo " resume_offset=$RESUME_OFFSET"
echo "==> Step 4/6: ensure 'resume' hook in /etc/mkinitcpio.conf"
if grep -qE "^HOOKS=\(.*\bresume\b.*\)" /etc/mkinitcpio.conf; then
echo " resume hook already present — skipping"
else
sed -i -E 's/(^HOOKS=\(.*\bblock\b)/\1 resume/' /etc/mkinitcpio.conf
if ! grep -qE "^HOOKS=\(.*\bresume\b.*\)" /etc/mkinitcpio.conf; then
echo "ERROR: failed to insert 'resume' hook. Check /etc/mkinitcpio.conf manually." >&2
exit 1
fi
echo " inserted 'resume' after 'block'"
fi
echo "==> Step 5/6: append resume params to /etc/kernel/cmdline"
if grep -q "resume=" /etc/kernel/cmdline; then
echo " cmdline already contains resume= — skipping"
else
sed -i "1 s|\$| resume=PARTUUID=$ROOT_PARTUUID resume_offset=$RESUME_OFFSET|" /etc/kernel/cmdline
echo " appended to /etc/kernel/cmdline"
fi
echo "==> Step 6/6: regenerate UKI"
mkinitcpio -P
echo
echo "Done. Reboot to activate hibernation:"
echo " sudo reboot"
echo
echo "After reboot, test with: systemctl hibernate"
```
- [ ] **Step 2: Make it executable**
```bash
chmod +x scripts/enable-hibernation.sh
ls -la scripts/enable-hibernation.sh
```
Expected: file mode shows `-rwxr-xr-x` (or similar with `x` bits set).
- [ ] **Step 3: Syntax check**
```bash
bash -n scripts/enable-hibernation.sh && echo "syntax ok"
```
Expected: `syntax ok` and exit 0.
- [ ] **Step 4: Dry-run verify the idempotency guards (without sudo)**
The script bails on the EUID check first if not root, so we can safely "run" it as a non-root user to confirm the bail-out:
```bash
bash scripts/enable-hibernation.sh 2>&1 | head -3
```
Expected output:
```
Run as root: sudo bash scripts/enable-hibernation.sh
```
Exit code 1.
Also visually inspect the script — confirm each of the six steps has a guard (`if [ ! -f ]`, `if ! grep`, etc.) so a re-run is a no-op.
- [ ] **Step 5: Commit**
```bash
git add scripts/enable-hibernation.sh
git commit -m "scripts: add enable-hibernation.sh (one-time bootstrap)"
```
---
## Task 2: Tracked drop-in configs + `install.sh` deploy
After this task, the repo carries the two drop-in files and `install.sh` deploys them. Re-running `install.sh` is safe and overwrites the live drop-ins to match the tracked versions (matches the existing greetd pattern).
**Files:**
- Create: `logind/lid.conf`
- Create: `sleep/hibernate-delay.conf`
- Modify: `install.sh` (add two lines after the existing `==> Deploying greetd config` block)
- [ ] **Step 1: Create `logind/lid.conf`**
```bash
mkdir -p logind
```
Write `logind/lid.conf` with this exact content:
```ini
[Login]
HandleLidSwitch=suspend-then-hibernate
HandleLidSwitchExternalPower=suspend-then-hibernate
HandleLidSwitchDocked=ignore
```
- [ ] **Step 2: Create `sleep/hibernate-delay.conf`**
```bash
mkdir -p sleep
```
Write `sleep/hibernate-delay.conf` with this exact content:
```ini
[Sleep]
HibernateDelaySec=30min
```
- [ ] **Step 3: Verify both INI files**
```bash
cat logind/lid.conf
cat sleep/hibernate-delay.conf
```
Expected: each file's contents print verbatim as above.
- [ ] **Step 4: Add deploy lines to `install.sh`**
Find the existing greetd deploy block in `install.sh`:
```bash
echo "==> Deploying greetd config"
sudo cp "$(pwd)/greetd/config.toml" /etc/greetd/config.toml
sudo cp "$(pwd)/greetd/regreet.toml" /etc/greetd/regreet.toml
```
Immediately *after* the second `sudo cp` line, insert:
```bash
echo "==> Deploying suspend/hibernate config"
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 they don't exist (the `.conf.d` paths may not exist on a fresh install) and sets file mode to 644 — the right tool for system drop-in files.
- [ ] **Step 5: Bash-syntax-check install.sh**
```bash
bash -n install.sh && echo "syntax ok"
```
Expected: `syntax ok` and exit 0.
- [ ] **Step 6: Confirm the new lines appear in the right place**
```bash
grep -A 4 "==> Deploying suspend" install.sh
```
Expected output:
```
echo "==> Deploying suspend/hibernate config"
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
```
- [ ] **Step 7: Commit**
```bash
git add logind/lid.conf sleep/hibernate-delay.conf install.sh
git commit -m "logind+sleep: track lid hibernate config; deploy via install.sh"
```
---
## Task 3: niri swayidle action
After this task, the 30-min idle path uses `systemctl suspend-then-hibernate` instead of `systemctl suspend`, matching the lid handler.
**Files:**
- Modify: `niri/config.kdl` (line 39)
- [ ] **Step 1: Replace the swayidle action**
In `niri/config.kdl`, find line 39:
```kdl
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"
```
Replace `"systemctl suspend"` with `"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"
```
Only one substring changes; nothing else on the line moves. The `before-sleep "gtklock" "-d"` clause stays — gtklock fires on suspend-then-hibernate's suspend phase too.
- [ ] **Step 2: Validate niri config**
```bash
niri validate 2>&1 | tail -2
```
Expected: a final line `INFO niri: config is valid`. Any "ERROR" or non-zero exit means a syntax issue.
- [ ] **Step 3: Confirm exactly one substitution happened**
```bash
grep -c "systemctl suspend-then-hibernate" niri/config.kdl
grep -c "\"systemctl suspend\"" niri/config.kdl
```
Expected:
- First count: `1` (the new value is present)
- Second count: `0` (the old exact-string `"systemctl suspend"` is gone)
- [ ] **Step 4: Commit**
```bash
git add niri/config.kdl
git commit -m "niri: route swayidle 30-min timeout through suspend-then-hibernate"
```
---
## Task 4: README — One-time hibernation enablement subsection
After this task, the README documents the one-time bootstrap step so a reader setting up the system fresh knows to run the script.
**Files:**
- Modify: `README.md` (append a subsection)
- [ ] **Step 1: Append the new subsection**
The README has exactly one top-level section, `## Setup` (line 22). The new subsection nests under it as `### One-time hibernation enablement`.
Append to the end of `README.md` (after the closing line of the existing Setup section):
````markdown
### One-time hibernation enablement
Hibernation requires a persistent swap file, kernel resume parameters, and a regenerated UKI. Run once, then reboot:
```bash
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.
````
(The outer fence above is four backticks because the block contains a triple-backtick code block — paste the inner content verbatim, no enclosing 4-backtick fence.)
- [ ] **Step 2: Verify the section was added**
```bash
grep -A 6 "One-time hibernation enablement" README.md
```
Expected: the new subsection prints, including the `sudo bash scripts/enable-hibernation.sh` line.
- [ ] **Step 3: Commit**
```bash
git add README.md
git commit -m "docs: document one-time hibernation enablement"
```
---
## Final verification (controller, post-merge)
These can only be run on the live system, after merging and pushing all four task commits.
- [ ] **Bootstrap and reboot**
```bash
sudo bash scripts/enable-hibernation.sh
# Output should show steps 16 with each guard either firing or skipping.
sudo reboot
```
- [ ] **After reboot, verify prerequisites**
```bash
swapon --show # /swapfile, ≥14 GiB
cat /proc/cmdline | grep -oE "resume=\S+ resume_offset=\S+" # both present
grep "^HOOKS=" /etc/mkinitcpio.conf | grep resume # hook in array
```
- [ ] **Re-deploy tracked configs**
```bash
cd ~/Documents/dotfiles
bash install.sh
# Should print "==> Deploying suspend/hibernate config" and the two install lines.
ls -la /etc/systemd/logind.conf.d/lid.conf /etc/systemd/sleep.conf.d/hibernate-delay.conf
```
Expected: both files exist, mode 644, owned by root.
- [ ] **Reload logind so lid.conf takes effect**
```bash
sudo systemctl restart systemd-logind # closes session — log back in after
```
(Optional: a niri restart also picks up the new swayidle args. The change takes effect on next login regardless.)
- [ ] **Functional checks**
1. `systemctl hibernate` — laptop powers off completely; press power → resumes to gtklock in ~2030 s.
2. Close lid on battery — laptop suspends instantly; reopen within 30 min → instant resume; reopen after 30+ min → ~30 s resume from hibernation.
3. Close lid on AC — same as battery (no longer just locks).
4. Leave laptop idle 30 min — `journalctl -u systemd-suspend-then-hibernate.service` shows the action firing.