From 8049627deba2a09a75253eb8e322e69165b09f9c Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:03:49 +0200 Subject: [PATCH 01/11] Cut Getting started to what a first-time user needs 2,645 words to 957. Each fact once; details left to the reference and How it works, which the page links. Co-Authored-By: Claude Opus 5.5 --- docs/getting-started.md | 386 ++++++++++------------------------------ 1 file changed, 95 insertions(+), 291 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 7c75392..c5d4df3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,82 +1,46 @@ # Getting started -You want one thing to happen when you start a game, and another when you stop. -That is all this program does. Here is how to get there in about five minutes. +Run one thing when a game starts, another when it stops. Five minutes. ## TL;DR 1. Run `GameModeExecutor-.msi` from the - [latest release](https://github.com/Geeooff/GameModeExecutor/releases/latest) - — *More info*, *Run anyway* if Windows warns. A grey controller appears + [latest release](https://github.com/Geeooff/GameModeExecutor/releases/latest). + If Windows warns: *More info*, *Run anyway*. A grey controller appears beside the clock. -2. Right-click it, **Edit configuration**, add your commands — see - [Say what to run](#say-what-to-run) — and save. It applies at once. -3. Play. The icon turns green while a game runs; red means the file has a - mistake, and the menu's first line says which. - -## The two files - -It ships as the same program twice. They differ in one way: one has a black -console window, the other has none. - -| Executable | Role | -| --- | --- | -| **`gamemode-executorw.exe`** | The one that works. It starts by itself when you log on and never shows anything — no window, no output. You never launch it yourself; the logon task and the installer do. | -| **`gamemode-executor.exe`** | The one you talk to. Open it in a terminal to set things up, test, or check. It answers, then it is done. It does not keep watching. | - -The `w` just means *windowless*, the same convention as `python.exe` and -`pythonw.exe`. +2. Right-click it, **Edit configuration**, add your commands + ([below](#say-what-to-run)), save. It applies at once. +3. Play. The icon turns green while a game runs. ## Installing -The release page offers two files that hold the same two executables. - -**The installer, `GameModeExecutor-.msi`.** Run it. Windows may -stop you first with *Windows protected your PC*, because the installer is -not signed with a certificate it knows: *More info*, then *Run anyway*. The -release page lists the file's SHA-256 if you want to check what you -downloaded. It asks for no administrator rights and installs for you alone, -into -`%LOCALAPPDATA%\Programs\GameModeExecutor`. It also writes a starter -configuration if you have none, registers the logon task, and starts the -watcher: the confirmation that it worked is the **grey controller icon** -that appears in the notification area, beside the clock. No window to click -through, and *Programs and Features* lists it afterwards. A newer release -installs over it the same way: it stops the running watcher, replaces the -files, leaves your configuration and your task where they are, and starts -the watcher again. Skip to [Say what to run](#say-what-to-run). - -**The zip, `GameModeExecutor-.zip`,** for anyone who would rather -not run an installer: unzip it and keep it somewhere. Two things make the -choice of somewhere worth a moment's thought. - -**It has to stay put.** The logon task records the full path to the executable, -so moving the folder afterwards means running `install-task --force` again. - -The tidiest place, and the Windows convention for a program installed for one -user, is: +**The installer** (`.msi`) needs no administrator rights. It installs for you +alone in `%LOCALAPPDATA%\Programs\GameModeExecutor`, writes a starter +configuration, registers the logon task and starts the watcher. Windows warns +because it is not signed; the release page lists its SHA-256. A newer release +installs over it and keeps your configuration. -``` -%LOCALAPPDATA%\Programs\GameModeExecutor +**The zip** holds the same files. Unzip it somewhere it will stay — +`%LOCALAPPDATA%\Programs\GameModeExecutor` is the convention — not in +`C:\Program Files` (needs administrator rights) nor a synced folder like +OneDrive. Then, once: + +```bash +gamemode-executor init +gamemode-executor install-task ``` -Anywhere of your own works too — `C:\Tools\GameModeExecutor`, say. Two to -avoid: +Moving the folder later means `install-task --force` again. -| Not here | Why | -| --- | --- | -| `C:\Program Files` | Needs administrator rights to write, and then your own configuration sits in a folder you cannot edit. This program is built to never ask for those rights. | -| A OneDrive or Dropbox folder | Synced folders move files, lock them mid-sync, and can turn them into online-only placeholders. For something that starts at logon, that is a bad bet. | +There are two executables. `gamemode-executorw.exe` is the watcher: no window, +started at logon, never by you. `gamemode-executor.exe` is the one you type +commands into. ## Say what to run -The starter configuration runs nothing: as installed, the watcher detects -games, names them and logs them, and that is all — which is a fine way to see -it work before deciding what it should do. Right-click the icon and choose -**Edit configuration**, or open `%APPDATA%\GameModeExecutor\config.toml` -yourself. (From the zip, `gamemode-executor init` writes that file first.) - -The part that matters looks like this: +Right-click the icon, **Edit configuration** (the file is +`%APPDATA%\GameModeExecutor\config.toml`). As installed it runs nothing. Add +one block per command: ```toml [[on_game_start.actions]] @@ -90,138 +54,57 @@ program = "powercfg.exe" args = ["/setactive", "SCHEME_BALANCED"] ``` -One block per command. Add as many as you like to either event. The starter -file carries two commands that beep, commented out: uncomment them to *hear* -a game being detected before you write anything real. +Write paths between single quotes, so backslashes need no doubling: +`program = 'C:\Program Files\Something\tool.exe'`. The starter file has two +beeps, commented out, to hear a detection before writing anything real. -Write paths between **single quotes** — that way Windows backslashes need no -doubling: - -```toml -program = 'C:\Program Files\Something\tool.exe' -``` +Save, and the watcher reloads it within a second, even mid-game. Only +`log_dir` and `log_days` wait for the next start. -## Check it +Test without a game: ```bash gamemode-executor validate -``` - -Either it says the file is fine, or it points at the line and column of the -mistake. - -Then try your commands for real, without waiting for a game: - -```bash gamemode-executor trigger start gamemode-executor trigger stop ``` -Save the file, and that is all: the watcher notices within a second, reads -it again and writes `Configuration reloaded` in the log. Nothing to restart, -and a game in progress is not disturbed — the commands that run when it ends -are the ones you just saved. The settings that wait for the next start are -`log_dir` and `log_days`, since the log is already open; the log says so. - -If the file cannot be used, the icon turns **red, with a slash**, a -notification with the error glyph says what is wrong in full — the line -number and the parser's words, and what it expected instead — and the -first line of the icon's menu keeps the short of it, such as -`Configuration error: line 3: unknown field 'log_levl'`. Nothing runs until -you fix it: not the old commands, not their stop half. **Edit -configuration** still opens the file, and saving a good one brings the icon -back to grey, with a notification saying the watcher is watching again. -The same words are in the log, marked `ERROR`. The notifications are -silent, and Windows keeps them in its notification centre. - -**That is the end of the setup.** Play. The commands fire by themselves. - -From the zip, one more command the first time, to register the task that -starts the watcher at every logon and to start it now: - -```bash -gamemode-executor install-task -``` - -No administrator rights, no password, no window. The installer did this for -you. - -Want a complete worked example rather than a blank page? [Recipes](recipes/) — -also on the release page as `GameModeExecutor-recipes-.zip` — has -one per job, each with a `config.toml` you can copy straight over. +Commands can use the game's name: see the placeholders in the +[reference](reference.md#actions). For a complete example, the +[recipes](recipes/) each have a `config.toml` to copy. -## The notification area icon - -Once it is running you get a small controller in the notification area, beside -the clock. It is the only thing this program ever puts on screen. +## The icon | Icon | Meaning | | --- | --- | -| **grey controller** | running, no game. What you will see almost all the time. | -| **green controller** | a game is detected | -| **red controller, slashed** | the configuration cannot be used and nothing is watched until it is fixed; the menu's first line says what is wrong | - -Hover it and the tooltip names the game. Right-click and the first line of the -menu says the same — it is greyed out because it is an answer, not a button. - -A game Windows tracks but does not describe shows as *"A game is running, but -Windows does not name it"*. That is not a failure: detection never depended on -the name. See [How it works](how-it-works.md#naming-the-game). - -Windows hides new icons by default: if you cannot see it, click the **`^`** -arrow next to the clock, and drag it onto the taskbar to keep it there. +| grey controller | running, no game | +| green controller | a game is detected; the tooltip names it | +| red, slashed | the configuration has a mistake; nothing runs until it is fixed, and the menu's first line says where | -**Right-click it** for a short menu: +Not visible? Click the `^` next to the clock and drag it onto the taskbar. -| Entry | What it does | +| Menu | | | --- | --- | -| **Edit configuration** | opens your `config.toml` in whatever you use for text files — Notepad if `.toml` is not associated with anything | -| **Open log** | opens today's log the same way | -| **Documentation** | opens this page for **the exact build you are running**, not for whatever the project looks like today | -| **Check for updates** | asks GitHub whether a newer release exists — the only time this program ever connects to anything, and only when you click. The menu closes, as menus do; the answer arrives as a silent notification a second later, and waits in the menu too: *0.1.0 is the latest version*, or **Download and install 0.2.0** beside a **What changed in 0.2.0** that opens the release page | -| **Quit** | stops the watcher, running the stop commands on the way out so you are not left on a gaming profile | +| **Edit configuration** | opens `config.toml` | +| **Open log** | opens today's log | +| **Documentation** | this page, for the build you run | +| **Check for updates** | asks GitHub — the only time the program connects to anything | +| **Quit** | stops the watcher, running the stop commands first | -Quitting only stops it until the next time you log on — or until you start -**GameModeExecutor** from the Start menu, which the installer adds. To stop it -for good, see [Turning it off](#turning-it-off). +After *Quit*, it starts again at the next logon, or from **GameModeExecutor** +in the Start menu. ## Updating -Right-click the icon, **Check for updates**. The menu closes and a -notification answers a second later; the answer waits in the menu too. If a -newer release exists, **Download and install** fetches it, checks it against -the checksums the release publishes, and installs it — in the middle of a -game if you like: the running watcher hands the game over to the new one, -which picks it up where it was without touching your commands. The icon -disappears for about a second and comes back, and the new version says so -with a notification, since the install itself is too quick to watch. The -menu and the log, under `update`, say the same. - -The same from a terminal: - -```bash -gamemode-executor update --check -gamemode-executor update -``` - -Nothing is ever checked or downloaded unless you ask. If a check or an -install fails, the menu says so in one line ending in *(see log)*, and the -log has the reason — no connection, a refusal from GitHub, a file that did -not verify, or Windows Installer's own error code. +**Check for updates**, then **Download and install**. It checks the file +against the release's checksums and installs, even mid-game: the new watcher +takes the game over without running any command. From a terminal: +`gamemode-executor update`. -## Checking that it is alive +## Checking on it -```bash -gamemode-executor status -``` - -Tells you what it sees right now: whether a game is running, which one, and -whether Windows recognises it. - -The log keeps the history of every session. It lives in -`%LOCALAPPDATA%\GameModeExecutor\logs`, one file a day — the last seven, or -`log_days` in the configuration — and **Open log** opens today's. It reads -like this: +`gamemode-executor status` says what it sees now. The log, one file a day in +`%LOCALAPPDATA%\GameModeExecutor\logs` (**Open log**), says what happened: ``` 2026-09-10 17:51:02.433 INFO watcher GameModeExecutor 0.1.0 starting @@ -229,129 +112,55 @@ like this: 2026-09-10 17:58:41.833 INFO game Game no longer detected: bf6.exe ``` -It starts earlier than the first session: the lines marked `setup` say when -the configuration was written and the logon task registered, whether you -typed the command or the installer did it. `init`, `install-task`, -`uninstall-task` and `stop` print those same lines as they run. - -## Using the game's name in your command +`gamemode-executor --version` names the exact build, with a link to this page +as it was for that build. -If you want the command to know which game started, these get substituted: - -| Placeholder | Becomes | -| --- | --- | -| `{event}` | `game_start` or `game_stop` | -| `{process_name}` | e.g. `bf6.exe` | -| `{process_id}` | e.g. `14552` | -| `{process_path}` | the full path, when readable | - -```toml -args = ["-Command", "Write-Host 'playing {process_name}'"] -``` +## If your program needs administrator rights -Detection never depends on these. A game Windows tracks but does not describe -leaves them empty, and everything still runs. +The watcher never runs elevated, so Windows refuses such a program (error +740). Register it once as a scheduled task *with highest privileges* and run +`schtasks /Run` instead. [Fan profiles with FanControl](recipes/fancontrol-fan-profiles/) +does exactly that, step by step. -## If your program needs administrator rights +## When something is not right -Some do, usually because they talk to hardware. The watcher runs without -administrator rights on purpose and cannot start those directly: Windows refuses -with error 740. +**Nothing happens when I start a game.** Bring the game to the front, then +run `gamemode-executor status`. If it sees no game, Windows does not count +this title as one: open the Game Bar over it (`Win+G`) and tick *Remember +this is a game*. See [How it works](how-it-works.md#how-it-knows-a-game-is-running). -The way round it is a scheduled task per command, registered once with *run with -highest privileges*. Triggering one needs no rights and raises no prompt, so -your command becomes `schtasks /Run` instead. +**A program that is not a game starts my commands.** It was ticked *Remember +this is a game*; the log and `status` say so. Untick it in the Game Bar. -[Fan profiles with FanControl](recipes/fancontrol-fan-profiles/) walks through -that case end to end, with ready-made task files. +**The log says a game I marked is now known to Microsoft.** Untick the box: +Windows recognises it by itself now. -## Which build is this? +**My command does not run.** The log says why — usually a wrong path, or a +program that needs administrator rights. -```bash -gamemode-executor --version -``` +**The icon is red.** The notification and the menu say which line is wrong; +`gamemode-executor validate` gives the full account. Save a fixed file and +the icon turns grey. -``` -gamemode-executor 0.1.0 (de538e33) -commit: de538e33ac376c062064131c8bbb5bcf2444ac82 -repository: https://github.com/Geeooff/GameModeExecutor -documentation: https://github.com/Geeooff/GameModeExecutor/blob/de538e33.../docs/getting-started.md -``` +**I logged off mid-game and the fans stayed loud.** They calm down at the +next logon: Windows refuses to start programs once the session is ending +([why](how-it-works.md#logging-off-mid-game)). -The first question to ask of a machine that is not yours. The commit is the -exact revision these executables were built from, and the documentation link -goes to this page **as it was at that commit** — so it describes the program in -front of you rather than whatever the project looks like today. +**The log says the last watcher ended without stopping.** Something ended it +mid-game — Task Manager, a crash, a power cut. That start has run the stop +commands, or resumed the game. If nothing you know of did it, +[open an issue](https://github.com/geeooff/GameModeExecutor/issues) with that +day's log. -`gamemode-executorw.exe --version` says the same thing, and `status` prints it -too. +**The update failed.** The log's `update` lines give the reason; the old +version keeps running. -## When something is not right +**The fans take ages to calm down after I quit.** Windows holds its "a game is +running" signal until you touch the PC again, then lets go within a second to +a minute and a half. *(Corrected 2026-09-25.)* -**Nothing happens when I start a game.** -Bring it to the front first: Windows may not count a game still loading -behind another window. Then run `gamemode-executor status`. If it says no -game is running, Windows itself is not flagging that title — see -[How it works](how-it-works.md). A game Windows does not recognise, you can -mark yourself: open the Game Bar over it (`Win+G`), and in its settings tick -*Remember this is a game*. The watcher follows those too, from the next two -seconds on — [How it works](how-it-works.md#how-it-knows-a-game-is-running) -says how. - -**A program that is not a game starts my game commands.** -Someone ticked *Remember this is a game* over it. The log says so on the -*Game detected* line, and `gamemode-executor status` lists every program -marked that way: open the Game Bar over it and untick the box. - -**The log says a game I marked is now known to Microsoft.** -Microsoft added it to its own list since you ticked the box. Untick -*Remember this is a game* for it and start the game again: Windows then -recognises it by itself, and so does this program. - -**It says another instance is already running.** -The background watcher is doing its job. That message means you tried to start a -second one, which is refused so your commands cannot fire twice. - -**My command does not run.** -Check the log. A command that fails to start is recorded with the reason, and it -never prevents the others from running. The usual cause is a wrong path, or a -program that needs administrator rights (see above). - -**The icon is red, with a slash.** -The configuration file cannot be used. A notification said why when it -happened — it is still in Windows' notification centre — and the first line -of the icon's menu keeps the short of it: a line number and what the parser -found there, or *the file is missing*. Nothing runs until it is fixed: -**Edit configuration** opens the file, and the moment a usable one is saved -the icon is grey again, a notification says so, and the log says -`Configuration reloaded`. `gamemode-executor validate` tells the same story -in a terminal, with the parser's full account. - -**I logged off during a game and the fans stayed loud.** -They calm down at your next logon. Windows does not let the stop commands run -once the session is ending, so the watcher runs them the moment it starts -again — see [How it works](how-it-works.md#logging-off-mid-game). - -**The log says the last watcher ended without stopping.** -Something ended it during a game without asking it to stop: Task Manager, a -crash, a power cut. The start that wrote the line has run the stop commands, -or taken the session up if the game was still on. If nothing you know of -did it, that day's log is worth -[an issue](https://github.com/geeooff/GameModeExecutor/issues). - -**The update failed, the menu says so.** -Read the log: the `update` lines carry the reason. A download that did not -verify is deleted, and a check that could not reach GitHub is just that — -try again later. If Windows Installer refused with a code, the log names it -and `%LOCALAPPDATA%\GameModeExecutor\updates\install.log` has its own -account. The version you had keeps running either way. - -**The fans take ages to calm down after I quit.** -That wait is Windows': it keeps its "a game is running" signal for as long as -nobody touches the PC, and at the keyboard lets go within a second to a -minute and a half. Quit and walk away, and the fans wait for your return. -Nothing to tune; `log_level = "debug"` shows where the time went. -*(Corrected 2026-09-25.)* +**It says another instance is already running.** The watcher is already +running; a second one is refused. ## Turning it off @@ -360,13 +169,8 @@ gamemode-executor stop gamemode-executor uninstall-task ``` -The first stops the one running now; the second removes the logon task, so -nothing starts at the next logon. To take the program off the machine, see -[Removing it](how-it-works.md#removing-it): *Programs and Features* for the -installer's copy, or `purge` for every trace whichever way it came. - -## Everything else +To remove it entirely: *Programs and Features*, or `purge` — see +[Removing it](how-it-works.md#removing-it). -The [reference](reference.md) lists every command, every configuration field -and every exit code. [How it works](how-it-works.md) explains the mechanism -for the curious. +The [reference](reference.md) lists every command, setting and exit code; +[How it works](how-it-works.md) explains the mechanism. From 37b577d3573ee394379231da192783c32396cc99 Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:08:57 +0200 Subject: [PATCH 02/11] Cut the README to what the program is and where to start 523 words to 255; installing details now live in Getting started only. Co-Authored-By: Claude Opus 5.5 --- README.md | 79 +++++++++++++++++-------------------------------------- 1 file changed, 24 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 8165035..36fdda4 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,40 @@ # GameModeExecutor -A small Windows program, written in Rust, that runs the executables you -configure when a game starts and when it stops. +A small Windows program, written in Rust, that runs the commands you configure +when a game starts and when it stops — fan curves, a power plan, RGB +lighting, anything with a command line. -``` -game starts -> your commands -game ends -> your other commands -``` +**No list of games to maintain.** Windows decides when a game is running: the +watcher follows the Game Bar's own signal and the games you marked in the Game +Bar. It does not poll while you play, needs no administrator rights, connects +to nothing unless you ask for an update, and shows only an icon beside the +clock. -It knows nothing about any particular tool. It launches programs with -arguments, which is enough to switch fan profiles, a power plan, RGB lighting, -or anything else with a command line. +## Install -**There is no list of games to maintain.** Windows itself decides when a game -is running — the watcher observes the Game Bar presence writer that Windows -starts for one, and follows the games you marked yourself in the Game Bar — -and it does not poll while you play. It runs as you, with no -administrator rights, connects to nothing unless you ask it to look for an -update, and shows nothing but a small icon in the notification area. +Run the `.msi` from the +[latest release](https://github.com/Geeooff/GameModeExecutor/releases/latest): +no administrator prompt, and the icon appears beside the clock. Right-click +it, *Edit configuration*, and say what to run. [Getting started](docs/getting-started.md) +covers the zip, updates and removal. ## Documentation -| Page | What it is for | +| Page | For | | --- | --- | | [Getting started](docs/getting-started.md) | Running in five minutes. Start here. | -| [Recipes](docs/recipes/) | Worked examples, one folder each with a ready-made `config.toml` — including fan profiles with FanControl, which needs one extra step because it requires administrator rights. Also on each release as a zip. | -| [How it works](docs/how-it-works.md) | For the curious: how it knows a game is running, why there are two executables, why the wait after you quit. No programming needed. | -| [Reference](docs/reference.md) | Commands, configuration fields, exit codes, the log contract, building. | -| [Changelog](CHANGELOG.md) | What each release changed for you, in plain sentences. | -| [Design record](docs/design/) | Why it is built this way: the decisions, the measurements behind them, and what is still open. | -| [AGENTS.md](AGENTS.md) | How to work in this repository — for coding agents, and for people. | +| [Recipes](docs/recipes/) | Worked examples with a ready-made `config.toml`, FanControl among them. | +| [How it works](docs/how-it-works.md) | How it knows a game is running, and why it behaves as it does. | +| [Reference](docs/reference.md) | Commands, settings, exit codes, the log, building. | +| [Changelog](CHANGELOG.md) | What each release changed. | +| [Design record](docs/design/) | The decisions and the measurements behind them. | +| [AGENTS.md](AGENTS.md) | How to work in this repository. | ## Status -In daily use on its author's machine, and released from -[the releases page](https://github.com/Geeooff/GameModeExecutor/releases). -Developed and measured on Windows 11 25H2. It relies on the Game Bar -component Windows ships by default, so a machine where Game Bar has been -removed will not detect anything. - -## Install - -Two self-contained executables, no runtime dependencies. `gamemode-executor.exe` -is the one you type commands into; `gamemode-executorw.exe` is the same program -with no console, started at logon by a task it registers for you. - -From the [latest release](https://github.com/Geeooff/GameModeExecutor/releases/latest), -take the **`.msi`** and run it: per user, no administrator prompt, into -`%LOCALAPPDATA%\Programs\GameModeExecutor`. It writes a starter -configuration, registers the logon task and starts the watcher — the icon -appearing beside the clock is the confirmation. Right-click it, *Edit -configuration*, and say what to run. - -The **`.zip`** holds the same executables for anyone who would rather unpack -them by hand; then: - -```powershell -gamemode-executor init # write the starter config.toml -gamemode-executor install-task # start the watcher now and at every logon -``` - -Later releases install themselves: right-click the icon, **Check for -updates**, and the menu offers the newer version — nothing is checked -unless you ask. To remove it, *Programs and Features* takes the executables -and the logon task away and leaves your configuration; `gamemode-executor -purge` removes every trace. Building -from source is in the [reference](docs/reference.md#building-and-releasing). +In daily use, released on [GitHub](https://github.com/Geeooff/GameModeExecutor/releases). +Developed on Windows 11 25H2. It needs the Game Bar component Windows ships by +default. ## License From bb877e1d114ba6f064a5d4b72c31a36b2da7f85f Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:08:57 +0200 Subject: [PATCH 03/11] Cut How it works to the mechanism, each fact once 3,377 words to 1,369. History and reasoning stay in the design record, which the page links. Co-Authored-By: Claude Opus 5.5 --- docs/how-it-works.md | 444 +++++++++++++------------------------------ 1 file changed, 128 insertions(+), 316 deletions(-) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index f5a62c2..5bb56f6 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -1,372 +1,184 @@ # How it works -For the curious. No programming needed — but it assumes you are comfortable with -the idea of processes, files and scheduled tasks on Windows. - -If you only want it working, [Getting started](getting-started.md) is enough. +For the curious. To get it running, [Getting started](getting-started.md) is +enough. ## How it knows a game is running -**There is no list of games in this program.** None. That matters, because every -list is out of date the day it ships, and yours would need editing every time -you install something. - -Instead it asks Windows, indirectly but reliably. - -Windows ships a component for the Game Bar — the overlay you get with `Win+G`. -When Windows decides a game is running, it starts a small process called -`GameBarPresenceWriter.exe`. When the game is gone, Windows shuts that process -down. That process existing *is* Windows' own verdict that you are playing. -Windows may wait for the game to first come to the front: Battlefield 6, left -loading behind another window, counted only once it was brought forward. - -So the watcher does something very simple: it watches whether that process -exists. - -- **No game:** it looks every two seconds. That is the only polling it ever - does, and it is cheap: it asks Windows for the list of process numbers - alone, which takes a few hundredths of a millisecond, and asks a process - its name only the first time it sees it — a full list of every process by - name, which costs a hundred times more, only every thirty seconds. -- **Game running:** it stops looking entirely, and asks Windows to wake it when - that process ends. Zero activity while you play — which is rather the point of - a program that runs during games. - -The result is that anything Windows knows as a game triggers it, including -titles released after this program was written — Windows keeps its list of -games up to date on its own. - -**Games you marked yourself.** A title Windows does not recognise, you can -teach it: open the Game Bar over it and tick *Remember this is a game*. -Windows treats it as a game from then on — overlay, capture, Game Mode — but -does not start the process above for it, because that process exists to -tell Xbox what you are playing, and a title you named by hand has no Xbox -identity to tell. So the watcher also reads Windows' own list of the games -you marked: when one of those executables runs, that is a game session, with -the same commands at both ends. It is the same two-second look, and the -list is read again only when you tick or untick a box, so ticking it in the -middle of a game starts the session within those two seconds — nothing to -relaunch. While that game runs, the watcher waits on the game itself, and -does nothing else. The log names such a game as one *marked as a game by -hand*: a program ticked by mistake — a browser, say — would be a game -session whenever it runs, and that line is how you find the box to untick. - -**Boxes you no longer need.** Microsoft adds games to its list after they -are released, and a box you ticked before that keeps Windows on your own -entry rather than its own — measured on 2026-09-23 with *Death Stranding 2* -and *Wreckfest 2*, both listed by Microsoft by then. Each time the watcher -starts, it compares the games you marked with Microsoft's list and says, in -the log, which ones Microsoft now knows. For those, untick *Remember this is -a game*, quit the game and start it again: Windows recognises it by itself -— the Xbox overlay shows you *playing* it — and so does this program. -`gamemode-executor status` lists the games you marked and says the same. - -Which process to watch is read from the registry at startup rather than -hard-coded, so a machine where that registration differs still works. +**There is no list of games in this program.** It asks Windows. -## Naming the game +When Windows decides a game is running, it starts `GameBarPresenceWriter.exe`, +a Game Bar process that tells Xbox what you play, and stops it when the game is +gone. That process existing *is* Windows' verdict. Windows may wait for the +game to come to the front first. + +- **No game:** the watcher looks for that process every two seconds. That is + its only polling, and it is cheap: process numbers only, names for new ones. +- **Game running:** it stops looking and asks Windows to wake it when the + process ends. Nothing runs while you play. -Detecting and *naming* are two separate things here, and only the first one -matters. +Any game Windows knows triggers it, including ones released after this +program. -The name is a convenience — for the log, and for the `{process_name}` you can -put in your commands. It comes from a list Windows itself maintains of titles it -has seen, under `HKCU\System\GameConfigStore`. The watcher matches running -processes against it. +**Games you marked yourself.** Tick *Remember this is a game* in the Game Bar +(`Win+G`) over a title Windows does not recognise. Windows starts no presence +process for it, so the watcher also reads that list: when a marked program +runs, that is a session, and the watcher waits on the program itself. The log +says *marked as a game by hand*, which is how you find a box ticked by +mistake. At each start the log also names the marked games Microsoft's own +list now knows: untick those, Windows recognises them by itself. -The catch is that a single game brings several processes that all match: a -launcher, an anti-cheat service, and the game. The satellites usually start -first, so the first match is often the wrong one. +## Naming the game -So, twenty seconds into a session, the watcher asks the graphics card. Windows -exposes per-process GPU counters — the same numbers Task Manager shows in its -"GPU engine" column — and among the processes that already matched, the one -actually drawing the picture is the game. Video decoding and copy engines are -excluded, otherwise a video player would outrank a paused game. +The name only serves the log and the `{process_name}` placeholder; detection +never depends on it. It comes from the games Windows has seen +(`HKCU\System\GameConfigStore`), matched against running processes. -A real session looked like this: +One game often brings a launcher and an anti-cheat that match too, and start +first. So twenty seconds in, the watcher asks the graphics card which match is +drawing the picture: ``` 17:53:14 Game detected: EAAntiCheat.GameServiceLauncher.exe 17:53:35 Game identified more precisely: bf6.exe (74% of the rendering) ``` -Still on a loading screen, nothing drawing yet? It asks again every twenty -seconds, for two minutes at most, then keeps whatever matched first. This -only ever changes the *name*: nothing about detection depends on it. +Nothing drawing yet? It asks again every twenty seconds, two minutes at most. ## The wait after you quit -The commands for "game stopped" can fire well after you have quit. - -That wait is Windows'. It never lets go of its "a game is running" signal -while nobody touches the PC: quit and walk away, and the commands wait for -your return — two hours, once. At the keyboard it has taken from under a -second to a minute and a half. No setting changes that. This program adds -about two seconds, a grace period in case Windows briefly restarts the -signal mid-session. - -Set `log_level = "debug"` and the log says when Windows let go and whether the -game had already exited. - -*Corrected 2026-09-25: this page called the wait unpredictable, then said -about twenty seconds.* +The stop commands can fire well after you quit. That wait is Windows': it keeps +its signal while nobody touches the PC — two hours, once — and at the keyboard +lets go within a second to a minute and a half. The watcher adds two seconds, +in case the signal blinks. `log_level = "debug"` shows where the time went. +*(Corrected 2026-09-25: this page called the wait unpredictable, then about +twenty seconds.)* ## What it costs your machine -Measured on the whole watcher, as Windows counts it, on a gaming PC in -September 2026. Memory here is private memory — Task Manager's *Commit -size* column in *Details*; its *Memory* column shows less. +Measured on a gaming PC in September 2026, private memory (Task Manager's +*Commit size*): | When | Processor | Memory | | --- | --- | --- | -| Waiting for a game, looking every two seconds | 0.03 % of one core | about 2 MB | -| During a game | nothing measurable: it waits for Windows to wake it | unchanged | -| Naming a game from what the graphics card draws, early in a session | | about 0.25 MB, once; asking again adds nothing | -| The icon's menu, the first time it opens | | about 1 MB, once — Windows' cost for a program's first menu | -| *Edit configuration*, *Open log*, *Documentation* | | nothing that stays: a short-lived helper opens them and takes the cost with it | -| *Check for updates* | | about 1 MB, until the watcher next starts | - -Over one evening — a Battlefield 6 session, five hours of GTA Online, the -menu used, and the rest idle, six hours in all — the watcher used 1.25 -seconds of processor time. - -## Why there are two executables - -Windows makes a program choose, when it is built, between two kinds: - -- a **console** program, which owns a black window and can print into it; -- a **windowless** program, which has nowhere to print at all. - -It is not a setting you flip afterwards. So a program that must both sit -silently in the background *and* answer you when you type commands cannot be one -file. - -Hence the pair. Both understand the same commands. `gamemode-executor.exe` -is the one you type them into, because it answers where you can read it and -a shell waits for it to finish. `gamemode-executorw.exe` prints nothing and -nobody waits for it, which is exactly right for the two things that run it: -the logon task, which runs the watcher, and the installer, which writes your -starter configuration and registers the task through it — a console program -would flash a black window in the middle of the install. What it did is -written in the log instead. Python solves the same problem the same way, with -`python.exe` and `pythonw.exe`. - -The alternative — one windowless program that borrows the terminal it was -launched from — was tried on paper and rejected: a shell does not wait for a -windowless program, so the prompt comes back before the output, and commands -like `validate` would stop reporting success or failure to any script using -them. Silently, which is the worst way for that to break. - -Both files are built from the same code, so the watcher they run is identical, -and so is every command. - -## Only one watcher at a time - -A named lock makes sure of it. If you run `gamemode-executor run` while the -background one is going, the second one refuses and exits with a distinct code -rather than doubling up your commands. That is also why the Start menu's -**GameModeExecutor** is safe to click at any time: it starts the watcher, or -notes in the log that one is already running. - -## What starts it, and why not a service - -A **per-user scheduled task**, triggered at logon, fifteen seconds in. The -installer registers it and starts it once the files are in place, which is -why the icon appears as the install ends; from the zip, `install-task` does -the same by hand. Before an upgrade or an uninstall replaces or removes the -executables, the installer runs `stop` — *Quit*, typed — so Windows never -finds the watcher holding a file it is about to touch and never has to ask -you to close it. - -A Windows service was considered and dropped. Services run before anyone logs -on, in a separate session, where they cannot see the desktop the game is on — -which is precisely what this needs to observe. A task also needs no -administrator rights to install and no password. - -The task is registered from a full XML definition rather than the usual -command-line shortcut, because the shortcut leaves three defaults behind that -are wrong for something meant to run forever: a 72-hour execution limit that -kills it after three days, and two battery settings that stop it on an unplugged -laptop. - -## The window you cannot see - -The windowless watcher does own one window — created, never shown. It is what -the notification icon hangs off, and it is how the watcher hears the shell: -theme changes, and the moment Windows starts ending your session. +| Waiting for a game | 0.03 % of one core | about 2 MB | +| During a game | nothing measurable | unchanged | +| Naming a game from the graphics card | | 0.25 MB, once | +| The menu, first opened | | about 1 MB, once | +| *Check for updates* | | about 1 MB, until the next start | + +One evening of six hours, five of them in GTA Online: 1.25 seconds of +processor in all. + +## Why two executables + +A Windows program is built either as a console program, which can print, or +as a windowless one, which cannot. One file cannot be both. +`gamemode-executor.exe` answers your commands in a terminal, and the shell +waits for its result. `gamemode-executorw.exe` runs the watcher from the logon +task and the installer's steps, with no window to flash; it writes to the log +instead. Python does the same with `python.exe` and `pythonw.exe`. + +A named lock allows one watcher at a time: a second one is refused, so the +Start menu's **GameModeExecutor** is safe to click. + +## What starts it + +A per-user scheduled task, at logon, fifteen seconds in. Not a service: +services run in a separate session and cannot see the desktop the game is on. +The task is registered from XML, because the usual shortcut would leave a +72-hour execution limit and two battery conditions on it. The installer stops +the watcher before it replaces or removes the files. + +The watcher owns one hidden window: the icon hangs off it, and it is how +Windows tells the watcher the session is ending. ## Logging off mid-game -When you log off or shut down with a game running, Windows asks every window -whether it may end the session. The watcher says yes, writes `Windows is -ending the session, so the watcher stops` in the log, and starts nothing: a -program started even one millisecond after that question fails to -initialise, because the session it would live in is already being torn down. -That is measured, not guessed. *Until 2026-10-16 the watcher still started -the stop commands then, and they died.* - -So the stop commands run at the **next logon** instead. While a game is -running, a small file in `%LOCALAPPDATA%\GameModeExecutor` — -`pending-stop-actions`, no extension — says so. A game that stops normally -removes it once its stop commands have run. A session that ends any other way -leaves it behind, and the watcher's first act at your next logon is to run -the stop commands and remove it. The log reads: +When you log off or shut down, Windows asks every window whether the session +may end. The watcher says yes, writes `Windows is ending the session, so the +watcher stops`, and starts nothing: a program started at that point dies +before it runs. Measured, not guessed. *Until 2026-10-16 the watcher still +started the stop commands then, and they died.* + +So the stop commands run at the **next logon**. While a game runs, a file +says so: `pending-stop-actions` in `%LOCALAPPDATA%\GameModeExecutor`. A game +that ends normally removes it once its stop commands have run. Any other end +leaves it, and the next start runs the stop commands: ``` The stop commands of the last session, with Starfield.exe, did not run then, so they run now ``` -A stop command that fails leaves the file too, so the next start runs the -commands once more — Windows can refuse them that way when a game closes with -the shutdown. And if the watcher itself ended without stopping — ended from -Task Manager, crashed, or the machine lost power — the file still says a -watcher held the session, and the next start says so at `warn`. +A stop command that fails leaves the file too, for one more try at the next +start; Windows refuses them that way when a game closes with the shutdown. If +the watcher itself was ended mid-game — Task Manager, a crash, a power cut — +the next start says so at `warn`. -The delay is the time between logging off and logging back on, plus a few -seconds for the watcher to start. Across a shutdown, that is time the machine -is off. +The same file hands a game from one watcher to the next during an update or a +configuration reload: nothing runs, and the new watcher takes the session up +where it was. `stop_actions_on_exit = false` skips the stop commands on *Quit* +and at logoff alike. -`stop_actions_on_exit = false` opts out of both: quitting the watcher mid-game -leaves your profile alone, and so does the next logon. +## Changing the configuration -The same file lets one watcher hand a game to the next. When the watcher is -stopped for an update — by the installer, or by `stop --handover` — with a -game on, it runs nothing and leaves the file saying the session is open; the -watcher that starts a second later finds the game still running and takes -the session up where it was, icon and name included. Nothing runs twice, and -your gaming configuration is never switched off and on again in the middle -of a game. If the game ended in that second, the new watcher runs the stop -commands instead, as after a logoff. +The watcher waits on Windows' change notification for the configuration's +folder. When the file changes, it stops the engine and starts one on the new +file, a quarter of a second later; the log says `Configuration reloaded`. A +game in progress carries on, as for an update. -`gamemode-executor status` shows whether that file is there, and where. +A file that cannot be used **disables the watcher outright**: red icon, a +notification, the mistake on the menu's first line, nothing watched until a +usable file is saved. It never falls back to the last good file, which would +run something other than what the file says. `log_dir` and `log_days` wait for +the next start. -## Changing the configuration +## Updating itself -The watcher does not read the file once and forget it. A small thread waits -on the folder's change notification — Windows' own, nothing polled — and -when the file's bytes change it stops the running engine and starts one on -the new file. Editors save in several steps, so the read waits a quarter of -a second after the last change; the log then says `Configuration reloaded`. -A file written back unchanged is not a reload. - -A game in progress survives it, by the same handover an update uses: the -engine stops without running anything, leaving the session open in the -[marker](#logging-off-mid-game), and the next engine finds the presence -writer still running and takes the session up where it was. The stop -commands that run when the game ends are the new ones. - -A file that cannot be used — a typo, a value the program refuses, a file -that is gone — does not stop the program and does not fall back to the last -good one. It **disables it outright**: the icon turns red, a notification -says what is wrong, the menu's first line keeps the short of it, and -nothing is watched until a usable file is saved; when one is, a -notification says the watcher is back. Frozen, deliberately. Falling back -to the last good configuration -would mean the program runs something other than what the file says, with -nothing on screen to say so; frozen with a red icon is an unambiguous -state, and *Edit configuration* is right there. While it is frozen a game -that ends gets no stop commands, and the session marker remembers that: the -moment a usable file is saved, the new engine settles what was left — the -stop commands run if the game is gone, the session resumes if it is still -on. `log_dir` and `log_days` are the settings a reload cannot apply, because -the log is already open; they take effect at the next start, and the log -says so. +*Check for updates* asks GitHub where `releases/latest` points — one request, +only when you click. *Download and install* fetches the installer (or the zip, +for a hand-installed copy), checks its SHA-256 against the release's +`SHA256SUMS.txt` and deletes it on a mismatch. The installer then stops the +watcher with a handover, replaces the files and starts the new version. A +zip copy is updated by a hidden shell that waits for the watcher to exit, +unpacks the zip over the folder, keeping the old executables as `.old`, and +runs `install-task`. -## Removing it +The hash proves the file is the one the release published; the program has no +code signature ([why](design/13-updating.md)). -Uninstalling from *Programs and Features* removes what the installer put -there — the executables and the logon task it registered, which would -otherwise try to start a missing program at every logon — and nothing else, -as Windows applications ordinarily do: your configuration, the log and the -session marker stay, so that installing again finds everything as you left -it. Upgrading touches none of them, the task included. +## Removing it -When you want every trace gone, ask for it: +*Programs and Features* removes the executables and the logon task, and keeps +your configuration and log, as Windows applications do. ``` gamemode-executor purge ``` -It lists what it is about to remove and waits for a `yes`: the logon task, -the configuration wherever it found it, every log file wherever the log was -written, the session marker, the folder updates are downloaded to, its two -profile folders once they are empty, -and last the executables — through Windows Installer when they were -installed from the package, waiting for it to finish so that the few lines -the uninstall itself logs go too, or by deleting them once the command has -exited when they were unpacked from the zip. It refuses while a game is running, because a purge -then would leave your gaming configuration on with nothing left to restore -it; and it stops the running watcher first, the way *Quit* does. - -It removes only what it recognises as its own. A scheduled task it did not -register — the ones a recipe had you create, say — stays, and so does the -`\GameModeExecutor` folder in Task Scheduler around it; a folder that holds -anything else stays too. The recipes carry their own way out for what they -added. - -## Updating itself - -*Check for updates* asks GitHub for the latest release — one request, no -API, no key — and compares the tag with the version running. If it is -newer, *Download and install* fetches the installer (or the zip, for a -copy unpacked by hand) by its tag, computes its SHA-256 with Windows' own -cryptography and compares it with the `SHA256SUMS.txt` the release -publishes; a file that does not match is deleted before anything can run -it. Then the installer is the updater: the package is run quietly, stops -the watcher with a handover, replaces the files and starts the new version, -which resumes the game session if there was one. An unpacked copy does the -same through a small hidden shell that waits for the watcher to exit, -expands the archive over the folder — keeping the previous executables as -`.old` until the new version has started — and runs `install-task`. - -The hash proves the file is the one the release published, not that the -release is honest; the program has no code signature, and the design record -says why. A file it downloads carries no mark of the web, so Windows' -SmartScreen never sees it: the program vouches for it, through the hash. - -The notification that answers *Check for updates* is one of two times the -program shows anything beyond its icon: a menu closes when you click in it, -so the answer has to reach you somewhere. The other is a configuration that -cannot be used, and its end — the one thing that needs you. Both are -silent and respect your quiet hours; nothing else is ever said. +removes every trace after a `yes`: the task, the configuration, every log +file, the session marker, the updates folder, then the executables. It refuses +during a game, and leaves what it did not create, such as a recipe's tasks. ## What it does not do -- **No network it did not ask you about.** It connects to exactly one - thing, GitHub, and only when you click *Check for updates* or run - `update`. There is no telemetry, nothing is polled, and the check itself - is one request: where does `releases/latest` redirect — the tag is the - answer. -- **No administrator rights.** It runs as you, deliberately. That is why programs - needing elevation go through a scheduled task instead. -- **It does not touch your games.** It reads which processes exist and what the - GPU counters say. It never injects anything, never modifies a game, and never - interferes with anti-cheat. -- **It does not modify Windows.** The Game Bar registration is read, never - written. +- **No network unasked.** One request to GitHub, on *Check for updates*. No + telemetry. +- **No administrator rights.** Programs that need them go through a scheduled + task. +- **Nothing in your games.** It reads which processes exist and the GPU + counters; it injects nothing and stays clear of anti-cheat. +- **Nothing changed in Windows.** The Game Bar registration is only read. ## Where things are | What | Where | | --- | --- | -| The program | `%LOCALAPPDATA%\Programs\GameModeExecutor` from the installer — the Windows convention for a per-user install, and it stays writable, which `C:\Program Files` would not. From the zip, wherever you put it | -| Configuration | `config.toml` next to the executable if there is one, otherwise `%APPDATA%\GameModeExecutor\config.toml` | -| Log | `%LOCALAPPDATA%\GameModeExecutor\logs`, one file a day, `gamemode-executor.YYYY-MM-DD.log`, the last seven kept; the day changes at midnight UTC, the lines keep local time | -| Scheduled tasks | a `GameModeExecutor` folder in Task Scheduler, holding `Watcher` and anything a recipe added | -| A release being installed | `%LOCALAPPDATA%\GameModeExecutor\updates\`, emptied once the new version has started | - -**Roaming for the configuration, Local for the log**, and the split is -deliberate. Windows carries `%APPDATA%` between machines on a roaming profile -and leaves `%LOCALAPPDATA%` behind. The configuration is worth carrying — it -names no path of its own, because anything needing administrator rights is -reached through a scheduled task, and the task holds the machine-specific part. -A log is the opposite: it describes one machine's sessions, and copying it back -and forth at every logon would achieve nothing. - -## Going further - -The [design record](design/) has the measurements behind all of the above, -and why every other detection mechanism was ruled out — start with -[Detection](design/00-detection.md). The [reference](reference.md) has every -command, configuration field and exit code. +| The program | `%LOCALAPPDATA%\Programs\GameModeExecutor`, or wherever you unzipped it | +| Configuration | `config.toml` next to the executable if there is one, else `%APPDATA%\GameModeExecutor\config.toml` (roams with your profile) | +| Log | `%LOCALAPPDATA%\GameModeExecutor\logs`, one file a day, seven kept; the day changes at midnight UTC | +| Scheduled tasks | the `GameModeExecutor` folder in Task Scheduler | + +The [design record](design/) has the measurements behind all this, starting +with [Detection](design/00-detection.md); the [reference](reference.md) has +every command and setting. From 90e5f06458fbae29c081ff4952c2cee3fc028434 Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:10:24 +0200 Subject: [PATCH 04/11] Cut the reference to its tables and the rules behind them 3,056 words to 1,534. Each command, setting and file in one row; the long prose around them gone. Co-Authored-By: Claude Opus 5.5 --- docs/reference.md | 313 +++++++++++++++------------------------------- 1 file changed, 98 insertions(+), 215 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index be6c216..ed2a713 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1,72 +1,56 @@ # Reference -The exact behaviour: commands, configuration, exit codes, the log contract, -building. For the shortest path to a working setup read -[Getting started](getting-started.md); for the reasoning behind any of this, -the [design record](design/). +The exact behaviour. To get started, read [Getting started](getting-started.md); +for the reasons, the [design record](design/). ## The executables -Self-contained, no runtime dependencies. - -| Executable | What it is for | +| Executable | For | | --- | --- | -| `gamemode-executor.exe` | Everything you type. A console program, so a shell waits for it, pipes work and exit codes come back. | -| `gamemode-executorw.exe` | The same commands with no console at all: it prints nothing, and a shell does not wait for it. What the logon task runs, and what the installer runs. | -| `presence-probe.exe` | Diagnostics, not shipped in the bundle. See [Detection](design/00-detection.md#the-instrument). | - -The `w` suffix is the same convention as `python.exe` and `pythonw.exe`, for -the same reason: a program cannot be both a console and a windowless one in a -single file. Both share one library, so the commands they run are the same code. +| `gamemode-executor.exe` | Every command you type. A console program: a shell waits for it and gets its exit code. | +| `gamemode-executorw.exe` | The same commands, no console, prints nothing. Run by the logon task (`run`) and the installer (`stop`, `init`, `install-task`, `uninstall-task`). | +| `presence-probe.exe` | Diagnostics, not shipped. See [Detection](design/00-detection.md#the-instrument). | ## Commands -Both executables take the same command line. Type it into -`gamemode-executor.exe`: it answers where you can read it and a shell waits -for it. `gamemode-executorw.exe` accepts the same line but prints nothing and -nobody waits for it, which suits its two callers — the logon task, which -runs `run`, and the installer, which runs `stop`, `init`, `install-task` -and `uninstall-task`. What those commands do is written in the log either -way. - | Command | What it does | | --- | --- | -| `run` | Watch and react, in this console. The default command. For an unattended instance use `gamemode-executorw.exe`. | -| `status` | The build, the presence writer's registration and whether it runs, the session marker, what the Known Game List holds and which running processes match it, ranked by GPU rendering share, and the games marked by hand — each with whether it runs and whether Microsoft's own list now knows it. | -| `check ` | Ask whether Windows knows a given executable as a game. | -| `trigger start\|stop` | Run one set of actions immediately, ignoring detection. Handy to test your commands. | -| `validate` | Parse and check the configuration. The command to script against: it returns 3 or 4 without starting anything. | -| `init [--force]` | Write the starter configuration file into `%APPDATA%\GameModeExecutor`. One that is already there is kept unless `--force`. The installer runs this. What happened is logged under `setup`. | -| `install-task [--delay 15s] [--force]` | Register a per-user logon task that runs `gamemode-executorw.exe` with no window, then start it now. A task already registered is kept unless `--force`. The configuration path is stored absolute. The installer runs this too. Logged under `setup`. | -| `uninstall-task` | Remove that task. No task is not an error. The installer runs this on an uninstall, not on an upgrade. Logged under `setup`. | -| `update [--check]` | Look for a newer release on GitHub; with `--check`, say so and stop. Otherwise download it, verify it against the release's `SHA256SUMS.txt` and install it the way the package or the zip's shell does — a running watcher hands its game session to the new one. The one command that connects to anything. Logged under `update`. | -| `stop [--handover]` | Stop the running watcher the way *Quit* in its menu does — mid-game, the stop commands run on the way out — and wait until it has gone. With `--handover` an open game session is left to the watcher that follows: the stop commands do not run, and the next start resumes the session with nothing run twice — for an update or an upgrade, where one follows within seconds. None running is not an error. The task is left alone; `install-task` starts it again. The installer runs this before removing (plain) or replacing (`--handover`) the executables. Logged under `setup`. | -| `purge [--yes]` | Remove every trace of the program: the logon task, the configuration, every log file, the session marker, the updater's folder, the executables. It lists what it will remove and asks; `--yes` is for scripts. Refuses while a game is running. See [Removing it](how-it-works.md#removing-it). | -| `open ` | Not in the help, because it is the menu's own: *Edit configuration*, *Open log*, *Documentation* and a release page start the watcher's executable with it, so that what the shell loads to open a file stays in a process that ends rather than in the watcher. Opens `` the way Explorer would — a file no program is associated with, in Notepad — and exits: 0 when it opened, 1 when nothing could. | +| `run` | Watch and react, in this console. The default. | +| `status` | The build, the presence writer and whether it runs, the session marker, the Known Game List matches ranked by GPU share, and the games marked by hand. | +| `check ` | Whether Windows knows that executable as a game; `check --pid ` reads a running process instead. | +| `trigger start\|stop` | Run one event's commands now, to test them. | +| `validate` | Check the configuration; exits 3 or 4 on a problem. | +| `init [--force]` | Write the starter configuration to `%APPDATA%\GameModeExecutor`; an existing one is kept unless `--force`. | +| `install-task [--delay 15s] [--force]` | Register the per-user logon task running `gamemode-executorw.exe`, and start it. An existing task is kept unless `--force`. | +| `uninstall-task` | Remove that task. | +| `update [--check]` | Look for a newer release on GitHub; without `--check`, download, verify against `SHA256SUMS.txt` and install it. The only command that connects to anything. | +| `stop [--handover]` | Stop the running watcher as *Quit* does, and wait for it to go. With `--handover`, an open game session is left to the next watcher, nothing run. | +| `purge [--yes]` | Remove every trace: task, configuration, logs, session marker, updates folder, executables. Asks first; refuses during a game. | +| `open ` | Hidden: the menu's helper, which opens a file or page the way Explorer would and exits. | Global options: `--config `, `--log-level `, `--version`. +Without `--config`, `config.toml` next to the executable is used if present, +else `%APPDATA%\GameModeExecutor\config.toml`. -Without `--config`, the file is looked up next to the executable first -(`config.toml`, a hand-installed copy), then in -`%APPDATA%\GameModeExecutor\config.toml`. +The setup commands (`init`, `install-task`, `uninstall-task`, `stop`) log what +they did under `setup`, `update` under `update`. ## Configuration -TOML. Every table refuses unknown keys, so a misspelt one is reported with its -line and column. [`config.example.toml`](../config.example.toml) documents -every field; the essentials: +TOML; unknown keys are refused with their line and column. +[`config.example.toml`](../config.example.toml) documents every field. ```toml [general] -stop_actions_on_exit = true # run the stop commands if the watcher is stopped mid-game +stop_actions_on_exit = true # run the stop commands when the watcher stops mid-game log_level = "info" # error | warn | info | debug | trace #log_dir = 'C:\somewhere' # default: %LOCALAPPDATA%\GameModeExecutor\logs #log_days = 7 # days of log kept, today's included; 1 or more [detection] -poll_interval = "2s" # how often to look for a game while idle: the writer, or a game marked by hand -stop_delay = "2s" # grace after the writer exits before the session ends -identify_after = "20s" # when to ask the GPU which matched process is the game, and again while it has no answer (six times at most) +poll_interval = "2s" # how often to look for a game while idle +stop_delay = "2s" # grace after the game's signal ends +identify_after = "20s" # when to ask the GPU which process is the game; again while undecided, six attempts in all gpu_sample = "1s" [on_game_start] @@ -78,73 +62,54 @@ program = "powercfg.exe" args = ["/setactive", "SCHEME_MIN"] wait = true timeout = "10s" - -[on_game_stop] -mode = "series" - -[[on_game_stop.actions]] -name = "balanced power plan" -program = "powercfg.exe" -args = ["/setactive", "SCHEME_BALANCED"] ``` -Write Windows paths between single quotes: TOML takes those literally, so -backslashes need no doubling. +Write paths between single quotes; backslashes then need no doubling. -The watcher reads the file again whenever it changes — within about a -second of a save, the log says `Configuration reloaded` — and applies -everything but `log_dir` and `log_days`, which wait for the next start. A file it cannot -use disables it until one it can is saved: the icon turns red and its -menu's first line carries the reason; nothing runs meanwhile, and the exit -codes below are for the commands, since the watcher no longer exits over -the file. +The watcher reloads the file within a second of a save, except `log_dir` and +`log_days`, which wait for the next start. A file it cannot use disables it +until a usable one is saved. ### Actions -Each event has a mode and a list of commands. `series` runs each command after -the previous one has been waited for; `parallel` starts them all at once and -then waits. A command that fails to start is logged and never prevents the -others: an event is a set of independent side effects, not a pipeline. +`series` runs each command after the previous one is waited for; `parallel` +starts them all, then waits. A command that fails never stops the others. | Field | Default | Meaning | | --- | --- | --- | -| `name` | the program | the label used in the log | +| `name` | the program | label in the log | | `program` | required | path or name of the executable | | `args` | `[]` | one string per argument | | `working_dir` | inherited | | | `env` | `{}` | extra environment variables | -| `no_window` | `true` | start the process without a console window | -| `wait` | `false` | wait for it to exit before the next command, and record its exit status | -| `timeout` | none | give up waiting after this; the process is left running | +| `no_window` | `true` | no console window | +| `wait` | `false` | wait for it to exit and record its status | +| `timeout` | none | stop waiting after this; the process keeps running | | `enabled` | `true` | | -These placeholders are substituted in `program`, `args`, `working_dir` and -`env`: +Placeholders, substituted in `program`, `args`, `working_dir` and `env`, empty +when Windows does not name the game: | Placeholder | Value | | --- | --- | | `{event}` | `game_start` or `game_stop` | | `{process_name}` | e.g. `bf6.exe` | -| `{process_id}` | e.g. `12345` — on `game_stop` this is the id captured at the start, and that process is usually gone by then | -| `{process_path}` | the full image path, when readable | +| `{process_id}` | e.g. `12345`; at `game_stop`, the id from the start, usually gone | +| `{process_path}` | the full path, when readable | -Detection never depends on these. A game Windows tracks but does not name -leaves them empty, and everything still runs. +A stop command that fails — a waited one that exits non-zero or times out, or +one that cannot start — runs again once at the next start. At a logoff or shutdown no command +is tried; they run at the next start. A fire-and-forget command started as a +shutdown begins is lost, since nothing reports its failure. ### Programs that require administrator rights -The watcher runs unelevated, on purpose, and Windows will not let it start a -program whose manifest requires administrator rights: the attempt fails with -error 740, `ERROR_ELEVATION_REQUIRED`. The bridge is a scheduled task per -command, registered once with *run with highest privileges*; triggering a task -needs no elevation and raises no prompt, so the action becomes -`schtasks /Run /TN `. - -The [FanControl recipe](recipes/fancontrol-fan-profiles/) has the full -treatment, the task templates and a script that registers them. The watcher is -never run elevated instead: the configuration names arbitrary programs, and an -elevated watcher would turn that file into a way to run code as administrator -with no prompt. +The watcher never runs elevated, so Windows refuses such a program with error +740. Register a scheduled task *with highest privileges* once, and run +`schtasks /Run /TN `; no prompt. The +[FanControl recipe](recipes/fancontrol-fan-profiles/) has templates and a +script. An elevated watcher would let the configuration file run anything as +administrator. ## Exit codes @@ -152,166 +117,84 @@ with no prompt. | --- | --- | | 0 | success | | 1 | anything else | -| 2 | command line misuse (returned by the argument parser) | +| 2 | command line misuse | | 3 | configuration file not found | -| 4 | configuration invalid: syntax or validation | +| 4 | configuration invalid | | 5 | another instance is already running | -`3` and `4` come from `validate`, `status`, `trigger` and the setup commands, -which need a usable file. The watcher itself starts whatever the file says -and shows the fault in its icon instead. +3 and 4 come from the commands that need a usable file; the watcher shows the +fault in its icon instead. ## The log -In `%LOCALAPPDATA%\GameModeExecutor\logs` unless `log_dir` says otherwise, one -file a day, `gamemode-executor.YYYY-MM-DD.log`, local timestamps, written -synchronously. The day changes at midnight UTC — the date is -`tracing-appender`'s, which offers no other — and the running watcher moves to -the new file at its first line after it. The last `log_days` files are kept, -7 by default, sometimes one more; the oldest go, the single -`gamemode-executor.log` of earlier versions among them. *Open log* opens -the file being written. Run the watcher in a terminal and the same lines -appear there, coloured. - -One log serves two readers, and `log_level` is the dial between them: +`%LOCALAPPDATA%\GameModeExecutor\logs` (or `log_dir`), one file a day, +`gamemode-executor.YYYY-MM-DD.log`, local time, written as it happens. The day +changes at midnight UTC — the library's only choice. The last `log_days` files +are kept (7), sometimes one more. -| Level | Written for | What it promises | +| Level | For | Promise | | --- | --- | --- | -| `error` | anyone | Something needs you. It names the file or command and what to check. | -| `warn` | technician | A degradation the program absorbed and carried on from — a command that exited non-zero, a marker it could not write, a watcher that ended without stopping. | -| `info` | anyone | The story of a session, in plain sentences. **The default.** | -| `debug` | technician | Why the program did what it did — and every line above, annotated. | +| `error` | anyone | Something needs you, and it says what. | +| `warn` | technician | A degradation absorbed: a command that failed, a marker not written, a watcher that ended without stopping. | +| `info` | anyone | The story of a session. **The default.** | +| `debug` | technician | Why, with every line above annotated. | | `trace` | technician | Raw measurements. | `info` is reserved for what the program is for: a game detected, named or -gone, the watcher starting or stopping — Windows ending the session -among the reasons — a session recovered at start, the -configuration reloaded — and what was done to this machine to set it up, -which is the same story one chapter earlier. A game you marked by hand in -the Game Bar is detected as `Game detected: TOS.exe, which is marked as a -game by hand in the Game Bar`, and at start one line per such game that -Microsoft's own list now knows says the box can be unticked. Nothing else competes with -those lines. A configuration the watcher cannot use is an `error`, the one -line in the log that asks something of you: `The configuration cannot be -used, so nothing is watched until it is fixed: line 3: unknown field -'log_levl'`. - -Each line is `time LEVEL category message`, with the category one of -`watcher`, `game`, `commands`, `setup` or `update`: - -``` -2026-09-18 00:51:36.740 INFO setup Starter configuration written -2026-09-18 00:51:37.102 INFO setup Logon task registered: it starts the watcher at every logon, with no execution time limit -2026-09-10 17:51:02.433 INFO watcher GameModeExecutor 0.1.0 starting -2026-09-10 17:53:14.080 INFO game Game detected: bf6.exe -2026-09-10 17:58:41.833 INFO game Game no longer detected: bf6.exe -``` +gone; the watcher starting or stopping, Windows ending the session included; a +session recovered at start; the configuration reloaded; what the setup and +update commands did. A configuration the watcher cannot use is the one +`error`. -`setup` is written by `init`, `install-task`, `uninstall-task` and `stop`, -whether a person typed them or the installer ran them: a configuration -written, kept or replaced; a task registered, kept, replaced or removed; the -watcher started or stopped. Two processes then write the same file — `stop` -and the watcher it stops — and their lines interleave whole: the file is -opened for appending only, so Windows itself places each write at the end, -and a line is one write. -Those commands open the log where the watcher would — the configuration's -`log_dir` when a configuration can be read, the default location otherwise — -so a fresh install's first lines say what the installer did, and a machine -that misbehaves can be read back to the day it was set up. - -`update` is every step of looking for, fetching and installing a newer -release — the only thing in the program that touches a network, so each -request is written down: `Checking for updates`, `0.1.0 is the latest -version` or `Update available: 0.2.0`, `Downloading 0.2.0 (1.4 MB)`, -`Downloaded and verified 0.2.0`, `Installing 0.2.0; the watcher stops now -and comes back on the new version`, then from the new watcher `Updated to -0.2.0`. A failure is a `warn` with the WinHTTP or Windows Installer code as -a field; `RUST_LOG=update=debug` adds every request and its status. - -`debug` does not give a different log. It gives the same one annotated — the -technical detail rides along as fields rather than in lines of its own: +Each line is `time LEVEL category message`; the category is `watcher`, +`game`, `commands`, `setup` or `update`. At `debug` the same lines carry their +details as fields: ``` 2026-09-10 17:53:14.080 INFO game Game detected: bf6.exe pid=14552 matched_by="exe path" 2026-09-10 17:53:35.458 INFO game Game identified more precisely: bf6.exe (74% of the rendering) pid=14552 rendering_share=73.8 -2026-09-10 17:53:35.462 DEBUG commands Running the configured commands count=2 mode="series" ``` -A crash is logged as `FATAL:` at `error` level with the build and the location. -`RUST_LOG` overrides `log_level` when set and takes the usual `tracing` filter -syntax — `RUST_LOG=game=debug` for the detection lines alone. +A crash is a `FATAL:` line at `error`. `RUST_LOG` overrides `log_level`, with +`tracing`'s filter syntax (`RUST_LOG=game=debug`). Several processes can write +the file at once; lines never interleave, since each is one append. ## Files the watcher keeps | File | Where | Nature | | --- | --- | --- | -| Configuration | next to the executable, or `%APPDATA%\GameModeExecutor\config.toml` | yours; roams with the profile | +| Configuration | next to the executable, or `%APPDATA%\GameModeExecutor\config.toml` | yours; roams | | Log | `%LOCALAPPDATA%\GameModeExecutor\logs\` | disposable | -| Session marker | `%LOCALAPPDATA%\GameModeExecutor\pending-stop-actions` | present while a game session is open; left behind by a logoff, shutdown, crash, handover or failed stop command, and settled at the next start — the session resumed if the game is still on, its stop commands run once if it is gone. Its `state` is `open` while a watcher holds the session and `left` once one stopped and left it; `open` at a start means that watcher ended without stopping, said at `warn`. `status` reports it. | -| Fault marker | `%LOCALAPPDATA%\GameModeExecutor\configuration-fault` | present while the configuration cannot be used; removed when a usable one is read, which is how a watcher started on a file fixed meanwhile knows to say the fault is over | -| Logon task | `\GameModeExecutor\Watcher` in Task Scheduler | records the absolute path of the executable; removed with the package, kept through an upgrade | -| Updates | `%LOCALAPPDATA%\GameModeExecutor\updates\` | a downloaded release and the installer's log while an update runs; emptied when the next watcher starts, the log kept if the update failed — and, after an update through the installer, until the start after, since Windows Installer still holds it when the new watcher starts (corrected 2026-09-23) | +| Session marker | `%LOCALAPPDATA%\GameModeExecutor\pending-stop-actions` | a game session not yet closed. `state = open` while a watcher holds it, `left` once one stopped and left it to the next start; `open` found at a start means that watcher ended without stopping. | +| Fault marker | `%LOCALAPPDATA%\GameModeExecutor\configuration-fault` | the configuration was unusable when last read, so the next usable read says the fault is over | +| Logon task | `\GameModeExecutor\Watcher` | holds the executable's absolute path; removed on uninstall, kept on upgrade | +| Updates | `%LOCALAPPDATA%\GameModeExecutor\updates\` | a release being installed; emptied at the next start, the installer's log kept if the update failed | ## Building and releasing -Requires the Rust toolchain, stable, edition 2024. The Windows SDK's `rc.exe` -embeds the icon and the version block each executable carries — the one the -Properties dialog shows, with the commit in *File version*; without it the -build warns and continues. +Rust stable, edition 2024, and the Windows SDK's `rc.exe` for the icon and +version block (without it the build warns and goes on). ```powershell -.\scripts\build.ps1 # test -.\scripts\build.ps1 build # test, then a release build -.\scripts\build.ps1 release # test, build, the zip archive and the installer in dist\ +.\scripts\build.ps1 # fmt, clippy -D warnings, tests, shipped configs, doc links +.\scripts\build.ps1 build # + release build, PE subsystem check +.\scripts\build.ps1 release # + clean tree, stamped commit, dist\ zips, MSI and its ICE validation ``` -Each mode runs everything the one before it does. `test` is more than -`cargo test`: +A release is a tag `vX.Y.Z` on the commit that bumps `Cargo.toml` and dates +the version's `CHANGELOG.md` section. The workflow then runs the same script +and publishes the installer, the zips and their checksums, with that section +as the notes. A version the changelog lacks is refused. -| Step | Catches | -| --- | --- | -| `cargo fmt --check` | | -| `cargo clippy --all-targets -- -D warnings` | | -| `cargo test` | | -| every shipped `config.toml` through `validate` | a typo in a file people copy over their own | -| every documentation link resolved from its own file | a page that moved and a link that did not | - -`build` adds the release build, then reads the **subsystem out of each PE -header**: a console program and a windowless one cannot be the same file, and -getting that backwards is invisible until someone sees a black window at logon. - -A release proper is a tag. The version is bumped in `Cargo.toml` in the -release commit, `CHANGELOG.md` gains that version's section, that commit -is tagged `vX.Y.Z`, and pushing the tag makes the release workflow run -this same script on a GitHub runner, then publish the installer, the zip -and their SHA-256 checksums as a GitHub release, with the changelog section -as the notes and the commits since the previous tag below it. A version -the changelog does not carry is refused, by `release` here and by the -workflow. Nothing is built or uploaded by hand. - -`release` refuses a dirty tree, checks the commit stamped into the binaries is -the commit being built, checks the version block each executable carries, -stages the bundle, zips it into `dist\`, builds the Windows Installer package -next to it and runs the SDK's every ICE over that package — a warning fails -the build — and refuses to finish if the archive names the building account -or if a task template has lost the placeholders that make it reusable. The -package is written by `scripts\msi.ps1` from Windows Installer's own -automation; nothing but the SDK is needed, and the validation tools are -unpacked from the SDK on first use. - -**From VS Code:** `Ctrl+Shift+B` builds, and *Terminal → Run Task* offers the -same three plus two for driving an installed watcher — restart it, or follow -its log. They all call the script, so what runs in the editor is what runs in -a terminal. +From VS Code, `Ctrl+Shift+B` builds; *Run Task* has the same modes and two for +an installed watcher. ## Limits -- Detection inherits Game Bar's coverage exactly: a title Windows has not - recognised as a game triggers nothing — but it would not have got Game Mode - either. -- The writer is activated for games, not for ordinary applications, but nothing - guarantees a game is the only possible cause. `status` and the log show what - fired. -- Only one instance runs per session; a second one exits immediately with code 5. -- The time between quitting a game and Windows releasing its signal is not - predictable and not tunable — see [How it works](how-it-works.md). +- Detection is the Game Bar's: a title Windows does not see as a game triggers + nothing (and gets no Game Mode either), unless you mark it. +- Nothing guarantees only games start the presence writer; `status` and the log + show what fired. +- One watcher per session; a second exits with code 5. +- The wait between quitting and Windows' release is Windows' + ([How it works](how-it-works.md#the-wait-after-you-quit)). From 69f7cf97005667388a3536e115caa5d1387e4d64 Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:11:24 +0200 Subject: [PATCH 05/11] Cut the FanControl recipe to its steps 2,447 words to 758. The script and the XML import stay; the click-by-click Task Scheduler route goes, its settings kept in the closing table. Co-Authored-By: Claude Opus 5.5 --- .../recipes/fancontrol-fan-profiles/README.md | 377 +++--------------- 1 file changed, 66 insertions(+), 311 deletions(-) diff --git a/docs/recipes/fancontrol-fan-profiles/README.md b/docs/recipes/fancontrol-fan-profiles/README.md index b317a88..e982e81 100644 --- a/docs/recipes/fancontrol-fan-profiles/README.md +++ b/docs/recipes/fancontrol-fan-profiles/README.md @@ -1,133 +1,42 @@ # Fan profiles with FanControl -**The goal.** FanControl sits on your everyday fan curves normally, and -switches to your gaming ones the moment Windows sees a game start — then back -when you stop. +FanControl stays on your everyday fan curves, and switches to your gaming ones +while a game runs. FanControl is Rémi Mercier's work: [getfancontrol.com](https://getfancontrol.com/), -and its releases on [GitHub](https://github.com/Rem0o/FanControl.Releases). +releases on [GitHub](https://github.com/Rem0o/FanControl.Releases). It calls a +saved set of curves a **configuration**; this program's `config.toml` is called +the configuration **file** here. -FanControl calls a saved set of fan curves a **configuration** — that is the -word its menus use and the name of the folder they land in — and so does this -guide. To keep the two apart, *this program's* `config.toml` is always called -the configuration **file**. +## Why a detour -**Two roles, your names.** The watcher only knows two situations, *Idle* (no -game) and *Game*, and it triggers a scheduled task named after each: -`FanControl Idle` and `FanControl Game`. Which of *your* FanControl -configurations each task applies is decided when you register the tasks, and -lives there. Call them `Quiet` and `Game`, `Silent` and `Loud`, whatever you -like — the configuration file never has to know. +FanControl requires administrator rights, and the watcher never has them: +Windows refuses the start with error 740. So each role gets a **scheduled +task** registered once *with highest privileges*; running a task needs no +rights and shows no prompt. The watcher runs `FanControl Game` when a game +starts and `FanControl Idle` when it stops. Which of your configurations each +applies is set in the task, so `config.toml` is the same for everyone. -In this folder: - -| File | What it is | +| File | | | --- | --- | -| [`config.toml`](config.toml) | the complete configuration, ready to copy — the same for everyone | -| [`install-tasks.cmd`](install-tasks.cmd) | double-click it: runs `install-tasks.ps1`, even where Windows refuses scripts | -| [`install-tasks.ps1`](install-tasks.ps1) | registers both tasks for you, asking which configuration plays which role | -| [`uninstall-tasks.cmd`](uninstall-tasks.cmd) | the same for `uninstall-tasks.ps1` | -| [`uninstall-tasks.ps1`](uninstall-tasks.ps1) | removes those two tasks again, and nothing else | -| [`elevate.ps1`](elevate.ps1) | shared by the two scripts: asks for administrator rights so you need not open an elevated window | -| [`FanControl-Idle.xml`](FanControl-Idle.xml) | Task Scheduler definition for the *Idle* role | -| [`FanControl-Game.xml`](FanControl-Game.xml) | the same for *Game* | - -This one takes a detour, and it is worth understanding why before you start. - -## Why it is not a one-liner - -FanControl talks to your hardware, so it declares in its manifest that it -requires administrator rights. This watcher runs **without** administrator -rights, on purpose, and Windows will not let a program without them start one -that needs them: the attempt fails with error `740`, -`ERROR_ELEVATION_REQUIRED`. - -The way round is a **scheduled task** for each role, registered once with -*run with highest privileges*. Asking Task Scheduler to run a task needs no -rights at all and shows no prompt. So the watcher triggers tasks, and the tasks -run FanControl. - -You do this once. After that it is invisible. - -## 1. Check FanControl's side - -You need two configurations saved in FanControl: one for everyday use and one -for games. Their names are yours — this guide uses `Quiet` and `Game` as its -example, and you will be asked for the real ones. - -Create them in FanControl's interface if you have not already — set the curves -you want, then **Save configuration as…**. They land in the `Configurations` -folder next to `FanControl.exe` — for the installer, -`C:\Program Files (x86)\FanControl\Configurations`, service or not. A fresh -FanControl has one configuration already, `userConfig`; that is your everyday -one until you rename it, and it is fine to use it as such. - -## 2. Find where FanControl actually lives - -FanControl comes two ways, and they end up in different places: - -- **The installer** puts it in `C:\Program Files (x86)\FanControl` and offers - to run it **as a Windows service**. Both are fine here. `-c` reaches the - service exactly as it reaches the windowed application, and needs the same - elevation either way — so nothing below changes for a service install. -- **The archive** is extracted wherever you like, so there is no path to - assume. - -This finds it: - -```powershell -@( - (Get-Process FanControl -ErrorAction Ignore | Select-Object -First 1).Path, - "${env:ProgramFiles(x86)}\FanControl\FanControl.exe", - "$env:ProgramFiles\FanControl\FanControl.exe", - "$env:LOCALAPPDATA\Programs\FanControl\FanControl.exe", - "$env:USERPROFILE\scoop\apps\fancontrol\current\FanControl.exe" -) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 | Split-Path -``` - -If it prints nothing, you have a portable copy somewhere of your own choosing — -right-click your FanControl shortcut and pick **Open file location**. - -Keep that folder path. Everything below calls it **the FanControl folder**. +| [`config.toml`](config.toml) | the configuration file, ready to copy | +| [`install-tasks.cmd`](install-tasks.cmd) / [`.ps1`](install-tasks.ps1) | registers both tasks, asking which configuration plays which role | +| [`uninstall-tasks.cmd`](uninstall-tasks.cmd) / [`.ps1`](uninstall-tasks.ps1) | removes those two tasks, nothing else | +| [`elevate.ps1`](elevate.ps1) | asks for administrator rights for the two scripts | +| [`FanControl-Idle.xml`](FanControl-Idle.xml), [`FanControl-Game.xml`](FanControl-Game.xml) | the task definitions | -## 3. Create the two tasks +## 1. Two configurations in FanControl -Each task runs FanControl with `-c` and a configuration name. That flag does -exactly what is needed here: it applies the configuration, and if FanControl is -*already* running it switches the live one instead of starting a second copy. -With FanControl installed as a service, the same command hands the -configuration to the service and exits at once. +Save one for everyday use and one for games (**Save configuration as…**). +Names are yours; this page says `Quiet` and `Game`. A fresh FanControl's +`userConfig` can be the everyday one. FanControl may run as an application or +as a service; both work the same here. -| Task | Runs | Applied | -| --- | --- | --- | -| `FanControl Game` | `FanControl.exe -c Game.json` | when a game starts | -| `FanControl Idle` | `FanControl.exe -c Quiet.json` | when it stops | - -— with `Game.json` and `Quiet.json` standing for *your* configuration names. - -Registering a task that runs with highest privileges **needs administrator -rights, once**. Without them the registration is refused with `Access is -denied`. - -### The quick way: let the script do it - -[`install-tasks.ps1`](install-tasks.ps1) fills the placeholders in and registers -both tasks. **Double-click `install-tasks.cmd`** in this folder: it runs the -script with Windows' refusal of scripts set aside for that one run, and -changes nothing on the machine. If Windows asks whether to run a file that -came from the internet, allow it — or, before unzipping, right-click the zip, -*Properties*, *Unblock*. +## 2. Register the tasks -From a PowerShell window instead: `.\install-tasks.ps1`, or, where Windows -refuses scripts, `powershell -ExecutionPolicy Bypass -File .\install-tasks.ps1`. - -It asks for administrator rights itself — one prompt. On Windows 11 with -`sudo` enabled in *inline* mode it carries on in the same window; otherwise -a second window opens for the elevated part and waits for Enter before -closing. Refuse the prompt and nothing is changed. - -It finds FanControl by itself, lists the configurations you have saved, and -asks which one plays each role: +**Double-click `install-tasks.cmd`.** It runs the script even where Windows +refuses scripts, asks for administrator rights once, finds FanControl, lists +your configurations and asks which plays each role: ``` FanControl : C:\Program Files (x86)\FanControl @@ -139,143 +48,46 @@ Active now : Quiet Configuration for Idle -- applied when no game is running number or name [Quiet]: - -Configuration for Game -- applied while a game is running - number or name [Game]: ``` -Answer with a number from the list or a name as saved; the defaults in -brackets are taken with Enter. For *Idle* the default is the configuration -FanControl is applying right now — nobody runs this in the middle of a game, -so that is the everyday one; for *Game*, a configuration named `Game` if you -saved one. Anything that is not a saved configuration is refused and asked -again, and the list is read afresh each time, so you can switch to FanControl, -save the configuration you are missing, and come back to type its name. To -skip the questions, or if it cannot find FanControl, say so: +Enter takes the default. If Windows warns about a file from the internet, +allow it, or *Unblock* the zip in its *Properties* first. Run it again after +renaming a configuration. To skip the questions or point at FanControl: ```powershell .\install-tasks.ps1 -IdleConfiguration Quiet -GameConfiguration Game .\install-tasks.ps1 -FanControlDir "D:\Tools\FanControl" ``` -A name given this way that is not saved is refused too, with nothing -registered. At the end it prints the commands to test what it registered. -Run it again any time you rename a configuration: the tasks are simply -re-registered. - -### Or by hand: import the templates - -Open [`FanControl-Idle.xml`](FanControl-Idle.xml) and -[`FanControl-Game.xml`](FanControl-Game.xml) in a text editor and replace three -placeholders in each: - -| Placeholder | Replace with | -| --- | --- | -| `__DOMAIN__\__USERNAME__` | your account — run `whoami` to print it | -| `__FANCONTROL_DIR__` | the FanControl folder (it appears **twice** per file) | -| `__CONFIGURATION__` | the configuration for that role — `Quiet` in the Idle file, `Game` in the Game file, or whatever yours are called | - -Both files are UTF-16 with a BOM, the encoding Task Scheduler itself exports — -keep it if your editor asks. - -Then, from a PowerShell or Command Prompt **opened as administrator**, in this -folder: +**By hand instead:** in both XML files replace `__DOMAIN__\__USERNAME__` +(`whoami`), `__FANCONTROL_DIR__` (twice) and `__CONFIGURATION__`. Keep the +files UTF-16. Then, as administrator, in this folder: ``` schtasks /Create /XML "FanControl-Idle.xml" /TN "GameModeExecutor\FanControl Idle" /F schtasks /Create /XML "FanControl-Game.xml" /TN "GameModeExecutor\FanControl Game" /F ``` -The backslash in the name puts the task inside a **`GameModeExecutor` folder** -in Task Scheduler rather than loose at the root, next to Windows' own. The -folder is created on demand; nothing has to make it first. - -Check what landed: - -```powershell -Get-ScheduledTask -TaskPath '\GameModeExecutor\' | - Select-Object TaskName, @{n='RunLevel';e={$_.Principal.RunLevel}} -``` +Each task runs `FanControl.exe -c .json`, which switches a +running FanControl, or the service, to that configuration. -### Or by hand, in Task Scheduler +## 3. Test the tasks -Press `Win+R`, type `taskschd.msc`, and run it **as administrator**. - -First make the folder, once: right-click **Task Scheduler Library** in the left -pane → **New Folder…** → `GameModeExecutor`. Then select that folder, so what -you create next lands inside it rather than at the root. - -Then **Action → Create Task…** — not *Create Basic Task*, which does not offer -the settings that matter. - -Do this twice, once per role. For `Game`: - -**General tab** -- Name: `FanControl Game` — the folder already says which program it belongs to -- ☑ **Run with highest privileges** ← this is the entire point of the detour -- Leave *Run only when user is logged on* selected - -**Triggers tab** -- **Nothing.** Add no trigger at all. A trigger would apply a configuration on - its own; this task must only ever run when asked. - -**Actions tab** → New… -- Action: *Start a program* -- Program: the full path to `FanControl.exe` -- Add arguments: `-c Game.json` — your gaming configuration's file name -- **Start in**: the FanControl folder — without it, FanControl will not find the - configuration - -**Conditions tab** -- ☐ Uncheck **Start the task only if the computer is on AC power** -- ☐ Uncheck **Stop if the computer switches to battery power** - - *Otherwise nothing happens on a laptop running on battery.* - -**Settings tab** -- ☐ Uncheck **Stop the task if it runs longer than** -- ☐ Uncheck **If the running task does not end when requested, force it to stop** -- At the bottom, *If the task is already running…*: choose **Run a new instance - in parallel** - - *These three matter for one reason: if FanControl was not already running, the - task's own process **is** FanControl and stays alive. With the defaults, Task - Scheduler would eventually kill it, and would silently ignore every later - request to switch configuration. With the service, the task's process exits at once - and these settings never come into play — they cost nothing, and keep the - task right for both ways of running FanControl.* - -Then repeat, changing only the name to `FanControl Idle` and the argument to -your everyday configuration — `-c Quiet.json` in this guide's example. - -## 4. Test the tasks on their own - -Before involving any game. From a normal, **non**-administrator prompt: +From a normal prompt; FanControl's active configuration should change each +time: ```powershell schtasks /Run /TN "GameModeExecutor\FanControl Game" schtasks /Run /TN "GameModeExecutor\FanControl Idle" ``` -Watch FanControl's window: the active configuration should change each time. If -this does not work, nothing further will, and the problem is on this side. - -## 5. The configuration +## 4. The configuration file -Replace your `config.toml` with [this one](config.toml): copy it over, -whole, rather than merging the two — the starter file the installer wrote -has nothing in it worth keeping. Only if you already run commands of your -own should you add these two actions to your file instead. It is the -complete file: +Replace your `config.toml` with [this one](config.toml), whole. (If you already +run commands of your own, add its two actions to your file instead.) The +actions: ```toml -[general] -stop_actions_on_exit = true -log_level = "info" - -[on_game_start] -mode = "series" - [[on_game_start.actions]] name = "FanControl - Game" program = "schtasks.exe" @@ -283,9 +95,6 @@ args = ["/Run", "/TN", 'GameModeExecutor\FanControl Game'] wait = true timeout = "15s" -[on_game_stop] -mode = "series" - [[on_game_stop.actions]] name = "FanControl - Idle" program = "schtasks.exe" @@ -294,91 +103,37 @@ wait = true timeout = "15s" ``` -Note there is no path to FanControl anywhere in it, and no configuration -name either. That is the point of the detour: both live in the task, so this -file is the same for everyone and stays something a non-administrator can edit -freely. - -```bash -gamemode-executor validate -``` - -## 6. Test the chain, then turn it on - -```bash -gamemode-executor trigger start # should switch FanControl to your gaming configuration -gamemode-executor trigger stop # and back to the everyday one -``` - -You can confirm it from FanControl's own state rather than from our log — -FanControl records the active configuration in a file called `CACHE`, in its -`Configurations` folder: - -```powershell -(Get-Content "\Configurations\CACHE" -Raw | - ConvertFrom-Json).CurrentConfigFileName -``` - -When both work, save the file: the watcher reads it again by itself and -the log says `Configuration reloaded`. From the zip, on a first install, -one more command registers the logon task and starts the watcher: - -```bash -gamemode-executor install-task -``` -Done. Play a game and the fans follow. +Then `gamemode-executor trigger start` and `trigger stop` should switch +FanControl both ways. Saved, the watcher applies it at once. Play, and the fans +follow. ## Worth knowing -**FanControl must be running** for a switch to apply to a live system. -If it is not, the task starts it — which works, but is slower. Most people have -it start with Windows. - -**Nothing happens at logon.** There is no "no game" event when the watcher -starts, so FanControl keeps whatever configuration it had. Since it remembers -its last one across restarts, and the watcher applies the idle configuration -whenever a session ends, it settles correctly on its own. - -**The switch back can lag**, sometimes by a minute or more. That wait is Windows -releasing its own "a game is running" signal, not this program — -[How it works](../../how-it-works.md#the-wait-after-you-quit) explains it, and -there is nothing to tune. +- **FanControl should be running.** If not, the task starts it, more slowly. +- **Nothing is applied at logon.** FanControl keeps its last configuration, + and the watcher applies *Idle* at every session's end. +- **The switch back can lag** a minute or more: that is Windows + ([why](../../how-it-works.md#the-wait-after-you-quit)). ## Removing the recipe -Double-click **`uninstall-tasks.cmd`** in this folder, or run -`.\uninstall-tasks.ps1` from PowerShell. It asks for administrator rights the -same way the install script does. - -It removes the two tasks it knows — *FanControl Idle* and *FanControl Game* — -and leaves everything else: FanControl and its configurations, the -`\GameModeExecutor` folder in Task Scheduler with the watcher's own task in -it, and any task it did not register, which it lists. FanControl keeps -whichever configuration is active at that moment; pick the one you want in -FanControl itself. - -Then edit `config.toml`: it still names the two tasks, and the watcher would -report a failed command at the next game. Replace the commands with whatever -you want run instead, or start over from another recipe. - -GameModeExecutor does not know which recipe you followed, so removing the -program never touches these tasks; this script is how they go. - -## Adapting this to another program +Double-click `uninstall-tasks.cmd`. It removes the two tasks and lists any +other in the folder; FanControl keeps its current configuration. Then remove +the two actions from `config.toml`, or the watcher will report them failing. +Removing GameModeExecutor never touches these tasks. -Nothing here is FanControl-specific beyond `` and `` in the -task files. Any program needing administrator rights can use the same shape: -copy a template, change those two elements and the description, register it -under a new name, and point an action at it with `schtasks /Run`. +## Another program needing administrator rights -The settings to keep, and why: +Copy a template, change ``, `` and the description, +register it under a new name, and run it with `schtasks /Run`. Keep these +settings: -| Setting | Value | Reason | +| Setting | Value | Why | | --- | --- | --- | -| `` | empty | The task must only run when something asks. A trigger would fire it on its own. | -| `RunLevel` | `HighestAvailable` | The whole point: runs the program elevated without a prompt. | -| `LogonType` | `InteractiveToken` | A desktop application belongs in your interactive session. | -| `MultipleInstancesPolicy` | `Parallel` | If the program was not already running, the task's process *is* it. `IgnoreNew` would silently drop every later request. | -| `ExecutionTimeLimit` | `PT0S` | Same reason inverted: a limit would eventually kill the program. | -| `AllowHardTerminate` | `false` | Keeps the scheduler from force-killing a program it happens to own. | -| battery settings | `false` | Otherwise nothing happens on a laptop on battery. | +| `` | empty | runs only when asked | +| `RunLevel` | `HighestAvailable` | elevated without a prompt | +| `LogonType` | `InteractiveToken` | in your desktop session | +| `MultipleInstancesPolicy` | `Parallel` | if the task's process *is* the program, `IgnoreNew` drops every later request | +| `ExecutionTimeLimit` | `PT0S` | no limit to kill the program | +| `AllowHardTerminate` | `false` | the scheduler never kills it | +| battery settings | `false` | works on battery | From ee2d19e2e67dcf172685b9db3ae2994f8d782674 Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:11:24 +0200 Subject: [PATCH 06/11] Cut the power plan recipe to its steps 365 words to 212. Co-Authored-By: Claude Opus 5.5 --- docs/recipes/windows-power-plan/README.md | 72 +++++------------------ 1 file changed, 14 insertions(+), 58 deletions(-) diff --git a/docs/recipes/windows-power-plan/README.md b/docs/recipes/windows-power-plan/README.md index b6c6de7..ce639a2 100644 --- a/docs/recipes/windows-power-plan/README.md +++ b/docs/recipes/windows-power-plan/README.md @@ -1,29 +1,15 @@ # Windows power plan -**The goal.** High performance while you play, balanced the rest of the time. +High performance while you play, balanced the rest of the time. No elevation +and no scheduled task: `powercfg.exe` runs as you. -This is what most recipes look like: no elevation, no scheduled tasks, no -detour. `powercfg.exe` ships with Windows and runs perfectly well as you. - -In this folder: [`config.toml`](config.toml), the complete configuration. - -## 1. Find out which plans you have +## 1. Your plans ```powershell powercfg /list ``` -You get something like: - -``` -Power Scheme GUID: 381b4222-f694-41f0-9685-ff5bb260df2e (Balanced) * -Power Scheme GUID: 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c (High performance) -``` - -The `*` marks the active one. - -Windows accepts these aliases in place of the GUIDs, which is what the -configuration uses: +Windows accepts these aliases instead of the GUIDs: | Alias | Plan | | --- | --- | @@ -31,21 +17,14 @@ configuration uses: | `SCHEME_BALANCED` | Balanced | | `SCHEME_MIN` | High performance | -If a plan you want has no alias — an Ultimate Performance plan, or one from your -laptop's manufacturer — use its GUID from the list above instead. +A plan without an alias, such as Ultimate Performance or a manufacturer's, +goes by its GUID from the list. -## 2. The configuration +## 2. The configuration file -Copy [`config.toml`](config.toml) over your own: +Copy [`config.toml`](config.toml) over your own. Its two actions: ```toml -[general] -stop_actions_on_exit = true -log_level = "info" - -[on_game_start] -mode = "series" - [[on_game_start.actions]] name = "power plan - high performance" program = "powercfg.exe" @@ -53,9 +32,6 @@ args = ["/setactive", "SCHEME_MIN"] wait = true timeout = "10s" -[on_game_stop] -mode = "series" - [[on_game_stop.actions]] name = "power plan - balanced" program = "powercfg.exe" @@ -67,35 +43,15 @@ timeout = "10s" ## 3. Check it ```bash -gamemode-executor validate gamemode-executor trigger start ``` -Then confirm from Windows rather than from our log: - -```powershell -powercfg /getactivescheme -``` - -```bash -gamemode-executor trigger stop -``` - -When both work, save the file: the watcher reads it again by itself and -the log says `Configuration reloaded`. From the zip, on a first install, -one more command registers the logon task and starts the watcher: - -```bash -gamemode-executor install-task -``` +`powercfg /getactivescheme` should show High performance; `trigger stop` brings +Balanced back. Saved, the watcher applies the file at once. ## Worth knowing -**On a laptop, Windows may override you.** Some machines switch plans by -themselves when the power source changes, or when a manufacturer utility decides -to. If the plan does not stick, that is usually the cause, and it is outside -this program. - -**You can combine recipes.** Nothing stops you adding the fan commands from -[Fan profiles with FanControl](../fancontrol-fan-profiles/) to the same events — -list the actions one after another and they run in order. +- **A laptop may switch plans by itself** on a change of power source, or a + manufacturer's utility may. That is outside this program. +- **Recipes combine:** add the [FanControl](../fancontrol-fan-profiles/) + actions to the same events and they run in order. From 5084a80319186c3a2b8454d5508bd16ad2e1640e Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:13:28 +0200 Subject: [PATCH 07/11] Say where each lot stands today in the design index The index's status column becomes the one place for what holds now; the long status paragraphs of Lots 8, 13, 14 and 17 shrink to that, the history left in their bodies. Co-Authored-By: Claude Opus 5.5 --- docs/design/08-distribution.md | 10 +-- docs/design/13-updating.md | 18 ++--- docs/design/14-release-notes.md | 11 ++- docs/design/17-log-rotation.md | 16 ++--- docs/design/README.md | 115 ++++++++++++-------------------- 5 files changed, 61 insertions(+), 109 deletions(-) diff --git a/docs/design/08-distribution.md b/docs/design/08-distribution.md index 6e0cd2f..36a8580 100644 --- a/docs/design/08-distribution.md +++ b/docs/design/08-distribution.md @@ -1,12 +1,8 @@ # Lot 8 — Distribution -**Status: done 2026-09-18.** `v0.1.0` was published that night by the -workflow alone, from the tag's commit, with the installer, the zip and their -checksums — which is the "done when" below. The lot was taken 2026-09-17/18 -in the order listed; `scripts/build.ps1` was written first on purpose, since -a release that cannot be made by hand is not one CI can make either. The -last field check, the first install on a second machine, came in on the -evening of 2026-09-18 with its log; what it taught is below. +**Status: done 2026-09-18**, when the workflow alone published `v0.1.0` +from its tag: installer, zip and checksums. The first install on a second +machine came in the same evening; what it taught is below. - [x] Create the public GitHub repository and push — done 2026-09-17, with approval - [x] The three measurements below, on a minimal package, before any table is written — done 2026-09-17 diff --git a/docs/design/13-updating.md b/docs/design/13-updating.md index afcbdc8..ca564ee 100644 --- a/docs/design/13-updating.md +++ b/docs/design/13-updating.md @@ -1,18 +1,10 @@ # Lot 13 — Updating -**Status: done 2026-09-23**, on the first update from a published 0.2.0 -to the published 0.3.0, through the menu, below. Closed on the -maintainer's machine alone, by the maintainer's decision the same day — -*an acceptable shortcut*; the second machine updates from its own 0.2.0 -that evening, and its log is recorded here if it says anything new. -Shipped in 0.2.0 on 2026-09-18. Decided 2026-09-17 -to be a lot of its own rather than a tail of [Lot 8](08-distribution.md): -updating touches the "no network" non-goal, the tray menu and the running -process, and each of those deserves its own measurement. The shape below -was agreed with the maintainer on 2026-09-18, against `v0.1.0`, before a -line was written. The lot closes on the first update *from* 0.2.0, which -is the first version that carries it: 0.1.0 to 0.2.0 is done by hand, and -meets the Restart Manager's dialog once, as recorded below. +**Status: done 2026-09-23**, on the first update through the menu, from a +published 0.2.0 to 0.3.0 on the maintainer's machine. Shipped in 0.2.0. A lot +of its own rather than part of [Lot 8](08-distribution.md), because it +touches the "no network" non-goal, the menu and the running process. The +second machine updated itself the same way on 2026-10-01. - [x] The session handed from one watcher to the next: `stop --handover`, and a start that resumes an open session instead of closing it — three scenarios in `engine/tests.rs`, 2026-09-18 - [x] `update`: the state machine, tested whole through a scripted feed; the WinHTTP feed and the CNG hash behind it — 2026-09-18, and no test ever calls GitHub: the network path is measured by hand, below diff --git a/docs/design/14-release-notes.md b/docs/design/14-release-notes.md index 8adeac2..b7f5ae1 100644 --- a/docs/design/14-release-notes.md +++ b/docs/design/14-release-notes.md @@ -1,12 +1,9 @@ # Lot 14 — Release notes people can read -**Status: done 2026-09-18, the day it was proposed**, on the maintainer's -remark after the first update ran through the menu: *What changed in -0.1.0* opened the release page, and the page said *First public release* -over a list of commits. That is a changelog for the people who wrote the -commits, not for the person who clicked. Taken before 0.2.0, so that -release read well from the moment it was published — it did, and 0.1.0's -page was rewritten the same evening. +**Status: done 2026-09-18, the day it was proposed.** The first release +page listed commits under *First public release*: notes for those who wrote +them, not for the person who clicked *What changed*. Done before 0.2.0, and +0.1.0's page rewritten the same evening. - [x] `CHANGELOG.md`, with the sections for 0.1.0 and what 0.2.0 will carry — 2026-09-18 - [x] `release-notes.ps1` takes the section for the tag and refuses a version without one; `build.ps1 release` refuses it first, on the machine that can still write it — 2026-09-18 diff --git a/docs/design/17-log-rotation.md b/docs/design/17-log-rotation.md index 3dc4604..8f3c99b 100644 --- a/docs/design/17-log-rotation.md +++ b/docs/design/17-log-rotation.md @@ -1,15 +1,11 @@ # Lot 17 — A log that turns over each day -**Status: done 2026-09-26.** Proposed 2026-09-24 by the maintainer, and -decided the same day: `tracing-appender`, with its constraints. Taken on -2026-09-25, after Lots 8 and 9, on the condition that a spike show the file -turning over without a restart — it does, below. Closed with three things -left to see, none a reason to reopen it: the updater's zip path at the next -release, the old single file pruned around 2026-10-01, and the next real -`purge` taking the updater's folder. The second was seen on 2026-10-16, in -the folder: `gamemode-executor.log` gone, eight dated files left, from -2026-10-06 to 2026-10-15 — ten calendar days, since a day the watcher wrote -nothing has no file. +**Status: done 2026-09-26**, with `tracing-appender`, once a spike showed +the file turning over without a restart. Left to see: the updater's zip path +at the next release, and the next real `purge` taking the updates folder. The +old single file was seen pruned on 2026-10-16: eight dated files left, from +2026-10-06 to 2026-10-15, ten calendar days, since a day with no line has no +file. - [x] A spike, reproducible, of the turnover inside a running process and of what the library prunes, and when — 2026-09-25, below - [x] The log written through `tracing-appender`'s `RollingFileAppender`, daily, synchronously, with every line as it is today — built 2026-09-25, below diff --git a/docs/design/README.md b/docs/design/README.md index f928cfe..efe0e5c 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -1,88 +1,59 @@ # Design record -Why the program is the way it is: the decisions, the measurements behind -them, and what is still open. Written for someone evaluating or changing the -code — the pages for people who *use* the program are -[Getting started](../getting-started.md) and [How it works](../how-it-works.md). +Why the program is the way it is: decisions, measurements, what is open. For +using it, read [Getting started](../getting-started.md) and +[How it works](../how-it-works.md). -Work is organised in numbered **lots**. A lot is a deliverable with a stated -"done when", taken in order, closed when it is verified against a real game -session rather than when the code compiles. Each has its own page. +Work comes in numbered **lots**, each with a page and a "done when", closed +once verified in a real game session. The table says where each stands today; +the pages keep the history. -| Lot | Page | Status | +| Lot | Page | Today | | --- | --- | --- | -| — | [Detection: why the Game Bar presence writer](00-detection.md) | the foundation | -| 1 | [A console watcher that is correct and boring](01-console-watcher.md) | done | -| 2 | [Configuration, and how commands run](02-configuration.md) | done | -| 3 | [Naming the game](03-game-naming.md) | done | -| 4 | [Logs that speak to two readers](04-logging.md) | done | -| 5 | [A Windows program with no window](05-windowless-watcher.md) | done | -| 6 | [Notification area icon](06-notification-icon.md) | done | -| 7 | [Icon, tooltip and menu as one state](07-tray-state.md) | done | -| 8 | [Distribution](08-distribution.md) | done; the second machine's three proposals built and verified 2026-09-25 | -| 9 | [Robustness](09-robustness.md) | partly done; faults and live reload done; 2026-09-25: the refinement's retry built, `ShutdownBlockReasonCreate` decided against, two games back to back measured and closed | -| 10 | [Configuration window](10-configuration-window.md) | proposed; set aside until a UX/UI analysis refines it | -| 11 | [Documentation for the people who use it](11-user-documentation.md) | done | +| — | [Detection: why the Game Bar presence writer](00-detection.md) | the foundation, with the games marked by hand added by Lot 15 | +| 1 | [A console watcher that is correct and boring](01-console-watcher.md) | done 2026-09-10 | +| 2 | [Configuration, and how commands run](02-configuration.md) | done 2026-09-10 | +| 3 | [Naming the game](03-game-naming.md) | done 2026-09-10; the retry while undecided is Lot 9's | +| 4 | [Logs that speak to two readers](04-logging.md) | done 2026-09-10; one file a day since Lot 17 | +| 5 | [A Windows program with no window](05-windowless-watcher.md) | done; the logoff it could not cover is Lot 9's | +| 6 | [Notification area icon](06-notification-icon.md) | done 2026-09-15 | +| 7 | [Icon, tooltip and menu as one state](07-tray-state.md) | done 2026-09-15 | +| 8 | [Distribution](08-distribution.md) | done 2026-09-18; the second machine's proposals built 2026-09-25 | +| 9 | [Robustness](09-robustness.md) | partly done: session marker, faults, live reload, every way a session ends (2026-10-17); the refinement's retry not yet met in a game | +| 10 | [Configuration window](10-configuration-window.md) | proposed; set aside until a UX/UI analysis | +| 11 | [Documentation for the people who use it](11-user-documentation.md) | done 2026-09-14; cut for concision 2026-10-17 | | 12 | [Editing the configuration without breaking it](12-editing-on-a-copy.md) | proposed | -| 13 | [Updating](13-updating.md) | done | -| 14 | [Release notes people can read](14-release-notes.md) | done | -| 15 | [Games Windows knows only from you](15-marked-games.md) | done | -| 16 | [What the watcher keeps](16-footprint.md) | done | -| 17 | [A log that turns over each day](17-log-rotation.md) | done 2026-09-26; the updater's zip path seen at the next release | +| 13 | [Updating](13-updating.md) | done 2026-09-23 | +| 14 | [Release notes people can read](14-release-notes.md) | done 2026-09-18 | +| 15 | [Games Windows knows only from you](15-marked-games.md) | done 2026-09-23 | +| 16 | [What the watcher keeps](16-footprint.md) | done 2026-09-23 | +| 17 | [A log that turns over each day](17-log-rotation.md) | done 2026-09-26; the zip's update path to see at the next release | | 18 | [When the game is gone and nobody is there](18-game-gone-nobody-there.md) | proposed, not decided | -**Dependency order:** 1 → 2 → 4 → 5 → 6 → 7, with 3 independent and 7 needing -both 3 and 6. Logging sits before the icon deliberately — the icon logs too, -and writing it in the finished vocabulary is cheaper than converting it after. -The windowless build sits before the icon for the same reason the other way -round: the icon hangs off a window and a message loop, and those were proved -with nothing on screen before anything was drawn on them. - -The numbering moved twice while the lots were being taken — logging became -Lot 4 and the tray icon lot was split in two. The pages and the commit -messages both use the final numbers. One lot is deliberately unnumbered: a -portable mode proper, everything in one folder — see -[Lot 8](08-distribution.md) for why the zip is not that — waits for a need. - -**Standing rule:** any lot that changes what the user sees updates -`getting-started.md` and `how-it-works.md` in the same commit. Documentation -that describes a program that no longer exists is worse than none. +Logging came before the icon, so the icon was written in the final log +vocabulary; the windowless build came before it too, so the window and message +loop it hangs off were proved with nothing drawn. The numbers moved twice +early on; pages and commits use the final ones. ## Non-goals -Recorded so they stop coming back. - -- **No FanControl-specific integration.** The program runs executables; that - is all. FanControl is the example that drove the project and lives in its - recipe and nowhere else. -- **No Windows service.** Session 0 cannot see the desktop or the user's - applications. -- **No allow-list of game executables, and no heuristics that guess at what a - game is.** Detection is Windows' verdict, read from Windows. -- **No telemetry, and no network access the user did not ask for.** The one - connection the program opens is *Check for updates*, on a click, and - [Lot 13](13-updating.md) says what it sends and to whom. Nothing is ever - polled, and an opt-in check at start, if it ever comes, defaults to off. -- **No elevation.** The watcher runs as the user, on purpose. Programs that - need administrator rights are reached through a scheduled task, never by - elevating the watcher — see [Lot 1](01-console-watcher.md). -- **No configuration GUI before Lot 10.** Until then the menu opens the file - and the user brings their own editor. -- **No version key in `config.toml` or the session marker.** The marker's - contract is its presence; its contents are informational and parsed - leniently, so no future format can lose anything. For the configuration, - `deny_unknown_fields` on every table already refuses a file from a newer - program and names the key, and serde defaults absorb a file from an older - one. The one case a number would catch — a breaking rename — is a major - version bump once public, with a migration written then against a real old - format. Until there is something to migrate, the program's version is the - format's version. +- **No integration with a particular tool.** The program runs executables. + FanControl lives in its recipe. +- **No Windows service.** Session 0 cannot see the desktop. +- **No list of games, no heuristics.** Detection is Windows' verdict. +- **No telemetry, no unasked network.** One connection: *Check for updates*, + on a click ([Lot 13](13-updating.md)). +- **No elevation.** Programs needing administrator rights go through a + scheduled task ([Lot 1](01-console-watcher.md)). +- **No configuration window before Lot 10.** +- **No version key in `config.toml` or the session marker.** Unknown keys are + refused and missing ones defaulted, and the marker is read leniently; a + breaking rename would be a major version with its migration. ## Assumptions still to verify | Assumption | Status | | --- | --- | -| FanControl switches configurations with `-c .json` | Settled. Unreachable directly because the binary requires elevation; bridged through a scheduled task, and the chain verified by reading the applied configuration back from FanControl's own `CACHE` file. Holds for the installer version running as a service too. | -| The presence writer is activated for games only | Notepad was a clean negative control; not proof for every application. | -| Naming covers the titles actually played | Named in the field on every title played so far: Farming Simulator 25 and Skyrim Special Edition by executable path, Starfield by package family, Battlefield 6 by its anti-cheat's parent directory then by the GPU. | -| The writer never blinks mid-session | Holds over sessions including alt-tabs; `presence-probe watch` polls at 100 ms, so a sub-100 ms dip could hide. | +| The presence writer starts for games only | Notepad was a clean negative; not proof for every application. | +| The writer never blinks mid-session | Held over every session so far, alt-tabs included; `presence-probe watch` samples at 100 ms, so a shorter dip could hide. | +| Naming covers the titles played | Every title played so far was named, by path, package family or the GPU. | From 9935a59cdd0baea3e38a5e6e9ed6883efe4e4c31 Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:14:21 +0200 Subject: [PATCH 08/11] Tighten the unreleased changelog 432 words to 292, one short line per change, and a line for the shorter documentation. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 68 ++++++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1088400..63b8a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,54 +17,44 @@ a section is written. ### Added -- **GameModeExecutor** in the Start menu, from the installer: it starts the - watcher again after *Quit*, and does nothing if one is already running. -- The recipes as a separate zip on each release, - `GameModeExecutor-recipes-.zip`. -- The FanControl recipe links FanControl's site and its GitHub project, and - its scripts have a `.cmd` to double-click, which runs them even where - Windows refuses scripts. -- A warning at start when the last watcher ended during a game without - stopping: ended from Task Manager, crashed, or the machine lost power. +- **GameModeExecutor** in the Start menu, from the installer: starts the + watcher again after *Quit*. +- The recipes as their own zip on each release. +- The FanControl recipe links FanControl's site and GitHub, and its scripts + have a `.cmd` to double-click, which works even where Windows refuses + scripts. +- A warning at start when the last watcher ended mid-game without stopping: + Task Manager, a crash, a power cut. ### Changed -- The log turns over each day: one file a day, - `gamemode-executor.YYYY-MM-DD.log`, the last seven kept, or as many as - `log_days` in the configuration says. The day changes at midnight UTC; - the lines keep local time. *Open log* opens today's file, `purge` removes - them all, and the old `gamemode-executor.log` goes with the oldest. -- Telling a game from its launcher by what the graphics card is drawing no - longer adds some 3.5 MB to the watcher's memory for the rest of its run: - the counters are read the lighter way Windows offers for them. -- *Edit configuration*, *Open log* and *Documentation* no longer leave some - 1.2 MB in the watcher for the rest of its run, and more for each kind of - file: a short-lived helper opens them and takes that cost with it when it - ends. -- At a logoff or shutdown mid-game the watcher no longer starts the stop - commands, which Windows refuses at that point, nor warns that they failed; - they run at the next logon, as they already did. +- One log file a day, `gamemode-executor.YYYY-MM-DD.log`, seven kept or + `log_days`. The day changes at midnight UTC; lines keep local time. *Open + log* opens today's. +- Naming a game from the graphics card no longer leaves 3.5 MB in the + watcher. +- *Edit configuration*, *Open log* and *Documentation* no longer leave 1.2 MB + or more in the watcher. +- At a logoff or shutdown mid-game, no stop command is tried (Windows refuses + them then) and no warning is written; they run at the next logon, as + before. +- The documentation is shorter: each page says what you need once. ### Fixed -- `purge` leaves nothing behind any more: the lines the uninstall itself - writes to the log are deleted once it is over, and the folder updates - are downloaded to goes too, where it used to stay, empty, and keep - `%LOCALAPPDATA%\GameModeExecutor` with it. -- A game still on its loading screen twenty seconds in no longer keeps its - launcher's name for the whole session: the watcher asks again every - twenty seconds, for two minutes at most. -- Windows ending the session is written in the log at the default level. A - shutdown could leave no line at all, and look like a watcher that had - vanished. -- A stop command that fails when a game ends runs again at the next start. - Windows makes it fail when the game closes with the shutdown, and the - gaming configuration then stayed on. +- `purge` leaves nothing behind: the uninstall's own log lines and the + updates folder go too. +- A game still loading twenty seconds in no longer keeps its launcher's name: + the watcher asks again every twenty seconds, two minutes at most. +- Windows ending the session is written in the log at the default level, so a + shutdown no longer looks like a vanished watcher. +- A stop command that fails when a game ends, as at a shutdown, runs again at + the next start instead of leaving the gaming configuration on. ### Removed -- The *Writing your own* recipe: the starter configuration's comments and the - reference already list every key and placeholder. +- The *Writing your own* recipe: the starter configuration and the reference + already list every key and placeholder. ## [0.3.0] - 2026-09-23 From 0938b51931eed655c41c526113b3f2d125a137ea Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:25:20 +0200 Subject: [PATCH 09/11] Cut the recipes index, and count the naming attempts the same way everywhere The index drops the steps each recipe already has; the example configuration says six attempts in all, as the reference and the code do. Co-Authored-By: Claude Opus 5.5 --- config.example.toml | 2 +- docs/recipes/README.md | 36 ++++++++++-------------------------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/config.example.toml b/config.example.toml index bfe60df..f2e918f 100644 --- a/config.example.toml +++ b/config.example.toml @@ -77,7 +77,7 @@ stop_delay = "2s" # This far into a session, the per-process GPU counters are read and the # candidate that is actually rendering wins. While there is no answer yet -- # nothing rendering, no match -- they are read again at the same interval, six -# times at most. Set to "0s" to skip it and keep whichever process matched +# attempts in all. Set to "0s" to skip it and keep whichever process matched # first. # # This only ever changes the *name*: detection never depends on it, and being diff --git a/docs/recipes/README.md b/docs/recipes/README.md index 300aa72..f3ea3d8 100644 --- a/docs/recipes/README.md +++ b/docs/recipes/README.md @@ -1,35 +1,19 @@ # Recipes -Worked examples, start to finish. Each one is a folder: the instructions, plus -the files you need, ready to copy. Each release carries them all as -`GameModeExecutor-recipes-.zip`. - -If you have not set the program up yet, read -[Getting started](../getting-started.md) first. +Worked examples, one folder each: the steps and a complete `config.toml` to +copy over your own. Each release also carries them as +`GameModeExecutor-recipes-.zip`. Set the program up first with +[Getting started](../getting-started.md). | Recipe | What it does | Comes with | | --- | --- | --- | -| [Fan profiles with FanControl](fancontrol-fan-profiles/) | Quiet fans outside games, a game profile while playing. The full treatment for a program that needs administrator rights. | `config.toml`, two Task Scheduler templates | -| [Windows power plan](windows-power-plan/) | High performance while playing, balanced the rest of the time. The simple shape most commands have. | `config.toml` | - -Writing your own: the starter configuration's comments list every key a -command takes and every placeholder, and [the reference](../reference.md#configuration) -has the full table. - -## How to use one - -Each folder holds a `config.toml` that is the complete file, not a fragment. -Copy it over your own configuration, adjust the paths it calls out, and check -it: +| [Fan profiles with FanControl](fancontrol-fan-profiles/) | Quiet fan curves outside games, gaming ones while playing; the way to run a program that needs administrator rights. | `config.toml`, two task templates, install scripts | +| [Windows power plan](windows-power-plan/) | High performance while playing, balanced otherwise; the simple shape most commands have. | `config.toml` | -```bash -gamemode-executor validate -gamemode-executor trigger start -gamemode-executor trigger stop -``` +For your own commands, the starter configuration's comments and the +[reference](../reference.md#actions) list every field and placeholder. ## Adding one -A recipe is a folder containing at least a `README.md`, and usually a -`config.toml` beside it. Add the folder, add a row to the table above, and -nothing else moves. +A folder with a `README.md`, usually a `config.toml` beside it, and a row in +the table above. From e4d34f0441c0fc03549b53910c7ab51395cf0338 Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:27:31 +0200 Subject: [PATCH 10/11] Check documentation links down to the section build.ps1 resolved a link's file and ignored its #section, so a link to a renamed heading passed and landed at the top of the page. It now computes GitHub's anchors for each page and refuses one that is missing. Co-Authored-By: Claude Opus 5.5 --- scripts/build.ps1 | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/scripts/build.ps1 b/scripts/build.ps1 index e63f335..40bf035 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -166,6 +166,28 @@ function Invoke-Tests { } Step "Documentation links resolve" + # Sections too: a link to a renamed heading lands at the top of the page, + # which looks fine and is not. GitHub's anchors: lower case, punctuation + # dropped, spaces to hyphens, a repeated heading numbered. + $anchorsOf = @{} + function Get-Anchors([string]$path) { + if (-not $anchorsOf.ContainsKey($path)) { + $seen = @{} + $anchors = @{} + $inCode = $false + foreach ($line in Get-Content $path) { + if ($line -match '^```') { $inCode = -not $inCode; continue } + if ($inCode -or $line -notmatch '^#{1,6}\s+(.*)$') { continue } + $text = $Matches[1] -replace '\[([^\]]*)\]\([^)]*\)', '$1' -replace '[`*]', '' + $slug = ($text.ToLowerInvariant() -replace '[^\w\- ]', '') -replace ' ', '-' + $n = [int]$seen[$slug] + $seen[$slug] = $n + 1 + $anchors[$(if ($n) { "$slug-$n" } else { $slug })] = $true + } + $anchorsOf[$path] = $anchors + } + $anchorsOf[$path] + } $broken = @() $pages = @((Join-Path $root 'README.md'), (Join-Path $root 'AGENTS.md'), (Join-Path $root 'CHANGELOG.md')) + (Get-ChildItem (Join-Path $root 'docs') -Recurse -Filter '*.md' | @@ -174,11 +196,17 @@ function Invoke-Tests { $dir = Split-Path -Parent $page foreach ($match in [regex]::Matches((Get-Content $page -Raw), '\]\(([^)]+)\)')) { $link = $match.Groups[1].Value - if ($link -match '^(https?:|#)') { continue } - $target = ($link -split '#')[0] - if (-not $target) { continue } - if (-not (Test-Path (Join-Path $dir $target))) { + if ($link -match '^https?:') { continue } + $target, $anchor = $link -split '#', 2 + $path = if ($target) { Join-Path $dir $target } else { $page } + if (-not (Test-Path $path)) { $broken += "$($page.Substring($root.Length + 1)) -> $link" + continue + } + if (-not $anchor) { continue } + if (Test-Path $path -PathType Container) { $path = Join-Path $path 'README.md' } + if ($path -like '*.md' -and (Test-Path $path) -and -not (Get-Anchors $path).ContainsKey($anchor)) { + $broken += "$($page.Substring($root.Length + 1)) -> $link (no such section)" } } } From a669b5f14d37738f74c1aae92b2d9f010b53ebef Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:28:37 +0200 Subject: [PATCH 11/11] Move the release procedure into a release skill The steps were spread over AGENTS.md, loaded in every session, and memory. They now live in .claude/skills/release/SKILL.md, read only when a release is made, with the checks before, the commit, the tag and the field checks after; AGENTS.md points to it. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/release/SKILL.md | 66 +++++++++++++++++++++++++++++++++ AGENTS.md | 17 ++------- 2 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 .claude/skills/release/SKILL.md diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..1915f21 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,66 @@ +--- +name: release +description: Prepare and publish a GameModeExecutor release (vX.Y.Z), from the checks before the release commit to the field checks after it. Use when the maintainer asks to prepare, cut or publish a release. +--- + +# Releasing GameModeExecutor + +Every push, merge and tag below needs the maintainer's explicit approval, each +time. + +## 1. Ready? + +- `main` holds every lot meant for the release; nothing left on a branch. +- `CHANGELOG.md`'s `[Unreleased]` lists what the user sees, one short line per + change (the rules are in `AGENTS.md`). +- The user pages are short and current: README, *Getting started*, *How it + works*, the reference, the recipes. The design index's *Today* column says + where each lot stands. +- If the idle look or the in-game wait changed since the last release, measure + the installed watcher against it (`presence-probe cost`, `footprint`, + `menu-cost`) and put the figures in the record and the changelog. +- External links, by hand, FanControl's site and releases first — + `build.ps1` checks only the repository's own links and sections: + + ```bash + grep -rhoE 'https?://[^ )>`"]+' README.md CHANGELOG.md docs | sed 's/[.,;]$//' | sort -u + ``` + + then `curl -s -o /dev/null -L -w '%{http_code}'` each; expect 200. A URL + ending in `)` is cut by the pattern: check it whole. +- The version, per `docs/design/08-distribution.md`: patch for fixes; minor + for a feature or, while 0.x, a contract change said in the notes. 1.0.0 + waits for its criteria. + +## 2. The release commit + +On `release/X.Y.Z`, branched from `main`: + +1. `version = "X.Y.Z"` in `Cargo.toml`; a build updates `Cargo.lock`. +2. `CHANGELOG.md`: `## [Unreleased]` becomes `## [X.Y.Z] - YYYY-MM-DD`, the + commit's date, with a one-sentence summary of the release under it; a new, + empty `## [Unreleased]` above; at the bottom, `[Unreleased]` compares + `vX.Y.Z...HEAD` and `[X.Y.Z]` the previous tag with `vX.Y.Z`. +3. `.\scripts\build.ps1 test`, then commit `Release X.Y.Z`, its body saying + what the release is and why this bump. +4. `.\scripts\build.ps1 release` from that commit: clean tree, stamped commit, + `dist\` with the installer, the zip and the recipes zip. Read the whole + output down to `release OK`. + +## 3. Publish + +1. Push the branch, open the PR, wait for CI, merge. +2. Tag the merge commit and push the tag alone: + `git tag vX.Y.Z `, `git push origin vX.Y.Z`. +3. The workflow builds and publishes. Check the release page: installer, zip, + recipes zip, `SHA256SUMS.txt`, and the changelog section as the notes. + +## 4. After + +- This machine updates as a user would: *Check for updates*, *Download and + install*; read the `update` lines and the new version's start in the log. +- The second machine: the maintainer updates it from the menu and brings its + log back. +- The field checks that waited for this release, listed in memory and in the + lot pages. +- One line in `.local/journal.md`. diff --git a/AGENTS.md b/AGENTS.md index 41b5b4b..d55ba6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,19 +114,10 @@ This file says how to work here. The *why* is in - Run `test` before every commit and read its result; never commit in the same block as a check that may fail. -- `release` needs a clean tree: the binaries carry their commit, and - `--version` links to that commit's `docs/getting-started.md`. Build it from - the commit with the final documentation. -- **Publishing a release** is a tag, approved like any push. Bump `version` - in `Cargo.toml`, turn `[Unreleased]` in `CHANGELOG.md` into - `## [x.y.z] - YYYY-MM-DD` dated the release commit, merge, tag `vX.Y.Z`, - push the tag. The workflow publishes the installer, the zips and their - checksums with that section as the notes, and refuses a version the - changelog does not carry, dated. Versions follow - `docs/design/08-distribution.md`; 1.0.0 waits for its criteria. -- Before each release, check the documentation's external links by hand — - FanControl's site and releases above all. `build.ps1` checks only the - repository's own links. +- **Releasing** follows [`.claude/skills/release/SKILL.md`](.claude/skills/release/SKILL.md), + the `release` skill: checks, release commit, build, tag, field checks + after. `release` builds from a clean tree, since the binaries carry their + commit and `--version` links to that commit's `docs/getting-started.md`. - **The changelog is the agent's to write**, read by the maintainer as a diff. Each line is something the user sees or does, under *Added*, *Changed*, *Fixed* or *Removed* — never code, modules or commits, which