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.
This commit is contained in:
Pierre Kisters committed 2026-09-29 01:02:28 +02:00
1 parent 5a3d0b9d05
commit aa9ca183f1
16 files changed
+948 -922

No files matched your search

-67
View File
@@ -1,67 +0,0 @@
# Cleanup and installer: how they work
What steam-frame-nix writes outside the Nix store, how to remove it, and
rollback/uninstall: README,
[Changes outside Nix](../README.md#changes-outside-nix-exceptions),
[Rollback](../README.md#rollback), [Uninstall](../README.md#uninstall).
## Cleanup
`steam-frame-nix-cleanup` (`steamFrame.cleanup.package`, module `cleanup`,
imported by every module) is `install.sh cleanup`, so the same code runs
with Nix (on every switch: `cleanup --orphans --keep <what the configuration
still uses>`) and without it, from the script (bash, coreutils, findutils,
jq, all in SteamOS' `/usr/bin`). Home Manager's `uninstall = true;` runs
`cleanup --all` from the activation.
It knows everything any version wrote outside the store and removes an
artifact only when it is proven to be its own; anything else is reported as
"left alone" and never touched:
| Artifact | Proof / rule |
|---|---|
| `debugger`: `VRWebHelper.DebuggerEnabled` in `steamvr.vrsettings` | `~/.local/state/steam-frame-nix/steamvr-debugger.armed` holds the value before (older versions: the empty marker `steamvr-debugger`, value before = absent). Restored once SteamVR is stopped; while it runs the runtime drop-in restores it when SteamVR stops. Also the runtime drop-in and restore script themselves. |
| `icons`: `hicolor/scalable/apps/<name>.svg` | links to Breeze in the store, listed in `~/.local/state/steam-frame-nix/icon-fallbacks` (the icon-fallbacks script of 2026-09) |
| `firefox`: `user.js` in Firefox profiles | links to `/app/etc/firefox/steam-frame-nix-desktop-user.js` (left alone while the profile is in use), older links to `*-firefox-*user.js` and copies starting with the steam-frame-nix marker comment, and the values they left in `prefs.js` (only with Firefox closed) |
| `jellyfin` | the hwdec shim entries in the Jellyfin Flatpak's user override `~/.local/share/flatpak/overrides/org.jellyfin.JellyfinDesktop` (nix-flatpak), an empty override file, and the shim copy of earlier versions in `~/.var/app/org.jellyfin.JellyfinDesktop` (marker `~/.local/state/steam-frame-nix/jellyfin-hwdec-shim`) |
| `ui-state`: `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | the dashboard patches' saved choices; `--all` only, never `--orphans`; stray `*.json.tmp` files |
| `dirs` | `~/.local/state/steam-frame-nix` and `/run/user/1000/steam-frame-nix` when empty |
### Left by older versions
The rows above include what only older versions wrote: the icon fallback
links and their list (`~/.local/state/steam-frame-nix/icon-fallbacks`), the
debugger marker `steamvr-debugger`, Firefox `user.js` copies and links and
their `prefs.js` values, the Jellyfin hwdec entries of
`~/.local/share/flatpak/overrides/org.jellyfin.JellyfinDesktop` (an empty
override file too) and the old shim copy in
`~/.var/app/org.jellyfin.JellyfinDesktop`.
## Set up by install.sh
`install.sh install` (the bootstrap, not the modules) changes more, and
`install.sh uninstall` undoes it:
- Nix via [nix-installer](https://github.com/NixOS/nix-installer)
(`steam-deck` planner, flakes on): `/nix` (bind mount of `/home/nix`,
survives SteamOS updates), files in `/etc` (systemd units, profile
scripts, `nix.conf`) and its receipt `/nix/receipt.json`; the read-only
root is unlocked only while it installs or uninstalls. Skipped if Nix
already works;
- `experimental-features = nix-command flakes` in `~/.config/nix/nix.conf`
if Nix was already there without flakes;
- `~/nix-config` (your configuration, a git repository, from the template
with your user name filled into `flake.nix`) and the link
`~/.config/home-manager` to it, unless `~/.config/home-manager` exists or
`--flake <dir-or-flakeref>` is given; uninstall removes the link, never
the configuration;
- dotfiles Home Manager found in its way, renamed to `*.hm-backup-<time>`
(kept by uninstall);
- `~/.local/state/home-manager` and `~/.local/state/nix` (profiles,
generations), `~/.nix-profile`, `~/.nix-defexpr`, `~/.nix-channels`,
`~/.cache/nix`.
The installer runs `systemctl --user` against the outer session's user
manager (the nested desktop can't reach it with its own environment) and
uses the installed `home-manager` if there is one, else Home Manager's
`master`.
+43
View File
@@ -0,0 +1,43 @@
# Cleanup and installer
How `steam-frame-nix-cleanup` and `install.sh` work. What steam-frame-nix
writes outside the Nix store, how to remove it, rollback and uninstall:
[README, Changes outside Nix](../README.md#changes-outside-nix-exceptions),
[Rollback](../README.md#rollback), [Uninstall](../README.md#uninstall).
## Cleanup
`steam-frame-nix-cleanup` (`steamFrame.cleanup.package`, module `cleanup`,
imported by every module) is `install.sh cleanup`, so the same code runs
with Nix (on every switch: `cleanup --orphans --keep <what the configuration
still uses>`) and without it, from the script (bash, coreutils, findutils,
jq, all in SteamOS' `/usr/bin`). Home Manager's `uninstall = true;` runs
`cleanup --all` from the activation. `--quiet` prints only actions,
deferrals and warnings.
It removes an artifact only when it is proven to be its own; anything else
is reported as "left alone" and never touched:
| Artifact | Proof / rule |
|---|---|
| `debugger`: `VRWebHelper.DebuggerEnabled` in `steamvr.vrsettings` | `~/.local/state/steam-frame-nix/steamvr-debugger.armed` holds the value before (older versions: the empty marker `steamvr-debugger`, value before = absent). Restored once SteamVR is stopped; while it runs the runtime drop-in restores it when SteamVR stops. Also the runtime drop-in and restore script themselves. |
| `icons`: `hicolor/scalable/apps/<name>.svg` | links to Breeze in the store, listed in `~/.local/state/steam-frame-nix/icon-fallbacks` (the icon-fallbacks script of 2026-09) |
| `firefox`: `user.js` in Firefox profiles | links to `/app/etc/firefox/steam-frame-nix-desktop-user.js` (left alone while the profile is in use), older links to `*-firefox-*user.js` and copies starting with the steam-frame-nix marker comment, and the values they left in `prefs.js` (only with Firefox closed) |
| `jellyfin` | the hwdec shim entries in the Jellyfin Flatpak's user override `~/.local/share/flatpak/overrides/org.jellyfin.JellyfinDesktop` (nix-flatpak), an empty override file, and the shim copy of earlier versions in `~/.var/app/org.jellyfin.JellyfinDesktop` (marker `~/.local/state/steam-frame-nix/jellyfin-hwdec-shim`) |
| `ui-state`: `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | the dashboard patches' saved choices; `--all` only, never `--orphans`; stray `*.json.tmp` files |
| `dirs` | `~/.local/state/steam-frame-nix` and `/run/user/1000/steam-frame-nix` when empty |
### Left by older versions
Of the rows above, only older versions wrote: the icon fallback links and
their list, the debugger marker `steamvr-debugger`, Firefox `user.js` copies
and links and their `prefs.js` values, the Jellyfin hwdec override entries
(and an empty override file) and the old shim copy.
## Installer
What `install.sh install` sets up is listed in the README under
[Set up by install.sh](../README.md#set-up-by-installsh). The installer runs
`systemctl --user` against the outer session's user manager (the nested
desktop can't reach it with its own environment) and uses the installed
`home-manager` if there is one, else Home Manager's `master`.
+61
View File
@@ -0,0 +1,61 @@
# Dashboard windows
`dashboard.windows.*`, module `dashboard-windows` ([options](../README.md#options)).
A [SteamVR dashboard patch](ui-patches.md#steamvr-dashboard-patches).
## Problem
SteamVR dashboard windows can only be enlarged to 2x, and grabbed windows
pushed back only to 5 m (6 m in theater), too close for a big screen.
## What you get
Higher limits (`null` keeps stock):
| Option | Stock |
|---|---|
| `maxScale` | 2 (relative to the window's default size; the theater screen's default is 2.8x larger) |
| `distance.world.{min,max}` | 0.25-5 m |
| `distance.theater.{min,max}` | 1-6 m |
| `distance.dashboard.{min,max}` | 0.3-4 m |
Distances limit pulling in / pushing back a grabbed window (thumbstick or
scroll while dragging). Changes apply immediately, and turning options off
reverts on the next switch. The keyboard's range is not patched.
## Configuration
```nix
steamFrame.dashboard.windows = {
maxScale = 4.0; # resize up to 4x (theater: 11.2x)
distance.world.max = 10.0; # push windows back up to 10 m
distance.theater.max = 12.0;
};
```
## Caveats
If SteamVR changes its stock grab distances, the distance options silently
do nothing.
## How it works
`systemui` sends its scene graph to vrcompositor via
`mailbox.SendMessage("vrcompositor_systemlayer", { type: "update_scene_graph", scene_graph })`,
and vrcompositor enforces the limits in it:
- `frame-resize-scale-min` / `-max` on window frames;
- `min-distance` / `max-distance` on grab nodes: `grab-scale` for world
windows, `grab-transform` for the theater screen and the dashboard (the
keyboard's `grab-transform`, 0.2 / 1 m, is left alone).
They are literals in the bundle, so the patch hooks `SendMessage` on the
mailbox prototype ([shared hooks](ui-patches.md#shared-method-hooks), shared
with [window curvature](window-curvature.md)) and edits outgoing scene graphs
in place. Grab nodes are matched by type plus exact stock values, which is
why a change of them makes the distance rewrite a no-op. A scene-graph
resend applies new limits at once.
State: `window.__sfuiDashboardWindows` in the `systemui` page (counters in
`.hits`).
-105
View File
@@ -1,105 +0,0 @@
# SteamVR dashboard patches: how they work
Technical details of the four dashboard patches. What they do and how to
configure them: README, [dashboard windows](../README.md#dashboard-windows-dashboardwindows),
[Steam close button](../README.md#steam-close-button-dashboardsteamclosebuttonenable),
[window curvature](../README.md#window-curvature-dashboardwindowcurvature),
[window control bar](../README.md#window-control-bar-dashboardframecontrols).
All four are `mkPatch` [UI patches](ui-patches.md) of SteamVR's dashboard
(`vrwebhelper`, page title `systemui`, DevTools `127.0.0.1:8087`, webpack
chunk `webpackChunkvrwebui`), registered only when enabled; they turn on the
[SteamVR debugger](steamvr-debugger.md). Each is found by signature (its
entry in `modules/lib/signatures.json` has the patch's name); on mismatch
the dashboard stays stock. Sources: `modules/<name>/patch.js`, whose header
comments go into more detail.
## Dashboard windows
`dashboard-windows`. `systemui` sends its scene graph to vrcompositor via
`mailbox.SendMessage("vrcompositor_systemlayer", { type: "update_scene_graph", scene_graph })`,
and vrcompositor enforces the limits in it:
- `frame-resize-scale-min` / `-max` on window frames;
- `min-distance` / `max-distance` on grab nodes: `grab-scale` for world
windows, `grab-transform` for the theater screen and the dashboard (the
keyboard's `grab-transform`, 0.2 / 1 m, is left alone).
They are literals in the bundle, so the patch hooks `SendMessage` on the
mailbox prototype ([shared hooks](ui-patches.md#shared-method-hooks), shared
with window curvature) and edits outgoing scene graphs in place. Grab nodes
are matched by type plus exact stock values, so if SteamVR changes them the
distance rewrite becomes a no-op. A scene-graph resend applies new limits at
once.
State: `window.__sfuiDashboardWindows` (counters in `.hits`).
## Steam close button
`steam-close-button`, with [persistent state](ui-patches.md#persistent-state)
(`{ schema: 1, steamHidden }`).
- **Button:** the frame's `closing` component shows X when
`componentProps.onCloseRequested` exists; the Steam window's instance
(overlay `valve.steam.gamepadui.main`) gets one and its props are
re-assigned so the MobX computed re-evaluates.
- **Staying hidden:** while hidden, stock fallbacks to Steam go to the
previous window or bar-only instead: `Dashboard.autoSwitchOverlayIfNeeded`
(instance override) and the mailbox handler `dashboard_overlay_destroyed`.
Mailbox show/switch requests for Steam with reason `SetDockLocation` (the
echo of X docking Steam) are dropped, "theater frame destroyed" ones are
passed on without the Steam key. Explicit requests (tab click, Steam menu
pick, `SwitchToDashboardOverlay`) show Steam again.
- **Restarts:** a restored hidden state stays pending (`restorePending`)
until Steam's frame is seen, since the injector attaches within ~5 s of
the page appearing.
Debugging: `window.__sfuiSteamClose.plan()` / `homePlan()`; state in
`window.__sfuiSteamCloseState` (survives re-injection; teardown restores all
overrides and never switches frames).
## Window curvature
`window-curvature`. Stock curvature: the frame renders a transform
`frame:<id>:curvature-origin` at `z = DashboardStore.curvatureDistance` when
curved, else 1000; its panels reference it as `curvature-origin-id` and
vrcompositor bends them onto a cylinder around it (curvature = 1/radius).
Those MobX properties are non-configurable, so the patch hooks the mailbox
`SendMessage` (shared with dashboard windows) and rewrites the origin's z
in outgoing scene graphs to stock / value. On/off stays the stock toggle
state, so stock toggle and wheel always agree.
Values are kept per window (key: first overlay key) in
`window.__sfuiWindowCurvatureState`, across re-patch and unpatch, not across
a dashboard reload. The bar button uses the coordinates SteamVR keeps
sending past the pressed panel's edge while the trigger is held; the bar
panel is never resized.
Debugging: `window.__sfuiWindowCurvature.dump()` (`.log` recent events).
### Contract for other patches
For patches handling presses on the curvature controls (e.g. the window
control bar's long press): every element the patch drives has class
`sfui-curv-ctl`; when a press becomes a drag, a bubbling `CustomEvent`
`sfui-curv-dragstart` (detail `{ frameID, where: 'menu' | 'bar' }`) is
dispatched on it, and `sfui-curv-dragend` when it ends;
`window.__sfuiWindowCurvature.scalePressDragThreshold(factor)` sets the
current press's drag threshold to `factor` × `dragThresholdPixels` (from the
press start; returns whether it applied, i.e. a press that is not yet a
drag); `window.__sfuiWindowCurvature.cancelPress()` ends a press without its
click. A patch with its own gesture lets `mousemove` through while
undecided, drops its gesture on `sfui-curv-dragstart`, may raise the
threshold while its gesture is under way, and calls `cancelPress()` when it
takes the press over. The window control bar uses factor 3 once its
progress ring shows.
## Window control bar
`frame-controls`, with [persistent state](ui-patches.md#persistent-state)
(placements). Short presses stay stock; the three-dot button can't be
moved. Works with window curvature through the
[contract](#contract-for-other-patches) above.
Debugging: `window.__sfuiFrameControls.dump()`, `.placement()`, `.reset()`
(forget choices), `.log`, `window.__sfuiFrameControlsState`.
+68 -23
View File
@@ -1,36 +1,81 @@
# Firefox: how it works
# Firefox
`firefox.*` (module `firefox`). What it does and how to configure it:
README, [Firefox](../README.md#firefox-firefox).
`firefox.*`, module `firefox`. Options:
[README, Options](../README.md#options).
## Default prefs
## Problem
`prefs` and the fixes are *default* values (`pref()`), not user values:
`about:config` can still change them per profile, and nothing is written to
`prefs.js`, so removing one leaves nothing behind. Firefox reads default
prefs from `defaults/pref/*.js` in its system config dir, which in the
Flatpak is `/app/etc/firefox`, the mount point of the
In the Steam session gamescope never shows fullscreen windows, so Firefox
looks frozen when a page goes fullscreen; sites send AV1, which the Frame
decodes in software; and the two sessions can't see each other's Firefox,
so a second instance stops at the locked profile.
## What you get
A launcher for the Flathub Firefox Flatpak (`org.mozilla.firefox`, stable;
install it yourself). The launcher shadows the Flatpak's own entry (same
ID), so default-browser associations keep working.
- **`vrFullscreenFix`** (on): `full-screen-api.ignore-widgets` makes
fullscreen fill just the window. Not applied in the desktop profile.
- **`disableAv1`** (off): `media.av1.enabled = false`. The Frame's decoder
driver (`iris`) has no AV1, only H.264, HEVC and VP9, so YouTube and co.
send VP9/H.264, decoded in hardware, instead of software AV1.
- **`prefs`:** further `about:config` values for every profile; they can
also override the fixes above.
- **`desktopProfile`** (`"desktop"`): in the nested desktop the launcher
uses this separate profile (a normal Firefox profile with its own browser
data, created on first use). `null`: the default profile in both sessions.
`prefs` and the fixes are *default* values, not user values: `about:config`
can still change them per profile, and removing one leaves nothing behind.
Changes take effect at the next start of Firefox.
## Configuration
```nix
steamFrame.firefox = {
enable = true;
disableAv1 = true;
prefs."browser.startup.page" = 3; # restore the previous session
};
```
## Caveats
- A `user.js` of your own in the desktop profile is never touched (the
fullscreen fix then stays on there).
- A `org.mozilla.firefox.systemconfig` Flatpak extension of your own would
conflict with the one this module provides.
- **Remove** `vrFullscreenFix` **when** gamescope shows fullscreen X11
windows in VR, `disableAv1` **when** a SteamOS kernel adds AV1 to `iris`.
## How it works
### Default prefs
The prefs are written with `pref()`, and nothing goes to `prefs.js`.
Firefox reads default prefs from `defaults/pref/*.js` in its system config
dir, which in the Flatpak is `/app/etc/firefox`, the mount point of the
`org.mozilla.firefox.systemconfig` extension. home-manager provides that
extension as a link from
`~/.local/share/flatpak/extension/org.mozilla.firefox.systemconfig/aarch64/stable`
to a store directory; Flatpak mounts it itself, so the sandbox doesn't get
`/nix`. (A systemconfig extension of your own would conflict with it.)
`/nix`.
## Desktop profile
### Desktop profile
The launcher (a desktop entry with the Flatpak's ID, shadowing its entry)
picks `desktopProfile` when started in the nested desktop. The desktop
profile undoes the fullscreen fix only while its Firefox runs: the launcher
links the profile's `user.js` to
`/app/etc/firefox/steam-frame-nix-desktop-user.js` (a sandbox path) right
before starting Firefox, waits for 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 just hands a URL to the running
Firefox leaves both in place. A `user.js` of your own is never touched (the
fix then stays on in that profile). After a crash, the next launch or
`steam-frame-nix-cleanup` (on switch) removes them.
The launcher (a desktop entry with the Flatpak's ID) picks `desktopProfile`
when started in the nested desktop. The desktop profile undoes the
fullscreen fix only while its Firefox runs: the launcher links the
profile's `user.js` to `/app/etc/firefox/steam-frame-nix-desktop-user.js`
(a sandbox path) right before starting Firefox, waits for 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 just hands
a URL to the running Firefox leaves both in place. After a crash, the next
launch or `steam-frame-nix-cleanup` (on switch) removes them.
## Older versions
### Older versions
Older versions linked or copied a `user.js` into every profile; those and
the values they left in `prefs.js` are removed by `steam-frame-nix-cleanup`
+56 -20
View File
@@ -1,13 +1,15 @@
# Jellyfin hardware decoding: how it works
# Jellyfin hardware decoding
`jellyfin.hardwareDecoding.*` (module `jellyfin`). What it does, how to
configure it and its caveats: README,
[Jellyfin hardware decoding](../README.md#jellyfin-hardware-decoding-jellyfinhardwaredecoding).
`jellyfin.hardwareDecoding.*`, module `jellyfin`, for the Flathub
[Jellyfin Desktop](https://github.com/jellyfin/jellyfin-desktop) Flatpak
(`org.jellyfin.JellyfinDesktop`), which plays video with libmpv. Options:
[README, Options](../README.md#options).
## Why mpv doesn't use the decoder
## Problem
The Frame's hardware decoder is a V4L2 memory-to-memory device
(`qcom-iris`, `/dev/video*`), which
Video is decoded in software (1080p H.264: ~40 % CPU). The Frame's hardware
decoder is a V4L2 memory-to-memory device (`qcom-iris`, `/dev/video*`),
which
- the Flatpak can't open: its `devices=dri` covers only the GPU, and Flatpak
has nothing between that and `devices=all`;
@@ -15,23 +17,58 @@ The Frame's hardware decoder is a V4L2 memory-to-memory device
probing leaves out V4L2 M2M on purpose (its quality varies by SoC).
Jellyfin has no way to pass mpv options.
## The shim
## What you get
The Jellyfin desktop entry (same ID as the Flatpak's, so the KDE menu and
the "+" menu start it) runs the Flatpak with device access (`devices=all`)
and makes mpv use `hwdec` (default `v4l2m2m-copy,auto-copy`); explicit
values such as `no` stay. mpv tries the listed decoders in order and falls
back to software decoding per stream. With the default, 1080p H.264 plays
at ~15-20 % CPU. Changes take effect at the next start of Jellyfin.
## Configuration
Installing the Flatpak is up to you, e.g.
`flatpak install --user flathub org.jellyfin.JellyfinDesktop`, or with
nix-flatpak:
```nix
services.flatpak.packages = [ "org.jellyfin.JellyfinDesktop" ];
steamFrame.jellyfin.hardwareDecoding.enable = true;
```
From a terminal, start it with the command line of
`steamFrame.jellyfin.hardwareDecoding.command` (or `grep ^Exec=
~/.local/share/applications/org.jellyfin.JellyfinDesktop.desktop`); its
output shows `mpv-hwdec-shim: hwdec "auto-copy" -> "v4l2m2m-copy,auto-copy"`,
then mpv's `Using hardware decoding (v4l2m2m-copy)`.
## Caveats
- `devices=all` gives the app all of `/dev` (cameras, input devices, ...),
not just the decoder.
- V4L2 M2M decoding quality varies with drivers and codecs. Tested: 8-bit
H.264; 10-bit HEVC is untested. mpv falls back to software only when the
decoder fails; for streams that decode with artifacts, disable the option
(or set `hwdec = "auto-copy"`, Jellyfin's own value).
## How it works
### The shim
An `LD_PRELOAD` shim (`modules/jellyfin/mpv-hwdec-shim.c`, a few libmpv
wrappers, only libc) rewrites an `hwdec` value starting with `auto` (set
through libmpv's `mpv_set_*` functions) to `$SFN_MPV_HWDEC` (the `hwdec`
option); explicit values such as `no` stay. It is preloaded from the Nix
store; only its store path is exposed (read-only) to the sandbox. It would
work for any libmpv app that sets `hwdec=auto*`, but only Jellyfin Desktop
is set up here.
option). It is preloaded from the Nix store; only its store path is exposed
(read-only) to the sandbox. It would work for any libmpv app that sets
`hwdec=auto*`, but only Jellyfin Desktop is set up here.
## The desktop entry
### The desktop entry
Nothing is written to Flatpak's overrides: a desktop entry shadowing the
Flatpak's (`~/.local/share/applications/org.jellyfin.JellyfinDesktop.desktop`,
same ID, so the KDE menu and the "+" menu start it) passes device access and
the shim as `flatpak run` options, so they apply to launches from that entry
and are gone with it:
Nothing is written to Flatpak's overrides: the entry
(`~/.local/share/applications/org.jellyfin.JellyfinDesktop.desktop`) passes
device access and the shim as `flatpak run` options, so they apply to
launches from that entry and are gone with it:
```sh
flatpak run --branch=stable --arch=aarch64 --command=jellyfin-desktop \
@@ -40,8 +77,7 @@ flatpak run --branch=stable --arch=aarch64 --command=jellyfin-desktop \
--env=SFN_MPV_HWDEC=v4l2m2m-copy,auto-copy org.jellyfin.JellyfinDesktop
```
(The exact line with the store path is the read-only option
`steamFrame.jellyfin.hardwareDecoding.command`.)
(`command` is this line with the store path.)
Older versions used a Flatpak override (via nix-flatpak or a Home Manager
link) and a shim copy in `~/.var/app/org.jellyfin.JellyfinDesktop`;
+122 -25
View File
@@ -1,52 +1,149 @@
# VR keyboard: how it works
# VR keyboard
Technical details of the two VR keyboard patches. What they do and how to
configure them: README, [extra keys](../README.md#steam-keyboard-patch-keyboardvrextrakeysenable)
and [swipe and suggestions](../README.md#vr-keyboard-swipe-and-suggestions-keyboardvr).
Two patches of Steam's VR keyboard: [extra keys](#extra-keys) and
[swipe and suggestions](#swipe-and-suggestions). They work together or
alone. Options: [README, Options](../README.md#options) (`keyboard.vr.*`).
The keyboard layout of the Steam session itself is a separate fix
([keyboard layout](session.md#keyboard-layout)).
## Extra keys
`keyboard.vr.extraKeys.enable`, module `steam-keyboard-patch`.
**Problem:** Steam's VR keyboard has no Ctrl, Alt or Esc, can't press real
keys, and its text emulation only maps plain ASCII: non-ASCII and
AltGr/dead-key characters on the German keymap (`| @ { [ ] } \ ~ ^`,
backtick, `ä ö ü €`) come out as `1`.
**What you get:**
- Bottom row: `Esc Ctrl Alt [Space] AltGr ← ↑ ↓ → Close`, stable with Shift
or AltGr. Esc, Ctrl and Alt have Steam's dark special-key face, like its
AltGr and layout keys.
- AltGr + arrows: Home, End, Page Up, Page Down (hinted on the keys);
Shift + arrows select text.
- AltGr + the key left of Backspace (`´` on German, `=` on US): Delete,
labelled like Steam's Delete key in its language (`Entf`; `Del` if that is
longer), hinted on the key without AltGr; repeats while held. Layouts with
an AltGr character on that key get none.
- Layouts without AltGr (US, Dvorak, Colemak, Bulgarian, Chinese, Japanese,
Korean) get an `Fn` key right of the space bar: Steam's AltGr toggle
(tap: once, tap twice: locked, hold), for Delete and Home/End/Page Up/Down.
- Ctrl/Alt chords and Esc are real key presses in the VR-selected window; a
toggled Ctrl/Alt is held down while the keyboard is open (e.g.
Ctrl+scroll).
- Characters Steam would type as `1` are typed correctly; everything else
goes through Steam as before.
- Enter always types Return (stock Steam may send it to a Steam search box).
Applies right away: no reboot or Steam restart needed, and it is re-applied
after Steam restarts. Turning it off reverts the keyboard.
**Layouts:** the character routing targets the German keymap; on others it
is harmless, and Esc/Ctrl/Alt/arrows work regardless.
**Security:** the helper service that presses the keys only accepts
single-key Ctrl/Alt chords, the extra keys, Ctrl/Alt hold/release and single
non-ASCII/AltGr characters; it cannot type ASCII text or press Enter.
**Caveat:** depends on Steam UI internals; after a Steam update that changes
them the keyboard stays stock and the journal says why (see
[after a Steam update](ui-patches.md#after-a-steam-update)). Tested with
Steam client 1790377368 (UI build 11041156).
**Remove when** Steam's VR keyboard gets these keys.
### How it works
- The `steam-keyboard-patch` user service (`helper.mjs`) injects a patch
into Steam's `SharedJSContext` over DevTools (`127.0.0.1:8080`) and
re-injects it after Steam restarts; stopping it (or disabling the option)
runs the unpatch, so no reboot or Steam restart is needed.
runs the unpatch.
- Ctrl/Alt chords and Esc are sent with `xdotool key` on `:0` (focus follows
the VR-selected window); a toggled Ctrl/Alt is held down with `xdotool`
while the keyboard is open.
the VR-selected window); a toggled Ctrl/Alt is held down with `xdotool`.
- Problem characters (non-ASCII, AltGr/dead-key characters on the German
keymap) are typed with `xdotool type`; everything else goes through
Steam's own text emulation. On other keymaps those characters are still
typed by xdotool, which is why the routing is harmless there.
- The helper's command set is deliberately narrow (see the README's
security note): single-key Ctrl/Alt chords, the extra keys, Ctrl/Alt
hold/release and single non-ASCII/AltGr characters.
Found by signature (see [finders and signatures](ui-patches.md#finders-and-signatures));
if one stops matching, the keyboard stays stock and the journal says why
(see [after a Steam update](ui-patches.md#after-a-steam-update)).
- The new keys use Steam's key type Meta (the dark face); the Delete key
takes Backspace's key type.
- Found by signature (see
[finders and signatures](ui-patches.md#finders-and-signatures)).
## Swipe and suggestions
`keyboard.vr.*`, module `vr-keyboard`.
**Problem:** Steam's VR keyboard is tap-only: no swipe typing, no
suggestions, and deleting more than a few characters means many Backspace
taps.
**What you get** with `keyboard.vr.enable` (the sub-features `swipe`,
`autocorrect`, `completions`, `backspaceDrag` and `haptics` are on by
default):
- **Swipe:** press the trigger on the first letter, sweep over the others,
release on the last. The word is typed with a space before it if needed
(`text.autoSpace`); alternatives show in the strip. `'` and `-` are typed,
not swiped.
- **Suggestions** never change text by themselves: a finished tapped word
that isn't in the dictionary gets corrections (itself first;
`autocorrect`), a word being tapped gets completions (the typed letters
first; `completions`). A pick replaces exactly what it typed and can be
switched again.
- **Backspace drag:** drag Backspace left to delete one character per
`pixelsPerChar`, with a detent (`wordDetentPixels`) at each word border
and at the start of what the keyboard typed; drag back right to retype.
- **Strip** (`suggestions.position`): a SteamVR dashboard panel below or
above the keyboard, or inside the keyboard over its number row. Its
buttons take the keyboard's key style. Below/above uses the
[SteamVR debugger](steamvr-debugger.md), turned on automatically.
- **Haptics:** light ticks for drag steps and picks, a Snap at word detents.
**Dictionary** (`dictionary.*`): by default the `keyboard.layout` language
(de, fr, es, it, nl, pt, sv) plus English, else English only. Add words
(`extraWords`, `extraWordFiles`), remove some (`excludeWords`) or configure
the languages:
```nix
steamFrame.keyboard.vr = {
enable = true;
suggestions.position = "inside"; # no SteamVR panel
dictionary.extraWords = [ "SteamOS" "Nix" ];
dictionary.excludeWords = [ "teh" ];
backspaceDrag.pixelsPerChar = 20;
};
```
**Text memory:** the keyboard can't read the text field, so it remembers
what it typed itself (`text.bufferChars`); anything it can't follow (Enter,
arrows, extraKeys' keys, another field, `text.resetAfterIdleSeconds`) resets
that, and suggestions only replace text the memory proves intact. Works with
and without [extra keys](#extra-keys) (with it, non-ASCII words are typed
via its xdotool helper).
**Caveats:** depends on Steam/SteamVR UI internals; after an update that
changes them the keyboard stays stock (see
[after a Steam update](ui-patches.md#after-a-steam-update)). Accented words
of other languages are in the dictionary but only swipable where the layout
has the letters.
**Remove when** Steam's VR keyboard gets swipe typing and suggestions.
### How it works
- A Steam UI patch (`vr-keyboard`, injected by `steam-ui-patches` like the
other [UI patches](ui-patches.md)).
- Swiped words are matched by shape (SHARK2-style template matching)
against a dictionary built at build time from wordfreq frequency lists
and Hunspell, both from nixpkgs (`dictionary.*`: per language the `words`
most frequent wordfreq entries, shifted by `frequencyOffset`, filtered by
Hunspell except words at or above `keepFrequentAbove`).
and Hunspell, both from nixpkgs (per language the `words` most frequent
wordfreq entries, shifted by `frequencyOffset`, filtered by Hunspell
except words at or above `keepFrequentAbove`).
- The strip below/above the keyboard is a SteamVR dashboard panel
(`panel.js`, a patch of SteamVR's `systemui` page on port 8087, via the
[SteamVR debugger](steamvr-debugger.md)), fed by the `vr-keyboard-relay`
user service (`relay.mjs`) between the two pages. With `inside` neither
the panel nor the relay runs.
- With `extraKeys`, non-ASCII words are typed via its xdotool helper.
Found by signature (entries `vr-keyboard`, `vr-keyboard-panel`); if one
stops matching the keyboard stays stock.
(`panel.js`, a patch of SteamVR's `systemui` page on port 8087), fed by
the `vr-keyboard-relay` user service (`relay.mjs`) between the two pages.
With `inside` neither the panel nor the relay runs.
- Found by signature (entries `vr-keyboard`, `vr-keyboard-panel`).
**Tests:** `nix flake check` (checks `vr-keyboard`: text model, corrector,
decoder accuracy on German + English) and `keyboard.vr.checks` for the
+104 -14
View File
@@ -1,15 +1,73 @@
# Launcher menu: how it works
# Launcher menu
Technical details of the "+" menu features. What they do and how to
configure them: README, [launcher menu](../README.md#launcher-menu-launchermenu),
[hidden apps](../README.md#hidden-apps-launchermenuhiddenapps),
[icon fallbacks](../README.md#icon-fallbacks-launchermenuiconfallbacks).
`launcherMenu.*`: the VR dashboard's "+" menu (non-Steam programs), with
[hidden apps](#hidden-apps) and [icon fallbacks](#icon-fallbacks). Options:
[README, Options](../README.md#options).
## Menu patches
## Problem
`launcherMenu.*` (module `launcher-menu`): [UI patches](ui-patches.md) in
Steam's `SharedJSContext` (`modules/launcher-menu/`), each registered only
when its option is set and reverted by its unpatch when unset (next switch):
The "+" menu is in random order with "Desktop" somewhere in a scrolling
list, and a click shows no feedback until the window appears, so programs
often get started twice.
## What you get
Patches of Steam's UI, each on its own option:
- `sort`: programs sorted by name (case-insensitive), Desktop included.
- `pinDesktop = "top"` / `"bottom"`: Desktop pinned above/below the list,
always visible.
- `closeOnLaunch`: the menu closes on click.
- `launchDebounceSeconds = <seconds>`: a repeat launch of the same command
within that time is ignored (and logged); a program that exits right away
can only be restarted once the time is up.
- `grid.enable`: the programs section becomes a grid of tiles (icon, name
below); "Add desktop window" stays a list. The popup is 300 px wide, so
`grid.columns` sets the tile size (3 ≈ 92 px, 4 ≈ 68 px, 5 ≈ 53 px);
`grid.maxRows` limits visible rows, the rest scrolls.
- `showAllApps`: every program, see
[below](#all-programs-and-developer-mode).
All revert when turned off (next switch).
## Configuration
```nix
steamFrame.launcherMenu = {
sort = true;
pinDesktop = "bottom";
closeOnLaunch = true;
launchDebounceSeconds = 10;
grid = { enable = true; columns = 4; maxRows = 4; };
showAllApps = true;
hiddenApps = [ "lxterminal" "cmake-gui" "firewall-config" "renderdoc" ];
};
```
### All programs and Developer Mode
Without Developer Mode Steam hides `konsole`, `systemsettings`, `dolphin`,
`plasma-discover`, `vlc`, `firewall-config`, `cmake-gui`, `qrenderdoc`,
`lxterminal` and `sh`. **Steam Developer Mode** (a Steam setting, not managed
here) makes the "+" menu list every desktop entry; `showAllApps` lifts that
filter only, so Developer Mode (sshd, xrdp, LAN DevTools forwards) can stay
off. No feature needs Developer Mode; keep it off (see
[DevTools on the LAN](ui-patches.md#devtools-on-the-lan)). Hide single
programs with [`hiddenApps`](#hidden-apps).
## Limitations
- The pinned Desktop works with the laser but not with thumbstick / D-pad
navigation.
- Tested with Steam client 1790377368. A Steam update can break the
patches; the menu then stays stock
([after a Steam update](ui-patches.md#after-a-steam-update)).
## How it works
[UI patches](ui-patches.md) in Steam's `SharedJSContext`
(`modules/launcher-menu/`, module `launcher-menu`), each registered only when
its option is set and reverted by its unpatch when unset:
- `order/`: sorts `ScanForInstalledNonSteamApps()` by name (Steam lists the
programs in GLib hash-table order);
@@ -26,14 +84,46 @@ and verified by the [offline checker](ui-patches.md#after-a-steam-update).
## Hidden apps
`launcherMenu.hiddenApps` (module `hidden-apps`): a Home Manager-linked
desktop entry with `Hidden=true` per id in `~/.local/share/applications`,
which masks the system entry of the same id for Steam and KDE alike.
`launcherMenu.hiddenApps`, module `hidden-apps`.
**Problem:** with Developer Mode or `showAllApps`, the "+" menu lists every
desktop entry, including system tools.
**What you get:** the listed desktop entry ids (no `.desktop`) are hidden
from the "+" menu and the KDE menu. The "+" menu always hides `steam` and
`vrurlhandler`; for Konsole in VR use
[`showAllApps`](#all-programs-and-developer-mode).
**How it works:** a Home Manager-linked desktop entry with `Hidden=true` per
id in `~/.local/share/applications`, which masks the system entry of the
same id for Steam and KDE alike.
## Icon fallbacks
`launcherMenu.iconFallbacks.*`: Home Manager links nixpkgs' Breeze SVGs
into `~/.local/share/icons/hicolor/scalable/apps/`. When the set of links
`launcherMenu.iconFallbacks.*`. On by default.
**Problem:** Steam resolves `Icon=` only in the hicolor theme (and
`pixmaps`), so Konsole and KDE System Settings, whose icons only Breeze has,
show without icon.
**What you get:** Breeze's `utilities-terminal` and `preferences-system`
icons (plus `extra`) in hicolor, so the "+" menu shows them. A running Steam
picks up changes without a restart. Each switch also prints hints: icons of
programs Steam can't find that Breeze has (add them to `extra`), and
fallbacks hicolor has anyway.
**Configuration:** `extra` adds Breeze icon names; a name Breeze doesn't have
fails the build, `enable = false` provides none.
```nix
steamFrame.launcherMenu.iconFallbacks.extra = [ "system-file-manager" ];
```
`iconFallbacks` used to be a list; a list now fails with a hint (use
`extra`, or `enable = false` for `[ ]`).
**How it works:** Home Manager links nixpkgs' Breeze SVGs into
`~/.local/share/icons/hicolor/scalable/apps/`. When the set of links
changes, the switch bumps the mtime of `~/.local/share/icons/hicolor`, so a
running Steam rescans (GTK only rereads a theme whose directory changed).
The switch's hints come from `icon-fallbacks.sh`.
+101
View File
@@ -0,0 +1,101 @@
# Session fixes
Four fixes for the Frame's [two graphical sessions](../README.md#two-sessions):
session settings and services, the portal, the keyboard layout and the
clipboard. Options: [README, Options](../README.md#options) (`session.*`,
`keyboard.layout`, `keyboard.variant`, `clipboardSync.*`).
## Session settings and services
Module `session` (`session.nix`), used by the other modules; normally
nothing to set.
**Problem:** the nested desktop can't see the Steam session's runtime dir
and bus (services, KDE wallet), and switching from the nested desktop,
Home Manager can't reach the service manager ("User systemd daemon not
running") and skips `reloadSystemd`.
**What you get:**
- `session.runtimeDir` / `session.bus` point at the Steam session's runtime
dir and bus; `session.busEnv` is a launcher prefix to reach them.
- After every switch this module reloads the Steam session's user manager
and applies `session.services.start` / `stop` / `restart`, which other
modules fill (you can add your own units).
**Configuration:** a launcher for an app that must use the single wallet on
the outer bus (see [Two sessions](../README.md#two-sessions)):
```nix
{ config, ... }: {
xdg.dataFile."applications/org.example.App.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Example
Exec=${config.steamFrame.session.busEnv} flatpak run org.example.App %U
'';
}
```
## Portal fix
`session.portalFix.enable`, module `portal`. On by default.
**Problem:** the Frame image (SteamOS 0.3.0, build 20260922) points the Steam
session's `xdg-desktop-portal` at `/usr/share/xdg-desktop-portal/gamescope-portals`,
which lacks `gamescope-portals.conf`: no backend, no OpenURI, so no app in
the Steam session can open links.
**What you get:** a working OpenURI portal in the Steam session; the
desktop's portal is unaffected.
**Remove when** SteamOS ships `gamescope-portals.conf`.
**How it works:** a portal dir in `~/.local/share` linking Valve's `.portal`
files plus a config (`default=holo;gamescope`), and a drop-in on
`xdg-desktop-portal.service`.
## Keyboard layout
`keyboard.layout`, `keyboard.variant`, module `keyboard-layout`.
**Problem:** gamescope and its Xwayland displays use US unless
`XKB_DEFAULT_*` is set; KDE's layout only affects the nested desktop, and
`~/.config/environment.d` isn't read on the Frame.
**What you get:** the layout in the Steam session, from the next Steam
session start. The layout also picks the
[VR keyboard](keyboard.md#swipe-and-suggestions)'s default dictionary
language.
**Configuration:** `keyboard.variant` picks a variant of the layout, e.g. for
`de`: `null` (standard, with dead keys: `^`, `` ` ``, `´` wait for the next
key), `"nodeadkeys"` (those are typed immediately), `"mac"`, `"neo"`, `"e1"`,
`"us"` (German letters on a US layout). List them with
`localectl list-x11-keymap-variants <layout>`.
```nix
steamFrame.keyboard = { layout = "de"; variant = "nodeadkeys"; };
```
**Remove when** SteamOS applies a layout setting to gamescope.
**How it works:** a drop-in on `gamescope-session.service` setting
`XKB_DEFAULT_LAYOUT`/`VARIANT`.
## Clipboard sync
`clipboardSync.enable`, module `clipboard-sync`. On by default.
**Problem:** the Steam session's X displays and the nested desktop have
separate clipboards.
**What you get:** one clipboard: copy in one session, paste in the other.
**Configuration:** `clipboardSync.package` replaces the package. Switch from
a desktop terminal: each switch restarts clipboard-sync if outdated, and it
must restart with the desktop's environment.
**How it works:** [clipboard-sync](https://github.com/dnut/clipboard-sync),
built from source (its flake is x86-only), started via KDE autostart (the
desktop can't reach the user manager, and `:2` must exist first).
+66
View File
@@ -0,0 +1,66 @@
# Steam close button
`dashboard.steamCloseButton.enable`, module `steam-close-button` ([options](../README.md#options)).
A [SteamVR dashboard patch](ui-patches.md#steamvr-dashboard-patches).
## Problem
Every dashboard window has a close (X) button except Steam's own, and with
no other window open the dashboard always shows it.
## What you get
The Steam window gets an X that hides Steam: it docks the window back if it
was in the world, theater or on a hand, then shows the most recently active
other dashboard window, or **just the dashboard bar** if there is none.
Steam stays hidden until you bring it back (Steam tab, a Steam menu pick,
SteamVR asking for it): closing the active window, or a theater window,
then goes to the previous window or the bar instead of Steam. This survives
dashboard reopens, patch-service restarts, SteamVR restarts and reboots
("Steam hidden" is saved in
`~/.local/state/steam-frame-nix/ui-patches/steam-close-button.json`, see
[Changes outside Nix](../README.md#changes-outside-nix-exceptions)). After a
restart the patch attaches a few seconds after the dashboard appears,
possibly after SteamVR has already shown Steam: if Steam is (or first
becomes) the active window then, it is hidden once like with X (only the bar
at that point); otherwise it just stays hidden.
## Configuration
```nix
steamFrame.dashboard.steamCloseButton.enable = true;
```
## Limitations
- SteamVR's rarer "go home" paths (Now Playing after a game quits, message
overlays) still show Steam.
- No effect with a VRLink remote dashboard.
- Turning the option off while bar-only leaves no active window until the
next tab click or dashboard open.
## How it works
`steam-close-button`, with [persistent state](ui-patches.md#persistent-state)
(`{ schema: 1, steamHidden }`).
- **Button:** the frame's `closing` component shows X when
`componentProps.onCloseRequested` exists; the Steam window's instance
(overlay `valve.steam.gamepadui.main`) gets one and its props are
re-assigned so the MobX computed re-evaluates.
- **Staying hidden:** while hidden, stock fallbacks to Steam go to the
previous window or bar-only instead: `Dashboard.autoSwitchOverlayIfNeeded`
(instance override) and the mailbox handler `dashboard_overlay_destroyed`.
Mailbox show/switch requests for Steam with reason `SetDockLocation` (the
echo of X docking Steam) are dropped, "theater frame destroyed" ones are
passed on without the Steam key. Explicit requests (tab click, Steam menu
pick, `SwitchToDashboardOverlay`) show Steam again.
- **Restarts:** a restored hidden state stays pending (`restorePending`)
until Steam's frame is seen, since the injector attaches within ~5 s of
the page appearing.
Debugging: `window.__sfuiSteamClose.plan()` / `homePlan()` in the `systemui`
page; state in `window.__sfuiSteamCloseState` (survives re-injection;
teardown restores all overrides and never switches frames).
+36 -17
View File
@@ -1,18 +1,37 @@
# SteamVR debugger: how it works
# SteamVR debugger
`steamvrDebugger.enable` (module `steamvr-debugger`). What it is for and
what you need to do: README,
[SteamVR debugger](../README.md#steamvr-debugger-steamvrdebuggerenable).
`steamvrDebugger.enable`, module `steamvr-debugger`. Automatic; nothing to
set. Options: [README, Options](../README.md#options).
Enabled automatically when any patch in `steamFrame.uiPatches.patches` has
an `endpoint` on port 8087 (all [dashboard](dashboard.md) patches and the VR
keyboard's strip below/above the keyboard).
## Problem
SteamVR opens its DevTools port only with `VRWebHelper/DebuggerEnabled`
(port `VRWebHelper/DebuggerPort`, default 8087). SteamVR rewrites
`~/.config/openvr/config/steamvr.vrsettings` from memory, so the key can't
be a link and can't be edited while SteamVR runs. It is set only while
SteamVR runs:
The [dashboard patches](ui-patches.md#steamvr-dashboard-patches) (and the
[VR keyboard](keyboard.md#swipe-and-suggestions)'s strip below/above the
keyboard) need SteamVR's DevTools port, which SteamVR opens only with its
setting `VRWebHelper/DebuggerEnabled`. That setting lives in
`~/.config/openvr/config/steamvr.vrsettings`, which SteamVR rewrites from
memory, so it can't be a Nix link and can't be edited while SteamVR runs.
## What you get
The port, enabled automatically when any patch in
`steamFrame.uiPatches.patches` has an `endpoint` on port 8087. The setting
is **set only while SteamVR runs**: set before every SteamVR start, put back
to its previous value when SteamVR stops, also after a rollback or uninstall
(without Nix). A value you set to `true` yourself is never touched.
**The first time, restart SteamVR once** (e.g. reboot); until then the
dashboard patches wait. Turned off, the setting is restored at the switch
(or when SteamVR stops, if it runs).
## Security
The port listens on `127.0.0.1` only; keep Developer Mode off (see
[DevTools on the LAN](ui-patches.md#devtools-on-the-lan)).
## How it works
Port: `VRWebHelper/DebuggerPort`, default 8087.
- before every SteamVR start, the `steamvr-webhelper-debugger` oneshot
(a drop-in on `steamvr.service`, running `install.sh steamvr-debugger-arm`)
@@ -25,12 +44,12 @@ SteamVR runs:
- when SteamVR stops, the key goes back to its previous value (removed if
it wasn't there) and `.armed` is removed.
The runtime files don't need Nix, so this also works after a rollback or
uninstall; they are gone at reboot, and `.armed` (the only trace after a
The runtime files don't need Nix, which is why rollback and uninstall are
covered; they are gone at reboot, and `.armed` (the only trace after a
power loss) is resolved by the next SteamVR start or
`steam-frame-nix-cleanup`. A key set to `true` by the user is never touched.
Disabled, there is no unit; the switch (cleanup) restores the key if SteamVR
is stopped, otherwise the runtime drop-in does when it stops.
`steam-frame-nix-cleanup`. Disabled, there is no unit; the switch (cleanup)
restores the key if SteamVR is stopped, otherwise the runtime drop-in does
when it stops.
Until SteamVR has been restarted once with the drop-in, the port is closed
and `steam-ui-patches` keeps polling it.
+73 -24
View File
@@ -1,8 +1,15 @@
# UI patches
For patch authors, and for fixing patches after a Steam update. User-level
overview: README, [UI patches](../README.md#ui-patches-uipatchespatches);
options `steamFrame.uiPatches.patches` and `steamFrame.uiPatches.lib`.
`uiPatches.patches`, `uiPatches.lib` (modules `steam-ui-patches`,
`steamvr-debugger`). For patch authors, and for fixing patches after a Steam
update. Options: [README, Options](../README.md#options).
## Problem
Steam's UI and SteamVR's dashboard can't be extended: their files belong to
Steam, and Steam overwrites them on every update.
## What you get
Steam's UI and SteamVR's dashboard (`vrwebhelper`) are CEF web pages with a
local DevTools port: `127.0.0.1:8080` for Steam (SteamOS passes
@@ -10,15 +17,51 @@ local DevTools port: `127.0.0.1:8080` for Steam (SteamOS passes
[its debugger](steamvr-debugger.md) is on. The `steam-ui-patches` user
service (`injector.mjs`) patches the running pages through them; Steam's
files are never modified. The [launcher menu](launcher-menu.md),
[dashboard](dashboard.md) and [VR keyboard](keyboard.md) features are such
patches (the extra keys have their own injector, `steam-keyboard-patch`),
and you can add your own.
[dashboard](#steamvr-dashboard-patches) and [VR keyboard](keyboard.md)
features are such patches (the extra keys have their own injector,
`steam-keyboard-patch`), and you can add your own.
Don't expose these ports: see
[DevTools on the LAN](../README.md#devtools-on-the-lan).
Turning a feature off (or stopping the `steam-ui-patches` /
`steam-keyboard-patch` user services) restores the stock UI without a Steam
restart. Log: `journalctl --user -u steam-ui-patches -u steam-keyboard-patch`.
**Caveat:** patches depend on Steam UI internals and can break with an
update; find modules by signature, not id ([below](#finders-and-signatures)).
## Caveats and security
- Patches depend on Steam UI internals and can break with an update; find
modules by signature, not id ([below](#finders-and-signatures)).
- Don't expose the DevTools ports ([DevTools on the LAN](#devtools-on-the-lan)).
### DevTools on the LAN
Steam's Developer Mode enables `steam-web-debug-portforward`
(`0.0.0.0:8081` → `8080`) and `steamvr-web-debug-portforward`
(`0.0.0.0:8088` → `8087`), and firewalld allows ports 1024-65535, so anyone
on the network could run code in Steam's UI. No patch needs Developer Mode,
so keep it off. If it was on while those units were masked, they may stay
enabled: check with
`systemctl is-enabled steam-web-debug-portforward steamvr-web-debug-portforward`
and `sudo systemctl disable` them.
## SteamVR dashboard patches
[Dashboard windows](dashboard-windows.md),
[Steam close button](steam-close-button.md),
[window curvature](window-curvature.md) and
[window control bar](window-control-bar.md) patch SteamVR's dashboard while
it runs. What they have in common:
- Each turns on the [SteamVR debugger](steamvr-debugger.md) (**the first
time, restart SteamVR once**).
- They depend on SteamVR UI internals: each is found by signature (its entry
in `modules/lib/signatures.json` has the patch's name); after an update
that changes them the dashboard stays stock (see
[after a Steam update](#after-a-steam-update)). Tested with SteamVR build
11008059.
- All are laser-only: gamepad navigation sees the stock dashboard.
- All are `mkPatch` patches of `vrwebhelper` (page title `systemui`,
DevTools `127.0.0.1:8087`, webpack chunk `webpackChunkvrwebui`),
registered only when enabled. Sources: `modules/<name>/patch.js`, whose
header comments go into more detail.
## Defining a patch
@@ -46,8 +89,8 @@ Patches are evaluated on attach, after new JS contexts (reloads) and every
15 s, so they must be idempotent: return e.g. `"patched"` once, then
`"unchanged"` (not logged); other results are logged when they change
(`journalctl --user -u steam-ui-patches`). On stop, `unpatch` restores the
stock UI without a Steam restart. The service exists only while the list is
non-empty and is restarted on every switch.
stock UI. The service exists only while the list is non-empty and is
restarted on every switch.
## Persistent state
@@ -66,12 +109,11 @@ per such patch in `~/.local/state/steam-frame-nix/ui-patches/<name>.json`
service writes the file atomically, only on change, only for that page's
`state` patches, at most 64 KiB.
Used by the [window control bar](dashboard.md#window-control-bar) and the
[Steam close button](dashboard.md#steam-close-button). The file is user
data, not generated by Nix: it is kept when the patch is disabled or removed
(the choices come back when you enable it again) and removed only by
`steam-frame-nix-cleanup --all` or `install.sh uninstall`; see
[changes outside Nix](../README.md#changes-outside-nix-exceptions).
Used by the [window control bar](window-control-bar.md) and the
[Steam close button](steam-close-button.md). The file is user data, not
generated by Nix: it is kept when the patch is disabled or removed and
removed only by `steam-frame-nix-cleanup --all` or `install.sh uninstall`;
see [changes outside Nix](../README.md#changes-outside-nix-exceptions).
## Finders and signatures
@@ -134,9 +176,9 @@ steamFrame.uiPatches.patches = [ {
`modules/lib/hooks.js`, argument `hooks`, also `window.__sfuiHooks`: patches
intercepting the same method (e.g. the dashboard mailbox's `SendMessage`,
used by [dashboard windows](dashboard.md#dashboard-windows) and
[window curvature](dashboard.md#window-curvature)) register named hooks; one
wrapper per method runs them in registration order, so patches can be
used by [dashboard windows](dashboard-windows.md#how-it-works) and
[window curvature](window-curvature.md#how-it-works)) register named hooks;
one wrapper per method runs them in registration order, so patches can be
injected, upgraded and reverted in any order.
```js
@@ -147,10 +189,18 @@ hooks.remove(Mailbox.prototype, 'SendMessage', 'my-patch'); // in unpatch
```
Patches reacting to presses on the curvature controls follow the
[window curvature contract](dashboard.md#contract-for-other-patches) (`sfui-curv-*`).
[window curvature contract](window-curvature.md#contract-for-other-patches)
(`sfui-curv-*`).
## After a Steam update
If a Steam or SteamVR update changes Steam's code so a signature no longer
matches exactly once, that patch changes nothing: the feature stays stock
and the journal (see [above](#what-you-get)) says why, e.g.
`signature not found, Steam left unpatched: …`. Update steam-frame-nix
(`nix flake update steam-frame-nix`, then switch) once it supports the new
build.
Check the signatures offline (Steam need not run) from a checkout:
```sh
@@ -178,5 +228,4 @@ To fix a signature, inspect the new module sources
(`scripts/webpack-modules.mjs`), adjust `signatures.json` and bump the
patch's `VERSION` if its code changes. Flags: `--signatures FILE` (your own,
bundles `steamui` / `vrwebui-systemui`), `--dir steamui=DIR`,
`--patch NAME`, `--strict` (fail on warnings), `--json`. Live:
`journalctl --user -u steam-keyboard-patch -u steam-ui-patches`.
`--patch NAME`, `--strict` (fail on warnings), `--json`.
+57
View File
@@ -0,0 +1,57 @@
# Window control bar
`dashboard.frameControls.*`, module `frame-controls` ([options](../README.md#options)).
A [SteamVR dashboard patch](ui-patches.md#steamvr-dashboard-patches).
## Problem
The controls under a dashboard window are fixed: some in the bottom bar,
others only in the three-dot menu (curvature, dock to a controller), and
theater windows have no "Float".
## What you get
- **long press** a bar icon or menu row (`longPressMs`; a progress ring
shows from half the time, at most after 1 s), then **Show in bar** in the
popup moves that control between bar and menu **for all windows**.
Placements survive SteamVR restarts and reboots (saved in
`~/.local/state/steam-frame-nix/ui-patches/frame-controls.json`, see
[Changes outside Nix](../README.md#changes-outside-nix-exceptions)).
Short presses stay stock.
- `inBar` / `inMenu` set where controls start; a popup choice wins until
that control's entry changes.
- `floatInTheater` gives theater windows the "Float" control.
With [window curvature](window-curvature.md), a drag on the curvature
control adjusts curvature and cancels the long press, also after the ring
shows; once the ring shows, the drag needs 3× the usual travel
(`dragThresholdPixels`, counted from where the press started), so laser
drift during the hold doesn't cancel it.
## Configuration
```nix
steamFrame.dashboard.frameControls = {
enable = true;
# longPressMs = 1500;
# inBar = [ "curvature" ]; inMenu = [ "theater" ];
# floatInTheater = true;
};
```
## Limitations
- Laser only: no right-click or thumbstick click reaches the dashboard;
gamepad navigation sees stock controls.
- Placements are per control type, not per window.
- The three-dot button itself can't be moved.
## How it works
`frame-controls`, with [persistent state](ui-patches.md#persistent-state)
(placements). Works with window curvature through its
[contract](window-curvature.md#contract-for-other-patches).
Debugging: `window.__sfuiFrameControls.dump()`, `.placement()`, `.reset()`
(forget choices), `.log`, `window.__sfuiFrameControlsState`.
+85
View File
@@ -0,0 +1,85 @@
# Window curvature
`dashboard.windowCurvature.*`, module `window-curvature` ([options](../README.md#options)).
A [SteamVR dashboard patch](ui-patches.md#steamvr-dashboard-patches).
## Problem
SteamVR dashboard windows are either curved (fixed radius) or flat, and
world windows start flat.
## What you get
The "Toggle Curvature" row of a window's three-dot menu becomes a control
showing the window's value; the same control in the bottom bar (see
[window control bar](window-control-bar.md)) works without the value, with
haptic steps.
- **click:** curved → flat, flat → stock (1);
- **drag up/down** with the laser: curvature from 0 (flat) to `max`,
relative to stock (2 = half the radius), rounded to `step`, with a detent
of `detentPixels` of drag at each of `detentPoints` (no values skipped),
and haptics (`haptics`) for detents, edges and steps. Drag distance:
`dragPixelsPerUnit` in the menu, `barDragPixelsPerUnit` on the bar button,
after `dragThresholdPixels`.
A window without its own value is shown at `initial` once curved in the
world or on a hand, at 1 in the dashboard or theater. Values are kept per
window until SteamVR restarts.
## Configuration
```nix
steamFrame.dashboard.windowCurvature = {
enable = true;
# initial = 1.0; max = 3.0; step = 0.05;
# detentPoints = [ 0 1.0 ]; detentPixels = 24;
# dragPixelsPerUnit = 60; # full range in one drag (see below)
};
```
## Limitations
- Laser only; with gamepad navigation the row is the stock toggle.
- No thumbstick scrolling (SteamVR sends no wheel events to the menu).
- The laser stops at the menu's edge (~190 px above the row): with the
default 120 px per 1.0, 0 → 1 fits into one drag, 0 → 3 takes two. Lower
`dragPixelsPerUnit` (≤ 60) for the full range in one drag.
## How it works
Stock curvature: the frame renders a transform `frame:<id>:curvature-origin`
at `z = DashboardStore.curvatureDistance` when curved, else 1000; its panels
reference it as `curvature-origin-id` and vrcompositor bends them onto a
cylinder around it (curvature = 1/radius). Those MobX properties are
non-configurable, so the patch hooks the mailbox `SendMessage`
([shared hooks](ui-patches.md#shared-method-hooks), shared with
[dashboard windows](dashboard-windows.md)) and rewrites the origin's z in
outgoing scene graphs to stock / value. On/off stays the stock toggle state,
so stock toggle and wheel always agree.
Values are kept per window (key: first overlay key) in
`window.__sfuiWindowCurvatureState`, across re-patch and unpatch, not across
a dashboard reload. The bar button uses the coordinates SteamVR keeps
sending past the pressed panel's edge while the trigger is held; the bar
panel is never resized.
Debugging: `window.__sfuiWindowCurvature.dump()` (`.log` recent events).
### Contract for other patches
For patches handling presses on the curvature controls (e.g. the window
control bar's long press): every element the patch drives has class
`sfui-curv-ctl`; when a press becomes a drag, a bubbling `CustomEvent`
`sfui-curv-dragstart` (detail `{ frameID, where: 'menu' | 'bar' }`) is
dispatched on it, and `sfui-curv-dragend` when it ends;
`window.__sfuiWindowCurvature.scalePressDragThreshold(factor)` sets the
current press's drag threshold to `factor` × `dragThresholdPixels` (from the
press start; returns whether it applied, i.e. a press that is not yet a
drag); `window.__sfuiWindowCurvature.cancelPress()` ends a press without its
click. A patch with its own gesture lets `mousemove` through while
undecided, drops its gesture on `sfui-curv-dragstart`, may raise the
threshold while its gesture is under way, and calls `cancelPress()` when it
takes the press over. The window control bar uses factor 3 once its
progress ring shows.