Compare commits
21 Commits
7a5a276efd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 996f11d333 | |||
| be1fa77c6e | |||
| be1d406b26 | |||
| 4303562324 | |||
| d1d3c7f0e9 | |||
| 45ce4594aa | |||
| cc17e7bab4 | |||
| a812715e46 | |||
| 0de20dcd12 | |||
| 841ee432d6 | |||
| 5f00fdc2be | |||
| 096ac3f76a | |||
| de657685db | |||
| 3eb101771e | |||
| fdcec3dd7d | |||
| e3a6986f81 | |||
| 126b03ad26 | |||
| 86cfcb0da5 | |||
| 4e69e155d7 | |||
| d2e9b47584 | |||
| 85ef13492e |
+2
-2
@@ -132,9 +132,9 @@ This system is a **Wayland-first desktop environment** built around the **Niri c
|
|||||||
### Background Services
|
### Background Services
|
||||||
|
|
||||||
| Function | Tool |
|
| Function | Tool |
|
||||||
| ----------- | ------------ |
|
| ----------- | -------------------- |
|
||||||
| Wallpaper | swww |
|
| Wallpaper | swww |
|
||||||
| Screenshots | flameshot |
|
| Screenshots | grim + slurp + satty |
|
||||||
| Clipboard | cliphist |
|
| Clipboard | cliphist |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ A personal Wayland desktop configuration centered around the **Niri** tiling com
|
|||||||
| Lockscreen | gtklock |
|
| Lockscreen | gtklock |
|
||||||
| Login/greeter | greetd + regreet |
|
| Login/greeter | greetd + regreet |
|
||||||
| Wallpaper | swww |
|
| Wallpaper | swww |
|
||||||
| Screenshots | flameshot |
|
| Screenshots | grim + slurp + satty |
|
||||||
| Clipboard | cliphist |
|
| Clipboard | cliphist |
|
||||||
| Theme | Tomorrow Night (GTK: Materia-dark, icons: Papirus) |
|
| Theme | Tomorrow Night (GTK: Materia-dark, icons: Papirus) |
|
||||||
|
|
||||||
@@ -25,3 +25,14 @@ Everything is symlinked into `~/.config` via `install.sh`, with packages listed
|
|||||||
git clone git@github.com:YOURNAME/dotfiles.git
|
git clone git@github.com:YOURNAME/dotfiles.git
|
||||||
cd dotfiles
|
cd dotfiles
|
||||||
./install.sh
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|||||||
@@ -0,0 +1,401 @@
|
|||||||
|
# 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 1–6 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 ~20–30 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.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Power-Profile → Fan-Profile Sync Trigger 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 `scripts/power-profile.sh --menu` instantly trigger `scripts/fan-profile.sh` to re-evaluate (so fan auto mode applies the new mapping in under a second instead of waiting for the next 5 s waybar poll).
|
||||||
|
|
||||||
|
**Architecture:** One-line change. Chain `pkill -RTMIN+9 waybar` (the fan widget's signal) onto the existing `pkill -RTMIN+8 waybar` after `powerprofilesctl set` succeeds. Reuses the existing single-source-of-truth mapping in `fan-profile.sh::map_profile_to_strategy` — no new logic, no new files.
|
||||||
|
|
||||||
|
**Tech Stack:** bash, waybar (`signal:` mechanism), `powerprofilesctl`, `pkill -RTMIN+N`.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-05-11-power-profile-fan-trigger-design.md`
|
||||||
|
|
||||||
|
**Verification model:** Bash syntax check covers static correctness; end-to-end behaviour requires a live waybar + active power profile change (controller verifies after merge). One task only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `scripts/power-profile.sh` | One line modified (line 13): chain `&& pkill -RTMIN+9 waybar` onto the existing `&&`-chained command. |
|
||||||
|
|
||||||
|
No new files. No other files touched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Add fan-widget signal to power-profile menu handler
|
||||||
|
|
||||||
|
After this task, picking a power profile from the wofi menu fires both the power-widget refresh signal (RTMIN+8) and the fan-widget refresh signal (RTMIN+9). The fan widget's existing handler (in `fan-profile.sh`) checks the auto-mode sentinel and applies the mapped fw-fanctrl strategy if appropriate.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/power-profile.sh` (line 13)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Make the one-line change**
|
||||||
|
|
||||||
|
Find line 13 in `scripts/power-profile.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[ -n "$CHOICE" ] && powerprofilesctl set "$CHOICE" && pkill -RTMIN+8 waybar
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[ -n "$CHOICE" ] && powerprofilesctl set "$CHOICE" && pkill -RTMIN+8 waybar && pkill -RTMIN+9 waybar
|
||||||
|
```
|
||||||
|
|
||||||
|
The only change: append ` && pkill -RTMIN+9 waybar` to the end of the existing chain. Indentation, surrounding lines, and the `if`/`exit` block remain untouched.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Syntax-check the script**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n scripts/power-profile.sh && echo "syntax ok"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `syntax ok` and exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Confirm both signals appear in the chain**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "pkill -RTMIN" scripts/power-profile.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output (one line, both signals present):
|
||||||
|
```
|
||||||
|
13: [ -n "$CHOICE" ] && powerprofilesctl set "$CHOICE" && pkill -RTMIN+8 waybar && pkill -RTMIN+9 waybar
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Confirm the status-emit branch is unchanged**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sed -n '17,27p' scripts/power-profile.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: shows the `CURRENT=$(powerprofilesctl get)` block, `case "$CURRENT" in ... esac`, and `printf` JSON line — all byte-identical to before this commit.
|
||||||
|
|
||||||
|
- [ ] **Step 5: End-to-end smoke test (controller — subagent skip)**
|
||||||
|
|
||||||
|
Subagents have no display; skip this step. The controller will:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Confirm both waybar signals are wired up correctly
|
||||||
|
powerprofilesctl get # note current profile
|
||||||
|
~/.local/bin/power-profile --menu # pick a different profile in wofi
|
||||||
|
powerprofilesctl get # confirm it changed
|
||||||
|
fw-fanctrl print | grep '^Strategy:' # if fan auto is on, this should match the mapped strategy within ~1 s
|
||||||
|
```
|
||||||
|
|
||||||
|
If fan auto is off (`~/.local/state/fan-profile-auto` does not exist), `fw-fanctrl` strategy stays unchanged — that's the correct behaviour for Option C.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/power-profile.sh
|
||||||
|
git commit -m "power-profile: signal fan widget on profile change"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final verification (controller, post-merge)
|
||||||
|
|
||||||
|
- [ ] With fan auto on: change power profile via the wofi menu → fan strategy follows within ~1 s (`fw-fanctrl print | grep ^Strategy:` shows the mapped value).
|
||||||
|
- [ ] With fan auto off: change power profile via the wofi menu → fan strategy unchanged (`fw-fanctrl print | grep ^Strategy:` shows the previously-set manual value).
|
||||||
|
- [ ] If `powerprofilesctl set` fails (simulate by stubbing `powerprofilesctl` in `PATH` to return exit 1), neither pkill fires — `journalctl --user -u waybar` shows no spurious refreshes.
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
# Volume Notification 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:** Replace the three `pamixer`-direct media-key bindings with calls to a wrapper script that also fires a mako notification showing the new volume level (or "Muted") with a progress bar.
|
||||||
|
|
||||||
|
**Architecture:** New `scripts/volume.sh` wrapper accepting `up|down|mute`. Adjusts master sink via `pamixer`, then calls `notify-send` with mako's `x-canonical-private-synchronous` and `int:value` hints so successive notifications replace each other and render as a progress bar. niri keybinds call the wrapper through its `~/.local/bin/volume` symlink. install.sh symlinks the script alongside the existing entries.
|
||||||
|
|
||||||
|
**Tech Stack:** bash, pamixer, notify-send, mako (notification daemon), niri.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-05-11-volume-notification-design.md`
|
||||||
|
|
||||||
|
**Verification model:** Bash syntax check + dry-run script invocation with each subcommand. niri config validity via `niri validate`. End-to-end (actually pressing the volume key and seeing a mako bubble) requires the live desktop — the controller verifies after both task commits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `scripts/volume.sh` | New wrapper: pamixer adjust + notify-send. Three subcommands: `up`, `down`, `mute`. |
|
||||||
|
| `install.sh` | One new `ln -sf` line in the `==> Installing scripts` block. |
|
||||||
|
| `niri/config.kdl` | Three lines (97–99) modified to call `volume up/down/mute` instead of `pamixer` directly. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Wrapper script + install.sh symlink
|
||||||
|
|
||||||
|
After this task, `~/.local/bin/volume up|down|mute` works from any shell and produces both the volume change and the mako notification. niri keybinds still call `pamixer` directly — that swap happens in Task 2.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/volume.sh`
|
||||||
|
- Modify: `install.sh` (append one line in the scripts symlink block)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `scripts/volume.sh`**
|
||||||
|
|
||||||
|
Write `scripts/volume.sh` with this exact content:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up) pamixer -i 5 ;;
|
||||||
|
down) pamixer -d 5 ;;
|
||||||
|
mute) pamixer -t ;;
|
||||||
|
*) echo "usage: $0 up|down|mute" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$(pamixer --get-mute)" = "true" ]; then
|
||||||
|
notify-send -h string:x-canonical-private-synchronous:volume \
|
||||||
|
-h int:value:0 \
|
||||||
|
-t 1500 \
|
||||||
|
"Muted"
|
||||||
|
else
|
||||||
|
LEVEL=$(pamixer --get-volume)
|
||||||
|
notify-send -h string:x-canonical-private-synchronous:volume \
|
||||||
|
-h int:value:"$LEVEL" \
|
||||||
|
-t 1500 \
|
||||||
|
"Volume: ${LEVEL}%"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Make it executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/volume.sh
|
||||||
|
ls -la scripts/volume.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `-rwxr-xr-x` mode.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Syntax-check**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n scripts/volume.sh && echo "syntax ok"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `syntax ok`, exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Dry-run the usage-error branch**
|
||||||
|
|
||||||
|
The unrecognised-arg branch is safe to exercise without side effects (pamixer never called):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/volume.sh 2>&1
|
||||||
|
echo "exit=$?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
```
|
||||||
|
usage: scripts/volume.sh up|down|mute
|
||||||
|
exit=1
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Symlink it into `~/.local/bin`**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$PWD/scripts/volume.sh" ~/.local/bin/volume
|
||||||
|
ls -la ~/.local/bin/volume
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: symlink pointing to `$PWD/scripts/volume.sh` (absolute path of repo's script).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Add the symlink line to `install.sh`**
|
||||||
|
|
||||||
|
Find the `==> Installing scripts` block in `install.sh`. It currently ends like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$(pwd)/scripts/waybar-restart.sh" ~/.local/bin/waybar-restart
|
||||||
|
ln -sf "$(pwd)/scripts/screenshot.sh" ~/.local/bin/screenshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Append one line after `screenshot.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -sf "$(pwd)/scripts/volume.sh" ~/.local/bin/volume
|
||||||
|
```
|
||||||
|
|
||||||
|
The block then ends with three `ln -sf` lines (waybar-restart, screenshot, volume).
|
||||||
|
|
||||||
|
- [ ] **Step 7: Sanity-check install.sh parses**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n install.sh && echo "syntax ok"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `syntax ok`, exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Functional smoke test (controller verifies; subagent skip if no audio session)**
|
||||||
|
|
||||||
|
If you have an audio session, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/bin/volume up
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: volume rises 5%, a mako bubble appears with `Volume: NN%` and a progress bar, the bubble auto-dismisses in 1.5 s. Then run `~/.local/bin/volume down` and `~/.local/bin/volume mute` and confirm each behaves.
|
||||||
|
|
||||||
|
If your sandbox has no audio session or no notification daemon, skip; the controller will verify after the commit.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/volume.sh install.sh
|
||||||
|
git commit -m "volume: add notification wrapper script"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Point niri keybinds at the wrapper
|
||||||
|
|
||||||
|
After this task, pressing the three XF86Audio keys runs the wrapper instead of `pamixer` directly. Volume changes now produce notifications.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `niri/config.kdl` (lines 97–99)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the three keybind lines**
|
||||||
|
|
||||||
|
Find lines 97–99 in `niri/config.kdl`:
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true { spawn "pamixer" "-i" "5"; }
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "pamixer" "-d" "5"; }
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "pamixer" "-t"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true { spawn "volume" "up"; }
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "volume" "down"; }
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "volume" "mute"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
`allow-when-locked=true` stays — same behaviour, just routed through the wrapper.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Validate niri config**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
niri validate 2>&1 | tail -2
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: a final line `INFO niri: config is valid`. Non-zero exit or `ERROR` means a syntax issue.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Confirm pamixer is no longer referenced in keybinds**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "pamixer" niri/config.kdl
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: zero matches (the wrapper is invoked via `volume`, pamixer is implementation detail).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Confirm the three new lines are in place**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n 'spawn "volume"' niri/config.kdl
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: three lines, one each for `"up"`, `"down"`, `"mute"`:
|
||||||
|
```
|
||||||
|
97: XF86AudioRaiseVolume allow-when-locked=true { spawn "volume" "up"; }
|
||||||
|
98: XF86AudioLowerVolume allow-when-locked=true { spawn "volume" "down"; }
|
||||||
|
99: XF86AudioMute allow-when-locked=true { spawn "volume" "mute"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Functional smoke test (controller verifies)**
|
||||||
|
|
||||||
|
niri picks up `config.kdl` changes live; no reload needed. Press the volume up key — a mako notification should appear within ~100 ms showing the new level + progress bar. Press mute — notification shows `Muted` with empty bar. Press volume up again — notification returns to `Volume: NN%`.
|
||||||
|
|
||||||
|
Each key press should replace the previous notification (no stacking), thanks to the `x-canonical-private-synchronous` hint.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add niri/config.kdl
|
||||||
|
git commit -m "niri: route volume keys through notification wrapper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final verification (controller, post-merge)
|
||||||
|
|
||||||
|
- [ ] Pressing XF86AudioRaiseVolume / XF86AudioLowerVolume produces a mako notification with the new percentage and a progress bar.
|
||||||
|
- [ ] Pressing XF86AudioMute toggles the mute state; the notification shows `Muted` (with empty bar) when muting and `Volume: NN%` when unmuting.
|
||||||
|
- [ ] Holding the key (rapid repeat) produces a single updating bubble, not a stack.
|
||||||
|
- [ ] Volume changes still work on the lock screen (gtklock). The bubble may not be visible while locked depending on mako's layer ordering relative to gtklock; the volume change itself fires regardless.
|
||||||
|
- [ ] `~/.local/bin/volume bogus` prints the usage message and exits 1.
|
||||||
@@ -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.**
|
||||||
|
- `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]`<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=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 ~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).
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Power-Profile → Fan-Profile Sync Trigger Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-11
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
When the user changes power profile via `scripts/power-profile.sh --menu`, also nudge the waybar fan widget so `scripts/fan-profile.sh` immediately re-evaluates its auto-mode mapping. Without this nudge, the fan strategy follows the new power profile only on the next 5 s waybar poll; with it, the change is essentially instant. If fan auto mode is off (the `~/.local/state/fan-profile-auto` sentinel is absent), nothing changes — manual fan strategy is preserved.
|
||||||
|
|
||||||
|
## Current Behaviour
|
||||||
|
|
||||||
|
`scripts/power-profile.sh --menu` ends with:
|
||||||
|
```bash
|
||||||
|
[ -n "$CHOICE" ] && powerprofilesctl set "$CHOICE" && pkill -RTMIN+8 waybar
|
||||||
|
```
|
||||||
|
|
||||||
|
Signal `RTMIN+8` is the `signal:` value declared by waybar's `custom/power-profile` module — refreshes the power widget. The fan widget (`signal: 9`) is NOT refreshed; it only re-evaluates on its `interval: 5` poll.
|
||||||
|
|
||||||
|
`scripts/fan-profile.sh` already has the coupling logic. When the auto sentinel exists, every status emit:
|
||||||
|
1. Reads `powerprofilesctl get`
|
||||||
|
2. Maps it via `map_profile_to_strategy`
|
||||||
|
3. Calls `fw-fanctrl use <mapped>` if the active strategy differs
|
||||||
|
|
||||||
|
So changing the power profile causes the fan strategy to follow within ≤5 s already — but only on the next poll. The user wants this to be instant.
|
||||||
|
|
||||||
|
## Target Behaviour
|
||||||
|
|
||||||
|
After the user picks a new power profile from the wofi menu:
|
||||||
|
1. `powerprofilesctl set` applies the change.
|
||||||
|
2. `pkill -RTMIN+8 waybar` refreshes the power widget.
|
||||||
|
3. **`pkill -RTMIN+9 waybar` refreshes the fan widget**, which re-runs `fan-profile.sh`. If auto mode is on, fan-profile.sh notices the strategy mismatch and applies the new strategy via `fw-fanctrl use`. If auto mode is off, the script just re-renders the existing strategy (no functional change).
|
||||||
|
|
||||||
|
End-to-end latency: well under a second.
|
||||||
|
|
||||||
|
## Why This Implementation
|
||||||
|
|
||||||
|
Reuses the existing single-source-of-truth mapping in `fan-profile.sh::map_profile_to_strategy`. Power-profile.sh doesn't need to know:
|
||||||
|
- The mapping table (lives in fan-profile.sh)
|
||||||
|
- The auto sentinel path (lives in fan-profile.sh)
|
||||||
|
- The `fw-fanctrl use` command (lives in fan-profile.sh)
|
||||||
|
|
||||||
|
It just sends a "hey, you should re-evaluate" signal. Zero duplication. Zero new files. One line.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### `scripts/power-profile.sh`
|
||||||
|
|
||||||
|
Change line 13 from:
|
||||||
|
```bash
|
||||||
|
[ -n "$CHOICE" ] && powerprofilesctl set "$CHOICE" && pkill -RTMIN+8 waybar
|
||||||
|
```
|
||||||
|
to:
|
||||||
|
```bash
|
||||||
|
[ -n "$CHOICE" ] && powerprofilesctl set "$CHOICE" && pkill -RTMIN+8 waybar && pkill -RTMIN+9 waybar
|
||||||
|
```
|
||||||
|
|
||||||
|
The chained `&&` means the fan-widget refresh fires only if the power-widget refresh succeeded, which only fires if `powerprofilesctl set` succeeded. So a failure to apply the profile (e.g. dbus not running) skips both refreshes — the bar reflects the unchanged state on its next 5 s poll.
|
||||||
|
|
||||||
|
## Files Touched
|
||||||
|
|
||||||
|
- `scripts/power-profile.sh` — one-line change to line 13.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- **Power-profile changes via tools other than this script.** If the user runs `powerprofilesctl set X` from a terminal or via a GNOME settings panel, the fan still follows on fan-profile.sh's 5 s poll (auto mode). Making that path zero-lag would need a dbus listener for `org.freedesktop.UPower.PowerProfiles` — bigger architectural change, deferred.
|
||||||
|
- **Changing the mapping** — `power-saver→lazy`, `balanced→medium`, `performance→agile` is locked from the original fan-profile auto spec.
|
||||||
|
- **Making auto default-on** — auto sentinel still has to be created via the fan wofi menu choice; this spec doesn't change that opt-in.
|
||||||
|
- **Race condition between powerprofilesctl set and fan-profile.sh re-read.** `powerprofilesctl set` is synchronous on dbus; by the time it returns, `powerprofilesctl get` reflects the new value. No race.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Volume Notification Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-11
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Show a mako notification with the current volume level (or "Muted") whenever the user presses the XF86Audio volume keys. Notifications collapse onto themselves so rapid key presses produce a single updating bubble rather than a stack. Mako's progress-bar render shows the current level visually.
|
||||||
|
|
||||||
|
## Current Behaviour
|
||||||
|
|
||||||
|
`niri/config.kdl` lines 97–99 wire the media keys directly to `pamixer`:
|
||||||
|
```kdl
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true { spawn "pamixer" "-i" "5"; }
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "pamixer" "-d" "5"; }
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "pamixer" "-t"; }
|
||||||
|
```
|
||||||
|
Volume changes silently. The waybar `pulseaudio` widget reflects the new value on its next 5 s poll, but there is no immediate visual feedback at the moment of the keypress — and on the lock screen, no feedback at all (waybar isn't visible).
|
||||||
|
|
||||||
|
## Target Behaviour
|
||||||
|
|
||||||
|
Each volume keypress:
|
||||||
|
1. Adjusts the master sink via `pamixer -i 5` / `pamixer -d 5` / `pamixer -t`.
|
||||||
|
2. Reads the resulting state.
|
||||||
|
3. Fires a `notify-send` with mako's progress-bar hint, replacing any previous volume notification (no stacking).
|
||||||
|
|
||||||
|
| Key | Action | Notification |
|
||||||
|
|---|---|---|
|
||||||
|
| XF86AudioRaiseVolume | volume +5% | `Volume: NN%` with N% progress bar |
|
||||||
|
| XF86AudioLowerVolume | volume −5% | `Volume: NN%` with N% progress bar |
|
||||||
|
| XF86AudioMute | toggle mute | `Muted` (with 0% bar) when muting; `Volume: NN%` when unmuting |
|
||||||
|
|
||||||
|
Notification timeout: 1500 ms.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### `scripts/volume.sh` (new)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up) pamixer -i 5 ;;
|
||||||
|
down) pamixer -d 5 ;;
|
||||||
|
mute) pamixer -t ;;
|
||||||
|
*) echo "usage: $0 up|down|mute" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$(pamixer --get-mute)" = "true" ]; then
|
||||||
|
notify-send -h string:x-canonical-private-synchronous:volume \
|
||||||
|
-h int:value:0 \
|
||||||
|
-t 1500 \
|
||||||
|
"Muted"
|
||||||
|
else
|
||||||
|
LEVEL=$(pamixer --get-volume)
|
||||||
|
notify-send -h string:x-canonical-private-synchronous:volume \
|
||||||
|
-h int:value:"$LEVEL" \
|
||||||
|
-t 1500 \
|
||||||
|
"Volume: ${LEVEL}%"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
The `string:x-canonical-private-synchronous:volume` hint is mako's standard mechanism for "transient" notifications — successive notifications carrying the same synchronous-key replace the previous one in place rather than stacking. Mashing the volume key produces a single updating bubble.
|
||||||
|
|
||||||
|
`int:value:N` makes mako render a progress bar at N% (mako displays this for any notification carrying the value hint).
|
||||||
|
|
||||||
|
### `niri/config.kdl`
|
||||||
|
|
||||||
|
Replace lines 97–99 with calls to the wrapper:
|
||||||
|
```kdl
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true { spawn "volume" "up"; }
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "volume" "down"; }
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "volume" "mute"; }
|
||||||
|
```
|
||||||
|
|
||||||
|
`allow-when-locked=true` stays — the notification is still wanted on the lock screen (the lockscreen appears above mako, but mako's bubble shows briefly when the screen is unlocked, and the volume change itself happens regardless).
|
||||||
|
|
||||||
|
### `install.sh`
|
||||||
|
|
||||||
|
Append one symlink line in the existing `==> Installing scripts` block (after the existing `waybar-restart.sh` and `screenshot.sh` lines):
|
||||||
|
```bash
|
||||||
|
ln -sf "$(pwd)/scripts/volume.sh" ~/.local/bin/volume
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Touched
|
||||||
|
|
||||||
|
| File | Action |
|
||||||
|
|---|---|
|
||||||
|
| `scripts/volume.sh` | created |
|
||||||
|
| `niri/config.kdl` | three lines (97–99) modified |
|
||||||
|
| `install.sh` | one symlink line appended |
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- **Volume changes from other sources** (terminal `pamixer`, GUI like pavucontrol). Only this wrapper's invocations trigger notifications. A pulseaudio event listener daemon could cover that case but is bigger scope.
|
||||||
|
- **Per-app sink-input volumes.** Master volume only.
|
||||||
|
- **Variable step sizes** (Shift for 1% etc.). Fixed 5% step matches the existing keybinds.
|
||||||
|
- **Microphone toggle** (XF86AudioMicMute). Not in current keybinds.
|
||||||
|
- **Custom notification icon.** Default mako presentation suffices; icon hint would add machine-specific theming.
|
||||||
+6
-13
@@ -40,19 +40,6 @@ default-timeout=4000
|
|||||||
"""
|
"""
|
||||||
open(os.path.expanduser("~/.config/mako/config"), "w").write(config)
|
open(os.path.expanduser("~/.config/mako/config"), "w").write(config)
|
||||||
PYEOF
|
PYEOF
|
||||||
mkdir -p ~/.config/flameshot
|
|
||||||
if [ ! -f ~/.config/flameshot/flameshot.ini ]; then
|
|
||||||
cat > ~/.config/flameshot/flameshot.ini <<'INI'
|
|
||||||
[General]
|
|
||||||
disabledTrayIcon=true
|
|
||||||
showStartupLaunchMessage=false
|
|
||||||
showHelp=false
|
|
||||||
copyAndCloseAfterUpload=true
|
|
||||||
uiColor=#81a2be
|
|
||||||
contrastUiColor=#1d1f21
|
|
||||||
contrastOpacity=190
|
|
||||||
INI
|
|
||||||
fi
|
|
||||||
ln -sf "$(pwd)/gtklock/config.ini" ~/.config/gtklock/config
|
ln -sf "$(pwd)/gtklock/config.ini" ~/.config/gtklock/config
|
||||||
ln -sf "$(pwd)/gtklock/style.css" ~/.config/gtklock/style.css
|
ln -sf "$(pwd)/gtklock/style.css" ~/.config/gtklock/style.css
|
||||||
ln -sf "$(pwd)/fish/config.fish" ~/.config/fish/config.fish
|
ln -sf "$(pwd)/fish/config.fish" ~/.config/fish/config.fish
|
||||||
@@ -71,6 +58,8 @@ ln -sf "$(pwd)/scripts/clipboard.sh" ~/.local/bin/clipboard-picker
|
|||||||
ln -sf "$(pwd)/scripts/power-profile.sh" ~/.local/bin/power-profile
|
ln -sf "$(pwd)/scripts/power-profile.sh" ~/.local/bin/power-profile
|
||||||
ln -sf "$(pwd)/scripts/fan-profile.sh" ~/.local/bin/fan-profile
|
ln -sf "$(pwd)/scripts/fan-profile.sh" ~/.local/bin/fan-profile
|
||||||
ln -sf "$(pwd)/scripts/waybar-restart.sh" ~/.local/bin/waybar-restart
|
ln -sf "$(pwd)/scripts/waybar-restart.sh" ~/.local/bin/waybar-restart
|
||||||
|
ln -sf "$(pwd)/scripts/screenshot.sh" ~/.local/bin/screenshot
|
||||||
|
ln -sf "$(pwd)/scripts/volume.sh" ~/.local/bin/volume
|
||||||
|
|
||||||
echo "==> Enabling systemd user services"
|
echo "==> Enabling systemd user services"
|
||||||
mkdir -p ~/.config/systemd/user
|
mkdir -p ~/.config/systemd/user
|
||||||
@@ -79,4 +68,8 @@ echo "==> Deploying greetd config"
|
|||||||
sudo cp "$(pwd)/greetd/config.toml" /etc/greetd/config.toml
|
sudo cp "$(pwd)/greetd/config.toml" /etc/greetd/config.toml
|
||||||
sudo cp "$(pwd)/greetd/regreet.toml" /etc/greetd/regreet.toml
|
sudo cp "$(pwd)/greetd/regreet.toml" /etc/greetd/regreet.toml
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
echo "==> Done. Start Niri with: niri-session"
|
echo "==> Done. Start Niri with: niri-session"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[Login]
|
||||||
|
HandleLidSwitch=suspend-then-hibernate
|
||||||
|
HandleLidSwitchExternalPower=suspend-then-hibernate
|
||||||
|
HandleLidSwitchDocked=ignore
|
||||||
+11
-9
@@ -31,17 +31,19 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
XDG_CURRENT_DESKTOP "niri:GNOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn-at-startup "sh" "-c" "systemctl --user import-environment XDG_CURRENT_DESKTOP && dbus-update-activation-environment XDG_CURRENT_DESKTOP"
|
||||||
spawn-at-startup "waybar"
|
spawn-at-startup "waybar"
|
||||||
spawn-at-startup "mako"
|
spawn-at-startup "mako"
|
||||||
spawn-at-startup "awww-daemon"
|
spawn-at-startup "awww-daemon"
|
||||||
spawn-at-startup "nm-applet" "--indicator"
|
|
||||||
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
|
||||||
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"
|
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"
|
||||||
spawn-at-startup "wl-paste" "--watch" "cliphist" "store"
|
spawn-at-startup "wl-paste" "--watch" "cliphist" "store"
|
||||||
spawn-at-startup "blueman-applet"
|
|
||||||
spawn-at-startup "wlsunset" "-l" "49.2" "-L" "-123.1"
|
spawn-at-startup "wlsunset" "-l" "49.2" "-L" "-123.1"
|
||||||
spawn-at-startup "xwayland-satellite"
|
spawn-at-startup "xwayland-satellite"
|
||||||
spawn-at-startup "snixembed" "--no-startup-id"
|
|
||||||
|
|
||||||
binds {
|
binds {
|
||||||
Mod+Q repeat=false { close-window; }
|
Mod+Q repeat=false { close-window; }
|
||||||
@@ -84,7 +86,7 @@ binds {
|
|||||||
Mod+Shift+8 { move-window-to-workspace 8; }
|
Mod+Shift+8 { move-window-to-workspace 8; }
|
||||||
Mod+Shift+9 { move-window-to-workspace 9; }
|
Mod+Shift+9 { move-window-to-workspace 9; }
|
||||||
|
|
||||||
Mod+Print { spawn "flameshot" "gui"; }
|
Mod+Print { spawn "screenshot"; }
|
||||||
|
|
||||||
Mod+M { move-window-to-workspace "minimized"; }
|
Mod+M { move-window-to-workspace "minimized"; }
|
||||||
Mod+Shift+M { focus-workspace "minimized"; }
|
Mod+Shift+M { focus-workspace "minimized"; }
|
||||||
@@ -94,9 +96,9 @@ binds {
|
|||||||
Mod+Shift+C { spawn "clipboard-picker"; }
|
Mod+Shift+C { spawn "clipboard-picker"; }
|
||||||
Mod+Shift+X { quit; }
|
Mod+Shift+X { quit; }
|
||||||
|
|
||||||
XF86AudioRaiseVolume allow-when-locked=true { spawn "pamixer" "-i" "5"; }
|
XF86AudioRaiseVolume allow-when-locked=true { spawn "volume" "up"; }
|
||||||
XF86AudioLowerVolume allow-when-locked=true { spawn "pamixer" "-d" "5"; }
|
XF86AudioLowerVolume allow-when-locked=true { spawn "volume" "down"; }
|
||||||
XF86AudioMute allow-when-locked=true { spawn "pamixer" "-t"; }
|
XF86AudioMute allow-when-locked=true { spawn "volume" "mute"; }
|
||||||
|
|
||||||
XF86MonBrightnessUp allow-when-locked=true { spawn "brightnessctl" "set" "+10%"; }
|
XF86MonBrightnessUp allow-when-locked=true { spawn "brightnessctl" "set" "+10%"; }
|
||||||
XF86MonBrightnessDown allow-when-locked=true { spawn "brightnessctl" "set" "10%-"; }
|
XF86MonBrightnessDown allow-when-locked=true { spawn "brightnessctl" "set" "10%-"; }
|
||||||
@@ -123,7 +125,7 @@ window-rule {
|
|||||||
variable-refresh-rate true
|
variable-refresh-rate true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wine System Tray: hide the host window, icons bridge to SNI via snixembed
|
// Wine System Tray: hide the empty host window off-screen
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id="^explorer.exe$"
|
match app-id="^explorer.exe$"
|
||||||
match title="^Wine System Tray$"
|
match title="^Wine System Tray$"
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ brightnessctl
|
|||||||
pamixer
|
pamixer
|
||||||
gtklock
|
gtklock
|
||||||
swayidle
|
swayidle
|
||||||
snixembed
|
|
||||||
ttf-jetbrains-mono-nerd
|
ttf-jetbrains-mono-nerd
|
||||||
network-manager-applet
|
|
||||||
polkit-gnome
|
polkit-gnome
|
||||||
xdg-desktop-portal-wlr
|
xdg-desktop-portal-wlr
|
||||||
thunar
|
thunar
|
||||||
@@ -39,4 +37,3 @@ power-profiles-daemon
|
|||||||
fw-fanctrl
|
fw-fanctrl
|
||||||
satty
|
satty
|
||||||
starship
|
starship
|
||||||
flameshot
|
|
||||||
|
|||||||
Executable
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#!/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 /)
|
||||||
|
[ -n "$ROOT_PARTUUID" ] || { echo "ERROR: could not determine root PARTUUID via findmnt." >&2; exit 1; }
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -10,7 +10,7 @@ if [ "$1" = "--menu" ]; then
|
|||||||
--no-actions \
|
--no-actions \
|
||||||
--insensitive \
|
--insensitive \
|
||||||
| awk '{print $NF}')
|
| awk '{print $NF}')
|
||||||
[ -n "$CHOICE" ] && powerprofilesctl set "$CHOICE" && pkill -RTMIN+8 waybar
|
[ -n "$CHOICE" ] && powerprofilesctl set "$CHOICE" && pkill -RTMIN+8 waybar && pkill -RTMIN+9 waybar
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ CHOICE=$(printf " Logout\n Restart\n Sleep\n Shutdown" \
|
|||||||
case "$CHOICE" in
|
case "$CHOICE" in
|
||||||
Logout) niri msg action quit ;;
|
Logout) niri msg action quit ;;
|
||||||
Restart) systemctl reboot ;;
|
Restart) systemctl reboot ;;
|
||||||
Sleep) systemctl suspend ;;
|
Sleep) systemctl suspend-then-hibernate ;;
|
||||||
Shutdown) systemctl poweroff ;;
|
Shutdown) systemctl poweroff ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
FILE="$HOME/Pictures/screenshot-$(date +%s).png"
|
||||||
|
mkdir -p "$HOME/Pictures"
|
||||||
|
|
||||||
|
REGION=$(slurp) || exit 0
|
||||||
|
grim -g "$REGION" - | satty \
|
||||||
|
--filename - \
|
||||||
|
--output-filename "$FILE" \
|
||||||
|
--copy-command wl-copy \
|
||||||
|
--early-exit
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
up) pamixer -i 5 ;;
|
||||||
|
down) pamixer -d 5 ;;
|
||||||
|
mute) pamixer -t ;;
|
||||||
|
*) echo "usage: $0 up|down|mute" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$(pamixer --get-mute)" = "true" ]; then
|
||||||
|
notify-send -h string:x-canonical-private-synchronous:volume \
|
||||||
|
-h int:value:0 \
|
||||||
|
-t 1500 \
|
||||||
|
"Muted"
|
||||||
|
else
|
||||||
|
LEVEL=$(pamixer --get-volume)
|
||||||
|
notify-send -h string:x-canonical-private-synchronous:volume \
|
||||||
|
-h int:value:"$LEVEL" \
|
||||||
|
-t 1500 \
|
||||||
|
"Volume: ${LEVEL}%"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[Sleep]
|
||||||
|
HibernateDelaySec=30min
|
||||||
Reference in New Issue
Block a user