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

+75 -626
View File
@@ -18,87 +18,42 @@ for everything).
All options live under `steamFrame.*`. The portal fix and clipboard sync are
on by default; everything else is opt-in.
- [Features](#features) · [Install](#install) · [Two sessions](#two-sessions) ·
[Usage](#usage) · [Options](#options)
- [Fixes in detail](#fixes-in-detail):
[session](#session-settings-and-background-services-sessionnix),
[portal](#portal-sessionportalfix),
[keyboard layout](#keyboard-layout-keyboardlayout-keyboardvariant),
[VR keyboard extra keys](#steam-keyboard-patch-keyboardvrextrakeysenable),
[swipe and suggestions](#vr-keyboard-swipe-and-suggestions-keyboardvr),
[launcher menu](#launcher-menu-launchermenu),
[hidden apps](#hidden-apps-launchermenuhiddenapps),
[icon fallbacks](#icon-fallbacks-launchermenuiconfallbacks),
[dashboard windows](#dashboard-windows-dashboardwindows),
[Steam close button](#steam-close-button-dashboardsteamclosebuttonenable),
[window curvature](#window-curvature-dashboardwindowcurvature),
[window control bar](#window-control-bar-dashboardframecontrols),
[SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable),
[clipboard sync](#clipboard-sync-clipboardsyncenable),
[Firefox](#firefox-firefox),
[Jellyfin hardware decoding](#jellyfin-hardware-decoding-jellyfinhardwaredecoding)
- [UI patches](#ui-patches-uipatchespatches):
[DevTools on the LAN](#devtools-on-the-lan),
[after a Steam update](#after-a-steam-update)
- [Changes outside Nix](#changes-outside-nix-exceptions) ·
[Rollback](#rollback) · [Uninstall](#uninstall)
Technical documentation (how the patches work, writing your own, fixing them
after a Steam update): [`docs/`](docs).
## Features
Session and desktop:
One page per feature in [`docs/`](docs): problem, what you get,
configuration, limitations and how it works.
- [Portal fix](#portal-sessionportalfix) (`session.portalFix`, on): apps in
the Steam session can open links.
- [Clipboard sync](#clipboard-sync-clipboardsyncenable) (`clipboardSync`,
on): one clipboard for the Steam session and the nested desktop.
- [Session settings](#session-settings-and-background-services-sessionnix)
(`session.*`): outer bus and user services, for launchers and the other
modules.
**Session** ([docs/session.md](docs/session.md)):
Keyboard:
- [Session settings](docs/session.md#session-settings-and-services) (`session.*`): outer bus and user services, for launchers and the other modules.
- [Portal fix](docs/session.md#portal-fix) (`session.portalFix`, on): apps in the Steam session can open links.
- [Keyboard layout](docs/session.md#keyboard-layout) (`keyboard.layout`, `keyboard.variant`): XKB layout for the Steam session.
- [Clipboard sync](docs/session.md#clipboard-sync) (`clipboardSync`, on): one clipboard for the Steam session and the nested desktop.
- [Keyboard layout](#keyboard-layout-keyboardlayout-keyboardvariant)
(`keyboard.layout`, `keyboard.variant`): XKB layout for the Steam session.
- [Extra keys](#steam-keyboard-patch-keyboardvrextrakeysenable)
(`keyboard.vr.extraKeys`): Esc/Ctrl/Alt, arrows, Delete, real chords and
AltGr/non-ASCII characters on the VR keyboard.
- [Swipe and suggestions](#vr-keyboard-swipe-and-suggestions-keyboardvr)
(`keyboard.vr`): swipe typing, corrections, completions, Backspace drag.
**VR keyboard** ([docs/keyboard.md](docs/keyboard.md)):
VR "+" menu:
- [Extra keys](docs/keyboard.md#extra-keys) (`keyboard.vr.extraKeys`): Esc/Ctrl/Alt, arrows, Delete, real chords and AltGr/non-ASCII characters.
- [Swipe and suggestions](docs/keyboard.md#swipe-and-suggestions) (`keyboard.vr`): swipe typing, corrections, completions, Backspace drag.
- [Launcher menu](#launcher-menu-launchermenu) (`launcherMenu.*`): sorted,
Desktop pinned, closes on launch, no double launches, grid of tiles, all
programs without Developer Mode.
- [Hidden apps](#hidden-apps-launchermenuhiddenapps)
(`launcherMenu.hiddenApps`) and
[icon fallbacks](#icon-fallbacks-launchermenuiconfallbacks)
(`launcherMenu.iconFallbacks`, on) for Konsole and KDE System Settings.
**VR "+" menu** ([docs/launcher-menu.md](docs/launcher-menu.md)):
SteamVR dashboard (turns on the
[SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable) automatically):
- [Launcher menu](docs/launcher-menu.md) (`launcherMenu.*`): sorted, Desktop pinned, closes on launch, no double launches, grid of tiles, all programs without Developer Mode.
- [Hidden apps](docs/launcher-menu.md#hidden-apps) (`launcherMenu.hiddenApps`) and [icon fallbacks](docs/launcher-menu.md#icon-fallbacks) (`launcherMenu.iconFallbacks`, on) for Konsole and KDE System Settings.
- [Dashboard windows](#dashboard-windows-dashboardwindows)
(`dashboard.windows`): larger max scale and push-back distance.
- [Steam close button](#steam-close-button-dashboardsteamclosebuttonenable)
(`dashboard.steamCloseButton`): an X that hides the Steam window.
- [Window curvature](#window-curvature-dashboardwindowcurvature)
(`dashboard.windowCurvature`): adjustable curvature per window.
- [Window control bar](#window-control-bar-dashboardframecontrols)
(`dashboard.frameControls`): move controls between bar and three-dot menu.
**SteamVR dashboard** ([dashboard patches](docs/ui-patches.md#steamvr-dashboard-patches)):
Apps:
- [Dashboard windows](docs/dashboard-windows.md) (`dashboard.windows`): larger max scale and push-back distance.
- [Steam close button](docs/steam-close-button.md) (`dashboard.steamCloseButton`): an X that hides the Steam window.
- [Window curvature](docs/window-curvature.md) (`dashboard.windowCurvature`): adjustable curvature per window.
- [Window control bar](docs/window-control-bar.md) (`dashboard.frameControls`): move controls between bar and three-dot menu.
- [SteamVR debugger](docs/steamvr-debugger.md) (`steamvrDebugger`, automatic): SteamVR's DevTools port for these, set only while SteamVR runs.
- [Firefox](#firefox-firefox) (`firefox`): launcher for the Flatpak with a VR
fullscreen fix, optional AV1 off, a separate desktop profile.
- [Jellyfin](#jellyfin-hardware-decoding-jellyfinhardwaredecoding)
(`jellyfin.hardwareDecoding`): hardware video decoding in the Jellyfin
Desktop Flatpak.
**Apps:**
Your own patches of Steam's UI: [UI patches](#ui-patches-uipatchespatches).
- [Firefox](docs/firefox.md) (`firefox`): launcher for the Flatpak with a VR fullscreen fix, optional AV1 off, a separate desktop profile.
- [Jellyfin](docs/jellyfin.md) (`jellyfin.hardwareDecoding`): hardware video decoding in the Jellyfin Desktop Flatpak.
**Your own patches** of Steam's UI, and fixing patches after a Steam update: [UI patches](docs/ui-patches.md).
## Install
@@ -113,22 +68,13 @@ The short link redirects to
[`install.sh`](https://raw.githubusercontent.com/lhns/steam-frame-nix/main/install.sh)
on `main`. `sudo` needs a password: run `passwd` first if you never set one.
The installer:
1. installs Nix with the [NixOS nix-installer](https://github.com/NixOS/nix-installer)
(`steam-deck` planner: store in `/home/nix`, survives SteamOS updates;
flakes on), unlocking the read-only root only for the install. Skipped if
Nix already works;
2. uses `~/.config/home-manager` or `--flake <dir-or-flakeref>`; if there is
none, creates `~/nix-config` (a git repo, linked to
`~/.config/home-manager`) from the [template](template) with your user
name;
3. runs `home-manager switch`; conflicting dotfiles are renamed to
`*.hm-backup-<time>`.
Re-running it just switches again (`--yes` answers every question).
Afterwards edit `~/nix-config/home.nix` and run `home-manager switch` from a
terminal in the nested desktop (see [Usage](#usage)).
The installer installs Nix (skipped if Nix already works), uses
`~/.config/home-manager` or `--flake <dir-or-flakeref>` (if there is none, it
creates `~/nix-config` from the [template](template) with your user name) and
runs `home-manager switch`; what it sets up is listed under
[Set up by install.sh](#set-up-by-installsh). Re-running it just switches
again (`--yes` answers every question). Afterwards edit
`~/nix-config/home.nix` and switch (see [Usage](#usage)).
```sh
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- status # Nix, generation, services
@@ -137,9 +83,7 @@ curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all # see Ch
```
`bash -s -- --help` lists all commands and flags. See
[Uninstall](#uninstall) for what `uninstall` removes and keeps, and
[Set up by install.sh](#set-up-by-installsh) for everything the installer
changes.
[Uninstall](#uninstall) for what `uninstall` removes and keeps.
**Manual setup** (Nix with flakes and standalone home-manager):
`nix flake init -t github:lhns/steam-frame-nix` creates a commented
@@ -164,18 +108,18 @@ What this means for you:
- **Switch from the nested desktop:** run `home-manager switch` in a
terminal there, so clipboard-sync restarts with the desktop's
environment. User services are handled for you
([session settings](#session-settings-and-background-services-sessionnix)).
([session settings](docs/session.md#session-settings-and-services)).
- **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 such launchers' `Exec=` with `steamFrame.session.busEnv`
(example under [Usage](#usage)).
([example](docs/session.md#session-settings-and-services)).
- **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, clipboard, Firefox:** each session has its own; see
[keyboard layout](#keyboard-layout-keyboardlayout-keyboardvariant),
[clipboard sync](#clipboard-sync-clipboardsyncenable),
[Firefox](#firefox-firefox) (desktop profile).
[keyboard layout](docs/session.md#keyboard-layout),
[clipboard sync](docs/session.md#clipboard-sync),
[Firefox](docs/firefox.md#desktop-profile) (desktop profile).
## Usage
@@ -213,7 +157,7 @@ these two files ([`template/`](template), with more comments):
```
```nix
# home.nix
# home.nix (the template has steamFrame commented out)
{ config, pkgs, username, homeDirectory, ... }: {
home.username = username;
home.homeDirectory = homeDirectory;
@@ -248,19 +192,10 @@ these two files ([`template/`](template), with more comments):
firefox.disableAv1 = true;
jellyfin.hardwareDecoding.enable = true; # install the Flatpak yourself
};
# Example: an app that must use the single wallet on the outer bus.
# 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
# '';
}
```
Switch from a terminal **in the nested desktop** (so clipboard-sync restarts
with the desktop's environment):
Switch from a terminal in the nested desktop (see [Two sessions](#two-sessions)):
```sh
home-manager switch # config in ~/.config/home-manager (installer)
@@ -274,11 +209,6 @@ imports all modules; single ones:
Every module imports `cleanup` (see
[Changes outside Nix](#changes-outside-nix-exceptions)).
**Steam Developer Mode** (a Steam setting, not managed here) makes the "+"
menu list every desktop entry; `launcherMenu.showAllApps` does the same
without it. No feature needs Developer Mode; keep it off (see
[DevTools on the LAN](#devtools-on-the-lan)).
## Options
| Option | Type | Default | Description |
@@ -291,9 +221,9 @@ without it. No feature needs Developer Mode; keep it off (see
| `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. |
| `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-layout-keyboardlayout-keyboardvariant). |
| `steamFrame.keyboard.variant` | null or str | `null` | XKB variant for the Steam session, e.g. `"nodeadkeys"`; see [Keyboard layout](docs/session.md#keyboard-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](#vr-keyboard-swipe-and-suggestions-keyboardvr). |
| `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](docs/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. |
@@ -315,7 +245,7 @@ without it. No feature needs Developer Mode; keep it off (see
| `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. |
| `steamFrame.uiPatches.patches` | list of submodules | `[ ]` | Runtime patches of Steam's web UIs, see [UI patches](#ui-patches-uipatchespatches). |
| `steamFrame.uiPatches.patches` | list of submodules | `[ ]` | Runtime patches of Steam's web UIs, see [UI patches](docs/ui-patches.md). |
| `steamFrame.uiPatches.lib` | attrs, read-only | | Patch helpers (`mkPatch`), see [Finders and signatures](docs/ui-patches.md#mkpatch). |
| `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. |
@@ -324,14 +254,14 @@ without it. No feature needs Developer Mode; keep it off (see
| `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-launchermenu). |
| `steamFrame.launcherMenu.iconFallbacks.enable` | bool | `true` | Breeze icons of Konsole and KDE System Settings in hicolor, so the "+" menu shows them, see [Icon fallbacks](#icon-fallbacks-launchermenuiconfallbacks). |
| `steamFrame.launcherMenu.showAllApps` | bool | `false` | List all programs without Developer Mode, see [Launcher menu](docs/launcher-menu.md#all-programs-and-developer-mode). |
| `steamFrame.launcherMenu.iconFallbacks.enable` | bool | `true` | Breeze icons of Konsole and KDE System Settings in hicolor, so the "+" menu shows them, see [Icon fallbacks](docs/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. |
| `steamFrame.dashboard.windows.maxScale` | null or positive number | `null` | Max resize scale of dashboard windows; `null`: stock (2), see [Dashboard windows](#dashboard-windows-dashboardwindows). |
| `steamFrame.dashboard.windows.maxScale` | null or positive number | `null` | Max resize scale of dashboard windows; `null`: stock (2), see [Dashboard windows](docs/dashboard-windows.md). |
| `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](#steam-close-button-dashboardsteamclosebuttonenable). |
| `steamFrame.dashboard.windowCurvature.enable` | bool | `false` | Adjustable curvature per window, see [Window curvature](#window-curvature-dashboardwindowcurvature). |
| `steamFrame.dashboard.steamCloseButton.enable` | bool | `false` | X button on the dashboard's Steam window, see [Steam close button](docs/steam-close-button.md). |
| `steamFrame.dashboard.windowCurvature.enable` | bool | `false` | Adjustable curvature per window, see [Window curvature](docs/window-curvature.md). |
| `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`). |
@@ -341,12 +271,12 @@ without it. No feature needs Developer Mode; keep it off (see
| `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](#window-control-bar-dashboardframecontrols). |
| `steamFrame.dashboard.frameControls.enable` | bool | `false` | Move window controls between bar and three-dot menu, see [Window control bar](docs/window-control-bar.md). |
| `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. |
| `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-steamvrdebuggerenable). |
| `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](docs/steamvr-debugger.md). |
| `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. |
| `steamFrame.firefox.enable` | bool | `false` | Launcher for the Flathub Firefox Flatpak with the fixes below. |
@@ -354,7 +284,7 @@ without it. No feature needs Developer Mode; keep it off (see
| `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. |
| `steamFrame.jellyfin.hardwareDecoding.enable` | bool | `false` | Hardware video decoding in the Jellyfin Desktop Flatpak, see [Jellyfin](#jellyfin-hardware-decoding-jellyfinhardwaredecoding). |
| `steamFrame.jellyfin.hardwareDecoding.enable` | bool | `false` | Hardware video decoding in the Jellyfin Desktop Flatpak, see [Jellyfin](docs/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. |
| `steamFrame.cleanup.package` | package, read-only | | `steam-frame-nix-cleanup` (on `PATH` too), see [Changes outside Nix](#changes-outside-nix-exceptions). |
@@ -376,496 +306,6 @@ Renamed options still work under their old names, with a warning:
| `dashboard.windowCurvature.snapPixels`, `snapPoints` | `detentPixels`, `detentPoints` |
| `dashboard.windowCurvature.dragThreshold` | `dragThresholdPixels` |
## Fixes in detail
### Session settings and background services (`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
(`Exec=${config.steamFrame.session.busEnv} flatpak run …`, see
[Usage](#usage)).
- Switching from the nested desktop, Home Manager can't reach the service
manager ("User systemd daemon not running") and skips `reloadSystemd`. So
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).
### Portal (`session.portalFix`)
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`.
### 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>`.
The layout also picks the [VR keyboard](#vr-keyboard-swipe-and-suggestions-keyboardvr)'s
default dictionary language.
**Remove when** SteamOS applies a layout setting to gamescope.
### Steam keyboard patch (`keyboard.vr.extraKeys.enable`)
**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.
- 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 (with `xdotool` on
`:0`) 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 (see [After a Steam update](#after-a-steam-update)).
Tested with Steam client 1790377368 (UI build 11041156).
How it works: [docs/keyboard.md](docs/keyboard.md#extra-keys).
**Remove when** Steam's VR keyboard gets these keys.
### VR keyboard: swipe and suggestions (`keyboard.vr.*`)
**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-steamvrdebuggerenable), turned on
automatically.
- **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 words
(`extraWords`, `extraWordFiles`), remove some (`excludeWords`) or configure
the languages, see [Options](#options).
**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 [`keyboard.vr.extraKeys`](#steam-keyboard-patch-keyboardvrextrakeysenable)
(with it, non-ASCII words are typed via its helper).
**Caveats:** depends on Steam/SteamVR UI internals; after an update that
changes them the keyboard stays stock (see
[After a Steam update](#after-a-steam-update)). Accented words of other
languages are in the dictionary but only swipable where the layout has the
letters.
How it works, tests, debugging: [docs/keyboard.md](docs/keyboard.md#swipe-and-suggestions).
**Remove when** Steam's VR keyboard gets swipe typing and suggestions.
### Launcher menu (`launcherMenu.*`)
**Problem:** the VR dashboard's "+" menu (non-Steam programs) 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):
- `sort`: programs sorted by name (case-insensitive), Desktop included.
- `pinDesktop = "top"` / `"bottom"`: Desktop pinned above/below the list,
always visible. **Limitation:** the pinned copy works with the laser but
not with thumbstick / D-pad navigation.
- `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-launchermenuhiddenapps).
All revert when turned off (next switch). Tested with Steam client
1790377368. How it works: [docs/launcher-menu.md](docs/launcher-menu.md).
#### Hidden apps (`launcherMenu.hiddenApps`)
**Problem:** with Developer Mode or `showAllApps`, the "+" menu lists every
desktop entry, including system tools.
**Fix:** list desktop entry ids (no `.desktop`); 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`](#launcher-menu-launchermenu).
#### Icon fallbacks (`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.
**Fix:** 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. 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.
`iconFallbacks` used to be a list; a list now fails with a hint (use
`extra`, or `enable = false` for `[ ]`). How it works and migration from the
2026-09 script: [docs/launcher-menu.md](docs/launcher-menu.md#icon-fallbacks).
### SteamVR dashboard patches
The next four features patch SteamVR's dashboard while it runs. Each turns
on the [SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable)
(**the first time, restart SteamVR once**). They depend on SteamVR UI
internals: 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.
How they work and debugging: [docs/dashboard.md](docs/dashboard.md).
### Dashboard windows (`dashboard.windows.*`)
**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.
**Fix:** raises these 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.
```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:** if SteamVR changes its stock grab distances, the distance options
silently do nothing.
### Steam close button (`dashboard.steamCloseButton.enable`)
**Problem:** every dashboard window has a close (X) button except Steam's
own, and with no other window open the dashboard always shows it.
**Fix:** 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
[Changes outside Nix](#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.
**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.
### Window curvature (`dashboard.windowCurvature.*`)
**Problem:** SteamVR dashboard windows are either curved (fixed radius) or
flat, and world windows start flat.
**Fix:** 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-dashboardframecontrols)) 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.
### 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".
**Fix:**
- **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](#changes-outside-nix-exceptions)).
- `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-dashboardwindowcurvature), 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; the three-dot button itself can't be moved.
### SteamVR debugger (`steamvrDebugger.enable`)
Dashboard patches (and the VR keyboard's strip below/above the keyboard)
need SteamVR's DevTools port, which SteamVR opens only with its setting
`VRWebHelper/DebuggerEnabled`. It is enabled automatically when any patch in
`steamFrame.uiPatches.patches` uses port 8087; nothing to set.
The setting lives in `~/.config/openvr/config/steamvr.vrsettings`, which
SteamVR rewrites, so it can't be a Nix link. It 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](#devtools-on-the-lan)).
How it works: [docs/steamvr-debugger.md](docs/steamvr-debugger.md).
### 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.
### Firefox (`firefox.*`)
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): 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.
`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. 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.
How it works: [docs/firefox.md](docs/firefox.md).
### Jellyfin hardware decoding (`jellyfin.hardwareDecoding.*`)
For the Flathub [Jellyfin Desktop](https://github.com/jellyfin/jellyfin-desktop)
Flatpak (`org.jellyfin.JellyfinDesktop`), which plays video with libmpv.
**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 the Flatpak can't open (its `devices=dri` covers only
the GPU) and mpv never tries (Jellyfin hard-sets `hwdec=auto-copy`, whose
probing leaves out V4L2 M2M, and has no way to pass mpv options).
**Fix:** 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. Nothing is written to
Flatpak's overrides: it applies to launches from that entry and is gone with
it. Changes take effect at the next start of Jellyfin.
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" ];
```
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: [docs/jellyfin.md](docs/jellyfin.md).
## UI patches (`uiPatches.patches`)
Steam's UI and SteamVR's dashboard are web pages; the launcher menu,
dashboard and VR keyboard features patch them while they run, over their
local DevTools ports. Steam's files are never modified, and 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`.
You can add your own patches with `steamFrame.uiPatches.patches`; see
[docs/ui-patches.md](docs/ui-patches.md) (patch definition, persistent state,
finders and signatures, `mkPatch`, shared hooks).
### 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.
### After a Steam update
The patches find Steam's code by signature. If a Steam or SteamVR update
changes it so a signature no longer matches exactly once, that patch changes
nothing: the feature stays stock and the journal (above) 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. Checking and fixing signatures:
[docs/ui-patches.md](docs/ui-patches.md#after-a-steam-update).
## Changes outside Nix (exceptions)
Everything not listed here is a Home Manager link into the Nix store or
@@ -873,29 +313,26 @@ lives in memory (the UI patches). These are written at runtime:
| Path | Feature | Lifetime | Removed by |
|---|---|---|---|
| `VRWebHelper.DebuggerEnabled` in `~/.config/openvr/config/steamvr.vrsettings` | [SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable) | only while SteamVR runs | SteamVR stopping (runtime drop-in below); `steam-frame-nix-cleanup` while SteamVR is stopped |
| `VRWebHelper.DebuggerEnabled` in `~/.config/openvr/config/steamvr.vrsettings` | [SteamVR debugger](docs/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` | Saved choices 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](#icon-fallbacks-launchermenuiconfallbacks): a running Steam rescans icons | only the directory's timestamp | nothing to remove |
| `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | Saved choices of dashboard patches ([persistent state](docs/ui-patches.md#persistent-state)): 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](docs/launcher-menu.md#icon-fallbacks): a running Steam rescans icons | only the directory's timestamp | nothing to remove |
**`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.
safely; `--dry-run` shows what it would do. How it decides and what older
versions left: [docs/cleanup.md](docs/cleanup.md).
- 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`):
- Without Nix or after a rollback it runs from the script:
`curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all`.
- It also removes what older versions left (icon fallback links, Firefox
`user.js` files and their values, Jellyfin Flatpak override entries, ...;
list: [docs/changes-outside-nix.md](docs/changes-outside-nix.md#left-by-older-versions)).
### Only while running
@@ -903,18 +340,30 @@ safely; `--dry-run` shows what it would do.
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 [docs/firefox.md](docs/firefox.md#desktop-profile)).
Firefox runs (see [Firefox](docs/firefox.md#how-it-works)).
- Jellyfin: the hardware decoding permissions are `flatpak run` options of
the desktop entry, not a Flatpak override.
### Set up by install.sh
`install.sh install` (the bootstrap, not the modules) also installs Nix
(`/nix`, files in `/etc`), `~/nix-config` with the link
`~/.config/home-manager`, Nix's and Home Manager's per-user state, and
renames conflicting dotfiles to `*.hm-backup-<time>`; `install.sh uninstall`
undoes it (full list:
[docs/changes-outside-nix.md](docs/changes-outside-nix.md#set-up-by-installsh)).
`install.sh install` (the bootstrap, not the modules) also changes these, 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`), its receipt `/nix/receipt.json`; the read-only root is
unlocked only while it installs or uninstalls;
- `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 that exists or `--flake` is given;
uninstall removes the link, never the configuration;
- dotfiles in Home Manager's way, renamed to `*.hm-backup-<time>` (kept);
- `~/.local/state/home-manager`, `~/.local/state/nix` (profiles,
generations), `~/.nix-profile`, `~/.nix-defexpr`, `~/.nix-channels`,
`~/.cache/nix`.
### App data you create
-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.
+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 (README,
# stopped. Developer Mode forwards the port to 0.0.0.0:8088 (docs/ui-patches.md,
# "DevTools on the LAN"); our patches use 127.0.0.1 only.
{ config, lib, pkgs, ... }:
let