Plan: niri gamemode implementation (6 tasks, TDD)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,563 @@
|
||||
# Niri Gamemode 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:** A compositor-level gamemode for niri that inhibits idle, silences notifications, switches to the performance power profile (and bumps the fan), and hides waybar while a game runs — reverting cleanly when it ends — driven by both Feral gamemode hooks and a `Mod+G` toggle.
|
||||
|
||||
**Architecture:** A single reference-counted bash script, `scripts/gamemode-session.sh`, tracks two "sources" (`feral`, `manual`) via marker files in a runtime state dir. It applies effects only on the first active source (0→1) and reverts only on the last (1→0), snapshotting the pre-game baseline so revert restores exactly what was there. Feral's `gamemode.ini` custom hooks call `add feral`/`del feral`; a niri keybind calls `toggle` (flips `manual`).
|
||||
|
||||
**Tech Stack:** bash, `systemd-inhibit`, `makoctl`, `powerprofilesctl`, `fw-fanctrl`, niri KDL config, Feral gamemode (`gamemoded`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Script naming: `scripts/<name>.sh` symlinked to `~/.local/bin/<name>` (no `.sh` in the bin name). New script is `scripts/gamemode-session.sh` → `~/.local/bin/gamemode-session`. Name is `gamemode-session`, NOT `gamemode` (avoids Feral's `gamemoderun`/`gamemoded` namespace).
|
||||
- Script must be crash-tolerant and never wedge the session: every effect step is individually guarded; a missing tool or a failed sub-step degrades to a no-op, and revert always runs every restore step.
|
||||
- Effects apply ONLY on 0→1 active sources and revert ONLY on 1→0. Never re-snapshot baseline while already active.
|
||||
- Power profile: only change it if the baseline profile differs from `performance`; on revert, only restore if we changed it (never clobber a deliberate `performance` choice).
|
||||
- All new dotfiles are tracked in `~/Documents/dotfiles` (this repo) and deployed to their live locations by symlink/copy exactly as existing configs are.
|
||||
- Commit after each task. Co-author trailer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
|
||||
- Verified environment facts: `makoctl mode -a/-r <mode>` and `systemd-inhibit --what=idle` are available; hypridle already honors systemd idle inhibitors (`ignore_systemd_inhibit = false`); `setsid`, `fw-fanctrl`, `powerprofilesctl`, `pgrep`, `pkill` all present.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Reference-counted state machine (`gamemode-session.sh` core)
|
||||
|
||||
Build the script's dispatch + source refcounting + baseline snapshot, with effects behind a `GAMEMODE_DRYRUN` seam so the state machine is testable without touching the live session. Effect *bodies* are stubbed in this task (dryrun-logged placeholders); Task 2 fills in the real commands.
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/gamemode-session.sh`
|
||||
- Test: `scripts/tests/gamemode-session.test.sh` (new throwaway test harness, committed alongside)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: CLI `gamemode-session {add <source>|del <source>|toggle|status|reset}`.
|
||||
- `add <src>`: touch `src.<src>` marker; on 0→1 snapshot baseline + `apply_effects`.
|
||||
- `del <src>`: remove `src.<src>`; on 1→0 `revert_effects` + clear baseline/idle files.
|
||||
- `toggle`: flip the `manual` source.
|
||||
- `status`: `active` (exit 0) / `inactive` (exit 1).
|
||||
- `reset`: force-revert + wipe state dir.
|
||||
- Env seams (consumed by the test): `GAMEMODE_STATE_DIR` overrides the state dir; `GAMEMODE_DRYRUN=1` makes `run()` and effect functions log instead of act.
|
||||
- Produces for later tasks: `apply_effects`/`revert_effects` and per-effect functions `effect_idle_on/off`, `effect_dnd_on/off`, `effect_perf_on/off`, `effect_waybar_on/off` (Task 2 replaces their bodies).
|
||||
|
||||
- [ ] **Step 1: Write the failing test harness**
|
||||
|
||||
Create `scripts/tests/gamemode-session.test.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Drives gamemode-session in dryrun against a temp state dir and asserts the
|
||||
# reference-counting state machine. No live effects run.
|
||||
set -u
|
||||
HERE=$(cd "$(dirname "$0")/.." && pwd)
|
||||
SCRIPT="$HERE/gamemode-session.sh"
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export GAMEMODE_STATE_DIR="$TMP/state"
|
||||
export GAMEMODE_DRYRUN=1
|
||||
|
||||
fail=0
|
||||
check() { # desc, actual, expected
|
||||
if [ "$2" = "$3" ]; then printf 'ok - %s\n' "$1"
|
||||
else printf 'FAIL - %s (got [%s] want [%s])\n' "$1" "$2" "$3"; fail=1; fi
|
||||
}
|
||||
active() { "$SCRIPT" status >/dev/null 2>&1 && echo active || echo inactive; }
|
||||
|
||||
check "starts inactive" "$(active)" "inactive"
|
||||
|
||||
"$SCRIPT" add feral >/dev/null 2>&1
|
||||
check "feral activates" "$(active)" "active"
|
||||
check "src.feral marker set" "$([ -e "$GAMEMODE_STATE_DIR/src.feral" ] && echo y)" "y"
|
||||
check "baseline captured" "$([ -e "$GAMEMODE_STATE_DIR/baseline.profile" ] && echo y)" "y"
|
||||
|
||||
"$SCRIPT" add manual >/dev/null 2>&1
|
||||
check "manual keeps active" "$(active)" "active"
|
||||
|
||||
"$SCRIPT" del feral >/dev/null 2>&1
|
||||
check "still active on 1 left" "$(active)" "active"
|
||||
|
||||
"$SCRIPT" del manual >/dev/null 2>&1
|
||||
check "last del deactivates" "$(active)" "inactive"
|
||||
check "baseline cleared" "$([ -e "$GAMEMODE_STATE_DIR/baseline.profile" ] && echo y || echo n)" "n"
|
||||
|
||||
"$SCRIPT" toggle >/dev/null 2>&1
|
||||
check "toggle on activates" "$(active)" "active"
|
||||
"$SCRIPT" toggle >/dev/null 2>&1
|
||||
check "toggle off deactivates" "$(active)" "inactive"
|
||||
|
||||
"$SCRIPT" reset >/dev/null 2>&1
|
||||
check "reset leaves inactive" "$(active)" "inactive"
|
||||
|
||||
exit $fail
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `bash scripts/tests/gamemode-session.test.sh`
|
||||
Expected: FAIL / non-zero (script `gamemode-session.sh` does not exist yet — `bash: .../gamemode-session.sh: No such file`).
|
||||
|
||||
- [ ] **Step 3: Write the core script**
|
||||
|
||||
Create `scripts/gamemode-session.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# gamemode-session — compositor-level gamemode for niri.
|
||||
# Reference-counted over two sources (feral, manual). Applies effects on the
|
||||
# first active source (0->1) and reverts on the last (1->0).
|
||||
# Spec: docs/superpowers/specs/2026-07-15-niri-gamemode-design.md
|
||||
set -u
|
||||
|
||||
STATE_DIR="${GAMEMODE_STATE_DIR:-${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/gamemode}"
|
||||
FAN_AUTO="${XDG_STATE_HOME:-$HOME/.local/state}/fan-profile-auto"
|
||||
DRYRUN="${GAMEMODE_DRYRUN:-0}"
|
||||
|
||||
log() { printf 'gamemode: %s\n' "$*" >&2; }
|
||||
run() { if [ "$DRYRUN" = 1 ]; then log "would: $*"; else "$@"; fi; }
|
||||
|
||||
is_active() {
|
||||
[ -n "$(find "$STATE_DIR" -maxdepth 1 -name 'src.*' -print -quit 2>/dev/null)" ]
|
||||
}
|
||||
|
||||
snapshot_baseline() {
|
||||
mkdir -p "$STATE_DIR"
|
||||
powerprofilesctl get 2>/dev/null > "$STATE_DIR/baseline.profile" || : > "$STATE_DIR/baseline.profile"
|
||||
if pgrep -x waybar >/dev/null 2>&1; then echo 1; else echo 0; fi > "$STATE_DIR/baseline.waybar"
|
||||
if [ -f "$FAN_AUTO" ]; then echo 1; else echo 0; fi > "$STATE_DIR/baseline.fanauto"
|
||||
}
|
||||
|
||||
# --- effects (bodies filled in Task 2; dryrun-safe stubs for now) -------------
|
||||
effect_idle_on() { run : "idle inhibit on"; }
|
||||
effect_idle_off() { run : "idle inhibit off"; }
|
||||
effect_dnd_on() { run : "dnd on"; }
|
||||
effect_dnd_off() { run : "dnd off"; }
|
||||
effect_perf_on() { run : "perf on"; }
|
||||
effect_perf_off() { run : "perf off"; }
|
||||
effect_waybar_on() { run : "waybar hide"; }
|
||||
effect_waybar_off(){ run : "waybar restore"; }
|
||||
|
||||
apply_effects() { effect_idle_on; effect_dnd_on; effect_perf_on; effect_waybar_on; }
|
||||
revert_effects() { effect_idle_off; effect_dnd_off; effect_perf_off; effect_waybar_off; }
|
||||
|
||||
# --- state machine ------------------------------------------------------------
|
||||
cmd_add() {
|
||||
local src="$1" was=0
|
||||
is_active && was=1
|
||||
mkdir -p "$STATE_DIR"
|
||||
touch "$STATE_DIR/src.$src"
|
||||
if [ "$was" = 0 ]; then
|
||||
snapshot_baseline
|
||||
apply_effects
|
||||
log "activated (source: $src)"
|
||||
else
|
||||
log "already active; +source: $src"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_del() {
|
||||
local src="$1"
|
||||
rm -f "$STATE_DIR/src.$src"
|
||||
if ! is_active; then
|
||||
revert_effects
|
||||
rm -f "$STATE_DIR"/baseline.* "$STATE_DIR/idle.pid"
|
||||
log "deactivated (last source: $src)"
|
||||
else
|
||||
log "still active; -source: $src"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_toggle() {
|
||||
if [ -e "$STATE_DIR/src.manual" ]; then cmd_del manual; else cmd_add manual; fi
|
||||
}
|
||||
|
||||
cmd_status() { if is_active; then echo active; exit 0; else echo inactive; exit 1; fi; }
|
||||
|
||||
cmd_reset() {
|
||||
revert_effects 2>/dev/null || true
|
||||
rm -rf "$STATE_DIR"
|
||||
log "reset"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
add) [ -n "${2:-}" ] || { log "add needs a source"; exit 2; }; cmd_add "$2" ;;
|
||||
del) [ -n "${2:-}" ] || { log "del needs a source"; exit 2; }; cmd_del "$2" ;;
|
||||
toggle) cmd_toggle ;;
|
||||
status) cmd_status ;;
|
||||
reset) cmd_reset ;;
|
||||
*) log "usage: gamemode-session {add|del <source>|toggle|status|reset}"; exit 2 ;;
|
||||
esac
|
||||
```
|
||||
|
||||
Make it executable: `chmod +x scripts/gamemode-session.sh`.
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes + shellcheck**
|
||||
|
||||
Run: `bash -n scripts/gamemode-session.sh && shellcheck -x scripts/gamemode-session.sh; bash scripts/tests/gamemode-session.test.sh`
|
||||
Expected: `bash -n` silent; shellcheck clean (or only style-level SC2086-type notes — fix any warnings); all `ok -` lines, exit 0.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Documents/dotfiles
|
||||
git add scripts/gamemode-session.sh scripts/tests/gamemode-session.test.sh
|
||||
git commit -m "gamemode-session: reference-counted state machine core
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Real effect bodies (idle / DND / power+fan / waybar)
|
||||
|
||||
Replace the eight stubbed effect functions with the real commands. The Task 1 state-machine test still passes unchanged (it runs in dryrun, so effect bodies only log).
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/gamemode-session.sh` (the eight `effect_*` functions)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `STATE_DIR`, `FAN_AUTO`, `DRYRUN`, `run()`, `log()`, and baseline files `baseline.profile`/`baseline.waybar`/`baseline.fanauto` written by `snapshot_baseline`.
|
||||
- Produces: `idle.pid` file holding the systemd-inhibit PID while active.
|
||||
|
||||
- [ ] **Step 1: Replace the effect stubs**
|
||||
|
||||
In `scripts/gamemode-session.sh`, replace the entire `# --- effects` stub block with:
|
||||
|
||||
```bash
|
||||
# --- effects ------------------------------------------------------------------
|
||||
effect_idle_on() {
|
||||
if [ "$DRYRUN" = 1 ]; then log "would: systemd-inhibit idle"; return; fi
|
||||
command -v systemd-inhibit >/dev/null 2>&1 || { log "no systemd-inhibit"; return; }
|
||||
systemd-inhibit --what=idle --who=gamemode --why="gaming session" --mode=block \
|
||||
sleep infinity >/dev/null 2>&1 &
|
||||
echo $! > "$STATE_DIR/idle.pid"
|
||||
}
|
||||
effect_idle_off() {
|
||||
local pid; pid=$(cat "$STATE_DIR/idle.pid" 2>/dev/null || echo "")
|
||||
[ -n "$pid" ] && run kill "$pid"
|
||||
rm -f "$STATE_DIR/idle.pid"
|
||||
}
|
||||
|
||||
effect_dnd_on() { command -v makoctl >/dev/null 2>&1 && run makoctl mode -a do-not-disturb; }
|
||||
effect_dnd_off() { command -v makoctl >/dev/null 2>&1 && run makoctl mode -r do-not-disturb; }
|
||||
|
||||
# maps a power profile to the fw-fanctrl strategy (mirrors fan-profile.sh)
|
||||
fan_for() { case "$1" in power-saver) echo lazy;; balanced) echo medium;; performance) echo agile;; *) return 1;; esac; }
|
||||
|
||||
effect_perf_on() {
|
||||
local base; base=$(cat "$STATE_DIR/baseline.profile" 2>/dev/null || echo "")
|
||||
if [ -n "$base" ] && [ "$base" != performance ]; then
|
||||
run powerprofilesctl set performance
|
||||
run pkill -RTMIN+8 waybar
|
||||
fi
|
||||
if [ "$(cat "$STATE_DIR/baseline.fanauto" 2>/dev/null)" = 1 ]; then
|
||||
run fw-fanctrl use agile
|
||||
fi
|
||||
}
|
||||
effect_perf_off() {
|
||||
local base; base=$(cat "$STATE_DIR/baseline.profile" 2>/dev/null || echo "")
|
||||
if [ -n "$base" ] && [ "$base" != performance ]; then
|
||||
run powerprofilesctl set "$base"
|
||||
run pkill -RTMIN+8 waybar
|
||||
fi
|
||||
if [ -n "$base" ] && [ "$(cat "$STATE_DIR/baseline.fanauto" 2>/dev/null)" = 1 ]; then
|
||||
local strat; strat=$(fan_for "$base") && run fw-fanctrl use "$strat"
|
||||
fi
|
||||
}
|
||||
|
||||
effect_waybar_on() { # hide
|
||||
pgrep -x waybar >/dev/null 2>&1 && run pkill -x waybar
|
||||
}
|
||||
effect_waybar_off() { # restore if it was running at entry
|
||||
[ "$(cat "$STATE_DIR/baseline.waybar" 2>/dev/null)" = 1 ] || return 0
|
||||
if [ "$DRYRUN" = 1 ]; then log "would: relaunch waybar"; else setsid waybar >/dev/null 2>&1 & fi
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Re-run the state-machine test (must still pass in dryrun)**
|
||||
|
||||
Run: `bash -n scripts/gamemode-session.sh && shellcheck -x scripts/gamemode-session.sh; bash scripts/tests/gamemode-session.test.sh`
|
||||
Expected: shellcheck clean; all `ok -`, exit 0. (Effects only log under dryrun, so the state machine is unaffected.)
|
||||
|
||||
- [ ] **Step 3: Dryrun effect-ordering smoke (visual check of the "would:" log)**
|
||||
|
||||
Run: `GAMEMODE_STATE_DIR=$(mktemp -d) GAMEMODE_DRYRUN=1 bash scripts/gamemode-session.sh add feral 2>&1`
|
||||
Expected output includes, in order: `would: systemd-inhibit idle`, `would: makoctl mode -a do-not-disturb`, a `would: powerprofilesctl set performance` (only if your current profile isn't already performance) and/or `would: fw-fanctrl use agile`, `would: pkill -x waybar`, then `activated (source: feral)`.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Documents/dotfiles
|
||||
git add scripts/gamemode-session.sh
|
||||
git commit -m "gamemode-session: real effects (idle, DND, perf+fan, waybar)
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: mako do-not-disturb mode config
|
||||
|
||||
mako currently runs on defaults with no config file. Add a tracked config that folds in the current visible settings and adds the `do-not-disturb` mode so `effect_dnd_*` actually suppresses toasts.
|
||||
|
||||
**Files:**
|
||||
- Create: `mako/config` (dotfiles) → deployed to `~/.config/mako/config`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `makoctl mode -a/-r do-not-disturb` from Task 2.
|
||||
- Produces: a `[mode=do-not-disturb]` block with `invisible=1`.
|
||||
|
||||
- [ ] **Step 1: Write the mako config**
|
||||
|
||||
Create `mako/config`:
|
||||
|
||||
```ini
|
||||
background-color=#1d1f21
|
||||
text-color=#c5c8c6
|
||||
border-size=2
|
||||
border-color=#81a2be
|
||||
default-timeout=4000
|
||||
|
||||
[mode=do-not-disturb]
|
||||
invisible=1
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Deploy + reload mako**
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/mako
|
||||
ln -sf ~/Documents/dotfiles/mako/config ~/.config/mako/config # match existing dotfile symlink style
|
||||
makoctl reload
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify DND suppresses, then restores**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
makoctl mode -a do-not-disturb; makoctl mode # expect list to include: do-not-disturb
|
||||
notify-send "gamemode test" "should NOT appear" # no toast while DND active
|
||||
makoctl mode -r do-not-disturb; makoctl mode # expect: default only
|
||||
notify-send "gamemode test" "SHOULD appear" # toast appears
|
||||
```
|
||||
Expected: after `-a`, `makoctl mode` lists `do-not-disturb` and the first notification does not show; after `-r`, list is back to `default` and the second notification shows. (If `notify-send` is missing, install `libnotify` or skip — the mode-list assertion is the load-bearing check.)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Documents/dotfiles
|
||||
git add mako/config
|
||||
git commit -m "mako: config with do-not-disturb mode for gamemode
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Feral gamemode hooks (`gamemode.ini`)
|
||||
|
||||
Wire Feral `gamemoded`'s custom start/end hooks to the script so gamemode-aware games trigger it automatically.
|
||||
|
||||
**Files:**
|
||||
- Create: `gamemode.ini` (dotfiles) → deployed to `~/.config/gamemode.ini`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `gamemode-session add feral` / `del feral` from Tasks 1–2.
|
||||
|
||||
- [ ] **Step 1: Write `gamemode.ini`**
|
||||
|
||||
Create `gamemode.ini`:
|
||||
|
||||
```ini
|
||||
[custom]
|
||||
start=/home/alex/.local/bin/gamemode-session add feral
|
||||
end=/home/alex/.local/bin/gamemode-session del feral
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Deploy**
|
||||
|
||||
```bash
|
||||
ln -sf ~/Documents/dotfiles/gamemode.ini ~/.config/gamemode.ini
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify Feral reads the hooks**
|
||||
|
||||
Run: `gamemoderun gamemode-session status; echo "exit=$?"`
|
||||
Expected: while `gamemoderun` holds the game session, the custom `start` hook has fired `add feral`, so `gamemode-session status` prints `active` (exit 0). When `gamemoderun` exits it fires `end` → `del feral`. Immediately after, `gamemode-session status` prints `inactive` (exit 1). (If it stays active, run `gamemode-session reset` and check `journalctl --user -u gamemoded` / the `gamemode.ini` path.)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Documents/dotfiles
|
||||
git add gamemode.ini
|
||||
git commit -m "gamemode.ini: Feral custom hooks drive gamemode-session
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: niri config — keybind + remove dead VRR rules
|
||||
|
||||
Add the `Mod+G` toggle and remove the three no-op VRR rules the review found (panel has no VRR).
|
||||
|
||||
**Files:**
|
||||
- Modify: `niri/config.kdl`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `gamemode-session toggle` from Tasks 1–2.
|
||||
|
||||
- [ ] **Step 1: Add the keybind**
|
||||
|
||||
In `niri/config.kdl`, inside the `binds { ... }` block, near the other `spawn` binds (after the `Mod+Shift+B` line), add:
|
||||
|
||||
```
|
||||
Mod+G { spawn "gamemode-session" "toggle"; }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove the dead VRR rules**
|
||||
|
||||
In `niri/config.kdl`:
|
||||
|
||||
a) In the `steam_app_` window-rule, delete the line `variable-refresh-rate true` and change the comment to note the VRR removal. The rule becomes:
|
||||
|
||||
```
|
||||
// Fullscreen games: no border, open fullscreen.
|
||||
// (Re-add `variable-refresh-rate true` here if a VRR-capable external
|
||||
// monitor is ever attached — the internal eDP-1 panel has no VRR.)
|
||||
window-rule {
|
||||
match app-id="steam_app_"
|
||||
open-fullscreen true
|
||||
border {
|
||||
off
|
||||
}
|
||||
focus-ring {
|
||||
off
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
b) Delete this entire rule (it wrongly targets every focused window and is a no-op here):
|
||||
|
||||
```
|
||||
// Generic fullscreen request (any app asking to go fullscreen)
|
||||
window-rule {
|
||||
match is-focused=true
|
||||
variable-refresh-rate true
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Validate niri config**
|
||||
|
||||
Run: `niri validate -c ~/.config/niri/config.kdl`
|
||||
Expected: `Config is valid` (or niri's success message). Fix any parse error before continuing.
|
||||
|
||||
- [ ] **Step 4: Verify the bind live (optional if in a niri session)**
|
||||
|
||||
Run: `niri msg action do-nothing 2>/dev/null; grep -n "Mod+G" ~/.config/niri/config.kdl`
|
||||
Expected: the `Mod+G` line is present. After niri reloads the config (niri hot-reloads on save), pressing `Mod+G` toggles gamemode; confirm with `gamemode-session status`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Documents/dotfiles
|
||||
git add niri/config.kdl
|
||||
git commit -m "niri: Mod+G gamemode toggle; drop dead VRR rules
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Wiring + live end-to-end smoke
|
||||
|
||||
Create the `~/.local/bin` symlink for the script (matching every other script), update `install.sh` if it enumerates them, then run one real activate/deactivate cycle and assert observable effects revert.
|
||||
|
||||
**Files:**
|
||||
- Create: symlink `~/.local/bin/gamemode-session` → `~/Documents/dotfiles/scripts/gamemode-session.sh`
|
||||
- Modify: `install.sh` (only if it lists script/config symlinks; otherwise no change)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: everything from Tasks 1–5.
|
||||
|
||||
- [ ] **Step 1: Create the bin symlink**
|
||||
|
||||
```bash
|
||||
ln -sf ~/Documents/dotfiles/scripts/gamemode-session.sh ~/.local/bin/gamemode-session
|
||||
gamemode-session status; echo "exit=$?" # expect: inactive / exit=1
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Wire install.sh if applicable**
|
||||
|
||||
Inspect `install.sh` for where existing scripts/configs get symlinked (e.g. a loop over `scripts/*.sh`, or explicit `ln -sf` lines). If found, add `gamemode-session`, `~/.config/gamemode.ini`, and `~/.config/mako/config` to the same mechanism. If `install.sh` does NOT enumerate these (the existing bin symlinks appear to be created manually), skip this step and note it in the commit.
|
||||
|
||||
Run: `grep -nE "ln -s|scripts/|\.local/bin|\.config" install.sh | head`
|
||||
Expected: shows the deploy mechanism (or nothing → manual, skip).
|
||||
|
||||
- [ ] **Step 3: Live end-to-end — capture baseline**
|
||||
|
||||
```bash
|
||||
echo "profile: $(powerprofilesctl get)"
|
||||
pgrep -x waybar >/dev/null && echo "waybar: up" || echo "waybar: down"
|
||||
makoctl mode
|
||||
```
|
||||
Record these; the cycle must return to exactly this state.
|
||||
|
||||
- [ ] **Step 4: Activate and assert effects applied**
|
||||
|
||||
```bash
|
||||
gamemode-session add manual
|
||||
sleep 1
|
||||
gamemode-session status; echo "status_exit=$?" # active / 0
|
||||
systemd-inhibit --list | grep -q gamemode && echo "idle: inhibited" # idle: inhibited
|
||||
makoctl mode | grep -q do-not-disturb && echo "dnd: on" # dnd: on
|
||||
echo "profile now: $(powerprofilesctl get)" # performance (unless baseline already performance)
|
||||
pgrep -x waybar >/dev/null && echo "waybar: up" || echo "waybar: down" # down
|
||||
```
|
||||
Expected: status active; idle inhibited (a `gamemode` entry in the list); DND on; profile is `performance`; waybar down.
|
||||
|
||||
- [ ] **Step 5: Deactivate and assert full revert**
|
||||
|
||||
```bash
|
||||
gamemode-session del manual
|
||||
sleep 2
|
||||
gamemode-session status; echo "status_exit=$?" # inactive / 1
|
||||
systemd-inhibit --list | grep -q gamemode && echo "idle: STILL inhibited (BUG)" || echo "idle: released"
|
||||
makoctl mode | grep -q do-not-disturb && echo "dnd: STILL on (BUG)" || echo "dnd: off"
|
||||
echo "profile now: $(powerprofilesctl get)" # back to Step 3 value
|
||||
pgrep -x waybar >/dev/null && echo "waybar: up" || echo "waybar: down" # up (matches Step 3)
|
||||
```
|
||||
Expected: status inactive; idle released; DND off; profile matches the Step 3 baseline; waybar back up. If any line reports a BUG or a mismatch, run `gamemode-session reset` to force-clean, then debug the corresponding `effect_*_off` before proceeding.
|
||||
|
||||
- [ ] **Step 6: Manual verification note (idle timeout)**
|
||||
|
||||
The 5-minute blank-suppression can't be asserted in seconds. Manually verify once: activate gamemode, leave the machine idle past 5 minutes (no input), confirm the screen does NOT blank; then `del`/toggle off and confirm hypridle resumes (screen blanks on the next idle window). Record the result. If it DOES blank while active, hypridle isn't honoring the systemd idle inhibitor on this build — fall back to a Wayland idle-inhibitor helper (documented follow-up), but the systemd path matches the current hypridle config intent.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
cd ~/Documents/dotfiles
|
||||
git add -A install.sh # only if changed; otherwise omit
|
||||
git commit -m "gamemode: wire bin symlink + live smoke verified
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>" # allow empty note if only the symlink (untracked) changed
|
||||
```
|
||||
|
||||
(The `~/.local/bin/gamemode-session` symlink lives outside the repo — like the other script symlinks — so it isn't committed; note that it was created.)
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:**
|
||||
- Reference-counted two-source state machine → Task 1. ✓
|
||||
- Idle inhibit / DND / performance+fan / hide waybar effects, each with exact-baseline revert → Task 2. ✓
|
||||
- Baseline snapshot (profile, waybar, fanauto, idle.pid) → Task 1 (`snapshot_baseline`) + Task 2 (`idle.pid`). ✓
|
||||
- "only change profile if baseline != performance; only restore if changed" → Task 2 `effect_perf_on/off`. ✓
|
||||
- mako `[mode=do-not-disturb]` config → Task 3. ✓
|
||||
- `gamemode.ini` Feral hooks → Task 4. ✓
|
||||
- niri `Mod+G` bind + removal of 3 dead VRR rules (incl. `is-focused=true`) + breadcrumb comment → Task 5. ✓
|
||||
- `reset` escape hatch → Task 1 dispatch + used in Task 6 debugging. ✓
|
||||
- `status` subcommand for future waybar module → Task 1. ✓
|
||||
- Script naming `gamemode-session`, `scripts/<name>.sh`→`~/.local/bin/<name>` convention → Global Constraints + Task 6. ✓
|
||||
- Steam `gamemoderun %command%` usage → user-side (documented in spec Usage; not a code task), reflected in Task 4 verification via `gamemoderun`. ✓
|
||||
|
||||
**Placeholder scan:** No TBD/TODO; every code step shows full content; no "add error handling" hand-waves (guards are written out). ✓
|
||||
|
||||
**Type/name consistency:** `effect_idle_on/off`, `effect_dnd_on/off`, `effect_perf_on/off`, `effect_waybar_on/off`, `apply_effects`, `revert_effects`, `snapshot_baseline`, `is_active`, `fan_for`, and the env seams `GAMEMODE_STATE_DIR`/`GAMEMODE_DRYRUN` are named identically across Tasks 1, 2, and 6. Baseline filenames `baseline.profile`/`baseline.waybar`/`baseline.fanauto` and `idle.pid`/`src.<source>` are consistent throughout. ✓
|
||||
Reference in New Issue
Block a user