Files
lhns--steam-frame-nix/docs/ui-patches.md
T
Pierre Kisters 5a3d0b9d05 README: full user documentation again, docs/ only technical
The README is the complete user documentation again: features, install,
two sessions, usage, the full options table (and renamed options), every
feature with problem, usage, limitations, security and "remove when",
UI patches at user level (DevTools on the LAN, after a Steam update),
changes outside Nix, rollback and uninstall.

docs/ keeps only the technical side (how the patches work, writing
patches, signatures and the update procedure, the SteamVR debugger
mechanics, cleanup and installer internals), linked from each feature.
docs/options.md, two-sessions.md and desktop-integration.md are gone
(their content is in the README).
2026-09-29 00:47:16 +02:00

183 lines
7.5 KiB
Markdown

# UI patches
For patch authors, and for fixing patches after a Steam update. User-level
overview: README, [UI patches](../README.md#ui-patches-uipatchespatches);
options `steamFrame.uiPatches.patches` and `steamFrame.uiPatches.lib`.
Steam's UI and SteamVR's dashboard (`vrwebhelper`) are CEF web pages with a
local DevTools port: `127.0.0.1:8080` for Steam (SteamOS passes
`-cef-enable-debugging`), `127.0.0.1:8087` for SteamVR once
[its debugger](steamvr-debugger.md) is on. The `steam-ui-patches` user
service (`injector.mjs`) patches the running pages through them; Steam's
files are never modified. The [launcher menu](launcher-menu.md),
[dashboard](dashboard.md) and [VR keyboard](keyboard.md) features are such
patches (the extra keys have their own injector, `steam-keyboard-patch`),
and you can add your own.
Don't expose these ports: see
[DevTools on the LAN](../README.md#devtools-on-the-lan).
**Caveat:** patches depend on Steam UI internals and can break with an
update; find modules by signature, not id ([below](#finders-and-signatures)).
## Defining a patch
`steamFrame.uiPatches.patches` entries:
| Attribute | Default | Description |
|---|---|---|
| `name` | | Unique name (log). |
| `endpoint` | `"http://127.0.0.1:8080"` | DevTools base URL; `/json/list` is polled every 5 s. |
| `target.title` / `target.titleRegex` / `target.urlRegex` | `null` | Pages to patch; all given criteria must match (JS regexes). |
| `patch` | | JS file evaluated in every matching page (awaited). |
| `unpatch` | `null` | JS file evaluated when the service stops. |
| `state` | `false` | Give the patch one [persistent JSON value](#persistent-state). |
```nix
steamFrame.uiPatches.patches = [ {
name = "my-patch";
target.title = "SharedJSContext"; # Steam's main JS context
patch = ./my-patch/patch.js;
unpatch = ./my-patch/unpatch.js;
} ];
```
Patches are evaluated on attach, after new JS contexts (reloads) and every
15 s, so they must be idempotent: return e.g. `"patched"` once, then
`"unchanged"` (not logged); other results are logged when they change
(`journalctl --user -u steam-ui-patches`). On stop, `unpatch` restores the
stock UI without a Steam restart. The service exists only while the list is
non-empty and is restarted on every switch.
## Persistent state
With `state = true`: a patch's choices made in the UI can't be kept in the
page's own storage, since SteamOS's `steamvr.service` runs
`rm -rf ~/.cache/SteamVR` (vrwebhelper's browser profile, incl.
localStorage) on every SteamVR start. So the service keeps one JSON value
per such patch in `~/.local/state/steam-frame-nix/ui-patches/<name>.json`
(`$XDG_STATE_HOME`):
- before each evaluation it defines `window.__sfuiStore.get(name)` /
`.set(name, value)` in the page and fills `get` from the file only while
the page has no value yet (a fresh page after a SteamVR restart, reboot or
reload);
- `set` goes through the DevTools binding `window.__sfuiStoreSave`; the
service writes the file atomically, only on change, only for that page's
`state` patches, at most 64 KiB.
Used by the [window control bar](dashboard.md#window-control-bar) and the
[Steam close button](dashboard.md#steam-close-button). The file is user
data, not generated by Nix: it is kept when the patch is disabled or removed
(the choices come back when you enable it again) and removed only by
`steam-frame-nix-cleanup --all` or `install.sh uninstall`; see
[changes outside Nix](../README.md#changes-outside-nix-exceptions).
## Finders and signatures
Webpack module ids and export names change with every Steam UI build, so
patches never use them. `modules/lib/finders.js` (like Decky Loader's
`findModule`/`findInReactTree` or Vencord's `find`) locates by *signature*:
- a **module** by strings/regexes in its factory source;
- an **export** by shape: type, function source, arity, data properties,
prototype methods or getters;
- **React** fibers by props (`findFiberUp`, `findFiberDown`,
`findInReactTree`).
Every signature must match exactly once, otherwise the patch changes nothing
and reports it (e.g. `signature not found, Steam left unpatched:
layouts.currentLayout (module 40222): ambiguous export, candidates r_, xy`).
Results are cached per page.
Signatures live in `modules/lib/signatures.json`, shared by patches and the
offline checker. An entry can also list `expects` (strings the patch relies
on, checked offline only) or be `checkOnly` (anchors not used through the
finder, checked offline only; with `stylesheet` instead of `module` it is
matched against the bundle's CSS).
### mkPatch
`steamFrame.uiPatches.lib.mkPatch` wraps a patch file — a function
expression `(find, sigs, opts, hooks) => …` returning a status string — with
the finder library, its signatures, options and the shared hooks (details in
`modules/lib/default.nix`):
```nix
steamFrame.uiPatches.patches = [ {
name = "my-patch";
target.title = "SharedJSContext";
patch = config.steamFrame.uiPatches.lib.mkPatch {
name = "my-patch";
src = ./my-patch/patch.js; # ((find, sigs, opts, hooks) => { … })
signatures.thing = {
module.includes = [ "SomeUniqueString" ];
exports.Thing = { type = "class"; protoMethods = [ "DoIt" ]; };
};
opts.factor = 2;
};
unpatch = ./my-patch/unpatch.js;
} ];
```
```js
((find, sigs, opts, hooks) => {
let mods;
try { mods = find.resolveAll(find.getWebpackRequire('webpackChunksteamui'), sigs); }
catch (e) { return `not patched: ${e.message}`; }
const Thing = mods.thing.exports.Thing; // SteamVR dashboard: 'webpackChunkvrwebui'
…
})
```
### Shared method hooks
`modules/lib/hooks.js`, argument `hooks`, also `window.__sfuiHooks`: patches
intercepting the same method (e.g. the dashboard mailbox's `SendMessage`,
used by [dashboard windows](dashboard.md#dashboard-windows) and
[window curvature](dashboard.md#window-curvature)) register named hooks; one
wrapper per method runs them in registration order, so patches can be
injected, upgraded and reverted in any order.
```js
hooks.before(Mailbox.prototype, 'SendMessage', 'my-patch', (args) => {
if (args[1]?.type === 'update_scene_graph') rewrite(args[1].scene_graph);
});
hooks.remove(Mailbox.prototype, 'SendMessage', 'my-patch'); // in unpatch
```
Patches reacting to presses on the curvature controls follow the
[window curvature contract](dashboard.md#contract-for-other-patches) (`sfui-curv-*`).
## After a Steam update
Check the signatures offline (Steam need not run) from a checkout:
```sh
nix shell nixpkgs#nodejs -c node scripts/check-signatures.mjs
```
It loads Steam's UI bundle (`~/.local/share/Steam/steamui`) and SteamVR's
dashboard (`/opt/steamvr/resources/webinterface/dashboard/systemui.html`),
evaluates every signature with the patches' finder code (exports in an inert
sandbox) and prints per patch `found` (module id, export name), `ambiguous`
or `missing`, plus warnings for missing `expects`; exit status 1 if anything
is missing or ambiguous:
```
bundle steamui: 2827 modules in /home/deck/.local/share/Steam/steamui (build 11041156)
steam-keyboard-patch (steamui)
layouts found module 40222 (chunk~2dcc5aaf7.js)
.currentLayout found export r_
…
OK: all signatures match exactly once
```
To fix a signature, inspect the new module sources
(`scripts/webpack-modules.mjs`), adjust `signatures.json` and bump the
patch's `VERSION` if its code changes. Flags: `--signatures FILE` (your own,
bundles `steamui` / `vrwebui-systemui`), `--dir steamui=DIR`,
`--patch NAME`, `--strict` (fail on warnings), `--json`. Live:
`journalctl --user -u steam-keyboard-patch -u steam-ui-patches`.