gamemode: global auto-trigger via niri window watch
Add scripts/gamemode-watch.py, launched at niri startup, which subscribes to niri's event stream and toggles gamemode whenever a game window (steam_app_*, RuneLite, gamescope) is open — so Steam games trigger gamemode with zero per-game setup instead of needing gamemoderun %command%. 'winwatch' is a third reference-counted source alongside feral + manual. Pure Watcher class is unit-tested; SIGTERM/finally guarantee cleanup so the watcher stopping never leaves gamemode stuck on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,16 +8,24 @@
|
||||
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:
|
||||
`scripts/gamemode-session.sh`, is the whole engine. It is driven by **three
|
||||
independent sources** and reference-counted, so the 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.
|
||||
- **Window watch (`winwatch`)** — `scripts/gamemode-watch.py`, launched at niri
|
||||
startup, subscribes to niri's event stream and toggles gamemode whenever a game
|
||||
window (app_id matching `steam_app_*`, RuneLite, or gamescope) is open. This makes
|
||||
Steam games automatic with **zero per-game setup** — the primary trigger.
|
||||
- **Feral (`feral`)** — Feral `gamemoded` custom start/end hooks, for
|
||||
gamemode-aware launchers run with `gamemoderun` that the window watch may not match.
|
||||
- **Manual (`manual`)** — a `Mod+G` niri keybind, for anything else.
|
||||
|
||||
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.
|
||||
|
||||
> **Addendum (2026-07-15):** The window-watch source was added after the initial
|
||||
> design so Steam games trigger gamemode globally instead of needing
|
||||
> `gamemoderun %command%` per game. See "Window watch" below.
|
||||
|
||||
## Current Behaviour
|
||||
|
||||
- **Compositor:** niri 26.04, single internal panel `eDP-1` (BOE, 2256×1504@60,
|
||||
@@ -174,22 +182,47 @@ Mod+G { spawn "gamemode-session" "toggle"; }
|
||||
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.
|
||||
|
||||
### 5. `scripts/gamemode-watch.py` (new) — window watch
|
||||
|
||||
A Python daemon (Python already used by the theme generator) launched via
|
||||
`spawn-at-startup "gamemode-watch"`. It runs `niri msg --json event-stream` and
|
||||
maintains the set of open **game** windows:
|
||||
|
||||
- `is_game(app_id)` matches `steam_app_` (substring), the RuneLite ids
|
||||
(`net-runelite-client-RuneLite|runelite|RuneLite`), and `gamescope`
|
||||
(case-insensitive) — mirrors the niri window rules. Bolt's *launcher* window is
|
||||
deliberately excluded (it would trigger before you're in-game).
|
||||
- `WindowsChanged` (connect snapshot) rebuilds the set; `WindowOpenedOrChanged`
|
||||
adds/removes by app_id; `WindowClosed` (id only) discards by id.
|
||||
- On empty→non-empty it calls `gamemode-session add winwatch`; non-empty→empty,
|
||||
`del winwatch`.
|
||||
- Event logic is a pure `Watcher` class (injected `emit`) so it unit-tests without
|
||||
niri or subprocesses (`scripts/tests/gamemode-watch.test.py`).
|
||||
- Robustness: reconnects if the stream drops while niri is alive; a SIGTERM handler
|
||||
and a `finally` guarantee `del winwatch` on exit, so the watcher stopping can
|
||||
never leave gamemode stuck on.
|
||||
|
||||
## Files Touched
|
||||
|
||||
- `scripts/gamemode-session.sh` — **new**, the engine.
|
||||
- `mako/config` — **new**, adds the `do-not-disturb` mode (plus current settings).
|
||||
- `scripts/gamemode-watch.py` — **new**, niri-event-stream window watch (primary
|
||||
auto-trigger). Tested by `scripts/tests/gamemode-watch.test.py`.
|
||||
- `mako/config` — **new**, adds the `do-not-disturb` mode (plus current settings);
|
||||
also un-ignored in `.gitignore` (was previously untracked by choice).
|
||||
- `gamemode.ini` — **new**, 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).
|
||||
- `niri/config.kdl` — add `Mod+G` bind; `spawn-at-startup "gamemode-watch"`; remove
|
||||
3 dead VRR rules; add one comment.
|
||||
- `install.sh` — link `gamemode-session`, `gamemode-watch`, `gamemode.ini`, and
|
||||
`mako/config` (the last was never linked before).
|
||||
|
||||
## 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.
|
||||
- **Steam / RuneLite / gamescope games:** nothing to do — the window watch turns
|
||||
gamemode on when a game window opens and off when the last one closes.
|
||||
- **Gamemode-aware launchers** (Lutris/Heroic via `gamemoderun`): covered by the
|
||||
Feral hooks even if their window app_id isn't matched.
|
||||
- **Anything else / manual override:** `Mod+G` to toggle.
|
||||
- All three converge on `gamemode-session`; overlapping is safe (reference-counted).
|
||||
|
||||
## Out of Scope (YAGNI)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user