Compare commits

...

34 Commits

Author SHA1 Message Date
funman300 996f11d333 drop snixembed: waybar provides its own SNI watcher
snixembed registered as the StatusNotifierWatcher but its
RegisterStatusNotifierItem implementation didn't accept SNI publishers
— Telegram, Vesktop, blueman-tray, and a libayatana-appindicator test
script all failed to register, leaving waybar's tray empty.

Removing snixembed lets waybar's tray module register itself as both
host AND watcher (when no other watcher exists). After this change,
all three SNI items registered immediately.

Trade-off: legacy XEmbed-based Wine tray icons no longer bridge to
SNI. The Wine System Tray host window rule is kept (still hides the
empty window off-screen) and the comment updated.

- niri/config.kdl: drop snixembed spawn-at-startup; update Wine rule
- packages.txt: drop snixembed (no longer used)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:10:44 -07:00
funman300 be1fa77c6e niri: propagate XDG_CURRENT_DESKTOP to systemd-user and dbus
The niri 'environment' block only affects processes niri spawns
directly. Apps launched via dbus activation or systemd-user (most
.desktop launches go through one of these) get systemd-user's env,
which still has bare 'niri' — set by niri-session BEFORE niri itself
runs.

Adding a one-shot spawn-at-startup that re-imports XDG_CURRENT_DESKTOP
from niri's env into both systemd-user and dbus-activation, after
niri starts. Future dbus-activated apps will see niri:GNOME.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:35:23 -07:00
funman300 be1d406b26 niri: set XDG_CURRENT_DESKTOP=niri:GNOME for tray support
Many tray libraries (Electron's, Qt's StatusNotifier) gate icon
creation on a recognised XDG_CURRENT_DESKTOP value. Bare "niri"
makes them silently skip tray creation. Adding ":GNOME" as a
fallback identifier keeps niri primary while letting apps create
their tray icons.

Vesktop and Telegram are confirmed to want tray icons but were
not creating them; this fix unblocks both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:23:42 -07:00
funman300 4303562324 remove nm-applet and blueman-applet autostart
Both daemons were running but not registering SNI items on niri/Wayland,
leaving the waybar tray slot permanently empty. Network is already
accessible via the waybar network module (left-click → networkmanager_dmenu).
Bluetooth GUI (blueman-manager) is still available on demand; only the
applet is dropped.

Tray module stays configured so Electron apps (Discord/Vesktop, Steam,
etc.) still get a tray slot when they're running.

- niri/config.kdl: drop nm-applet and blueman-applet spawn-at-startup
- packages.txt: drop network-manager-applet (nothing else uses it);
  keep blueman package for blueman-manager GUI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:19:07 -07:00
funman300 d1d3c7f0e9 niri: route volume keys through notification wrapper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 12:17:31 -07:00
funman300 45ce4594aa volume: add notification wrapper script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 11:18:22 -07:00
funman300 cc17e7bab4 docs: implementation plan for volume notification
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:09:40 -07:00
funman300 a812715e46 docs: spec for volume change notification
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:36:47 -07:00
funman300 0de20dcd12 power-profile: signal fan widget on profile change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:55:54 -07:00
funman300 841ee432d6 docs: implementation plan for power-profile fan-trigger
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:54:44 -07:00
funman300 5f00fdc2be docs: spec for power-profile→fan sync trigger
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:52:28 -07:00
funman300 096ac3f76a powermenu: route Sleep through suspend-then-hibernate
Aligns the third sleep path (power menu) with the lid handler and
the swayidle 30-min timer — all three now suspend immediately and
hibernate after HibernateDelaySec (30 min).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:30:32 -07:00
funman300 de657685db docs: document one-time hibernation enablement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:24:50 -07:00
funman300 3eb101771e niri: route swayidle 30-min timeout through suspend-then-hibernate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:22:53 -07:00
funman300 fdcec3dd7d logind+sleep: track lid hibernate config; deploy via install.sh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:19:51 -07:00
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
funman300 126b03ad26 scripts: guard enable-hibernation against empty root PARTUUID
If `findmnt -no PARTUUID /` returns nothing (root on LVM, no GPT,
unusual mount state) the script would silently write a broken
resume=PARTUUID= line to /etc/kernel/cmdline. Bail with an error
message instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:18:25 -07:00
funman300 86cfcb0da5 scripts: add enable-hibernation.sh (one-time bootstrap)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:08:28 -07:00
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
funman300 d2e9b47584 docs: spec for lid-close hibernate (suspend-then-hibernate)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:59:23 -07:00
funman300 85ef13492e screenshot: revert flameshot, use grim+slurp+satty pipeline
Flameshot's portal-based capture path on niri triggers an
xdg-desktop-portal access prompt on every invocation, blocking
the screenshot itself. grim talks to wlr-screencopy directly and
never touches the portal, so no prompt.

Pipeline: slurp (region) → grim (capture) → satty (annotate +
copy + save). satty replaces the wofi action menu by being the
post-capture surface itself.

- scripts/screenshot.sh added
- install.sh symlinks it; flameshot.ini generator removed
- niri Mod+Print → "screenshot"
- packages.txt drops flameshot
- docs tables updated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:27:16 -07:00
funman300 7a5a276efd docs: track implementation plans for fan-profile auto and flameshot
Both plans were generated during their respective implementation
runs but never committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:18:31 -07:00
funman300 4775c16bb4 docs: fix table alignment after flameshot rename
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:18:31 -07:00
funman300 5995139b44 docs: update screenshot tool reference to flameshot
The "Tooling" tables in README.md and ARCHITECTURE.md still listed
grim + slurp after the flameshot swap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:15:16 -07:00
funman300 d9f4538ea7 screenshot: remove old grim/slurp/wofi pipeline (replaced by flameshot)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 23:11:20 -07:00
funman300 004ffe3ec7 niri: bind Mod+Print to flameshot gui
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 23:08:14 -07:00
funman300 4db0d690a9 install: generate flameshot.ini with idempotent guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 23:04:04 -07:00
funman300 caa34f2fd9 packages: add flameshot 2026-05-05 22:56:58 -07:00
funman300 c44ab1e266 docs: spec for flameshot screenshot tool swap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:52:19 -07:00
funman300 07388423a8 niri: float the Thunar file-operation progress dialog
The progress window for copy/move/delete operations is small and
transient — floating it stops it from taking over a column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:49:55 -07:00
funman300 370b4aa096 fan-profile: drop redundant 2>/dev/null on mapping function call
map_profile_to_strategy never writes to stderr — the suppression was
silencing nothing real and would mislead a future reader.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:33:50 -07:00
funman300 9ff1190549 fan-profile: render auto mode and reconcile fw-fanctrl on poll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:29:56 -07:00
funman300 cc164dfa50 fan-profile: route 'auto' menu choice through state file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:24:58 -07:00
funman300 b04357de87 fan-profile: add state path and profile→strategy mapping (unused yet)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:05:14 -07:00
22 changed files with 2060 additions and 62 deletions
+2 -2
View File
@@ -132,9 +132,9 @@ This system is a **Wayland-first desktop environment** built around the **Niri c
### Background Services
| Function | Tool |
| ----------- | ------------ |
| ----------- | -------------------- |
| Wallpaper | swww |
| Screenshots | grim + slurp |
| Screenshots | grim + slurp + satty |
| Clipboard | cliphist |
---
+12 -1
View File
@@ -13,7 +13,7 @@ A personal Wayland desktop configuration centered around the **Niri** tiling com
| Lockscreen | gtklock |
| Login/greeter | greetd + regreet |
| Wallpaper | swww |
| Screenshots | grim + slurp |
| Screenshots | grim + slurp + satty |
| Clipboard | cliphist |
| 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
cd dotfiles
./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,373 @@
# Fan-Profile "Auto" Mode 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:** Add a virtual `auto` strategy to `scripts/fan-profile.sh` that mirrors the active power-profiles-daemon profile to a corresponding `fw-fanctrl` strategy, reconciling on every waybar refresh.
**Architecture:** Three additions to a single bash script — a state-file constant, a profile→strategy mapping function, and two new conditional branches (one in the `--menu` handler, one in the status emit). State lives in `~/.local/state/fan-profile-auto`. No new files, no new daemons, no fw-fanctrl config edits.
**Tech Stack:** bash, `fw-fanctrl`, `powerprofilesctl`, wofi, waybar.
**Spec:** `docs/superpowers/specs/2026-05-01-fan-profile-auto-design.md`
**Verification model:** Bash script — no unit-test framework. Each task ends with manual `bash scripts/fan-profile.sh` invocations under representative state (auto-on / auto-off / power-profile mismatch / `powerprofilesctl` failure simulated by overriding `PATH`), inspecting JSON output and side-effects on `fw-fanctrl`.
**Mapping (locked from spec):**
| power-profiles-daemon | fw-fanctrl |
|---|---|
| `power-saver` | `lazy` |
| `balanced` | `medium` |
| `performance` | `agile` |
---
## File Structure
- **Modify:** `scripts/fan-profile.sh` — add a constant, a function, and modify two existing branches.
No other files.
---
## Task 1: Add state-file constant and mapping function
After this task, the script has a place to store auto-mode state and a pure function to map a power profile to a fan strategy. No behavior change yet — these are unused until Tasks 2 and 3 wire them in.
**Files:**
- Modify: `scripts/fan-profile.sh` (insert after the `#!/bin/bash` shebang on line 1)
- [ ] **Step 1: Add the constant and function at the top of the script**
Open `scripts/fan-profile.sh`. After the shebang line (`#!/bin/bash`) and the blank line that follows, insert:
```bash
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
map_profile_to_strategy() {
case "$1" in
power-saver) echo lazy ;;
balanced) echo medium ;;
performance) echo agile ;;
*) return 1 ;;
esac
}
```
The script's first 4 lines should now read:
```bash
#!/bin/bash
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
map_profile_to_strategy() {
```
- [ ] **Step 2: Sanity-check the function**
Run:
```bash
bash -c 'source scripts/fan-profile.sh; map_profile_to_strategy power-saver; map_profile_to_strategy balanced; map_profile_to_strategy performance; map_profile_to_strategy bogus; echo "exit=$?"'
```
Expected output:
```
lazy
medium
agile
exit=1
```
(`bogus` produces no stdout and exits 1; the other three each print one strategy name on its own line.)
Note: sourcing the script will also execute the rest of it (the status emit). That's fine — its output gets mixed in. Only inspect that the four lines above appear in the right order somewhere.
- [ ] **Step 3: Verify status emit still works**
Run the existing status path:
```bash
bash scripts/fan-profile.sh
```
Expected: a single JSON line like `{"text": "󰈐 medium", "tooltip": "Fan strategy: medium\nSpeed: 28%\nClick to change", "class": "medium"}` — same shape as before. The script's behavior is unchanged because nothing references `STATE_FILE` or `map_profile_to_strategy` yet.
- [ ] **Step 4: Commit**
```bash
git add scripts/fan-profile.sh
git commit -m "fan-profile: add state path and profile→strategy mapping (unused yet)"
```
---
## Task 2: Wire `auto` into the wofi menu
After this task, picking `auto` from the menu enables auto mode (creates the state file and applies the mapped strategy immediately). Picking any other strategy disables auto mode (deletes the state file). The status emit still behaves as before — auto-mode rendering comes in Task 3.
**Files:**
- Modify: `scripts/fan-profile.sh` — replace the existing `--menu` block (currently lines ~316 of the post-Task-1 file) with the version below.
- [ ] **Step 1: Replace the `--menu` branch**
Find the existing block:
```bash
if [ "$1" = "--menu" ]; then
CHOICE=$(fw-fanctrl print list 2>/dev/null \
| grep "^-" \
| sed 's/^- //' \
| wofi --dmenu \
--prompt "Fan Strategy:" \
--width 260 \
--height 300 \
--hide-scroll \
--no-actions \
--insensitive)
[ -n "$CHOICE" ] && fw-fanctrl use "$CHOICE" && pkill -RTMIN+9 waybar
exit
fi
```
Replace it with:
```bash
if [ "$1" = "--menu" ]; then
STRATS=$(fw-fanctrl print list 2>/dev/null | grep "^-" | sed 's/^- //')
CHOICE=$(printf 'auto\n%s\n' "$STRATS" \
| wofi --dmenu \
--prompt "Fan Strategy:" \
--width 260 \
--height 320 \
--hide-scroll \
--no-actions \
--insensitive)
[ -z "$CHOICE" ] && exit
if [ "$CHOICE" = "auto" ]; then
mkdir -p "$(dirname "$STATE_FILE")"
touch "$STATE_FILE"
PROFILE=$(powerprofilesctl get 2>/dev/null) \
&& MAPPED=$(map_profile_to_strategy "$PROFILE") \
&& fw-fanctrl use "$MAPPED"
else
rm -f "$STATE_FILE"
fw-fanctrl use "$CHOICE"
fi
pkill -RTMIN+9 waybar
exit
fi
```
Three substantive changes:
1. `auto\n` is prepended to the strategy list piped into wofi.
2. Empty selection (user cancels) now exits early instead of falling through.
3. `auto` and non-`auto` selections are routed differently — auto creates the state file and applies the mapped strategy; everything else removes the state file (if present) and uses fw-fanctrl directly.
The wofi `--height` was bumped from 300 → 320 to accommodate the extra row.
- [ ] **Step 2: Verify the menu branch in dry-run**
You can't fully exercise wofi without a display, but you can verify the strategy list construction:
```bash
bash -c 'STRATS=$(fw-fanctrl print list 2>/dev/null | grep "^-" | sed "s/^- //"); printf "auto\n%s\n" "$STRATS"'
```
Expected output (one strategy per line, `auto` first):
```
auto
laziest
lazy
medium
agile
very-agile
deaf
aeolus
```
- [ ] **Step 3: Verify state-file logic by simulating "auto" selection**
Manually run the auto branch:
```bash
mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
bash -c 'source scripts/fan-profile.sh; CHOICE=auto
if [ "$CHOICE" = "auto" ]; then
mkdir -p "$(dirname "$STATE_FILE")"
touch "$STATE_FILE"
echo "state file created at: $STATE_FILE"
fi'
ls -la "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
```
Expected: file exists.
Now simulate selecting a real strategy:
```bash
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
touch "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
bash -c 'source scripts/fan-profile.sh; CHOICE=lazy
if [ "$CHOICE" != "auto" ]; then
rm -f "$STATE_FILE"
echo "state file removed"
fi'
ls -la "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto" 2>&1
```
Expected: `cannot access ... No such file or directory` (file removed). Then clean up:
```bash
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
```
- [ ] **Step 4: Commit**
```bash
git add scripts/fan-profile.sh
git commit -m "fan-profile: route 'auto' menu choice through state file"
```
---
## Task 3: Render auto mode in the status emit
After this task, when the state file exists the status emit reconciles `fw-fanctrl` to the power profile's mapped strategy and renders `auto` in the bar. When the state file is absent, behavior is unchanged.
**Files:**
- Modify: `scripts/fan-profile.sh` — insert a new conditional block before the existing status emit (the `OUTPUT=$(fw-fanctrl print 2>/dev/null)` line and everything after it).
- [ ] **Step 1: Insert the auto-mode status block**
Find this section in the script (it's the part that runs when no `--menu` arg was passed):
```bash
OUTPUT=$(fw-fanctrl print 2>/dev/null)
CURRENT=$(echo "$OUTPUT" | awk -F"'" '/^Strategy:/{print $2}')
SPEED=$(echo "$OUTPUT" | awk '/^Speed:/{print $2}')
```
Immediately *before* that `OUTPUT=` line, insert:
```bash
if [ -f "$STATE_FILE" ]; then
PROFILE=$(powerprofilesctl get 2>/dev/null)
MAPPED=$(map_profile_to_strategy "$PROFILE" 2>/dev/null)
if [ -n "$MAPPED" ]; then
ACTIVE=$(fw-fanctrl print 2>/dev/null | awk -F"'" '/^Strategy:/{print $2}')
[ "$ACTIVE" != "$MAPPED" ] && fw-fanctrl use "$MAPPED" >/dev/null 2>&1
printf '{"text": "󰈐 auto", "tooltip": "Auto fan strategy\\nProfile: %s → %s\\nClick to change", "class": "auto"}\n' "$PROFILE" "$MAPPED"
else
printf '{"text": "󰈐 auto (?)", "tooltip": "Auto: power-profiles-daemon unreachable or unknown profile\\nClick to change", "class": "auto"}\n'
fi
exit
fi
```
(Trailing blank line included so there's separation between the new block and the existing `OUTPUT=` line.)
- [ ] **Step 2: Verify auto-off path is unchanged**
Make sure the state file does NOT exist:
```bash
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
bash scripts/fan-profile.sh
```
Expected: same JSON as before Task 1 — `{"text": "󰈐 <strategy>", "tooltip": "Fan strategy: <strategy>\nSpeed: <pct>%\nClick to change", "class": "<level>"}`.
- [ ] **Step 3: Verify auto-on happy path**
Create the state file and run the script:
```bash
mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"
touch "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
bash scripts/fan-profile.sh
```
Expected output (assuming `powerprofilesctl get` returns a known profile, e.g. `balanced`):
```json
{"text": "󰈐 auto", "tooltip": "Auto fan strategy\nProfile: balanced → medium\nClick to change", "class": "auto"}
```
Also verify the side-effect — `fw-fanctrl` should now be on the mapped strategy:
```bash
fw-fanctrl print | grep '^Strategy:'
```
Expected: `Strategy: 'medium'` (or `lazy`/`agile` depending on your active power profile).
- [ ] **Step 4: Verify auto-on error path (unknown profile)**
Simulate `powerprofilesctl` returning something the mapping doesn't know. We do this by stubbing `powerprofilesctl` via `PATH`:
```bash
mkdir -p /tmp/fan-stub
cat >/tmp/fan-stub/powerprofilesctl <<'STUB'
#!/bin/bash
echo bogus-profile
STUB
chmod +x /tmp/fan-stub/powerprofilesctl
PATH=/tmp/fan-stub:$PATH bash scripts/fan-profile.sh
```
Expected:
```json
{"text": "󰈐 auto (?)", "tooltip": "Auto: power-profiles-daemon unreachable or unknown profile\nClick to change", "class": "auto"}
```
- [ ] **Step 5: Verify auto-on error path (powerprofilesctl missing)**
```bash
cat >/tmp/fan-stub/powerprofilesctl <<'STUB'
#!/bin/bash
exit 1
STUB
PATH=/tmp/fan-stub:$PATH bash scripts/fan-profile.sh
```
Expected: same `auto (?)` output as above (since `PROFILE` ends up empty, `map_profile_to_strategy` returns 1, `MAPPED` is empty, error branch fires).
Clean up:
```bash
rm -rf /tmp/fan-stub
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
```
- [ ] **Step 6: Reload waybar to confirm end-to-end**
Restart waybar so it picks up the script changes (the script file itself is loaded each invocation, so this is mostly to refresh the bar widget visually):
```bash
waybar-restart
```
Then enable auto mode without going through wofi:
```bash
mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"
touch "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
pkill -RTMIN+9 waybar
```
The bar's fan-profile widget should now read `auto` (within ~5 s — the next waybar poll). Hover the widget; the tooltip should show `Profile: <name> → <strategy>`. Switch your power profile (e.g. `powerprofilesctl set performance`) and within ~5 s the tooltip should update to `Profile: performance → agile` and `fw-fanctrl print` should confirm the strategy switched.
To turn auto off and verify the original behaviour returns:
```bash
rm "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
pkill -RTMIN+9 waybar
```
The bar should revert to showing the underlying strategy name.
- [ ] **Step 7: Commit**
```bash
git add scripts/fan-profile.sh
git commit -m "fan-profile: render auto mode and reconcile fw-fanctrl on poll"
```
---
## Final verification
- [ ] **Auto persists across waybar restarts.** With the state file present, run `waybar-restart`. The bar should come back showing `auto` (within 5 s of restart).
- [ ] **Auto persists across reboot.** Optional but the design promises it. The state file lives in `~/.local/state/` which survives reboot.
- [ ] **Wofi menu shows `auto` first.** Click the fan-profile widget; the wofi menu should list `auto` at the top followed by the seven fw-fanctrl strategies.
- [ ] **Selecting `auto` from the menu works end-to-end.** Pick `auto` → bar updates to `auto` within seconds, `fw-fanctrl print` shows the mapped strategy.
- [ ] **Selecting a non-auto strategy from the menu disables auto.** Pick e.g. `aeolus` → bar updates to `aeolus`, state file is gone (`ls ~/.local/state/fan-profile-auto` returns "No such file or directory"), and subsequent power-profile changes do NOT swap the fan strategy automatically.
@@ -0,0 +1,340 @@
# Flameshot Screenshot Swap 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 bespoke `scripts/screenshot.sh` pipeline with Flameshot, bound to `Mod+Print`.
**Architecture:** Pure tool swap. Install Flameshot from `extra`, generate a small `~/.config/flameshot/flameshot.ini` from `install.sh` (mirrors the existing `mako/config` generator pattern), point `Mod+Print` at `flameshot gui`, then delete the old script + symlink + install.sh entry. Tasks ordered so `Mod+Print` keeps working at every commit boundary.
**Tech Stack:** flameshot (Qt6, official Arch `extra`), niri, bash for `install.sh`.
**Spec:** `docs/superpowers/specs/2026-05-02-flameshot-screenshot-design.md`
**Verification model:** Bash + system commands. Each task ends with shell verification commands and a one-line user check (e.g. "press `Mod+Print`, confirm Flameshot toolbar appears"). The controller has a display; subagents do not — defer visual checks to the controller.
---
## File Structure
| File | Change |
|---|---|
| `packages.txt` | add `flameshot` |
| `install.sh` | remove the `ln -sf ".../screenshot.sh" ...` line; add a flameshot.ini generator block (idempotent: only writes if file doesn't exist) |
| `niri/config.kdl` | change `Mod+Print { spawn "screenshot"; }` to `Mod+Print { spawn "flameshot" "gui"; }` |
| `scripts/screenshot.sh` | deleted |
| `~/.local/bin/screenshot` | symlink removed (filesystem only, not git-tracked) |
| `~/.config/flameshot/flameshot.ini` | created on disk by the install.sh block (filesystem only, not git-tracked) |
---
## Task 1: Install Flameshot and add to packages.txt
After this task, the `flameshot` binary is on PATH and `packages.txt` declares it as a dependency. The old screenshot script and keybind are still wired up — nothing functional has changed yet.
**Files:**
- Modify: `packages.txt` (add one line)
- [ ] **Step 1: Install flameshot via the AUR helper**
```bash
yay -S --needed --noconfirm flameshot
```
Expected: package installs (or "is up to date" if already present). If `yay` is unavailable, substitute `paru` or `sudo pacman -S --needed --noconfirm flameshot`.
- [ ] **Step 2: Verify the binary is on PATH**
```bash
flameshot --version
```
Expected: a version string like `Flameshot v13.3.0 (compiled with Qt 6.x)`. If "command not found", the install failed — escalate.
- [ ] **Step 3: Add `flameshot` to `packages.txt`**
Open `packages.txt` and insert `flameshot` in alphabetical-ish order. The existing list is roughly grouped by topic, not strictly sorted. Insert after `fw-fanctrl` (line 39) so it sits with the small one-off utilities cluster:
```
fw-fanctrl
satty
starship
flameshot
```
(Final order doesn't matter as long as `flameshot` appears once and on its own line. Don't reorder unrelated lines.)
- [ ] **Step 4: Verify packages.txt is well-formed**
```bash
grep -c "^flameshot$" packages.txt
```
Expected: `1` (exactly one occurrence).
- [ ] **Step 5: Commit**
```bash
git add packages.txt
git commit -m "packages: add flameshot"
```
---
## Task 2: Add flameshot config generator to install.sh and run it
After this task, `~/.config/flameshot/flameshot.ini` exists with the spec's defaults. Re-running `install.sh` will not clobber the file (the generator has an `[ ! -f ]` guard so user-saved preferences like `savePath` are preserved on subsequent runs). The screenshot keybind still points at the old script.
**Files:**
- Modify: `install.sh` (insert generator block after the existing mako python block, around line 42)
- [ ] **Step 1: Insert the flameshot config generator into `install.sh`**
Find the existing mako block in `install.sh`. It looks like:
```bash
python3 - <<'PYEOF'
import json, os
c = json.load(open("theme/colors.json"))
config = f"""background-color={c['background']}
text-color={c['foreground']}
border-size=2
border-color={c['blue']}
default-timeout=4000
"""
open(os.path.expanduser("~/.config/mako/config"), "w").write(config)
PYEOF
```
Immediately *after* the `PYEOF` line that closes the mako block, insert:
```bash
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
```
The `[ ! -f ]` guard is required: Flameshot writes runtime state (savePath, last folder) back to this file, so re-running `install.sh` must not overwrite user preferences.
- [ ] **Step 2: Sanity-check `install.sh` parses as bash**
```bash
bash -n install.sh
```
Expected: no output, exit 0. (`-n` is dry-run syntax check.)
- [ ] **Step 3: Run the new block manually to create the live config**
You can either run install.sh in full, or extract just the new block. Cleanest is to run the block standalone:
```bash
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
```
- [ ] **Step 4: Verify the file was written with the right contents**
```bash
cat ~/.config/flameshot/flameshot.ini
```
Expected output:
```
[General]
disabledTrayIcon=true
showStartupLaunchMessage=false
showHelp=false
copyAndCloseAfterUpload=true
uiColor=#81a2be
contrastUiColor=#1d1f21
contrastOpacity=190
```
- [ ] **Step 5: Verify the guard works (re-run is a no-op)**
```bash
# Mutate the file slightly
echo "savePath=/home/alex/Pictures" >> ~/.config/flameshot/flameshot.ini
# Re-run the generator block (paste from Step 3)
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
# Confirm the savePath line is still present (file was not regenerated)
grep "^savePath=" ~/.config/flameshot/flameshot.ini
```
Expected: `savePath=/home/alex/Pictures` (proves the `[ ! -f ]` guard prevented overwrite).
Clean up the test mutation (so the file matches the spec's defaults again):
```bash
sed -i '/^savePath=/d' ~/.config/flameshot/flameshot.ini
```
- [ ] **Step 6: Commit**
```bash
git add install.sh
git commit -m "install: generate flameshot.ini with idempotent guard"
```
---
## Task 3: Repoint `Mod+Print` to Flameshot
After this task, pressing `Mod+Print` opens Flameshot's GUI (region selector + on-canvas annotation toolbar). The old `scripts/screenshot.sh` is no longer reachable via keybind, but the script and its symlink still exist on disk — Task 4 deletes them.
**Files:**
- Modify: `niri/config.kdl` (line 87)
- [ ] **Step 1: Replace the keybind**
In `niri/config.kdl`, find line 87:
```kdl
Mod+Print { spawn "screenshot"; }
```
Replace with:
```kdl
Mod+Print { spawn "flameshot" "gui"; }
```
Note the two-string form for `spawn` — niri's `spawn` action takes the program and each argument as separate quoted strings. `spawn "flameshot gui"` (single string) would not work; niri would try to exec a binary literally named `flameshot gui`.
- [ ] **Step 2: Validate the niri config**
```bash
niri validate
```
Expected: exit 0 and a final line `INFO niri: config is valid`. Any "ERROR" or non-zero exit means a syntax issue — fix before continuing.
- [ ] **Step 3: User-side visual verification (controller only — subagent skip)**
Niri picks up `config.kdl` changes live; no reload needed. Press `Mod+Print`. Expected: Flameshot's region-selector overlay appears, with a floating toolbar showing drawing tools and Save/Copy/Discard buttons. Cancel out (Esc) without taking a real screenshot.
If a portal permission prompt appears the first time, allow it — Flameshot uses `xdg-desktop-portal-wlr` to capture under Wayland.
If `Mod+Print` does nothing or you see "command not found" in `journalctl --user`, the binary isn't on PATH for niri — re-run Task 1, Step 1.
- [ ] **Step 4: Commit**
```bash
git add niri/config.kdl
git commit -m "niri: bind Mod+Print to flameshot gui"
```
---
## Task 4: Delete the old screenshot script, symlink, and install.sh entry
After this task, all traces of `scripts/screenshot.sh` are gone — script file deleted, `~/.local/bin/screenshot` symlink removed, and the corresponding line in `install.sh` removed so re-running it doesn't recreate the dead symlink. `Mod+Print` continues to work via Flameshot.
**Files:**
- Delete: `scripts/screenshot.sh`
- Modify: `install.sh` (remove one line)
- Filesystem: `~/.local/bin/screenshot` (remove symlink)
- [ ] **Step 1: Remove the symlink line from `install.sh`**
In `install.sh`, find this line (it's inside the `==> Installing scripts` section, around line 61):
```bash
ln -sf "$(pwd)/scripts/screenshot.sh" ~/.local/bin/screenshot
```
Delete the entire line. The surrounding lines (other `ln -sf` calls for the remaining scripts) stay intact.
- [ ] **Step 2: Sanity-check install.sh still parses**
```bash
bash -n install.sh
```
Expected: no output, exit 0.
- [ ] **Step 3: Delete the live symlink**
```bash
rm ~/.local/bin/screenshot
```
Expected: silent success. Verify it's gone:
```bash
ls -la ~/.local/bin/screenshot 2>&1
```
Expected: `cannot access ... No such file or directory`.
- [ ] **Step 4: Delete the script file from the repo**
```bash
git rm scripts/screenshot.sh
```
Expected: `rm 'scripts/screenshot.sh'`.
- [ ] **Step 5: Verify nothing else in the repo references the deleted script**
```bash
grep -rn "screenshot" --include="*.kdl" --include="*.sh" --include="*.jsonc" --include="*.toml" --include="*.fish" --exclude-dir=docs /home/alex/Documents/dotfiles
```
Expected: zero hits, or only matches inside markdown/spec files (which the include-glob excludes anyway). If the keybind from Task 3 still says `spawn "screenshot"`, Task 3 didn't land — go back and check.
- [ ] **Step 6: Confirm `Mod+Print` still works (controller only — subagent skip)**
Press `Mod+Print`. Flameshot's GUI should still appear (this isn't a regression test — it's just a sanity check that nothing in the cleanup broke the live keybind).
- [ ] **Step 7: Commit**
```bash
git add install.sh scripts/screenshot.sh
git commit -m "screenshot: remove old grim/slurp/wofi pipeline (replaced by flameshot)"
```
(`git rm` from Step 4 already staged the deletion; this commit picks up both the staged deletion and the install.sh edit.)
---
## Final verification
- [ ] **Pressing `Mod+Print` opens Flameshot.** Region selector + on-canvas toolbar with drawing tools and Save/Copy/Discard buttons.
- [ ] **`flameshot gui` produces a usable capture.** Pick a region, click Copy → confirm clipboard has the image (`wl-paste --list-types | grep image/png` should show `image/png`). Click Save → file lands wherever Flameshot prompts (first-time prompt expected; pick `~/Pictures`).
- [ ] **No leftover references.** `grep -rn "screenshot\.sh\|/local/bin/screenshot" /home/alex/Documents/dotfiles --exclude-dir=.git --exclude-dir=docs` → no hits.
- [ ] **install.sh is re-runnable without breakage.** Optional: rerun `bash install.sh` and confirm flameshot.ini is preserved (the `[ ! -f ]` guard) and no `~/.local/bin/screenshot` symlink reappears (because the line was removed).
- [ ] **Toolbar colors match the theme.** The Flameshot floating toolbar should show the Tomorrow Night blue accent (`#81a2be`) on the dark background (`#1d1f21`).
@@ -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 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.
@@ -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 (9799) 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 9799)
- [ ] **Step 1: Replace the three keybind lines**
Find lines 9799 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,118 @@
# Switch Screenshot Stack to Flameshot
**Date:** 2026-05-02
**Status:** Approved
## Summary
Replace the bespoke screenshot pipeline (`scripts/screenshot.sh` + grim/slurp/wofi/notification flow) with Flameshot. Flameshot provides region select, on-canvas annotation, save, copy, and discard in a single integrated GUI — collapsing the current two-step "capture → notification → wofi → tool" flow into one.
## Current Behaviour
`Mod+Print` runs `~/.local/bin/screenshot` which is a symlink to `scripts/screenshot.sh`:
1. `slurp | grim` for region select + capture, save to `~/Pictures/screenshot-<unix-ts>.png`.
2. `wl-copy` the file.
3. mako notification with thumbnail + "Actions" button (10 s timeout).
4. If "Actions" clicked, wofi menu offers: Annotate (swappy), Annotate (satty), Open in imv, Copy path, Delete.
Three keypresses or clicks to reach the annotator. The notification window steals focus for 10 seconds. Two annotators offered; both must be installed.
## Target Behaviour
`Mod+Print` runs `flameshot gui`. The Flameshot toolbar appears overlaid on the screen with region selector + drawing tools (arrow, rectangle, blur, text, etc.) + Save/Copy/Discard buttons. Capture, edit, and dispatch are one continuous interaction. No notification, no wofi menu, no separate annotator.
## Implementation
### Packages
- **Add to `packages.txt`:** `flameshot` (extra repo, no AUR helper needed).
- **Remove from `packages.txt`:** none — `swappy`, `satty`, `grim`, `slurp` are kept (used by other tools or potentially useful standalone). Flameshot replaces only the orchestration script.
### Files to delete
- `scripts/screenshot.sh`
- `~/.local/bin/screenshot` (the symlink)
- `install.sh` line: `ln -sf "$(pwd)/scripts/screenshot.sh" ~/.local/bin/screenshot`
### Niri keybind change
In `niri/config.kdl`, replace:
```kdl
Mod+Print { spawn "screenshot"; }
```
with:
```kdl
Mod+Print { spawn "flameshot" "gui"; }
```
Single keybind only — no additional shortcuts for full-screen / delayed / monitor modes. Those live inside the Flameshot toolbar already.
### Flameshot configuration
The flameshot config is **generated by `install.sh`**, not tracked in the repo. The pattern mirrors how `mako/config` is handled today — Flameshot writes machine-local state back to the file at runtime (savePath, last folder, etc.), so committing the file would create churn or risk leaking user paths into the repo.
The generated file content:
```ini
[General]
disabledTrayIcon=true
showStartupLaunchMessage=false
showHelp=false
copyAndCloseAfterUpload=true
uiColor=#81a2be
contrastUiColor=#1d1f21
contrastOpacity=190
```
- `disabledTrayIcon=true` — no SNI icon (the user has no system tray on the bar).
- `showStartupLaunchMessage=false` — suppress first-run notification.
- `showHelp=false` — suppress on-canvas tooltip overlay every capture.
- `copyAndCloseAfterUpload=true` — auto-copy to clipboard when saved.
- `uiColor` / `contrastUiColor` — Tomorrow Night palette (`@tn-blue` and `@tn-bg` from `theme/colors.css`) so the toolbar matches the rest of the desktop.
- `contrastOpacity=190` — Flameshot's standard mid-opacity backdrop dimming during capture.
Notably **NOT** included:
- `savePath` — Flameshot's INI format does not expand `~`, so a hardcoded `/home/alex/Pictures` would break for any other user. On first save Flameshot will prompt for a destination; the user picks `~/Pictures` and Flameshot remembers it locally (the resulting file gets written through the symlink back into the repo, but `flameshot/flameshot.ini` is gitignored — see below — so it stays local).
### `install.sh` changes
- **Remove:** `ln -sf "$(pwd)/scripts/screenshot.sh" ~/.local/bin/screenshot`
- **Add:** A here-doc generator block (mirroring the mako pattern, lines 32-42) that writes `~/.config/flameshot/flameshot.ini` if the file doesn't already exist:
```bash
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
```
The `if [ ! -f ]` guard prevents clobbering the user's saved preferences (savePath, last folder, etc.) on re-runs. This differs from the mako block (which always overwrites) — Flameshot's config accumulates user-chosen state, so we must not stomp it.
## Files Touched
- `packages.txt` — add `flameshot`
- `niri/config.kdl` — change one keybind line
- `install.sh` — remove one symlink line, add the flameshot config generator
- `scripts/screenshot.sh` — deleted
## Out of Scope
- Additional keybinds (full-screen, delayed, monitor) — Flameshot's toolbar already exposes these. One keybind is enough.
- Removing `swappy` / `satty` / `imv` / `grim` / `slurp` from packages — they're useful standalone and don't carry weight.
- Theming Flameshot beyond the two color values — Flameshot's UI is otherwise Qt-default and doesn't take much further customization.
- Setting `savePath` declaratively — the INI format does not support `~` expansion; let Flameshot's first-save prompt handle it.
- Adapting to portal limitations on Wayland — if Flameshot misbehaves under the `xdg-desktop-portal-wlr` backend, that's a Flameshot/portal issue to file upstream, not something this spec addresses.
@@ -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 ~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).
@@ -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 9799 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 9799 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 (9799) 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 -1
View File
@@ -57,8 +57,9 @@ ln -sf "$(pwd)/scripts/powermenu.sh" ~/.local/bin/powermenu
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/fan-profile.sh" ~/.local/bin/fan-profile
ln -sf "$(pwd)/scripts/screenshot.sh" ~/.local/bin/screenshot
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"
mkdir -p ~/.config/systemd/user
@@ -67,4 +68,8 @@ echo "==> Deploying greetd config"
sudo cp "$(pwd)/greetd/config.toml" /etc/greetd/config.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"
+4
View File
@@ -0,0 +1,4 @@
[Login]
HandleLidSwitch=suspend-then-hibernate
HandleLidSwitchExternalPower=suspend-then-hibernate
HandleLidSwitchDocked=ignore
+16 -8
View File
@@ -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 "mako"
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 "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 "blueman-applet"
spawn-at-startup "wlsunset" "-l" "49.2" "-L" "-123.1"
spawn-at-startup "xwayland-satellite"
spawn-at-startup "snixembed" "--no-startup-id"
binds {
Mod+Q repeat=false { close-window; }
@@ -94,9 +96,9 @@ binds {
Mod+Shift+C { spawn "clipboard-picker"; }
Mod+Shift+X { quit; }
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"; }
XF86AudioRaiseVolume allow-when-locked=true { spawn "volume" "up"; }
XF86AudioLowerVolume allow-when-locked=true { spawn "volume" "down"; }
XF86AudioMute allow-when-locked=true { spawn "volume" "mute"; }
XF86MonBrightnessUp 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
}
// 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 {
match app-id="^explorer.exe$"
match title="^Wine System Tray$"
@@ -148,3 +150,9 @@ window-rule {
off
}
}
// Thunar file-operation progress dialog
window-rule {
match app-id="^thunar$" title="^File Operation Progress$"
open-floating true
}
-2
View File
@@ -12,9 +12,7 @@ brightnessctl
pamixer
gtklock
swayidle
snixembed
ttf-jetbrains-mono-nerd
network-manager-applet
polkit-gnome
xdg-desktop-portal-wlr
thunar
+79
View File
@@ -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"
+39 -5
View File
@@ -1,17 +1,51 @@
#!/bin/bash
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
map_profile_to_strategy() {
case "$1" in
power-saver) echo lazy ;;
balanced) echo medium ;;
performance) echo agile ;;
*) return 1 ;;
esac
}
if [ "$1" = "--menu" ]; then
CHOICE=$(fw-fanctrl print list 2>/dev/null \
| grep "^-" \
| sed 's/^- //' \
STRATS=$(fw-fanctrl print list 2>/dev/null | grep "^-" | sed 's/^- //')
CHOICE=$(printf 'auto\n%s\n' "$STRATS" \
| wofi --dmenu \
--prompt "Fan Strategy:" \
--width 260 \
--height 300 \
--height 320 \
--hide-scroll \
--no-actions \
--insensitive)
[ -n "$CHOICE" ] && fw-fanctrl use "$CHOICE" && pkill -RTMIN+9 waybar
[ -z "$CHOICE" ] && exit
if [ "$CHOICE" = "auto" ]; then
mkdir -p "$(dirname "$STATE_FILE")"
touch "$STATE_FILE"
PROFILE=$(powerprofilesctl get 2>/dev/null) \
&& MAPPED=$(map_profile_to_strategy "$PROFILE") \
&& fw-fanctrl use "$MAPPED"
else
rm -f "$STATE_FILE"
fw-fanctrl use "$CHOICE"
fi
pkill -RTMIN+9 waybar
exit
fi
if [ -f "$STATE_FILE" ]; then
PROFILE=$(powerprofilesctl get 2>/dev/null)
MAPPED=$(map_profile_to_strategy "$PROFILE")
if [ -n "$MAPPED" ]; then
ACTIVE=$(fw-fanctrl print 2>/dev/null | awk -F"'" '/^Strategy:/{print $2}')
[ "$ACTIVE" != "$MAPPED" ] && fw-fanctrl use "$MAPPED" >/dev/null 2>&1
printf '{"text": "󰈐 auto", "tooltip": "Auto fan strategy\\nProfile: %s → %s\\nClick to change", "class": "auto"}\n' "$PROFILE" "$MAPPED"
else
printf '{"text": "󰈐 auto (?)", "tooltip": "Auto: power-profiles-daemon unreachable or unknown profile\\nClick to change", "class": "auto"}\n'
fi
exit
fi
+1 -1
View File
@@ -10,7 +10,7 @@ if [ "$1" = "--menu" ]; then
--no-actions \
--insensitive \
| 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
fi
+1 -1
View File
@@ -13,6 +13,6 @@ CHOICE=$(printf " Logout\n Restart\n Sleep\n Shutdown" \
case "$CHOICE" in
Logout) niri msg action quit ;;
Restart) systemctl reboot ;;
Sleep) systemctl suspend ;;
Sleep) systemctl suspend-then-hibernate ;;
Shutdown) systemctl poweroff ;;
esac
+6 -38
View File
@@ -1,43 +1,11 @@
#!/bin/bash
FILE="$HOME/Pictures/screenshot-$(date +%s).png"
mkdir -p "$HOME/Pictures"
# Exit silently if the user cancels region selection
if ! grim -g "$(slurp)" "$FILE"; then
exit 0
fi
wl-copy < "$FILE"
action=$(notify-send "Screenshot captured" \
"Saved · Copied to clipboard" \
--hint="string:image-path:$FILE" \
--expire-time=10000 \
--action="actions:Actions")
[[ "$action" != "actions" ]] && exit 0
choice=$(printf 'Annotate with swappy\nAnnotate with satty\nOpen in imv\nCopy path\nDelete' \
| wofi --dmenu --prompt "Screenshot")
case "$choice" in
"Annotate with swappy")
swappy -f "$FILE" &
;;
"Annotate with satty")
satty --filename "$FILE" &
;;
"Open in imv")
imv "$FILE" &
;;
"Copy path")
printf '%s' "$FILE" | wl-copy
notify-send "Path copied" "$FILE"
;;
"Delete")
rm "$FILE"
notify-send "Screenshot deleted" "File removed"
;;
esac
REGION=$(slurp) || exit 0
grim -g "$REGION" - | satty \
--filename - \
--output-filename "$FILE" \
--copy-command wl-copy \
--early-exit
+21
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
[Sleep]
HibernateDelaySec=30min