Files
dotfiles/docs/superpowers/plans/2026-05-01-fan-profile-auto.md
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

13 KiB
Raw Permalink Blame History

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:

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:

#!/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 -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 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
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:

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:

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 -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:

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:

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:

rm -f "${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
  • Step 4: Commit
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):

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:

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:

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:

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):

{"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:

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:

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:

{"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)
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:

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):

waybar-restart

Then enable auto mode without going through wofi:

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:

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
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.