Files
dotfiles/docs/superpowers/specs/2026-07-15-niri-gamemode-design.md
T
funman300 5b24b21610 Spec: niri gamemode (idle inhibit, DND, perf profile, hide waybar)
Hybrid trigger (Feral gamemode hooks + Mod+G toggle), reference-counted
so overlapping sources apply once and revert once. Also removes three
dead VRR window-rules (panel has no VRR support).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:23:34 -07:00

9.4 KiB
Raw Blame History

Niri Gamemode Design

Date: 2026-07-15 Status: Approved

Summary

Add a compositor-level "gamemode": while a game is running, inhibit idle/blanking, silence notifications, switch to the performance power profile (and bump the fan), and hide waybar — then revert everything cleanly when the game ends. One script, scripts/gamemode-session.sh, is the whole engine. It is driven by two independent sources and reference-counted, so the two triggers can overlap safely:

  • Automatic — Feral gamemoded custom start/end hooks (any game launched with gamemoderun).
  • Manual — a Mod+G niri keybind, for games that don't call gamemode.

Effects apply on the first activation (0→1 active sources) and revert on the last deactivation (1→0). Also removes three dead VRR window-rules found during review.

Current Behaviour

  • Compositor: niri 26.04, single internal panel eDP-1 (BOE, 2256×1504@60, VRR not supported). Config at niri/config.kdl.
  • Idle: hypridle blanks the panel at 5 min, locks at 10 min, suspend-then- hibernate at 30 min. It honors idle inhibitors (ignore_dbus_inhibit = false, ignore_systemd_inhibit = false). A controller/turn-based game with no kbd/mouse input therefore goes dark at 5 minutes.
  • Notifications: mako, currently with no config file (running on defaults) — so toasts pop over fullscreen games and there is no mode to suppress them.
  • Power: power-profiles-daemon (AMD pstate); active profile read/set via powerprofilesctl, waybar module refreshed with pkill -RTMIN+8 waybar (and -RTMIN+9). Fan via fw-fanctrl with a virtual auto mode that maps profile→strategy (performanceagile) when $XDG_STATE_HOME/fan-profile-auto exists.
  • waybar: launched directly (spawn-at-startup "waybar"); waybar-restart script exists.
  • Feral gamemode: gamemoded installed, no gamemode.ini → no custom hooks.
  • Script convention: scripts/<name>.sh is symlinked to ~/.local/bin/<name> (no .sh in the bin name), e.g. power-profile, fan-profile, screenshot.

Review findings (delivered separately, cleanup folded into this spec)

  • 🔴 Three VRR rules are dead code. The panel doesn't support VRR, so all three variable-refresh-rate true rules are no-ops. The match is-focused=true one is also conceptually wrong (it targets every focused window, not games).
  • 🟠 Idle blanks mid-game (the 5-min issue above) — the main problem gamemode fixes.
  • 🟡 Notifications interrupt fullscreen.
  • 🟢 match app-id="steam_app_" is an unanchored regex (looser than the other anchored rules) — noted, left as-is.

Target Behaviour

Sources and reference counting

State lives in a runtime dir ${XDG_RUNTIME_DIR}/gamemode/. A "source" is either feral or manual, represented by a marker file (src.feral, src.manual). Gamemode is active iff at least one source marker exists.

add <source>:
  was_active = (any src.* exists)
  touch src.<source>
  if not was_active:  snapshot baseline + apply effects

del <source>:
  rm -f src.<source>
  if no src.* remain:  read baseline + revert effects, clear state dir

toggle:            # what Mod+G calls
  if src.manual exists → del manual   else → add manual

status:            # for a future waybar module; prints active|inactive, exit 0/1

Applying only on 0→1 and reverting only on 1→0 makes overlap safe: launch a Steam game (feral add) and hit Mod+G (manual add), and effects apply once; effects revert only when both are gone. Idempotent and crash-tolerant — stale markers are harmless, and baseline is re-snapshotted only when transitioning from zero sources.

Baseline snapshot (captured on 0→1)

Written into the state dir so revert restores exactly what was there:

  • baseline.profilepowerprofilesctl get at entry.
  • baseline.waybar1 if waybar was running at entry, else 0.
  • baseline.fanauto1 if $XDG_STATE_HOME/fan-profile-auto exists at entry.
  • idle.pid — PID of the idle-inhibit process (written when it launches).

Effects

Effect Enter Revert
Idle inhibit systemd-inhibit --what=idle --who=gamemode --why="gaming session" --mode=block sleep infinity &, record PID in idle.pid. hypridle honors systemd idle inhibitors, so its blank/lock/hibernate timers stop. kill the recorded PID; remove idle.pid.
Do-not-disturb makoctl mode -a do-not-disturb (requires a [mode=do-not-disturb] block — see mako config below). makoctl mode -r do-not-disturb.
Performance profile if baseline.profile != performance: powerprofilesctl set performance, then pkill -RTMIN+8 waybar. If baseline.fanauto=1: fw-fanctrl use agile. restore baseline.profile (only if we changed it), pkill -RTMIN+8 waybar; if baseline.fanauto=1 remap fan to match the restored profile (fan-profile's existing auto poll also reconciles within 5 s, so this is belt-and-suspenders).
Hide waybar if waybar running: pkill -x waybar. if baseline.waybar=1: relaunch waybar detached (setsid waybar >/dev/null 2>&1 &).

All effect steps are individually guarded (command -v, 2>/dev/null) so a missing tool degrades to a no-op rather than aborting the rest — gamemode must never wedge a session. Revert always runs every restore step regardless of individual failures.

Failure / edge cases

  • Stale markers after a crash: next add sees markers, treats itself as already-active, and won't re-snapshot — but a manual gamemode-session reset (clears the state dir and force-reverts) is provided as an escape hatch. toggle from a stale-active state will del manual and, if that empties the sources, revert — self-healing in the common case.
  • Idle PID already dead: kill failure is ignored.
  • profile unchanged at entry (already performance): we don't touch it and don't restore it, so we never clobber a deliberate user choice.

Implementation

1. scripts/gamemode-session.sh (new)

bash, mirrors the house style of the other scripts. Structure:

  • Constants: STATE_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/gamemode", FAN_AUTO="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto".
  • Helpers: is_active, snapshot_baseline, apply_effects, revert_effects, each effect as a small function.
  • Dispatch on $1: add/del (with $2 = source), toggle, status, reset.
  • Symlinked to ~/.local/bin/gamemode-session (same as every other script; add the symlink line to install.sh if it enumerates scripts, else create it manually to match the existing ones).

Named gamemode-session (not gamemode) to avoid any collision with Feral's gamemoderun/gamemoded namespace.

2. mako/config (new file in dotfiles → ~/.config/mako/config)

mako has no config today; DND needs a mode block. Fold in the current visible settings so nothing changes visually, then add the mode:

background-color=#1d1f21
text-color=#c5c8c6
border-size=2
border-color=#81a2be
default-timeout=4000

[mode=do-not-disturb]
invisible=1

invisible=1 hides new notifications while the mode is active; removing the mode restores normal display. (History is retained — nothing is lost, just not shown.)

3. gamemode.ini (new file in dotfiles → ~/.config/gamemode.ini)

[custom]
start=/home/alex/.local/bin/gamemode-session add feral
end=/home/alex/.local/bin/gamemode-session del feral

Absolute path because gamemoded's exec environment is minimal. No other gamemode tuning (Feral's defaults already set the CPU governor).

4. niri/config.kdl (edited)

Add the keybind (near the other spawn binds):

Mod+G { spawn "gamemode-session" "toggle"; }

Remove the three dead VRR rules:

  • the variable-refresh-rate true line inside the steam_app_ rule,
  • the entire match is-focused=true { variable-refresh-rate true } rule.

Leave a one-line comment on the steam_app_ rule noting to re-add variable-refresh-rate true if a VRR-capable external monitor is ever attached.

Files Touched

  • scripts/gamemode-session.shnew, the engine.
  • mako/confignew, adds the do-not-disturb mode (plus current settings).
  • gamemode.ininew, Feral start/end hooks.
  • niri/config.kdl — add Mod+G bind; remove 3 dead VRR rules; add one comment.
  • install.sh — add the gamemode-session symlink + gamemode.ini/mako/config placement, matching how existing scripts/configs are linked (or create the symlink manually if install.sh doesn't enumerate).

Usage

  • Steam: set launch options to gamemoderun %command% (per-game or as a global default) → fully automatic enter/revert.
  • Other games / anything gamemode doesn't catch: Mod+G to toggle.
  • Both converge on gamemode-session; overlapping is safe.

Out of Scope (YAGNI)

  • gamescope wrapper / upscaling / frame limiting — separate concern; can be layered into a game's launch command independently.
  • Per-game profiles — one uniform gamemode for now.
  • waybar gamemode indicator — the status subcommand is provided so a custom/gamemode module can be added later, but no module ships now.
  • Cross-machine sync of gamemode state — state is per-boot runtime only, by design.
  • Touching VRR behaviour for external monitors — left as a comment breadcrumb; no rule added until such a monitor exists.