Files
dotfiles/docs/superpowers/plans/2026-05-01-fan-profile-auto.md
T
funman300 7a5a276efd docs: track implementation plans for fan-profile auto and flameshot
Both plans were generated during their respective implementation
runs but never committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:18:31 -07:00

374 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fan-Profile "Auto" Mode Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a virtual `auto` strategy to `scripts/fan-profile.sh` that mirrors the active power-profiles-daemon profile to a corresponding `fw-fanctrl` strategy, reconciling on every waybar refresh.
**Architecture:** Three additions to a single bash script — a state-file constant, a profile→strategy mapping function, and two new conditional branches (one in the `--menu` handler, one in the status emit). State lives in `~/.local/state/fan-profile-auto`. No new files, no new daemons, no fw-fanctrl config edits.
**Tech Stack:** bash, `fw-fanctrl`, `powerprofilesctl`, wofi, waybar.
**Spec:** `docs/superpowers/specs/2026-05-01-fan-profile-auto-design.md`
**Verification model:** Bash script — no unit-test framework. Each task ends with manual `bash scripts/fan-profile.sh` invocations under representative state (auto-on / auto-off / power-profile mismatch / `powerprofilesctl` failure simulated by overriding `PATH`), inspecting JSON output and side-effects on `fw-fanctrl`.
**Mapping (locked from spec):**
| power-profiles-daemon | fw-fanctrl |
|---|---|
| `power-saver` | `lazy` |
| `balanced` | `medium` |
| `performance` | `agile` |
---
## File Structure
- **Modify:** `scripts/fan-profile.sh` — add a constant, a function, and modify two existing branches.
No other files.
---
## Task 1: Add state-file constant and mapping function
After this task, the script has a place to store auto-mode state and a pure function to map a power profile to a fan strategy. No behavior change yet — these are unused until Tasks 2 and 3 wire them in.
**Files:**
- Modify: `scripts/fan-profile.sh` (insert after the `#!/bin/bash` shebang on line 1)
- [ ] **Step 1: Add the constant and function at the top of the script**
Open `scripts/fan-profile.sh`. After the shebang line (`#!/bin/bash`) and the blank line that follows, insert:
```bash
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
map_profile_to_strategy() {
case "$1" in
power-saver) echo lazy ;;
balanced) echo medium ;;
performance) echo agile ;;
*) return 1 ;;
esac
}
```
The script's first 4 lines should now read:
```bash
#!/bin/bash
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
map_profile_to_strategy() {
```
- [ ] **Step 2: Sanity-check the function**
Run:
```bash
bash -c 'source scripts/fan-profile.sh; map_profile_to_strategy power-saver; map_profile_to_strategy balanced; map_profile_to_strategy performance; map_profile_to_strategy bogus; echo "exit=$?"'
```
Expected output:
```
lazy
medium
agile
exit=1
```
(`bogus` produces no stdout and exits 1; the other three each print one strategy name on its own line.)
Note: sourcing the script will also execute the rest of it (the status emit). That's fine — its output gets mixed in. Only inspect that the four lines above appear in the right order somewhere.
- [ ] **Step 3: Verify status emit still works**
Run the existing status path:
```bash
bash scripts/fan-profile.sh
```
Expected: a single JSON line like `{"text": "󰈐 medium", "tooltip": "Fan strategy: medium\nSpeed: 28%\nClick to change", "class": "medium"}` — same shape as before. The script's behavior is unchanged because nothing references `STATE_FILE` or `map_profile_to_strategy` yet.
- [ ] **Step 4: Commit**
```bash
git add scripts/fan-profile.sh
git commit -m "fan-profile: add state path and profile→strategy mapping (unused yet)"
```
---
## Task 2: Wire `auto` into the wofi menu
After this task, picking `auto` from the menu enables auto mode (creates the state file and applies the mapped strategy immediately). Picking any other strategy disables auto mode (deletes the state file). The status emit still behaves as before — auto-mode rendering comes in Task 3.
**Files:**
- Modify: `scripts/fan-profile.sh` — replace the existing `--menu` block (currently lines ~316 of the post-Task-1 file) with the version below.
- [ ] **Step 1: Replace the `--menu` branch**
Find the existing block:
```bash
if [ "$1" = "--menu" ]; then
CHOICE=$(fw-fanctrl print list 2>/dev/null \
| grep "^-" \
| sed 's/^- //' \
| wofi --dmenu \
--prompt "Fan Strategy:" \
--width 260 \
--height 300 \
--hide-scroll \
--no-actions \
--insensitive)
[ -n "$CHOICE" ] && fw-fanctrl use "$CHOICE" && pkill -RTMIN+9 waybar
exit
fi
```
Replace it with:
```bash
if [ "$1" = "--menu" ]; then
STRATS=$(fw-fanctrl print list 2>/dev/null | grep "^-" | sed 's/^- //')
CHOICE=$(printf 'auto\n%s\n' "$STRATS" \
| wofi --dmenu \
--prompt "Fan Strategy:" \
--width 260 \
--height 320 \
--hide-scroll \
--no-actions \
--insensitive)
[ -z "$CHOICE" ] && exit
if [ "$CHOICE" = "auto" ]; then
mkdir -p "$(dirname "$STATE_FILE")"
touch "$STATE_FILE"
PROFILE=$(powerprofilesctl get 2>/dev/null) \
&& MAPPED=$(map_profile_to_strategy "$PROFILE") \
&& fw-fanctrl use "$MAPPED"
else
rm -f "$STATE_FILE"
fw-fanctrl use "$CHOICE"
fi
pkill -RTMIN+9 waybar
exit
fi
```
Three substantive changes:
1. `auto\n` is prepended to the strategy list piped into wofi.
2. Empty selection (user cancels) now exits early instead of falling through.
3. `auto` and non-`auto` selections are routed differently — auto creates the state file and applies the mapped strategy; everything else removes the state file (if present) and uses fw-fanctrl directly.
The wofi `--height` was bumped from 300 → 320 to accommodate the extra row.
- [ ] **Step 2: Verify the menu branch in dry-run**
You can't fully exercise wofi without a display, but you can verify the strategy list construction:
```bash
bash -c 'STRATS=$(fw-fanctrl print list 2>/dev/null | grep "^-" | sed "s/^- //"); printf "auto\n%s\n" "$STRATS"'
```
Expected output (one strategy per line, `auto` first):
```
auto
laziest
lazy
medium
agile
very-agile
deaf
aeolus
```
- [ ] **Step 3: Verify state-file logic by simulating "auto" selection**
Manually run the auto branch:
```bash
mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
bash -c 'source scripts/fan-profile.sh; CHOICE=auto
if [ "$CHOICE" = "auto" ]; then
mkdir -p "$(dirname "$STATE_FILE")"
touch "$STATE_FILE"
echo "state file created at: $STATE_FILE"
fi'
ls -la "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
```
Expected: file exists.
Now simulate selecting a real strategy:
```bash
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
touch "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
bash -c 'source scripts/fan-profile.sh; CHOICE=lazy
if [ "$CHOICE" != "auto" ]; then
rm -f "$STATE_FILE"
echo "state file removed"
fi'
ls -la "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto" 2>&1
```
Expected: `cannot access ... No such file or directory` (file removed). Then clean up:
```bash
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
```
- [ ] **Step 4: Commit**
```bash
git add scripts/fan-profile.sh
git commit -m "fan-profile: route 'auto' menu choice through state file"
```
---
## Task 3: Render auto mode in the status emit
After this task, when the state file exists the status emit reconciles `fw-fanctrl` to the power profile's mapped strategy and renders `auto` in the bar. When the state file is absent, behavior is unchanged.
**Files:**
- Modify: `scripts/fan-profile.sh` — insert a new conditional block before the existing status emit (the `OUTPUT=$(fw-fanctrl print 2>/dev/null)` line and everything after it).
- [ ] **Step 1: Insert the auto-mode status block**
Find this section in the script (it's the part that runs when no `--menu` arg was passed):
```bash
OUTPUT=$(fw-fanctrl print 2>/dev/null)
CURRENT=$(echo "$OUTPUT" | awk -F"'" '/^Strategy:/{print $2}')
SPEED=$(echo "$OUTPUT" | awk '/^Speed:/{print $2}')
```
Immediately *before* that `OUTPUT=` line, insert:
```bash
if [ -f "$STATE_FILE" ]; then
PROFILE=$(powerprofilesctl get 2>/dev/null)
MAPPED=$(map_profile_to_strategy "$PROFILE" 2>/dev/null)
if [ -n "$MAPPED" ]; then
ACTIVE=$(fw-fanctrl print 2>/dev/null | awk -F"'" '/^Strategy:/{print $2}')
[ "$ACTIVE" != "$MAPPED" ] && fw-fanctrl use "$MAPPED" >/dev/null 2>&1
printf '{"text": "󰈐 auto", "tooltip": "Auto fan strategy\\nProfile: %s → %s\\nClick to change", "class": "auto"}\n' "$PROFILE" "$MAPPED"
else
printf '{"text": "󰈐 auto (?)", "tooltip": "Auto: power-profiles-daemon unreachable or unknown profile\\nClick to change", "class": "auto"}\n'
fi
exit
fi
```
(Trailing blank line included so there's separation between the new block and the existing `OUTPUT=` line.)
- [ ] **Step 2: Verify auto-off path is unchanged**
Make sure the state file does NOT exist:
```bash
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
bash scripts/fan-profile.sh
```
Expected: same JSON as before Task 1 — `{"text": "󰈐 <strategy>", "tooltip": "Fan strategy: <strategy>\nSpeed: <pct>%\nClick to change", "class": "<level>"}`.
- [ ] **Step 3: Verify auto-on happy path**
Create the state file and run the script:
```bash
mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"
touch "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
bash scripts/fan-profile.sh
```
Expected output (assuming `powerprofilesctl get` returns a known profile, e.g. `balanced`):
```json
{"text": "󰈐 auto", "tooltip": "Auto fan strategy\nProfile: balanced → medium\nClick to change", "class": "auto"}
```
Also verify the side-effect — `fw-fanctrl` should now be on the mapped strategy:
```bash
fw-fanctrl print | grep '^Strategy:'
```
Expected: `Strategy: 'medium'` (or `lazy`/`agile` depending on your active power profile).
- [ ] **Step 4: Verify auto-on error path (unknown profile)**
Simulate `powerprofilesctl` returning something the mapping doesn't know. We do this by stubbing `powerprofilesctl` via `PATH`:
```bash
mkdir -p /tmp/fan-stub
cat >/tmp/fan-stub/powerprofilesctl <<'STUB'
#!/bin/bash
echo bogus-profile
STUB
chmod +x /tmp/fan-stub/powerprofilesctl
PATH=/tmp/fan-stub:$PATH bash scripts/fan-profile.sh
```
Expected:
```json
{"text": "󰈐 auto (?)", "tooltip": "Auto: power-profiles-daemon unreachable or unknown profile\nClick to change", "class": "auto"}
```
- [ ] **Step 5: Verify auto-on error path (powerprofilesctl missing)**
```bash
cat >/tmp/fan-stub/powerprofilesctl <<'STUB'
#!/bin/bash
exit 1
STUB
PATH=/tmp/fan-stub:$PATH bash scripts/fan-profile.sh
```
Expected: same `auto (?)` output as above (since `PROFILE` ends up empty, `map_profile_to_strategy` returns 1, `MAPPED` is empty, error branch fires).
Clean up:
```bash
rm -rf /tmp/fan-stub
rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
```
- [ ] **Step 6: Reload waybar to confirm end-to-end**
Restart waybar so it picks up the script changes (the script file itself is loaded each invocation, so this is mostly to refresh the bar widget visually):
```bash
waybar-restart
```
Then enable auto mode without going through wofi:
```bash
mkdir -p "${XDG_STATE_HOME:-$HOME/.local/state}"
touch "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
pkill -RTMIN+9 waybar
```
The bar's fan-profile widget should now read `auto` (within ~5 s — the next waybar poll). Hover the widget; the tooltip should show `Profile: <name> → <strategy>`. Switch your power profile (e.g. `powerprofilesctl set performance`) and within ~5 s the tooltip should update to `Profile: performance → agile` and `fw-fanctrl print` should confirm the strategy switched.
To turn auto off and verify the original behaviour returns:
```bash
rm "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
pkill -RTMIN+9 waybar
```
The bar should revert to showing the underlying strategy name.
- [ ] **Step 7: Commit**
```bash
git add scripts/fan-profile.sh
git commit -m "fan-profile: render auto mode and reconcile fw-fanctrl on poll"
```
---
## Final verification
- [ ] **Auto persists across waybar restarts.** With the state file present, run `waybar-restart`. The bar should come back showing `auto` (within 5 s of restart).
- [ ] **Auto persists across reboot.** Optional but the design promises it. The state file lives in `~/.local/state/` which survives reboot.
- [ ] **Wofi menu shows `auto` first.** Click the fan-profile widget; the wofi menu should list `auto` at the top followed by the seven fw-fanctrl strategies.
- [ ] **Selecting `auto` from the menu works end-to-end.** Pick `auto` → bar updates to `auto` within seconds, `fw-fanctrl print` shows the mapped strategy.
- [ ] **Selecting a non-auto strategy from the menu disables auto.** Pick e.g. `aeolus` → bar updates to `aeolus`, state file is gone (`ls ~/.local/state/fan-profile-auto` returns "No such file or directory"), and subsequent power-profile changes do NOT swap the fan strategy automatically.