From 5d15902ba1e6214141cf8d9d86293f382b4521a3 Mon Sep 17 00:00:00 2001 From: Pierre Kisters <1524059+lhns@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:42:34 +0200 Subject: [PATCH] Clearer file names and layout; docs/development.md - steam-keyboard-patch.nix -> vr-keyboard-extra-keys.nix (option keyboard.vr.extraKeys), helper.mjs -> xdotool-helper.mjs; homeManagerModules.steam-keyboard-patch stays as an alias. - vr-keyboard: panel.js/unpatch-panel.js/relay.mjs -> suggestions-panel/{patch,unpatch}.js + relay.mjs; decoder.js -> swipe-decoder.js; build.nix split into dictionary.nix and check.nix. - modules/lib -> modules/steam-ui-patches/lib (the UI patch library). - docs/development.md: repository layout (what runs where), runtime names. Patch names, user services and state files are unchanged. --- README.md | 10 +- docs/development.md | 128 ++++++++++++++++++ docs/keyboard.md | 17 ++- docs/launcher-menu.md | 2 +- docs/ui-patches.md | 13 +- flake.nix | 20 ++- modules/dashboard-windows.nix | 2 +- modules/dashboard-windows/patch.js | 8 +- modules/frame-controls.nix | 2 +- modules/frame-controls/patch.js | 4 +- modules/launcher-menu.nix | 2 +- modules/launcher-menu/grid/patch.js | 6 +- modules/launcher-menu/launch/patch.js | 5 +- modules/launcher-menu/pinned-desktop/patch.js | 6 +- modules/launcher-menu/show-all/patch.js | 5 +- modules/steam-close-button.nix | 2 +- modules/steam-ui-patches.nix | 15 +- .../{ => steam-ui-patches}/lib/default.nix | 0 modules/{ => steam-ui-patches}/lib/finders.js | 0 modules/{ => steam-ui-patches}/lib/hooks.js | 0 .../lib/signatures.json | 0 ...d-patch.nix => vr-keyboard-extra-keys.nix} | 13 +- .../patch.js | 11 +- .../unpatch.js | 6 +- .../xdotool-helper.mjs} | 10 +- modules/vr-keyboard.nix | 37 ++--- modules/vr-keyboard/build.nix | 39 ------ modules/vr-keyboard/check.nix | 30 ++++ modules/vr-keyboard/dictionary.nix | 24 ++++ modules/vr-keyboard/patch.js | 12 +- .../{panel.js => suggestions-panel/patch.js} | 5 +- .../{ => suggestions-panel}/relay.mjs | 7 +- .../unpatch.js} | 4 +- .../{decoder.js => swipe-decoder.js} | 6 +- modules/vr-keyboard/tests/corrector.test.mjs | 2 +- ...ecoder.test.mjs => swipe-decoder.test.mjs} | 5 +- modules/window-curvature.nix | 2 +- modules/window-curvature/patch.js | 14 +- scripts/check-signatures.mjs | 14 +- scripts/vr-keyboard-replay.mjs | 3 +- 40 files changed, 330 insertions(+), 161 deletions(-) create mode 100644 docs/development.md rename modules/{ => steam-ui-patches}/lib/default.nix (100%) rename modules/{ => steam-ui-patches}/lib/finders.js (100%) rename modules/{ => steam-ui-patches}/lib/hooks.js (100%) rename modules/{ => steam-ui-patches}/lib/signatures.json (100%) rename modules/{steam-keyboard-patch.nix => vr-keyboard-extra-keys.nix} (80%) rename modules/{steam-keyboard-patch => vr-keyboard-extra-keys}/patch.js (98%) rename modules/{steam-keyboard-patch => vr-keyboard-extra-keys}/unpatch.js (85%) rename modules/{steam-keyboard-patch/helper.mjs => vr-keyboard-extra-keys/xdotool-helper.mjs} (93%) delete mode 100644 modules/vr-keyboard/build.nix create mode 100644 modules/vr-keyboard/check.nix create mode 100644 modules/vr-keyboard/dictionary.nix rename modules/vr-keyboard/{panel.js => suggestions-panel/patch.js} (98%) rename modules/vr-keyboard/{ => suggestions-panel}/relay.mjs (92%) rename modules/vr-keyboard/{unpatch-panel.js => suggestions-panel/unpatch.js} (52%) rename modules/vr-keyboard/{decoder.js => swipe-decoder.js} (97%) rename modules/vr-keyboard/tests/{decoder.test.mjs => swipe-decoder.test.mjs} (97%) diff --git a/README.md b/README.md index 014dfe0..6059f0c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ configuration, limitations and how it works. **Your own patches** of Steam's UI, and fixing patches after a Steam update: [UI patches](docs/ui-patches.md). +**Working on steam-frame-nix:** [Development](docs/development.md) (repository layout: which file runs where, runtime names, checks). + ## Install On the Frame (or a Steam Deck), in a terminal (Konsole in desktop mode or the @@ -213,9 +215,11 @@ home-manager switch --flake .#steamos # manual setup, from the flake's director In your own flake, add the input as above and `steam-frame-nix.homeManagerModules.default` to the modules. `default` imports all modules; single ones: -`homeManagerModules.{session,portal,keyboard-layout,steam-keyboard-patch,vr-keyboard,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,cleanup,dashboard-windows,steam-close-button,window-curvature,frame-controls,clipboard-sync,firefox,jellyfin,keyring,docker}`. -Every module imports `cleanup` (see -[Changes outside Nix](#changes-outside-nix-exceptions)). +`homeManagerModules.{session,portal,keyboard-layout,vr-keyboard-extra-keys,vr-keyboard,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,cleanup,dashboard-windows,steam-close-button,window-curvature,frame-controls,clipboard-sync,firefox,jellyfin,keyring,docker}` +(`steam-keyboard-patch` still works as the former name of +`vr-keyboard-extra-keys`). Every module imports `cleanup` (see +[Changes outside Nix](#changes-outside-nix-exceptions)); which file is +which: [Repository layout](docs/development.md). ## Options diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..951b1ab --- /dev/null +++ b/docs/development.md @@ -0,0 +1,128 @@ +# Development + +Which file does what and where it runs, for working on steam-frame-nix +itself. Writing your own patches: [UI patches](ui-patches.md). + +## Conventions + +- `modules/.nix` is `homeManagerModules.`; files only it uses + are in `modules//`. +- `patch.js` / `unpatch.js`: a runtime patch of a web page and the + expression that reverts it. A module with several patches has one + directory per patch (`launcher-menu/grid/`, + `vr-keyboard/suggestions-panel/`). +- `*.js` in `modules/` is browser JavaScript evaluated in Steam's or + SteamVR's pages (single expressions, no imports); `*.mjs` is a Node + program (user service, test or script). +- `check.nix` is the module's flake check (`nix flake check`, attribute + `checks..`); `tests/*.test.mjs` are run by it. +- Runtime names (patch names, user services, state files) are listed + [below](#runtime-names); they stay stable when files move. + +Where things run: + +| Tag | Where | +|---|---| +| **Steam** | Steam's UI, page `SharedJSContext`, DevTools `127.0.0.1:8080` | +| **SteamVR** | SteamVR's dashboard (`vrwebhelper`), page `systemui`, DevTools `127.0.0.1:8087` | +| **service** | systemd user service (outer Steam/VR session) | +| **switch** | Home Manager activation (`home-manager switch`) | +| **build** | Nix build time | +| **app** | started with an app (launcher, `LD_PRELOAD`, autostart) | +| **test** | `nix flake check` | +| **dev** | run by hand from a checkout | + +## Repository layout + +```text +flake.nix homeManagerModules, packages/apps (cleanup), checks, template +install.sh install / uninstall / cleanup (curl | bash; also the + steam-frame-nix-cleanup package and the debugger arm step) +template/ `nix flake init -t` / installer config: flake.nix, home.nix +docs/ one page per feature; ui-patches.md for patch authors +scripts/ + check-signatures.mjs dev: check signatures.json against the installed Steam + webpack-modules.mjs dev: webpack module extraction (used by check-signatures) + vr-keyboard-replay.mjs dev: replay recorded swipes through the swipe decoder +modules/ + session.nix switch: outer bus/runtime dir, user services on switch + cleanup.nix switch: `cleanup --orphans`; steam-frame-nix-cleanup on PATH + cleanup/package.nix build: install.sh as a command (cleanup, steamvr-debugger-arm) + cleanup/check.nix test: install.sh cleanup on fake home/runtime dirs + portal.nix Steam session portal config (session.portalFix) + keyboard-layout.nix gamescope-session drop-in with XKB_DEFAULT_* (keyboard.layout) + clipboard-sync.nix app: KDE autostart of clipboard-sync (clipboardSync) + hidden-apps.nix Hidden=true desktop entries (launcherMenu.hiddenApps) + keyring.nix app: launchers sharing the KDE wallet (keyring) + docker.nix service: rootless dockerd (docker) + firefox.nix Flatpak prefs extension and launcher entry (firefox) + firefox/launcher.nix app: launcher script (desktop profile, fullscreen fix) + firefox/check.nix test: launcher against a fake flatpak + jellyfin.nix desktop entry with flatpak run options (jellyfin.hardwareDecoding) + jellyfin/mpv-hwdec-shim.c app: LD_PRELOAD shim, hwdec auto* -> v4l2m2m-copy + jellyfin/shim.nix build: the shim as lib/mpv-hwdec-shim.so + jellyfin/check.nix test: shim ELF and rewriting + steam-ui-patches.nix service steam-ui-patches: runs the injector (uiPatches.*) + steam-ui-patches/ + injector.mjs service: injects/re-injects/reverts patches over DevTools + lib/default.nix build: mkPatch (wraps a patch.js with the library) + lib/finders.js Steam+SteamVR: signature lookup of webpack modules/React fibers + lib/hooks.js Steam+SteamVR: shared method hooks (e.g. SendMessage) + lib/signatures.json build+dev: per-patch signatures (also check-signatures) + steamvr-debugger.nix service steamvr-webhelper-debugger: DevTools port 8087 + launcher-menu.nix the VR "+" menu (launcherMenu.*), icon fallbacks + launcher-menu/ + order/ Steam: sort the list + pinned-desktop/ Steam: pin Desktop above/below the list + launch/ Steam: close on launch, debounce + grid/ Steam: programs as a grid of tiles + show-all/ Steam: all programs without Developer Mode + icon-fallbacks.sh switch: hints for iconFallbacks (read-only) + vr-keyboard-extra-keys.nix Esc/Ctrl/Alt/arrows etc. (keyboard.vr.extraKeys) + vr-keyboard-extra-keys/ + patch.js, unpatch.js Steam: the extra bottom row and key routing + xdotool-helper.mjs service steam-keyboard-patch: own injector + xdotool keys + vr-keyboard.nix swipe, suggestions, Backspace drag (keyboard.vr) + vr-keyboard/ + patch.js, unpatch.js Steam: gestures, text model, suggestion strip + swipe-decoder.js Steam (argument of patch.js): swipe path -> words + textmodel.js Steam (argument of patch.js): what the keyboard typed + corrector.js Steam (argument of patch.js): corrections, completions + suggestions-panel/ + patch.js, unpatch.js SteamVR: the strip as a panel above/below the keyboard + relay.mjs service vr-keyboard-relay: strip state Steam <-> SteamVR + dictionary.nix, gen-dict.py build: dictionary from wordfreq + Hunspell + check.nix, tests/ test (also built before the patch): text model, + corrector, swipe-decoder accuracy + dashboard-windows.nix, dashboard-windows/ SteamVR: window scale/distance limits + steam-close-button.nix, steam-close-button/ SteamVR: X on the Steam window + window-curvature.nix, window-curvature/ SteamVR: curvature wheel + frame-controls.nix, frame-controls/ SteamVR: window control bar +``` + +## Runtime names + +Patch names appear in the journal, key `signatures.json` and name the +state files (`~/.local/state/steam-frame-nix/ui-patches/.json`), so +they are kept even where a file name says more: + +| Module | Option | Patches (page) | User services | +|---|---|---|---| +| `launcher-menu` | `launcherMenu` | `launcher-menu-{order,pinned-desktop,launch,grid,show-all}` (Steam) | `steam-ui-patches` | +| `vr-keyboard` | `keyboard.vr` | `vr-keyboard` (Steam), `vr-keyboard-panel` = `suggestions-panel/` (SteamVR) | `steam-ui-patches`, `vr-keyboard-relay` | +| `vr-keyboard-extra-keys` | `keyboard.vr.extraKeys` | `steam-keyboard-patch` (Steam) | `steam-keyboard-patch` | +| `dashboard-windows` | `dashboard.windows` | `dashboard-windows` (SteamVR) | `steam-ui-patches` | +| `steam-close-button` | `dashboard.steamCloseButton` | `steam-close-button` (SteamVR, state) | `steam-ui-patches` | +| `window-curvature` | `dashboard.windowCurvature` | `window-curvature` (SteamVR) | `steam-ui-patches` | +| `frame-controls` | `dashboard.frameControls` | `frame-controls` (SteamVR, state) | `steam-ui-patches` | +| `steamvr-debugger` | `steamvrDebugger` | | `steamvr-webhelper-debugger` | + +Log of all patches: +`journalctl --user -u steam-ui-patches -u steam-keyboard-patch -u vr-keyboard-relay`. + +## Checks + +```sh +nix flake check # all checks, on aarch64-linux +nix shell nixpkgs#nodejs -c node scripts/check-signatures.mjs --strict # on the Frame +``` diff --git a/docs/keyboard.md b/docs/keyboard.md index 0631b21..972c0ec 100644 --- a/docs/keyboard.md +++ b/docs/keyboard.md @@ -8,7 +8,8 @@ The keyboard layout of the Steam session itself is a separate fix ## Extra keys -`keyboard.vr.extraKeys.enable`, module `steam-keyboard-patch`. +`keyboard.vr.extraKeys.enable`, module `vr-keyboard-extra-keys` (sources in +`modules/vr-keyboard-extra-keys/`). **Problem:** Steam's VR keyboard has no Ctrl, Alt or Esc, can't press real keys, and its text emulation only maps plain ASCII: non-ASCII and @@ -55,9 +56,10 @@ Steam client 1790377368 (UI build 11041156). ### How it works -- The `steam-keyboard-patch` user service (`helper.mjs`) injects a patch - into Steam's `SharedJSContext` over DevTools (`127.0.0.1:8080`) and - re-injects it after Steam restarts; stopping it (or disabling the option) +- The `steam-keyboard-patch` user service + (`modules/vr-keyboard-extra-keys/xdotool-helper.mjs`) injects a patch into + Steam's `SharedJSContext` over DevTools (`127.0.0.1:8080`) and re-injects + it after Steam restarts; stopping it (or disabling the option) runs the unpatch. - Ctrl/Alt chords and Esc are sent with `xdotool key` on `:0` (focus follows the VR-selected window); a toggled Ctrl/Alt is held down with `xdotool`. @@ -133,15 +135,16 @@ has the letters. ### How it works - A Steam UI patch (`vr-keyboard`, injected by `steam-ui-patches` like the - other [UI patches](ui-patches.md)). + other [UI patches](ui-patches.md)); sources in `modules/vr-keyboard/`. - Swiped words are matched by shape (SHARK2-style template matching) against a dictionary built at build time from wordfreq frequency lists and Hunspell, both from nixpkgs (per language the `words` most frequent wordfreq entries, shifted by `frequencyOffset`, filtered by Hunspell except words at or above `keepFrequentAbove`). - The strip below/above the keyboard is a SteamVR dashboard panel - (`panel.js`, a patch of SteamVR's `systemui` page on port 8087), fed by - the `vr-keyboard-relay` user service (`relay.mjs`) between the two pages. + (`suggestions-panel/patch.js`, a patch of SteamVR's `systemui` page on + port 8087), fed by the `vr-keyboard-relay` user service + (`suggestions-panel/relay.mjs`) between the two pages. With `inside` neither the panel nor the relay runs. - Found by signature (entries `vr-keyboard`, `vr-keyboard-panel`). diff --git a/docs/launcher-menu.md b/docs/launcher-menu.md index 965a057..f6b5a49 100644 --- a/docs/launcher-menu.md +++ b/docs/launcher-menu.md @@ -79,7 +79,7 @@ its option is set and reverted by its unpatch when unset: sounds and controller navigation keep working; - `show-all/`: empties the list Steam hides without Developer Mode. -Their anchors (APIs, React props, CSS) are in `modules/lib/signatures.json` +Their anchors (APIs, React props, CSS) are in `modules/steam-ui-patches/lib/signatures.json` and verified by the [offline checker](ui-patches.md#after-a-steam-update). ## Hidden apps diff --git a/docs/ui-patches.md b/docs/ui-patches.md index 0d863c9..e28772d 100644 --- a/docs/ui-patches.md +++ b/docs/ui-patches.md @@ -2,7 +2,8 @@ `uiPatches.patches`, `uiPatches.lib` (modules `steam-ui-patches`, `steamvr-debugger`). For patch authors, and for fixing patches after a Steam -update. Options: [README, Options](../README.md#options). +update. Options: [README, Options](../README.md#options). Which file runs +where: [Development](development.md). ## Problem @@ -53,7 +54,7 @@ it runs. What they have in common: - Each turns on the [SteamVR debugger](steamvr-debugger.md) (**the first time, restart SteamVR once**). - They depend on SteamVR UI internals: each is found by signature (its entry - in `modules/lib/signatures.json` has the patch's name); after an update + in `modules/steam-ui-patches/lib/signatures.json` has the patch's name); after an update that changes them the dashboard stays stock (see [after a Steam update](#after-a-steam-update)). Tested with SteamVR build 11008059. @@ -110,7 +111,7 @@ 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 +patches never use them. `modules/steam-ui-patches/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; @@ -126,7 +127,7 @@ candidates r_, xy` as the patch's result. Results are cached per page. The library also has `ensureStyle(doc, id, css)` and `logger(buffer)` (a capped debug log). -Signatures live in `modules/lib/signatures.json`, shared by patches and the +Signatures live in `modules/steam-ui-patches/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 @@ -137,7 +138,7 @@ matched against the bundle's CSS). `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`): +`modules/steam-ui-patches/lib/default.nix`): ```nix steamFrame.uiPatches.patches = [ { @@ -167,7 +168,7 @@ steamFrame.uiPatches.patches = [ { ### Shared method hooks -`modules/lib/hooks.js`, argument `hooks`, also `window.__sfuiHooks`: patches +`modules/steam-ui-patches/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-windows.md#how-it-works) and [window curvature](window-curvature.md#how-it-works)) register named hooks; diff --git a/flake.nix b/flake.nix index aaa9665..8a971b8 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,7 @@ session = ./modules/session.nix; portal = ./modules/portal.nix; keyboard-layout = ./modules/keyboard-layout.nix; - steam-keyboard-patch = ./modules/steam-keyboard-patch.nix; + vr-keyboard-extra-keys = ./modules/vr-keyboard-extra-keys.nix; vr-keyboard = ./modules/vr-keyboard.nix; hidden-apps = ./modules/hidden-apps.nix; steam-ui-patches = ./modules/steam-ui-patches.nix; @@ -37,10 +37,15 @@ keyring = ./modules/keyring.nix; docker = ./modules/docker.nix; }; + # Former attribute names, kept so existing imports keep working (not in + # `default`, which imports each module once under its current name). + aliases = { + steam-keyboard-patch = modules.vr-keyboard-extra-keys; # its name until 2026-09 + }; systems = [ "aarch64-linux" "x86_64-linux" ]; forSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); in { - homeManagerModules = modules // { + homeManagerModules = modules // aliases // { default = { imports = builtins.attrValues modules; }; }; @@ -64,16 +69,7 @@ cleanup = import ./modules/cleanup/check.nix { inherit pkgs; }; firefox = import ./modules/firefox/check.nix { inherit pkgs; }; jellyfin = import ./modules/jellyfin/check.nix { inherit pkgs; }; - vr-keyboard = (import ./modules/vr-keyboard/build.nix { - inherit pkgs; - dictionary = { - languages = map (l: l // { keepFrequentAbove = 4.0; }) [ - { language = "de"; hunspell = "de_DE"; words = 60000; frequencyOffset = 0.0; } - { language = "en"; hunspell = "en_US"; words = 40000; frequencyOffset = -0.3; } - ]; - contractions = true; extraWords = [ ]; extraWordsFrequency = 5.0; extraWordFiles = [ ]; excludeWords = [ ]; - }; - }).checks; + vr-keyboard = import ./modules/vr-keyboard/check.nix { inherit pkgs; }; }); # nix flake init -t github:lhns/steam-frame-nix diff --git a/modules/dashboard-windows.nix b/modules/dashboard-windows.nix index 8524730..9bfd4f1 100644 --- a/modules/dashboard-windows.nix +++ b/modules/dashboard-windows.nix @@ -6,7 +6,7 @@ let cfg = config.steamFrame.dashboard.windows; inherit (lib) mkOption types; - inherit (import ./lib { inherit pkgs; }) mkPatch; + inherit (import ./steam-ui-patches/lib { inherit pkgs; }) mkPatch; # Stock grab distance ranges (meters), matched by the patch. stockDistance = { diff --git a/modules/dashboard-windows/patch.js b/modules/dashboard-windows/patch.js index dcdfd91..f4d14df 100644 --- a/modules/dashboard-windows/patch.js +++ b/modules/dashboard-windows/patch.js @@ -1,7 +1,8 @@ // dashboard-windows: resize and grab-distance limits of SteamVR dashboard // windows (Steam, app windows, overlays, theater screen, the dashboard). // Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title -// "systemui"). mkPatch patch (see lib/default.nix); opts (null = stock): +// "systemui"). mkPatch patch (see steam-ui-patches/lib/default.nix); opts +// (null = stock): // { maxScale: 4.0, // distance: { world: {min: null, max: 10}, theater: {...}, dashboard: {...} } } // @@ -16,8 +17,9 @@ // dashboard "grab-transform" 0.3 / 4 the dashboard itself // (the keyboard's grab-transform, 0.2 / 1, is left alone). // They are literals in systemui's bundle, so SendMessage is hooked on the -// mailbox prototype (lib/hooks.js, shared with window-curvature) and outgoing -// scene graphs (fresh objects per update) are edited in place. Grab nodes are +// mailbox prototype (steam-ui-patches/lib/hooks.js, shared with +// window-curvature) and outgoing scene graphs (fresh objects per update) are +// edited in place. Grab nodes are // matched by type plus exact stock values, so if SteamVR changes them the // distance rewrite becomes a no-op. A scene-graph resend (systemui's debounced // scheduler) applies new limits at once. diff --git a/modules/frame-controls.nix b/modules/frame-controls.nix index 946e61d..e7fbac5 100644 --- a/modules/frame-controls.nix +++ b/modules/frame-controls.nix @@ -7,7 +7,7 @@ let cfg = config.steamFrame.dashboard.frameControls; inherit (lib) mkOption types; - inherit (import ./lib { inherit pkgs; }) mkPatch; + inherit (import ./steam-ui-patches/lib { inherit pkgs; }) mkPatch; # Control names -> SteamVR action icon enums (the patch keys controls by icon). names = [ "keyboard" "float" "dashboard" "theater" "dockLeft" "dockRight" "close" "curvature" ]; diff --git a/modules/frame-controls/patch.js b/modules/frame-controls/patch.js index ffc0b22..329215d 100644 --- a/modules/frame-controls/patch.js +++ b/modules/frame-controls/patch.js @@ -6,8 +6,8 @@ // opts.floatInTheater gives theater windows the "Float" control back. // // Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title -// "systemui"). mkPatch patch (see lib/default.nix); opts: { inBar, inMenu, -// longPressMs, floatInTheater }. +// "systemui"). mkPatch patch (see steam-ui-patches/lib/default.nix); opts: +// { inBar, inMenu, longPressMs, floatInTheater }. // // Stock: each Frame renders // (2 = action, 1 = spacer); passes the lists to the MobX diff --git a/modules/launcher-menu.nix b/modules/launcher-menu.nix index b92a9cb..3d5c5dd 100644 --- a/modules/launcher-menu.nix +++ b/modules/launcher-menu.nix @@ -22,7 +22,7 @@ { config, lib, pkgs, ... }: let cfg = config.steamFrame.launcherMenu; - inherit (import ./lib { inherit pkgs; }) mkPatch; + inherit (import ./steam-ui-patches/lib { inherit pkgs; }) mkPatch; sharedJSContext = { title = "SharedJSContext"; }; iconSuggest = pkgs.writeShellApplication { diff --git a/modules/launcher-menu/grid/patch.js b/modules/launcher-menu/grid/patch.js index 65eb185..274ae04 100644 --- a/modules/launcher-menu/grid/patch.js +++ b/modules/launcher-menu/grid/patch.js @@ -1,7 +1,7 @@ // grid: shows the programs section of the VR dashboard's "+" menu // (#VRDashboard_LaunchNonSteamApp) as a grid of tiles (icon, name below). -// mkPatch patch (see lib/default.nix); opts: { columns, maxRows } (maxRows -// null: the stock 600 px max height). +// mkPatch patch (see steam-ui-patches/lib/default.nix); opts: { columns, +// maxRows } (maxRows null: the stock 600 px max height). // // Pure restyling in SharedJSContext (the bar popups share its realm): Steam's // items and handlers stay, so the other launcher-menu patches keep working. A @@ -19,7 +19,7 @@ // maxRows caps the scroller, the inner panel is what scrolls. // Gamepad navigation follows the computed display: grid. The pinned-desktop // row (.sfui-pinned-desktop) is made slim and centred. -// Anchors: "launcher-menu-grid" in lib/signatures.json. +// Anchors: "launcher-menu-grid" in steam-ui-patches/lib/signatures.json. // unpatch.js (or a new VERSION/options) calls __sfuiLauncherGrid.stop(). ((find, sigs, opts) => { const NAME = 'launcher-menu-grid'; diff --git a/modules/launcher-menu/launch/patch.js b/modules/launcher-menu/launch/patch.js index 9c6b4f4..a85ad22 100644 --- a/modules/launcher-menu/launch/patch.js +++ b/modules/launcher-menu/launch/patch.js @@ -1,5 +1,6 @@ // launch: what activating a program in the VR dashboard's "+" menu does. -// mkPatch patch (see lib/default.nix); opts: { closeOnLaunch, launchDebounceSeconds }. +// mkPatch patch (see steam-ui-patches/lib/default.nix); opts: { closeOnLaunch, +// launchDebounceSeconds }. // // Stock, an item only calls SteamClient.Apps.LaunchNonSteamApp(cmdline) and // the popup stays open until the new window appears, so users click twice. @@ -12,7 +13,7 @@ // popup handle (closePopup(), as stock does after adding a desktop window), // found through React props: a .VRDashboardBarSmallButton whose fiber // ancestors have refBarPopopHandle and, above, allowLaunchProgram -// ("launcher-menu-launch" in lib/signatures.json). +// ("launcher-menu-launch" in steam-ui-patches/lib/signatures.json). // Covers every activation path (pointer, controller, pinned Desktop copy). // Original kept as __sfuiOrig for unpatch.js; bump VERSION on changes. ((find, sigs, opts) => { diff --git a/modules/launcher-menu/pinned-desktop/patch.js b/modules/launcher-menu/pinned-desktop/patch.js index 7b46466..a41acb9 100644 --- a/modules/launcher-menu/pinned-desktop/patch.js +++ b/modules/launcher-menu/pinned-desktop/patch.js @@ -1,6 +1,7 @@ // pinned-desktop: pins "Desktop" (the nested Plasma session) above or below // the scrolling list of the VR dashboard's "+" menu. -// mkPatch patch (see lib/default.nix); opts: { position: "top" | "bottom" }. +// mkPatch patch (see steam-ui-patches/lib/default.nix); opts: { position: +// "top" | "bottom" }. // // DOM patch in SharedJSContext: a timer attaches a MutationObserver to every // bar popup document (g_PopupManager). The stock Desktop item (fiber list key @@ -9,7 +10,8 @@ // region. Clicking the clone clicks the hidden item, so Steam's own handler // runs. The list container is a flex column with a max-height, so the scroll // region shrinks and the menu keeps its size. -// Anchors: "launcher-menu-pinned-desktop" in lib/signatures.json. +// Anchors: "launcher-menu-pinned-desktop" in +// steam-ui-patches/lib/signatures.json. // unpatch.js (or a new VERSION/position) calls __sfuiPinnedDesktop.stop(). ((find, sigs, opts) => { const NAME = 'launcher-menu-pinned-desktop'; diff --git a/modules/launcher-menu/show-all/patch.js b/modules/launcher-menu/show-all/patch.js index 24a2c77..e6f653f 100644 --- a/modules/launcher-menu/show-all/patch.js +++ b/modules/launcher-menu/show-all/patch.js @@ -1,7 +1,8 @@ // show-all: the VR dashboard's "+" menu lists all programs without Steam's // Developer Mode. -// mkPatch patch (see lib/default.nix); no opts; sigs: "launcher-menu-show-all" -// (the module exporting the list, plus a check-only anchor for the filter). +// mkPatch patch (see steam-ui-patches/lib/default.nix); no opts; sigs: +// "launcher-menu-show-all" (the module exporting the list, plus a check-only +// anchor for the filter). // // Steam hides some executables always (steam, vrurlhandler) and, without // Developer Mode, a second list (firewall-config, vlc, dolphin, cmake-gui, diff --git a/modules/steam-close-button.nix b/modules/steam-close-button.nix index ba78860..b2e88c4 100644 --- a/modules/steam-close-button.nix +++ b/modules/steam-close-button.nix @@ -4,7 +4,7 @@ { config, lib, pkgs, ... }: let cfg = config.steamFrame.dashboard.steamCloseButton; - inherit (import ./lib { inherit pkgs; }) mkPatch; + inherit (import ./steam-ui-patches/lib { inherit pkgs; }) mkPatch; in { imports = [ ./cleanup.nix ./steam-ui-patches.nix ./steamvr-debugger.nix ]; diff --git a/modules/steam-ui-patches.nix b/modules/steam-ui-patches.nix index dde44dc..6c4641a 100644 --- a/modules/steam-ui-patches.nix +++ b/modules/steam-ui-patches.nix @@ -7,8 +7,8 @@ # instance reverts removed patches) and is stopped once the list is empty. # Patches with `state = true` get a persistent JSON value (injector.mjs: # "Persistent state"), kept when the patch goes and removed by -# steam-frame-nix-cleanup --all. Patch calling convention (mkPatch): -# lib/default.nix. +# steam-frame-nix-cleanup --all. Patch calling convention (mkPatch) and the +# finder library: steam-ui-patches/lib/default.nix. { config, pkgs, lib, ... }: let cfg = config.steamFrame.uiPatches; @@ -97,12 +97,13 @@ in { options.steamFrame.uiPatches.lib = mkOption { type = types.attrsOf types.raw; readOnly = true; - default = import ./lib { inherit pkgs; }; - defaultText = lib.literalMD "the helpers of `modules/lib`"; + default = import ./steam-ui-patches/lib { inherit pkgs; }; + defaultText = lib.literalMD "the helpers of `modules/steam-ui-patches/lib`"; description = '' - Patch helpers from modules/lib/default.nix: `mkPatch { name, src, - signatures ? …, opts ? { }, extraArgs ? [ ] }` (calls `src` as - `(find, sigs, opts, hooks, ...extraArgs) => …`; see there), plus + Patch helpers from modules/steam-ui-patches/lib/default.nix: + `mkPatch { name, src, signatures ? …, opts ? { }, extraArgs ? [ ] }` + (calls `src` as `(find, sigs, opts, hooks, ...extraArgs) => …`; see + there), plus `finders`, `hooks` (paths) and `signatures` (parsed signatures.json). ''; }; diff --git a/modules/lib/default.nix b/modules/steam-ui-patches/lib/default.nix similarity index 100% rename from modules/lib/default.nix rename to modules/steam-ui-patches/lib/default.nix diff --git a/modules/lib/finders.js b/modules/steam-ui-patches/lib/finders.js similarity index 100% rename from modules/lib/finders.js rename to modules/steam-ui-patches/lib/finders.js diff --git a/modules/lib/hooks.js b/modules/steam-ui-patches/lib/hooks.js similarity index 100% rename from modules/lib/hooks.js rename to modules/steam-ui-patches/lib/hooks.js diff --git a/modules/lib/signatures.json b/modules/steam-ui-patches/lib/signatures.json similarity index 100% rename from modules/lib/signatures.json rename to modules/steam-ui-patches/lib/signatures.json diff --git a/modules/steam-keyboard-patch.nix b/modules/vr-keyboard-extra-keys.nix similarity index 80% rename from modules/steam-keyboard-patch.nix rename to modules/vr-keyboard-extra-keys.nix index 7e86e5d..aa0b45c 100644 --- a/modules/steam-keyboard-patch.nix +++ b/modules/vr-keyboard-extra-keys.nix @@ -1,3 +1,5 @@ +# steamFrame.keyboard.vr.extraKeys (user service steam-keyboard-patch; patch +# name "steam-keyboard-patch" in logs and signatures.json). # Steam's VR keyboard can only send text (ControllerKeyboardSetKeyState throws # "Unknown method" in VR), and its layouts are hardcoded. # - patch.js (injected over CEF DevTools, 127.0.0.1:8080) adds a bottom row @@ -5,14 +7,15 @@ # hands chords, Shift+arrows and characters Steam would turn into "1" # (non-ASCII, AltGr/dead keys) to the helper. AltGr + the key left of # Backspace = Delete. -# - helper.mjs keeps it injected, sends those keys with xdotool on :0 (an +# - xdotool-helper.mjs keeps it injected (its own injector, not the +# steam-ui-patches service), sends those keys with xdotool on :0 (an # allowlist: no ASCII text, no Enter) and unpatches on stop. # Tested with Steam client 1790377368 (UI build 11041156). { config, pkgs, lib, ... }: let - patch = (import ./lib { inherit pkgs; }).mkPatch { + patch = (import ./steam-ui-patches/lib { inherit pkgs; }).mkPatch { name = "steam-keyboard-patch"; - src = ./steam-keyboard-patch/patch.js; + src = ./vr-keyboard-extra-keys/patch.js; }; cfg = config.steamFrame.keyboard.vr.extraKeys; in { @@ -35,9 +38,9 @@ in { Service = { ExecStart = lib.escapeShellArgs [ "${pkgs.nodejs}/bin/node" - "${./steam-keyboard-patch/helper.mjs}" + "${./vr-keyboard-extra-keys/xdotool-helper.mjs}" "${patch}" - "${./steam-keyboard-patch/unpatch.js}" + "${./vr-keyboard-extra-keys/unpatch.js}" "${pkgs.xdotool}/bin/xdotool" ]; Restart = "always"; diff --git a/modules/steam-keyboard-patch/patch.js b/modules/vr-keyboard-extra-keys/patch.js similarity index 98% rename from modules/steam-keyboard-patch/patch.js rename to modules/vr-keyboard-extra-keys/patch.js index 49992eb..542c85a 100644 --- a/modules/steam-keyboard-patch/patch.js +++ b/modules/vr-keyboard-extra-keys/patch.js @@ -1,4 +1,5 @@ -// Injected into Steam's SharedJSContext (CEF, 127.0.0.1:8080) by helper.mjs. +// Injected into Steam's SharedJSContext (CEF, 127.0.0.1:8080) by +// xdotool-helper.mjs. // Extends Steam's VR keyboard for gamescope app windows: // - bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ → Close, stable with Shift // and AltGr; AltGr + arrows = Pos1/PgUp/PgDn/End @@ -16,11 +17,11 @@ // - Enter types Return for app windows, even if a Steam search box had focus // before (Steam then labels it "Search" and closes the keyboard instead) // Key output goes through the CDP binding window.__vrkbdKey(":"), -// run by helper.mjs with xdotool on :0. Replaced functions keep their +// run by xdotool-helper.mjs with xdotool on :0. Replaced functions keep their // original as __vrkbdOrig; window.__vrkbdRefs etc. are for unpatch.js. // -// mkPatch patch (see lib/default.nix); no options. On a signature mismatch -// it returns an error and changes nothing. Idempotent. +// mkPatch patch (see steam-ui-patches/lib/default.nix); no options. On a +// signature mismatch it returns an error and changes nothing. Idempotent. ((find, sigs) => { const VERSION = 22; const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg); @@ -143,7 +144,7 @@ // All text for gamescope windows ends in ControllerKeyboardSendText, which // only maps plain ASCII on the base/shift levels (else "1"). Those // characters go to the helper, in order; the rest passes through. - // (helper.mjs checks the same set.) + // (xdotool-helper.mjs checks the same set.) const viaHelper = (c) => c.codePointAt(0) > 127 || '|@{[]}\\~^`'.includes(c); wrap(SteamClient.Input, 'ControllerKeyboardSendText', (orig) => function (text, ...rest) { if (typeof text !== 'string') return orig.call(this, text, ...rest); diff --git a/modules/steam-keyboard-patch/unpatch.js b/modules/vr-keyboard-extra-keys/unpatch.js similarity index 85% rename from modules/steam-keyboard-patch/unpatch.js rename to modules/vr-keyboard-extra-keys/unpatch.js index 6a48b60..c3f6fe0 100644 --- a/modules/steam-keyboard-patch/unpatch.js +++ b/modules/vr-keyboard-extra-keys/unpatch.js @@ -1,6 +1,6 @@ -// Reverts patch.js in Steam's SharedJSContext (run by helper.mjs on stop). -// Patched functions keep their original as __vrkbdOrig; objects Steam doesn't -// expose are in window.__vrkbdRefs/__vrkbdLayouts/__vrkbdProto/__vrkbdInst; +// Reverts patch.js in Steam's SharedJSContext (run by xdotool-helper.mjs on +// stop). Patched functions keep their original as __vrkbdOrig; objects Steam +// doesn't expose are in window.__vrkbdRefs/__vrkbdLayouts/__vrkbdProto/__vrkbdInst; // the Delete-repeat listeners of a keyboard window in its __vrkbdDelDetach. // Safe when not patched. (() => { diff --git a/modules/steam-keyboard-patch/helper.mjs b/modules/vr-keyboard-extra-keys/xdotool-helper.mjs similarity index 93% rename from modules/steam-keyboard-patch/helper.mjs rename to modules/vr-keyboard-extra-keys/xdotool-helper.mjs index 46b94af..2fd5413 100644 --- a/modules/steam-keyboard-patch/helper.mjs +++ b/modules/vr-keyboard-extra-keys/xdotool-helper.mjs @@ -1,9 +1,11 @@ -// helper.mjs: injects the patch (patch.js wrapped with the finder library by -// lib/default.nix mkPatch) into Steam's UI via CEF DevTools and -// performs the key requests of the patched VR keyboard with xdotool on :0. +// xdotool-helper.mjs (user service steam-keyboard-patch): injects the patch +// (patch.js wrapped with the finder library by mkPatch, +// steam-ui-patches/lib/default.nix) into Steam's UI via CEF DevTools itself +// (not through the steam-ui-patches injector) and performs the key requests +// of the patched VR keyboard with xdotool on :0. // On SIGTERM/SIGINT it reverts the patch (unpatch.js), so stopping the service // restores Steam's stock keyboard without restarting Steam. -// usage: node helper.mjs [xdotool] +// usage: node xdotool-helper.mjs [xdotool] import { readFileSync } from 'node:fs'; import { execFile } from 'node:child_process'; diff --git a/modules/vr-keyboard.nix b/modules/vr-keyboard.nix index 32abcce..12165c4 100644 --- a/modules/vr-keyboard.nix +++ b/modules/vr-keyboard.nix @@ -1,22 +1,24 @@ -# Swipe typing, suggestions and a Backspace drag for Steam's VR keyboard. -# - vr-keyboard/patch.js (Steam UI, 8080): swipe gestures (decoder.js), a -# model of what the keyboard typed (textmodel.js), suggestions (swipe +# Swipe typing, suggestions and a Backspace drag for Steam's VR keyboard +# (steamFrame.keyboard.vr; the extra keys are vr-keyboard-extra-keys.nix). +# - vr-keyboard/patch.js (Steam UI, 8080): swipe gestures (swipe-decoder.js), +# a model of what the keyboard typed (textmodel.js), suggestions (swipe # alternatives, corrections and completions, corrector.js; never changing # text by themselves), Backspace drag (left: delete with a detent at word # borders; right: retype), haptic ticks. # - The suggestion strip: over the number row ("inside"), or a SteamVR -# dashboard panel above/below the keyboard (panel.js in systemui, 8087), -# fed by relay.mjs (user service vr-keyboard-relay). -# - Dictionary: built by gen-dict.py from wordfreq frequency lists, filtered -# and cased by Hunspell (nixpkgs' hunspellDicts). +# dashboard panel above/below the keyboard (suggestions-panel/patch.js in +# systemui, 8087), fed by suggestions-panel/relay.mjs (user service +# vr-keyboard-relay). +# - Dictionary: dictionary.nix (gen-dict.py) from wordfreq frequency lists, +# filtered and cased by Hunspell (nixpkgs' hunspellDicts). # Works alongside keyboard.vr.extraKeys (both hook the same keyboard; this -# one resets its text model on the extra keys it can't follow). Tests: the -# checks below (also flake check `vr-keyboard`). +# one resets its text model on the extra keys it can't follow). Tests: +# check.nix (built before the patch; also flake check `vr-keyboard`). { config, pkgs, lib, ... }: let cfg = config.steamFrame.keyboard.vr; inherit (lib) mkOption mkEnableOption types; - uiLib = import ./lib { inherit pkgs; }; + uiLib = import ./steam-ui-patches/lib { inherit pkgs; }; on = cfg.enable && (cfg.swipe.enable || cfg.autocorrect.enable || cfg.completions.enable || cfg.backspaceDrag.enable); panel = on && cfg.suggestions.position != "inside"; @@ -34,9 +36,10 @@ let ++ [ { language = "en"; hunspell = "en_US"; words = if layoutLanguage != null then 40000 else 60000; frequencyOffset = if layoutLanguage != null then -0.3 else 0.0; } ]; - built = import ./vr-keyboard/build.nix { inherit pkgs; inherit (cfg) dictionary; }; + dictionaryJs = import ./vr-keyboard/dictionary.nix { inherit pkgs; inherit (cfg) dictionary; }; + checks = import ./vr-keyboard/check.nix { inherit pkgs; inherit (cfg) dictionary; }; patch = pkgs.runCommand "vr-keyboard.js" { nativeBuildInputs = [ pkgs.nodejs ]; } '' - : ${built.checks} + : ${checks} cp ${uiLib.mkPatch { name = "vr-keyboard"; src = ./vr-keyboard/patch.js; @@ -52,7 +55,7 @@ let inherit (cfg.backspaceDrag) wordDetentPixels; inherit (cfg) haptics; }; - extraArgs = [ ./vr-keyboard/decoder.js ./vr-keyboard/textmodel.js ./vr-keyboard/corrector.js built.dictionary ]; + extraArgs = [ ./vr-keyboard/swipe-decoder.js ./vr-keyboard/textmodel.js ./vr-keyboard/corrector.js dictionaryJs ]; }} $out node --check $out ''; @@ -180,7 +183,7 @@ in { checks = mkOption { type = types.package; readOnly = true; - default = built.checks; + default = checks; defaultText = lib.literalMD "the tests, built with the configured dictionary"; description = "The tests (also built with the patch)."; }; @@ -202,15 +205,15 @@ in { name = "vr-keyboard-panel"; endpoint = "http://127.0.0.1:8087"; target.title = "systemui"; - patch = pkgs.writeText "vr-keyboard-panel.js" "(${builtins.readFile ./vr-keyboard/panel.js})()"; - unpatch = ./vr-keyboard/unpatch-panel.js; + patch = pkgs.writeText "vr-keyboard-panel.js" "(${builtins.readFile ./vr-keyboard/suggestions-panel/patch.js})()"; + unpatch = ./vr-keyboard/suggestions-panel/unpatch.js; }; steamFrame.session.services.${if panel then "restart" else "stop"} = [ "vr-keyboard-relay.service" ]; } (lib.mkIf panel { systemd.user.services.vr-keyboard-relay = { Unit.Description = "Relay of the VR keyboard's suggestion strip (Steam UI <-> SteamVR dashboard)"; - Service = { ExecStart = "${pkgs.nodejs}/bin/node ${./vr-keyboard/relay.mjs}"; Restart = "always"; RestartSec = 5; }; + Service = { ExecStart = "${pkgs.nodejs}/bin/node ${./vr-keyboard/suggestions-panel/relay.mjs}"; Restart = "always"; RestartSec = 5; }; Install.WantedBy = [ "default.target" ]; }; }) diff --git a/modules/vr-keyboard/build.nix b/modules/vr-keyboard/build.nix deleted file mode 100644 index f7a8b7a..0000000 --- a/modules/vr-keyboard/build.nix +++ /dev/null @@ -1,39 +0,0 @@ -# Build-time parts of the VR keyboard: the dictionary and the tests. Used by -# vr-keyboard.nix and by the flake's checks..vr-keyboard. -{ pkgs, dictionary }: -let - inherit (pkgs) lib; - d = dictionary; - words = pkgs.runCommand "vr-keyboard-dictionary.js" { } '' - ${pkgs.python3.withPackages (ps: [ ps.wordfreq ])}/bin/python3 ${./gen-dict.py} $out ${pkgs.hunspell}/bin/hunspell \ - ${pkgs.writeText "vr-keyboard-dictionary.json" (builtins.toJSON { - languages = map (l: { - inherit (l) words keepFrequentAbove; - lang = l.language; - offset = l.frequencyOffset; - dict = if l.hunspell == null then null else "${pkgs.hunspellDicts.${l.hunspell}}/share/hunspell/${l.hunspell}"; - }) d.languages; - extra = map (w: [ w d.extraWordsFrequency ]) d.extraWords; - extraFiles = map toString d.extraWordFiles; - extraZipf = d.extraWordsFrequency; - exclude = d.excludeWords; - inherit (d) contractions; - })} - ''; - # The accuracy thresholds (German keyboard geometry, German and English - # words) fail the build only for German + English dictionaries. - strict = lib.sort lib.lessThan (map (l: l.language) d.languages) == [ "de" "en" ]; -in { - dictionary = words; - checks = pkgs.runCommand "vr-keyboard-checks" { nativeBuildInputs = [ pkgs.nodejs ]; } '' - set -o pipefail - node ${./tests/textmodel.test.mjs} ${./textmodel.js} - node ${./tests/corrector.test.mjs} ${./corrector.js} ${./decoder.js} ${words} ${lib.optionalString (!strict) "|| echo warning: corrector test failed"} - node ${./tests/decoder.test.mjs} ${./decoder.js} ${words} 100 1 | tee $out ${lib.optionalString (!strict) "|| true"} - node -e ' - const bad = require("fs").readFileSync(process.argv[1], "utf8").split("\n") - .filter((l) => /sigma 0.25/.test(l) && +l.match(/top-3 ([\d.]+)%/)[1] < 85); - if (bad.length) { console.error("decoder below 85 % top-3:", bad); process.exit(process.argv[2] === "strict" ? 1 : 0); }' \ - $out ${if strict then "strict" else "warn"} - ''; -} diff --git a/modules/vr-keyboard/check.nix b/modules/vr-keyboard/check.nix new file mode 100644 index 0000000..157c143 --- /dev/null +++ b/modules/vr-keyboard/check.nix @@ -0,0 +1,30 @@ +# Tests of the VR keyboard (flake check `vr-keyboard`, also built before the +# patch by vr-keyboard.nix with the configured dictionary): text model, +# corrector and swipe-decoder accuracy (tests/*.test.mjs). The accuracy +# thresholds (German keyboard geometry, German and English words) fail the +# build only for German + English dictionaries, the default here. +{ pkgs +, dictionary ? { + languages = map (l: l // { keepFrequentAbove = 4.0; }) [ + { language = "de"; hunspell = "de_DE"; words = 60000; frequencyOffset = 0.0; } + { language = "en"; hunspell = "en_US"; words = 40000; frequencyOffset = -0.3; } + ]; + contractions = true; extraWords = [ ]; extraWordsFrequency = 5.0; extraWordFiles = [ ]; excludeWords = [ ]; + } +}: +let + inherit (pkgs) lib; + words = import ./dictionary.nix { inherit pkgs dictionary; }; + strict = lib.sort lib.lessThan (map (l: l.language) dictionary.languages) == [ "de" "en" ]; +in +pkgs.runCommand "vr-keyboard-checks" { nativeBuildInputs = [ pkgs.nodejs ]; } '' + set -o pipefail + node ${./tests/textmodel.test.mjs} ${./textmodel.js} + node ${./tests/corrector.test.mjs} ${./corrector.js} ${./swipe-decoder.js} ${words} ${lib.optionalString (!strict) "|| echo warning: corrector test failed"} + node ${./tests/swipe-decoder.test.mjs} ${./swipe-decoder.js} ${words} 100 1 | tee $out ${lib.optionalString (!strict) "|| true"} + node -e ' + const bad = require("fs").readFileSync(process.argv[1], "utf8").split("\n") + .filter((l) => /sigma 0.25/.test(l) && +l.match(/top-3 ([\d.]+)%/)[1] < 85); + if (bad.length) { console.error("decoder below 85 % top-3:", bad); process.exit(process.argv[2] === "strict" ? 1 : 0); }' \ + $out ${if strict then "strict" else "warn"} +'' diff --git a/modules/vr-keyboard/dictionary.nix b/modules/vr-keyboard/dictionary.nix new file mode 100644 index 0000000..25a5366 --- /dev/null +++ b/modules/vr-keyboard/dictionary.nix @@ -0,0 +1,24 @@ +# The VR keyboard's dictionary (vr-keyboard-dictionary.js, passed to patch.js; +# also used by check.nix), built by gen-dict.py from wordfreq frequency lists, +# filtered and cased by Hunspell. `dictionary`: the +# steamFrame.keyboard.vr.dictionary options. +{ pkgs, dictionary }: +let + d = dictionary; +in +pkgs.runCommand "vr-keyboard-dictionary.js" { } '' + ${pkgs.python3.withPackages (ps: [ ps.wordfreq ])}/bin/python3 ${./gen-dict.py} $out ${pkgs.hunspell}/bin/hunspell \ + ${pkgs.writeText "vr-keyboard-dictionary.json" (builtins.toJSON { + languages = map (l: { + inherit (l) words keepFrequentAbove; + lang = l.language; + offset = l.frequencyOffset; + dict = if l.hunspell == null then null else "${pkgs.hunspellDicts.${l.hunspell}}/share/hunspell/${l.hunspell}"; + }) d.languages; + extra = map (w: [ w d.extraWordsFrequency ]) d.extraWords; + extraFiles = map toString d.extraWordFiles; + extraZipf = d.extraWordsFrequency; + exclude = d.excludeWords; + inherit (d) contractions; + })} +'' diff --git a/modules/vr-keyboard/patch.js b/modules/vr-keyboard/patch.js index 63271d9..be19a6b 100644 --- a/modules/vr-keyboard/patch.js +++ b/modules/vr-keyboard/patch.js @@ -1,8 +1,8 @@ // Gestures and suggestions for Steam's VR keyboard (vr-keyboard.nix), // injected into Steam's SharedJSContext (8080). The keyboard is a popup of // that context ("SteamVR - Keyboard"); everything works on its document. -// mkPatch convention plus four more arguments: decoder.js, textmodel.js, -// corrector.js and the dictionary text ("wordzipf*10\n..."). +// mkPatch convention plus four more arguments: swipe-decoder.js, +// textmodel.js, corrector.js and the dictionary text ("wordzipf*10\n..."). // // - Swipe: the laser arrives as touch events; Steam types the key a touch // started on at release. Once a press leaves its first key we drop that @@ -20,9 +20,9 @@ // replace text only while the model proves the characters they replace // intact. // - Strip: in the page over the number row ("inside") or as a SteamVR panel -// above/below the keyboard ("above"/"below", panel.js): state out via the -// CDP binding __sfuiStripOut, picks back via -// __sfuiSwipe.remote.pick(seq, index) (relay.mjs). +// above/below the keyboard ("above"/"below", suggestions-panel/patch.js): +// state out via the CDP binding __sfuiStripOut, picks back via +// __sfuiSwipe.remote.pick(seq, index) (suggestions-panel/relay.mjs). // All Steam internals are checked first (sigs, instance members); if one is // missing the keyboard stays stock. Only passive listeners; never blocks // Steam's events. Debugging: __sfuiSwipeLog, __sfuiSwipePaths @@ -87,7 +87,7 @@ const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k); // ---- trail and in-page strip ------------------------------------------------- - const inPage = O.position === 'inside'; // else a SteamVR panel (panel.js) + const inPage = O.position === 'inside'; // else a SteamVR panel (suggestions-panel/) const style = doc.createElement('style'); style.textContent = ` #sfui-swipe-trail { position: fixed; left: 0; top: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 10000; } diff --git a/modules/vr-keyboard/panel.js b/modules/vr-keyboard/suggestions-panel/patch.js similarity index 98% rename from modules/vr-keyboard/panel.js rename to modules/vr-keyboard/suggestions-panel/patch.js index b600728..b6eab1c 100644 --- a/modules/vr-keyboard/panel.js +++ b/modules/vr-keyboard/suggestions-panel/patch.js @@ -1,6 +1,7 @@ // The suggestion strip as a SteamVR dashboard panel above or below Steam's -// VR keyboard (suggestions.position "above" / "below"), injected into -// SteamVR's systemui page (8087). State comes from the keyboard page through relay.mjs: +// VR keyboard (suggestions.position "above" / "below"; patch name +// "vr-keyboard-panel"), injected into SteamVR's systemui page (8087). State +// comes from the keyboard page (../patch.js) through relay.mjs: // __sfuiKbdStrip.show({ seq, items, current, visible, style }); a click calls // the binding __sfuiStripPick('{"seq":n,"index":i}'). // diff --git a/modules/vr-keyboard/relay.mjs b/modules/vr-keyboard/suggestions-panel/relay.mjs similarity index 92% rename from modules/vr-keyboard/relay.mjs rename to modules/vr-keyboard/suggestions-panel/relay.mjs index e3c97d2..aa73d2a 100644 --- a/modules/vr-keyboard/relay.mjs +++ b/modules/vr-keyboard/suggestions-panel/relay.mjs @@ -1,6 +1,7 @@ -// relay.mjs: carries the suggestion strip between Steam's keyboard page -// (SharedJSContext, 8080) and SteamVR's systemui page (8087) for -// suggestions.position "above" / "below". CDP bindings: the keyboard page calls +// relay.mjs (user service vr-keyboard-relay): carries the suggestion strip +// between Steam's keyboard page (../patch.js in SharedJSContext, 8080) and +// SteamVR's systemui page (patch.js here, 8087) for suggestions.position +// "above" / "below". CDP bindings: the keyboard page calls // __sfuiStripOut(state json) -> systemui __sfuiKbdStrip.show(state); the // panel calls __sfuiStripPick({ seq, index } json) -> keyboard page // __sfuiSwipe.remote.pick(seq, index). Only these two messages, validated. diff --git a/modules/vr-keyboard/unpatch-panel.js b/modules/vr-keyboard/suggestions-panel/unpatch.js similarity index 52% rename from modules/vr-keyboard/unpatch-panel.js rename to modules/vr-keyboard/suggestions-panel/unpatch.js index a375f2c..33afed3 100644 --- a/modules/vr-keyboard/unpatch-panel.js +++ b/modules/vr-keyboard/suggestions-panel/unpatch.js @@ -1,5 +1,5 @@ -// Reverts panel.js in SteamVR's systemui page: removes the strip panel, its -// embedded-UV slot and stylesheet. Safe when nothing is patched. +// Reverts suggestions-panel/patch.js in SteamVR's systemui page: removes the +// strip panel, its embedded-UV slot and stylesheet. Safe when nothing is patched. (() => { const S = window.__sfuiKbdStrip; if (!S) return 'not patched'; diff --git a/modules/vr-keyboard/decoder.js b/modules/vr-keyboard/swipe-decoder.js similarity index 97% rename from modules/vr-keyboard/decoder.js rename to modules/vr-keyboard/swipe-decoder.js index 727620e..46aef73 100644 --- a/modules/vr-keyboard/decoder.js +++ b/modules/vr-keyboard/swipe-decoder.js @@ -1,5 +1,5 @@ -// decoder.js: swipe path -> words. Evaluates to { VERSION, parseDict, layout, -// decode, ... }. SHARK2-style template matching (Kristensson & Zhai 2004): +// swipe-decoder.js: swipe path -> words. Evaluates to { VERSION, parseDict, +// layout, decode, ... }. SHARK2-style template matching (Kristensson & Zhai 2004): // each word's ideal path runs through its key centres; candidates starting // and ending near the path's ends, with every key near the path, are scored // by point distances of the resampled paths (proportional and DTW), how @@ -9,7 +9,7 @@ const VERSION = 3; const N = 32; // resample points const SKIP = new Set(["'", '-', '\u2019']); - // Cost weights and pruning limits (tuned with tests/decoder.test.mjs; distances in key widths). + // Cost weights and pruning limits (tuned with tests/swipe-decoder.test.mjs; distances in key widths). const W = { loc: 4.4, // mean point distance, proportional alignment dtw: 2.0, // mean point distance, dynamic time warping diff --git a/modules/vr-keyboard/tests/corrector.test.mjs b/modules/vr-keyboard/tests/corrector.test.mjs index 36d95eb..554ad44 100644 --- a/modules/vr-keyboard/tests/corrector.test.mjs +++ b/modules/vr-keyboard/tests/corrector.test.mjs @@ -1,5 +1,5 @@ // Offline check of corrector.js on the built dictionary. -// usage: node corrector.test.mjs +// usage: node corrector.test.mjs import { readFileSync } from 'node:fs'; import assert from 'node:assert/strict'; diff --git a/modules/vr-keyboard/tests/decoder.test.mjs b/modules/vr-keyboard/tests/swipe-decoder.test.mjs similarity index 97% rename from modules/vr-keyboard/tests/decoder.test.mjs rename to modules/vr-keyboard/tests/swipe-decoder.test.mjs index 54c2988..93d8ba1 100644 --- a/modules/vr-keyboard/tests/decoder.test.mjs +++ b/modules/vr-keyboard/tests/swipe-decoder.test.mjs @@ -1,6 +1,7 @@ -// Offline check of decoder.js with synthetic swipes on Steam's German VR +// Offline check of swipe-decoder.js with synthetic swipes on Steam's German VR // keyboard (key centres measured in the keyboard popup, 854x280 CSS px). -// usage: node decoder.test.mjs [words-per-sample] [seed] +// usage: node swipe-decoder.test.mjs +// [words-per-sample] [seed] // Each word's path goes through its key centres with random offsets (up to // about half a key), cuts corners (spline) and jitters. Prints top-1/top-3 // accuracy for frequent German and English words and a list of fixed words. diff --git a/modules/window-curvature.nix b/modules/window-curvature.nix index 6386c5e..7bd6f9d 100644 --- a/modules/window-curvature.nix +++ b/modules/window-curvature.nix @@ -5,7 +5,7 @@ let cfg = config.steamFrame.dashboard.windowCurvature; inherit (lib) mkOption types; - inherit (import ./lib { inherit pkgs; }) mkPatch; + inherit (import ./steam-ui-patches/lib { inherit pkgs; }) mkPatch; path = [ "steamFrame" "dashboard" "windowCurvature" ]; rename = from: to: lib.mkRenamedOptionModule (path ++ [ from ]) (path ++ [ to ]); diff --git a/modules/window-curvature/patch.js b/modules/window-curvature/patch.js index a8c25ec..c29eb6b 100644 --- a/modules/window-curvature/patch.js +++ b/modules/window-curvature/patch.js @@ -6,9 +6,9 @@ // curve); dragging up/down with the laser sets the curvature live. // // Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title -// "systemui"). mkPatch patch (see lib/default.nix); opts: { initial, max, -// step, detentPixels, detentPoints, dragThresholdPixels, dragPixelsPerUnit, -// barDragPixelsPerUnit, haptics }. +// "systemui"). mkPatch patch (see steam-ui-patches/lib/default.nix); opts: +// { initial, max, step, detentPixels, detentPoints, dragThresholdPixels, +// dragPixelsPerUnit, barDragPixelsPerUnit, haptics }. // // Stock curvature (frame.curvature, systemui's `curvature` component): // shouldCurve = m_bCurveOverride (set by ToggleCurvature(), cleared on dock @@ -18,10 +18,10 @@ // 1.8 m) : 1000; its panels reference it as "curvature-origin-id" and // vrcompositor bends them onto a cylinder around it (curvature = 1/radius). // Those MobX properties are non-configurable, so this patch hooks the mailbox -// SendMessage (lib/hooks.js, shared with dashboard-windows) and rewrites the -// origin's z in outgoing scene graphs to stock / value (1 = stock, 2 = half -// the radius, 0 = flat = stock toggle off). On/off stays the stock state, so -// stock toggle and wheel always agree. +// SendMessage (steam-ui-patches/lib/hooks.js, shared with dashboard-windows) +// and rewrites the origin's z in outgoing scene graphs to stock / value +// (1 = stock, 2 = half the radius, 0 = flat = stock toggle off). On/off stays +// the stock state, so stock toggle and wheel always agree. // // Values per window (key: first overlay key) in // window.__sfuiWindowCurvatureState (schema 1: { values, log }); kept across diff --git a/scripts/check-signatures.mjs b/scripts/check-signatures.mjs index ce82fce..c145233 100644 --- a/scripts/check-signatures.mjs +++ b/scripts/check-signatures.mjs @@ -1,9 +1,10 @@ #!/usr/bin/env node // check-signatures.mjs: checks offline (no Steam, no browser) that every -// signature in modules/lib/signatures.json still matches the installed Steam / -// SteamVR web UI bundles: module and export signatures exactly once, -// "expects" strings still present (warnings), "stylesheet" signatures exactly -// one file of the bundle's "styles" directory. Run it after a Steam update: +// signature in modules/steam-ui-patches/lib/signatures.json still matches the +// installed Steam / SteamVR web UI bundles: module and export signatures +// exactly once, "expects" strings still present (warnings), "stylesheet" +// signatures exactly one file of the bundle's "styles" directory. Run it +// after a Steam update: // // nix shell nixpkgs#nodejs -c node scripts/check-signatures.mjs // @@ -20,7 +21,8 @@ // Modules come from webpack-modules.mjs (factory sources as the page's // Function.prototype.toString sees them). For export signatures the matched // factory runs in a throwaway VM context where imports and unknown globals -// are inert stubs, and is matched with modules/lib/finders.js. +// are inert stubs, and is matched with +// modules/steam-ui-patches/lib/finders.js. import { readFileSync, readdirSync, existsSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { homedir } from 'node:os'; @@ -29,7 +31,7 @@ import vm from 'node:vm'; import { loadBundles, pageFiles } from './webpack-modules.mjs'; const here = dirname(fileURLToPath(import.meta.url)); -const libDir = join(here, '..', 'modules', 'lib'); +const libDir = join(here, '..', 'modules', 'steam-ui-patches', 'lib'); // ---- arguments ----------------------------------------------------------------- const args = process.argv.slice(2); diff --git a/scripts/vr-keyboard-replay.mjs b/scripts/vr-keyboard-replay.mjs index 4a9130e..7127c2d 100644 --- a/scripts/vr-keyboard-replay.mjs +++ b/scripts/vr-keyboard-replay.mjs @@ -1,7 +1,8 @@ // Replays recorded swipes (window.__sfuiSwipePaths of Steam's SharedJSContext, // saved as JSON) through the decoder: top candidates and the cost terms of // the expected word. The dictionary is the built vr-keyboard-dictionary.js. -// usage: node scripts/vr-keyboard-replay.mjs modules/vr-keyboard/decoder.js [word] +// usage: node scripts/vr-keyboard-replay.mjs modules/vr-keyboard/swipe-decoder.js +// [word] import { readFileSync } from 'node:fs'; const [, , decoderPath, dictPath, pathsPath, expected] = process.argv; const D = (0, eval)(readFileSync(decoderPath, 'utf8'));