Compare commits
48 Commits
7ab5116e5d
..
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 | |||
| 7a5a276efd | |||
| 4775c16bb4 | |||
| 5995139b44 | |||
| d9f4538ea7 | |||
| 004ffe3ec7 | |||
| 4db0d690a9 | |||
| caa34f2fd9 | |||
| c44ab1e266 | |||
| 07388423a8 | |||
| 370b4aa096 | |||
| 9ff1190549 | |||
| cc164dfa50 | |||
| b04357de87 | |||
| ef4d8cb44d | |||
| d4daad2071 | |||
| c27f72a8a8 | |||
| 2b4054b44b | |||
| 6372695b16 | |||
| c21a2834b7 | |||
| 2c851c7561 | |||
| 1305b1cda1 | |||
| 67fbea93a4 | |||
| 1646dfcd90 | |||
| 8b35df2989 | |||
| 51353cecd0 | |||
| 17abe4651e | |||
| 412034ea9f |
+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 | grim + slurp |
|
| 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 | grim + slurp |
|
| 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,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 ~3–16 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 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,142 @@
|
|||||||
|
# Fan-Profile "Auto" Mode Design
|
||||||
|
|
||||||
|
**Date:** 2026-05-01
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add a virtual `auto` strategy to the waybar fan-profile module. When auto is on, `scripts/fan-profile.sh` keeps `fw-fanctrl` in sync with the active `power-profiles-daemon` profile — `power-saver` → `lazy`, `balanced` → `medium`, `performance` → `agile`. Reconciliation rides waybar's existing 5 s poll; no new daemon, no udev hooks, no fw-fanctrl strategy edits.
|
||||||
|
|
||||||
|
## Current Behaviour
|
||||||
|
|
||||||
|
`scripts/fan-profile.sh` is invoked by waybar's `custom/fan-profile` module on a 5 s interval and on click:
|
||||||
|
|
||||||
|
- **Status emit:** runs `fw-fanctrl print`, parses out the active strategy and current speed, prints a JSON line with an icon and tooltip.
|
||||||
|
- **`--menu`:** runs `fw-fanctrl print list`, pipes the strategy names through wofi, calls `fw-fanctrl use <choice>`, signals `pkill -RTMIN+9 waybar` so the bar refreshes.
|
||||||
|
|
||||||
|
The strategy list is whatever `fw-fanctrl print list` returns: `laziest, lazy, medium, agile, very-agile, deaf, aeolus`. There is currently no notion of an automatic mode.
|
||||||
|
|
||||||
|
## Target Behaviour
|
||||||
|
|
||||||
|
### Wofi menu
|
||||||
|
|
||||||
|
A new entry `auto` is prepended to the strategy list before passing it to wofi. Selecting it:
|
||||||
|
|
||||||
|
1. Touches the state file `~/.local/state/fan-profile-auto`.
|
||||||
|
2. Resolves the current power profile and calls `fw-fanctrl use <mapped>` once.
|
||||||
|
3. Signals waybar to refresh.
|
||||||
|
|
||||||
|
Selecting any non-`auto` strategy:
|
||||||
|
|
||||||
|
1. Removes the state file (if it exists).
|
||||||
|
2. Calls `fw-fanctrl use <choice>` (current behaviour, unchanged).
|
||||||
|
3. Signals waybar to refresh.
|
||||||
|
|
||||||
|
### Status emit (every 5 s)
|
||||||
|
|
||||||
|
```
|
||||||
|
state file exists?
|
||||||
|
├── yes → read powerprofilesctl get
|
||||||
|
│ ├── ok → resolve mapped strategy
|
||||||
|
│ │ ├── differs from fw-fanctrl's active → fw-fanctrl use <mapped>
|
||||||
|
│ │ └── matches → no-op
|
||||||
|
│ │ emit JSON: text="auto", tooltip="auto → <mapped>", class="auto"
|
||||||
|
│ └── error → emit JSON: text="auto (?)", tooltip="auto: power-profiles-daemon unreachable", class="auto"
|
||||||
|
└── no → existing behaviour: emit fw-fanctrl's active strategy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mapping
|
||||||
|
|
||||||
|
| power-profiles-daemon | fw-fanctrl |
|
||||||
|
|---|---|
|
||||||
|
| `power-saver` | `lazy` |
|
||||||
|
| `balanced` | `medium` |
|
||||||
|
| `performance` | `agile` |
|
||||||
|
|
||||||
|
Hardcoded in the script — small, stable, doesn't need a config file. If a profile name comes back unrecognized (e.g. a custom profile), the script falls into the error branch above (`auto (?)`).
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### `scripts/fan-profile.sh` (only file changed)
|
||||||
|
|
||||||
|
Three additions to the existing script:
|
||||||
|
|
||||||
|
1. **State path constant** at top:
|
||||||
|
```bash
|
||||||
|
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Mapping function**:
|
||||||
|
```bash
|
||||||
|
map_profile_to_strategy() {
|
||||||
|
case "$1" in
|
||||||
|
power-saver) echo lazy ;;
|
||||||
|
balanced) echo medium ;;
|
||||||
|
performance) echo agile ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Menu branch** — prepend `auto` and route the choice:
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Status branch** — wrap the existing emit with auto-mode handling:
|
||||||
|
```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
|
||||||
|
# ... existing status emit (fw-fanctrl print → icon/level → JSON) ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reconciliation cadence
|
||||||
|
|
||||||
|
Waybar polls `custom/fan-profile` every 5 s (`"interval": 5` in `waybar/config.jsonc`). Each poll runs the script, which reads the state file and reconciles. No additional daemon, signal, or hook is needed. Worst-case desync: 5 s after a power-profile change.
|
||||||
|
|
||||||
|
The existing `pkill -RTMIN+9 waybar` from `power-profile.sh --menu` will trigger an immediate refresh when the user changes profile via the bar — so in the common case, the fan strategy switches within waybar's render cycle, not 5 s later.
|
||||||
|
|
||||||
|
### Persistence
|
||||||
|
|
||||||
|
State file lives at `$XDG_STATE_HOME/fan-profile-auto` (default `~/.local/state/fan-profile-auto`). XDG state dir survives reboot, so auto stays on across sessions. Removing the file (or selecting any non-auto strategy) disables auto mode.
|
||||||
|
|
||||||
|
## Files Touched
|
||||||
|
|
||||||
|
- `scripts/fan-profile.sh` — three additions (state constant, mapping function, two new branches in menu and status handlers).
|
||||||
|
|
||||||
|
No CSS, no waybar config, no install.sh changes.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Syncing the auto-on state across machines (would require moving the state file into `~/.config/` and tracking it in the dotfiles repo).
|
||||||
|
- Mapping to strategies other than `lazy`/`medium`/`agile`. The user can edit the mapping function inline if they want different curves later.
|
||||||
|
- Reacting to anything other than power-profiles-daemon (CPU temp, AC vs battery, etc.). Those were rejected during brainstorming in favour of the cleaner power-profile coupling.
|
||||||
|
- A separate icon for auto mode. Reuses the existing `` glyph; the `auto` text in the bar is the differentiator.
|
||||||
|
- Tracking `/etc/fw-fanctrl/config.json` in the dotfiles repo (raised during clarifications, not adopted — auto doesn't need any new fw-fanctrl strategies).
|
||||||
@@ -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 ~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
-1
@@ -25,7 +25,6 @@ ln -sf "$(pwd)/networkmanager-dmenu/config.ini" ~/.config/networkmanager-dmenu/c
|
|||||||
ln -sf "$(pwd)/niri/config.kdl" ~/.config/niri/config.kdl
|
ln -sf "$(pwd)/niri/config.kdl" ~/.config/niri/config.kdl
|
||||||
ln -sf "$(pwd)/waybar/config.jsonc" ~/.config/waybar/config.jsonc
|
ln -sf "$(pwd)/waybar/config.jsonc" ~/.config/waybar/config.jsonc
|
||||||
ln -sf "$(pwd)/waybar/style.css" ~/.config/waybar/style.css
|
ln -sf "$(pwd)/waybar/style.css" ~/.config/waybar/style.css
|
||||||
ln -sf "$(pwd)/waybar/mouse-battery.sh" ~/.config/waybar/mouse-battery.sh
|
|
||||||
mkdir -p ~/.config/theme
|
mkdir -p ~/.config/theme
|
||||||
ln -sf "$(pwd)/theme/colors.css" ~/.config/theme/colors.css
|
ln -sf "$(pwd)/theme/colors.css" ~/.config/theme/colors.css
|
||||||
ln -sf "$(pwd)/wofi/config" ~/.config/wofi/config
|
ln -sf "$(pwd)/wofi/config" ~/.config/wofi/config
|
||||||
@@ -58,7 +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/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/screenshot.sh" ~/.local/bin/screenshot
|
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
|
||||||
@@ -67,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
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
background-color=#1d1f21
|
|
||||||
text-color=#c5c8c6
|
|
||||||
border-size=2
|
|
||||||
border-color=#81a2be
|
|
||||||
default-timeout=4000
|
|
||||||
+19
-10
@@ -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 "swww-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 "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-then-hibernate" "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" "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; }
|
||||||
@@ -52,6 +54,7 @@ binds {
|
|||||||
Mod+Return { spawn "alacritty"; }
|
Mod+Return { spawn "alacritty"; }
|
||||||
Mod+D { spawn "wofi" "--show" "drun"; }
|
Mod+D { spawn "wofi" "--show" "drun"; }
|
||||||
Mod+E { spawn "thunar"; }
|
Mod+E { spawn "thunar"; }
|
||||||
|
Mod+Shift+B { spawn "waybar-restart"; }
|
||||||
|
|
||||||
Mod+H { focus-column-left; }
|
Mod+H { focus-column-left; }
|
||||||
Mod+L { focus-column-right; }
|
Mod+L { focus-column-right; }
|
||||||
@@ -93,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%-"; }
|
||||||
@@ -122,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$"
|
||||||
@@ -147,3 +150,9 @@ window-rule {
|
|||||||
off
|
off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thunar file-operation progress dialog
|
||||||
|
window-rule {
|
||||||
|
match app-id="^thunar$" title="^File Operation Progress$"
|
||||||
|
open-floating true
|
||||||
|
}
|
||||||
|
|||||||
+2
-3
@@ -3,7 +3,7 @@ waybar
|
|||||||
wofi
|
wofi
|
||||||
mako
|
mako
|
||||||
alacritty
|
alacritty
|
||||||
swww
|
awww
|
||||||
grim
|
grim
|
||||||
slurp
|
slurp
|
||||||
swappy
|
swappy
|
||||||
@@ -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
|
||||||
@@ -38,3 +36,4 @@ xwayland-satellite
|
|||||||
power-profiles-daemon
|
power-profiles-daemon
|
||||||
fw-fanctrl
|
fw-fanctrl
|
||||||
satty
|
satty
|
||||||
|
starship
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
waybar &
|
|
||||||
mako &
|
|
||||||
swww-daemon &
|
|
||||||
nm-applet --indicator &
|
|
||||||
polkit-gnome-authentication-agent-1 &
|
|
||||||
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"
|
||||||
+39
-5
@@ -1,17 +1,51 @@
|
|||||||
#!/bin/bash
|
#!/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
|
if [ "$1" = "--menu" ]; then
|
||||||
CHOICE=$(fw-fanctrl print list 2>/dev/null \
|
STRATS=$(fw-fanctrl print list 2>/dev/null | grep "^-" | sed 's/^- //')
|
||||||
| grep "^-" \
|
CHOICE=$(printf 'auto\n%s\n' "$STRATS" \
|
||||||
| sed 's/^- //' \
|
|
||||||
| wofi --dmenu \
|
| wofi --dmenu \
|
||||||
--prompt "Fan Strategy:" \
|
--prompt "Fan Strategy:" \
|
||||||
--width 260 \
|
--width 260 \
|
||||||
--height 300 \
|
--height 320 \
|
||||||
--hide-scroll \
|
--hide-scroll \
|
||||||
--no-actions \
|
--no-actions \
|
||||||
--insensitive)
|
--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
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+6
-38
@@ -1,43 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
FILE="$HOME/Pictures/screenshot-$(date +%s).png"
|
FILE="$HOME/Pictures/screenshot-$(date +%s).png"
|
||||||
|
|
||||||
mkdir -p "$HOME/Pictures"
|
mkdir -p "$HOME/Pictures"
|
||||||
|
|
||||||
# Exit silently if the user cancels region selection
|
REGION=$(slurp) || exit 0
|
||||||
if ! grim -g "$(slurp)" "$FILE"; then
|
grim -g "$REGION" - | satty \
|
||||||
exit 0
|
--filename - \
|
||||||
fi
|
--output-filename "$FILE" \
|
||||||
|
--copy-command wl-copy \
|
||||||
wl-copy < "$FILE"
|
--early-exit
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
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
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
pkill -x waybar
|
||||||
|
while pgrep -x waybar >/dev/null; do sleep 0.05; done
|
||||||
|
setsid -f waybar </dev/null >/dev/null 2>&1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[Sleep]
|
||||||
|
HibernateDelaySec=30min
|
||||||
+24
-22
@@ -5,14 +5,16 @@
|
|||||||
|
|
||||||
"modules-left": ["niri/workspaces"],
|
"modules-left": ["niri/workspaces"],
|
||||||
"modules-center": ["custom/clock"],
|
"modules-center": ["custom/clock"],
|
||||||
"modules-right": ["cpu", "temperature", "custom/power-profile", "custom/fan-profile", "pulseaudio", "network", "battery", "custom/mouse-battery", "tray"],
|
"modules-right": [
|
||||||
"custom/mouse-battery": {
|
"cpu",
|
||||||
"exec": "~/.config/waybar/mouse-battery.sh",
|
"temperature",
|
||||||
"interval": 60,
|
"custom/power-profile",
|
||||||
"format": " {output}",
|
"custom/fan-profile",
|
||||||
"tooltip": false
|
"pulseaudio",
|
||||||
},
|
"network",
|
||||||
|
"battery",
|
||||||
|
"tray",
|
||||||
|
],
|
||||||
"cpu": {
|
"cpu": {
|
||||||
"format": " {usage}%",
|
"format": " {usage}%",
|
||||||
"format-tooltip": "{usage}% total\n{icon0} {icon1} {icon2} {icon3}\n{icon4} {icon5} {icon6} {icon7}\n{icon8} {icon9} {icon10} {icon11}",
|
"format-tooltip": "{usage}% total\n{icon0} {icon1} {icon2} {icon3}\n{icon4} {icon5} {icon6} {icon7}\n{icon8} {icon9} {icon10} {icon11}",
|
||||||
@@ -20,8 +22,8 @@
|
|||||||
"interval": 3,
|
"interval": 3,
|
||||||
"states": {
|
"states": {
|
||||||
"warning": 60,
|
"warning": 60,
|
||||||
"critical": 85
|
"critical": 85,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
"temperature": {
|
"temperature": {
|
||||||
@@ -31,8 +33,8 @@
|
|||||||
"format-critical": " {temperatureC}°C",
|
"format-critical": " {temperatureC}°C",
|
||||||
"states": {
|
"states": {
|
||||||
"warm": 60,
|
"warm": 60,
|
||||||
"critical": 80
|
"critical": 80,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
"custom/power-profile": {
|
"custom/power-profile": {
|
||||||
@@ -40,7 +42,7 @@
|
|||||||
"return-type": "json",
|
"return-type": "json",
|
||||||
"interval": 30,
|
"interval": 30,
|
||||||
"signal": 8,
|
"signal": 8,
|
||||||
"on-click": "~/.local/bin/power-profile --menu"
|
"on-click": "~/.local/bin/power-profile --menu",
|
||||||
},
|
},
|
||||||
|
|
||||||
"custom/fan-profile": {
|
"custom/fan-profile": {
|
||||||
@@ -48,14 +50,14 @@
|
|||||||
"return-type": "json",
|
"return-type": "json",
|
||||||
"interval": 5,
|
"interval": 5,
|
||||||
"signal": 9,
|
"signal": 9,
|
||||||
"on-click": "~/.local/bin/fan-profile --menu"
|
"on-click": "~/.local/bin/fan-profile --menu",
|
||||||
},
|
},
|
||||||
|
|
||||||
"custom/clock": {
|
"custom/clock": {
|
||||||
"exec": "date '+%a %b %d %H:%M'",
|
"exec": "date '+%a %b %d %-I:%M %p'",
|
||||||
"interval": 30,
|
"interval": 30,
|
||||||
"format": "{}",
|
"format": "{}",
|
||||||
"tooltip": false
|
"tooltip": false,
|
||||||
},
|
},
|
||||||
|
|
||||||
"battery": {
|
"battery": {
|
||||||
@@ -65,23 +67,23 @@
|
|||||||
"format-icons": ["", "", "", "", ""],
|
"format-icons": ["", "", "", "", ""],
|
||||||
"states": {
|
"states": {
|
||||||
"warning": 30,
|
"warning": 30,
|
||||||
"critical": 15
|
"critical": 15,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
"tray": {
|
"tray": {
|
||||||
"spacing": 8
|
"spacing": 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
"network": {
|
"network": {
|
||||||
"format-wifi": " {essid}",
|
"format-wifi": " {essid}",
|
||||||
"format-ethernet": "",
|
"format-ethernet": "",
|
||||||
"format-disconnected": "⚠",
|
"format-disconnected": "⚠",
|
||||||
"on-click": "networkmanager_dmenu"
|
"on-click": "networkmanager_dmenu",
|
||||||
},
|
},
|
||||||
|
|
||||||
"pulseaudio": {
|
"pulseaudio": {
|
||||||
"format": " {volume}%",
|
"format": " {volume}%",
|
||||||
"format-muted": ""
|
"format-muted": "",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Generic mouse battery script for Waybar
|
|
||||||
|
|
||||||
MOUSE_BATTERY=$(upower -e | grep -i mouse | head -n1)
|
|
||||||
|
|
||||||
if [ -n "$MOUSE_BATTERY" ]; then
|
|
||||||
upower -i "$MOUSE_BATTERY" | awk '/percentage:/ { print $2 }'
|
|
||||||
else
|
|
||||||
echo "N/A"
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user