mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
docs: split feature details into docs/
One page per feature group (sessions, keyboard, launcher menu, dashboard, SteamVR debugger, Firefox, Jellyfin, UI patches, changes outside Nix) and the full options table in docs/options.md.
This commit is contained in:
1 parent
924ac6b82f
commit
13d7a437f8
12 files changed
+1095
-1
No files matched your search
@@ -0,0 +1,112 @@
|
||||
# Changes outside Nix
|
||||
|
||||
Everything not listed here is a Home Manager link into the Nix store or
|
||||
lives in memory (the [UI patches](ui-patches.md)).
|
||||
|
||||
## Written at runtime
|
||||
|
||||
| Path | Feature | Lifetime | Removed by |
|
||||
|---|---|---|---|
|
||||
| `VRWebHelper.DebuggerEnabled` in `~/.config/openvr/config/steamvr.vrsettings` | [SteamVR debugger](steamvr-debugger.md) | only while SteamVR runs | SteamVR stopping (runtime drop-in below); `steam-frame-nix-cleanup` while SteamVR is stopped |
|
||||
| `~/.local/state/steam-frame-nix/steamvr-debugger.armed` | SteamVR debugger: the key's previous value | while SteamVR runs; after a power loss until the next SteamVR start or cleanup | SteamVR stopping; `steam-frame-nix-cleanup` |
|
||||
| `/run/user/1000/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf`, `/run/user/1000/steam-frame-nix/steamvr-debugger-restore` | SteamVR debugger: puts the key back when SteamVR stops, without Nix | until reboot (tmpfs) | reboot; `steam-frame-nix-cleanup` while SteamVR is stopped and the debugger is off |
|
||||
| `~/.local/state/steam-frame-nix/ui-patches/<name>.json` | [persistent state](ui-patches.md#persistent-state) of dashboard patches: window control bar placements (`frame-controls`), "Steam hidden" (`steam-close-button`). SteamOS's `steamvr.service` deletes `~/.cache/SteamVR` (the dashboard's own browser storage) on every SteamVR start. | until removed: kept when a patch is disabled (the choices come back when you enable it again) | `steam-frame-nix-cleanup --all`, `install.sh uninstall` |
|
||||
| mtime of `~/.local/share/icons/hicolor` | [icon fallbacks](launcher-menu.md#icon-fallbacks): a running Steam rescans icons | only the directory's timestamp | nothing to remove |
|
||||
|
||||
## Only while running
|
||||
|
||||
- The UI patches (Steam, SteamVR dashboard, VR keyboard) live in the pages'
|
||||
memory; stopping `steam-ui-patches` / `steam-keyboard-patch` reverts them.
|
||||
- clipboard-sync runs from KDE autostart (a Home Manager link).
|
||||
- Firefox: the desktop profile's `user.js` link exists only while its
|
||||
Firefox runs (see [Firefox](firefox.md#how-it-works)).
|
||||
- Jellyfin: the hardware decoding permissions are `flatpak run` options of
|
||||
the desktop entry, not a Flatpak override.
|
||||
|
||||
## Cleanup
|
||||
|
||||
**`steam-frame-nix-cleanup`** (`install.sh cleanup`,
|
||||
`steamFrame.cleanup.package`) knows everything any version of
|
||||
steam-frame-nix wrote outside the store, removes only what is provably its
|
||||
own (everything else is reported as "left alone") and can be run again
|
||||
safely; `--dry-run` shows what it would do.
|
||||
|
||||
- On every switch, `cleanup --orphans` removes what the configuration no
|
||||
longer uses (never the saved patch state).
|
||||
- `steam-frame-nix-cleanup --all` removes everything, also the saved patch
|
||||
state. SteamVR's key can't be changed while SteamVR runs: it is then left
|
||||
to the runtime drop-in (restored when SteamVR stops).
|
||||
- Without Nix or after a rollback it runs from the script
|
||||
(bash, coreutils, findutils, jq, all in SteamOS' `/usr/bin`):
|
||||
`curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all`.
|
||||
- It also removes what older versions left: the icon fallback links and
|
||||
their list (`~/.local/state/steam-frame-nix/icon-fallbacks`), the
|
||||
debugger marker `steamvr-debugger`, Firefox `user.js` copies and links
|
||||
and their `prefs.js` values, the Jellyfin hwdec entries of
|
||||
`~/.local/share/flatpak/overrides/org.jellyfin.JellyfinDesktop` (an empty
|
||||
override file too) and the old shim copy in
|
||||
`~/.var/app/org.jellyfin.JellyfinDesktop`.
|
||||
|
||||
## 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) and the link `~/.config/home-manager` to it, unless
|
||||
`~/.config/home-manager` exists or `--flake <dir-or-flakeref>` is given;
|
||||
uninstall removes the link, never the configuration;
|
||||
- dotfiles Home Manager found in its way, renamed to `*.hm-backup-<time>`
|
||||
(kept by uninstall);
|
||||
- `~/.local/state/home-manager` and `~/.local/state/nix` (profiles,
|
||||
generations), `~/.nix-profile`, `~/.nix-defexpr`, `~/.nix-channels`,
|
||||
`~/.cache/nix`.
|
||||
|
||||
## App data you create
|
||||
|
||||
Not steam-frame-nix's to remove: the Firefox desktop profile
|
||||
(`~/.var/app/org.mozilla.firefox/config/mozilla/firefox/desktop`, browser
|
||||
data), and whatever apps keep in `~/.var/app/*`, Flatpak apps and their
|
||||
runtimes.
|
||||
|
||||
## Rollback
|
||||
|
||||
`home-manager generations` lists previous generations; run
|
||||
`<store path>/activate` of the one you want. Generations with
|
||||
steam-frame-nix clean up after themselves on activation (orphans). After
|
||||
rolling back to a generation **without** steam-frame-nix, or to one older
|
||||
than `steam-frame-nix-cleanup` (2026-09-29), remove what the newer one wrote
|
||||
outside the store:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all
|
||||
# or: nix run github:lhns/steam-frame-nix#cleanup -- --all
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```sh
|
||||
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- uninstall # --keep-nix keeps Nix
|
||||
```
|
||||
|
||||
stops Home Manager's user services (reverting the UI patches), runs
|
||||
`cleanup --all`, runs `home-manager uninstall`, then removes Nix and the
|
||||
per-user Nix state (see [Set up by install.sh](#set-up-by-installsh)). If
|
||||
SteamVR is running, its key is restored when SteamVR stops (the closing
|
||||
message says so). Your configuration, `*.hm-backup-*` files, app data and
|
||||
Flatpaks stay.
|
||||
|
||||
To drop steam-frame-nix from a Home Manager configuration you keep, first
|
||||
run `steam-frame-nix-cleanup --all`, then remove it and switch. Or set Home
|
||||
Manager's `uninstall = true;` in the configuration that still imports
|
||||
steam-frame-nix and switch: its activation runs `cleanup --all` while Home
|
||||
Manager removes its files. (`home-manager uninstall` alone doesn't load
|
||||
steam-frame-nix's modules, so it can't clean up after them.)
|
||||
@@ -0,0 +1,169 @@
|
||||
# SteamVR dashboard
|
||||
|
||||
Four patches of SteamVR's dashboard (`systemui` page): [window size and
|
||||
distance](#dashboard-windows), [Steam close button](#steam-close-button),
|
||||
[window curvature](#window-curvature), [window control bar](#window-control-bar).
|
||||
Options: [options.md#dashboard](options.md#dashboard).
|
||||
|
||||
All are [UI patches](ui-patches.md) and turn on the
|
||||
[SteamVR debugger](steamvr-debugger.md). Each is found by signature (its
|
||||
entry in `signatures.json` has the patch's name); on mismatch the dashboard
|
||||
stays stock. Tested with SteamVR build 11008059.
|
||||
|
||||
## Dashboard windows
|
||||
|
||||
`dashboard.windows.*`.
|
||||
|
||||
**Problem:** dashboard windows can only be enlarged to 2x, and grabbed
|
||||
windows pushed back only to 5 m (6 m in theater), too close for a big
|
||||
screen.
|
||||
|
||||
**What it does:** raises these limits, which the dashboard sends to the
|
||||
compositor in its scene graph. `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; turning options off
|
||||
reverts on the next switch. The keyboard's range is not patched.
|
||||
|
||||
```nix
|
||||
steamFrame.dashboard.windows = {
|
||||
maxScale = 4.0; # resize up to 4x (theater: 11.2x)
|
||||
distance.world.max = 10.0; # push windows back up to 10 m
|
||||
distance.theater.max = 12.0;
|
||||
};
|
||||
```
|
||||
|
||||
**Caveat:** grab nodes are recognized by their exact stock values, so if
|
||||
SteamVR changes them the distance options silently do nothing. State:
|
||||
`window.__sfuiDashboardWindows`.
|
||||
|
||||
## 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.
|
||||
|
||||
**What it does:** gives the Steam window an X that hides Steam: it docks the
|
||||
window back if it was in the world, theater or on a hand, then shows the
|
||||
most recently active other dashboard window, or **just the dashboard bar**
|
||||
if there is none.
|
||||
|
||||
Steam stays hidden until you bring it back (Steam tab, a Steam menu pick,
|
||||
SteamVR asking for it): closing the active window, or a theater window, then
|
||||
goes to the previous window or the bar instead of Steam. This survives
|
||||
dashboard reopens, patch-service restarts, SteamVR restarts and reboots
|
||||
("Steam hidden" is saved in
|
||||
`~/.local/state/steam-frame-nix/ui-patches/steam-close-button.json`, see
|
||||
[persistent state](ui-patches.md#persistent-state)). After a restart the
|
||||
patch attaches a few seconds after the dashboard appears, possibly after
|
||||
SteamVR has already shown Steam: if Steam is (or first becomes) the active
|
||||
window then, it is hidden once like with X (only the bar at that point);
|
||||
otherwise it just stays hidden.
|
||||
|
||||
**Limitations:** SteamVR's rarer "go home" paths (Now Playing after a game
|
||||
quits, message overlays) still show Steam; no effect with a VRLink remote
|
||||
dashboard; turning the option off while bar-only leaves no active window
|
||||
until the next tab click or dashboard open.
|
||||
|
||||
Debugging: `window.__sfuiSteamClose.plan()` / `homePlan()` and
|
||||
`window.__sfuiSteamCloseState`.
|
||||
|
||||
## Window curvature
|
||||
|
||||
`dashboard.windowCurvature.*`.
|
||||
|
||||
**Problem:** dashboard windows are either curved (fixed radius) or flat, and
|
||||
world windows start flat.
|
||||
|
||||
**What it does:** turns the "Toggle Curvature" row of a window's three-dot
|
||||
menu into a control showing the window's value; the same control in the
|
||||
bottom bar (see [window control bar](#window-control-bar)) works without the
|
||||
value, with haptic steps.
|
||||
|
||||
- **click:** curved → flat, flat → stock (1);
|
||||
- **drag up/down** with the laser: curvature from 0 (flat) to `max`,
|
||||
relative to stock (2 = half the radius), rounded to `step`, with a detent
|
||||
of `detentPixels` of drag at each of `detentPoints` (no values skipped),
|
||||
and haptics (`haptics`) for detents, edges and steps. Drag distance:
|
||||
`dragPixelsPerUnit` in the menu, `barDragPixelsPerUnit` on the bar button,
|
||||
after `dragThresholdPixels`.
|
||||
|
||||
A window without its own value is shown at `initial` once curved in the
|
||||
world or on a hand, at 1 in the dashboard or theater. Values are kept per
|
||||
window until SteamVR restarts.
|
||||
|
||||
**Limitations:**
|
||||
|
||||
- Laser only; with gamepad navigation the row is the stock toggle.
|
||||
- No thumbstick scrolling (SteamVR sends no wheel events to the menu).
|
||||
- The laser stops at the menu's edge (~190 px above the row): with the
|
||||
default 120 px per 1.0, 0 → 1 fits into one drag, 0 → 3 takes two. Lower
|
||||
`dragPixelsPerUnit` (≤ 60) for the full range in one drag.
|
||||
|
||||
Debugging: `window.__sfuiWindowCurvature.dump()` (`.log` recent events).
|
||||
|
||||
### Contract for other patches
|
||||
|
||||
For patches handling presses on the curvature controls: every
|
||||
element the patch drives has class `sfui-curv-ctl`; when a press becomes a
|
||||
drag, a bubbling `CustomEvent` `sfui-curv-dragstart` (detail
|
||||
`{ frameID, where: 'menu' | 'bar' }`) is dispatched on it, and
|
||||
`sfui-curv-dragend` when it ends;
|
||||
`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.
|
||||
|
||||
## Window control bar
|
||||
|
||||
`dashboard.frameControls.*`.
|
||||
|
||||
**Problem:** the controls under a dashboard window are fixed: some in the
|
||||
bottom bar, others only in the three-dot menu (curvature, dock to a
|
||||
controller), and theater windows have no "Float".
|
||||
|
||||
**What it does:**
|
||||
|
||||
- **long press** a bar icon or menu row (`longPressMs`; a progress ring
|
||||
shows from half the time, at most after 1 s), then **Show in bar** in the
|
||||
popup moves that control between bar and menu **for all windows**.
|
||||
Placements survive SteamVR restarts and reboots (saved in
|
||||
`~/.local/state/steam-frame-nix/ui-patches/frame-controls.json`, see
|
||||
[persistent state](ui-patches.md#persistent-state)).
|
||||
- `inBar` / `inMenu` set where controls start; a popup choice wins until
|
||||
that control's entry changes.
|
||||
- `floatInTheater` gives theater windows the "Float" control.
|
||||
|
||||
```nix
|
||||
steamFrame.dashboard.frameControls = {
|
||||
enable = true;
|
||||
# longPressMs = 1500;
|
||||
# inBar = [ "curvature" ]; inMenu = [ "theater" ];
|
||||
# floatInTheater = true;
|
||||
};
|
||||
```
|
||||
|
||||
With [window curvature](#window-curvature), a drag on the curvature control
|
||||
adjusts curvature and cancels the long press, also after the ring shows;
|
||||
once the ring shows, the drag needs 3× the usual travel
|
||||
(`dragThresholdPixels`, counted from where the press started), so laser
|
||||
drift during the hold doesn't cancel it.
|
||||
|
||||
**Limitations:** laser only (no right-click or thumbstick click reaches the
|
||||
dashboard; gamepad navigation sees stock controls); placements are per
|
||||
control type, not per window.
|
||||
|
||||
Debugging: `window.__sfuiFrameControls.dump()`, `.placement()`, `.reset()`
|
||||
(forget choices), `.log`, `window.__sfuiFrameControlsState`.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Session, portal and clipboard
|
||||
|
||||
Plumbing between the Frame's [two sessions](two-sessions.md). Options:
|
||||
[Session](options.md#session), [Clipboard sync](options.md#clipboard-sync).
|
||||
|
||||
## Session settings and services
|
||||
|
||||
Module `session.nix`, used by the other modules; normally nothing to set.
|
||||
|
||||
- `session.runtimeDir` / `session.bus` point at the Steam session's runtime
|
||||
dir and bus (services, KDE wallet), which the nested desktop can't see.
|
||||
`session.busEnv` is a launcher prefix to reach them:
|
||||
|
||||
```nix
|
||||
xdg.dataFile."applications/org.example.App.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Example
|
||||
Exec=${config.steamFrame.session.busEnv} flatpak run org.example.App %U
|
||||
'';
|
||||
```
|
||||
|
||||
- Switching from the nested desktop, Home Manager can't reach the service
|
||||
manager ("User systemd daemon not running"). So after every switch this
|
||||
module reloads the Steam session's user manager and applies
|
||||
`session.services.start` / `stop` / `restart`, which other modules fill.
|
||||
|
||||
## Portal
|
||||
|
||||
`session.portalFix.enable`, on by default.
|
||||
|
||||
**Problem:** the Frame image (SteamOS 0.3.0, build 20260922) points the Steam
|
||||
session's `xdg-desktop-portal` at `/usr/share/xdg-desktop-portal/gamescope-portals`,
|
||||
which lacks `gamescope-portals.conf`: no backend, no OpenURI, so no app in
|
||||
the Steam session can open links.
|
||||
|
||||
**Fix:** a portal dir in `~/.local/share` linking Valve's `.portal` files plus
|
||||
a config (`default=holo;gamescope`), and a drop-in on
|
||||
`xdg-desktop-portal.service`. The desktop's portal is unaffected.
|
||||
|
||||
**Remove when** SteamOS ships `gamescope-portals.conf`.
|
||||
|
||||
## Clipboard sync
|
||||
|
||||
`clipboardSync.enable`, on by default.
|
||||
|
||||
**Problem:** the Steam session's X displays and the nested desktop have
|
||||
separate clipboards.
|
||||
|
||||
**Fix:** [clipboard-sync](https://github.com/dnut/clipboard-sync), built from
|
||||
source (its flake is x86-only; `clipboardSync.package` to replace it),
|
||||
started via KDE autostart (the desktop can't reach the user manager, and
|
||||
`:2` must exist first). Each switch restarts it if outdated, so switch from a
|
||||
desktop terminal.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Firefox
|
||||
|
||||
`firefox.enable`: a launcher for the Flathub Firefox Flatpak
|
||||
(`org.mozilla.firefox`, stable). It shadows the Flatpak's own entry (same
|
||||
ID), so default-browser associations keep working. Options:
|
||||
[options.md#firefox](options.md#firefox).
|
||||
|
||||
## Options
|
||||
|
||||
- **`vrFullscreenFix`** (on): gamescope never shows fullscreen windows, so
|
||||
Firefox looks frozen. `full-screen-api.ignore-widgets` makes fullscreen
|
||||
fill just the window. Not applied in the desktop profile. **Remove when**
|
||||
gamescope shows fullscreen X11 windows in VR.
|
||||
- **`disableAv1`** (off): `media.av1.enabled = false`. The Frame's decoder
|
||||
driver (`iris`) has no AV1, only H.264, HEVC and VP9, so YouTube and co.
|
||||
send VP9/H.264, decoded in hardware, instead of software AV1. **Remove
|
||||
when** a SteamOS kernel adds AV1 to `iris`.
|
||||
- **`prefs`:** further `about:config` values for every profile; they can
|
||||
also override the fixes above.
|
||||
- **`desktopProfile`** (`"desktop"`): the sessions can't see each other's
|
||||
Firefox, so a second instance stops at the locked profile; in the nested
|
||||
desktop the launcher uses this separate profile (a normal Firefox profile
|
||||
with its own browser data, created on first use). `null`: the default
|
||||
profile in both sessions.
|
||||
|
||||
Changes take effect at the next start of Firefox.
|
||||
|
||||
## How it works
|
||||
|
||||
`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
|
||||
`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.)
|
||||
|
||||
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.
|
||||
|
||||
Older versions linked or copied a `user.js` into every profile; those and
|
||||
the values they left in `prefs.js` are removed by `steam-frame-nix-cleanup`
|
||||
on switch, for each profile not in use at that moment.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Jellyfin hardware decoding
|
||||
|
||||
`jellyfin.hardwareDecoding.enable`, for the Flathub
|
||||
[Jellyfin Desktop](https://github.com/jellyfin/jellyfin-desktop) Flatpak
|
||||
(`org.jellyfin.JellyfinDesktop`), which plays video with libmpv. Options:
|
||||
[options.md#jellyfin](options.md#jellyfin).
|
||||
|
||||
**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 Flatpak
|
||||
has nothing between that and `devices=all`;
|
||||
- mpv never tries: Jellyfin hard-sets `hwdec=auto-copy`, and mpv's `auto`
|
||||
probing leaves out V4L2 M2M on purpose (its quality varies by SoC).
|
||||
Jellyfin has no way to pass mpv options.
|
||||
|
||||
**What it does:** device access (`devices=all`) and an `LD_PRELOAD` shim
|
||||
that rewrites an `hwdec` value starting with `auto` (set through libmpv's
|
||||
`mpv_set_*` functions) to `$SFN_MPV_HWDEC` (the `hwdec` option, default
|
||||
`v4l2m2m-copy,auto-copy`); explicit values such as `no` stay. mpv tries the
|
||||
listed decoders in order and falls back to software decoding per stream.
|
||||
With the default, 1080p H.264 plays through `v4l2m2m-copy` at ~15-20 % CPU.
|
||||
Changes take effect at the next start of Jellyfin.
|
||||
|
||||
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" ];
|
||||
```
|
||||
|
||||
**Caveats:**
|
||||
|
||||
- `devices=all` gives the app all of `/dev` (cameras, input devices, ...),
|
||||
not just the decoder.
|
||||
- V4L2 M2M decoding quality varies with drivers and codecs. Tested: 8-bit
|
||||
H.264; 10-bit HEVC is untested. mpv falls back to software only when the
|
||||
decoder fails; for streams that decode with artifacts, disable the option
|
||||
(or set `hwdec = "auto-copy"`, Jellyfin's own value).
|
||||
|
||||
## How it works
|
||||
|
||||
The shim (a few libmpv wrappers, only libc) is preloaded from the Nix store;
|
||||
only its store path is exposed (read-only) to the sandbox. It would work for
|
||||
any libmpv app that sets `hwdec=auto*`, but only Jellyfin Desktop is set up
|
||||
here.
|
||||
|
||||
Nothing is written to Flatpak's overrides: a desktop entry shadowing the
|
||||
Flatpak's (`~/.local/share/applications/org.jellyfin.JellyfinDesktop.desktop`,
|
||||
same ID, so the KDE menu and the "+" menu start it) passes them as
|
||||
`flatpak run` options, so they apply to launches from that entry and are
|
||||
gone with it. From a terminal:
|
||||
|
||||
```sh
|
||||
flatpak run --branch=stable --arch=aarch64 --command=jellyfin-desktop \
|
||||
--device=all --filesystem=<shim>:ro \
|
||||
--env=LD_PRELOAD=<shim>/lib/mpv-hwdec-shim.so \
|
||||
--env=SFN_MPV_HWDEC=v4l2m2m-copy,auto-copy org.jellyfin.JellyfinDesktop
|
||||
```
|
||||
|
||||
The exact line with the store path: `grep ^Exec=
|
||||
~/.local/share/applications/org.jellyfin.JellyfinDesktop.desktop`, or the
|
||||
read-only option `steamFrame.jellyfin.hardwareDecoding.command`. In its
|
||||
output: `mpv-hwdec-shim: hwdec "auto-copy" -> "v4l2m2m-copy,auto-copy"`,
|
||||
then mpv's `Using hardware decoding (v4l2m2m-copy)`.
|
||||
|
||||
Older versions used a Flatpak override (via nix-flatpak or a Home Manager
|
||||
link); `steam-frame-nix-cleanup` removes their entries from it.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Keyboard
|
||||
|
||||
Keyboard layout of the Steam session, and two patches of Steam's VR
|
||||
keyboard: [extra keys](#extra-keys) and [swipe and suggestions](#swipe-and-suggestions).
|
||||
All options: [options.md#keyboard](options.md#keyboard).
|
||||
|
||||
## Layout
|
||||
|
||||
`keyboard.layout`, `keyboard.variant`.
|
||||
|
||||
**Problem:** gamescope and its Xwayland displays use US unless
|
||||
`XKB_DEFAULT_*` is set; KDE's layout only affects the nested desktop, and
|
||||
`~/.config/environment.d` isn't read on the Frame.
|
||||
|
||||
**Fix:** a drop-in on `gamescope-session.service` setting
|
||||
`XKB_DEFAULT_LAYOUT`/`VARIANT`; applies at the next Steam session start.
|
||||
|
||||
`keyboard.variant` picks a variant of the layout, e.g. for `de`: `null`
|
||||
(standard, with dead keys: `^`, `` ` ``, `´` wait for the next key),
|
||||
`"nodeadkeys"` (those are typed immediately), `"mac"`, `"neo"`, `"e1"`, `"us"`
|
||||
(German letters on a US layout). List them with
|
||||
`localectl list-x11-keymap-variants <layout>`.
|
||||
|
||||
**Remove when** SteamOS applies a layout setting to gamescope.
|
||||
|
||||
## Extra keys
|
||||
|
||||
`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 it does:**
|
||||
|
||||
- Bottom row: `Esc Ctrl Alt [Space] AltGr ← ↑ ↓ → Close`, stable with Shift
|
||||
or AltGr.
|
||||
- AltGr + arrows: Home, End, Page Up, Page Down (hinted on the keys);
|
||||
Shift + arrows select text.
|
||||
- AltGr + the key left of Backspace (`´` on German, `=` on US): Delete,
|
||||
labelled like Steam's Delete key in its language (`Entf`; `Del` if that is
|
||||
longer), hinted on the key without AltGr; repeats while held. Layouts with
|
||||
an AltGr character on that key get none.
|
||||
- Layouts without AltGr (US, Dvorak, Colemak, Bulgarian, Chinese, Japanese,
|
||||
Korean) get an `Fn` key right of the space bar: Steam's AltGr toggle
|
||||
(tap: once, tap twice: locked, hold), for Delete and Home/End/Page Up/Down.
|
||||
- Ctrl/Alt chords and Esc are sent with `xdotool key` on `:0` (focus follows
|
||||
the VR-selected window); a toggled Ctrl/Alt is held down while the
|
||||
keyboard is open (e.g. Ctrl+scroll).
|
||||
- Problem characters are typed with `xdotool type`; everything else goes
|
||||
through Steam.
|
||||
- Enter always types Return (stock Steam may send it to a Steam search box).
|
||||
|
||||
**Layouts:** the character routing targets the German keymap; on others it
|
||||
is harmless (those characters are typed by xdotool), and Esc/Ctrl/Alt/arrows
|
||||
work regardless.
|
||||
|
||||
**Security:** the helper only accepts single-key Ctrl/Alt chords, the extra
|
||||
keys, Ctrl/Alt hold/release and single non-ASCII/AltGr characters; it cannot
|
||||
type ASCII text or press Enter.
|
||||
|
||||
**How it works:** the `steam-keyboard-patch` user service (`helper.mjs`)
|
||||
injects a patch over DevTools (`127.0.0.1:8080`) and re-injects it after
|
||||
Steam restarts; stopping it (or disabling the option) reverts the patch. No
|
||||
reboot or Steam restart needed.
|
||||
|
||||
**Caveat:** found by signature (see
|
||||
[finders and signatures](ui-patches.md#finders-and-signatures)); if one stops
|
||||
matching, the keyboard stays stock and the journal says why (see
|
||||
[after a Steam update](ui-patches.md#after-a-steam-update)). Tested with
|
||||
Steam client 1790377368 (UI build 11041156).
|
||||
|
||||
**Remove when** Steam's VR keyboard gets these keys.
|
||||
|
||||
## Swipe and suggestions
|
||||
|
||||
`keyboard.vr.enable`; the sub-features (`swipe`, `autocorrect`,
|
||||
`completions`, `backspaceDrag`, `haptics`) are on by default.
|
||||
|
||||
**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 it does:**
|
||||
|
||||
- **Swipe:** press the trigger on the first letter, sweep over the others,
|
||||
release on the last. The word is typed with a space before it if needed
|
||||
(`text.autoSpace`); alternatives show in the strip. `'` and `-` are typed,
|
||||
not swiped.
|
||||
- **Suggestions** never change text by themselves: a finished tapped word
|
||||
that isn't in the dictionary gets corrections (itself first; `autocorrect`),
|
||||
a word being tapped gets completions (the typed letters first;
|
||||
`completions`). A pick replaces exactly what it typed and can be switched
|
||||
again.
|
||||
- **Backspace drag:** drag Backspace left to delete one character per
|
||||
`pixelsPerChar`, with a detent (`wordDetentPixels`) at each word border
|
||||
and at the start of what the keyboard typed; drag back right to retype.
|
||||
- **Strip** (`suggestions.position`): a SteamVR dashboard panel below or
|
||||
above the keyboard, or inside the keyboard over its number row. Its
|
||||
buttons take the keyboard's key style.
|
||||
- **Haptics:** light ticks for drag steps and picks, a Snap at word detents.
|
||||
|
||||
**Dictionary** (`dictionary.*`): built from wordfreq frequency lists and
|
||||
Hunspell, both from nixpkgs. By default the `keyboard.layout` language (de,
|
||||
fr, es, it, nl, pt, sv) plus English, else English only; add or exclude
|
||||
words and word lists, see [options](options.md#keyboard).
|
||||
|
||||
**Text memory:** the keyboard can't read the text field, so it remembers
|
||||
what it typed itself (`text.bufferChars`); anything it can't follow (Enter,
|
||||
arrows, extraKeys' xdotool keys, another field,
|
||||
`text.resetAfterIdleSeconds`) resets that, and suggestions only replace text
|
||||
the memory proves intact. Works with and without `keyboard.vr.extraKeys`
|
||||
(with it, non-ASCII words are typed via its xdotool helper).
|
||||
|
||||
**Caveats:** found by signature (entries `vr-keyboard`, `vr-keyboard-panel`);
|
||||
if one stops matching the keyboard stays stock. Accented words of other
|
||||
languages are in the dictionary but only swipable where the layout has the
|
||||
letters.
|
||||
|
||||
**How it works:** a Steam UI patch (`vr-keyboard`, injected like the other
|
||||
[UI patches](ui-patches.md)). Words are matched by shape (SHARK2-style
|
||||
template matching). The strip below/above is a patch of SteamVR's
|
||||
`systemui`, with the `vr-keyboard-relay` user service carrying it between
|
||||
the two pages.
|
||||
|
||||
**Tests:** `nix flake check` (checks `vr-keyboard`: text model, corrector,
|
||||
decoder accuracy on German + English) and `keyboard.vr.checks` for the
|
||||
configured dictionary. Debugging: `window.__sfuiSwipeLog` and
|
||||
`__sfuiSwipePaths` in Steam's SharedJSContext (replay swipes with
|
||||
`scripts/vr-keyboard-replay.mjs`).
|
||||
|
||||
**Remove when** Steam's VR keyboard gets swipe typing and suggestions.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Launcher menu
|
||||
|
||||
The VR dashboard's "+" menu (non-Steam programs). Options:
|
||||
[options.md#launcher-menu](options.md#launcher-menu).
|
||||
|
||||
## Menu patches
|
||||
|
||||
`launcherMenu.*`.
|
||||
|
||||
**Problem:** the menu is in random order with "Desktop" somewhere in a
|
||||
scrolling list, and a click shows no feedback until the window appears, so
|
||||
programs often get started twice.
|
||||
|
||||
**What it does:**
|
||||
|
||||
- `sort`: programs sorted by name (case-insensitive), Desktop included.
|
||||
- `pinDesktop = "top"` / `"bottom"`: Desktop pinned above/below the list,
|
||||
always visible.
|
||||
- `closeOnLaunch`: the menu closes on click.
|
||||
- `launchDebounceSeconds = <seconds>`: a repeat launch of the same command
|
||||
within that time is ignored (and logged); a program that exits right away
|
||||
can only be restarted once the time is up.
|
||||
- `grid.enable`: the programs section becomes a grid of tiles (icon, name
|
||||
below); "Add desktop window" stays a list. Only restyles Steam's items, so
|
||||
launching, sounds and controller navigation keep working. The popup is
|
||||
300 px wide, so `grid.columns` sets the tile size (3 ≈ 92 px, 4 ≈ 68 px,
|
||||
5 ≈ 53 px); `grid.maxRows` limits visible rows, the rest scrolls.
|
||||
- `showAllApps`: without Developer Mode Steam hides `konsole`,
|
||||
`systemsettings`, `dolphin`, `plasma-discover`, `vlc`, `firewall-config`,
|
||||
`cmake-gui`, `qrenderdoc`, `lxterminal` and `sh`; this lifts that filter
|
||||
only, so Developer Mode (sshd, xrdp, LAN DevTools forwards) can stay off.
|
||||
Hide single programs with [`hiddenApps`](#hidden-apps).
|
||||
|
||||
**Steam Developer Mode** (a Steam setting, not managed here) also makes the
|
||||
menu list every desktop entry; `showAllApps` does the same without it.
|
||||
|
||||
**Limitation:** the pinned Desktop works with the laser but not with
|
||||
thumbstick / D-pad navigation.
|
||||
|
||||
**How it works:** [UI patches](ui-patches.md) in Steam's `SharedJSContext`.
|
||||
All revert when turned off (next switch). The anchors (APIs, React props,
|
||||
CSS) are verified by the offline checker. Tested with Steam client
|
||||
1790377368.
|
||||
|
||||
## Hidden apps
|
||||
|
||||
`launcherMenu.hiddenApps`: desktop entry ids (no `.desktop`).
|
||||
|
||||
**Problem:** with Developer Mode or `showAllApps`, the "+" menu lists every
|
||||
desktop entry, including system tools.
|
||||
|
||||
**Fix:** a user entry with `Hidden=true` in `~/.local/share/applications`
|
||||
masks the system one (also in the KDE menu). The "+" menu always hides
|
||||
`steam` and `vrurlhandler`; for Konsole in VR use
|
||||
[`showAllApps`](#menu-patches).
|
||||
|
||||
## Icon fallbacks
|
||||
|
||||
`launcherMenu.iconFallbacks.enable` (on by default), `iconFallbacks.extra`.
|
||||
|
||||
**Problem:** Steam resolves `Icon=` only in the hicolor theme (and
|
||||
`pixmaps`), so Konsole and KDE System Settings, whose icons only Breeze has,
|
||||
show without icon.
|
||||
|
||||
**Fix:** Home Manager links nixpkgs' Breeze SVGs of `utilities-terminal`
|
||||
and `preferences-system` (plus `extra`) into
|
||||
`~/.local/share/icons/hicolor/scalable/apps/`; a name Breeze doesn't have
|
||||
fails the build, `enable = false` provides none. When the set of links
|
||||
changes, the switch bumps the mtime of `~/.local/share/icons/hicolor`, so a
|
||||
running Steam rescans (GTK only rereads a theme whose directory changed).
|
||||
Each switch also prints hints: icons of programs Steam can't find that
|
||||
Breeze has (add them to `extra`), and fallbacks hicolor has anyway.
|
||||
|
||||
**Migration:** until 2026-09 a script made these links on switch and listed
|
||||
them in `~/.local/state/steam-frame-nix/icon-fallbacks`; the first switch
|
||||
replaces them with Home Manager's and `steam-frame-nix-cleanup` removes the
|
||||
rest. `iconFallbacks` used to be a list; a list now fails with a hint (use
|
||||
`extra`, or `enable = false` for `[ ]`).
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
# Options
|
||||
|
||||
All options live under `steamFrame.*`. The portal fix and clipboard sync
|
||||
are on by default; everything else is opt-in.
|
||||
|
||||
## Session
|
||||
|
||||
Details: [desktop-integration.md](desktop-integration.md).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.session.runtimeDir` | str | `"/run/user/1000"` | `XDG_RUNTIME_DIR` of the outer (Steam/VR) session. |
|
||||
| `steamFrame.session.bus` | str | `"unix:path=${runtimeDir}/bus"` | Outer session D-Bus (user manager, `kwalletd6`). |
|
||||
| `steamFrame.session.busEnv` | str, read-only | `"env DBUS_SESSION_BUS_ADDRESS=${bus}"` | Prefix for launchers that must use the outer bus. |
|
||||
| `steamFrame.session.services.start` | list of str | `[ ]` | User units started on switch if not running. |
|
||||
| `steamFrame.session.services.restart` | list of str | `[ ]` | User units restarted on every switch. |
|
||||
| `steamFrame.session.services.stop` | list of str | `[ ]` | User units stopped on switch if running (e.g. of a disabled feature). |
|
||||
| `steamFrame.session.portalFix.enable` | bool | `true` | Working portal config (OpenURI) for the Steam session. |
|
||||
|
||||
## Keyboard
|
||||
|
||||
Details: [keyboard.md](keyboard.md).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.keyboard.layout` | null or str | `null` | XKB layout for the Steam session, e.g. `"de"`; `null`: US. |
|
||||
| `steamFrame.keyboard.variant` | null or str | `null` | XKB variant for the Steam session, e.g. `"nodeadkeys"`; see [Keyboard layout](keyboard.md#layout). |
|
||||
| `steamFrame.keyboard.vr.extraKeys.enable` | bool | `false` | VR keyboard with Esc/Ctrl/Alt, arrows, real chords, AltGr/non-ASCII. |
|
||||
| `steamFrame.keyboard.vr.enable` | bool | `false` | Swipe typing, suggestions and Backspace drag on the VR keyboard; the sub-features below are on by default, see [VR keyboard](keyboard.md#swipe-and-suggestions). |
|
||||
| `steamFrame.keyboard.vr.swipe.enable` | bool | `true` | Swipe typing. |
|
||||
| `steamFrame.keyboard.vr.dictionary.languages` | list of submodules | layout language + English | `{ language; hunspell; words; frequencyOffset; keepFrequentAbove; }`: wordfreq language, `pkgs.hunspellDicts` name (or `null`), most frequent words taken, zipf offset, keep words Hunspell rejects from this zipf on (default `4.0`). Default: the `keyboard.layout` language (de, fr, es, it, nl, pt, sv; 60000) + English (40000, `-0.3`), else English (60000). |
|
||||
| `steamFrame.keyboard.vr.dictionary.contractions` | bool | `true` | Words with apostrophes (`couldn't`, `geht's`), swiped by their letters. |
|
||||
| `steamFrame.keyboard.vr.dictionary.extraWords` | list of str | `[ ]` | Words always included, casing as given. |
|
||||
| `steamFrame.keyboard.vr.dictionary.extraWordsFrequency` | number | `5.0` | Zipf frequency of extra words. |
|
||||
| `steamFrame.keyboard.vr.dictionary.extraWordFiles` | list of paths | `[ ]` | Word lists, `word` or `word<TAB>zipf` per line. |
|
||||
| `steamFrame.keyboard.vr.dictionary.excludeWords` | list of str | `[ ]` | Words never suggested. |
|
||||
| `steamFrame.keyboard.vr.text.bufferChars` | int | `128` | Characters of typed text the keyboard remembers. |
|
||||
| `steamFrame.keyboard.vr.text.resetAfterIdleSeconds` | int | `30` | Forget it after this long without typing (`0`: never). |
|
||||
| `steamFrame.keyboard.vr.text.autoSpace` | bool | `true` | Space before a swiped word after a known non-space character. |
|
||||
| `steamFrame.keyboard.vr.suggestions.position` | `"below"`, `"above"`, `"inside"` | `"above"` | Suggestion strip: SteamVR panel below/above the keyboard, or over its number row. |
|
||||
| `steamFrame.keyboard.vr.suggestions.count` | int | `6` | Suggestions shown. |
|
||||
| `steamFrame.keyboard.vr.autocorrect.enable` | bool | `true` | Correction suggestions for finished tapped words not in the dictionary. |
|
||||
| `steamFrame.keyboard.vr.autocorrect.maxEditDistance` | int | `2` | Largest edit distance (neighbouring keys and swaps count 0.5). |
|
||||
| `steamFrame.keyboard.vr.completions.enable` | bool | `true` | Completions of the tapped word. |
|
||||
| `steamFrame.keyboard.vr.completions.minPrefix` | int | `2` | Letters typed before completions show. |
|
||||
| `steamFrame.keyboard.vr.backspaceDrag.enable` | bool | `true` | Backspace drag: left deletes, back right retypes. |
|
||||
| `steamFrame.keyboard.vr.backspaceDrag.pixelsPerChar` | int | `25` | Travel per character (keyboard px; a key is ~60). |
|
||||
| `steamFrame.keyboard.vr.backspaceDrag.wordDetentPixels` | int | `90` | Extra travel across a word border (`0`: none). |
|
||||
| `steamFrame.keyboard.vr.haptics` | bool | `true` | Haptic ticks for drag steps, word detents and picks. |
|
||||
| `steamFrame.keyboard.vr.checks` | package, read-only | | The tests, built with the configured dictionary. |
|
||||
|
||||
## UI patches
|
||||
|
||||
Details: [ui-patches.md](ui-patches.md).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.uiPatches.patches` | list of submodules | `[ ]` | Runtime patches of Steam's web UIs, see [UI patches](ui-patches.md). |
|
||||
| `steamFrame.uiPatches.lib` | attrs, read-only | | Patch helpers (`mkPatch`), see [Finders and signatures](ui-patches.md#mkpatch). |
|
||||
|
||||
## Launcher menu
|
||||
|
||||
Details: [launcher-menu.md](launcher-menu.md).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.launcherMenu.sort` | bool | `false` | Sort the "+" menu alphabetically. |
|
||||
| `steamFrame.launcherMenu.pinDesktop` | null or `"top"` / `"bottom"` | `null` | Pin "Desktop" above/below the "+" menu's list; `null`: normal entry. |
|
||||
| `steamFrame.launcherMenu.closeOnLaunch` | bool | `false` | Close the "+" menu when a program is clicked. |
|
||||
| `steamFrame.launcherMenu.launchDebounceSeconds` | unsigned int (s) | `0` | Ignore repeat launches of a program within this time; `0`: off. |
|
||||
| `steamFrame.launcherMenu.grid.enable` | bool | `false` | Show the "+" menu's programs as a grid of tiles. |
|
||||
| `steamFrame.launcherMenu.grid.columns` | int, 1-8 | `4` | Tiles per row (3 ≈ 92 px, 4 ≈ 68 px, 5 ≈ 53 px). |
|
||||
| `steamFrame.launcherMenu.grid.maxRows` | null or positive int | `null` | Visible rows, the rest scrolls; `null`: up to 600 px. |
|
||||
| `steamFrame.launcherMenu.showAllApps` | bool | `false` | List all programs without Developer Mode, see [Launcher menu](launcher-menu.md#menu-patches). |
|
||||
| `steamFrame.launcherMenu.iconFallbacks.enable` | bool | `true` | Breeze icons of Konsole and KDE System Settings in hicolor, so the "+" menu shows them, see [Icon fallbacks](launcher-menu.md#icon-fallbacks). |
|
||||
| `steamFrame.launcherMenu.iconFallbacks.extra` | list of str | `[ ]` | Further Breeze app icon names to provide (a name Breeze lacks fails the build). |
|
||||
| `steamFrame.launcherMenu.hiddenApps` | list of str | `[ ]` | Desktop entry ids (no `.desktop`) hidden from the "+" and KDE menus. |
|
||||
|
||||
## Dashboard
|
||||
|
||||
Details: [dashboard.md](dashboard.md).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.dashboard.windows.maxScale` | null or positive number | `null` | Max resize scale of dashboard windows; `null`: stock (2), see [Dashboard windows](dashboard.md#dashboard-windows). |
|
||||
| `steamFrame.dashboard.windows.distance.{world,theater,dashboard}.{min,max}` | null or positive number (m) | `null` | Pull-in / push-back limits of grabbed windows; `null`: stock (world 0.25-5, theater 1-6, dashboard 0.3-4 m). |
|
||||
| `steamFrame.dashboard.steamCloseButton.enable` | bool | `false` | X button on the dashboard's Steam window, see [Steam close button](dashboard.md#steam-close-button). |
|
||||
| `steamFrame.dashboard.windowCurvature.enable` | bool | `false` | Adjustable curvature per window, see [Window curvature](dashboard.md#window-curvature). |
|
||||
| `steamFrame.dashboard.windowCurvature.initial` | non-negative number | `1.0` | Curvature of curved world/hand windows without own value (1 = stock, 0 = flat). |
|
||||
| `steamFrame.dashboard.windowCurvature.max` | positive number | `3.0` | Largest curvature. |
|
||||
| `steamFrame.dashboard.windowCurvature.step` | positive number | `0.05` | Rounding step while dragging (at most `max`). |
|
||||
| `steamFrame.dashboard.windowCurvature.detentPixels` | unsigned int (px) | `24` | Detent at each detent point in drag pixels: the value holds there, then continues (nothing skipped); `0`: none. |
|
||||
| `steamFrame.dashboard.windowCurvature.detentPoints` | list of non-negative numbers | `[ 0 1.0 ]` | Detent points (flat, stock), at most `max`. |
|
||||
| `steamFrame.dashboard.windowCurvature.dragThresholdPixels` | unsigned int (px) | `8` | Vertical travel before a press becomes a drag. |
|
||||
| `steamFrame.dashboard.windowCurvature.dragPixelsPerUnit` | positive number (px) | `120` | Drag distance per 1.0 in the menu (6 px per 0.05 step). |
|
||||
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | positive number (px) | `60` | Drag distance per 1.0 on the bar button. |
|
||||
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging (steps, detents, edges); the dashboard's hover clicks are muted during a drag. |
|
||||
| `steamFrame.dashboard.frameControls.enable` | bool | `false` | Move window controls between bar and three-dot menu, see [Window control bar](dashboard.md#window-control-bar). |
|
||||
| `steamFrame.dashboard.frameControls.longPressMs` | int, 300-10000 (ms) | `1500` | Long-press duration. |
|
||||
| `steamFrame.dashboard.frameControls.inBar` | list of control names | `[ ]` | Controls that start in the bar: `keyboard`, `float`, `dashboard`, `theater`, `dockLeft`, `dockRight`, `close`, `curvature`, `"icon:<n>"`. |
|
||||
| `steamFrame.dashboard.frameControls.inMenu` | list of control names | `[ ]` | Controls that start in the three-dot menu. |
|
||||
| `steamFrame.dashboard.frameControls.floatInTheater` | bool | `false` | "Float" control on theater windows. |
|
||||
|
||||
## SteamVR debugger
|
||||
|
||||
Details: [steamvr-debugger.md](steamvr-debugger.md).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.steamvrDebugger.enable` | bool | automatic | SteamVR dashboard DevTools on `127.0.0.1:8087` (set only while SteamVR runs); on when a dashboard patch is, see [SteamVR debugger](steamvr-debugger.md). |
|
||||
|
||||
## Clipboard sync
|
||||
|
||||
Details: [desktop-integration.md](desktop-integration.md#clipboard-sync).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
|
||||
| `steamFrame.clipboardSync.package` | package | built from `dnut/clipboard-sync` | The clipboard-sync package. |
|
||||
|
||||
## Firefox
|
||||
|
||||
Details: [firefox.md](firefox.md).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.firefox.enable` | bool | `false` | Launcher for the Flathub Firefox Flatpak with the fixes below. |
|
||||
| `steamFrame.firefox.vrFullscreenFix` | bool | `true` | Default `full-screen-api.ignore-widgets` to `true` (not in the desktop profile). |
|
||||
| `steamFrame.firefox.disableAv1` | bool | `false` | Default `media.av1.enabled` to `false`: the Frame's decoder driver has no AV1, so sites send VP9/H.264, decoded in hardware. |
|
||||
| `steamFrame.firefox.prefs` | attrs of bool, int or str | `{ }` | Further `about:config` default values for every profile (override the fixes too). |
|
||||
| `steamFrame.firefox.desktopProfile` | null or str | `"desktop"` | Separate profile (directory name) for the nested desktop; `null`: the default profile in both sessions. |
|
||||
|
||||
## Jellyfin
|
||||
|
||||
Details: [jellyfin.md](jellyfin.md).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.jellyfin.hardwareDecoding.enable` | bool | `false` | Hardware video decoding in the Jellyfin Desktop Flatpak, see [Jellyfin](jellyfin.md). |
|
||||
| `steamFrame.jellyfin.hardwareDecoding.hwdec` | str | `"v4l2m2m-copy,auto-copy"` | mpv `hwdec` used instead of Jellyfin's automatic one. |
|
||||
| `steamFrame.jellyfin.hardwareDecoding.command` | str, read-only | | The `flatpak run …` command line of the desktop entry, for a terminal. |
|
||||
|
||||
## Cleanup
|
||||
|
||||
Details: [changes-outside-nix.md](changes-outside-nix.md#cleanup).
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `steamFrame.cleanup.package` | package, read-only | | `steam-frame-nix-cleanup` (on `PATH` too), see [Changes outside Nix](changes-outside-nix.md#cleanup). |
|
||||
|
||||
## Renamed options
|
||||
|
||||
Renamed options still work under their old names, with a warning:
|
||||
|
||||
| Old | New |
|
||||
|---|---|
|
||||
| `keyboardLayout`, `keyboardVariant` | `keyboard.layout`, `keyboard.variant` |
|
||||
| `steamKeyboardPatch.enable` | `keyboard.vr.extraKeys.enable` |
|
||||
| `hiddenApps` | `launcherMenu.hiddenApps` |
|
||||
| `launcherMenu.launchDebounce` | `launcherMenu.launchDebounceSeconds` |
|
||||
| `runtimeDir`, `userBus`, `outerBusEnv` | `session.runtimeDir`, `session.bus`, `session.busEnv` |
|
||||
| `userServices.{start,restart,stop}` | `session.services.{start,restart,stop}` |
|
||||
| `portalFix.enable` | `session.portalFix.enable` |
|
||||
| `dashboard.windowMaxScale` | `dashboard.windows.maxScale` |
|
||||
| `dashboard.windowDistance.*` | `dashboard.windows.distance.*` |
|
||||
| `dashboard.windowCurvature.default` | `dashboard.windowCurvature.initial` |
|
||||
| `dashboard.windowCurvature.snapPixels`, `snapPoints` | `detentPixels`, `detentPoints` |
|
||||
| `dashboard.windowCurvature.dragThreshold` | `dragThresholdPixels` |
|
||||
@@ -0,0 +1,36 @@
|
||||
# SteamVR debugger
|
||||
|
||||
`steamvrDebugger.enable`, automatic: on when any patch in
|
||||
`steamFrame.uiPatches.patches` uses port 8087 (all [dashboard](dashboard.md)
|
||||
patches and the VR keyboard's strip below/above the keyboard).
|
||||
|
||||
**Problem:** dashboard patches need SteamVR's DevTools port, opened only
|
||||
with `VRWebHelper/DebuggerEnabled` (port `VRWebHelper/DebuggerPort`, default
|
||||
8087). SteamVR rewrites `~/.config/openvr/config/steamvr.vrsettings` from
|
||||
memory, so the key can't be a link and can't be edited while SteamVR runs.
|
||||
|
||||
**What it does:** sets the key only while SteamVR runs:
|
||||
|
||||
- before every SteamVR start, the `steamvr-webhelper-debugger` oneshot
|
||||
(a drop-in on `steamvr.service`) stores the key's value in
|
||||
`~/.local/state/steam-frame-nix/steamvr-debugger.armed`, sets the key
|
||||
(with `jq`) and writes a runtime drop-in,
|
||||
`/run/user/1000/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf`,
|
||||
whose `ExecStopPost=` runs a restore script next to it
|
||||
(`/run/user/1000/steam-frame-nix/`, `/usr/bin` tools only);
|
||||
- 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. A key you set to `true` yourself is
|
||||
never touched. Disabled, there is no unit; the switch restores the key if
|
||||
SteamVR is stopped, otherwise the runtime drop-in does when it stops.
|
||||
|
||||
**The first time, restart SteamVR once** (e.g. reboot); until then
|
||||
`steam-ui-patches` keeps polling.
|
||||
|
||||
**Security:** the port listens on `127.0.0.1` only; keep Developer Mode off
|
||||
(see [DevTools on the LAN](ui-patches.md#devtools-on-the-lan)).
|
||||
|
||||
What it writes and when it is removed:
|
||||
[changes outside Nix](changes-outside-nix.md).
|
||||
@@ -0,0 +1,33 @@
|
||||
# Two sessions
|
||||
|
||||
The Frame runs two graphical sessions at once; most workarounds exist because
|
||||
of their differences:
|
||||
|
||||
| | Steam / VR session | Nested Plasma desktop |
|
||||
|---|---|---|
|
||||
| Compositor | gamescope | KWin (nested, shown as a VR window) |
|
||||
| Displays | X display `:0` (apps show as floating VR windows) | own Wayland + Xwayland `:2` |
|
||||
| D-Bus | the outer session bus, `/run/user/1000/bus` | a private bus |
|
||||
| `XDG_RUNTIME_DIR` | `/run/user/1000` | its own |
|
||||
| systemd user manager | yes | not reachable |
|
||||
|
||||
Consequences:
|
||||
|
||||
- **Wallet:** there should be one `kwalletd6`, on the outer bus; apps started
|
||||
from the desktop would otherwise start a second one whose secrets VR can't
|
||||
see. Prefix launchers' `Exec=` with `steamFrame.session.busEnv`
|
||||
([session settings](desktop-integration.md#session-settings-and-services)).
|
||||
- **User services:** home-manager skips `reloadSystemd` when switching from
|
||||
the desktop terminal, so `steamFrame.session.services` talks to the outer
|
||||
user manager directly.
|
||||
- **Launchers:** the "+" menu only sees `~/.local/share/applications` (not
|
||||
`~/.nix-profile/share`), so entries are written there, shadowing
|
||||
Flatpak/package entries with the same ID.
|
||||
- **Keyboard layout:** KDE's layout only affects the nested desktop
|
||||
([keyboard layout](keyboard.md#layout)).
|
||||
- **Clipboard:** separate per session ([clipboard sync](desktop-integration.md#clipboard-sync)).
|
||||
- **Firefox:** the sessions can't see each other's Firefox, so a second
|
||||
instance stops at the locked profile ([desktop profile](firefox.md#options)).
|
||||
|
||||
Switch (`home-manager switch`) from a terminal **in the nested desktop**, so
|
||||
clipboard-sync restarts with the desktop's environment.
|
||||
@@ -0,0 +1,188 @@
|
||||
# UI patches
|
||||
|
||||
For patch authors, and for fixing patches after a Steam update. Options:
|
||||
[options.md#ui-patches](options.md#ui-patches).
|
||||
|
||||
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
|
||||
`-cef-enable-debugging`), `127.0.0.1:8087` for SteamVR once
|
||||
[its debugger](steamvr-debugger.md) is on. The `steam-ui-patches` user
|
||||
service (`injector.mjs`) patches the running pages through them; Steam's
|
||||
files are never modified. The [launcher menu](launcher-menu.md),
|
||||
[dashboard](dashboard.md) and [VR keyboard](keyboard.md#swipe-and-suggestions)
|
||||
features are such patches, and you can add your own.
|
||||
|
||||
**Caveat:** patches depend on Steam UI internals and can break with an
|
||||
update; find modules by signature, not id ([below](#finders-and-signatures)).
|
||||
|
||||
## Defining a patch
|
||||
|
||||
`steamFrame.uiPatches.patches` entries:
|
||||
|
||||
| Attribute | Default | Description |
|
||||
|---|---|---|
|
||||
| `name` | | Unique name (log). |
|
||||
| `endpoint` | `"http://127.0.0.1:8080"` | DevTools base URL; `/json/list` is polled every 5 s. |
|
||||
| `target.title` / `target.titleRegex` / `target.urlRegex` | `null` | Pages to patch; all given criteria must match (JS regexes). |
|
||||
| `patch` | | JS file evaluated in every matching page (awaited). |
|
||||
| `unpatch` | `null` | JS file evaluated when the service stops. |
|
||||
| `state` | `false` | Give the patch one [persistent JSON value](#persistent-state). |
|
||||
|
||||
```nix
|
||||
steamFrame.uiPatches.patches = [ {
|
||||
name = "my-patch";
|
||||
target.title = "SharedJSContext"; # Steam's main JS context
|
||||
patch = ./my-patch/patch.js;
|
||||
unpatch = ./my-patch/unpatch.js;
|
||||
} ];
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Persistent state
|
||||
|
||||
With `state = true`: a patch's choices made in the UI can't be kept in the
|
||||
page's own storage, since SteamOS's `steamvr.service` runs
|
||||
`rm -rf ~/.cache/SteamVR` (vrwebhelper's browser profile, incl.
|
||||
localStorage) on every SteamVR start. So the service keeps one JSON value
|
||||
per such patch in `~/.local/state/steam-frame-nix/ui-patches/<name>.json`
|
||||
(`$XDG_STATE_HOME`):
|
||||
|
||||
- before each evaluation it defines `window.__sfuiStore.get(name)` /
|
||||
`.set(name, value)` in the page and fills `get` from the file only while
|
||||
the page has no value yet (a fresh page after a SteamVR restart, reboot or
|
||||
reload);
|
||||
- `set` goes through the DevTools binding `window.__sfuiStoreSave`; the
|
||||
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](changes-outside-nix.md).
|
||||
|
||||
## DevTools on the LAN
|
||||
|
||||
Steam's Developer Mode enables `steam-web-debug-portforward`
|
||||
(`0.0.0.0:8081` → `8080`) and `steamvr-web-debug-portforward`
|
||||
(`0.0.0.0:8088` → `8087`), and firewalld allows ports 1024-65535, so anyone
|
||||
on the network could run code in Steam's UI. No patch needs Developer Mode,
|
||||
so keep it off. If it was on while those units were masked, they may stay
|
||||
enabled: check with
|
||||
`systemctl is-enabled steam-web-debug-portforward steamvr-web-debug-portforward`
|
||||
and `sudo systemctl disable` them.
|
||||
|
||||
## Finders and signatures
|
||||
|
||||
Webpack module ids and export names change with every Steam UI build, so
|
||||
patches never use them. `modules/lib/finders.js` (like Decky Loader's
|
||||
`findModule`/`findInReactTree` or Vencord's `find`) locates by *signature*:
|
||||
|
||||
- a **module** by strings/regexes in its factory source;
|
||||
- an **export** by shape: type, function source, arity, data properties,
|
||||
prototype methods or getters;
|
||||
- **React** fibers by props (`findFiberUp`, `findFiberDown`,
|
||||
`findInReactTree`).
|
||||
|
||||
Every signature must match exactly once, otherwise the patch changes nothing
|
||||
and reports it (e.g. `signature not found, Steam left unpatched:
|
||||
layouts.currentLayout (module 40222): ambiguous export, candidates r_, xy`).
|
||||
Results are cached per page.
|
||||
|
||||
Signatures live in `modules/lib/signatures.json`, shared by patches and the
|
||||
offline checker. An entry can also list `expects` (strings the patch relies
|
||||
on, checked offline only) or be `checkOnly` (anchors not used through the
|
||||
finder, checked offline only; with `stylesheet` instead of `module` it is
|
||||
matched against the bundle's CSS).
|
||||
|
||||
### mkPatch
|
||||
|
||||
`steamFrame.uiPatches.lib.mkPatch` wraps a patch file — a function
|
||||
expression `(find, sigs, opts, hooks) => …` returning a status string — with
|
||||
the finder library, its signatures, options and the shared hooks (details in
|
||||
`modules/lib/default.nix`):
|
||||
|
||||
```nix
|
||||
steamFrame.uiPatches.patches = [ {
|
||||
name = "my-patch";
|
||||
target.title = "SharedJSContext";
|
||||
patch = config.steamFrame.uiPatches.lib.mkPatch {
|
||||
name = "my-patch";
|
||||
src = ./my-patch/patch.js; # ((find, sigs, opts, hooks) => { … })
|
||||
signatures.thing = {
|
||||
module.includes = [ "SomeUniqueString" ];
|
||||
exports.Thing = { type = "class"; protoMethods = [ "DoIt" ]; };
|
||||
};
|
||||
opts.factor = 2;
|
||||
};
|
||||
unpatch = ./my-patch/unpatch.js;
|
||||
} ];
|
||||
```
|
||||
|
||||
```js
|
||||
((find, sigs, opts, hooks) => {
|
||||
let mods;
|
||||
try { mods = find.resolveAll(find.getWebpackRequire('webpackChunksteamui'), sigs); }
|
||||
catch (e) { return `not patched: ${e.message}`; }
|
||||
const Thing = mods.thing.exports.Thing; // SteamVR dashboard: 'webpackChunkvrwebui'
|
||||
…
|
||||
})
|
||||
```
|
||||
|
||||
### Shared method hooks
|
||||
|
||||
`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
|
||||
injected, upgraded and reverted in any order.
|
||||
|
||||
```js
|
||||
hooks.before(Mailbox.prototype, 'SendMessage', 'my-patch', (args) => {
|
||||
if (args[1]?.type === 'update_scene_graph') rewrite(args[1].scene_graph);
|
||||
});
|
||||
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-*`).
|
||||
|
||||
## After a Steam update
|
||||
|
||||
Check the signatures offline (Steam need not run) from a checkout:
|
||||
|
||||
```sh
|
||||
nix shell nixpkgs#nodejs -c node scripts/check-signatures.mjs
|
||||
```
|
||||
|
||||
It loads Steam's UI bundle (`~/.local/share/Steam/steamui`) and SteamVR's
|
||||
dashboard (`/opt/steamvr/resources/webinterface/dashboard/systemui.html`),
|
||||
evaluates every signature with the patches' finder code (exports in an inert
|
||||
sandbox) and prints per patch `found` (module id, export name), `ambiguous`
|
||||
or `missing`, plus warnings for missing `expects`; exit status 1 if anything
|
||||
is missing or ambiguous:
|
||||
|
||||
```
|
||||
bundle steamui: 2827 modules in /home/deck/.local/share/Steam/steamui (build 11041156)
|
||||
|
||||
steam-keyboard-patch (steamui)
|
||||
layouts found module 40222 (chunk~2dcc5aaf7.js)
|
||||
.currentLayout found export r_
|
||||
…
|
||||
OK: all signatures match exactly once
|
||||
```
|
||||
|
||||
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`.
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user