Commit Graph
119 Commits
Author SHA1 Message Date
Pierre Kisters 0908713667 VR keyboard: the strip relay resyncs once per burst of new page contexts
Every executionContextCreated (Runtime.enable alone reports each existing
context) started its own resync timer; one debounced timer now, as the
injectors do.
2026-10-01 04:09:13 +02:00
Pierre Kisters 378265389d VR keyboard: unused exports and fields removed
textmodel's lastReset (the patch logs the reason itself), __sfuiSwipe.model,
corrector's distance/fold and the decoder's N/W/cost/resample exports were
read by nothing (patch, tests, replay script).
2026-10-01 04:09:13 +02:00
Pierre Kisters 70e9a1aeb1 VR keyboard: a closed keyboard popup's instance detaches itself
Its 0.5 s poll (a SharedJSContext timer) kept running until the next
injection and could publish a hidden strip with its own sequence numbers
over the new popup's, so a pick on the panel was dropped as stale. The
poll now detaches it once the popup's document is gone.
2026-10-01 04:09:13 +02:00
Pierre Kisters 76de7734c1 VR keyboard extra keys: docs: the helper's debug variables, one place for the other-keymaps note
VRKBD_VERBOSE and VRKBD_DISPLAY were undocumented; the extra keys section
gets a Debugging line like the others. That the routing is harmless on
other keymaps was said twice; now once, under Layouts.
2026-10-01 04:09:13 +02:00
Pierre Kisters c99b0b699f VR keyboard extra keys: the helper reconnects when Steam's page closes while connecting
A CDP call sent after the socket closed (e.g. Steam restarting between
Runtime.enable and the first inject) waited forever, so the helper hung
instead of reconnecting. Such calls now resolve empty at once.
2026-10-01 04:09:13 +02:00
Pierre Kisters 514f073a6e VR keyboard extra keys: the helper holds only Ctrl/Alt, checked in allowlist.mjs
down:/up: were checked with MODKEY[arg], which inherited names like
constructor or toString pass (xdotool then got a junk key name). The check
is now allowedMod() in allowlist.mjs, covered by its test.
2026-10-01 04:09:13 +02:00
Pierre Kisters ad7e4f7678 UI patches: docs line wrapping, mkPatch link text 2026-10-01 04:09:13 +02:00
Pierre Kisters 521092d66b UI patches: no re-injection timer for a session that closed while attaching
The attach handler set the 15 s interval after its awaits even when the
socket had closed meanwhile (onclose already ran), leaking a timer per
such session. Also documents the optional stateDir config key.
2026-10-01 04:09:13 +02:00
Pierre Kisters d6ddad9cfe Window curvature docs: the control bar's factor 3 only in its own docs 2026-10-01 04:09:13 +02:00
Pierre Kisters df501a169b Window curvature: the press contract documented once
- docs/window-curvature.md holds the full contract (now also: the next
  press starts at 1x, return values, a drag keeps its value, neither side
  restores the other's state); the patch headers of window-curvature and
  frame-controls point to it and keep only their own side.
- frame-controls header: the localStorage history points to
  docs/ui-patches.md#persistent-state.
Comments only, no VERSION bumps.
2026-10-01 04:09:13 +02:00
Pierre Kisters c2427b4e20 Window control bar: signature expects without minified names
The Float action's import alias (l.yWq) and the frame controls' local
names (p, v) change with every SteamVR build and would warn after each
update; match the stable parts only.
2026-10-01 04:09:13 +02:00
Pierre Kisters 503a9e742b Jellyfin docs: "approximately" for command (as for the launchers' command), rewrapped lines 2026-10-01 04:09:13 +02:00
Pierre Kisters 11e41874c1 Firefox docs: one parenthesis for the desktop profile, rewrapped lines 2026-10-01 04:09:13 +02:00
Pierre Kisters b08600f9c3 Jellyfin: the shim check runs with set -euo pipefail, like the Firefox one 2026-10-01 04:09:13 +02:00
Pierre Kisters 2e259c49fe Firefox: the desktop profile wrapper keeps prefs.js when rewriting it fails
A failed write of prefs.js.sfn (e.g. a full disk) emptied prefs.js; now
prefs.js and the user.js link stay for the next launch to retry.
2026-10-01 04:09:13 +02:00
Pierre Kisters 4538ecea06 VR "+" menu grid: a popup whose window throws no longer stops the scan
p.window was read outside the guard, so an exception there ended the
1 s scan for the remaining popups (pinned-desktop already guards it).
2026-10-01 04:09:13 +02:00
Pierre Kisters 9cd22f047d Launchers, VR "+" menu: module header comments point to the docs
The patch list, keyring summary, triggers and icon fallback history are
in docs/launchers.md, docs/launcher-menu.md and next to the code they
describe.
2026-10-01 04:09:13 +02:00
Pierre Kisters b0ca57d4bd VR "+" menu icon fallbacks: hints no longer stop at an unreadable entry
A launcher whose Flatpak is not installed leaves a dangling link in
~/.local/share/applications; awk failed on it and set -e ended the
hints there.
2026-10-01 04:09:13 +02:00
Pierre Kisters 667422cb50 Docker: its user service in the runtime names table 2026-10-01 04:09:13 +02:00
Pierre Kisters 1b581a40e2 Session modules: header comments point to the docs instead of repeating them
SteamVR debugger and Docker keep only the internals the docs lack; the
screenshots script names package.nix as what prepends its variables.
2026-10-01 04:09:13 +02:00
Pierre Kisters 464099c1cd Clipboard sync: a process gone since pgrep no longer aborts the switch
ps fails for it, and with pipefail the assignment ended the activation;
it is skipped now.
2026-10-01 04:09:13 +02:00
Pierre Kisters ae8686e284 Session: the outer bus and runtime dir only for the user-service step
steamFrameUserServices exported XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS
into the rest of the activation script; a subshell keeps them to its
systemctl calls.
2026-10-01 04:09:13 +02:00
Pierre Kisters 2442527514 VR keyboard: cleanup of the controller bridge, gesture input and docs
- gesture-input.js: drop the unused mode getter and release().
- relay.mjs: both sides have a binding; no condition.
- hub.js: the frame cadence is documented once (bridge-patch.js); errors
  listed.
- docs: the bridge's idle cost and "one word at a time" stated once;
  twoHanded's README row points to the docs; rewrapped lines.
2026-10-01 04:01:00 +02:00
Pierre Kisters 605e42ffa1 VR keyboard controller bridge: no 11 ms timer while the keyboard is hidden
The TICK_MS loop ran (and returned at once) at ~90 Hz all the time; it now
runs only while SteamVR shows the keyboard, started and stopped by the
250 ms poll. The pose query's 1 s timeout timer is cleared once the query
settles. Frames are unchanged.
2026-10-01 04:01:00 +02:00
Pierre Kisters aa8ccc67c9 VR keyboard F-keys: F1-F12 in the suggestion strip while AltGr is active
keyboard.vr.functionKeys.enable (default off): while AltGr (Fn on layouts
without AltGr) is one-shot, locked or held, the strip shows F1-F12 instead
of the suggestions. A tap types the extra key VKX_F<n>, which the extra
keys' patch presses with xdotool together with the active Ctrl/Alt/Shift
(Alt+F4, Ctrl+F5) and releases the toggles; the text model resets, like
for Esc. The suggestions are kept as they are meanwhile, so when AltGr goes
off the strip shows the same items and selection again.

- vr-keyboard/function-keys.js: what the strip shows, the F-key keys.
- patch.js (VERSION 27): the strip's view, F-key picks, AltGr changes via
  a componentDidUpdate on the keyboard instance (and the poll).
- Allowlist: F1-F12 with any modifiers; Enter still refused.
- relay.mjs: up to 12 strip items.
- Assertions: needs keyboard.vr.enable, extraKeys.enable and a strip
  position "above"/"below" ("inside" would hide the AltGr number row).
- Tests: function-keys.test.mjs (switching and restoring, keys against
  the allowlist, text model), allowlist F-key cases.
2026-10-01 03:53:01 +02:00
Pierre Kisters af32cf72ce VR keyboard swipe with two controllers: the path from the controller bridge
With both lasers on the keyboard SteamVR forwards per poll only one laser's
movement, so the pressing one may get no touchmoves and its swipe became a
tap. keyboard.vr.swipe.twoHanded (default on; turns on the controller
bridge): a press is attributed to the hand whose laser hit is nearest to it
(within 30 px, the trigger as a tiebreak; without recent frames the first
frame after the press decides, within 60 px). The path comes from runs of
one source: the touch's own moves while they flow, that hand's laser hit
per bridge frame once they pause for 60 ms, touchmoves again if frames
pause. A touchend disagreeing with the laser during a bridge run ends at
the laser. Touchmoves on the other hand's laser move the attribution.
Without fresh frames: touch events only, as before. A second press during
a swipe stays Steam's tap (one text field, one word at a time).

The bridge sends full rate only on demand now: hub.js demand(ms) goes
back through the relay (CDP binding __sfuiCtlIn) to systemui's
__sfuiCtl.demand; the swipe patch asks from the press to the release.

Tests: gesture-input with the two-controller recording and synthetic bridge
frames (a pressing hand without touchmoves, attribution with both lasers on
the keyboard, a stale bridge, frames stopping mid-swipe, re-attribution,
interleaved hover and presses); hub demand.
2026-10-01 03:45:47 +02:00
Pierre Kisters 0699d0f532 VR keyboard touch typing: the touched key highlights (and long-presses)
Steam's UpdateTouchState counts the event's `touches` per key: that count
is the pressed highlight (rgLayoutTouchCount -> Touched) and where the long
press (Backspace repeat, accents) starts. The touch-start event left the
new touch out of `touches`, like a touchend; now it is in, as in a DOM
touchstart (tracker.js touchEvent, with a test against a model of Steam's
handlers).
2026-10-01 03:36:13 +02:00
Pierre Kisters 63d589c261 VR keyboard controller bridge: no traffic while idle
The relay no longer enables the Runtime domain on systemui (bindings work
without it; it streamed the page's console and context events). Full-rate
frames only while a tip is within 10 cm of the keyboard or a trigger is
pulled (not for a laser resting on it); idle frames every 1 s instead of
250 ms. Nothing while the keyboard is hidden.
2026-10-01 03:33:07 +02:00
Pierre Kisters 224715d926 VR keyboard touch typing, on a controller bridge from the SteamVR dashboard
keyboard.vr.touchTyping (off by default): a key is pressed when a
controller's tip, where SteamVR's laser starts (/pose/tip: the device pose
times the render model's "tip" component), passes through it from the
front; released when pulled back 1 cm, re-armed 5 mm in front. Both hands,
also at once. Keys go through the keyboard's own HandleTouchStart /
HandleTouchEnd, like a laser press (Backspace repeats while held). Options
depth (cm) and haptics.

The geometry lives in a reusable controller bridge (vr-keyboard-controllers,
internal option): a systemui patch reads the keyboard's pose with SteamVR's
SGQueryService (an empty transform of ours in the keyboard's mount) and both
controllers' poses, and streams per hand the tip, the laser's hit on the
keyboard and the trigger (~90 Hz while relevant) through the
vr-keyboard-controllers-relay service to __sfuiControllers in Steam's
SharedJSContext. Tests: flake checks vr-keyboard-controllers and
vr-keyboard-touch.
2026-10-01 03:30:01 +02:00
Pierre Kisters 7d050b8c44 VR keyboard swipe: only the pressing controller's laser builds the path
With both controllers on the keyboard, the idle one's hover (mouse events,
pointerId 1) arrived during the other's press and went into the swipe path
("tust" for "test"). A gesture now belongs to the contact that started it
(touch identifier, pointerId or mouse; gesture-input.js): other contacts'
moves, releases and cancels are ignored, a second press meanwhile stays
Steam's tap. Test with a recorded two-controller swipe.
2026-10-01 03:12:36 +02:00
Pierre Kisters 26641d76a0 SteamVR screenshots linked into ~/Pictures
Steam keeps VR screenshots (Steam button + trigger) in
~/.local/share/Steam/userdata/<account ID>/760/remote/250820/screenshots,
where Dolphin, Gwenview and the file pickers don't look.

screenshots (module screenshots, opt-in) links ~/Pictures/<name> (default
"SteamVR Screenshots") there. The account ID comes from steamUserId, or,
unset, at runtime: the Home Manager link points to
<runtimeDir>/steam-frame-nix/screenshots (tmpfs), which the oneshot user
service steam-frame-nix-screenshots points to the account last logged in
(loginusers.vdf MostRecent, else latest timestamp, else the only userdata
folder), on switch, at login and when loginusers.vdf or userdata changes.

install.sh cleanup knows the tmpfs link (--keep screenshots while used).
Flake checks: account detection on fake Steam dirs; cleanup of the link.
2026-09-30 22:54:13 +02:00
Pierre Kisters b2babb3859 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).
2026-09-30 22:53:24 +02:00
Pierre Kisters 50e5991c59 Portal: KDE file dialog for Flatpak apps in the Steam session
Neither Valve backend (gamescope, holo) implements FileChooser, so sandboxed
apps fell back to an in-sandbox dialog without the home dir (e.g. Element's
Attachments). session.portalFix.fileChooser (on) links kde.portal into the
Steam session's portal dir and selects kde for FileChooser;
xdg-desktop-portal-kde is D-Bus activated with DISPLAY=:0. Flake check
for the portal config.
2026-09-29 03:20:12 +02:00
Pierre Kisters 356dcb9012 docs: VR keyboard screenshots (extra keys, swipe) 2026-09-29 03:20:04 +02:00
Pierre Kisters 59513bcbb1 Applications menu for KDE apps in the Steam session
SteamOS only ships /etc/xdg/menus/plasma-applications.menu; the nested
desktop sets XDG_MENU_PREFIX=plasma-, the Steam session doesn't, so KDE
apps started there (Dolphin from the "+" menu) built an empty app
database: "no installed application can open" every file.

session.applicationsMenu (module applications-menu, on by default) links
~/.config/menus/applications.menu to Plasma's menu. Flake check: the link
is generated by default and absent when disabled.
2026-09-29 03:01:38 +02:00
Pierre Kisters 0d9a238e5a Launchers: apps' own desktop entries, rewritten; keyring moves into them
steamFrame.launchers.<desktop ID> takes the app's own entry (a Flatpak's
export, a Nix package's or a host file) and rewrites only its command lines
(hostEnv, env, flatpakArgs, args, wrappers) and the keys asked for
(mimeTypes, defaultFor, settings), so name, icon, translations and actions
stay the app's and follow its updates.

- Package entries are rewritten at build time; Flatpak/host file entries at
  runtime by steam-frame-nix-launchers into
  <session.runtimeDir>/steam-frame-nix/applications (tmpfs), on switch, at
  login and when Flatpak installs/updates apps (path unit); the Home Manager
  links point there.
- keyring.{enable,electron} replaces steamFrame.keyring.{flatpaks,programs}
  (clean break: the old options fail with the new form).
- Firefox's launcher is the Flatpak's entry with the profile wrapper in
  front (no --profile for the profile manager); Jellyfin's hardware
  decoding sets flatpakArgs and env.
- cleanup --all removes the tmpfs entries.
- Check on copies of the real Element, KRDC, Firefox, gedit, Jellyfin and
  Claude entries, quoting and generator behaviour.
2026-09-29 02:05:06 +02:00
Pierre Kisters 5d15902ba1 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.
2026-09-29 01:42:34 +02:00
Pierre Kisters b3d93e8526 Trim: firefox launcher, debugger-arm migration, comments and docs
- firefox: the prefs.js patterns built once; the extension link condition
  without the redundant desktopFix (it implies a pref).
- install.sh steamvr-debugger-arm: no second copy of the old-marker
  migration (cleanup, run on every switch before the arm unit exists,
  already does it).
- steam-ui-patches: shorter state option description; lib's description
  mentions extraArgs.
- README: uiPatches.patches fields in the options table (moved from
  docs/ui-patches.md); docs/ui-patches.md: the finder helpers;
  docs/steamvr-debugger.md: repeated sentences removed.
2026-09-29 01:33:16 +02:00
Pierre Kisters a5ecbad76d lib: shared resolvePatch, ensureStyle, logger; legacy wrapper code removed
- finders.js (VERSION 2): resolvePatch (resolveAll or the "signature not
  found, ... left unpatched" status), ensureStyle and logger, used by the
  patches instead of their own copies.
- hooks.js (VERSION 2) no longer unwinds per-patch wrappers of old patch
  versions; dashboard-windows and steam-close-button drop their handling of
  versions <= 2 / <= 6 (in-page only, gone with any SteamVR restart).
- VERSIONs: dashboard-windows 4, steam-close-button 9, window-curvature 21,
  frame-controls 7, steam-keyboard-patch 22, launcher-menu grid 7,
  pinned-desktop 4, show-all 2.
2026-09-29 01:32:41 +02:00
Pierre Kisters 36886d61df Small fixes: quiet service stops, duplicate scheme handlers, script nits
- session.services.stop: stop only units that are active; stopping a unit
  that isn't loaded printed "Failed to stop" on every switch (by default
  for steam-ui-patches, steam-keyboard-patch and vr-keyboard-relay).
- keyring: a scheme in the schemeHandlers of two apps is an assertion
  instead of one app silently winning.
- vr-keyboard-replay.mjs: recorded paths have no time; label them by index.
- webpack-modules.mjs: skip page files that are missing instead of throwing.
2026-09-29 01:32:31 +02:00
Pierre Kisters b942352663 Injectors: no re-injection once the unpatch has started
On SIGTERM the periodic (and debounced) re-injection of injector.mjs and
helper.mjs could still run while or after the unpatch was evaluated, leaving
the page patched after the service was gone (e.g. a feature just turned off).
helper.mjs also gets a timeout on /json/list, like the injector.
2026-09-29 01:32:31 +02:00
Pierre Kisters b0131338de Keyring launchers, Firefox default browser, rootless Docker
- keyring.flatpaks / keyring.programs: desktop entries shadowing an app's
  own that run it on the outer bus (one kwalletd6 for both sessions), with
  the wallet's D-Bus names as `flatpak run --talk-name` options (no Flatpak
  overrides), --password-store=kwallet6 for Electron, and login callback
  schemes as default + recommended handlers.
- firefox.defaultBrowser: the launcher as default for http, https and
  text/html.
- docker: rootless dockerd as a user service, socket in the outer runtime
  dir, CLI with DOCKER_HOST for both sessions.
2026-09-29 01:13:47 +02:00
Pierre Kisters aa9ca183f1 docs: README as overview + options, one docs page per feature
README keeps intro, feature list linking docs/, install, two sessions,
usage, the complete options table, changes outside Nix, rollback,
uninstall. Each feature's details (problem, what you get, configuration,
limitations, how it works) move to its own page in docs/; docs/dashboard.md
is split per feature and docs/changes-outside-nix.md becomes
docs/cleanup.md.
2026-09-29 01:02:28 +02:00
Pierre Kisters 5a3d0b9d05 README: full user documentation again, docs/ only technical
The README is the complete user documentation again: features, install,
two sessions, usage, the full options table (and renamed options), every
feature with problem, usage, limitations, security and "remove when",
UI patches at user level (DevTools on the LAN, after a Steam update),
changes outside Nix, rollback and uninstall.

docs/ keeps only the technical side (how the patches work, writing
patches, signatures and the update procedure, the SteamVR debugger
mechanics, cleanup and installer internals), linked from each feature.
docs/options.md, two-sessions.md and desktop-integration.md are gone
(their content is in the README).
2026-09-29 00:47:16 +02:00
Pierre Kisters 23b79e9499 README: concise overview linking docs/ 2026-09-29 00:31:35 +02:00
Pierre Kisters 13d7a437f8 docs: split feature details into docs/
One page per feature group (sessions, keyboard, launcher menu, dashboard, SteamVR debugger, Firefox, Jellyfin, UI patches, changes outside Nix) and the full options table in docs/options.md.
2026-09-29 00:31:35 +02:00
Pierre Kisters 924ac6b82f README: changes outside Nix, rollback and uninstall
- Intro: no longer claims that everything reverts by activating an older
  generation; points to the exceptions and steam-frame-nix-cleanup.
- "Changes outside Nix (exceptions)": path, feature, lifetime, removed by
  (the SteamVR debugger key while SteamVR runs, its .armed file, the
  runtime drop-in and restore script, the dashboard patches' saved state,
  hicolor's mtime); what steam-frame-nix-cleanup does and what of older
  versions it removes; what exists only while running (UI patches,
  clipboard-sync, the Firefox desktop user.js, Jellyfin's flatpak run
  options); what install.sh sets up; app data that stays yours.
- Rollback: `cleanup --all` (curl or nix run) after rolling back to a
  generation without steam-frame-nix or older than the cleanup.
- Uninstall: what install.sh uninstall does; dropping steam-frame-nix from
  a kept configuration (cleanup --all first, or `uninstall = true;`).
- Per-module notes and options: icon fallbacks, SteamVR debugger, Firefox,
  Jellyfin; template comment for iconFallbacks.
2026-09-29 00:10:11 +02:00
Pierre Kisters 28c285815e Dashboard patch state: kept until cleanup --all; frame-controls v6
The policy for the `state = true` files
(~/.local/state/steam-frame-nix/ui-patches/<name>.json), now stated in the
option, the injector and the README: they are user data, kept when a patch
is disabled or removed (the choices come back when it is enabled again),
and removed only by `steam-frame-nix-cleanup --all` / `install.sh
uninstall`; `--orphans` removes stale *.json.tmp only.

frame-controls v6 drops v5's one-time read of version 4's localStorage copy:
steamvr.service deletes ~/.cache/SteamVR (and with it that localStorage) on
every SteamVR start, so there is nothing left to migrate.
2026-09-29 00:08:43 +02:00
Pierre Kisters 69909f5b4c jellyfin: hardware decoding through a desktop entry, not an override
The shim's permissions (devices=all, its store path, LD_PRELOAD,
SFN_MPV_HWDEC) were a Flatpak override: through nix-flatpak's
services.flatpak.overrides, whose override file outlives the
configuration (it left an empty file behind), or a Home Manager link that
`flatpak override --user` would replace (hence `force`). Now a desktop
entry shadowing the Flatpak's (same ID, same fields and actions; also what
the "+" menu sees) passes them as `flatpak run` options
(--device=all --filesystem=<shim>:ro --env=...), so nothing is written to
Flatpak's overrides and they disappear with the entry.
jellyfin.hardwareDecoding.command is that command line for a terminal.

The entries earlier versions put into the override file are removed by
steam-frame-nix-cleanup. Checked with `flatpak run ... --command=sh`: the
shim's store path is visible read-only, the environment is set and
/dev/video* is there.
2026-09-29 00:08:10 +02:00
Pierre Kisters 1672210ead firefox: desktop profile user.js only while its Firefox runs
The desktop profile's user.js link (undoing the fullscreen fix there) was
kept in place on every switch and before every launch. Now the launcher
(nested desktop only) makes it right before Firefox starts, waits for
Firefox instead of exec'ing it, and once it has exited and the profile is
no longer in use removes the link and the value Firefox stored from it in
prefs.js. A second launch that hands its URL to the running Firefox returns
at once and leaves both alone; a user.js of the user's own is never
touched. The desktop profile itself is a normal Firefox profile (browser
data) and is never removed.

No activation step touches profiles anymore: leftovers of a crash and what
older versions wrote (user.js copies and links, their prefs.js values) are
steam-frame-nix-cleanup's (orphans on switch), which leaves the desktop
link alone while the profile is in use; the firefox-desktop-userjs --keep
is gone. The launcher lives in firefox/launcher.nix; checks.firefox runs it
against a fake flatpak (link only while running, forwarded second launch,
prefs.js cleaned only once unlocked, own user.js untouched, Steam session).
2026-09-29 00:07:26 +02:00