Pierre Kisters aa9ca183f1 docs: README as overview + options, one docs page per feature
README keeps intro, feature list linking docs/, install, two sessions,
usage, the complete options table, changes outside Nix, rollback,
uninstall. Each feature's details (problem, what you get, configuration,
limitations, how it works) move to its own page in docs/; docs/dashboard.md
is split per feature and docs/changes-outside-nix.md becomes
docs/cleanup.md.
2026-09-29 01:02:28 +02:00
2026-09-27 23:48:48 +02:00

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, keyboard layout, clipboard, Firefox), enable hardware video decoding in Jellyfin, and extend Steam's and SteamVR's UIs at runtime (VR keyboard, "+" menu, dashboard windows, Steam close button, window curvature, window controls).

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 and clipboard sync 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.
  • Keyboard layout (keyboard.layout, keyboard.variant): XKB layout for the Steam session.
  • Clipboard sync (clipboardSync, on): one clipboard for the Steam session and the nested desktop.

VR keyboard (docs/keyboard.md):

  • Extra keys (keyboard.vr.extraKeys): Esc/Ctrl/Alt, arrows, Delete, real chords and AltGr/non-ASCII characters.
  • Swipe and suggestions (keyboard.vr): swipe typing, corrections, completions, Backspace drag.

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.
  • 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.
  • Jellyfin (jellyfin.hardwareDecoding): hardware video decoding in the Jellyfin Desktop Flatpak.

Your own patches of Steam's UI, and fixing patches after a Steam update: UI patches.

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 creates ~/nix-config from the template with your user name) and runs home-manager switch; what it sets up is listed under Set up by install.sh. Re-running it just switches again (--yes answers every question). Afterwards edit ~/nix-config/home.nix and switch (see Usage).

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 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. Prefix such launchers' Exec= with steamFrame.session.busEnv (example).
  • Launchers: the "+" menu only sees ~/.local/share/applications (not ~/.nix-profile/share), so entries are written there, shadowing Flatpak/package entries with the same ID.
  • Keyboard layout, clipboard, Firefox: each session has its own; see keyboard layout, 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;
  programs.home-manager.enable = true;

  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
    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;
    };
    firefox.enable = true;
    firefox.disableAv1 = true;
    jellyfin.hardwareDecoding.enable = true;  # install the Flatpak yourself
  };
}

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,keyboard-layout,steam-keyboard-patch,vr-keyboard,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,cleanup,dashboard-windows,steam-close-button,window-curvature,frame-controls,clipboard-sync,firefox,jellyfin}. Every module imports cleanup (see Changes outside Nix).

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}" 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.
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.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.
steamFrame.uiPatches.patches list of submodules [ ] Runtime patches of Steam's web UIs, see UI patches.
steamFrame.uiPatches.lib attrs, read-only Patch helpers (mkPatch), see Finders and signatures.
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.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 true Clipboard bridge between the Steam session and the nested desktop.
steamFrame.clipboardSync.package package built from dnut/clipboard-sync The clipboard-sync package.
steamFrame.firefox.enable bool false Launcher for the Flathub Firefox Flatpak with the fixes below.
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.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.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

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). 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: 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.
  • 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).
  • Jellyfin: the hardware decoding permissions are flatpak run options of the desktop entry, not a Flatpak override.

Set up by install.sh

install.sh install (the bootstrap, not the modules) also 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) and the link ~/.config/home-manager to it, unless that exists or --flake is given; uninstall removes the link, never the configuration;
  • dotfiles in Home Manager's way, renamed to *.hm-backup-<time> (kept);
  • ~/.local/state/home-manager, ~/.local/state/nix (profiles, generations), ~/.nix-profile, ~/.nix-defexpr, ~/.nix-channels, ~/.cache/nix.

App data you create

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:

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

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%