Keyboard patch: stable bottom row, AltGr Pos1/End/PgUp/PgDn, Shift+arrows

- every bottom-row key gets explicit normal/shift/AltGr variants: in Steam's
  stock row the close icon jumps with Shift and keys turn into empty
  full-width panels with AltGr
- AltGr + arrows: Pos1 / End / PgUp / PgDn (shown as small hints)
- Shift + arrows go through the helper as real Shift+arrow (Steam's own
  arrow path drops Shift), so text selection works
This commit is contained in:
Pierre Kisters committed 2026-09-27 15:37:03 +02:00
1 parent 5e7d631e50
commit bd341f45c2
2 files changed
+34 -13

No files matched your search

+4 -3
View File
@@ -16,12 +16,13 @@ const ENV = { ...process.env, DISPLAY: process.env.VRKBD_DISPLAY || ':0', LC_ALL
// 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
// to type ASCII text or press Enter on its behalf. Accepted: // to type ASCII text or press Enter on its behalf. Accepted:
// key:<combo> the extra keys (Esc, Del, Home, End, arrows), optionally with // key:<combo> the extra keys (Esc, Del, Home, End, PgUp/PgDn, arrows),
// modifiers; or ctrl and/or alt (+ optional shift) with one key // 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 // type:<char> one character Steam's key emulation can't produce: non-ASCII
// (äöü߀§°´…) or | @ { [ ] } \ ~ ^ ` // (äöü߀§°´…) or | @ { [ ] } \ ~ ^ `
// down:<mod> / up:<mod> hold/release ctrl or alt // down:<mod> / up:<mod> hold/release ctrl or alt
const SPECIAL = new Set(['Escape', 'Delete', 'Home', 'End', 'Left', 'Right', 'Up', 'Down']); 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 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' }; const MODKEY = { ctrl: 'Control_L', alt: 'Alt_L' };
function allowedCombo(combo) { function allowedCombo(combo) {
+30 -10
View File
@@ -12,7 +12,7 @@
// executed by helper.mjs with xdotool on :0. // executed by helper.mjs with xdotool on :0.
// Idempotent: safe to evaluate repeatedly. // Idempotent: safe to evaluate repeatedly.
(() => { (() => {
const VERSION = 5; const VERSION = 7;
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);
@@ -25,18 +25,33 @@
// 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. // 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
// 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
// Pos1/Ende/PgUp/PgDn (Shift+arrows stay arrows, for selecting).
const HALF = 2; const HALF = 2;
const k = (key, label) => ({ key, label, type: HALF }); const k = (key, label) => ({ key, label, type: HALF });
const MODS = [k('VKX_Escape', 'Esc'), k('Control', 'Ctrl'), k('Alt', 'Alt')]; // null as the shifted variant = "same key, no shift label". Steam still draws
const ARROWS = [Layouts.Md, Layouts.GO, Layouts.xl, Layouts.B6] // Steam's ArrowLeft/Up/Down/Right // the AltGr variant as a small secondary label; the stylesheet hides that in
.map((a) => ({ ...a, type: HALF })); // the bottom row (the arrows keep theirs as a Pos1/Ende/Bild hint).
const same = (x) => [x, null, x];
const MODS = [k('VKX_Escape', 'Esc'), k('Control', 'Ctrl'), k('Alt', 'Alt')].map(same);
const half = (a) => ({ ...a, type: HALF });
const ARROWS = [ // Steam's ArrowLeft/Up/Down/Right keys
[Layouts.Md, k('VKX_Home', 'Pos1')],
[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 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 && typeof y.key === 'string' && y.key.startsWith('Arrow'));
const bottomRow = (row) => { const bottomRow = (row) => {
const out = []; const out = [];
for (const x of row) { for (const x of row) {
if (x === undefined || isArrow(x)) continue; const key = first(x);
if (x && x.key === ' ') out.push(...MODS); if (!key || isArrow(x)) continue;
out.push(x); if (key.key === ' ') out.push(...MODS);
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;
@@ -90,9 +105,13 @@
const doc = kbPopup.window.document; const doc = kbPopup.window.document;
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_Escape', 'Control', 'Alt', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'ArrowRight',
'VKX_Home', 'VKX_Prior', 'VKX_Next', 'VKX_End']
.map((key) => `[data-key="${key}"]`).join(',') + '{ width: 43px !important; }' .map((key) => `[data-key="${key}"]`).join(',') + '{ width: 43px !important; }'
+ '[data-key="AltGr"]{ width: 52px !important; }'; + '[data-key="AltGr"]{ width: 52px !important; }'
// Secondary (AltGr) labels in the bottom row, except the arrows' hints.
+ `[data-key-row="${Math.max(...[...doc.querySelectorAll('[data-key-row]')].map((e) => +e.getAttribute('data-key-row')))}"]`
+ ':not([data-key^="Arrow"]) span ~ span { display: none !important; }';
const el = doc.querySelector('[data-key]'); const el = doc.querySelector('[data-key]');
let f = el[Object.keys(el).find((x) => x.startsWith('__reactFiber'))]; let f = el[Object.keys(el).find((x) => x.startsWith('__reactFiber'))];
while (f && !(f.stateNode && f.stateNode.TypeKeyInternal)) f = f.return; while (f && !(f.stateNode && f.stateNode.TypeKeyInternal)) f = f.return;
@@ -123,7 +142,8 @@
const key = st?.strKey; const key = st?.strKey;
const ts = this.state?.toggleStates || {}; const ts = this.state?.toggleStates || {};
const ctrl = active(ts.Control), alt = active(ts.Alt), shift = active(ts.Shift); const ctrl = active(ts.Control), alt = active(ts.Alt), shift = active(ts.Shift);
const special = key?.startsWith('VKX_'); // Extra keys always; Shift+arrow too (Steam's own arrow path drops Shift).
const special = key?.startsWith('VKX_') || (shift && key?.startsWith('Arrow'));
const isToggle = key && ['Shift', 'CapsLock', 'Control', 'Alt', 'AltGr'].includes(key); const isToggle = key && ['Shift', 'CapsLock', 'Control', 'Alt', 'AltGr'].includes(key);
if (key && !isToggle && (special || ctrl || alt)) { if (key && !isToggle && (special || ctrl || alt)) {
const sym = keysym(key); const sym = keysym(key);