Files
lhns--steam-frame-nix/README.md
T

574 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# steam-frame-nix
[Home Manager](https://github.com/nix-community/home-manager) modules for the
Valve Steam Frame (SteamOS, `aarch64-linux`, standalone home-manager). They
work around quirks of the Frame's [two graphical sessions](#two-sessions)
(portal, KDE app menu, keyboard layout, clipboard, KDE wallet, Firefox),
enable hardware video decoding in Jellyfin, run rootless Docker, and extend
Steam's and SteamVR's UIs at runtime (VR keyboard, "+" menu, dashboard
windows, Steam close button, window curvature, window controls, a VR pet).
Everything is declarative: files are links into the Nix store, UI patches
live in memory. The few things that have to be written elsewhere at runtime
are listed under [Changes outside Nix](#changes-outside-nix-exceptions), with
their lifetime and what removes them; `steam-frame-nix-cleanup` removes every
one of them (on each switch what the configuration no longer uses, `--all`
for everything).
All options live under `steamFrame.*`. The portal fix, the applications menu
and the "+" menu's icon fallbacks are on by default; everything else is
opt-in.
## Features
One page per feature in [`docs/`](docs): problem, what you get,
configuration, limitations and how it works.
**Session** ([docs/session.md](docs/session.md)):
- [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, and Flatpak apps get KDE's file dialog.
- [Applications menu](docs/session.md#applications-menu) (`session.applicationsMenu`, on): KDE apps in the Steam session (Dolphin from the "+" menu) know the installed apps.
- [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`): one clipboard for the Steam session and the nested desktop (built from source).
**VR keyboard** ([docs/keyboard.md](docs/keyboard.md)):
- [Extra keys](docs/keyboard.md#extra-keys) (`keyboard.vr.extraKeys`): Esc/Ctrl/Alt, arrows, Delete, Shift+Tab, real chords and AltGr/non-ASCII characters.
- [Swipe and suggestions](docs/keyboard.md#swipe-and-suggestions) (`keyboard.vr`): swipe typing (also two-handed), corrections, completions, Backspace drag, F1–F12 in the strip on AltGr (`functionKeys`).
- [Touch typing](docs/keyboard.md#touch-typing) (`keyboard.vr.touchTyping`): press keys by touching them with the controller's tip, both hands.
**VR "+" menu** ([docs/launcher-menu.md](docs/launcher-menu.md)):
- [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.
**SteamVR dashboard** ([dashboard patches](docs/ui-patches.md#steamvr-dashboard-patches)):
- [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.
- [VR pet](docs/pet.md) (`pet`): a cat (five coats), Shiba Inu, Fox or Dachshund in the scene that walks around you, can be picked up and petted; "Pet" in the "+" menu, the `vr-pet` command, your own models.
- [SteamVR debugger](docs/steamvr-debugger.md) (`steamvrDebugger`, automatic): SteamVR's DevTools port for these, set only while SteamVR runs.
**Apps:**
- [Firefox](docs/firefox.md) (`firefox`): launcher for the Flatpak with a VR fullscreen fix, optional AV1 off, a separate desktop profile, optionally the default browser.
- [Jellyfin](docs/jellyfin.md) (`jellyfin.hardwareDecoding`): hardware video decoding in the Jellyfin Desktop Flatpak.
- [Launchers](docs/launchers.md) (`launchers.<desktop ID>`): an app's own desktop entry (Flatpak, Nix package) with extra options, environment, wrappers and MIME defaults; `keyring` keeps its KDE wallet logins in both sessions (Electron too).
- [Docker](docs/docker.md) (`docker`): rootless Docker as a user service, CLI working in both sessions.
- [SteamVR screenshots](docs/screenshots.md) (`screenshots`): `~/Pictures/SteamVR Screenshots`, a link to Steam's folder of the current account.
**Your own patches** of Steam's UI, and fixing patches after a Steam update: [UI patches](docs/ui-patches.md).
**Working on steam-frame-nix:** [Development](docs/development.md) (repository layout: which file runs where, runtime names, checks).
## Install
On the Frame (or a Steam Deck), in a terminal (Konsole in desktop mode or the
nested desktop):
```sh
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- install
```
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 installs Nix (skipped if Nix already works), uses
`~/.config/home-manager` or `--flake <dir-or-flakeref>` (if there is none, it
uses `~/nix-config` as it is, or creates it 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). Everything works
in the running session right away, except two settings read only at a
process start: the [keyboard layout](docs/session.md#keyboard-layout) (next
Steam session start) and the
[SteamVR dashboard patches](docs/steamvr-debugger.md) (VR pet, VR keyboard
suggestions, window curvature, ...; next SteamVR start). If they
are waiting, the installer ends with "Restart once": reboot once. Re-running
it just switches again (`--yes` answers every question). Afterwards edit
`~/nix-config/home.nix` and switch (see [Usage](#usage)).
Your own config in a git repository: `--clone <git-url>` clones it into
`~/nix-config` (`--dir <path>`, branch `--ref <branch>`) and installs it like
`--flake <dir>`. URLs: `git@github.com:owner/repo`, `https://...` or
`github:owner/repo`. Without a Home Manager configuration it first activates
the template, so a private repository clones with its git credential
helpers: your GitHub CLI login (`~/.config/gh`, kept by `uninstall`) or
`~/.git-credentials`, never a password prompt. If the clone fails, it offers
`gh auth login` (browser or one-time code, works with 2FA); an SSH URL uses
your key instead. Keep the template's `programs.git`/`programs.gh` lines
(see [Usage](#usage)) in your config, or the helpers go with the switch to
it. Only a missing `~/nix-config` or the unchanged template is replaced by
the clone; anything else there is used as it is. So re-running continues
where it stopped (after a failed switch it just switches again).
```sh
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- install --clone git@github.com:owner/my-config
```
```sh
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- status # Nix, generation, services
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- uninstall # --keep-nix keeps Nix
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all # see Changes outside Nix
```
`bash -s -- --help` lists all commands and flags. See
[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
`flake.nix` and `home.nix` ([`template/`](template), shown under
[Usage](#usage)), or add the input to your own flake.
## 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 |
What this means for you:
- **Switch from the nested desktop:** run `home-manager switch` in a
terminal there, so clipboard-sync (if enabled) restarts with the
desktop's environment. User services are handled for you
([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. Give such apps a [launcher](docs/launchers.md) with `keyring.enable`.
- **Launchers:** the "+" menu only sees `~/.local/share/applications` (not
`~/.nix-profile/share`), so [launchers](docs/launchers.md) are linked
there, replacing Flatpak/package entries with the same ID.
- **Keyboard layout, clipboard, Firefox:** each session has its own; see
[keyboard layout](docs/session.md#keyboard-layout),
[clipboard sync](docs/session.md#clipboard-sync),
[Firefox](docs/firefox.md#desktop-profile) (desktop profile).
## Usage
`nix flake init -t github:lhns/steam-frame-nix` (or the installer) creates
these two files ([`template/`](template), with more comments):
```nix
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
steam-frame-nix = {
url = "github:lhns/steam-frame-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, home-manager, steam-frame-nix, ... }:
let
username = "steamos"; # filled in by install.sh
homeDirectory = "/home/steamos";
system = "aarch64-linux"; # Steam Deck: "x86_64-linux"
in {
homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.${system};
extraSpecialArgs = { inherit username homeDirectory; };
modules = [ steam-frame-nix.homeManagerModules.default ./home.nix ];
};
};
}
```
```nix
# home.nix (the template has steamFrame commented out)
{ config, pkgs, username, homeDirectory, ... }: {
home.username = username;
home.homeDirectory = homeDirectory;
home.stateVersion = "26.05";
targets.genericLinux.enable = true;
targets.genericLinux.gpu.enable = false; # no GPU drivers for Nix GUI apps (their setup writes to /etc as root)
programs.home-manager.enable = true;
programs.git = { # SteamOS's git, Home Manager writes its config
enable = true;
package = null;
settings.credential.helper = "store"; # ~/.git-credentials
};
programs.gh.enable = true; # github.com credentials: gh auth login
steamFrame = {
keyboard.layout = "de"; # XKB layout, Steam session
keyboard.vr.extraKeys.enable = true; # Esc/Ctrl/Alt/arrows in VR
keyboard.vr.enable = true; # swipe, suggestions, Backspace drag
clipboardSync.enable = true; # one clipboard for both sessions (builds from source)
launcherMenu = {
sort = true;
pinDesktop = "bottom";
closeOnLaunch = true;
launchDebounceSeconds = 10;
grid = { enable = true; columns = 4; maxRows = 4; };
showAllApps = true;
# iconFallbacks.extra = [ "system-file-manager" ];
# Listed in the "+" menu only with showAllApps or Steam Developer Mode:
hiddenApps = [ "lxterminal" "cmake-gui" "firewall-config" "renderdoc" ];
};
dashboard = {
windows.maxScale = 4.0;
windows.distance.world.max = 10.0;
windows.distance.theater.max = 12.0;
steamCloseButton.enable = true;
windowCurvature.enable = true;
frameControls.enable = true;
};
pet.enable = true; # VR pet (bakes ~0.5 GB of models)
firefox.enable = true;
firefox.disableAv1 = true;
firefox.defaultBrowser = true;
jellyfin.hardwareDecoding.enable = true; # install the Flatpak yourself
launchers."org.signal.Signal" = { # logins in both sessions; install the Flatpak yourself
keyring = { enable = true; electron = true; };
defaultFor = [ "x-scheme-handler/sgnl" "x-scheme-handler/signalcaptcha" ];
};
docker.enable = true; # rootless
};
}
```
Switch from a terminal in the nested desktop (see [Two sessions](#two-sessions)):
```sh
home-manager switch # config in ~/.config/home-manager (installer)
home-manager switch --flake .#steamos # manual setup, from the flake's directory
```
In your own flake, add the input as above and
`steam-frame-nix.homeManagerModules.default` to the modules. `default`
imports all modules; single ones:
`homeManagerModules.{session,portal,applications-menu,keyboard-layout,vr-keyboard-extra-keys,vr-keyboard,vr-keyboard-controllers,vr-keyboard-touch,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,cleanup,dashboard-windows,steam-close-button,window-curvature,frame-controls,clipboard-sync,firefox,jellyfin,launchers,docker,screenshots,pet}`
(`steam-keyboard-patch` and `keyring` still work as the former names of
`vr-keyboard-extra-keys` and `launchers`). Every module imports `cleanup` (see
[Changes outside Nix](#changes-outside-nix-exceptions)); which file is
which: [Repository layout](docs/development.md).
## Options
| 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}"` | `Exec=` prefix for hand-written entries 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. |
| `steamFrame.session.portalFix.fileChooser` | bool | `true` | KDE's file dialog as the Steam session's FileChooser portal (Flatpak apps can open and save files outside their sandbox). Needs `portalFix.enable`. |
| `steamFrame.session.applicationsMenu.enable` | bool | `true` | `~/.config/menus/applications.menu` linked to Plasma's, for KDE apps in 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](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](docs/keyboard.md#swipe-and-suggestions). |
| `steamFrame.keyboard.vr.swipe.enable` | bool | `true` | Swipe typing. |
| `steamFrame.keyboard.vr.swipe.twoHanded` | bool | `true` | Swipes also with both lasers on the keyboard: the path from the pressing controller's pose, see [VR keyboard](docs/keyboard.md#swipe-and-suggestions). |
| `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, 8-1024 | `128` | Characters of typed text the keyboard remembers. |
| `steamFrame.keyboard.vr.text.resetAfterIdleSeconds` | unsigned int (s) | `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, 1-8 | `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, 1-3 | `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, 1-10 | `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` | positive int (px) | `25` | Travel per character (keyboard px; a key is ~60). |
| `steamFrame.keyboard.vr.backspaceDrag.wordDetentPixels` | unsigned int (px) | `90` | Extra travel across a word border (`0`: none). |
| `steamFrame.keyboard.vr.functionKeys.enable` | bool | `false` | F1–F12 in the suggestion strip while AltGr (or Fn) is active, pressed with the active Ctrl/Alt/Shift; the suggestions come back unchanged. Needs `keyboard.vr.enable`, `keyboard.vr.extraKeys.enable` and `suggestions.position` `"above"`/`"below"`, see [VR keyboard](docs/keyboard.md#swipe-and-suggestions). |
| `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.keyboard.vr.touchTyping.enable` | bool | `false` | Touch typing: a key is pressed when a controller's tip touches it, both hands; the lasers work as before, see [Touch typing](docs/keyboard.md#touch-typing). |
| `steamFrame.keyboard.vr.touchTyping.depth` | number, -2 to 5 (cm) | `0` | How far behind the keyboard's surface a touch registers; negative: in front. |
| `steamFrame.keyboard.vr.touchTyping.haptics` | bool | `true` | A haptic tick when a touch presses a key. |
| `steamFrame.uiPatches.patches` | list of submodules | `[ ]` | Runtime patches of Steam's web UIs, see [UI patches](docs/ui-patches.md#defining-a-patch). Fields below. |
| `steamFrame.uiPatches.patches.*.name` | str | required | Unique name (log). |
| `steamFrame.uiPatches.patches.*.endpoint` | str | `"http://127.0.0.1:8080"` | DevTools base URL, `/json/list` polled every 5 s: Steam 8080, SteamVR 8087. |
| `steamFrame.uiPatches.patches.*.target.{title,titleRegex,urlRegex}` | null or str | `null` | Pages to patch: exact title, JS regexes; all given ones must match. |
| `steamFrame.uiPatches.patches.*.patch` | path | required | JS evaluated (awaited) in every matching page. |
| `steamFrame.uiPatches.patches.*.unpatch` | null or path | `null` | JS evaluated when the service stops. |
| `steamFrame.uiPatches.patches.*.state` | bool | `false` | One [persistent JSON value](docs/ui-patches.md#persistent-state) for the patch. |
| `steamFrame.uiPatches.lib` | attrs, read-only | | Patch helpers (`mkPatch`), see [mkPatch](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. |
| `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](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](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](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`). |
| `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](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.pet.enable` | bool | `false` | A 3D pet in SteamVR's scene, the `vr-pet` command and "Pet" in the "+" menu, see [VR pet](docs/pet.md); the first switch bakes its models (~0.5 GB in the store). |
| `steamFrame.pet.defaultModel` | null or str | `null` | Model id shown until another is picked; `null`: the spec's `"default": true` (the Ginger cat). |
| `steamFrame.pet.extraModels` | attrs of path or attrs | `{ }` | Your own models by id: a folder with a `model.json`, or the spec as an attrset (files as paths), see [VR pet models](docs/pet-models.md). |
| `steamFrame.pet.options` | attrs | `{ }` | Behaviour options of `modules/pet/core.js` (`DEFAULTS` there), e.g. `{ walkSpeed = 0.3; follow = 3; }`. |
| `steamFrame.pet.fps` | int, 4-60 | `24` | Animation frames per second (baked frames and scene graph updates): smoother, but more frames loaded in vrcompositor. |
| `steamFrame.pet.mount` | `"dynamic"`, `"all"` | `"dynamic"` | Baked frames kept mounted in the scene graph: the clips needed now or next, or all of them. |
| `steamFrame.pet.debug.demo` | bool | `false` | Demo tour: cycles through all poses and activities. |
| `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 | `false` | Clipboard bridge between the Steam session and the nested desktop; built from source (the first switch takes a while). |
| `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. |
| `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. |
| `steamFrame.firefox.defaultBrowser` | bool | `false` | Default for `http`, `https`, `text/html` (`xdg.mimeApps`). |
| `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.launchers` | attrs of submodules | `{ }` | Launchers by desktop ID (without `.desktop`): the app's own entry, rewritten, see [Launchers](docs/launchers.md). Fields below. |
| `steamFrame.launchers.<id>.source.flatpak` | null or str | the desktop ID, if no other source | Flatpak app ID whose exported entry is rewritten (at runtime). |
| `steamFrame.launchers.<id>.source.package` | null or package | `null` | Package whose `share/applications/<id>.desktop` is rewritten (at build time; the build fails without it). |
| `steamFrame.launchers.<id>.source.file` | null or str | `null` | Desktop entry on the host, rewritten at runtime. |
| `steamFrame.launchers.<id>.keyring.enable` | bool | `false` | KDE wallet shared by both sessions: outer bus; Flatpaks may talk to `org.kde.kwalletd6`, `org.freedesktop.secrets`. |
| `steamFrame.launchers.<id>.keyring.electron` | bool | `false` | Pass `--password-store=kwallet6` (Electron apps; needs `keyring.enable`). |
| `steamFrame.launchers.<id>.defaultFor` | list of str | `[ ]` | MIME types / `x-scheme-handler/<scheme>` the app becomes the default and a recommended handler for (also in `MimeType=`); a type claimed twice fails. |
| `steamFrame.launchers.<id>.mimeTypes` | list of str | `[ ]` | Added to `MimeType=`, not made default. |
| `steamFrame.launchers.<id>.flatpakArgs` | list of str | `[ ]` | `flatpak run` options before the app ID (Flatpaks only). |
| `steamFrame.launchers.<id>.env` | attrs of str | `{ }` | App environment: `--env=K=V` for Flatpaks, like `hostEnv` otherwise. |
| `steamFrame.launchers.<id>.hostEnv` | attrs of str | `{ }` | Environment of the started process (`env K=V`; for Flatpaks the `flatpak` client). |
| `steamFrame.launchers.<id>.args` | list of str | `[ ]` | Arguments right after the app ID / program. |
| `steamFrame.launchers.<id>.wrappers` | list of str | `[ ]` | Commands the command line is passed to, outermost first. |
| `steamFrame.launchers.<id>.settings` | attrs of null or str | `{ }` | `[Desktop Entry]` keys to set (key-file syntax) or remove (`null`); drops their localized variants. |
| `steamFrame.launchers.<id>.command` | str, read-only | | About the command the entry runs, for a terminal. |
| `steamFrame.docker.enable` | bool | `false` | Rootless Docker as a user service, CLI for both sessions, see [Docker](docs/docker.md). |
| `steamFrame.docker.package` | package | `pkgs.docker` | Docker package (daemon and CLI). |
| `steamFrame.docker.host` | str, read-only | `"unix://${runtimeDir}/docker.sock"` | The daemon's `DOCKER_HOST` (the CLI's default). |
| `steamFrame.screenshots.enable` | bool | `false` | Link `~/Pictures/<name>` to the SteamVR screenshots, see [SteamVR screenshots](docs/screenshots.md). |
| `steamFrame.screenshots.name` | str | `"SteamVR Screenshots"` | Name of the link in `~/Pictures`. |
| `steamFrame.screenshots.steamUserId` | null or str (digits) | `null` | Steam account ID (folder in `~/.local/share/Steam/userdata`); `null`: the account last logged in, found at runtime. |
| `steamFrame.cleanup.package` | package, read-only | | `steam-frame-nix-cleanup` (on `PATH` too), see [Changes outside Nix](#changes-outside-nix-exceptions). |
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` |
Removed options fail with the new form:
| Old | New |
|---|---|
| `keyring.flatpaks.<id>`, `keyring.programs.<id>` | `launchers.<id>.keyring` (fields: [Launchers](docs/launchers.md#configuration)) |
| `launcherMenu.iconFallbacks = [ … ]` (a list) | `iconFallbacks.enable` (`false` for none), `iconFallbacks.extra` |
| `dashboard.windowCurvature.snap` | `dashboard.windowCurvature.detentPixels` |
| `dashboard.windowCurvature.barDragRoom` | nothing (no longer needed) |
## Changes outside Nix (exceptions)
Everything not listed here is a Home Manager link into the Nix store or
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](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 ([persistent state](docs/ui-patches.md#persistent-state)): window control bar placements (`frame-controls`), "Steam hidden" (`steam-close-button`), the VR pet's spot, pose, model and whether it is hidden (`vr-pet`). 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` |
| `/run/user/1000/steam-frame-nix/applications/<id>.desktop` (with `.<id>.desktop.sum`, `.lock`) | [Launchers](docs/launchers.md) of Flatpaks and host files; `~/.local/share/applications/<id>.desktop` is a Home Manager link to it | until reboot (tmpfs), written again at login | reboot; the next switch or rewrite when the launcher or its app is gone; `steam-frame-nix-cleanup --all` |
| `/run/user/1000/steam-frame-nix/screenshots` | [SteamVR screenshots](docs/screenshots.md#how-it-works) without `steamUserId`: link to the current account's folder; `~/Pictures/<name>` is a Home Manager link to it | until reboot (tmpfs), written again at login | reboot; `steam-frame-nix-cleanup` once unused |
| `/run/user/1000/steam-frame-nix/vr-pet/icon.png` (with `.lock`) | [VR pet](docs/pet.md#how-it-works): link to the current model's "+" menu icon; `~/.local/share/icons/hicolor/256x256/apps/vr-pet.png` is a Home Manager link to it | until reboot (tmpfs), written again at login | reboot; `steam-frame-nix-cleanup` once unused |
| mtime of `~/.local/share/applications` | [Launchers](docs/launchers.md#how-it-works): a running Steam rescans the "+" menu | only the directory's timestamp | nothing to remove |
| mtime of `~/.local/share/icons/hicolor` | [icon fallbacks](docs/launcher-menu.md#icon-fallbacks), [VR pet](docs/pet.md#how-it-works) icon: 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. 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:
`curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all`.
### 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.
The VR keyboard's relays (`vr-keyboard-relay`,
`vr-keyboard-controllers-relay`) only pass messages and keep nothing.
- 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](docs/firefox.md#how-it-works)).
- Launchers (Jellyfin, wallet access): the permissions are `flatpak run`
options of the desktop entries, not Flatpak overrides.
- Docker: the daemon's socket in `/run/user/1000` (tmpfs) exists while
`docker.service` runs.
### Set up by install.sh
`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`, locked and committed; with `--clone` only until the
clone replaces it) and the link `~/.config/home-manager` to it, unless that
exists or `--flake` is given (with `--flake <dir>` or `--clone`, the link
to that directory unless it exists); uninstall removes the link, never
the configuration (a clone included);
- 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
Not steam-frame-nix's to remove: the Firefox desktop profile
(`~/.var/app/org.mozilla.firefox/config/mozilla/firefox/desktop`, browser
data), secrets apps stored in the KDE wallet, and whatever apps keep in
`~/.var/app/*`, Flatpak apps and their runtimes.
Docker's images, containers and volumes (`~/.local/share/docker`) are partly
owned by the subordinate UIDs of your containers, so a plain `rm` fails.
With the daemon running, then stopped:
```sh
docker system prune -a --volumes
systemctl --user stop docker
nix shell nixpkgs#rootlesskit -c rootlesskit rm -rf ~/.local/share/docker
```
## 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). Programs started from the Nix store (often the Steam
session itself) keep running until you log out or reboot: `uninstall` lists
them and removes Nix without waiting. If Nix's uninstaller fails, the rest
still runs (the `~/.config/home-manager` link goes) except the per-user Nix
state, and the closing message says to reboot and run `uninstall` again.
Your configuration, `*.hm-backup-*` files (the closing message names both),
app data, Flatpaks and the GitHub CLI login (`~/.config/gh`) 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.)
## Credits
The [VR pet](docs/pet.md)'s model sources are included in this repository,
unchanged (byte-identical to the originals linked below), each with a
`LICENSE.md` next to it: the cat's in `modules/pet/sources/`, the animals'
in `modules/pet/models/<id>/`. Nix bakes them into the store at build time.
- **Toon Cat FREE** by [Omabuarts Studio](https://sketchfab.com/omabuarts)
([model](https://sketchfab.com/3d-models/toon-cat-free-b2bd1ee7858444bda366110a2d960386)),
[CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/): the cat (mesh,
texture, rig, walk) and its coats. Modified: recoloured coats (Tuxedo,
Blue, Cream, Snow), retargeted and hand-keyed animations, baked to one OBJ
per frame. Included as `modules/pet/sources/toon-cat/toon_cat_free.glb`
(the glb of [DevTakao/threejs-cat](https://github.com/DevTakao/threejs-cat)).
- **Tuxedo Cat Animated 2.0** by [DreamNoms](https://sketchfab.com/DreamNoms)
([model](https://sketchfab.com/3d-models/tuxedo-cat-animated-20-783fcb78b55b4394a212c2b6392e1113)),
[CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/): only its
SitDown, IdleSit and StandUp clips, retargeted onto the Toon Cat and
baked. Included as `modules/pet/sources/tuxedo-cat/` (the glTF of
[xialin-he/xialin-he.github.io](https://github.com/xialin-he/xialin-he.github.io)).
- **Shiba Inu** and **Fox** from the
[Ultimate Animated Animal Pack](https://quaternius.com/packs/ultimateanimatedanimals.html)
by [Quaternius](https://quaternius.com),
[CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) (credited
anyway). Modified: clips sampled per frame, sit, lie, sleep and the
held-by-the-scruff pose hand-keyed, a wagging tail and breathing added,
material colours turned into a texture. Included as
`modules/pet/models/{shiba,fox}/model.glb` (the glbs of Poly Pizza:
[Shiba Inu](https://poly.pizza/m/y4wdQpg767),
[Fox](https://poly.pizza/m/Bc97C66HKi)).
- **Dachshund:** the Quaternius Shiba Inu (CC0 1.0), reshaped (longer back
and ears, shorter legs) and recoloured black and tan.
- **three.js** (desktop preview only), [MIT](https://github.com/mrdoob/three.js/blob/dev/LICENSE),
from npm at build time (not in this repository).
The baked cat's store output carries a short `CREDITS.md` pointing here.
## License
[Apache License 2.0](LICENSE).