Every executionContextCreated (Runtime.enable alone reports each existing
context) started its own resync timer; one debounced timer now, as the
injectors do.
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).
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
- 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.
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.
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.
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.
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).
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.
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.
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.
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.
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).
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.
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.
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.
- 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.
- 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.
- 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.
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.
- 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.
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.
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).
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.
- 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.
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.
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.
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).
The icon fallbacks were links an activation script made (and tracked in
~/.local/state/steam-frame-nix/icon-fallbacks) after scanning the desktop
entries. They are Home Manager links now:
~/.local/share/icons/hicolor/scalable/apps/<name>.svg -> the largest Breeze
app SVG, for utilities-terminal and preferences-system (SteamOS' Konsole and
KDE System Settings) plus iconFallbacks.extra; a name Breeze doesn't have
fails the build. enable = false: none.
- The scan is read-only now (steam-frame-icon-fallbacks --suggest): each
switch prints icons Steam can't find that Breeze has (for extra) and
fallbacks hicolor has anyway.
- hicolor's mtime is bumped when the set of links changed between
generations, so a running Steam rescans (GTK checks theme dir mtimes).
- The script's old links at those paths are removed before
checkLinkTargets (they would be collisions); any others and the manifest
go through steam-frame-nix-cleanup.
The assertion for the old list form stays until ~2026-12.
The key used to be merged into steamvr.vrsettings for good (reset at the
next start after disabling, via a marker). Now it is on only while SteamVR
runs and nothing depends on Nix to undo it:
- before each SteamVR start, the oneshot runs `install.sh
steamvr-debugger-arm`: the key's value goes to
~/.local/state/steam-frame-nix/steamvr-debugger.armed ("absent", "false",
"nofile"), the key is set (jq --indent 3, mode kept), and a runtime
drop-in $XDG_RUNTIME_DIR/systemd/user/steamvr.service.d/
50-steam-frame-nix-debugger.conf runs a /usr/bin-only restore script from
$XDG_RUNTIME_DIR/steam-frame-nix on ExecStopPost= (daemon-reload only when
newly written: once per boot). A key that is already true without .armed
is the user's own and never touched. The old marker migrates to .armed
("absent").
- when SteamVR stops, the key goes back to its previous value (removed if
it was absent, the file too if it only existed for the key) and .armed
is removed. .armed is the only trace a power loss can leave; the next
start or steam-frame-nix-cleanup resolves it.
- disabled: no unit and no drop-in at all; the orphan cleanup restores the
key once SteamVR is stopped (or leaves it to the runtime drop-in while it
runs).
A daemon-reload from a oneshot pulled in by a pending start job was tried
on dummy user units: the job survives and the new ExecStopPost= applies,
also on restart (stop, then the oneshot, then start).
checks.cleanup covers arming, re-arming, restoring and the user's own key.
Every module imports cleanup.nix (also exported as
homeManagerModules.cleanup), which
- puts steam-frame-nix-cleanup (install.sh cleanup) on PATH;
- runs `cleanup --orphans --quiet` after linkGeneration on every switch,
keeping what the configuration still uses (steamFrame.cleanup.keep, set
by the modules: debugger while steamvrDebugger is on, the Firefox desktop
profile's user.js while its fix applies); in a dry run it runs with
--dry-run;
- runs `cleanup --all` instead when Home Manager's `uninstall = true;` is
set (the manual uninstall route);
- removes links of older versions at paths Home Manager is about to own
before checkLinkTargets (steamFrame.cleanup.migrateLinks), which would
count as collisions otherwise.
install.sh cleanup gains --quiet (only actions, deferrals and warnings;
the header only when something is printed). The Jellyfin module's own
removal of the old shim copy goes: cleanup does it.
`install.sh cleanup [--dry-run] (--all | --orphans [--keep <artifact>]...)`
knows every file any version wrote outside the store and Home Manager's
links, removes only what is provably its own, prints every action and
reports everything else as "left alone"; a second run changes nothing.
Only bash, coreutils, findutils, grep, sed, awk and jq (all in /usr/bin on
SteamOS), so it also works piped from curl after a rollback or with Nix gone.
- SteamVR debugger: VRWebHelper.DebuggerEnabled is put back to its value
from before (steamvr-debugger.armed; the old empty marker means "absent"
and is migrated), only while SteamVR is stopped. While it runs, a runtime
drop-in (/run/user/<uid>/systemd/user/steamvr.service.d) runs a /usr/bin-only
restore script from /run/user/<uid>/steam-frame-nix when SteamVR stops.
- icon fallback links into Breeze and their manifest (dirs only if they
held nothing else; hicolor's mtime bumped so Steam rescans).
- Firefox: the desktop profile's user.js link, older user.js links and
marker-headed copies, and the values they left in prefs.js (only with the
profile closed; deferred otherwise).
- Jellyfin: the hwdec shim entries in the Flatpak override (devices=all
only when it came with them), an empty override file, the old shim copy.
- the dashboard patches' saved state: --all only; --orphans removes just
stale *.json.tmp files.
`install.sh uninstall` runs `cleanup --all` before removing Home Manager
(replacing its own ui-patches removal) and names what is deferred;
`install.sh status` shows what `cleanup --all` would do. The flake exports
it as packages.<system>.cleanup / apps.cleanup
(`nix run github:lhns/steam-frame-nix#cleanup -- --all`), and
checks.cleanup runs it against fake home and runtime dirs: every artifact
next to look-alikes that aren't ours, --dry-run, second runs, SteamVR
running (deferral, restore script), the user's own debugger key.
SteamOS's steamvr.service runs `rm -rf ~/.cache/SteamVR` on every SteamVR
start, which deletes vrwebhelper's browser profile and with it the
localStorage frame-controls kept its placements in, so they were lost on
every SteamVR restart and reboot.
steam-ui-patches: per-patch persistent state (`state = true`). The injector
keeps ~/.local/state/steam-frame-nix/ui-patches/<name>.json, seeds
window.__sfuiStore in a fresh page before the patch runs and writes the file
(atomically, on change, name-checked, max 64 KiB) when the page calls the
CDP binding __sfuiStoreSave.
frame-controls v5: placements saved there (v4 localStorage copy migrated
once, then removed). steam-close-button v8: "Steam hidden" saved there; a
restored hidden state hides Steam once if SteamVR already shows it when the
patch attaches (or first shows it before the patch sees Steam's frame).
README: "Changes outside Nix" lists the directory; install.sh uninstall
removes it.
prefs and vrFullscreenFix become default prefs (pref()) in
defaults/pref/steam-frame-nix.js of /app/etc/firefox, the mount point of
the org.mozilla.firefox.systemconfig extension point. home-manager
provides the extension as a user "unmaintained extension" link
($XDG_DATA_HOME/flatpak/extension/<id>/aarch64/stable) to a store dir,
which Flatpak mounts itself: the sandbox doesn't see /nix. Default prefs
are never written to prefs.js, so removing one leaves nothing behind.
The desktop profile undoes the fullscreen fix with a user.js linked to
/app/etc/firefox/steam-frame-nix-desktop-user.js. The sync (on switch,
and before each launch) removes the user.js copies and store links of
older versions and takes their values out of prefs.js, for each profile
not in use (Firefox holds .parentlock open).
Jellyfin Desktop hard-sets mpv's hwdec=auto-copy, whose probe list leaves
out V4L2 M2M, and the Flatpak can't see the Frame's V4L2 decoder. An
LD_PRELOAD shim (preloaded straight from the store, only that path exposed
read-only) rewrites hwdec to v4l2m2m-copy,auto-copy; devices=all makes the
decoder visible. Overrides via nix-flatpak, or a home-manager-owned override
file without it.
When the ring shows, frame-controls no longer takes the press over via
cancelPress(); it raises the press's drag threshold to 3x via the new
window.__sfuiWindowCurvature.scalePressDragThreshold(factor) (measured from
the press start). Laser drift keeps the long press, a deliberate drag starts
the curvature drag and cancels it. Completion still calls cancelPress().
Stock calls Frame.SetControlsItems once per control on every re-render
of a window's controls (~12 identical calls per window and scene-graph
update); each replaces three MobX arrays and re-renders the bar. The
wrapper now skips the stock setter when the (re-partitioned) lists equal
the frame's current ones, and reads each action's icon once per call
(protoForSteam is recomputed on every untracked read).
The Flatpak sandbox doesn't see /nix, so the linked user.js dangled and
Firefox never read it (vrFullscreenFix had no effect). The copy starts with a
marker line, so the module still recognises its own file.
Opt-in with steamFrame.keyboard.vr.enable; swipe, autocorrect and
completion suggestions, Backspace drag and haptics are on under it.
- vr-keyboard/patch.js (Steam UI): swipe decoder, a model of what the
keyboard typed, suggestions that only replace what they typed, Backspace
drag with word detents and retyping; checks its Steam internals by
signature (signatures.json: vr-keyboard) and stays stock otherwise.
- vr-keyboard/panel.js (SteamVR systemui) + relay.mjs (user service
vr-keyboard-relay): the suggestion strip as a dashboard panel below or
above the keyboard (signatures: vr-keyboard-panel).
- Dictionary built from nixpkgs' wordfreq and hunspellDicts; default: the
keyboard.layout language (de, fr, es, it, nl, pt, sv) plus English.
- Tests: keyboard.vr.checks and the flake check vr-keyboard.
US, Dvorak, Colemak, Bulgarian, Chinese, Japanese and Korean have no AltGr
key, so AltGr + Delete and the AltGr arrows were unreachable. They get
Steam's own AltGr toggle key, labelled Fn, right of the space bar like
German AltGr.
The Delete key's hint without AltGr is now a third layout entry (empty
key, label Entf), rendered by Steam exactly like its other AltGr hints
(e.g. } on 0) instead of a stylesheet pseudo-element.
The key that becomes Delete on AltGr shows a small hint (the arrow hints'
7 px) on the baseline of Steam's AltGr hints, while the layout has such a
free key.
Steam draws a [normal, shifted] entry as a bare empty key on AltGr and
drops its key type, so the Half-width ^ grew to a full key. Keep the type
with an empty AltGr entry. The AltGr Delete key gets Backspace's key type
(the dark special-key face), the replaced key's flexible width and a
centred label.
While AltGr is active, the key left of Backspace (German ´/`, empty on
AltGr) becomes Delete, labelled like Steam's Delete key (Entf; Del when
longer), sent by the helper; repeats while held like Backspace.
- dashboard.windowMaxScale / windowDistance -> dashboard.windows.maxScale /
distance
- windowCurvature: default -> initial, snapPixels/snapPoints ->
detentPixels/detentPoints, dragThreshold -> dragThresholdPixels (also the
patch's option keys; window-curvature VERSION 19); removed `snap` now
points to detentPixels
- types.numbers.positive/nonnegative instead of number + range assertions
(step <= max, initial/detentPoints <= max and distance min <= max stay)
- frame-controls patch accepts longPressMs from 300, like the option
(VERSION 2)
- "SteamVR dashboard patch" in descriptions, which fit into 80 columns
Old names keep working with a rename warning.
Closing Steam with X now marks it hidden (state steamHidden). Before,
bar-only ended as soon as any window became active, so closing a docked
window afterwards let stock SteamVR fall back to Steam.
While hidden, the stock fallbacks go to the most recently active other
docked window with a tab, else bar-only:
- autoSwitchOverlayIfNeeded with no active frame;
- the dashboard mailbox's dashboard_overlay_destroyed handler (stock:
switchToHomeOverlay -> Steam);
- show/switch requests for Steam with reason "theater frame destroyed"
(key removed) or "SetDockLocation" (echo of X docking Steam: dropped).
Steam becoming the active frame (tab click, Steam menu pick,
SwitchToDashboardOverlay) ends hidden.
The hidden state replaces the rule that swallowed the first
ShowOverlay(main) from Steam after each dashboard open in bar-only (Steam
doesn't send one there; its only effect was eating a real Steam menu
pick). Version 7; state of versions 5/6 is taken over (bar-only implies
hidden), their fields are left for a rollback. Signatures: mailbox and
dashboard handler anchors, the ShowOverlay ones dropped.
The bar panel is no longer padded during a bar drag (its new size and the
compensated origin reached the compositor at different times, so the bar
flickered). SteamVR keeps sending the pressed bar's coordinates past its
edge, so the drag simply uses them. `barDragRoom` is removed.
- Snap points are detents in drag distance (snapPixels, default 24): the
value holds at the point, then continues, so no value is skipped.
`snap` is removed in favour of `snapPixels`.
- The dashboard's own hover haptics are muted during a drag, so only the
value's ticks and detents are felt.
- Longer drag per step by default: 120 px per 1.0 in the menu, 60 on the
bar button (step stays 0.05).
Document the patch calling convention once in lib/default.nix; patch
headers say what, target, hook, state and contracts. Comments only, no
code changes.
Explain the steamvrDebugger default in plain words, and replace the
"mask the port forwards" advice: SteamOS only enables them with Steam's
Developer Mode, which none of the patches need.
- A short contents list; the intro names all features.
- "UI patches" (with finders/signatures and after-a-Steam-update) is its
own section before "Fixes in detail" instead of sitting between the
keyboard patch and the launcher menu; anchors are unchanged.
- The usage example says how it relates to template/ and uses the
template's stateVersion.
- Developer Mode notes (usage, hidden apps) mention launcherMenu.showAllApps;
the launcher section no longer says the patches use no webpack module;
window curvature points to the window control bar for the bar button.
- pinned-desktop (VERSION 3): drop the stop() of a predecessor global that
never existed in this repo; log errors as "sfui <NAME>:" through one
guard helper, like the grid patch.
- steamCloseButton.enable is a mkEnableOption like the other feature
switches; enable descriptions no longer end in a stray newline.
- launcher-menu.nix header lists show-all and no longer claims the patches
use no webpack module; hiddenApps mentions showAllApps.
- Patch comments: frame-controls ring timing matches the code (half of
longPressMs, at most 1 s), window-curvature names frame-controls as what
moves its control into the bar, steam-close-button no longer refers to an
unpublished version.
steamFrame.dashboard.frameControls: a long press (longPressMs, default
1.5 s; a ring shows the progress from half of it) on a control under a
dashboard window or on a More Options (three-dot) menu row opens a popup
with "Show in bar", which moves that control between the window's bottom
bar and the menu, for all windows. Placement is keyed by the control's
action icon, kept in the page and its localStorage (survives SteamVR
restarts); inBar / inMenu set defaults. floatInTheater gives theater
windows the Float control back.
The popup is a scene-graph panel of its own, attached to the pressed
button's (or the menu's) anchor like SteamVR's tooltips and menu, so the
bar and menu panels don't change; the menu's focus auto-close is held back
while its popup is open. Coordinates with window-curvature through its
sfui-curv-* contract. Lookups by signature ("frame-controls" in
lib/signatures.json, check-only anchors for the rest).
Without Steam's Developer Mode the "+" menu hides konsole, systemsettings,
dolphin, plasma-discover, vlc and a few more by executable name. The patch
empties only that list's iterator, leaving the setting itself untouched.
steamFrame.dashboard.windowCurvature: the "Toggle Curvature" row of a
window's More Options menu becomes a control (click: curved -> flat,
flat -> stock; drag up/down with the laser: curvature relative to
SteamVR's stock curve, snapping to 0 and 1). The patch moves each frame's
curvature-origin node to stock distance / value in outgoing scene graphs;
per-window values live in the systemui page. Lookups by signature
("window-curvature" in signatures.json, incl. check-only anchors).
Shared method hooks (lib/hooks.js, passed by mkPatch as 4th argument):
one SendMessage wrapper for dashboard-windows (now VERSION 3) and
window-curvature instead of a chain of per-patch wrappers; legacy
wrappers on top are unwound.
With maxRows capping the scroller, the inner grid panel scrolls (as in
Steam's own design, where the outer element holds the fixed edge shadows).
The fade/shadow state is now read from whichever of the two overflows and
updated on the panel's scroll events too, so the shadows show only where
more content is.
Besides the ScrollFade mask, the scroll region has ::before/::after shadows
toggled by stale can-scroll classes; with everything visible the bottom
shadow still covered the last row. Checker covers the shadow CSS.
steamFrame.dashboard.steamCloseButton.enable (default false) gives the
SteamVR dashboard's main Steam window a close button: it docks the window
back into the dashboard, then switches to the previously active window or,
with none, leaves just the dashboard bar. Bar-only survives dashboard
reopens and patch restarts (state in window.__sfuiSteamCloseState, taken
over from the earlier personal version 4 of this patch); the stock
"no active frame -> show Steam" paths are suppressed while it lasts.
MobX, the dock-location enum and the closing component class are found by
signature; check-only signatures cover the dashboard internals it uses.