Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.2 KiB
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: runsfw-fanctrl print list, pipes the strategy names through wofi, callsfw-fanctrl use <choice>, signalspkill -RTMIN+9 waybarso 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:
- Touches the state file
~/.local/state/fan-profile-auto. - Resolves the current power profile and calls
fw-fanctrl use <mapped>once. - Signals waybar to refresh.
Selecting any non-auto strategy:
- Removes the state file (if it exists).
- Calls
fw-fanctrl use <choice>(current behaviour, unchanged). - 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:
-
State path constant at top:
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto" -
Mapping function:
map_profile_to_strategy() { case "$1" in power-saver) echo lazy ;; balanced) echo medium ;; performance) echo agile ;; *) return 1 ;; esac } -
Menu branch — prepend
autoand route the choice: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 -
Status branch — wrap the existing emit with auto-mode handling:
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; theautotext in the bar is the differentiator. - Tracking
/etc/fw-fanctrl/config.jsonin the dotfiles repo (raised during clarifications, not adopted — auto doesn't need any new fw-fanctrl strategies).