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.
This commit is contained in:
Pierre Kisters committed 2026-09-29 01:42:34 +02:00
1 parent b3d93e8526
commit 5d15902ba1
40 files changed
+330 -161

No files matched your search

+7 -3
View File
@@ -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). **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 ## Install
On the Frame (or a Steam Deck), in a terminal (Konsole in desktop mode or the 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 In your own flake, add the input as above and
`steam-frame-nix.homeManagerModules.default` to the modules. `default` `steam-frame-nix.homeManagerModules.default` to the modules. `default`
imports all modules; single ones: 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}`. `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}`
Every module imports `cleanup` (see (`steam-keyboard-patch` still works as the former name of
[Changes outside Nix](#changes-outside-nix-exceptions)). `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 ## Options
+128
View File
@@ -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/<name>.nix` is `homeManagerModules.<name>`; files only it uses
are in `modules/<name>/`.
- `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.<system>.<name>`); `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/<name>.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
```
+10 -7
View File
@@ -8,7 +8,8 @@ The keyboard layout of the Steam session itself is a separate fix
## Extra keys ## 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 **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 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 ### How it works
- The `steam-keyboard-patch` user service (`helper.mjs`) injects a patch - The `steam-keyboard-patch` user service
into Steam's `SharedJSContext` over DevTools (`127.0.0.1:8080`) and (`modules/vr-keyboard-extra-keys/xdotool-helper.mjs`) injects a patch into
re-injects it after Steam restarts; stopping it (or disabling the option) 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. runs the unpatch.
- Ctrl/Alt chords and Esc are sent with `xdotool key` on `:0` (focus follows - 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`. the VR-selected window); a toggled Ctrl/Alt is held down with `xdotool`.
@@ -133,15 +135,16 @@ has the letters.
### How it works ### How it works
- A Steam UI patch (`vr-keyboard`, injected by `steam-ui-patches` like the - 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) - Swiped words are matched by shape (SHARK2-style template matching)
against a dictionary built at build time from wordfreq frequency lists against a dictionary built at build time from wordfreq frequency lists
and Hunspell, both from nixpkgs (per language the `words` most frequent and Hunspell, both from nixpkgs (per language the `words` most frequent
wordfreq entries, shifted by `frequencyOffset`, filtered by Hunspell wordfreq entries, shifted by `frequencyOffset`, filtered by Hunspell
except words at or above `keepFrequentAbove`). except words at or above `keepFrequentAbove`).
- The strip below/above the keyboard is a SteamVR dashboard panel - 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 (`suggestions-panel/patch.js`, a patch of SteamVR's `systemui` page on
the `vr-keyboard-relay` user service (`relay.mjs`) between the two pages. 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. With `inside` neither the panel nor the relay runs.
- Found by signature (entries `vr-keyboard`, `vr-keyboard-panel`). - Found by signature (entries `vr-keyboard`, `vr-keyboard-panel`).
+1 -1
View File
@@ -79,7 +79,7 @@ its option is set and reverted by its unpatch when unset:
sounds and controller navigation keep working; sounds and controller navigation keep working;
- `show-all/`: empties the list Steam hides without Developer Mode. - `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). and verified by the [offline checker](ui-patches.md#after-a-steam-update).
## Hidden apps ## Hidden apps
+7 -6
View File
@@ -2,7 +2,8 @@
`uiPatches.patches`, `uiPatches.lib` (modules `steam-ui-patches`, `uiPatches.patches`, `uiPatches.lib` (modules `steam-ui-patches`,
`steamvr-debugger`). For patch authors, and for fixing patches after a Steam `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 ## Problem
@@ -53,7 +54,7 @@ it runs. What they have in common:
- Each turns on the [SteamVR debugger](steamvr-debugger.md) (**the first - Each turns on the [SteamVR debugger](steamvr-debugger.md) (**the first
time, restart SteamVR once**). time, restart SteamVR once**).
- They depend on SteamVR UI internals: each is found by signature (its entry - 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 that changes them the dashboard stays stock (see
[after a Steam update](#after-a-steam-update)). Tested with SteamVR build [after a Steam update](#after-a-steam-update)). Tested with SteamVR build
11008059. 11008059.
@@ -110,7 +111,7 @@ see [changes outside Nix](../README.md#changes-outside-nix-exceptions).
## Finders and signatures ## Finders and signatures
Webpack module ids and export names change with every Steam UI build, so 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*: `findModule`/`findInReactTree` or Vencord's `find`) locates by *signature*:
- a **module** by strings/regexes in its factory source; - 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 library also has `ensureStyle(doc, id, css)` and `logger(buffer)` (a capped
debug log). 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 offline checker. An entry can also list `expects` (strings the patch relies
on, checked offline only) or be `checkOnly` (anchors not used through the on, checked offline only) or be `checkOnly` (anchors not used through the
finder, checked offline only; with `stylesheet` instead of `module` it is 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 `steamFrame.uiPatches.lib.mkPatch` wraps a patch file — a function
expression `(find, sigs, opts, hooks) => …` returning a status string — with expression `(find, sigs, opts, hooks) => …` returning a status string — with
the finder library, its signatures, options and the shared hooks (details in the finder library, its signatures, options and the shared hooks (details in
`modules/lib/default.nix`): `modules/steam-ui-patches/lib/default.nix`):
```nix ```nix
steamFrame.uiPatches.patches = [ { steamFrame.uiPatches.patches = [ {
@@ -167,7 +168,7 @@ steamFrame.uiPatches.patches = [ {
### Shared method hooks ### 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`, intercepting the same method (e.g. the dashboard mailbox's `SendMessage`,
used by [dashboard windows](dashboard-windows.md#how-it-works) and used by [dashboard windows](dashboard-windows.md#how-it-works) and
[window curvature](window-curvature.md#how-it-works)) register named hooks; [window curvature](window-curvature.md#how-it-works)) register named hooks;
+8 -12
View File
@@ -15,7 +15,7 @@
session = ./modules/session.nix; session = ./modules/session.nix;
portal = ./modules/portal.nix; portal = ./modules/portal.nix;
keyboard-layout = ./modules/keyboard-layout.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; vr-keyboard = ./modules/vr-keyboard.nix;
hidden-apps = ./modules/hidden-apps.nix; hidden-apps = ./modules/hidden-apps.nix;
steam-ui-patches = ./modules/steam-ui-patches.nix; steam-ui-patches = ./modules/steam-ui-patches.nix;
@@ -37,10 +37,15 @@
keyring = ./modules/keyring.nix; keyring = ./modules/keyring.nix;
docker = ./modules/docker.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" ]; systems = [ "aarch64-linux" "x86_64-linux" ];
forSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); forSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
in { in {
homeManagerModules = modules // { homeManagerModules = modules // aliases // {
default = { imports = builtins.attrValues modules; }; default = { imports = builtins.attrValues modules; };
}; };
@@ -64,16 +69,7 @@
cleanup = import ./modules/cleanup/check.nix { inherit pkgs; }; cleanup = import ./modules/cleanup/check.nix { inherit pkgs; };
firefox = import ./modules/firefox/check.nix { inherit pkgs; }; firefox = import ./modules/firefox/check.nix { inherit pkgs; };
jellyfin = import ./modules/jellyfin/check.nix { inherit pkgs; }; jellyfin = import ./modules/jellyfin/check.nix { inherit pkgs; };
vr-keyboard = (import ./modules/vr-keyboard/build.nix { vr-keyboard = import ./modules/vr-keyboard/check.nix { inherit pkgs; };
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;
}); });
# nix flake init -t github:lhns/steam-frame-nix # nix flake init -t github:lhns/steam-frame-nix
+1 -1
View File
@@ -6,7 +6,7 @@
let let
cfg = config.steamFrame.dashboard.windows; cfg = config.steamFrame.dashboard.windows;
inherit (lib) mkOption types; 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. # Stock grab distance ranges (meters), matched by the patch.
stockDistance = { stockDistance = {
+5 -3
View File
@@ -1,7 +1,8 @@
// dashboard-windows: resize and grab-distance limits of SteamVR dashboard // dashboard-windows: resize and grab-distance limits of SteamVR dashboard
// windows (Steam, app windows, overlays, theater screen, the dashboard). // windows (Steam, app windows, overlays, theater screen, the dashboard).
// Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title // 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, // { maxScale: 4.0,
// distance: { world: {min: null, max: 10}, theater: {...}, dashboard: {...} } } // distance: { world: {min: null, max: 10}, theater: {...}, dashboard: {...} } }
// //
@@ -16,8 +17,9 @@
// dashboard "grab-transform" 0.3 / 4 the dashboard itself // dashboard "grab-transform" 0.3 / 4 the dashboard itself
// (the keyboard's grab-transform, 0.2 / 1, is left alone). // (the keyboard's grab-transform, 0.2 / 1, is left alone).
// They are literals in systemui's bundle, so SendMessage is hooked on the // They are literals in systemui's bundle, so SendMessage is hooked on the
// mailbox prototype (lib/hooks.js, shared with window-curvature) and outgoing // mailbox prototype (steam-ui-patches/lib/hooks.js, shared with
// scene graphs (fresh objects per update) are edited in place. Grab nodes are // 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 // 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 // distance rewrite becomes a no-op. A scene-graph resend (systemui's debounced
// scheduler) applies new limits at once. // scheduler) applies new limits at once.
+1 -1
View File
@@ -7,7 +7,7 @@
let let
cfg = config.steamFrame.dashboard.frameControls; cfg = config.steamFrame.dashboard.frameControls;
inherit (lib) mkOption types; 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). # Control names -> SteamVR action icon enums (the patch keys controls by icon).
names = [ "keyboard" "float" "dashboard" "theater" "dockLeft" "dockRight" "close" "curvature" ]; names = [ "keyboard" "float" "dashboard" "theater" "dockLeft" "dockRight" "close" "curvature" ];
+2 -2
View File
@@ -6,8 +6,8 @@
// opts.floatInTheater gives theater windows the "Float" control back. // opts.floatInTheater gives theater windows the "Float" control back.
// //
// Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title // Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title
// "systemui"). mkPatch patch (see lib/default.nix); opts: { inBar, inMenu, // "systemui"). mkPatch patch (see steam-ui-patches/lib/default.nix); opts:
// longPressMs, floatInTheater }. // { inBar, inMenu, longPressMs, floatInTheater }.
// //
// Stock: each Frame renders <FrameControlsItem params={type, action_id}> // Stock: each Frame renders <FrameControlsItem params={type, action_id}>
// (2 = action, 1 = spacer); <FrameControls> passes the lists to the MobX // (2 = action, 1 = spacer); <FrameControls> passes the lists to the MobX
+1 -1
View File
@@ -22,7 +22,7 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.steamFrame.launcherMenu; cfg = config.steamFrame.launcherMenu;
inherit (import ./lib { inherit pkgs; }) mkPatch; inherit (import ./steam-ui-patches/lib { inherit pkgs; }) mkPatch;
sharedJSContext = { title = "SharedJSContext"; }; sharedJSContext = { title = "SharedJSContext"; };
iconSuggest = pkgs.writeShellApplication { iconSuggest = pkgs.writeShellApplication {
+3 -3
View File
@@ -1,7 +1,7 @@
// grid: shows the programs section of the VR dashboard's "+" menu // grid: shows the programs section of the VR dashboard's "+" menu
// (#VRDashboard_LaunchNonSteamApp) as a grid of tiles (icon, name below). // (#VRDashboard_LaunchNonSteamApp) as a grid of tiles (icon, name below).
// mkPatch patch (see lib/default.nix); opts: { columns, maxRows } (maxRows // mkPatch patch (see steam-ui-patches/lib/default.nix); opts: { columns,
// null: the stock 600 px max height). // maxRows } (maxRows null: the stock 600 px max height).
// //
// Pure restyling in SharedJSContext (the bar popups share its realm): Steam's // 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 // 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. // maxRows caps the scroller, the inner panel is what scrolls.
// Gamepad navigation follows the computed display: grid. The pinned-desktop // Gamepad navigation follows the computed display: grid. The pinned-desktop
// row (.sfui-pinned-desktop) is made slim and centred. // 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(). // unpatch.js (or a new VERSION/options) calls __sfuiLauncherGrid.stop().
((find, sigs, opts) => { ((find, sigs, opts) => {
const NAME = 'launcher-menu-grid'; const NAME = 'launcher-menu-grid';
+3 -2
View File
@@ -1,5 +1,6 @@
// launch: what activating a program in the VR dashboard's "+" menu does. // 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 // Stock, an item only calls SteamClient.Apps.LaunchNonSteamApp(cmdline) and
// the popup stays open until the new window appears, so users click twice. // 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), // popup handle (closePopup(), as stock does after adding a desktop window),
// found through React props: a .VRDashboardBarSmallButton whose fiber // found through React props: a .VRDashboardBarSmallButton whose fiber
// ancestors have refBarPopopHandle and, above, allowLaunchProgram // 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). // Covers every activation path (pointer, controller, pinned Desktop copy).
// Original kept as __sfuiOrig for unpatch.js; bump VERSION on changes. // Original kept as __sfuiOrig for unpatch.js; bump VERSION on changes.
((find, sigs, opts) => { ((find, sigs, opts) => {
@@ -1,6 +1,7 @@
// pinned-desktop: pins "Desktop" (the nested Plasma session) above or below // pinned-desktop: pins "Desktop" (the nested Plasma session) above or below
// the scrolling list of the VR dashboard's "+" menu. // 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 // DOM patch in SharedJSContext: a timer attaches a MutationObserver to every
// bar popup document (g_PopupManager). The stock Desktop item (fiber list key // 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 // 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 // runs. The list container is a flex column with a max-height, so the scroll
// region shrinks and the menu keeps its size. // 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(). // unpatch.js (or a new VERSION/position) calls __sfuiPinnedDesktop.stop().
((find, sigs, opts) => { ((find, sigs, opts) => {
const NAME = 'launcher-menu-pinned-desktop'; const NAME = 'launcher-menu-pinned-desktop';
+3 -2
View File
@@ -1,7 +1,8 @@
// show-all: the VR dashboard's "+" menu lists all programs without Steam's // show-all: the VR dashboard's "+" menu lists all programs without Steam's
// Developer Mode. // Developer Mode.
// mkPatch patch (see lib/default.nix); no opts; sigs: "launcher-menu-show-all" // mkPatch patch (see steam-ui-patches/lib/default.nix); no opts; sigs:
// (the module exporting the list, plus a check-only anchor for the filter). // "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 // Steam hides some executables always (steam, vrurlhandler) and, without
// Developer Mode, a second list (firewall-config, vlc, dolphin, cmake-gui, // Developer Mode, a second list (firewall-config, vlc, dolphin, cmake-gui,
+1 -1
View File
@@ -4,7 +4,7 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.steamFrame.dashboard.steamCloseButton; cfg = config.steamFrame.dashboard.steamCloseButton;
inherit (import ./lib { inherit pkgs; }) mkPatch; inherit (import ./steam-ui-patches/lib { inherit pkgs; }) mkPatch;
in { in {
imports = [ ./cleanup.nix ./steam-ui-patches.nix ./steamvr-debugger.nix ]; imports = [ ./cleanup.nix ./steam-ui-patches.nix ./steamvr-debugger.nix ];
+8 -7
View File
@@ -7,8 +7,8 @@
# instance reverts removed patches) and is stopped once the list is empty. # instance reverts removed patches) and is stopped once the list is empty.
# Patches with `state = true` get a persistent JSON value (injector.mjs: # Patches with `state = true` get a persistent JSON value (injector.mjs:
# "Persistent state"), kept when the patch goes and removed by # "Persistent state"), kept when the patch goes and removed by
# steam-frame-nix-cleanup --all. Patch calling convention (mkPatch): # steam-frame-nix-cleanup --all. Patch calling convention (mkPatch) and the
# lib/default.nix. # finder library: steam-ui-patches/lib/default.nix.
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let let
cfg = config.steamFrame.uiPatches; cfg = config.steamFrame.uiPatches;
@@ -97,12 +97,13 @@ in {
options.steamFrame.uiPatches.lib = mkOption { options.steamFrame.uiPatches.lib = mkOption {
type = types.attrsOf types.raw; type = types.attrsOf types.raw;
readOnly = true; readOnly = true;
default = import ./lib { inherit pkgs; }; default = import ./steam-ui-patches/lib { inherit pkgs; };
defaultText = lib.literalMD "the helpers of `modules/lib`"; defaultText = lib.literalMD "the helpers of `modules/steam-ui-patches/lib`";
description = '' description = ''
Patch helpers from modules/lib/default.nix: `mkPatch { name, src, Patch helpers from modules/steam-ui-patches/lib/default.nix:
signatures ? …, opts ? { }, extraArgs ? [ ] }` (calls `src` as `mkPatch { name, src, signatures ? …, opts ? { }, extraArgs ? [ ] }`
`(find, sigs, opts, hooks, ...extraArgs) => …`; see there), plus (calls `src` as `(find, sigs, opts, hooks, ...extraArgs) => …`; see
there), plus
`finders`, `hooks` (paths) and `signatures` (parsed signatures.json). `finders`, `hooks` (paths) and `signatures` (parsed signatures.json).
''; '';
}; };
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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 # Steam's VR keyboard can only send text (ControllerKeyboardSetKeyState throws
# "Unknown method" in VR), and its layouts are hardcoded. # "Unknown method" in VR), and its layouts are hardcoded.
# - patch.js (injected over CEF DevTools, 127.0.0.1:8080) adds a bottom row # - 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" # hands chords, Shift+arrows and characters Steam would turn into "1"
# (non-ASCII, AltGr/dead keys) to the helper. AltGr + the key left of # (non-ASCII, AltGr/dead keys) to the helper. AltGr + the key left of
# Backspace = Delete. # 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. # allowlist: no ASCII text, no Enter) and unpatches on stop.
# Tested with Steam client 1790377368 (UI build 11041156). # Tested with Steam client 1790377368 (UI build 11041156).
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let let
patch = (import ./lib { inherit pkgs; }).mkPatch { patch = (import ./steam-ui-patches/lib { inherit pkgs; }).mkPatch {
name = "steam-keyboard-patch"; name = "steam-keyboard-patch";
src = ./steam-keyboard-patch/patch.js; src = ./vr-keyboard-extra-keys/patch.js;
}; };
cfg = config.steamFrame.keyboard.vr.extraKeys; cfg = config.steamFrame.keyboard.vr.extraKeys;
in { in {
@@ -35,9 +38,9 @@ in {
Service = { Service = {
ExecStart = lib.escapeShellArgs [ ExecStart = lib.escapeShellArgs [
"${pkgs.nodejs}/bin/node" "${pkgs.nodejs}/bin/node"
"${./steam-keyboard-patch/helper.mjs}" "${./vr-keyboard-extra-keys/xdotool-helper.mjs}"
"${patch}" "${patch}"
"${./steam-keyboard-patch/unpatch.js}" "${./vr-keyboard-extra-keys/unpatch.js}"
"${pkgs.xdotool}/bin/xdotool" "${pkgs.xdotool}/bin/xdotool"
]; ];
Restart = "always"; Restart = "always";
@@ -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: // Extends Steam's VR keyboard for gamescope app windows:
// - bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ → Close, stable with Shift // - bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ → Close, stable with Shift
// and AltGr; AltGr + arrows = Pos1/PgUp/PgDn/End // 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 // - 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) // before (Steam then labels it "Search" and closes the keyboard instead)
// Key output goes through the CDP binding window.__vrkbdKey("<op>:<arg>"), // Key output goes through the CDP binding window.__vrkbdKey("<op>:<arg>"),
// 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. // original as __vrkbdOrig; window.__vrkbdRefs etc. are for unpatch.js.
// //
// mkPatch patch (see lib/default.nix); no options. On a signature mismatch // mkPatch patch (see steam-ui-patches/lib/default.nix); no options. On a
// it returns an error and changes nothing. Idempotent. // signature mismatch it returns an error and changes nothing. Idempotent.
((find, sigs) => { ((find, sigs) => {
const VERSION = 22; const VERSION = 22;
const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg); const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg);
@@ -143,7 +144,7 @@
// All text for gamescope windows ends in ControllerKeyboardSendText, which // All text for gamescope windows ends in ControllerKeyboardSendText, which
// only maps plain ASCII on the base/shift levels (else "1"). Those // only maps plain ASCII on the base/shift levels (else "1"). Those
// characters go to the helper, in order; the rest passes through. // 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); const viaHelper = (c) => c.codePointAt(0) > 127 || '|@{[]}\\~^`'.includes(c);
wrap(SteamClient.Input, 'ControllerKeyboardSendText', (orig) => function (text, ...rest) { wrap(SteamClient.Input, 'ControllerKeyboardSendText', (orig) => function (text, ...rest) {
if (typeof text !== 'string') return orig.call(this, text, ...rest); if (typeof text !== 'string') return orig.call(this, text, ...rest);
@@ -1,6 +1,6 @@
// Reverts patch.js in Steam's SharedJSContext (run by helper.mjs on stop). // Reverts patch.js in Steam's SharedJSContext (run by xdotool-helper.mjs on
// Patched functions keep their original as __vrkbdOrig; objects Steam doesn't // stop). Patched functions keep their original as __vrkbdOrig; objects Steam
// expose are in window.__vrkbdRefs/__vrkbdLayouts/__vrkbdProto/__vrkbdInst; // doesn't expose are in window.__vrkbdRefs/__vrkbdLayouts/__vrkbdProto/__vrkbdInst;
// the Delete-repeat listeners of a keyboard window in its __vrkbdDelDetach. // the Delete-repeat listeners of a keyboard window in its __vrkbdDelDetach.
// Safe when not patched. // Safe when not patched.
(() => { (() => {
@@ -1,9 +1,11 @@
// helper.mjs: injects the patch (patch.js wrapped with the finder library by // xdotool-helper.mjs (user service steam-keyboard-patch): injects the patch
// lib/default.nix mkPatch) into Steam's UI via CEF DevTools and // (patch.js wrapped with the finder library by mkPatch,
// performs the key requests of the patched VR keyboard with xdotool on :0. // 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 // On SIGTERM/SIGINT it reverts the patch (unpatch.js), so stopping the service
// restores Steam's stock keyboard without restarting Steam. // restores Steam's stock keyboard without restarting Steam.
// usage: node helper.mjs <patch.js> <unpatch.js> [xdotool] // usage: node xdotool-helper.mjs <patch.js> <unpatch.js> [xdotool]
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { execFile } from 'node:child_process'; import { execFile } from 'node:child_process';
+20 -17
View File
@@ -1,22 +1,24 @@
# Swipe typing, suggestions and a Backspace drag for Steam's VR keyboard. # Swipe typing, suggestions and a Backspace drag for Steam's VR keyboard
# - vr-keyboard/patch.js (Steam UI, 8080): swipe gestures (decoder.js), a # (steamFrame.keyboard.vr; the extra keys are vr-keyboard-extra-keys.nix).
# model of what the keyboard typed (textmodel.js), suggestions (swipe # - 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 # alternatives, corrections and completions, corrector.js; never changing
# text by themselves), Backspace drag (left: delete with a detent at word # text by themselves), Backspace drag (left: delete with a detent at word
# borders; right: retype), haptic ticks. # borders; right: retype), haptic ticks.
# - The suggestion strip: over the number row ("inside"), or a SteamVR # - The suggestion strip: over the number row ("inside"), or a SteamVR
# dashboard panel above/below the keyboard (panel.js in systemui, 8087), # dashboard panel above/below the keyboard (suggestions-panel/patch.js in
# fed by relay.mjs (user service vr-keyboard-relay). # systemui, 8087), fed by suggestions-panel/relay.mjs (user service
# - Dictionary: built by gen-dict.py from wordfreq frequency lists, filtered # vr-keyboard-relay).
# and cased by Hunspell (nixpkgs' hunspellDicts). # - 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 # 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 # one resets its text model on the extra keys it can't follow). Tests:
# checks below (also flake check `vr-keyboard`). # check.nix (built before the patch; also flake check `vr-keyboard`).
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let let
cfg = config.steamFrame.keyboard.vr; cfg = config.steamFrame.keyboard.vr;
inherit (lib) mkOption mkEnableOption types; 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); on = cfg.enable && (cfg.swipe.enable || cfg.autocorrect.enable || cfg.completions.enable || cfg.backspaceDrag.enable);
panel = on && cfg.suggestions.position != "inside"; panel = on && cfg.suggestions.position != "inside";
@@ -34,9 +36,10 @@ let
++ [ { language = "en"; hunspell = "en_US"; words = if layoutLanguage != null then 40000 else 60000; ++ [ { language = "en"; hunspell = "en_US"; words = if layoutLanguage != null then 40000 else 60000;
frequencyOffset = if layoutLanguage != null then -0.3 else 0.0; } ]; 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 ]; } '' patch = pkgs.runCommand "vr-keyboard.js" { nativeBuildInputs = [ pkgs.nodejs ]; } ''
: ${built.checks} : ${checks}
cp ${uiLib.mkPatch { cp ${uiLib.mkPatch {
name = "vr-keyboard"; name = "vr-keyboard";
src = ./vr-keyboard/patch.js; src = ./vr-keyboard/patch.js;
@@ -52,7 +55,7 @@ let
inherit (cfg.backspaceDrag) wordDetentPixels; inherit (cfg.backspaceDrag) wordDetentPixels;
inherit (cfg) haptics; 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 }} $out
node --check $out node --check $out
''; '';
@@ -180,7 +183,7 @@ in {
checks = mkOption { checks = mkOption {
type = types.package; type = types.package;
readOnly = true; readOnly = true;
default = built.checks; default = checks;
defaultText = lib.literalMD "the tests, built with the configured dictionary"; defaultText = lib.literalMD "the tests, built with the configured dictionary";
description = "The tests (also built with the patch)."; description = "The tests (also built with the patch).";
}; };
@@ -202,15 +205,15 @@ in {
name = "vr-keyboard-panel"; name = "vr-keyboard-panel";
endpoint = "http://127.0.0.1:8087"; endpoint = "http://127.0.0.1:8087";
target.title = "systemui"; target.title = "systemui";
patch = pkgs.writeText "vr-keyboard-panel.js" "(${builtins.readFile ./vr-keyboard/panel.js})()"; patch = pkgs.writeText "vr-keyboard-panel.js" "(${builtins.readFile ./vr-keyboard/suggestions-panel/patch.js})()";
unpatch = ./vr-keyboard/unpatch-panel.js; unpatch = ./vr-keyboard/suggestions-panel/unpatch.js;
}; };
steamFrame.session.services.${if panel then "restart" else "stop"} = [ "vr-keyboard-relay.service" ]; steamFrame.session.services.${if panel then "restart" else "stop"} = [ "vr-keyboard-relay.service" ];
} }
(lib.mkIf panel { (lib.mkIf panel {
systemd.user.services.vr-keyboard-relay = { systemd.user.services.vr-keyboard-relay = {
Unit.Description = "Relay of the VR keyboard's suggestion strip (Steam UI <-> SteamVR dashboard)"; 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" ]; Install.WantedBy = [ "default.target" ];
}; };
}) })
-39
View File
@@ -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.<system>.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"}
'';
}
+30
View File
@@ -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"}
''
+24
View File
@@ -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;
})}
''
+6 -6
View File
@@ -1,8 +1,8 @@
// Gestures and suggestions for Steam's VR keyboard (vr-keyboard.nix), // Gestures and suggestions for Steam's VR keyboard (vr-keyboard.nix),
// injected into Steam's SharedJSContext (8080). The keyboard is a popup of // injected into Steam's SharedJSContext (8080). The keyboard is a popup of
// that context ("SteamVR - Keyboard"); everything works on its document. // that context ("SteamVR - Keyboard"); everything works on its document.
// mkPatch convention plus four more arguments: decoder.js, textmodel.js, // mkPatch convention plus four more arguments: swipe-decoder.js,
// corrector.js and the dictionary text ("word<TAB>zipf*10\n..."). // textmodel.js, corrector.js and the dictionary text ("word<TAB>zipf*10\n...").
// //
// - Swipe: the laser arrives as touch events; Steam types the key a touch // - 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 // 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 // replace text only while the model proves the characters they replace
// intact. // intact.
// - Strip: in the page over the number row ("inside") or as a SteamVR panel // - 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 // above/below the keyboard ("above"/"below", suggestions-panel/patch.js):
// CDP binding __sfuiStripOut, picks back via // state out via the CDP binding __sfuiStripOut, picks back via
// __sfuiSwipe.remote.pick(seq, index) (relay.mjs). // __sfuiSwipe.remote.pick(seq, index) (suggestions-panel/relay.mjs).
// All Steam internals are checked first (sigs, instance members); if one is // All Steam internals are checked first (sigs, instance members); if one is
// missing the keyboard stays stock. Only passive listeners; never blocks // missing the keyboard stays stock. Only passive listeners; never blocks
// Steam's events. Debugging: __sfuiSwipeLog, __sfuiSwipePaths // Steam's events. Debugging: __sfuiSwipeLog, __sfuiSwipePaths
@@ -87,7 +87,7 @@
const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k); const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
// ---- trail and in-page strip ------------------------------------------------- // ---- 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'); const style = doc.createElement('style');
style.textContent = ` style.textContent = `
#sfui-swipe-trail { position: fixed; left: 0; top: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 10000; } #sfui-swipe-trail { position: fixed; left: 0; top: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 10000; }
@@ -1,6 +1,7 @@
// The suggestion strip as a SteamVR dashboard panel above or below Steam's // The suggestion strip as a SteamVR dashboard panel above or below Steam's
// VR keyboard (suggestions.position "above" / "below"), injected into // VR keyboard (suggestions.position "above" / "below"; patch name
// SteamVR's systemui page (8087). State comes from the keyboard page through relay.mjs: // "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 // __sfuiKbdStrip.show({ seq, items, current, visible, style }); a click calls
// the binding __sfuiStripPick('{"seq":n,"index":i}'). // the binding __sfuiStripPick('{"seq":n,"index":i}').
// //
@@ -1,6 +1,7 @@
// relay.mjs: carries the suggestion strip between Steam's keyboard page // relay.mjs (user service vr-keyboard-relay): carries the suggestion strip
// (SharedJSContext, 8080) and SteamVR's systemui page (8087) for // between Steam's keyboard page (../patch.js in SharedJSContext, 8080) and
// suggestions.position "above" / "below". CDP bindings: the keyboard page calls // 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 // __sfuiStripOut(state json) -> systemui __sfuiKbdStrip.show(state); the
// panel calls __sfuiStripPick({ seq, index } json) -> keyboard page // panel calls __sfuiStripPick({ seq, index } json) -> keyboard page
// __sfuiSwipe.remote.pick(seq, index). Only these two messages, validated. // __sfuiSwipe.remote.pick(seq, index). Only these two messages, validated.
@@ -1,5 +1,5 @@
// Reverts panel.js in SteamVR's systemui page: removes the strip panel, its // Reverts suggestions-panel/patch.js in SteamVR's systemui page: removes the
// embedded-UV slot and stylesheet. Safe when nothing is patched. // strip panel, its embedded-UV slot and stylesheet. Safe when nothing is patched.
(() => { (() => {
const S = window.__sfuiKbdStrip; const S = window.__sfuiKbdStrip;
if (!S) return 'not patched'; if (!S) return 'not patched';
@@ -1,5 +1,5 @@
// decoder.js: swipe path -> words. Evaluates to { VERSION, parseDict, layout, // swipe-decoder.js: swipe path -> words. Evaluates to { VERSION, parseDict,
// decode, ... }. SHARK2-style template matching (Kristensson & Zhai 2004): // layout, decode, ... }. SHARK2-style template matching (Kristensson & Zhai 2004):
// each word's ideal path runs through its key centres; candidates starting // 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 // 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 // by point distances of the resampled paths (proportional and DTW), how
@@ -9,7 +9,7 @@
const VERSION = 3; const VERSION = 3;
const N = 32; // resample points const N = 32; // resample points
const SKIP = new Set(["'", '-', '\u2019']); 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 = { const W = {
loc: 4.4, // mean point distance, proportional alignment loc: 4.4, // mean point distance, proportional alignment
dtw: 2.0, // mean point distance, dynamic time warping dtw: 2.0, // mean point distance, dynamic time warping
+1 -1
View File
@@ -1,5 +1,5 @@
// Offline check of corrector.js on the built dictionary. // Offline check of corrector.js on the built dictionary.
// usage: node corrector.test.mjs <corrector.js> <decoder.js> <dictionary.js> // usage: node corrector.test.mjs <corrector.js> <swipe-decoder.js> <dictionary.js>
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
@@ -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). // keyboard (key centres measured in the keyboard popup, 854x280 CSS px).
// usage: node decoder.test.mjs <decoder.js> <dictionary.js> [words-per-sample] [seed] // usage: node swipe-decoder.test.mjs <swipe-decoder.js> <dictionary.js>
// [words-per-sample] [seed]
// Each word's path goes through its key centres with random offsets (up to // 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 // 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. // accuracy for frequent German and English words and a list of fixed words.
+1 -1
View File
@@ -5,7 +5,7 @@
let let
cfg = config.steamFrame.dashboard.windowCurvature; cfg = config.steamFrame.dashboard.windowCurvature;
inherit (lib) mkOption types; inherit (lib) mkOption types;
inherit (import ./lib { inherit pkgs; }) mkPatch; inherit (import ./steam-ui-patches/lib { inherit pkgs; }) mkPatch;
path = [ "steamFrame" "dashboard" "windowCurvature" ]; path = [ "steamFrame" "dashboard" "windowCurvature" ];
rename = from: to: lib.mkRenamedOptionModule (path ++ [ from ]) (path ++ [ to ]); rename = from: to: lib.mkRenamedOptionModule (path ++ [ from ]) (path ++ [ to ]);
+7 -7
View File
@@ -6,9 +6,9 @@
// curve); dragging up/down with the laser sets the curvature live. // curve); dragging up/down with the laser sets the curvature live.
// //
// Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title // Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title
// "systemui"). mkPatch patch (see lib/default.nix); opts: { initial, max, // "systemui"). mkPatch patch (see steam-ui-patches/lib/default.nix); opts:
// step, detentPixels, detentPoints, dragThresholdPixels, dragPixelsPerUnit, // { initial, max, step, detentPixels, detentPoints, dragThresholdPixels,
// barDragPixelsPerUnit, haptics }. // dragPixelsPerUnit, barDragPixelsPerUnit, haptics }.
// //
// Stock curvature (frame.curvature, systemui's `curvature` component): // Stock curvature (frame.curvature, systemui's `curvature` component):
// shouldCurve = m_bCurveOverride (set by ToggleCurvature(), cleared on dock // 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 // 1.8 m) : 1000; its panels reference it as "curvature-origin-id" and
// vrcompositor bends them onto a cylinder around it (curvature = 1/radius). // vrcompositor bends them onto a cylinder around it (curvature = 1/radius).
// Those MobX properties are non-configurable, so this patch hooks the mailbox // Those MobX properties are non-configurable, so this patch hooks the mailbox
// SendMessage (lib/hooks.js, shared with dashboard-windows) and rewrites the // SendMessage (steam-ui-patches/lib/hooks.js, shared with dashboard-windows)
// origin's z in outgoing scene graphs to stock / value (1 = stock, 2 = half // and rewrites the origin's z in outgoing scene graphs to stock / value
// the radius, 0 = flat = stock toggle off). On/off stays the stock state, so // (1 = stock, 2 = half the radius, 0 = flat = stock toggle off). On/off stays
// stock toggle and wheel always agree. // the stock state, so stock toggle and wheel always agree.
// //
// Values per window (key: first overlay key) in // Values per window (key: first overlay key) in
// window.__sfuiWindowCurvatureState (schema 1: { values, log }); kept across // window.__sfuiWindowCurvatureState (schema 1: { values, log }); kept across
+8 -6
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env node #!/usr/bin/env node
// check-signatures.mjs: checks offline (no Steam, no browser) that every // check-signatures.mjs: checks offline (no Steam, no browser) that every
// signature in modules/lib/signatures.json still matches the installed Steam / // signature in modules/steam-ui-patches/lib/signatures.json still matches the
// SteamVR web UI bundles: module and export signatures exactly once, // installed Steam / SteamVR web UI bundles: module and export signatures
// "expects" strings still present (warnings), "stylesheet" signatures exactly // exactly once, "expects" strings still present (warnings), "stylesheet"
// one file of the bundle's "styles" directory. Run it after a Steam update: // 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 // 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 // Modules come from webpack-modules.mjs (factory sources as the page's
// Function.prototype.toString sees them). For export signatures the matched // Function.prototype.toString sees them). For export signatures the matched
// factory runs in a throwaway VM context where imports and unknown globals // 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 { readFileSync, readdirSync, existsSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path'; import { dirname, join, resolve } from 'node:path';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
@@ -29,7 +31,7 @@ import vm from 'node:vm';
import { loadBundles, pageFiles } from './webpack-modules.mjs'; import { loadBundles, pageFiles } from './webpack-modules.mjs';
const here = dirname(fileURLToPath(import.meta.url)); const here = dirname(fileURLToPath(import.meta.url));
const libDir = join(here, '..', 'modules', 'lib'); const libDir = join(here, '..', 'modules', 'steam-ui-patches', 'lib');
// ---- arguments ----------------------------------------------------------------- // ---- arguments -----------------------------------------------------------------
const args = process.argv.slice(2); const args = process.argv.slice(2);
+2 -1
View File
@@ -1,7 +1,8 @@
// Replays recorded swipes (window.__sfuiSwipePaths of Steam's SharedJSContext, // Replays recorded swipes (window.__sfuiSwipePaths of Steam's SharedJSContext,
// saved as JSON) through the decoder: top candidates and the cost terms of // 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. // the expected word. The dictionary is the built vr-keyboard-dictionary.js.
// usage: node scripts/vr-keyboard-replay.mjs modules/vr-keyboard/decoder.js <dictionary.js> <paths.json> [word] // usage: node scripts/vr-keyboard-replay.mjs modules/vr-keyboard/swipe-decoder.js
// <dictionary.js> <paths.json> [word]
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
const [, , decoderPath, dictPath, pathsPath, expected] = process.argv; const [, , decoderPath, dictPath, pathsPath, expected] = process.argv;
const D = (0, eval)(readFileSync(decoderPath, 'utf8')); const D = (0, eval)(readFileSync(decoderPath, 'utf8'));