README: full user documentation again, docs/ only technical

The README is the complete user documentation again: features, install,
two sessions, usage, the full options table (and renamed options), every
feature with problem, usage, limitations, security and "remove when",
UI patches at user level (DevTools on the LAN, after a Steam update),
changes outside Nix, rollback and uninstall.

docs/ keeps only the technical side (how the patches work, writing
patches, signatures and the update procedure, the SteamVR debugger
mechanics, cleanup and installer internals), linked from each feature.
docs/options.md, two-sessions.md and desktop-integration.md are gone
(their content is in the README).
This commit is contained in:
Pierre Kisters committed 2026-09-29 00:47:16 +02:00
1 parent 23b79e9499
commit 5a3d0b9d05
13 files changed
+1126 -856

No files matched your search

+881 -93
View File
File diff suppressed because it is too large. Load diff
+41 -86
View File
@@ -1,51 +1,41 @@
# Changes outside Nix
# Cleanup and installer: how they work
Everything not listed here is a Home Manager link into the Nix store or
lives in memory (the [UI patches](ui-patches.md)).
## Written at runtime
| Path | Feature | Lifetime | Removed by |
|---|---|---|---|
| `VRWebHelper.DebuggerEnabled` in `~/.config/openvr/config/steamvr.vrsettings` | [SteamVR debugger](steamvr-debugger.md) | only while SteamVR runs | SteamVR stopping (runtime drop-in below); `steam-frame-nix-cleanup` while SteamVR is stopped |
| `~/.local/state/steam-frame-nix/steamvr-debugger.armed` | SteamVR debugger: the key's previous value | while SteamVR runs; after a power loss until the next SteamVR start or cleanup | SteamVR stopping; `steam-frame-nix-cleanup` |
| `/run/user/1000/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf`, `/run/user/1000/steam-frame-nix/steamvr-debugger-restore` | SteamVR debugger: puts the key back when SteamVR stops, without Nix | until reboot (tmpfs) | reboot; `steam-frame-nix-cleanup` while SteamVR is stopped and the debugger is off |
| `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | [persistent state](ui-patches.md#persistent-state) of dashboard patches: window control bar placements (`frame-controls`), "Steam hidden" (`steam-close-button`). SteamOS's `steamvr.service` deletes `~/.cache/SteamVR` (the dashboard's own browser storage) on every SteamVR start. | until removed: kept when a patch is disabled (the choices come back when you enable it again) | `steam-frame-nix-cleanup --all`, `install.sh uninstall` |
| mtime of `~/.local/share/icons/hicolor` | [icon fallbacks](launcher-menu.md#icon-fallbacks): a running Steam rescans icons | only the directory's timestamp | nothing to remove |
## Only while running
- The UI patches (Steam, SteamVR dashboard, VR keyboard) live in the pages'
memory; stopping `steam-ui-patches` / `steam-keyboard-patch` reverts them.
- clipboard-sync runs from KDE autostart (a Home Manager link).
- Firefox: the desktop profile's `user.js` link exists only while its
Firefox runs (see [Firefox](firefox.md#how-it-works)).
- Jellyfin: the hardware decoding permissions are `flatpak run` options of
the desktop entry, not a Flatpak override.
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`** (`install.sh cleanup`,
`steamFrame.cleanup.package`) knows everything any version of
steam-frame-nix wrote outside the store, removes only what is provably its
own (everything else is reported as "left alone") and can be run again
safely; `--dry-run` shows what it would do.
`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.
- On every switch, `cleanup --orphans` removes what the configuration no
longer uses (never the saved patch state).
- `steam-frame-nix-cleanup --all` removes everything, also the saved patch
state. SteamVR's key can't be changed while SteamVR runs: it is then left
to the runtime drop-in (restored when SteamVR stops).
- Without Nix or after a rollback it runs from the script
(bash, coreutils, findutils, jq, all in SteamOS' `/usr/bin`):
`curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all`.
- It also removes what older versions left: 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`.
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
@@ -61,52 +51,17 @@ safely; `--dry-run` shows what it would do.
- `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) 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;
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`.
## App data you create
Not steam-frame-nix's to remove: the Firefox desktop profile
(`~/.var/app/org.mozilla.firefox/config/mozilla/firefox/desktop`, browser
data), and whatever apps keep in `~/.var/app/*`, Flatpak apps and their
runtimes.
## Rollback
`home-manager generations` lists previous generations; run
`<store path>/activate` of the one you want. Generations with
steam-frame-nix clean up after themselves on activation (orphans). After
rolling back to a generation **without** steam-frame-nix, or to one older
than `steam-frame-nix-cleanup` (2026-09-29), remove what the newer one wrote
outside the store:
```sh
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all
# or: nix run github:lhns/steam-frame-nix#cleanup -- --all
```
## Uninstall
```sh
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- uninstall # --keep-nix keeps Nix
```
stops Home Manager's user services (reverting the UI patches), runs
`cleanup --all`, runs `home-manager uninstall`, then removes Nix and the
per-user Nix state (see [Set up by install.sh](#set-up-by-installsh)). If
SteamVR is running, its key is restored when SteamVR stops (the closing
message says so). Your configuration, `*.hm-backup-*` files, app data and
Flatpaks stay.
To drop steam-frame-nix from a Home Manager configuration you keep, first
run `steam-frame-nix-cleanup --all`, then remove it and switch. Or set Home
Manager's `uninstall = true;` in the configuration that still imports
steam-frame-nix and switch: its activation runs `cleanup --all` while Home
Manager removes its files. (`home-manager uninstall` alone doesn't load
steam-frame-nix's modules, so it can't clean up after them.)
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`.
+69 -133
View File
@@ -1,122 +1,89 @@
# SteamVR dashboard
# SteamVR dashboard patches: how they work
Four patches of SteamVR's dashboard (`systemui` page): [window size and
distance](#dashboard-windows), [Steam close button](#steam-close-button),
[window curvature](#window-curvature), [window control bar](#window-control-bar).
Options: [options.md#dashboard](options.md#dashboard).
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 are [UI patches](ui-patches.md) and turn on the
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 `signatures.json` has the patch's name); on mismatch the dashboard
stays stock. Tested with SteamVR build 11008059.
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.*`.
`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:
**Problem:** 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.
- `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).
**What it does:** raises these limits, which the dashboard sends to the
compositor in its scene graph. `null` keeps stock:
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.
| 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; turning options off
reverts on the next switch. The keyboard's range is not patched.
```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;
};
```
**Caveat:** grab nodes are recognized by their exact stock values, so if
SteamVR changes them the distance options silently do nothing. State:
`window.__sfuiDashboardWindows`.
State: `window.__sfuiDashboardWindows` (counters in `.hits`).
## Steam close button
`dashboard.steamCloseButton.enable`.
`steam-close-button`, with [persistent state](ui-patches.md#persistent-state)
(`{ schema: 1, steamHidden }`).
**Problem:** every dashboard window has a close (X) button except Steam's
own, and with no other window open the dashboard always shows it.
- **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.
**What it does:** gives the Steam window 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
[persistent state](ui-patches.md#persistent-state)). 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.
**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.
Debugging: `window.__sfuiSteamClose.plan()` / `homePlan()` and
`window.__sfuiSteamCloseState`.
Debugging: `window.__sfuiSteamClose.plan()` / `homePlan()`; state in
`window.__sfuiSteamCloseState` (survives re-injection; teardown restores all
overrides and never switches frames).
## Window curvature
`dashboard.windowCurvature.*`.
`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.
**Problem:** dashboard windows are either curved (fixed radius) or flat, and
world windows start flat.
**What it does:** turns the "Toggle Curvature" row of a window's three-dot
menu into a control showing the window's value; the same control in the
bottom bar (see [window control bar](#window-control-bar)) 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.
**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.
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: 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;
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
@@ -124,46 +91,15 @@ 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.
takes the press over. The window control bar uses factor 3 once its
progress ring shows.
## Window control bar
`dashboard.frameControls.*`.
**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 it does:**
- **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
[persistent state](ui-patches.md#persistent-state)).
- `inBar` / `inMenu` set where controls start; a popup choice wins until
that control's entry changes.
- `floatInTheater` gives theater windows the "Float" control.
```nix
steamFrame.dashboard.frameControls = {
enable = true;
# longPressMs = 1500;
# inBar = [ "curvature" ]; inMenu = [ "theater" ];
# floatInTheater = true;
};
```
With [window curvature](#window-curvature), 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.
**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.
`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`.
-54
View File
@@ -1,54 +0,0 @@
# Session, portal and clipboard
Plumbing between the Frame's [two sessions](two-sessions.md). Options:
[Session](options.md#session), [Clipboard sync](options.md#clipboard-sync).
## Session settings and services
Module `session.nix`, used by the other modules; normally nothing to set.
- `session.runtimeDir` / `session.bus` point at the Steam session's runtime
dir and bus (services, KDE wallet), which the nested desktop can't see.
`session.busEnv` is a launcher prefix to reach them:
```nix
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
'';
```
- Switching from the nested desktop, Home Manager can't reach the service
manager ("User systemd daemon not running"). So after every switch this
module reloads the Steam session's user manager and applies
`session.services.start` / `stop` / `restart`, which other modules fill.
## Portal
`session.portalFix.enable`, 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.
**Fix:** 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`. The desktop's portal is unaffected.
**Remove when** SteamOS ships `gamescope-portals.conf`.
## Clipboard sync
`clipboardSync.enable`, on by default.
**Problem:** the Steam session's X displays and the nested desktop have
separate clipboards.
**Fix:** [clipboard-sync](https://github.com/dnut/clipboard-sync), built from
source (its flake is x86-only; `clipboardSync.package` to replace it),
started via KDE autostart (the desktop can't reach the user manager, and
`:2` must exist first). Each switch restarts it if outdated, so switch from a
desktop terminal.
+12 -28
View File
@@ -1,31 +1,9 @@
# Firefox
# Firefox: how it works
`firefox.enable`: a launcher for the Flathub Firefox Flatpak
(`org.mozilla.firefox`, stable). It shadows the Flatpak's own entry (same
ID), so default-browser associations keep working. Options:
[options.md#firefox](options.md#firefox).
`firefox.*` (module `firefox`). What it does and how to configure it:
README, [Firefox](../README.md#firefox-firefox).
## Options
- **`vrFullscreenFix`** (on): gamescope never shows fullscreen windows, so
Firefox looks frozen. `full-screen-api.ignore-widgets` makes fullscreen
fill just the window. Not applied in the desktop profile. **Remove when**
gamescope shows fullscreen X11 windows in VR.
- **`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. **Remove
when** a SteamOS kernel adds AV1 to `iris`.
- **`prefs`:** further `about:config` values for every profile; they can
also override the fixes above.
- **`desktopProfile`** (`"desktop"`): the sessions can't see each other's
Firefox, so a second instance stops at the locked profile; 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.
Changes take effect at the next start of Firefox.
## How it works
## Default prefs
`prefs` and the fixes are *default* values (`pref()`), not user values:
`about:config` can still change them per profile, and nothing is written to
@@ -38,8 +16,12 @@ extension as a link from
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.)
The desktop profile undoes the fullscreen fix only while its Firefox runs:
the launcher links the profile's `user.js` to
## 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
@@ -48,6 +30,8 @@ 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.
## 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`
on switch, for each profile not in use at that moment.
+25 -46
View File
@@ -1,13 +1,13 @@
# Jellyfin hardware decoding
# Jellyfin hardware decoding: how it works
`jellyfin.hardwareDecoding.enable`, for the Flathub
[Jellyfin Desktop](https://github.com/jellyfin/jellyfin-desktop) Flatpak
(`org.jellyfin.JellyfinDesktop`), which plays video with libmpv. Options:
[options.md#jellyfin](options.md#jellyfin).
`jellyfin.hardwareDecoding.*` (module `jellyfin`). What it does, how to
configure it and its caveats: README,
[Jellyfin hardware decoding](../README.md#jellyfin-hardware-decoding-jellyfinhardwaredecoding).
**Problem:** 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
## Why mpv doesn't use the decoder
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,43 +15,23 @@ Frame's hardware decoder is a V4L2 memory-to-memory device (`qcom-iris`,
probing leaves out V4L2 M2M on purpose (its quality varies by SoC).
Jellyfin has no way to pass mpv options.
**What it does:** device access (`devices=all`) and an `LD_PRELOAD` shim
that rewrites an `hwdec` value starting with `auto` (set through libmpv's
`mpv_set_*` functions) to `$SFN_MPV_HWDEC` (the `hwdec` option, 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 through `v4l2m2m-copy` at ~15-20 % CPU.
Changes take effect at the next start of Jellyfin.
## The shim
Installing the Flatpak is up to you, e.g.
`flatpak install --user flathub org.jellyfin.JellyfinDesktop`, or with
nix-flatpak:
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.
```nix
services.flatpak.packages = [ "org.jellyfin.JellyfinDesktop" ];
```
**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 (a few libmpv wrappers, only libc) 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
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 them as
`flatpak run` options, so they apply to launches from that entry and are
gone with it. From a terminal:
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:
```sh
flatpak run --branch=stable --arch=aarch64 --command=jellyfin-desktop \
@@ -60,11 +40,10 @@ 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: `grep ^Exec=
~/.local/share/applications/org.jellyfin.JellyfinDesktop.desktop`, or the
read-only option `steamFrame.jellyfin.hardwareDecoding.command`. In its
output: `mpv-hwdec-shim: hwdec "auto-copy" -> "v4l2m2m-copy,auto-copy"`,
then mpv's `Using hardware decoding (v4l2m2m-copy)`.
(The exact line with the store path is the read-only option
`steamFrame.jellyfin.hardwareDecoding.command`.)
Older versions used a Flatpak override (via nix-flatpak or a Home Manager
link); `steam-frame-nix-cleanup` removes their entries from it.
link) and a shim copy in `~/.var/app/org.jellyfin.JellyfinDesktop`;
`steam-frame-nix-cleanup` removes their entries from the override and the
copy.
+40 -117
View File
@@ -1,133 +1,56 @@
# Keyboard
# VR keyboard: how it works
Keyboard layout of the Steam session, and two patches of Steam's VR
keyboard: [extra keys](#extra-keys) and [swipe and suggestions](#swipe-and-suggestions).
All options: [options.md#keyboard](options.md#keyboard).
## Layout
`keyboard.layout`, `keyboard.variant`.
**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.
**Fix:** a drop-in on `gamescope-session.service` setting
`XKB_DEFAULT_LAYOUT`/`VARIANT`; applies at the next Steam session start.
`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>`.
**Remove when** SteamOS applies a layout setting to gamescope.
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).
## Extra keys
`keyboard.vr.extraKeys.enable`.
`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 it does:**
- Bottom row: `Esc Ctrl Alt [Space] AltGr ← ↑ ↓ → Close`, stable with Shift
or AltGr.
- 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.
- 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.
- 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 while the
keyboard is open (e.g. Ctrl+scroll).
- Problem characters are typed with `xdotool type`; everything else goes
through Steam.
- Enter always types Return (stock Steam may send it to a Steam search box).
the VR-selected window); a toggled Ctrl/Alt is held down with `xdotool`
while the keyboard is open.
- 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.
**Layouts:** the character routing targets the German keymap; on others it
is harmless (those characters are typed by xdotool), and Esc/Ctrl/Alt/arrows
work regardless.
**Security:** the helper 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.
**How it works:** the `steam-keyboard-patch` user service (`helper.mjs`)
injects a patch over DevTools (`127.0.0.1:8080`) and re-injects it after
Steam restarts; stopping it (or disabling the option) reverts the patch. No
reboot or Steam restart needed.
**Caveat:** 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)). Tested with
Steam client 1790377368 (UI build 11041156).
**Remove when** Steam's VR keyboard gets these keys.
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)).
## Swipe and suggestions
`keyboard.vr.enable`; the sub-features (`swipe`, `autocorrect`,
`completions`, `backspaceDrag`, `haptics`) are on by default.
`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.
- 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`).
- 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.
**What it does:**
- **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.
- **Haptics:** light ticks for drag steps and picks, a Snap at word detents.
**Dictionary** (`dictionary.*`): built from wordfreq frequency lists and
Hunspell, both from nixpkgs. By default the `keyboard.layout` language (de,
fr, es, it, nl, pt, sv) plus English, else English only; add or exclude
words and word lists, see [options](options.md#keyboard).
**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' xdotool keys, another field,
`text.resetAfterIdleSeconds`) resets that, and suggestions only replace text
the memory proves intact. Works with and without `keyboard.vr.extraKeys`
(with it, non-ASCII words are typed via its xdotool helper).
**Caveats:** found by signature (entries `vr-keyboard`, `vr-keyboard-panel`);
if one stops matching the keyboard stays stock. Accented words of other
languages are in the dictionary but only swipable where the layout has the
letters.
**How it works:** a Steam UI patch (`vr-keyboard`, injected like the other
[UI patches](ui-patches.md)). Words are matched by shape (SHARK2-style
template matching). The strip below/above is a patch of SteamVR's
`systemui`, with the `vr-keyboard-relay` user service carrying it between
the two pages.
Found by signature (entries `vr-keyboard`, `vr-keyboard-panel`); if one
stops matching the keyboard stays stock.
**Tests:** `nix flake check` (checks `vr-keyboard`: text model, corrector,
decoder accuracy on German + English) and `keyboard.vr.checks` for the
configured dictionary. Debugging: `window.__sfuiSwipeLog` and
`__sfuiSwipePaths` in Steam's SharedJSContext (replay swipes with
`scripts/vr-keyboard-replay.mjs`).
configured dictionary.
**Remove when** Steam's VR keyboard gets swipe typing and suggestions.
**Debugging:** `window.__sfuiSwipeLog` and `__sfuiSwipePaths` in Steam's
SharedJSContext (replay swipes with `scripts/vr-keyboard-replay.mjs`).
+26 -60
View File
@@ -1,78 +1,44 @@
# Launcher menu
# Launcher menu: how it works
The VR dashboard's "+" menu (non-Steam programs). Options:
[options.md#launcher-menu](options.md#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).
## Menu patches
`launcherMenu.*`.
`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):
**Problem:** 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.
- `order/`: sorts `ScanForInstalledNonSteamApps()` by name (Steam lists the
programs in GLib hash-table order);
- `pinned-desktop/`: hides Desktop in the list and pins a proxy above/below
it;
- `launch/`: wraps `LaunchNonSteamApp` (menu-only) to close the menu and/or
debounce repeated launches;
- `grid/`: restyles Steam's own items as tiles, which is why launching,
sounds and controller navigation keep working;
- `show-all/`: empties the list Steam hides without Developer Mode.
**What it does:**
- `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. Only restyles Steam's items, so
launching, sounds and controller navigation keep working. 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`: without Developer Mode Steam hides `konsole`,
`systemsettings`, `dolphin`, `plasma-discover`, `vlc`, `firewall-config`,
`cmake-gui`, `qrenderdoc`, `lxterminal` and `sh`; this lifts that filter
only, so Developer Mode (sshd, xrdp, LAN DevTools forwards) can stay off.
Hide single programs with [`hiddenApps`](#hidden-apps).
**Steam Developer Mode** (a Steam setting, not managed here) also makes the
menu list every desktop entry; `showAllApps` does the same without it.
**Limitation:** the pinned Desktop works with the laser but not with
thumbstick / D-pad navigation.
**How it works:** [UI patches](ui-patches.md) in Steam's `SharedJSContext`.
All revert when turned off (next switch). The anchors (APIs, React props,
CSS) are verified by the offline checker. Tested with Steam client
1790377368.
Their anchors (APIs, React props, CSS) are in `modules/lib/signatures.json`
and verified by the [offline checker](ui-patches.md#after-a-steam-update).
## Hidden apps
`launcherMenu.hiddenApps`: desktop entry ids (no `.desktop`).
**Problem:** with Developer Mode or `showAllApps`, the "+" menu lists every
desktop entry, including system tools.
**Fix:** a user entry with `Hidden=true` in `~/.local/share/applications`
masks the system one (also in the KDE menu). The "+" menu always hides
`steam` and `vrurlhandler`; for Konsole in VR use
[`showAllApps`](#menu-patches).
`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.
## Icon fallbacks
`launcherMenu.iconFallbacks.enable` (on by default), `iconFallbacks.extra`.
**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.
**Fix:** Home Manager links nixpkgs' Breeze SVGs of `utilities-terminal`
and `preferences-system` (plus `extra`) into
`~/.local/share/icons/hicolor/scalable/apps/`; a name Breeze doesn't have
fails the build, `enable = false` provides none. When the set of links
`launcherMenu.iconFallbacks.*`: 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).
Each switch also prints hints: icons of programs Steam can't find that
Breeze has (add them to `extra`), and fallbacks hicolor has anyway.
The switch's hints come from `icon-fallbacks.sh`.
**Migration:** until 2026-09 a script made these links on switch and listed
them in `~/.local/state/steam-frame-nix/icon-fallbacks`; the first switch
replaces them with Home Manager's and `steam-frame-nix-cleanup` removes the
rest. `iconFallbacks` used to be a list; a list now fails with a hint (use
`extra`, or `enable = false` for `[ ]`).
rest.
-168
View File
@@ -1,168 +0,0 @@
# Options
All options live under `steamFrame.*`. The portal fix and clipboard sync
are on by default; everything else is opt-in.
## Session
Details: [desktop-integration.md](desktop-integration.md).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.session.runtimeDir` | str | `"/run/user/1000"` | `XDG_RUNTIME_DIR` of the outer (Steam/VR) session. |
| `steamFrame.session.bus` | str | `"unix:path=${runtimeDir}/bus"` | Outer session D-Bus (user manager, `kwalletd6`). |
| `steamFrame.session.busEnv` | str, read-only | `"env DBUS_SESSION_BUS_ADDRESS=${bus}"` | Prefix for launchers that must use the outer bus. |
| `steamFrame.session.services.start` | list of str | `[ ]` | User units started on switch if not running. |
| `steamFrame.session.services.restart` | list of str | `[ ]` | User units restarted on every switch. |
| `steamFrame.session.services.stop` | list of str | `[ ]` | User units stopped on switch if running (e.g. of a disabled feature). |
| `steamFrame.session.portalFix.enable` | bool | `true` | Working portal config (OpenURI) for the Steam session. |
## Keyboard
Details: [keyboard.md](keyboard.md).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.keyboard.layout` | null or str | `null` | XKB layout for the Steam session, e.g. `"de"`; `null`: US. |
| `steamFrame.keyboard.variant` | null or str | `null` | XKB variant for the Steam session, e.g. `"nodeadkeys"`; see [Keyboard layout](keyboard.md#layout). |
| `steamFrame.keyboard.vr.extraKeys.enable` | bool | `false` | VR keyboard with Esc/Ctrl/Alt, arrows, real chords, AltGr/non-ASCII. |
| `steamFrame.keyboard.vr.enable` | bool | `false` | Swipe typing, suggestions and Backspace drag on the VR keyboard; the sub-features below are on by default, see [VR keyboard](keyboard.md#swipe-and-suggestions). |
| `steamFrame.keyboard.vr.swipe.enable` | bool | `true` | Swipe typing. |
| `steamFrame.keyboard.vr.dictionary.languages` | list of submodules | layout language + English | `{ language; hunspell; words; frequencyOffset; keepFrequentAbove; }`: wordfreq language, `pkgs.hunspellDicts` name (or `null`), most frequent words taken, zipf offset, keep words Hunspell rejects from this zipf on (default `4.0`). Default: the `keyboard.layout` language (de, fr, es, it, nl, pt, sv; 60000) + English (40000, `-0.3`), else English (60000). |
| `steamFrame.keyboard.vr.dictionary.contractions` | bool | `true` | Words with apostrophes (`couldn't`, `geht's`), swiped by their letters. |
| `steamFrame.keyboard.vr.dictionary.extraWords` | list of str | `[ ]` | Words always included, casing as given. |
| `steamFrame.keyboard.vr.dictionary.extraWordsFrequency` | number | `5.0` | Zipf frequency of extra words. |
| `steamFrame.keyboard.vr.dictionary.extraWordFiles` | list of paths | `[ ]` | Word lists, `word` or `word<TAB>zipf` per line. |
| `steamFrame.keyboard.vr.dictionary.excludeWords` | list of str | `[ ]` | Words never suggested. |
| `steamFrame.keyboard.vr.text.bufferChars` | int | `128` | Characters of typed text the keyboard remembers. |
| `steamFrame.keyboard.vr.text.resetAfterIdleSeconds` | int | `30` | Forget it after this long without typing (`0`: never). |
| `steamFrame.keyboard.vr.text.autoSpace` | bool | `true` | Space before a swiped word after a known non-space character. |
| `steamFrame.keyboard.vr.suggestions.position` | `"below"`, `"above"`, `"inside"` | `"above"` | Suggestion strip: SteamVR panel below/above the keyboard, or over its number row. |
| `steamFrame.keyboard.vr.suggestions.count` | int | `6` | Suggestions shown. |
| `steamFrame.keyboard.vr.autocorrect.enable` | bool | `true` | Correction suggestions for finished tapped words not in the dictionary. |
| `steamFrame.keyboard.vr.autocorrect.maxEditDistance` | int | `2` | Largest edit distance (neighbouring keys and swaps count 0.5). |
| `steamFrame.keyboard.vr.completions.enable` | bool | `true` | Completions of the tapped word. |
| `steamFrame.keyboard.vr.completions.minPrefix` | int | `2` | Letters typed before completions show. |
| `steamFrame.keyboard.vr.backspaceDrag.enable` | bool | `true` | Backspace drag: left deletes, back right retypes. |
| `steamFrame.keyboard.vr.backspaceDrag.pixelsPerChar` | int | `25` | Travel per character (keyboard px; a key is ~60). |
| `steamFrame.keyboard.vr.backspaceDrag.wordDetentPixels` | int | `90` | Extra travel across a word border (`0`: none). |
| `steamFrame.keyboard.vr.haptics` | bool | `true` | Haptic ticks for drag steps, word detents and picks. |
| `steamFrame.keyboard.vr.checks` | package, read-only | | The tests, built with the configured dictionary. |
## UI patches
Details: [ui-patches.md](ui-patches.md).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.uiPatches.patches` | list of submodules | `[ ]` | Runtime patches of Steam's web UIs, see [UI patches](ui-patches.md). |
| `steamFrame.uiPatches.lib` | attrs, read-only | | Patch helpers (`mkPatch`), see [Finders and signatures](ui-patches.md#mkpatch). |
## Launcher menu
Details: [launcher-menu.md](launcher-menu.md).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.launcherMenu.sort` | bool | `false` | Sort the "+" menu alphabetically. |
| `steamFrame.launcherMenu.pinDesktop` | null or `"top"` / `"bottom"` | `null` | Pin "Desktop" above/below the "+" menu's list; `null`: normal entry. |
| `steamFrame.launcherMenu.closeOnLaunch` | bool | `false` | Close the "+" menu when a program is clicked. |
| `steamFrame.launcherMenu.launchDebounceSeconds` | unsigned int (s) | `0` | Ignore repeat launches of a program within this time; `0`: off. |
| `steamFrame.launcherMenu.grid.enable` | bool | `false` | Show the "+" menu's programs as a grid of tiles. |
| `steamFrame.launcherMenu.grid.columns` | int, 1-8 | `4` | Tiles per row (3 ≈ 92 px, 4 ≈ 68 px, 5 ≈ 53 px). |
| `steamFrame.launcherMenu.grid.maxRows` | null or positive int | `null` | Visible rows, the rest scrolls; `null`: up to 600 px. |
| `steamFrame.launcherMenu.showAllApps` | bool | `false` | List all programs without Developer Mode, see [Launcher menu](launcher-menu.md#menu-patches). |
| `steamFrame.launcherMenu.iconFallbacks.enable` | bool | `true` | Breeze icons of Konsole and KDE System Settings in hicolor, so the "+" menu shows them, see [Icon fallbacks](launcher-menu.md#icon-fallbacks). |
| `steamFrame.launcherMenu.iconFallbacks.extra` | list of str | `[ ]` | Further Breeze app icon names to provide (a name Breeze lacks fails the build). |
| `steamFrame.launcherMenu.hiddenApps` | list of str | `[ ]` | Desktop entry ids (no `.desktop`) hidden from the "+" and KDE menus. |
## Dashboard
Details: [dashboard.md](dashboard.md).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.dashboard.windows.maxScale` | null or positive number | `null` | Max resize scale of dashboard windows; `null`: stock (2), see [Dashboard windows](dashboard.md#dashboard-windows). |
| `steamFrame.dashboard.windows.distance.{world,theater,dashboard}.{min,max}` | null or positive number (m) | `null` | Pull-in / push-back limits of grabbed windows; `null`: stock (world 0.25-5, theater 1-6, dashboard 0.3-4 m). |
| `steamFrame.dashboard.steamCloseButton.enable` | bool | `false` | X button on the dashboard's Steam window, see [Steam close button](dashboard.md#steam-close-button). |
| `steamFrame.dashboard.windowCurvature.enable` | bool | `false` | Adjustable curvature per window, see [Window curvature](dashboard.md#window-curvature). |
| `steamFrame.dashboard.windowCurvature.initial` | non-negative number | `1.0` | Curvature of curved world/hand windows without own value (1 = stock, 0 = flat). |
| `steamFrame.dashboard.windowCurvature.max` | positive number | `3.0` | Largest curvature. |
| `steamFrame.dashboard.windowCurvature.step` | positive number | `0.05` | Rounding step while dragging (at most `max`). |
| `steamFrame.dashboard.windowCurvature.detentPixels` | unsigned int (px) | `24` | Detent at each detent point in drag pixels: the value holds there, then continues (nothing skipped); `0`: none. |
| `steamFrame.dashboard.windowCurvature.detentPoints` | list of non-negative numbers | `[ 0 1.0 ]` | Detent points (flat, stock), at most `max`. |
| `steamFrame.dashboard.windowCurvature.dragThresholdPixels` | unsigned int (px) | `8` | Vertical travel before a press becomes a drag. |
| `steamFrame.dashboard.windowCurvature.dragPixelsPerUnit` | positive number (px) | `120` | Drag distance per 1.0 in the menu (6 px per 0.05 step). |
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | positive number (px) | `60` | Drag distance per 1.0 on the bar button. |
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging (steps, detents, edges); the dashboard's hover clicks are muted during a drag. |
| `steamFrame.dashboard.frameControls.enable` | bool | `false` | Move window controls between bar and three-dot menu, see [Window control bar](dashboard.md#window-control-bar). |
| `steamFrame.dashboard.frameControls.longPressMs` | int, 300-10000 (ms) | `1500` | Long-press duration. |
| `steamFrame.dashboard.frameControls.inBar` | list of control names | `[ ]` | Controls that start in the bar: `keyboard`, `float`, `dashboard`, `theater`, `dockLeft`, `dockRight`, `close`, `curvature`, `"icon:<n>"`. |
| `steamFrame.dashboard.frameControls.inMenu` | list of control names | `[ ]` | Controls that start in the three-dot menu. |
| `steamFrame.dashboard.frameControls.floatInTheater` | bool | `false` | "Float" control on theater windows. |
## SteamVR debugger
Details: [steamvr-debugger.md](steamvr-debugger.md).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.steamvrDebugger.enable` | bool | automatic | SteamVR dashboard DevTools on `127.0.0.1:8087` (set only while SteamVR runs); on when a dashboard patch is, see [SteamVR debugger](steamvr-debugger.md). |
## Clipboard sync
Details: [desktop-integration.md](desktop-integration.md#clipboard-sync).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
| `steamFrame.clipboardSync.package` | package | built from `dnut/clipboard-sync` | The clipboard-sync package. |
## Firefox
Details: [firefox.md](firefox.md).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.firefox.enable` | bool | `false` | Launcher for the Flathub Firefox Flatpak with the fixes below. |
| `steamFrame.firefox.vrFullscreenFix` | bool | `true` | Default `full-screen-api.ignore-widgets` to `true` (not in the desktop profile). |
| `steamFrame.firefox.disableAv1` | bool | `false` | Default `media.av1.enabled` to `false`: the Frame's decoder driver has no AV1, so sites send VP9/H.264, decoded in hardware. |
| `steamFrame.firefox.prefs` | attrs of bool, int or str | `{ }` | Further `about:config` default values for every profile (override the fixes too). |
| `steamFrame.firefox.desktopProfile` | null or str | `"desktop"` | Separate profile (directory name) for the nested desktop; `null`: the default profile in both sessions. |
## Jellyfin
Details: [jellyfin.md](jellyfin.md).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.jellyfin.hardwareDecoding.enable` | bool | `false` | Hardware video decoding in the Jellyfin Desktop Flatpak, see [Jellyfin](jellyfin.md). |
| `steamFrame.jellyfin.hardwareDecoding.hwdec` | str | `"v4l2m2m-copy,auto-copy"` | mpv `hwdec` used instead of Jellyfin's automatic one. |
| `steamFrame.jellyfin.hardwareDecoding.command` | str, read-only | | The `flatpak run …` command line of the desktop entry, for a terminal. |
## Cleanup
Details: [changes-outside-nix.md](changes-outside-nix.md#cleanup).
| Option | Type | Default | Description |
|---|---|---|---|
| `steamFrame.cleanup.package` | package, read-only | | `steam-frame-nix-cleanup` (on `PATH` too), see [Changes outside Nix](changes-outside-nix.md#cleanup). |
## Renamed options
Renamed options still work under their old names, with a warning:
| Old | New |
|---|---|
| `keyboardLayout`, `keyboardVariant` | `keyboard.layout`, `keyboard.variant` |
| `steamKeyboardPatch.enable` | `keyboard.vr.extraKeys.enable` |
| `hiddenApps` | `launcherMenu.hiddenApps` |
| `launcherMenu.launchDebounce` | `launcherMenu.launchDebounceSeconds` |
| `runtimeDir`, `userBus`, `outerBusEnv` | `session.runtimeDir`, `session.bus`, `session.busEnv` |
| `userServices.{start,restart,stop}` | `session.services.{start,restart,stop}` |
| `portalFix.enable` | `session.portalFix.enable` |
| `dashboard.windowMaxScale` | `dashboard.windows.maxScale` |
| `dashboard.windowDistance.*` | `dashboard.windows.distance.*` |
| `dashboard.windowCurvature.default` | `dashboard.windowCurvature.initial` |
| `dashboard.windowCurvature.snapPixels`, `snapPoints` | `detentPixels`, `detentPoints` |
| `dashboard.windowCurvature.dragThreshold` | `dragThresholdPixels` |
+21 -21
View File
@@ -1,18 +1,22 @@
# SteamVR debugger
# SteamVR debugger: how it works
`steamvrDebugger.enable`, automatic: on when any patch in
`steamFrame.uiPatches.patches` uses port 8087 (all [dashboard](dashboard.md)
patches and the VR keyboard's strip below/above the keyboard).
`steamvrDebugger.enable` (module `steamvr-debugger`). What it is for and
what you need to do: README,
[SteamVR debugger](../README.md#steamvr-debugger-steamvrdebuggerenable).
**Problem:** dashboard patches need SteamVR's DevTools port, opened 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.
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).
**What it does:** sets the key only while SteamVR runs:
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:
- before every SteamVR start, the `steamvr-webhelper-debugger` oneshot
(a drop-in on `steamvr.service`) stores the key's value in
(a drop-in on `steamvr.service`, running `install.sh steamvr-debugger-arm`)
stores the key's value in
`~/.local/state/steam-frame-nix/steamvr-debugger.armed`, sets the key
(with `jq`) and writes a runtime drop-in,
`/run/user/1000/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf`,
@@ -22,15 +26,11 @@ memory, so the key can't be a link and can't be edited while SteamVR runs.
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. A key you set to `true` yourself is
never touched. Disabled, there is no unit; the switch restores the key if
SteamVR is stopped, otherwise the runtime drop-in does when it stops.
uninstall; 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.
**The first time, restart SteamVR once** (e.g. reboot); until then
`steam-ui-patches` keeps polling.
**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)).
What it writes and when it is removed:
[changes outside Nix](changes-outside-nix.md).
Until SteamVR has been restarted once with the drop-in, the port is closed
and `steam-ui-patches` keeps polling it.
-33
View File
@@ -1,33 +0,0 @@
# Two sessions
The Frame runs two graphical sessions at once; most workarounds exist because
of their differences:
| | Steam / VR session | Nested Plasma desktop |
|---|---|---|
| Compositor | gamescope | KWin (nested, shown as a VR window) |
| Displays | X display `:0` (apps show as floating VR windows) | own Wayland + Xwayland `:2` |
| D-Bus | the outer session bus, `/run/user/1000/bus` | a private bus |
| `XDG_RUNTIME_DIR` | `/run/user/1000` | its own |
| systemd user manager | yes | not reachable |
Consequences:
- **Wallet:** there should be one `kwalletd6`, on the outer bus; apps started
from the desktop would otherwise start a second one whose secrets VR can't
see. Prefix launchers' `Exec=` with `steamFrame.session.busEnv`
([session settings](desktop-integration.md#session-settings-and-services)).
- **User services:** home-manager skips `reloadSystemd` when switching from
the desktop terminal, so `steamFrame.session.services` talks to the outer
user manager directly.
- **Launchers:** the "+" menu only sees `~/.local/share/applications` (not
`~/.nix-profile/share`), so entries are written there, shadowing
Flatpak/package entries with the same ID.
- **Keyboard layout:** KDE's layout only affects the nested desktop
([keyboard layout](keyboard.md#layout)).
- **Clipboard:** separate per session ([clipboard sync](desktop-integration.md#clipboard-sync)).
- **Firefox:** the sessions can't see each other's Firefox, so a second
instance stops at the locked profile ([desktop profile](firefox.md#options)).
Switch (`home-manager switch`) from a terminal **in the nested desktop**, so
clipboard-sync restarts with the desktop's environment.
+10 -16
View File
@@ -1,7 +1,8 @@
# UI patches
For patch authors, and for fixing patches after a Steam update. Options:
[options.md#ui-patches](options.md#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`.
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
@@ -9,8 +10,12 @@ 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#swipe-and-suggestions)
features are such patches, and you can add your own.
[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.
Don't expose these ports: see
[DevTools on the LAN](../README.md#devtools-on-the-lan).
**Caveat:** patches depend on Steam UI internals and can break with an
update; find modules by signature, not id ([below](#finders-and-signatures)).
@@ -66,18 +71,7 @@ Used by the [window control bar](dashboard.md#window-control-bar) and the
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](changes-outside-nix.md).
## 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.
[changes outside Nix](../README.md#changes-outside-nix-exceptions).
## Finders and signatures
+1 -1
View File
@@ -20,7 +20,7 @@
# trace after a power loss) is resolved by the next start or
# steam-frame-nix-cleanup.
# Off: no unit and no drop-in; cleanup restores the key once SteamVR is
# stopped. Developer Mode forwards the port to 0.0.0.0:8088 (docs/ui-patches.md,
# stopped. Developer Mode forwards the port to 0.0.0.0:8088 (README,
# "DevTools on the LAN"); our patches use 127.0.0.1 only.
{ config, lib, pkgs, ... }:
let