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

+1 -1
View File
@@ -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 = {
+5 -3
View File
@@ -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.
+1 -1
View File
@@ -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" ];
+2 -2
View File
@@ -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 <FrameControlsItem params={type, action_id}>
// (2 = action, 1 = spacer); <FrameControls> passes the lists to the MobX
+1 -1
View File
@@ -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 {
+3 -3
View File
@@ -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';
+3 -2
View File
@@ -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) => {
@@ -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';
+3 -2
View File
@@ -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,
+1 -1
View File
@@ -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 ];
+8 -7
View File
@@ -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).
'';
};
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
# "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";
@@ -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("<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.
//
// 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);
@@ -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.
(() => {
@@ -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 <patch.js> <unpatch.js> [xdotool]
// usage: node xdotool-helper.mjs <patch.js> <unpatch.js> [xdotool]
import { readFileSync } from 'node:fs';
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.
# - 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" ];
};
})
-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),
// 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 ("word<TAB>zipf*10\n...").
// mkPatch convention plus four more arguments: swipe-decoder.js,
// 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
// 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; }
@@ -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}').
//
@@ -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.
@@ -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';
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
// 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 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).
// 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
// 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.
+1 -1
View File
@@ -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 ]);
+7 -7
View File
@@ -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