mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
VR keyboard: Shift+Tab moves the focus backwards
Steam sends Tab as the text "\t" and drops Shift. With Shift, Tab now goes to the xdotool helper (shift+Tab, ISO_Left_Tab on any keymap); the suggestions text model resets on it like on Shift+arrows. The helper's allowlist moves to allowlist.mjs with a test (flake check vr-keyboard-extra-keys).
This commit is contained in:
1 parent
50e5991c59
commit
b2babb3859
11 files changed
+94
-43
No files matched your search
@@ -33,7 +33,7 @@ configuration, limitations and how it works.
|
||||
|
||||
**VR keyboard** ([docs/keyboard.md](docs/keyboard.md)):
|
||||
|
||||
- [Extra keys](docs/keyboard.md#extra-keys) (`keyboard.vr.extraKeys`): Esc/Ctrl/Alt, arrows, Delete, real chords and AltGr/non-ASCII characters.
|
||||
- [Extra keys](docs/keyboard.md#extra-keys) (`keyboard.vr.extraKeys`): Esc/Ctrl/Alt, arrows, Delete, Shift+Tab, real chords and AltGr/non-ASCII characters.
|
||||
- [Swipe and suggestions](docs/keyboard.md#swipe-and-suggestions) (`keyboard.vr`): swipe typing, corrections, completions, Backspace drag.
|
||||
|
||||
**VR "+" menu** ([docs/launcher-menu.md](docs/launcher-menu.md)):
|
||||
|
||||
@@ -91,6 +91,8 @@ modules/
|
||||
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
|
||||
allowlist.mjs service: the keys the helper may send
|
||||
check.nix, tests/ test: the allowlist
|
||||
vr-keyboard.nix swipe, suggestions, Backspace drag (keyboard.vr)
|
||||
vr-keyboard/
|
||||
patch.js, unpatch.js Steam: gestures, text model, suggestion strip
|
||||
|
||||
+13
-4
@@ -25,6 +25,10 @@ backtick, `ä ö ü €`) come out as `1`.
|
||||
AltGr and layout keys.
|
||||
- AltGr + arrows: Home, End, Page Up, Page Down (hinted on the keys);
|
||||
Shift + arrows select text.
|
||||
- Shift + Tab moves the focus backwards (stock Steam drops the Shift);
|
||||
Ctrl + Shift + Tab works too (e.g. previous Firefox tab). A locked Shift
|
||||
is released after it, like a tapped one. Shift + Enter and
|
||||
Shift + Backspace stay plain Enter and Backspace.
|
||||
- AltGr + the key left of Backspace (`´` on German, `=` on US): Delete,
|
||||
labelled like Steam's Delete key in its language (`Entf`; `Del` if that is
|
||||
longer), hinted on the key without AltGr; repeats while held. Layouts with
|
||||
@@ -46,8 +50,10 @@ after Steam restarts. Turning it off reverts the keyboard.
|
||||
is harmless, and Esc/Ctrl/Alt/arrows work regardless.
|
||||
|
||||
**Security:** the helper service that presses the keys only accepts
|
||||
single-key Ctrl/Alt chords, the extra keys, Ctrl/Alt hold/release and single
|
||||
non-ASCII/AltGr characters; it cannot type ASCII text or press Enter.
|
||||
single-key Ctrl/Alt chords, the extra keys, Shift + Tab, Ctrl/Alt
|
||||
hold/release and single non-ASCII/AltGr characters; it cannot type ASCII
|
||||
text or press Enter (`allowlist.mjs`, tested by the flake check
|
||||
`vr-keyboard-extra-keys`).
|
||||
|
||||
**Caveat:** depends on Steam UI internals; after a Steam update that changes
|
||||
them the keyboard stays stock and the journal says why (see
|
||||
@@ -63,8 +69,11 @@ Steam client 1790377368 (UI build 11041156).
|
||||
Steam's `SharedJSContext` over DevTools (`127.0.0.1:8080`) and re-injects
|
||||
it after Steam restarts; stopping it (or disabling the option)
|
||||
runs the unpatch.
|
||||
- Ctrl/Alt chords and Esc are sent with `xdotool key` on `:0` (focus follows
|
||||
the VR-selected window); a toggled Ctrl/Alt is held down with `xdotool`.
|
||||
- Ctrl/Alt chords, Esc, Shift + arrows and Shift + Tab are sent with
|
||||
`xdotool key` on `:0` (focus follows the VR-selected window); a toggled
|
||||
Ctrl/Alt is held down with `xdotool`. Shift is never held as a real key
|
||||
(Steam types capitals itself); `shift+Tab` is `ISO_Left_Tab` on any
|
||||
keymap.
|
||||
- Problem characters (non-ASCII, AltGr/dead-key characters on the German
|
||||
keymap) are typed with `xdotool type`; everything else goes through
|
||||
Steam's own text emulation. On other keymaps those characters are still
|
||||
|
||||
@@ -65,9 +65,10 @@
|
||||
}) self.packages;
|
||||
|
||||
# Tests of the VR keyboard (text model, corrector, swipe decoder on the
|
||||
# default German + English dictionary), the Jellyfin mpv shim, the
|
||||
# Firefox wrapper, the launchers, install.sh cleanup, the
|
||||
# applications.menu link and the portal config: nix flake check
|
||||
# default German + English dictionary), the extra keys' xdotool
|
||||
# allowlist, the Jellyfin mpv shim, the Firefox wrapper, the launchers,
|
||||
# install.sh cleanup, the applications.menu link and the portal config:
|
||||
# nix flake check
|
||||
checks = forSystems (pkgs: {
|
||||
applications-menu = import ./modules/applications-menu/check.nix { inherit pkgs; };
|
||||
cleanup = import ./modules/cleanup/check.nix { inherit pkgs; };
|
||||
@@ -76,6 +77,7 @@
|
||||
launchers = import ./modules/launchers/check.nix { inherit pkgs; };
|
||||
portal = import ./modules/portal/check.nix { inherit pkgs; };
|
||||
vr-keyboard = import ./modules/vr-keyboard/check.nix { inherit pkgs; };
|
||||
vr-keyboard-extra-keys = import ./modules/vr-keyboard-extra-keys/check.nix { inherit pkgs; };
|
||||
});
|
||||
|
||||
# nix flake init -t github:lhns/steam-frame-nix
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
# "Unknown method" in VR), and its layouts are hardcoded.
|
||||
# - patch.js (injected over CEF DevTools, 127.0.0.1:8080) adds a bottom row
|
||||
# Esc Ctrl Alt [space] AltGr ← ↑ ↓ → (AltGr+arrows = Pos1/PgUp/PgDn/End) and
|
||||
# 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.
|
||||
# hands chords, Shift+arrows, Shift+Tab and characters Steam would turn
|
||||
# into "1" (non-ASCII, AltGr/dead keys) to the helper. AltGr + the key left
|
||||
# of Backspace = Delete.
|
||||
# - 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, allowlist.mjs: no ASCII text, no Enter) and unpatches on stop.
|
||||
# Tests: vr-keyboard-extra-keys/check.nix (flake check vr-keyboard-extra-keys).
|
||||
# Tested with Steam client 1790377368 (UI build 11041156).
|
||||
{ config, pkgs, lib, ... }:
|
||||
let
|
||||
@@ -17,6 +18,14 @@ let
|
||||
name = "steam-keyboard-patch";
|
||||
src = ./vr-keyboard-extra-keys/patch.js;
|
||||
};
|
||||
# The helper imports allowlist.mjs from its own directory.
|
||||
helper = lib.fileset.toSource {
|
||||
root = ./vr-keyboard-extra-keys;
|
||||
fileset = lib.fileset.unions [
|
||||
./vr-keyboard-extra-keys/xdotool-helper.mjs
|
||||
./vr-keyboard-extra-keys/allowlist.mjs
|
||||
];
|
||||
};
|
||||
cfg = config.steamFrame.keyboard.vr.extraKeys;
|
||||
in {
|
||||
imports = [
|
||||
@@ -38,7 +47,7 @@ in {
|
||||
Service = {
|
||||
ExecStart = lib.escapeShellArgs [
|
||||
"${pkgs.nodejs}/bin/node"
|
||||
"${./vr-keyboard-extra-keys/xdotool-helper.mjs}"
|
||||
"${helper}/xdotool-helper.mjs"
|
||||
"${patch}"
|
||||
"${./vr-keyboard-extra-keys/unpatch.js}"
|
||||
"${pkgs.xdotool}/bin/xdotool"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Allowlist of xdotool-helper.mjs: requests come from Steam's UI JS, so the
|
||||
// helper must not be able to type ASCII text or press Enter on its behalf.
|
||||
// Accepted:
|
||||
// key:<combo> the extra keys (Esc, Del, Home, End, PgUp/PgDn, arrows),
|
||||
// optionally with modifiers; ctrl and/or alt (+ optional
|
||||
// shift) with one key; or shift+Tab
|
||||
// type:<char> one character Steam's key emulation can't produce: non-ASCII
|
||||
// (äöü߀§°´…) or | @ { [ ] } \ ~ ^ `
|
||||
// down:<mod> / up:<mod> hold/release ctrl or alt (checked in the helper)
|
||||
const SPECIAL = new Set(['Escape', 'Delete', 'Home', 'End', 'Prior', 'Next', 'Left', 'Right', 'Up', 'Down']);
|
||||
const KEY = /^([a-z0-9]|space|BackSpace|Tab|period|comma|minus|plus|numbersign|less|slash|ssharp|udiaeresis|odiaeresis|adiaeresis)$/;
|
||||
|
||||
export function allowedCombo(combo) {
|
||||
const parts = combo.split('+');
|
||||
const key = parts.pop();
|
||||
const mods = new Set(parts);
|
||||
if (parts.length !== mods.size || ![...mods].every((m) => ['ctrl', 'alt', 'shift'].includes(m))) return false;
|
||||
if (SPECIAL.has(key)) return true;
|
||||
if (key === 'Tab' && mods.size === 1 && mods.has('shift')) return true; // backwards focus
|
||||
return KEY.test(key) && (mods.has('ctrl') || mods.has('alt'));
|
||||
}
|
||||
|
||||
export function allowedChar(c) {
|
||||
if ([...c].length !== 1) return false;
|
||||
const cp = c.codePointAt(0);
|
||||
return (cp > 0xa0 && !/\s|\p{C}/u.test(c)) || '|@{[]}\\~^`'.includes(c);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Tests of the extra keys (flake check `vr-keyboard-extra-keys`): the xdotool
|
||||
# helper's allowlist (tests/allowlist.test.mjs).
|
||||
{ pkgs }:
|
||||
pkgs.runCommand "vr-keyboard-extra-keys-checks" { nativeBuildInputs = [ pkgs.nodejs ]; } ''
|
||||
node ${./tests/allowlist.test.mjs} ${./allowlist.mjs} | tee $out
|
||||
''
|
||||
@@ -23,7 +23,7 @@
|
||||
// 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 VERSION = 23;
|
||||
const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg);
|
||||
const mods = find.resolvePatch('webpackChunksteamui', sigs);
|
||||
if (typeof mods === 'string') return mods;
|
||||
@@ -252,8 +252,9 @@
|
||||
const key = st?.strKey;
|
||||
const ts = this.state?.toggleStates || {};
|
||||
const ctrl = active(ts.Control), alt = active(ts.Alt), shift = active(ts.Shift);
|
||||
// Extra keys always; Shift+arrow too (Steam's own arrow path drops Shift).
|
||||
const special = key?.startsWith('VKX_') || (shift && key?.startsWith('Arrow'));
|
||||
// Extra keys always; Shift+arrow and Shift+Tab too (Steam sends arrows
|
||||
// and Tab without Shift: Tab as the text "\t").
|
||||
const special = key?.startsWith('VKX_') || (shift && (key?.startsWith('Arrow') || key === 'Tab'));
|
||||
const sym = key && !TOGGLES.includes(key) && (special || ctrl || alt) && keysym(key);
|
||||
if (!sym) return orig.call(this, st);
|
||||
send('key:' + [ctrl && 'ctrl', alt && 'alt', shift && 'shift', sym].filter(Boolean).join('+'));
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// allowlist.mjs: what the xdotool helper accepts from Steam's UI JS.
|
||||
// usage: node allowlist.test.mjs <allowlist.mjs>
|
||||
import assert from 'node:assert/strict';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const { allowedCombo, allowedChar } = await import(pathToFileURL(process.argv[2]).href);
|
||||
const accept = ['shift+Tab', 'ctrl+Tab', 'ctrl+shift+Tab', 'ctrl+shift+t', 'alt+f',
|
||||
'shift+Left', 'Escape', 'Delete', 'shift+End', 'ctrl+BackSpace'];
|
||||
const reject = ['Tab', 'shift+a', 'a', 'Return', 'shift+Return', 'ctrl+Return', 'shift+BackSpace',
|
||||
'shift+shift+Tab', 'super+Tab', 'shift+space', 'ctrl+a+b'];
|
||||
const acceptChar = ['ä', '€', '|', '@', '\\', '`'];
|
||||
const rejectChar = ['a', 'A', '1', ' ', '\n', '\t', 'ab', ' '];
|
||||
for (const c of accept) assert.equal(allowedCombo(c), true, `accepts key:${c}`);
|
||||
for (const c of reject) assert.equal(allowedCombo(c), false, `rejects key:${c}`);
|
||||
for (const c of acceptChar) assert.equal(allowedChar(c), true, `accepts type:${c}`);
|
||||
for (const c of rejectChar) assert.equal(allowedChar(c), false, `rejects type:${JSON.stringify(c)}`);
|
||||
console.log(`allowlist: ${accept.length + reject.length + acceptChar.length + rejectChar.length} cases passed`);
|
||||
@@ -8,6 +8,7 @@
|
||||
// usage: node xdotool-helper.mjs <patch.js> <unpatch.js> [xdotool]
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { allowedCombo, allowedChar } from './allowlist.mjs';
|
||||
|
||||
const [, , patchPath, unpatchPath, xdotool = 'xdotool'] = process.argv;
|
||||
const PATCH = readFileSync(patchPath, 'utf8');
|
||||
@@ -19,30 +20,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
// :0 is the Steam session's Xwayland, where the VR app windows live.
|
||||
const ENV = { ...process.env, DISPLAY: process.env.VRKBD_DISPLAY || ':0', LC_ALL: 'C.UTF-8' };
|
||||
|
||||
// Allowlist: requests come from Steam's UI JS, so the helper must not be able
|
||||
// to type ASCII text or press Enter on its behalf. Accepted:
|
||||
// key:<combo> the extra keys (Esc, Del, Home, End, PgUp/PgDn, arrows),
|
||||
// optionally with modifiers; or ctrl and/or alt (+ optional
|
||||
// shift) with one key
|
||||
// type:<char> one character Steam's key emulation can't produce: non-ASCII
|
||||
// (äöü߀§°´…) or | @ { [ ] } \ ~ ^ `
|
||||
// down:<mod> / up:<mod> hold/release ctrl or alt
|
||||
const SPECIAL = new Set(['Escape', 'Delete', 'Home', 'End', 'Prior', 'Next', 'Left', 'Right', 'Up', 'Down']);
|
||||
const KEY = /^([a-z0-9]|space|BackSpace|Tab|period|comma|minus|plus|numbersign|less|slash|ssharp|udiaeresis|odiaeresis|adiaeresis)$/;
|
||||
const MODKEY = { ctrl: 'Control_L', alt: 'Alt_L' };
|
||||
function allowedCombo(combo) {
|
||||
const parts = combo.split('+');
|
||||
const key = parts.pop();
|
||||
const mods = new Set(parts);
|
||||
if (parts.length !== mods.size || ![...mods].every((m) => ['ctrl', 'alt', 'shift'].includes(m))) return false;
|
||||
if (SPECIAL.has(key)) return true;
|
||||
return KEY.test(key) && (mods.has('ctrl') || mods.has('alt'));
|
||||
}
|
||||
function allowedChar(c) {
|
||||
if ([...c].length !== 1) return false;
|
||||
const cp = c.codePointAt(0);
|
||||
return (cp > 0xa0 && !/\s|\p{C}/u.test(c)) || '|@{[]}\\~^`'.includes(c);
|
||||
}
|
||||
|
||||
const held = new Set(); // modifiers currently held down via keydown
|
||||
const xdo = (args) => execFile(xdotool, args, { env: ENV }, (err) => err && console.error('xdotool', args.join(' '), err.message));
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
// Steam's events. Debugging: __sfuiSwipeLog, __sfuiSwipePaths
|
||||
// (scripts/vr-keyboard-replay.mjs).
|
||||
((find, sigs, opts, hooks, D, T, C, DICT) => {
|
||||
const VERSION = 23;
|
||||
const VERSION = 24;
|
||||
const G = window;
|
||||
const O = opts;
|
||||
|
||||
@@ -186,9 +186,9 @@
|
||||
const key = st?.strKey, ts = this.state?.toggleStates || {};
|
||||
if (typeof key === 'string' && !TOGGLES.has(key) && !/^(SwitchKeys_|IME_)/.test(key)) {
|
||||
// Extra keys typed by keyboard.vr.extraKeys with xdotool (chords, Esc,
|
||||
// arrows, AltGr Delete: text after the cursor) and dead keys: the
|
||||
// model can't follow them.
|
||||
if (on(ts.Control) || on(ts.Alt) || key.startsWith('VKX_') || st.strDeadKeyNext || (on(ts.Shift) && key.startsWith('Arrow'))) {
|
||||
// arrows, Shift+Tab, AltGr Delete: text after the cursor) and dead
|
||||
// keys: the model can't follow them.
|
||||
if (on(ts.Control) || on(ts.Alt) || key.startsWith('VKX_') || st.strDeadKeyNext || (on(ts.Shift) && (key.startsWith('Arrow') || key === 'Tab'))) {
|
||||
reset(`key ${key}`);
|
||||
} else { model.freeze(); refreshLater(); }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user