steam-frame-nix

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 (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, 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/: problem, what you get, configuration, limitations and how it works.

Session (docs/session.md):

  • Session settings (session.*): outer bus and user services, for launchers and the other modules.
  • Portal fix (session.portalFix, on): apps in the Steam session can open links, and Flatpak apps get KDE's file dialog.
  • Applications menu (session.applicationsMenu, on): KDE apps in the Steam session (Dolphin from the "+" menu) know the installed apps.
  • Keyboard layout (keyboard.layout, keyboard.variant): XKB layout for the Steam session.
  • Clipboard sync (clipboardSync): one clipboard for the Steam session and the nested desktop (built from source).

VR keyboard (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 (keyboard.vr): swipe typing (also two-handed), corrections, completions, Backspace drag, F1–F12 in the strip on AltGr (functionKeys).
  • Touch typing (keyboard.vr.touchTyping): press keys by touching them with the controller's tip, both hands.

VR "+" menu (docs/launcher-menu.md):

  • Launcher menu (launcherMenu.*): sorted, Desktop pinned, closes on launch, no double launches, grid of tiles, all programs without Developer Mode.
  • Hidden apps (launcherMenu.hiddenApps) and icon fallbacks (launcherMenu.iconFallbacks, on) for Konsole and KDE System Settings.

SteamVR dashboard (dashboard patches):

  • Dashboard windows (dashboard.windows): larger max scale and push-back distance.
  • Steam close button (dashboard.steamCloseButton): an X that hides the Steam window.
  • Window curvature (dashboard.windowCurvature): adjustable curvature per window.
  • Window control bar (dashboard.frameControls): move controls between bar and three-dot menu.
  • VR pet (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 (steamvrDebugger, automatic): SteamVR's DevTools port for these, set only while SteamVR runs.

Apps:

  • Firefox (firefox): launcher for the Flatpak with a VR fullscreen fix, optional AV1 off, a separate desktop profile, optionally the default browser.
  • Jellyfin (jellyfin.hardwareDecoding): hardware video decoding in the Jellyfin Desktop Flatpak.
  • Launchers (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 (docker): rootless Docker as a user service, CLI working in both sessions.
  • SteamVR screenshots (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.

Working on steam-frame-nix: Development (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):

curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- install

The short link redirects to 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 with your user name) and runs home-manager switch; what it sets up is listed under Set up by install.sh. Everything works in the running session right away, except two settings read only at a process start: the keyboard layout (next Steam session start) and the SteamVR dashboard patches (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).

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) 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).

curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- install --clone git@github.com:owner/my-config
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 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/, shown under 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).
  • 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 with keyring.enable.
  • Launchers: the "+" menu only sees ~/.local/share/applications (not ~/.nix-profile/share), so launchers are linked there, replacing Flatpak/package entries with the same ID.
  • Keyboard layout, clipboard, Firefox: each session has its own; see keyboard layout, clipboard sync, Firefox (desktop profile).

Usage

nix flake init -t github:lhns/steam-frame-nix (or the installer) creates these two files (template/, with more comments):

# 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 ];
    };
  };
}
# 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):

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); which file is which: Repository layout.

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.
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.
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.
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.
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.
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. 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 for the patch.
steamFrame.uiPatches.lib attrs, read-only Patch helpers (mkPatch), see 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.
steamFrame.launcherMenu.iconFallbacks.enable bool true Breeze icons of Konsole and KDE System Settings in hicolor, so the "+" menu shows them, see 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.
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.
steamFrame.dashboard.windowCurvature.enable bool false Adjustable curvature per window, see 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.
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; 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.
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.
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.
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. 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.
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.
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.

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)
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 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): 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 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 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: 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: a running Steam rescans the "+" menu only the directory's timestamp nothing to remove
mtime of ~/.local/share/icons/hicolor icon fallbacks, VR pet 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.

  • 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).
  • 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 (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:

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:

curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all
# or: nix run github:lhns/steam-frame-nix#cleanup -- --all

Uninstall

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). 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'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 (model), CC-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).
  • Tuxedo Cat Animated 2.0 by DreamNoms (model), CC-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).
  • Shiba Inu and Fox from the Ultimate Animated Animal Pack by Quaternius, CC0 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, Fox).
  • 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, 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.

S
Description
GitHub pull mirror of lhns/steam-frame-nix. GitHub repository ID: 1390843264. Gitea import mode: github-full.
https://github.com/lhns/steam-frame-nix Readme Apache-2.0
2.6 MiB
0 Stars 1 Watchers 0 Forks
Languages
JavaScript 60.1%
Nix 20%
Python 11.4%
Shell 7.1%
Awk 0.7%
Other 0.7%