# 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": "󰈐 ", "tooltip": "Fan strategy: \nSpeed: %\nClick to change", "class": ""}`. - [ ] **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: `. 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.