diff --git a/docs/superpowers/specs/2026-05-01-fan-profile-auto-design.md b/docs/superpowers/specs/2026-05-01-fan-profile-auto-design.md new file mode 100644 index 0000000..e8852f3 --- /dev/null +++ b/docs/superpowers/specs/2026-05-01-fan-profile-auto-design.md @@ -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 `, 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 ` once. +3. Signals waybar to refresh. + +Selecting any non-`auto` strategy: + +1. Removes the state file (if it exists). +2. Calls `fw-fanctrl use ` (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 +│ │ └── matches → no-op +│ │ emit JSON: text="auto", tooltip="auto → ", 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).