Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ These decide most questions before they are asked.
option first and argue for a fallback only if it protects something
concrete.
- **Discreet.** No dialogs, no windows, no sounds. The icon, its tooltip and
its menu are the whole user interface, plus a silent notification to
answer something the user clicked -- a menu closes on a click, as every
Windows menu does, and the answer has to reach them somewhere; the log is
the rest.
its menu are the whole user interface, plus a silent notification in two
cases only: to answer something the user clicked -- a menu closes on a
click, as every Windows menu does, and the answer has to reach them
somewhere -- and to say that the configuration cannot be used, and then
that it can again, the one state that needs them. The log is the rest.
- **No elevation, no service, no telemetry, and no network the user did
not ask for.** Recorded as non-goals in the design record with their
reasons. The one connection the program ever opens is *Check for updates*,
Expand Down Expand Up @@ -73,9 +74,9 @@ deleted.
otherwise.
- **The tray renders state and holds no rule.** What the icon, the tooltip
and the menu show comes from objects that own the rules — the engine's
session, `update`'s phase — and the tray asks them what to draw and which
action a click means. A rule written in the menu code is in the wrong
place and cannot be tested.
session, `update`'s phase, the supervisor's verdict on the configuration
— and the tray asks them what to draw and which action a click means. A
rule written in the menu code is in the wrong place and cannot be tested.
- **The setup commands are a contract with three callers.** `stop`, `init`,
`install-task` and `uninstall-task` are sequenced by the package
(`scripts/msi.ps1`), by the zip's after-exit shell in `update`, and by
Expand Down
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,32 @@ a section is written.

## [Unreleased]

Nothing yet.
### Added

- The watcher reads `config.toml` again whenever you save it, within a
second: no more stopping and restarting it after a change. A game in
progress is not disturbed, and the commands that run when it ends are the
ones you just saved. `log_dir` is the one setting that waits for the next
start; the log says so.
- A configuration that cannot be used shows as a **red, slashed icon**, a
silent notification with the error glyph says what is wrong in full — the
line number, the parser's words and what it expected — and the first line
of the icon's menu keeps the short of it. Nothing runs until it is fixed;
*Edit configuration* opens the file, and saving a good one brings the
icon back, with a notification saying the watcher is watching again —
also when the fix came while the watcher was stopped.

### Changed

- The watcher starts whatever the configuration file says, rather than
exiting with a code and no icon when the file is wrong at logon. `validate`
still reports the exit codes 3 and 4 for scripts.

### Fixed

- A misspelt `log_level` — `"debg"` — used to be accepted and to leave a
log with nothing but errors in it, as if the program had gone quiet. It is
now a configuration error like any other, named in the icon's menu.

## [0.2.0] - 2026-09-18

Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ windows = { version = "0.62", features = [
# fetched. Microsoft's libraries, no HTTP or hashing crate.
"Win32_Networking_WinHttp",
"Win32_Security_Cryptography",
# FindFirstChangeNotificationW: the configuration folder, watched for the
# live reload.
"Win32_Storage_FileSystem",
# WNDCLASSEXW names HBRUSH, HICON and HCURSOR, so the window class needs Gdi
# even though this program never draws anything.
"Win32_Graphics_Gdi",
Expand Down
5 changes: 3 additions & 2 deletions docs/design/06-notification-icon.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ because the size is logged at all.
The idle icon first carried a diagonal slash, which in Windows iconography
reads as *disabled* — and idle is the state the program spends nearly all its
time in. The slash moved to a distinct **error** state, reserved and unused
until [Lot 9](09-robustness.md) gives it a meaning, so nobody borrows it for
anything else. The frames were checked rather than trusted: eight PNG frames
until [Lot 9](09-robustness.md) gave it a meaning on 2026-09-19 — the
configuration cannot be used and nothing is watched — so nobody borrowed it
for anything else meanwhile. The frames were checked rather than trusted: eight PNG frames
per `.ico` at 32-bit alpha, no C2PA payload, and the four luminance figures
from the design notes reproduce exactly.
129 changes: 122 additions & 7 deletions docs/design/09-robustness.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Lot 9 — Robustness

**Status: partly done.** The session marker is built and verified; the
configuration-fault design is decided and waiting; two smaller items remain.
configuration faults and the live reload are built and verified in the
field; two smaller items remain.

- [x] Restore at the next start what a logoff could not — done 2026-09-16, a race fixed and re-verified 2026-09-17
- [ ] Configuration faults shown in the tray, and live reload — designed, below
- [x] Configuration faults shown in the tray, and live reload — built 2026-09-19, measured without a game and then verified across two Starfield sessions on 2026-09-20, below
- [ ] Stop timing the refinement; let the OS say when — below
- [ ] `ShutdownBlockReasonCreate`, so Windows' shutdown screen says what is being restored rather than naming the process
- [ ] Behaviour across two games launched back to back
Expand Down Expand Up @@ -144,11 +145,125 @@ editing goes through a staged copy, the only way to put an invalid file on
disk is to edit it by hand outside the program, and then a frozen program with
a red icon is the honest answer.

**Mechanics.** `serve` loads the configuration itself and takes the path rather
than a `Config`; the engine reads an `Arc<RwLock<Config>>` at each use, so a
swap needs no wake-up. The tray gains a fault overlay on top of the session —
two different axes — and finally sets `State::Error`. Written so Lot 12 is
small: the watcher takes a path and an "apply" action.
**Mechanics, as planned.** `serve` loads the configuration itself and takes
the path rather than a `Config`; the engine reads an `Arc<RwLock<Config>>`
at each use, so a swap needs no wake-up. The tray gains a fault overlay on
top of the session — two different axes — and finally sets `State::Error`.
Written so Lot 12 is small: the watcher takes a path and an "apply" action.

**Mechanics, as built — 2026-09-19.** The `RwLock` was not built. Reading
the configuration at each use would have covered a *valid* change and left
the *invalid* one to new engine states: frozen while idle, frozen mid-game
with the writer's exit meaning nothing, then a recovery to re-run once the
file is valid again — each a branch in the loop and a scenario nobody had
written. The handover from [Lot 13](13-updating.md) already had every one of
those: an engine that stops with the session left open in the marker, and a
start that looks for the writer before recovering. So a change to the file
is a **handover from one engine to the next in the same process**:

- `service` runs a supervisor on the worker thread — one engine per usable
configuration, built on the file as it is. The engine is unchanged but
for one line: a stop whose reason is `Reload` returns the way `Handover`
does, marker kept, nothing run.
- `StopSignal` gained a third reason and a **child**: a signal that is set
when either its own event or its parent's is, with the parent's reason
winning. The engine runs on a child of the process-wide stop; the child's
own event carries the reload and is reset between engines
(`take_reload`); the parent carries *Quit*, the logoff and `stop`, and is
never reset. That is what keeps a *Quit* arriving during a reload from
being lost, without a lock around the wait.
- `config::watch` is the thread: `FindFirstChangeNotificationW` on the
folder, waited on with the process stop, a 250 ms settle after the last
notification because editors write in several steps, and a comparison of
the file's *bytes* with what is running — a folder touched or a file
written back unchanged is not a reload, a file that no longer parses is.
- A file that cannot be used is a `LoadError` with a one-line `summary`
— `line 3: unknown field `log_levl`, expected one of …`, `the file is
missing`, `detection.poll_interval must be greater than zero` — reported
to the tray through a `FaultSink` beside the session sink. The tray reads
both facts together: a fault is `State::Error` on every surface, the
tooltip *configuration error*, the menu's first line `Configuration
error: ` and the summary, cut at 160 characters. The supervisor then parks
on the child signal: the next change or the process stop ends that, and
nothing else.
- What a reload applies to the log itself: `log_level` follows live through
a `reload::Layer` around the filter and an atomic for the fields, unless
`--log-level` or `RUST_LOG` fixed it at start; `log_dir` cannot follow —
the file is open — and is said at `warn` to wait for the next start.
- `Config::load` no longer makes the watcher exit: the command line runs
`serve` on the path alone, and the exit codes 3 and 4 are the other
commands'. The recovery that closes a session now reports *Idle* to the
tray, since after a reload the icon may still show the session the last
engine left open; a first start swallows it as a repeat.

**Measured 2026-09-19, 01:58–02:00**, a development build on a scratch
configuration while the installed watcher was stopped, with no game: a
misspelt key at `T`, the `ERROR` line and the icon refreshed to `Error` at
`T + 250 ms` on the nose, the settle; the file fixed with `log_level` moved
to `info`, *Configuration reloaded* and the fields gone from the lines that
followed; back to `debug`, *Log level changed* and the fields back; a
`poll_interval` of zero, the validation's own sentence in the menu line;
the file deleted, *the file is missing*; the file back with `log_dir`
moved, the reload and the `warn` that the log waits; then `stop`, *Stopped*.
Six changes, six reloads, one process, 54 seconds.

**Verified in the field 2026-09-20, 14:41–14:54**, by the maintainer on
the installed copy, with the release build of the branch copied over it:

- A misspelt key while idle: the `ERROR` line, the red icon, the tooltip
and the menu line; fixed, *Configuration reloaded*, the icon grey. The
maintainer's remark that the menu line is long — the parser's list of
expected fields — is accepted as it is, for want of a better single line.
- **A reload during a game.** Starfield detected at 14:47:32, the start
commands run; the stop action renamed in the file at 14:49:01: *Stopping
for a reload*, *Configuration reloaded*, *A session was left open with
Starfield.exe still running, so it resumes where it was* — no command
run, no beep, the icon green throughout. The game quit at 14:49:40 and
the stop commands that ran were the renamed ones: `FanControl - Idle
(reloaded)`.
- **A fault during a game.** Starfield again at 14:51:45; `gpu_sample`
misspelt at 14:52:43: the engine stopped, the red icon, and the game
quit into a frozen watcher — nothing ran, the fans stayed on the gaming
configuration, as the strict rule says. The file fixed at 14:54:39:
*Configuration reloaded*, then *The last session ended with Starfield.exe
still running and its stop commands never ran, so they run now*, and
the idle configuration came back by itself. Between the two, the icon
showed *playing Starfield.exe* for 6 ms — the session the stopped engine
had left in the tray, until the recovery reported *Idle* — which is the
reason that report exists.

**Said with a notification, decided 2026-09-20** on the maintainer's
remark after the run: the red icon is easy to miss at logon and the menu
line was too long to read. So a fault is said, silently, with the shell's
error glyph and the whole summary — at start and at every reload that
fails — and the end of a fault is said too, so the person knows the
watcher is back; a reload that stays usable says nothing. The menu line
keeps a headline, the summary cut before the parser's list of expected
fields. This is the second thing the program ever says unasked, beside the
answer to a click; the principle in `AGENTS.md` names both. The tray
decides the wording, the supervisor decides which transition it is — a
`Report` of *faulty*, *restored* or *usable* — since the notice depends on
what came before, which only the supervisor knows. What came before
includes the last process: the maintainer broke the file, stopped, fixed
it, started again and got no word, so a fault is noted in a second file
beside the session marker, `configuration-fault`, removed when a usable
file is read, and a start that removes one says the fault is over. `purge`
removes it with the rest.

Two defects the run found, both fixed the same day:

- `log_level = "debg"` was **not** a fault. `validate` did not look at the
value, the filter took no directive from it and fell back to `error`
alone, and the log went quiet from 14:42:33 to 14:44:07 — the *reloaded*
line that should have said what happened was itself filtered out. A
pre-existing hole, first seen because the reload made the file easy to
break: the five levels are now validated, case-insensitively, and a
sixth word is a fault with the icon and the menu line like any other.
- The debug line at the writer's exit, after a resume, read *the game was
never named* with a session of 34 s: the resumed signal has a name and
no process id, and the engine's clock started at the resume. It now says
the game was known by name only, from the resumed session, and gives the
time since the resume rather than a session length it cannot know.

## Stop timing the refinement

Expand Down
2 changes: 1 addition & 1 deletion docs/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ session rather than when the code compiles. Each has its own page.
| 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 |
| 9 | [Robustness](09-robustness.md) | partly done |
| 9 | [Robustness](09-robustness.md) | partly done; faults and live reload done, two smaller items open |
| 10 | [Configuration window](10-configuration-window.md) | proposed |
| 11 | [Documentation for the people who use it](11-user-documentation.md) | done |
| 12 | [Editing the configuration without breaking it](12-editing-on-a-copy.md) | proposed |
Expand Down
43 changes: 34 additions & 9 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,34 @@ gamemode-executor trigger start
gamemode-executor trigger stop
```

The watcher reads the file when it starts, so after editing it, restart it:
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 one setting that waits for the next start
is `log_dir`, 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 stop
gamemode-executor install-task
```

The first is **Quit** from the icon's menu, typed. The second starts it again
— and, from the zip, registers the task that starts it at every logon, once.
No administrator rights, no password, no window. Doing this while a game is
running? `stop --handover` instead of `stop`: the game session is left to the
new watcher, which takes it up where it was without running anything.

**That is the end of the setup.** Play. The commands fire by themselves.
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/) has
one per job, each with a `config.toml` you can copy straight over.
Expand All @@ -133,6 +147,7 @@ the clock. It is the only thing this program ever puts on screen.
| --- | --- |
| **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.
Expand Down Expand Up @@ -272,6 +287,16 @@ 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
Expand Down
Loading