mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 05:00:24 +02:00
Keyboard patch cleanup; arrow icons centered again
- drop cruft: v4 DispatchKeypress undo, unused VKM import and __vrkbdPatched flag, redundant VRKBD_DISPLAY env in the unit - one wrap() helper for all replaced functions; key names for the stylesheet derived from the key definitions instead of repeated - unpatch: restore every layout ever patched (also ones disabled since) and the remembered component class instead of walking the tree again - helper: debounce re-injects after bursts of new JS contexts; resolve pending DevTools calls when the connection closes - arrow icons vertically centered again (hint stays at the bottom edge) - updated header comments
This commit is contained in:
1 parent
630357b886
commit
7d8e2f14cf
4 files changed
+128
-143
No files matched your search
@@ -1,20 +1,16 @@
|
|||||||
# Esc/Ctrl/Alt and four arrow keys on Steam's VR keyboard.
|
# Runtime patch of Steam's on-screen (VR) keyboard. Steam's layouts are
|
||||||
# The keyboard is part of Steam's UI (hardcoded layouts in steamui JS), and in
|
# hardcoded in its UI, and in VR it can only send text (no Ctrl/Alt/Esc;
|
||||||
# VR it can only send text; SteamClient.Input.ControllerKeyboardSetKeyState
|
# SteamClient.Input.ControllerKeyboardSetKeyState throws "Unknown method").
|
||||||
# throws "Unknown method". So:
|
# - patch.js is injected into Steam's running UI through its CEF DevTools port
|
||||||
# - patch.js is injected at runtime into Steam's UI through its CEF
|
# (127.0.0.1:8080; SteamOS starts Steam with -cef-enable-debugging); Steam's
|
||||||
# DevTools port (127.0.0.1:8080; SteamOS starts Steam with
|
# files are untouched. Bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ →, with
|
||||||
# -cef-enable-debugging). Steam's files are untouched, so updates don't undo
|
# AltGr + arrows = Pos1/PgUp/PgDn/End. Chords, Shift+arrows and characters
|
||||||
# it. It rebuilds the bottom row and, while Ctrl/Alt is active (or for Esc),
|
# Steam's key emulation turns into "1" (non-ASCII, AltGr/dead keys on the
|
||||||
# hands the key combo to the helper instead of typing text. It also holds
|
# de keymap) are handed to the helper; Ctrl/Alt are held while toggled.
|
||||||
# the real Ctrl/Alt down while the toggle is on (Ctrl+scroll), and routes
|
# - helper.mjs keeps the injection alive (Steam restarts, popup recreated),
|
||||||
# characters Steam's key emulation turns into "1" (non-ASCII, AltGr/dead
|
# performs those requests with xdotool on :0 (X focus follows the window
|
||||||
# keys on the de keymap: |@{[]}\~^`äöü€…) to the helper.
|
# selected in VR) and reverts the patch when stopped (unpatch.js). Its
|
||||||
# - helper.mjs keeps that injection alive (Steam restarts, keyboard popup
|
# allowlist can't type ASCII text or press Enter.
|
||||||
# recreated) and presses the combos with `xdotool key` on :0, where gamescope
|
|
||||||
# keeps X focus on the window selected in VR. It only accepts ctrl/alt
|
|
||||||
# chords and the extra keys, so the Steam UI can't use it to type text or
|
|
||||||
# press Enter.
|
|
||||||
# Depends on Steam UI internals; tested with Steam client 1790377368.
|
# Depends on Steam UI internals; tested with Steam client 1790377368.
|
||||||
{ config, pkgs, lib, ... }: {
|
{ config, pkgs, lib, ... }: {
|
||||||
imports = [ ./session.nix ];
|
imports = [ ./session.nix ];
|
||||||
@@ -39,7 +35,6 @@
|
|||||||
"${./steam-keyboard-patch/unpatch.js}"
|
"${./steam-keyboard-patch/unpatch.js}"
|
||||||
"${pkgs.xdotool}/bin/xdotool"
|
"${pkgs.xdotool}/bin/xdotool"
|
||||||
];
|
];
|
||||||
Environment = "VRKBD_DISPLAY=:0";
|
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
RestartSec = 5;
|
RestartSec = 5;
|
||||||
TimeoutStopSec = 5; # the helper unpatches Steam's UI on SIGTERM
|
TimeoutStopSec = 5; # the helper unpatches Steam's UI on SIGTERM
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const UNPATCH = readFileSync(unpatchPath, 'utf8');
|
|||||||
let current = null; // CDP `call` of the live session, if any
|
let current = null; // CDP `call` of the live session, if any
|
||||||
const CDP = 'http://127.0.0.1:8080/json/list';
|
const CDP = 'http://127.0.0.1:8080/json/list';
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
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' };
|
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
|
// Allowlist: requests come from Steam's UI JS, so the helper must not be able
|
||||||
@@ -76,14 +77,25 @@ async function session() {
|
|||||||
const v = r.result?.result?.value ?? r.result?.exceptionDetails?.exception?.description;
|
const v = r.result?.result?.value ?? r.result?.exceptionDetails?.exception?.description;
|
||||||
if (v !== 'patched' || process.env.VRKBD_VERBOSE) console.log('inject:', v);
|
if (v !== 'patched' || process.env.VRKBD_VERBOSE) console.log('inject:', v);
|
||||||
};
|
};
|
||||||
|
// New JS contexts (UI reload, popups) come in bursts: inject once after them.
|
||||||
|
let soon = null;
|
||||||
|
const injectSoon = () => { clearTimeout(soon); soon = setTimeout(() => inject().catch(() => {}), 3000); };
|
||||||
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
const m = JSON.parse(e.data);
|
const m = JSON.parse(e.data);
|
||||||
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); }
|
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); }
|
||||||
else if (m.method === 'Runtime.bindingCalled' && m.params.name === '__vrkbdKey') handle(m.params.payload);
|
else if (m.method === 'Runtime.bindingCalled' && m.params.name === '__vrkbdKey') handle(m.params.payload);
|
||||||
else if (m.method === 'Runtime.executionContextCreated') setTimeout(inject, 3000);
|
else if (m.method === 'Runtime.executionContextCreated') injectSoon();
|
||||||
};
|
};
|
||||||
const closed = new Promise((res) => { ws.onclose = () => { current = null; res(); }; });
|
const closed = new Promise((res) => {
|
||||||
|
ws.onclose = () => {
|
||||||
|
current = null;
|
||||||
|
clearTimeout(soon);
|
||||||
|
for (const r of pending.values()) r({}); // unblock calls that will never be answered
|
||||||
|
pending.clear();
|
||||||
|
res();
|
||||||
|
};
|
||||||
|
});
|
||||||
current = call;
|
current = call;
|
||||||
await call('Runtime.enable');
|
await call('Runtime.enable');
|
||||||
await call('Runtime.addBinding', { name: '__vrkbdKey' });
|
await call('Runtime.addBinding', { name: '__vrkbdKey' });
|
||||||
|
|||||||
@@ -1,140 +1,138 @@
|
|||||||
// 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 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 and four separate arrow keys
|
// - bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ → Close, stable with Shift
|
||||||
// - Ctrl/Alt chords and Esc are pressed for real (Steam can't: in VR
|
// and AltGr; AltGr + arrows = Pos1/PgUp/PgDn/End
|
||||||
// SteamClient.Input.ControllerKeyboardSetKeyState throws "Unknown method")
|
// - Ctrl/Alt chords, Esc, the AltGr arrow keys and Shift+arrows are pressed
|
||||||
|
// for real (Steam can't: in VR SteamClient.Input.ControllerKeyboardSetKeyState
|
||||||
|
// throws "Unknown method")
|
||||||
// - while Ctrl/Alt is active and the keyboard is open, the real modifier is
|
// - while Ctrl/Alt is active and the keyboard is open, the real modifier is
|
||||||
// held down, so e.g. Ctrl+scroll works
|
// held down, so e.g. Ctrl+scroll works
|
||||||
// - characters Steam's own key emulation (ControllerKeyboardSendText) turns
|
// - characters Steam's own key emulation (ControllerKeyboardSendText) turns
|
||||||
// into "1" -- anything non-ASCII or needing AltGr / a dead key on the X
|
// into "1" -- anything non-ASCII or needing AltGr / a dead key on the X
|
||||||
// keymap -- are typed by the helper instead
|
// keymap -- are typed by the helper instead
|
||||||
// Everything goes through the CDP binding window.__vrkbdKey("<op>:<arg>"),
|
// Everything goes through the CDP binding window.__vrkbdKey("<op>:<arg>"),
|
||||||
// executed by helper.mjs with xdotool on :0.
|
// executed by helper.mjs with xdotool on :0. Every replaced function keeps its
|
||||||
|
// original as __vrkbdOrig (unpatch.js restores them).
|
||||||
// Idempotent: safe to evaluate repeatedly.
|
// Idempotent: safe to evaluate repeatedly.
|
||||||
(() => {
|
(() => {
|
||||||
const VERSION = 7;
|
const VERSION = 8;
|
||||||
const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg);
|
const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg);
|
||||||
const wr = window.__vrkbdWr ||
|
const wr = window.__vrkbdWr ||
|
||||||
(webpackChunksteamui.push([[Symbol('vrkbd')], {}, (r) => { window.__vrkbdWr = r; }]), window.__vrkbdWr);
|
(webpackChunksteamui.push([[Symbol('vrkbd')], {}, (r) => { window.__vrkbdWr = r; }]), window.__vrkbdWr);
|
||||||
const Layouts = wr(40222); // keyboard layouts (exports G$: enabled, r_: current)
|
const Layouts = wr(40222); // keyboard layouts (exports G$: enabled, r_: current)
|
||||||
const VKM = wr(5363).PE; // VirtualKeyboardManager class
|
|
||||||
const Status = wr(58508).qL; // .VRKeyboardStatus
|
const Status = wr(58508).qL; // .VRKeyboardStatus
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
// Replace obj[name] with make(original), once per VERSION.
|
||||||
|
const wrap = (obj, name, make) => {
|
||||||
|
if (obj[name].__vrkbd === VERSION) return;
|
||||||
|
const orig = obj[name].__vrkbdOrig || obj[name];
|
||||||
|
const f = make(orig);
|
||||||
|
f.__vrkbd = VERSION; f.__vrkbdOrig = orig;
|
||||||
|
obj[name] = f;
|
||||||
|
changed = true;
|
||||||
|
};
|
||||||
|
|
||||||
// ---- bottom row ------------------------------------------------------------
|
// ---- bottom row ------------------------------------------------------------
|
||||||
// The keyboard window has a fixed height, so no extra row: Esc/Ctrl/Alt go
|
// The keyboard window has a fixed height, so no extra row: Esc/Ctrl/Alt go
|
||||||
// left of the space bar, and Steam's arrow keys become four separate keys
|
// left of the space bar, and Steam's arrow keys become four separate keys
|
||||||
// (stock layouts pair them as [Left, Up-when-shifted], [Right, Down-when-shifted]).
|
// (stock layouts pair them as [Left, Up-when-shifted], [Right, Down-when-shifted]).
|
||||||
// Key type "Half" for all added keys; the stylesheet below sets their width.
|
|
||||||
// Every entry is a [normal, shifted, altgr] triple: Steam's stock bottom row
|
// Every entry is a [normal, shifted, altgr] triple: Steam's stock bottom row
|
||||||
// lacks explicit variants, so with Shift the close icon jumps and with AltGr
|
// lacks explicit variants, so with Shift the close icon jumps and with AltGr
|
||||||
// keys turn into empty full-width panels. With AltGr the arrows become
|
// keys turn into empty full-width panels. null as the shifted variant means
|
||||||
// Pos1/Ende/PgUp/PgDn (Shift+arrows stay arrows, for selecting).
|
// "same key, no shift label".
|
||||||
const HALF = 2;
|
const HALF = 2; // key type "Half"; widths set by the stylesheet
|
||||||
const k = (key, label) => ({ key, label, type: HALF });
|
const k = (key, label) => ({ key, label, type: HALF });
|
||||||
// null as the shifted variant = "same key, no shift label". Steam still draws
|
|
||||||
// the AltGr variant as a small secondary label; the stylesheet hides that in
|
|
||||||
// the bottom row (the arrows keep theirs as a Pos1/Ende/Bild hint).
|
|
||||||
const same = (x) => [x, null, x];
|
const same = (x) => [x, null, x];
|
||||||
const MODS = [k('VKX_Escape', 'Esc'), k('Control', 'Ctrl'), k('Alt', 'Alt')].map(same);
|
const MODS = [k('VKX_Escape', 'Esc'), k('Control', 'Ctrl'), k('Alt', 'Alt')];
|
||||||
const half = (a) => ({ ...a, type: HALF });
|
const ALTGR_ARROWS = [k('VKX_Home', 'Pos1'), k('VKX_Prior', 'Bild↑'), k('VKX_Next', 'Bild↓'), k('VKX_End', 'Ende')];
|
||||||
const ARROWS = [ // Steam's ArrowLeft/Up/Down/Right keys
|
const ARROWS = [Layouts.Md, Layouts.GO, Layouts.xl, Layouts.B6] // Steam's ArrowLeft/Up/Down/Right keys
|
||||||
[Layouts.Md, k('VKX_Home', 'Pos1')],
|
.map((a, i) => [{ ...a, type: HALF }, null, ALTGR_ARROWS[i]]);
|
||||||
[Layouts.GO, k('VKX_Prior', 'Bild↑')],
|
|
||||||
[Layouts.xl, k('VKX_Next', 'Bild↓')],
|
|
||||||
[Layouts.B6, k('VKX_End', 'Ende')],
|
|
||||||
].map(([a, alt]) => [half(a), null, alt]);
|
|
||||||
const first = (x) => (Array.isArray(x) ? x.find((y) => y) : x);
|
const first = (x) => (Array.isArray(x) ? x.find((y) => y) : x);
|
||||||
const isArrow = (x) => (Array.isArray(x) ? x : [x]).some((y) => y && typeof y.key === 'string' && y.key.startsWith('Arrow'));
|
const isArrow = (x) => (Array.isArray(x) ? x : [x]).some((y) => y?.key?.startsWith?.('Arrow'));
|
||||||
const bottomRow = (row) => {
|
const bottomRow = (row) => {
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const x of row) {
|
for (const x of row) {
|
||||||
const key = first(x);
|
const key = first(x);
|
||||||
if (!key || isArrow(x)) continue;
|
if (!key || isArrow(x)) continue;
|
||||||
if (key.key === ' ') out.push(...MODS);
|
if (key.key === ' ') out.push(...MODS.map(same));
|
||||||
out.push(same(key));
|
out.push(same(key));
|
||||||
}
|
}
|
||||||
out.splice(out.length - 1, 0, ...ARROWS); // before Close/Done (last entry)
|
out.splice(out.length - 1, 0, ...ARROWS); // before Close/Done (last entry)
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
let changed = false;
|
window.__vrkbdLayouts ??= new Set(); // for unpatch.js, incl. layouts disabled since
|
||||||
const patchLayout = (l) => {
|
for (const l of [...(Layouts.G$() || []), Layouts.r_()]) {
|
||||||
if (!l || typeof l.rgLayout !== 'function' || l.rgLayout.__vrkbd === VERSION) return;
|
if (typeof l?.rgLayout !== 'function') continue;
|
||||||
changed = true;
|
window.__vrkbdLayouts.add(l);
|
||||||
const orig = l.rgLayout.__vrkbdOrig || l.rgLayout;
|
wrap(l, 'rgLayout', (orig) => (opts) => {
|
||||||
const f = (opts) => { const rows = orig(opts); return [...rows.slice(0, -1), bottomRow(rows[rows.length - 1])]; };
|
const rows = orig(opts);
|
||||||
f.__vrkbd = VERSION; f.__vrkbdOrig = orig;
|
return [...rows.slice(0, -1), bottomRow(rows[rows.length - 1])];
|
||||||
l.rgLayout = f;
|
});
|
||||||
};
|
}
|
||||||
for (const l of Layouts.G$() || []) patchLayout(l);
|
|
||||||
patchLayout(Layouts.r_());
|
|
||||||
|
|
||||||
// ---- text: characters Steam's key emulation can't produce --------------------
|
// ---- text: characters Steam's key emulation can't produce --------------------
|
||||||
// All keyboard text for gamescope windows ends up in
|
// All keyboard text for gamescope windows ends up in
|
||||||
// SteamClient.Input.ControllerKeyboardSendText (via several paths, incl. the
|
// SteamClient.Input.ControllerKeyboardSendText (via several paths, incl. the
|
||||||
// VR text override), which only maps plain ASCII on the base/shift levels;
|
// VR text override), which only maps plain ASCII on the base/shift levels;
|
||||||
// everything else comes out as "1". Hook that one call and hand those
|
// everything else comes out as "1". Hand those characters to the helper, in
|
||||||
// characters to the helper, in order; the rest passes through unchanged.
|
// order; the rest passes through unchanged. (helper.mjs checks the same set.)
|
||||||
if (VKM.prototype.DispatchKeypress.__vrkbdOrig) { // undo the v4 hook
|
|
||||||
VKM.prototype.DispatchKeypress = VKM.prototype.DispatchKeypress.__vrkbdOrig;
|
|
||||||
}
|
|
||||||
const viaHelper = (c) => c.codePointAt(0) > 127 || '|@{[]}\\~^`'.includes(c);
|
const viaHelper = (c) => c.codePointAt(0) > 127 || '|@{[]}\\~^`'.includes(c);
|
||||||
const Input = SteamClient.Input;
|
wrap(SteamClient.Input, 'ControllerKeyboardSendText', (orig) => function (text, ...rest) {
|
||||||
if (Input.ControllerKeyboardSendText.__vrkbd !== VERSION) {
|
if (typeof text !== 'string') return orig.call(this, text, ...rest);
|
||||||
const orig = Input.ControllerKeyboardSendText.__vrkbdOrig || Input.ControllerKeyboardSendText;
|
let run = '';
|
||||||
const patched = function (text, ...rest) {
|
for (const c of text) {
|
||||||
if (typeof text !== 'string') return orig.call(this, text, ...rest);
|
if (viaHelper(c)) {
|
||||||
let run = '';
|
if (run) { orig.call(this, run, ...rest); run = ''; }
|
||||||
for (const c of text) {
|
send('type:' + c);
|
||||||
if (viaHelper(c)) {
|
} else run += c;
|
||||||
if (run) { orig.call(this, run, ...rest); run = ''; }
|
}
|
||||||
send('type:' + c);
|
if (run) return orig.call(this, run, ...rest);
|
||||||
} else run += c;
|
});
|
||||||
}
|
|
||||||
if (run) return orig.call(this, run, ...rest);
|
|
||||||
};
|
|
||||||
patched.__vrkbd = VERSION; patched.__vrkbdOrig = orig;
|
|
||||||
Input.ControllerKeyboardSendText = patched;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- key handling ----------------------------------------------------------
|
// ---- keyboard component -------------------------------------------------------
|
||||||
const kbPopup = [...(g_PopupManager.GetPopups?.() || [])]
|
const kbPopup = [...(g_PopupManager.GetPopups?.() || [])]
|
||||||
.find((p) => p.window?.document.querySelector('[data-key]'));
|
.find((p) => p.window?.document.querySelector('[data-key]'));
|
||||||
if (!kbPopup) return 'no keyboard popup yet';
|
if (!kbPopup) return 'no keyboard popup yet';
|
||||||
// Widen the added half keys and slim AltGr; the space bar is the only
|
|
||||||
// flexible key, so it adjusts. Re-applied on every inject (the popup can be recreated).
|
|
||||||
const doc = kbPopup.window.document;
|
const doc = kbPopup.window.document;
|
||||||
|
const el = doc.querySelector('[data-key]');
|
||||||
|
let fiber = el[Object.keys(el).find((x) => x.startsWith('__reactFiber'))];
|
||||||
|
while (fiber && !fiber.stateNode?.TypeKeyInternal) fiber = fiber.return;
|
||||||
|
const inst = fiber?.stateNode;
|
||||||
|
if (!inst) return 'no keyboard component';
|
||||||
|
let proto = Object.getPrototypeOf(inst);
|
||||||
|
while (!Object.prototype.hasOwnProperty.call(proto, 'TypeKeyInternal')) proto = Object.getPrototypeOf(proto);
|
||||||
|
window.__vrkbdInst = inst; // for the hold timer and unpatch.js
|
||||||
|
window.__vrkbdProto = proto;
|
||||||
|
|
||||||
|
// Stylesheet, re-applied on every inject (the popup can be recreated). The
|
||||||
|
// space bar is the only flexible key, so it absorbs the width changes.
|
||||||
|
const sel = (keys, suffix = '') => keys.map((key) => `[data-key="${key}"]${suffix}`).join(',');
|
||||||
|
const lastRow = Math.max(...[...doc.querySelectorAll('[data-key-row]')].map((e) => +e.getAttribute('data-key-row')));
|
||||||
|
const added = [...MODS, ...ARROWS.map(first), ...ALTGR_ARROWS].map((x) => x.key);
|
||||||
let style = doc.getElementById('vrkbd-style');
|
let style = doc.getElementById('vrkbd-style');
|
||||||
if (!style) { style = doc.createElement('style'); style.id = 'vrkbd-style'; doc.head.appendChild(style); }
|
if (!style) { style = doc.createElement('style'); style.id = 'vrkbd-style'; doc.head.appendChild(style); }
|
||||||
style.textContent = ['VKX_Escape', 'Control', 'Alt', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'ArrowRight',
|
style.textContent = [
|
||||||
'VKX_Home', 'VKX_Prior', 'VKX_Next', 'VKX_End']
|
`${sel(added)} { width: 43px !important; }`,
|
||||||
.map((key) => `[data-key="${key}"]`).join(',') + '{ width: 43px !important; }'
|
'[data-key="AltGr"] { width: 52px !important; }',
|
||||||
+ '[data-key="AltGr"]{ width: 52px !important; }'
|
// Steam draws the AltGr variant as a small secondary label: hide it in the
|
||||||
// Secondary (AltGr) labels in the bottom row, except the arrows' hints.
|
// bottom row, except on the arrows, where it's the Pos1/Ende/Bild hint.
|
||||||
+ `[data-key-row="${Math.max(...[...doc.querySelectorAll('[data-key-row]')].map((e) => +e.getAttribute('data-key-row')))}"]`
|
`[data-key-row="${lastRow}"]:not([data-key^="Arrow"]) span ~ span { display: none !important; }`,
|
||||||
+ ':not([data-key^="Arrow"]) span ~ span { display: none !important; }'
|
// Arrow keys: smaller (still centered) icon and a small hint at the bottom edge.
|
||||||
// Arrow keys: smaller icon, raised, and a smaller Pos1/Ende/Bild hint at
|
'[data-key^="Arrow"] span ~ span { font-size: 7px !important; top: auto !important; bottom: 3px !important; line-height: 1 !important; }',
|
||||||
// the bottom edge, so the two don't overlap on the half-width keys.
|
'[data-key^="Arrow"] span:first-child svg { height: 18px !important; }',
|
||||||
+ '[data-key^="Arrow"] span ~ span { font-size: 7px !important; top: auto !important; bottom: 3px !important; line-height: 1 !important; }'
|
`${sel(ALTGR_ARROWS.map((x) => x.key), ' span')} { font-size: 13px !important; }`,
|
||||||
+ '[data-key^="Arrow"] span:first-child svg { height: 18px !important; transform: translateY(-4px); }'
|
].join('\n');
|
||||||
// AltGr variants of the arrows: labels slightly smaller than the 16px default.
|
|
||||||
+ ['VKX_Home', 'VKX_Prior', 'VKX_Next', 'VKX_End'].map((key) => `[data-key="${key}"] span`).join(',')
|
|
||||||
+ ' { font-size: 13px !important; }';
|
|
||||||
const el = doc.querySelector('[data-key]');
|
|
||||||
let f = el[Object.keys(el).find((x) => x.startsWith('__reactFiber'))];
|
|
||||||
while (f && !(f.stateNode && f.stateNode.TypeKeyInternal)) f = f.return;
|
|
||||||
const inst = f?.stateNode;
|
|
||||||
if (!inst) return 'no keyboard component';
|
|
||||||
window.__vrkbdInst = inst;
|
|
||||||
let proto = Object.getPrototypeOf(inst);
|
|
||||||
while (proto && !Object.prototype.hasOwnProperty.call(proto, 'TypeKeyInternal')) proto = Object.getPrototypeOf(proto);
|
|
||||||
|
|
||||||
|
// ---- key handling ----------------------------------------------------------
|
||||||
const active = (v) => (v & 7) !== 0; // toggle state bits: 1 one-shot, 2 locked
|
const active = (v) => (v & 7) !== 0; // toggle state bits: 1 one-shot, 2 locked
|
||||||
const release = (v) => { const z = v & 3; return (z === 1 ? 0 : z) | (v & 4); };
|
const release = (v) => { const z = v & 3; return (z === 1 ? 0 : z) | (v & 4); };
|
||||||
|
const TOGGLES = ['Shift', 'CapsLock', 'Control', 'Alt', 'AltGr'];
|
||||||
const SYMS = {
|
const SYMS = {
|
||||||
' ': 'space', Backspace: 'BackSpace', Enter: 'Return', Tab: 'Tab',
|
' ': 'space', Backspace: 'BackSpace', Enter: 'Return', Tab: 'Tab',
|
||||||
'.': 'period', ',': 'comma', '-': 'minus', '+': 'plus', '#': 'numbersign', '<': 'less',
|
'.': 'period', ',': 'comma', '-': 'minus', '+': 'plus', '#': 'numbersign', '<': 'less',
|
||||||
'/': 'slash', 'ß': 'ssharp', 'ü': 'udiaeresis', 'ö': 'odiaeresis', 'ä': 'adiaeresis',
|
'/': 'slash', 'ß': 'ssharp', 'ü': 'udiaeresis', 'ö': 'odiaeresis', 'ä': 'adiaeresis',
|
||||||
ArrowLeft: 'Left', ArrowRight: 'Right', ArrowUp: 'Up', ArrowDown: 'Down', // only with Ctrl/Alt; plain arrows stay Steam's
|
ArrowLeft: 'Left', ArrowRight: 'Right', ArrowUp: 'Up', ArrowDown: 'Down',
|
||||||
};
|
};
|
||||||
const keysym = (key) => {
|
const keysym = (key) => {
|
||||||
if (key.startsWith('VKX_')) return key.slice(4);
|
if (key.startsWith('VKX_')) return key.slice(4);
|
||||||
@@ -142,32 +140,19 @@
|
|||||||
if (/^[a-zA-Z0-9]$/.test(key)) return key.toLowerCase();
|
if (/^[a-zA-Z0-9]$/.test(key)) return key.toLowerCase();
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
wrap(proto, 'TypeKeyInternal', (orig) => function (st) {
|
||||||
if (proto.TypeKeyInternal.__vrkbd !== VERSION) {
|
const key = st?.strKey;
|
||||||
const orig = proto.TypeKeyInternal.__vrkbdOrig || proto.TypeKeyInternal;
|
const ts = this.state?.toggleStates || {};
|
||||||
const patched = function (st) {
|
const ctrl = active(ts.Control), alt = active(ts.Alt), shift = active(ts.Shift);
|
||||||
const key = st?.strKey;
|
// Extra keys always; Shift+arrow too (Steam's own arrow path drops Shift).
|
||||||
const ts = this.state?.toggleStates || {};
|
const special = key?.startsWith('VKX_') || (shift && key?.startsWith('Arrow'));
|
||||||
const ctrl = active(ts.Control), alt = active(ts.Alt), shift = active(ts.Shift);
|
const sym = key && !TOGGLES.includes(key) && (special || ctrl || alt) && keysym(key);
|
||||||
// Extra keys always; Shift+arrow too (Steam's own arrow path drops Shift).
|
if (!sym) return orig.call(this, st);
|
||||||
const special = key?.startsWith('VKX_') || (shift && key?.startsWith('Arrow'));
|
send('key:' + [ctrl && 'ctrl', alt && 'alt', shift && 'shift', sym].filter(Boolean).join('+'));
|
||||||
const isToggle = key && ['Shift', 'CapsLock', 'Control', 'Alt', 'AltGr'].includes(key);
|
this.setState((s) => ({ ...s, toggleStates: { ...s.toggleStates,
|
||||||
if (key && !isToggle && (special || ctrl || alt)) {
|
Shift: release(s.toggleStates.Shift), Control: release(s.toggleStates.Control),
|
||||||
const sym = keysym(key);
|
Alt: release(s.toggleStates.Alt), AltGr: release(s.toggleStates.AltGr) } }));
|
||||||
if (sym) {
|
});
|
||||||
send('key:' + [ctrl && 'ctrl', alt && 'alt', shift && 'shift', sym].filter(Boolean).join('+'));
|
|
||||||
this.setState((s) => ({ ...s, toggleStates: { ...s.toggleStates,
|
|
||||||
Shift: release(s.toggleStates.Shift), Control: release(s.toggleStates.Control),
|
|
||||||
Alt: release(s.toggleStates.Alt), AltGr: release(s.toggleStates.AltGr) } }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return orig.call(this, st);
|
|
||||||
};
|
|
||||||
patched.__vrkbd = VERSION; patched.__vrkbdOrig = orig;
|
|
||||||
proto.TypeKeyInternal = patched;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- held modifiers ---------------------------------------------------------
|
// ---- held modifiers ---------------------------------------------------------
|
||||||
// Keep the real Ctrl/Alt pressed while the toggle is active and the keyboard is
|
// Keep the real Ctrl/Alt pressed while the toggle is active and the keyboard is
|
||||||
@@ -175,8 +160,7 @@
|
|||||||
if (!window.__vrkbdHoldTimer) {
|
if (!window.__vrkbdHoldTimer) {
|
||||||
window.__vrkbdHeld = { ctrl: false, alt: false };
|
window.__vrkbdHeld = { ctrl: false, alt: false };
|
||||||
window.__vrkbdHoldTimer = setInterval(() => {
|
window.__vrkbdHoldTimer = setInterval(() => {
|
||||||
const i = window.__vrkbdInst;
|
const ts = window.__vrkbdInst?.state?.toggleStates || {};
|
||||||
const ts = i?.state?.toggleStates || {};
|
|
||||||
const open = !!Status.VRKeyboardStatus?.bIsOpen;
|
const open = !!Status.VRKeyboardStatus?.bIsOpen;
|
||||||
for (const [mod, state] of [['ctrl', ts.Control], ['alt', ts.Alt]]) {
|
for (const [mod, state] of [['ctrl', ts.Control], ['alt', ts.Alt]]) {
|
||||||
const want = open && active(state);
|
const want = open && active(state);
|
||||||
@@ -194,6 +178,5 @@
|
|||||||
inst.setState({ standardLayout: Layouts.r_() });
|
inst.setState({ standardLayout: Layouts.r_() });
|
||||||
inst.forceUpdate();
|
inst.forceUpdate();
|
||||||
}
|
}
|
||||||
window.__vrkbdPatched = VERSION;
|
|
||||||
return 'patched';
|
return 'patched';
|
||||||
})()
|
})()
|
||||||
@@ -2,28 +2,23 @@
|
|||||||
// when it stops (service stopped, module disabled). Every patched function
|
// when it stops (service stopped, module disabled). Every patched function
|
||||||
// keeps its original as __vrkbdOrig. Safe to run when nothing is patched.
|
// keeps its original as __vrkbdOrig. Safe to run when nothing is patched.
|
||||||
(() => {
|
(() => {
|
||||||
const wr = window.__vrkbdWr;
|
if (!window.__vrkbdWr) return 'not patched';
|
||||||
if (!wr) return 'not patched';
|
|
||||||
const unwrap = (obj, name) => {
|
const unwrap = (obj, name) => {
|
||||||
const f = obj?.[name];
|
const f = obj?.[name];
|
||||||
if (f && f.__vrkbdOrig) obj[name] = f.__vrkbdOrig;
|
if (f?.__vrkbdOrig) obj[name] = f.__vrkbdOrig;
|
||||||
};
|
};
|
||||||
const Layouts = wr(40222);
|
for (const l of window.__vrkbdLayouts || []) unwrap(l, 'rgLayout');
|
||||||
for (const l of [...(Layouts.G$() || []), Layouts.r_()]) unwrap(l, 'rgLayout');
|
|
||||||
unwrap(SteamClient.Input, 'ControllerKeyboardSendText');
|
unwrap(SteamClient.Input, 'ControllerKeyboardSendText');
|
||||||
unwrap(wr(5363).PE.prototype, 'DispatchKeypress');
|
unwrap(window.__vrkbdProto, 'TypeKeyInternal');
|
||||||
|
|
||||||
clearInterval(window.__vrkbdHoldTimer);
|
clearInterval(window.__vrkbdHoldTimer);
|
||||||
|
|
||||||
const inst = window.__vrkbdInst;
|
const inst = window.__vrkbdInst;
|
||||||
if (inst) {
|
if (inst) {
|
||||||
let proto = Object.getPrototypeOf(inst);
|
|
||||||
while (proto && !Object.prototype.hasOwnProperty.call(proto, 'TypeKeyInternal')) proto = Object.getPrototypeOf(proto);
|
|
||||||
unwrap(proto, 'TypeKeyInternal');
|
|
||||||
delete inst.__vrkbd;
|
delete inst.__vrkbd;
|
||||||
inst.setState({ standardLayout: Layouts.r_() });
|
inst.setState({ standardLayout: window.__vrkbdWr(40222).r_() });
|
||||||
inst.forceUpdate();
|
inst.forceUpdate();
|
||||||
}
|
}
|
||||||
for (const p of g_PopupManager.GetPopups?.() || []) p.window?.document.getElementById('vrkbd-style')?.remove();
|
for (const p of g_PopupManager.GetPopups?.() || []) p.window?.document.getElementById('vrkbd-style')?.remove();
|
||||||
for (const k of ['__vrkbdHoldTimer', '__vrkbdHeld', '__vrkbdInst', '__vrkbdPatched']) delete window[k];
|
for (const k of ['__vrkbdHoldTimer', '__vrkbdHeld', '__vrkbdInst', '__vrkbdProto', '__vrkbdLayouts']) delete window[k];
|
||||||
return 'unpatched';
|
return 'unpatched';
|
||||||
})()
|
})()
|
||||||
Reference in new issue
Block a user