{"article":{"slug":"nix-on-the-steam-frame","title":"Nix on the Steam Frame","subtitle":null,"summary":"John Murray walks through installing Nix on Valve's Steam Frame headset using the official installer's Steam Deck mode, then managing apps and dotfiles declaratively with standalone Home Manager, including launcher integration, system files on SteamOS's immutable root, Tailscale, and the Frametop desktop.","content_type":"tutorial","language":"en","canonical_url":"https://johns.codes/blog/nix-on-steam-frame","author":{"name":"John Murray","url":"https://johns.codes/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"John's Codes","url":"https://johns.codes/","listing_slug":null,"listing":null},"topics":[{"name":"Linux","slug":"linux","url":"https://listedarticles.com/topics/linux"},{"name":"Developer Tools","slug":"developer-tools","url":"https://listedarticles.com/topics/developer-tools"},{"name":"Hardware","slug":"hardware","url":"https://listedarticles.com/topics/hardware"},{"name":"Tutorials","slug":"tutorials","url":"https://listedarticles.com/topics/tutorials"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2004,"reading_minutes":9,"published_at":"2026-10-05T02:16:39.000Z","added_at":"2026-10-05T14:31:04.072Z","updated_at":"2026-10-05T14:31:04.072Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/nix-on-the-steam-frame","markdown_url":"https://listedarticles.com/articles/nix-on-the-steam-frame.md","example":false,"citation":"John Murray, John's Codes. \"Nix on the Steam Frame.\" 5 Oct 2026. https://johns.codes/blog/nix-on-steam-frame (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://johns.codes/blog/nix-on-steam-frame"},"body_markdown":"# Nix on the Steam Frame\n\nI got my Steam Frame and ofc the first thing I wanted to figure out was getting nix installed on it. Thankfully the Steam Deck runs a similar OS setup, so all the work people did to get nix working on the deck basically made the frame “just work”.\n\nSo here’s how you can get nix installed and some cool things you can do with it once it’s set up.\n\n## Install steps\n\n### Nix\n\n```\npasswd                                # no password is set by default, need one for sudo\nsudo steamos-readonly disable         # make root file system writable\nsudo mkdir -p /etc/tmpfiles.d         # The installer expects this path to exist\ncurl -fsSL -o nix-installer.sh https://artifacts.nixos.org/nix-installer\nless nix-installer.sh                 # give the install a spot check\nsh nix-installer.sh install steam-deck --enable-flakes\nsudo steamos-readonly enable          # go back to read only root\n```\nAfter that nix should be set up. The official installer’s Steam Deck mode works without issue on the frame and handles SteamOS’s immutable root for you.\n\nThis works by keeping all of `/nix` at `/home/nix` (which survives SteamOS updates) and on boot bind mounting it to `/nix` with a `nix.mount` systemd unit.\n\nOne thing to keep in mind, SteamOS updates replace the root file system. Your store and anything under `/home` is fine, but any edits you make to `/etc/nix/nix.conf` (like adding yourself to `trusted-users`) will need to be redone after an update.\n\n### Home manager\n\nJust having nix installed is fine, but the main reason I love nix so much is managing all my apps and settings declaratively. Running full NixOS on the frame would be a little crazy. I’m sure you could figure something out, but just using home manager to manage dotfiles is a good balance for me.\n\nInstalling home manager itself works the usual standalone way. Getting it to play nice with SteamOS takes a fair bit of frame specific config though, which is most of the rest of this post.\n\nI already have a nix flake for my [NixOS config](https://github.com/JRMurr/NixOsConfig), so I went with the flake install approach.\n\nHere is an example flake setup for the frame. It already has the inputs for the other modules I cover later in the post ([steamos-etc](#managing-system-files) and [frametop](#frametop)). You can drop them if you don’t want them.\n\n```\n# flake.nix\n{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    home-manager = {\n      url = \"github:nix-community/home-manager\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n    # I'll explain these later on in the blog, you can ignore these if you want\n    steamos-etc = {\n      url = \"github:JRMurr/steamos-etc-nix\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n    frametop = {\n      url = \"github:JRMurr/frametop-nix\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n    };\n  };\n  outputs = { nixpkgs, home-manager, ... }@inputs: {\n    homeConfigurations.steamos = home-manager.lib.homeManagerConfiguration {\n      pkgs = nixpkgs.legacyPackages.aarch64-linux;\n      # makes `inputs` available as a module argument in home.nix\n      extraSpecialArgs = { inherit inputs; };\n      modules = [ ./home.nix ];\n    };\n  };\n}\n```\n```\n# home.nix\n{ inputs, ... }:\n{\n  imports = [\n    # again these are sorta optional, ignore if you want\n    inputs.steamos-etc.homeManagerModules.default\n    inputs.frametop.homeManagerModules.default\n  ];\n  home.username = \"steamos\";\n  home.homeDirectory = \"/home/steamos\";\n  home.stateVersion = \"26.05\";\n  targets.genericLinux.enable = true;  # enables some options to make home manager work better on non-nixos setups\n  programs.home-manager.enable = true; # standalone installs need this to get the home-manager cli\n}\n```\nThen to get home manager set up you can run:\n\n```\n# cd to dir with flake\nnix build '.#homeConfigurations.\"steamos\".activationPackage'\nHOME_MANAGER_BACKUP_EXT=backup ./result/activate\n```\n(if you see `User systemd daemon not running. Skipping reload.` in the output, see the [Nested sessions section](#nested-sessions))\n\n`HOME_MANAGER_BACKUP_EXT=backup` will back up (by renaming) any existing files that home manager will now manage.\n\nYou only need to build and run `activate` like this the first time, to get home manager installed.\nAfter it’s set up you can run\n\n`home-manager switch --flake <path to flake>#steamos -b backup`\nto update your config going forward. Keep the `-b backup` around, SteamOS updates like to put their dotfiles back.\n\n#### Nested sessions\n\nOne thing that will probably bite you when you run `home-manager switch` is the nested plasma session. Gamescope is the actual session on the frame, it’s running the SteamOS dashboard and the “vr stuff”.\nWhen you open the “Desktop” app it runs `/usr/bin/steamos-nested-desktop`, which starts plasma *inside* that session. To keep the two from stepping on each other it does roughly:\n\n```\nexport XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR/nested_plasma\ndbus-run-session startplasma-wayland\n```\nSo anything you launch from that desktop (and later with frametop, which does the same thing with `.../frametop`) gets a runtime dir of `/run/user/1000/nested_plasma` and its own private D-Bus.\nYour user’s systemd manager is still at `/run/user/1000` though, so anything that looks for it through those vars can’t find it.\n\nHome manager is one of those things. The switch won’t fail. It will write all your files and just print:\n\n`User systemd daemon not running. Skipping reload.`\nThis means any new or changed user services won’t be started/restarted until your next login.\n\nTo fix it, point the switch back at the real session:\n\n`env XDG_RUNTIME_DIR=/run/user/$(id -u) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus home-manager switch --flake <path to flake>#steamos -b backup`\nThe same prefix works for the first `./result/activate` too.\n\nYou’ll hit the same problem with `systemctl --user`, and the same env override fixes it.\n\nThis class of issue should not affect you if you connect over ssh or launch a terminal from the SteamOS dashboard directly, since Steam itself runs with the real `/run/user/1000` and bus.\n\n#### Showing apps in the launcher\n\nOnce you start installing GUI apps with home manager you will notice they don’t show up in the VR ”+” menu. Two things get in the way:\n\n1. The ”+” menu only reads `~/.local/share/applications` , it ignores`XDG_DATA_DIRS` (which is where home manager’s`.desktop` files show up).\n2. The Steam session’s `PATH` doesn’t have`~/.nix-profile/bin` , so even if the entry showed up an`Exec=kitty` would fail to launch.\n\nSo I link every `.desktop` file from my profile into `~/.local/share/applications`, with the command rewritten to an absolute path:\n\n```\n{ config, pkgs, ... }:\nlet\n  profileBin = \"${config.home.profileDirectory}/bin\";\n  # The profile's desktop entries with Exec/TryExec pointing at the profile's bin.\n  # The profile link rather than a store path, so entries don't change every generation.\n  entries = pkgs.runCommand \"frame-desktop-entries\" { } ''\n    mkdir -p $out\n    for src in ${config.home.path}/share/applications/*.desktop; do\n      awk -v bin=${config.home.path}/bin -v profile=${profileBin} '\n        match($0, /^(TryExec|Exec)=/) {\n          key = substr($0, 1, RLENGTH)\n          rest = substr($0, RLENGTH + 1)\n          split(rest, words, \" \")\n          cmd = words[1]\n          if (cmd !~ /// && system(\"test -e \"\" bin \"/\" cmd \"\"\") == 0)\n            $0 = key profile \"/\" cmd substr(rest, length(cmd) + 1)\n        }\n        { print }\n      ' \"$src\" > \"$out/$(basename \"$src\")\"\n    done\n  '';\nin\n{\n  # Linked file by file (recursive) so Steam's own shortcuts in there stay.\n  xdg.dataFile.\"applications\" = {\n    source = entries;\n    recursive = true;\n  };\n}\n```\nThe plasma desktop has a similar `PATH` problem. It gets its environment from the systemd user manager, not a login shell, so you need this for launching from its menu to work:\n\n`systemd.user.sessionVariables.PATH = \"${config.home.profileDirectory}/bin:/nix/var/nix/profiles/default/bin${PATH:+:$PATH}\";`\n### Managing system files\n\nWith `targets.genericLinux.enable`, `home-manager switch` will ask you to run `non-nixos-gpu-setup`. This is needed to get GPU drivers working with nix built programs, since most of them look for GPU drivers in `/run/opengl-driver`.\n\nRunning that command adds a tmpfiles rule to symlink the right drivers into `/run/opengl-driver` on boot.\nThe problem is that rule is itself a symlink into the nix store, and tmpfiles runs before `/nix` is mounted. So on boot the rule can’t be read and you’re left without drivers.\nAlso any files you add to `/etc` get dropped on a SteamOS update unless they’re on the keep list in `/etc/atomic-update.conf.d/`.\n\nSo to handle this I created [steamos-etc](https://github.com/JRMurr/steamos-etc-nix), a home manager module with a small cli that lets you declare the `/etc` files you need and makes sure they survive SteamOS reboots/updates.\n\nThe [example flake](#home-manager) above already has it as an input and imports the module, so in your `home.nix` you just need to add:\n\n```\nprograms.steamos-etc = {\n  enable = true;\n  # Hold the user session until /nix is mounted.\n  waitForNix = true;\n  # /run/opengl-driver for Nix-built GUI apps.\n  gpuDrivers = true;\n};\n```\n`waitForNix` makes your user session wait for the nix mount. Without it your session can start before `/nix` is there, so any store links read at login dangle.\nThat includes home manager’s `environment.d` files (where plasma gets its `PATH` from) and `~/.config/user-dirs.dirs`, which `xdg-user-dirs-update` then helpfully replaces with a regular file.\n\n`gpuDrivers` sets up `/run/opengl-driver` with a real file instead of a store link. It replaces `non-nixos-gpu-setup` entirely (and silences home manager asking you to run it), so you don’t need to run that.\n\nHome manager only builds the files; the `steamos-etc` command actually installs them. Home manager activation can’t use `sudo`, so this is a separate step you run after every switch:\n\n`home-manager switch --flake <path to flake>#steamos -b backup && steamos-etc`\n(I would make an alias for the above)\n\nIt only asks for `sudo` when something actually changed. You don’t need `steamos-readonly disable` for this: `/etc` is a writable overlay (kept under `/var`). Its files just get dropped on updates unless they’re on the keep list, which is why `steamos-etc` adds every file it manages to `/etc/atomic-update.conf.d/steamos-etc.conf`.\n\nOn home manager switch a warning is displayed if `/etc` has drifted from your config, like after a rollback or a SteamOS update resetting `/etc`.\n\n#### Tailscale\n\n`steamos-etc` solves more problems than just the GPU drivers. You can use it to set up system services, written like home manager’s `systemd.user.services`.\nIt solves a similar problem to [system-manager](https://github.com/numtide/system-manager), but lets you stay in your home manager config and handles the SteamOS specific issues: `/nix` mounting late on boot and updates wiping `/etc`.\n\nFor example here is how you can set up tailscale:\n\n```\n{ pkgs, ... }:\nlet\n  tailscale = pkgs.tailscale;\nin\n{\n  home.packages = [ tailscale ];\n  programs.steamos-etc.services.tailscaled = {\n    Unit = {\n      Description = \"Tailscale node agent\";\n      Documentation = \"https://tailscale.com/docs/\";\n      Wants = [ \"network-pre.target\" ];\n      After = [\n        \"network-pre.target\"\n        \"NetworkManager.service\"\n        \"systemd-resolved.service\"\n      ];\n    };\n    Service = {\n      # 41641 is the default port the nixos tailscale module uses\n      ExecStart = \"${tailscale}/bin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=41641\";\n      ExecStopPost = \"${tailscale}/bin/tailscaled --cleanup\";\n      Restart = \"on-failure\";\n      Type = \"notify\";\n      RuntimeDirectory = \"tailscale\";\n      RuntimeDirectoryMode = \"0755\";\n      StateDirectory = \"tailscale\";\n      StateDirectoryMode = \"0700\";\n      CacheDirectory = \"tailscale\";\n      CacheDirectoryMode = \"0750\";\n    };\n    Install.WantedBy = [ \"multi-user.target\" ];\n  };\n}\n```\nAfter a switch, `steamos-etc` installs the unit and starts it (because of `Install.WantedBy`, which also starts it on every boot). Then just log in:\n\n`sudo tailscale up --operator=steamos`\nand it should “just work”. When the unit changes later (like a nixpkgs bump giving it a new tailscale), `steamos-etc` restarts it for you.\n\n## Frametop\n\nThe thing that excited me the most about the Steam Frame was the fact that it’s a full linux machine. I wanted to experiment with interesting development flows in VR.\n\n[frametop](https://github.com/DeeJanuz/frametop) greatly expands what you can do on the frame when it comes to managing desktops and programs in VR.\nIt also has (experimental) eye tracking as a mouse and hand tracking so you can use your frame like a poor man’s Apple Vision Pro (I guess not that poor given the frame’s price…).\n\nTo make it easy to manage frametop with home manager I created [frametop-nix](https://github.com/JRMurr/frametop-nix).\n\nLike steamos-etc, the [example flake](#home-manager) already has the input and imports the module, so you just need to add:\n\n`programs.frametop.enable = true;`\nThe `inputs.nixpkgs.follows` on the frametop input matters more than usual here. Frametop’s compositor gets Mesa from nixpkgs and the drivers in `/run/opengl-driver` come from your nixpkgs, and those need to be the same Mesa.\n(your nixpkgs also needs `wlroots_0_20`, which 26.05 has)\n\nAfter the first switch, restart SteamVR once so it loads the 3D mouse driver (this closes everything open in VR).\n\nNow you get the multi-screen desktop + 3D mouse. I’m surprised the 3D mouse isn’t built into SteamOS. By default your mouse is locked to one window, and you need to use the controller or headset to focus another window before the mouse works there. Frametop’s 3D mouse lets you move the mouse across all windows and move windows around in 3D space.\n\nGaze mode, hand tracking, and remote desktop aren’t packaged yet (but I will get them working soon™).\n\nFrametop is my current favorite thing on the frame. Since getting it set up I’ve been using my frame for basically all my computer tasks.\n","body_html":"<h1 id=\"nix-on-the-steam-frame\">Nix on the Steam Frame</h1>\n<p>I got my Steam Frame and ofc the first thing I wanted to figure out was getting nix installed on it. Thankfully the Steam Deck runs a similar OS setup, so all the work people did to get nix working on the deck basically made the frame “just work”.</p>\n<p>So here’s how you can get nix installed and some cool things you can do with it once it’s set up.</p>\n<h2 id=\"install-steps\">Install steps</h2>\n<h3 id=\"nix\">Nix</h3>\n<pre><code>passwd                                # no password is set by default, need one for sudo\nsudo steamos-readonly disable         # make root file system writable\nsudo mkdir -p /etc/tmpfiles.d         # The installer expects this path to exist\ncurl -fsSL -o nix-installer.sh https://artifacts.nixos.org/nix-installer\nless nix-installer.sh                 # give the install a spot check\nsh nix-installer.sh install steam-deck --enable-flakes\nsudo steamos-readonly enable          # go back to read only root</code></pre>\n<p>After that nix should be set up. The official installer’s Steam Deck mode works without issue on the frame and handles SteamOS’s immutable root for you.</p>\n<p>This works by keeping all of <code>/nix</code> at <code>/home/nix</code> (which survives SteamOS updates) and on boot bind mounting it to <code>/nix</code> with a <code>nix.mount</code> systemd unit.</p>\n<p>One thing to keep in mind, SteamOS updates replace the root file system. Your store and anything under <code>/home</code> is fine, but any edits you make to <code>/etc/nix/nix.conf</code> (like adding yourself to <code>trusted-users</code>) will need to be redone after an update.</p>\n<h3 id=\"home-manager\">Home manager</h3>\n<p>Just having nix installed is fine, but the main reason I love nix so much is managing all my apps and settings declaratively. Running full NixOS on the frame would be a little crazy. I’m sure you could figure something out, but just using home manager to manage dotfiles is a good balance for me.</p>\n<p>Installing home manager itself works the usual standalone way. Getting it to play nice with SteamOS takes a fair bit of frame specific config though, which is most of the rest of this post.</p>\n<p>I already have a nix flake for my <a href=\"https://github.com/JRMurr/NixOsConfig\" rel=\"nofollow ugc noopener\">NixOS config</a>, so I went with the flake install approach.</p>\n<p>Here is an example flake setup for the frame. It already has the inputs for the other modules I cover later in the post (<a href=\"#managing-system-files\">steamos-etc</a> and <a href=\"#frametop\">frametop</a>). You can drop them if you don’t want them.</p>\n<pre><code># flake.nix\n{\n  inputs = {\n    nixpkgs.url = &quot;github:NixOS/nixpkgs/nixos-unstable&quot;;\n    home-manager = {\n      url = &quot;github:nix-community/home-manager&quot;;\n      inputs.nixpkgs.follows = &quot;nixpkgs&quot;;\n    };\n    # I&#39;ll explain these later on in the blog, you can ignore these if you want\n    steamos-etc = {\n      url = &quot;github:JRMurr/steamos-etc-nix&quot;;\n      inputs.nixpkgs.follows = &quot;nixpkgs&quot;;\n    };\n    frametop = {\n      url = &quot;github:JRMurr/frametop-nix&quot;;\n      inputs.nixpkgs.follows = &quot;nixpkgs&quot;;\n    };\n  };\n  outputs = { nixpkgs, home-manager, ... }@inputs: {\n    homeConfigurations.steamos = home-manager.lib.homeManagerConfiguration {\n      pkgs = nixpkgs.legacyPackages.aarch64-linux;\n      # makes `inputs` available as a module argument in home.nix\n      extraSpecialArgs = { inherit inputs; };\n      modules = [ ./home.nix ];\n    };\n  };\n}</code></pre>\n<pre><code># home.nix\n{ inputs, ... }:\n{\n  imports = [\n    # again these are sorta optional, ignore if you want\n    inputs.steamos-etc.homeManagerModules.default\n    inputs.frametop.homeManagerModules.default\n  ];\n  home.username = &quot;steamos&quot;;\n  home.homeDirectory = &quot;/home/steamos&quot;;\n  home.stateVersion = &quot;26.05&quot;;\n  targets.genericLinux.enable = true;  # enables some options to make home manager work better on non-nixos setups\n  programs.home-manager.enable = true; # standalone installs need this to get the home-manager cli\n}</code></pre>\n<p>Then to get home manager set up you can run:</p>\n<pre><code># cd to dir with flake\nnix build &#39;.#homeConfigurations.&quot;steamos&quot;.activationPackage&#39;\nHOME_MANAGER_BACKUP_EXT=backup ./result/activate</code></pre>\n<p>(if you see <code>User systemd daemon not running. Skipping reload.</code> in the output, see the <a href=\"#nested-sessions\">Nested sessions section</a>)</p>\n<p><code>HOME_MANAGER_BACKUP_EXT=backup</code> will back up (by renaming) any existing files that home manager will now manage.</p>\n<p>You only need to build and run <code>activate</code> like this the first time, to get home manager installed.\nAfter it’s set up you can run</p>\n<p><code>home-manager switch --flake &lt;path to flake&gt;#steamos -b backup</code>\nto update your config going forward. Keep the <code>-b backup</code> around, SteamOS updates like to put their dotfiles back.</p>\n<h4 id=\"nested-sessions\">Nested sessions</h4>\n<p>One thing that will probably bite you when you run <code>home-manager switch</code> is the nested plasma session. Gamescope is the actual session on the frame, it’s running the SteamOS dashboard and the “vr stuff”.\nWhen you open the “Desktop” app it runs <code>/usr/bin/steamos-nested-desktop</code>, which starts plasma <em>inside</em> that session. To keep the two from stepping on each other it does roughly:</p>\n<pre><code>export XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR/nested_plasma\ndbus-run-session startplasma-wayland</code></pre>\n<p>So anything you launch from that desktop (and later with frametop, which does the same thing with <code>.../frametop</code>) gets a runtime dir of <code>/run/user/1000/nested_plasma</code> and its own private D-Bus.\nYour user’s systemd manager is still at <code>/run/user/1000</code> though, so anything that looks for it through those vars can’t find it.</p>\n<p>Home manager is one of those things. The switch won’t fail. It will write all your files and just print:</p>\n<p><code>User systemd daemon not running. Skipping reload.</code>\nThis means any new or changed user services won’t be started/restarted until your next login.</p>\n<p>To fix it, point the switch back at the real session:</p>\n<p><code>env XDG_RUNTIME_DIR=/run/user/$(id -u) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus home-manager switch --flake &lt;path to flake&gt;#steamos -b backup</code>\nThe same prefix works for the first <code>./result/activate</code> too.</p>\n<p>You’ll hit the same problem with <code>systemctl --user</code>, and the same env override fixes it.</p>\n<p>This class of issue should not affect you if you connect over ssh or launch a terminal from the SteamOS dashboard directly, since Steam itself runs with the real <code>/run/user/1000</code> and bus.</p>\n<h4 id=\"showing-apps-in-the-launcher\">Showing apps in the launcher</h4>\n<p>Once you start installing GUI apps with home manager you will notice they don’t show up in the VR ”+” menu. Two things get in the way:</p>\n<ol><li>The ”+” menu only reads <code>~/.local/share/applications</code> , it ignores<code>XDG_DATA_DIRS</code> (which is where home manager’s<code>.desktop</code> files show up).</li><li>The Steam session’s <code>PATH</code> doesn’t have<code>~/.nix-profile/bin</code> , so even if the entry showed up an<code>Exec=kitty</code> would fail to launch.</li></ol>\n<p>So I link every <code>.desktop</code> file from my profile into <code>~/.local/share/applications</code>, with the command rewritten to an absolute path:</p>\n<pre><code>{ config, pkgs, ... }:\nlet\n  profileBin = &quot;${config.home.profileDirectory}/bin&quot;;\n  # The profile&#39;s desktop entries with Exec/TryExec pointing at the profile&#39;s bin.\n  # The profile link rather than a store path, so entries don&#39;t change every generation.\n  entries = pkgs.runCommand &quot;frame-desktop-entries&quot; { } &#39;&#39;\n    mkdir -p $out\n    for src in ${config.home.path}/share/applications/*.desktop; do\n      awk -v bin=${config.home.path}/bin -v profile=${profileBin} &#39;\n        match($0, /^(TryExec|Exec)=/) {\n          key = substr($0, 1, RLENGTH)\n          rest = substr($0, RLENGTH + 1)\n          split(rest, words, &quot; &quot;)\n          cmd = words[1]\n          if (cmd !~ /// &amp;&amp; system(&quot;test -e &quot;&quot; bin &quot;/&quot; cmd &quot;&quot;&quot;) == 0)\n            $0 = key profile &quot;/&quot; cmd substr(rest, length(cmd) + 1)\n        }\n        { print }\n      &#39; &quot;$src&quot; &gt; &quot;$out/$(basename &quot;$src&quot;)&quot;\n    done\n  &#39;&#39;;\nin\n{\n  # Linked file by file (recursive) so Steam&#39;s own shortcuts in there stay.\n  xdg.dataFile.&quot;applications&quot; = {\n    source = entries;\n    recursive = true;\n  };\n}</code></pre>\n<p>The plasma desktop has a similar <code>PATH</code> problem. It gets its environment from the systemd user manager, not a login shell, so you need this for launching from its menu to work:</p>\n<p><code>systemd.user.sessionVariables.PATH = &quot;${config.home.profileDirectory}/bin:/nix/var/nix/profiles/default/bin${PATH:+:$PATH}&quot;;</code></p>\n<h3 id=\"managing-system-files\">Managing system files</h3>\n<p>With <code>targets.genericLinux.enable</code>, <code>home-manager switch</code> will ask you to run <code>non-nixos-gpu-setup</code>. This is needed to get GPU drivers working with nix built programs, since most of them look for GPU drivers in <code>/run/opengl-driver</code>.</p>\n<p>Running that command adds a tmpfiles rule to symlink the right drivers into <code>/run/opengl-driver</code> on boot.\nThe problem is that rule is itself a symlink into the nix store, and tmpfiles runs before <code>/nix</code> is mounted. So on boot the rule can’t be read and you’re left without drivers.\nAlso any files you add to <code>/etc</code> get dropped on a SteamOS update unless they’re on the keep list in <code>/etc/atomic-update.conf.d/</code>.</p>\n<p>So to handle this I created <a href=\"https://github.com/JRMurr/steamos-etc-nix\" rel=\"nofollow ugc noopener\">steamos-etc</a>, a home manager module with a small cli that lets you declare the <code>/etc</code> files you need and makes sure they survive SteamOS reboots/updates.</p>\n<p>The <a href=\"#home-manager\">example flake</a> above already has it as an input and imports the module, so in your <code>home.nix</code> you just need to add:</p>\n<pre><code>programs.steamos-etc = {\n  enable = true;\n  # Hold the user session until /nix is mounted.\n  waitForNix = true;\n  # /run/opengl-driver for Nix-built GUI apps.\n  gpuDrivers = true;\n};</code></pre>\n<p><code>waitForNix</code> makes your user session wait for the nix mount. Without it your session can start before <code>/nix</code> is there, so any store links read at login dangle.\nThat includes home manager’s <code>environment.d</code> files (where plasma gets its <code>PATH</code> from) and <code>~/.config/user-dirs.dirs</code>, which <code>xdg-user-dirs-update</code> then helpfully replaces with a regular file.</p>\n<p><code>gpuDrivers</code> sets up <code>/run/opengl-driver</code> with a real file instead of a store link. It replaces <code>non-nixos-gpu-setup</code> entirely (and silences home manager asking you to run it), so you don’t need to run that.</p>\n<p>Home manager only builds the files; the <code>steamos-etc</code> command actually installs them. Home manager activation can’t use <code>sudo</code>, so this is a separate step you run after every switch:</p>\n<p><code>home-manager switch --flake &lt;path to flake&gt;#steamos -b backup &amp;&amp; steamos-etc</code>\n(I would make an alias for the above)</p>\n<p>It only asks for <code>sudo</code> when something actually changed. You don’t need <code>steamos-readonly disable</code> for this: <code>/etc</code> is a writable overlay (kept under <code>/var</code>). Its files just get dropped on updates unless they’re on the keep list, which is why <code>steamos-etc</code> adds every file it manages to <code>/etc/atomic-update.conf.d/steamos-etc.conf</code>.</p>\n<p>On home manager switch a warning is displayed if <code>/etc</code> has drifted from your config, like after a rollback or a SteamOS update resetting <code>/etc</code>.</p>\n<h4 id=\"tailscale\">Tailscale</h4>\n<p><code>steamos-etc</code> solves more problems than just the GPU drivers. You can use it to set up system services, written like home manager’s <code>systemd.user.services</code>.\nIt solves a similar problem to <a href=\"https://github.com/numtide/system-manager\" rel=\"nofollow ugc noopener\">system-manager</a>, but lets you stay in your home manager config and handles the SteamOS specific issues: <code>/nix</code> mounting late on boot and updates wiping <code>/etc</code>.</p>\n<p>For example here is how you can set up tailscale:</p>\n<pre><code>{ pkgs, ... }:\nlet\n  tailscale = pkgs.tailscale;\nin\n{\n  home.packages = [ tailscale ];\n  programs.steamos-etc.services.tailscaled = {\n    Unit = {\n      Description = &quot;Tailscale node agent&quot;;\n      Documentation = &quot;https://tailscale.com/docs/&quot;;\n      Wants = [ &quot;network-pre.target&quot; ];\n      After = [\n        &quot;network-pre.target&quot;\n        &quot;NetworkManager.service&quot;\n        &quot;systemd-resolved.service&quot;\n      ];\n    };\n    Service = {\n      # 41641 is the default port the nixos tailscale module uses\n      ExecStart = &quot;${tailscale}/bin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=41641&quot;;\n      ExecStopPost = &quot;${tailscale}/bin/tailscaled --cleanup&quot;;\n      Restart = &quot;on-failure&quot;;\n      Type = &quot;notify&quot;;\n      RuntimeDirectory = &quot;tailscale&quot;;\n      RuntimeDirectoryMode = &quot;0755&quot;;\n      StateDirectory = &quot;tailscale&quot;;\n      StateDirectoryMode = &quot;0700&quot;;\n      CacheDirectory = &quot;tailscale&quot;;\n      CacheDirectoryMode = &quot;0750&quot;;\n    };\n    Install.WantedBy = [ &quot;multi-user.target&quot; ];\n  };\n}</code></pre>\n<p>After a switch, <code>steamos-etc</code> installs the unit and starts it (because of <code>Install.WantedBy</code>, which also starts it on every boot). Then just log in:</p>\n<p><code>sudo tailscale up --operator=steamos</code>\nand it should “just work”. When the unit changes later (like a nixpkgs bump giving it a new tailscale), <code>steamos-etc</code> restarts it for you.</p>\n<h2 id=\"frametop\">Frametop</h2>\n<p>The thing that excited me the most about the Steam Frame was the fact that it’s a full linux machine. I wanted to experiment with interesting development flows in VR.</p>\n<p><a href=\"https://github.com/DeeJanuz/frametop\" rel=\"nofollow ugc noopener\">frametop</a> greatly expands what you can do on the frame when it comes to managing desktops and programs in VR.\nIt also has (experimental) eye tracking as a mouse and hand tracking so you can use your frame like a poor man’s Apple Vision Pro (I guess not that poor given the frame’s price…).</p>\n<p>To make it easy to manage frametop with home manager I created <a href=\"https://github.com/JRMurr/frametop-nix\" rel=\"nofollow ugc noopener\">frametop-nix</a>.</p>\n<p>Like steamos-etc, the <a href=\"#home-manager\">example flake</a> already has the input and imports the module, so you just need to add:</p>\n<p><code>programs.frametop.enable = true;</code>\nThe <code>inputs.nixpkgs.follows</code> on the frametop input matters more than usual here. Frametop’s compositor gets Mesa from nixpkgs and the drivers in <code>/run/opengl-driver</code> come from your nixpkgs, and those need to be the same Mesa.\n(your nixpkgs also needs <code>wlroots_0_20</code>, which 26.05 has)</p>\n<p>After the first switch, restart SteamVR once so it loads the 3D mouse driver (this closes everything open in VR).</p>\n<p>Now you get the multi-screen desktop + 3D mouse. I’m surprised the 3D mouse isn’t built into SteamOS. By default your mouse is locked to one window, and you need to use the controller or headset to focus another window before the mouse works there. Frametop’s 3D mouse lets you move the mouse across all windows and move windows around in 3D space.</p>\n<p>Gaze mode, hand tracking, and remote desktop aren’t packaged yet (but I will get them working soon™).</p>\n<p>Frametop is my current favorite thing on the frame. Since getting it set up I’ve been using my frame for basically all my computer tasks.</p>","headings":[{"level":1,"text":"Nix on the Steam Frame","id":"nix-on-the-steam-frame"},{"level":2,"text":"Install steps","id":"install-steps"},{"level":3,"text":"Nix","id":"nix"},{"level":3,"text":"Home manager","id":"home-manager"},{"level":3,"text":"Managing system files","id":"managing-system-files"},{"level":2,"text":"Frametop","id":"frametop"}]}}