mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 01:00:13 +02:00
Keyring launchers, Firefox default browser, rootless Docker
- keyring.flatpaks / keyring.programs: desktop entries shadowing an app's own that run it on the outer bus (one kwalletd6 for both sessions), with the wallet's D-Bus names as `flatpak run --talk-name` options (no Flatpak overrides), --password-store=kwallet6 for Electron, and login callback schemes as default + recommended handlers. - firefox.defaultBrowser: the launcher as default for http, https and text/html. - docker: rootless dockerd as a user service, socket in the outer runtime dir, CLI with DOCKER_HOST for both sessions.
This commit is contained in:
1 parent
aa9ca183f1
commit
b0131338de
10 files changed
+617
-14
No files matched your search
@@ -3,8 +3,8 @@
|
||||
[Home Manager](https://github.com/nix-community/home-manager) modules for the
|
||||
Valve Steam Frame (SteamOS, `aarch64-linux`, standalone home-manager). They
|
||||
work around quirks of the Frame's [two graphical sessions](#two-sessions)
|
||||
(portal, keyboard layout, clipboard, Firefox), enable hardware video decoding
|
||||
in Jellyfin, and extend Steam's and SteamVR's UIs at runtime (VR keyboard,
|
||||
(portal, 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).
|
||||
|
||||
@@ -50,8 +50,10 @@ configuration, limitations and how it works.
|
||||
|
||||
**Apps:**
|
||||
|
||||
- [Firefox](docs/firefox.md) (`firefox`): launcher for the Flatpak with a VR fullscreen fix, optional AV1 off, a separate desktop profile.
|
||||
- [Firefox](docs/firefox.md) (`firefox`): launcher for the Flatpak with a VR fullscreen fix, optional AV1 off, a separate desktop profile, optionally the default browser.
|
||||
- [Jellyfin](docs/jellyfin.md) (`jellyfin.hardwareDecoding`): hardware video decoding in the Jellyfin Desktop Flatpak.
|
||||
- [Keyring launchers](docs/keyring.md) (`keyring.flatpaks`, `keyring.programs`): apps (Flatpak or Nix, Electron too) keep their KDE wallet logins in both sessions.
|
||||
- [Docker](docs/docker.md) (`docker`): rootless Docker as a user service, CLI working in both sessions.
|
||||
|
||||
**Your own patches** of Steam's UI, and fixing patches after a Steam update: [UI patches](docs/ui-patches.md).
|
||||
|
||||
@@ -111,8 +113,7 @@ What this means for you:
|
||||
([session settings](docs/session.md#session-settings-and-services)).
|
||||
- **Wallet:** there should be one `kwalletd6`, on the outer bus; apps started
|
||||
from the desktop would otherwise start a second one whose secrets VR can't
|
||||
see. Prefix such launchers' `Exec=` with `steamFrame.session.busEnv`
|
||||
([example](docs/session.md#session-settings-and-services)).
|
||||
see. List such apps in [keyring](docs/keyring.md).
|
||||
- **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.
|
||||
@@ -190,7 +191,14 @@ these two files ([`template/`](template), with more comments):
|
||||
};
|
||||
firefox.enable = true;
|
||||
firefox.disableAv1 = true;
|
||||
firefox.defaultBrowser = true;
|
||||
jellyfin.hardwareDecoding.enable = true; # install the Flatpak yourself
|
||||
keyring.flatpaks."im.riot.Riot" = { # Element: logins in both sessions
|
||||
name = "Element";
|
||||
electron = true;
|
||||
schemeHandlers = [ "element" "io.element.desktop" ];
|
||||
};
|
||||
docker.enable = true; # rootless
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -205,7 +213,7 @@ home-manager switch --flake .#steamos # manual setup, from the flake's director
|
||||
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}`.
|
||||
`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,keyring,docker}`.
|
||||
Every module imports `cleanup` (see
|
||||
[Changes outside Nix](#changes-outside-nix-exceptions)).
|
||||
|
||||
@@ -284,9 +292,29 @@ Every module imports `cleanup` (see
|
||||
| `steamFrame.firefox.disableAv1` | bool | `false` | Default `media.av1.enabled` to `false`: the Frame's decoder driver has no AV1, so sites send VP9/H.264, decoded in hardware. |
|
||||
| `steamFrame.firefox.prefs` | attrs of bool, int or str | `{ }` | Further `about:config` default values for every profile (override the fixes too). |
|
||||
| `steamFrame.firefox.desktopProfile` | null or str | `"desktop"` | Separate profile (directory name) for the nested desktop; `null`: the default profile in both sessions. |
|
||||
| `steamFrame.firefox.defaultBrowser` | bool | `false` | Default for `http`, `https`, `text/html` (`xdg.mimeApps`). |
|
||||
| `steamFrame.jellyfin.hardwareDecoding.enable` | bool | `false` | Hardware video decoding in the Jellyfin Desktop Flatpak, see [Jellyfin](docs/jellyfin.md). |
|
||||
| `steamFrame.jellyfin.hardwareDecoding.hwdec` | str | `"v4l2m2m-copy,auto-copy"` | mpv `hwdec` used instead of Jellyfin's automatic one. |
|
||||
| `steamFrame.jellyfin.hardwareDecoding.command` | str, read-only | | The `flatpak run …` command line of the desktop entry, for a terminal. |
|
||||
| `steamFrame.keyring.flatpaks` | attrs of submodules | `{ }` | Flatpaks by app ID getting a wallet launcher (outer bus, wallet D-Bus names), see [Keyring launchers](docs/keyring.md). Fields below. |
|
||||
| `steamFrame.keyring.programs` | attrs of submodules | `{ }` | Other programs by desktop ID (without `.desktop`) getting a wallet launcher (outer bus). Fields below. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.name` | str | required | `Name=`. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.genericName`, `.comment`, `.startupWMClass` | null or str | `null` | `GenericName=`, `Comment=`, `StartupWMClass=`. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.icon` | null or str | flatpaks: the app ID; programs: `null` | `Icon=`. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.categories` | list of str | `[ ]` | `Categories=`. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.schemeHandlers` | list of str | `[ ]` | URL schemes (e.g. login callbacks) the app handles and is made the default and recommended handler for. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.mimeTypes` | list of str | `[ ]` | Further `MimeType=` entries, not made default. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.electron` | bool | `false` | Pass `--password-store=kwallet6` (Electron apps). |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.args` | list of str | `[ ]` | Further app arguments (desktop entry syntax). |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.fieldCode` | `"%U"`, `"%u"`, `"%F"`, `"%f"`, `""` | `"%U"` | How the entry passes URLs/files. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.actions` | attrs of `{ name; args; }` | `{ }` | Desktop actions, each running the command with its args. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.settings` | attrs of str | `{ }` | Further `[Desktop Entry]` keys. |
|
||||
| `steamFrame.keyring.{flatpaks,programs}.<id>.command` | str, read-only | | The command line without args, for a terminal. |
|
||||
| `steamFrame.keyring.flatpaks.<id>.flatpakArgs` | list of str | `[ ]` | Further `flatpak run` options. |
|
||||
| `steamFrame.keyring.programs.<id>.executable` | str | required | The program to run, e.g. `"${pkgs.claude-desktop}/bin/claude-desktop"`. |
|
||||
| `steamFrame.docker.enable` | bool | `false` | Rootless Docker as a user service, CLI for both sessions, see [Docker](docs/docker.md). |
|
||||
| `steamFrame.docker.package` | package | `pkgs.docker` | Docker package (daemon and CLI). |
|
||||
| `steamFrame.docker.host` | str, read-only | `"unix://${runtimeDir}/docker.sock"` | The daemon's `DOCKER_HOST` (the CLI's default). |
|
||||
| `steamFrame.cleanup.package` | package, read-only | | `steam-frame-nix-cleanup` (on `PATH` too), see [Changes outside Nix](#changes-outside-nix-exceptions). |
|
||||
|
||||
Renamed options still work under their old names, with a warning:
|
||||
@@ -341,8 +369,10 @@ versions left: [docs/cleanup.md](docs/cleanup.md).
|
||||
- clipboard-sync runs from KDE autostart (a Home Manager link).
|
||||
- Firefox: the desktop profile's `user.js` link exists only while its
|
||||
Firefox runs (see [Firefox](docs/firefox.md#how-it-works)).
|
||||
- Jellyfin: the hardware decoding permissions are `flatpak run` options of
|
||||
the desktop entry, not a Flatpak override.
|
||||
- Jellyfin, keyring launchers: 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
|
||||
|
||||
@@ -369,8 +399,18 @@ versions left: [docs/cleanup.md](docs/cleanup.md).
|
||||
|
||||
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.
|
||||
data), secrets apps stored in the KDE wallet, and whatever apps keep in
|
||||
`~/.var/app/*`, Flatpak apps and their runtimes.
|
||||
|
||||
Docker's images, containers and volumes (`~/.local/share/docker`) are partly
|
||||
owned by the subordinate UIDs of your containers, so a plain `rm` fails.
|
||||
With the daemon running, then stopped:
|
||||
|
||||
```sh
|
||||
docker system prune -a --volumes
|
||||
systemctl --user stop docker
|
||||
nix shell nixpkgs#rootlesskit -c rootlesskit rm -rf ~/.local/share/docker
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Rootless Docker
|
||||
|
||||
`docker.*`, module `docker`. Options:
|
||||
[README, Options](../README.md#options).
|
||||
|
||||
## Problem
|
||||
|
||||
SteamOS has no Docker, and Home Manager can't install the usual root
|
||||
daemon. Rootless Docker's default socket is in `$XDG_RUNTIME_DIR`, which
|
||||
differs between the Frame's [two sessions](../README.md#two-sessions), and
|
||||
the nested desktop can't reach the user service manager.
|
||||
|
||||
## What you get
|
||||
|
||||
- `dockerd` in rootless mode as the systemd user service `docker.service`
|
||||
of the Steam session's user manager, started at login and on switch
|
||||
(never restarted by a switch, which would stop running containers).
|
||||
- The `docker` CLI on `PATH`, whose default `DOCKER_HOST` is the socket in
|
||||
the outer runtime dir (`docker.host`,
|
||||
`unix:///run/user/1000/docker.sock`): it works in both sessions. A
|
||||
`DOCKER_HOST` you set yourself wins.
|
||||
|
||||
SteamOS already has what rootless Docker needs: `newuidmap`/`newgidmap`
|
||||
with their capabilities, `/etc/subuid` and `/etc/subgid` entries for the
|
||||
user, user namespaces and cgroup v2 delegation.
|
||||
|
||||
## Configuration
|
||||
|
||||
```nix
|
||||
steamFrame.docker.enable = true;
|
||||
```
|
||||
|
||||
Then `docker run --rm hello-world`. `docker.package` replaces the Docker
|
||||
package (daemon and CLI).
|
||||
|
||||
## Caveats
|
||||
|
||||
- Rootless limits apply: no ports below 1024 without extra setup,
|
||||
`--network host` is the rootless network namespace, containers can't
|
||||
gain real root.
|
||||
- Images, containers and volumes are app data in `~/.local/share/docker`,
|
||||
see [App data you create](../README.md#app-data-you-create) for how to
|
||||
remove them.
|
||||
- Disabling the module removes the unit but doesn't stop a running daemon:
|
||||
run `systemctl --user stop docker` first (or add `docker.service` to
|
||||
`session.services.stop` for that switch).
|
||||
- Another `docker` package in `home.packages` collides with the wrapped
|
||||
CLI.
|
||||
|
||||
## How it works
|
||||
|
||||
The user unit runs `dockerd-rootless` (RootlessKit) with `PATH=/usr/bin`,
|
||||
for SteamOS's `newuidmap`/`newgidmap`, and `Delegate=yes`. The unit is
|
||||
added to `session.services.start`, so each switch starts it if it isn't
|
||||
running. The CLI is the package's `docker` wrapped with a default
|
||||
`DOCKER_HOST`; nothing is written outside the store (no `daemon.json`, no
|
||||
Docker context).
|
||||
+8
-2
@@ -7,8 +7,10 @@
|
||||
|
||||
In the Steam session gamescope never shows fullscreen windows, so Firefox
|
||||
looks frozen when a page goes fullscreen; sites send AV1, which the Frame
|
||||
decodes in software; and the two sessions can't see each other's Firefox,
|
||||
so a second instance stops at the locked profile.
|
||||
decodes in software; the two sessions can't see each other's Firefox,
|
||||
so a second instance stops at the locked profile; and without a default
|
||||
browser the portal opens links with the first installed `https` handler
|
||||
(e.g. Chromium), in both sessions.
|
||||
|
||||
## What you get
|
||||
|
||||
@@ -26,6 +28,9 @@ ID), so default-browser associations keep working.
|
||||
- **`desktopProfile`** (`"desktop"`): in the nested desktop the launcher
|
||||
uses this separate profile (a normal Firefox profile with its own browser
|
||||
data, created on first use). `null`: the default profile in both sessions.
|
||||
- **`defaultBrowser`** (off): the launcher becomes the default for `http`,
|
||||
`https` and `text/html` (Home Manager's `xdg.mimeApps`, which then owns
|
||||
`~/.config/mimeapps.list`).
|
||||
|
||||
`prefs` and the fixes are *default* values, not user values: `about:config`
|
||||
can still change them per profile, and removing one leaves nothing behind.
|
||||
@@ -37,6 +42,7 @@ Changes take effect at the next start of Firefox.
|
||||
steamFrame.firefox = {
|
||||
enable = true;
|
||||
disableAv1 = true;
|
||||
defaultBrowser = true;
|
||||
prefs."browser.startup.page" = 3; # restore the previous session
|
||||
};
|
||||
```
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# Keyring launchers
|
||||
|
||||
`keyring.flatpaks.<app ID>`, `keyring.programs.<desktop ID>`, module
|
||||
`keyring`. Options: [README, Options](../README.md#options).
|
||||
|
||||
## Problem
|
||||
|
||||
Apps that keep logins or passwords in the KDE wallet lose them between the
|
||||
Frame's [two sessions](../README.md#two-sessions):
|
||||
|
||||
- **A second wallet:** the nested desktop has its own D-Bus. An app started
|
||||
there starts a second `kwalletd6` on that bus; what it stores there is
|
||||
invisible to the same app in the Steam session, which talks to the running
|
||||
`kwalletd6` on the outer bus.
|
||||
- **Electron in the Steam session:** Electron picks its keyring from
|
||||
`XDG_CURRENT_DESKTOP`. In the Steam session that is `gamescope`, which it
|
||||
doesn't know, so it falls back to `basic` (a local, unencrypted store) and
|
||||
the login made in the desktop (stored in the wallet) is gone.
|
||||
- **Flatpak permissions:** many Flatpaks may not talk to the wallet at all
|
||||
(Element), or only to `org.kde.kwalletd6` while their KF6 wallet client
|
||||
reads through the Secret Service `org.freedesktop.secrets` (KRDC: "Password
|
||||
not found").
|
||||
- **Login callbacks:** SSO logins come back through a URL scheme
|
||||
(`io.element.desktop://`, `claude://`) opened by the portal. Unless the app
|
||||
is the default and a recommended handler, the portal opens an app chooser,
|
||||
which isn't shown in VR.
|
||||
|
||||
## What you get
|
||||
|
||||
For each listed app, a desktop entry in `~/.local/share/applications` with
|
||||
the app's own desktop ID, so it replaces the Flatpak's or package's entry in
|
||||
the KDE menu and the "+" menu. It starts the app
|
||||
|
||||
- on the outer bus (`session.busEnv`): one `kwalletd6` for both sessions;
|
||||
- for Flatpaks, with `--talk-name=org.kde.kwalletd6` and
|
||||
`--talk-name=org.freedesktop.secrets` as `flatpak run` options (not a
|
||||
Flatpak override: they apply only to launches from this entry and are
|
||||
gone with it);
|
||||
- with `electron = true`, with `--password-store=kwallet6`;
|
||||
- as the default and recommended handler of its `schemeHandlers`
|
||||
(`xdg.mimeApps`).
|
||||
|
||||
Logins then survive switching between the desktop and VR windows. Changes
|
||||
take effect at the next start of the app.
|
||||
|
||||
## Configuration
|
||||
|
||||
The Flatpak isn't in the Nix store, so its entry can't be read at build
|
||||
time: give the fields you want in menus (`name` is required; `icon`
|
||||
defaults to the app ID, as Flatpaks export it). A Flatpak (installing it is
|
||||
up to you):
|
||||
|
||||
```nix
|
||||
steamFrame.keyring.flatpaks = {
|
||||
"im.riot.Riot" = {
|
||||
name = "Element";
|
||||
electron = true;
|
||||
categories = [ "Network" "InstantMessaging" ];
|
||||
schemeHandlers = [ "element" "io.element.desktop" ]; # SSO callback
|
||||
};
|
||||
"org.kde.krdc" = {
|
||||
name = "KRDC";
|
||||
fieldCode = "%u";
|
||||
categories = [ "Qt" "KDE" "Network" "RemoteAccess" ];
|
||||
mimeTypes = [ "x-scheme-handler/vnc" "x-scheme-handler/rdp" ]; # listed, not made default
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
A program from a Nix package: use the package's own desktop ID (without
|
||||
`.desktop`), so the entry replaces the package's:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }: {
|
||||
home.packages = [ pkgs.claude-desktop ];
|
||||
steamFrame.keyring.programs."com.anthropic.Claude" = {
|
||||
name = "Claude";
|
||||
executable = "${pkgs.claude-desktop}/bin/claude-desktop";
|
||||
icon = "claude-desktop";
|
||||
electron = true;
|
||||
startupWMClass = "com.anthropic.Claude";
|
||||
schemeHandlers = [ "claude" ]; # login callback
|
||||
settings.StartupNotify = "true";
|
||||
actions.NewChat = {
|
||||
name = "New Chat";
|
||||
args = [ ''"claude://claude.ai/new?surface=chat&source=desktop_action"'' ];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`args`, `actions.<name>.args` and `settings` are written into the entry as
|
||||
given (desktop entry syntax: quote yourself). Each app's `command` (read
|
||||
only) is its command line without arguments, to start it from a terminal
|
||||
the same way. The fields each entry takes are in the
|
||||
[options](../README.md#options).
|
||||
|
||||
## Caveats
|
||||
|
||||
- The wallet must be KDE's (`kwalletd6`, and `ksecretd` for the Secret
|
||||
Service), as SteamOS ships it.
|
||||
- Only launches from the entry get the fixes: a Flatpak started with plain
|
||||
`flatpak run`, or a program from `~/.nix-profile/bin`, doesn't.
|
||||
- `schemeHandlers` turns on Home Manager's `xdg.mimeApps`, which then owns
|
||||
`~/.config/mimeapps.list`: set other defaults there too.
|
||||
- A Flatpak's own entry may have fields not given here (translations,
|
||||
`Keywords`, actions); add what you need through `settings` and `actions`.
|
||||
|
||||
## How it works
|
||||
|
||||
For `"im.riot.Riot" = { name = "Element"; electron = true; ... }` the entry's
|
||||
command line is
|
||||
|
||||
```sh
|
||||
env DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
|
||||
flatpak run --talk-name=org.kde.kwalletd6 --talk-name=org.freedesktop.secrets \
|
||||
im.riot.Riot --password-store=kwallet6 %U
|
||||
```
|
||||
|
||||
`flatpak run` connects the sandbox's D-Bus proxy to the bus in its own
|
||||
environment, so the prefix (not `--env=`) moves the app to the outer bus.
|
||||
`--talk-name` adds to the Flatpak's permissions for this launch only. The
|
||||
entry also sets `X-Flatpak=<app ID>`. For `programs` the prefix runs
|
||||
`executable` directly.
|
||||
|
||||
`schemeHandlers` go into `xdg.mimeApps.defaultApplications` and
|
||||
`associations.added` (Added Associations, what the portal treats as
|
||||
recommended), and into the entry's `MimeType=` with `mimeTypes`.
|
||||
+3
-2
@@ -23,8 +23,9 @@ running") and skips `reloadSystemd`.
|
||||
and applies `session.services.start` / `stop` / `restart`, which other
|
||||
modules fill (you can add your own units).
|
||||
|
||||
**Configuration:** a launcher for an app that must use the single wallet on
|
||||
the outer bus (see [Two sessions](../README.md#two-sessions)):
|
||||
**Configuration:** apps that keep secrets in the wallet get launchers from
|
||||
[keyring](keyring.md). Another launcher that must reach the outer session
|
||||
(its bus, services or wallet) uses the prefix:
|
||||
|
||||
```nix
|
||||
{ config, ... }: {
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
};
|
||||
firefox = ./modules/firefox.nix;
|
||||
jellyfin = ./modules/jellyfin.nix;
|
||||
keyring = ./modules/keyring.nix;
|
||||
docker = ./modules/docker.nix;
|
||||
};
|
||||
systems = [ "aarch64-linux" "x86_64-linux" ];
|
||||
forSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Rootless Docker as a systemd user service (home-manager can't install a
|
||||
# root daemon). SteamOS already provides what it needs: newuidmap/newgidmap
|
||||
# with their capabilities in /usr/bin, /etc/subuid and /etc/subgid entries
|
||||
# for the user, user namespaces and cgroup v2 delegation.
|
||||
# - The socket is in the outer runtime dir (session.runtimeDir): the nested
|
||||
# desktop has its own XDG_RUNTIME_DIR, so the CLI gets DOCKER_HOST as a
|
||||
# default in its wrapper instead of relying on $XDG_RUNTIME_DIR.
|
||||
# - Started, never restarted, on switch (session.services.start): a restart
|
||||
# would stop running containers.
|
||||
# Nothing is written outside the store by this module; dockerd keeps its data
|
||||
# (images, containers, volumes) in ~/.local/share/docker (app data).
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.steamFrame.docker;
|
||||
host = "unix://${config.steamFrame.session.runtimeDir}/docker.sock";
|
||||
|
||||
cli = pkgs.symlinkJoin {
|
||||
name = "docker-rootless-cli";
|
||||
paths = [ cfg.package ];
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
postBuild = ''
|
||||
wrapProgram $out/bin/docker --set-default DOCKER_HOST ${host}
|
||||
'';
|
||||
};
|
||||
in {
|
||||
imports = [ ./cleanup.nix ];
|
||||
|
||||
options.steamFrame.docker = {
|
||||
enable = lib.mkEnableOption ''
|
||||
rootless Docker: dockerd as a user service and the docker CLI, usable
|
||||
from both sessions'';
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.docker;
|
||||
defaultText = lib.literalExpression "pkgs.docker";
|
||||
description = ''
|
||||
Docker package: dockerd-rootless for the service, the CLI (wrapped
|
||||
with DOCKER_HOST) on PATH.
|
||||
'';
|
||||
};
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
default = host;
|
||||
defaultText = lib.literalExpression
|
||||
''"unix://''${config.steamFrame.session.runtimeDir}/docker.sock"'';
|
||||
description = "DOCKER_HOST of the daemon (for other clients).";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
home.packages = [ cli ];
|
||||
|
||||
systemd.user.services.docker = {
|
||||
Unit = {
|
||||
Description = "Docker Application Container Engine (Rootless)";
|
||||
StartLimitIntervalSec = 60;
|
||||
StartLimitBurst = 3;
|
||||
};
|
||||
Service = {
|
||||
Type = "notify";
|
||||
# /usr/bin for SteamOS's newuidmap/newgidmap (they need their caps).
|
||||
Environment = "PATH=/usr/bin";
|
||||
ExecStart = "${cfg.package}/bin/dockerd-rootless";
|
||||
ExecReload = "${pkgs.procps}/bin/kill -s HUP $MAINPID";
|
||||
TimeoutSec = 0;
|
||||
Restart = "always";
|
||||
RestartSec = 2;
|
||||
LimitNOFILE = "infinity";
|
||||
LimitNPROC = "infinity";
|
||||
LimitCORE = "infinity";
|
||||
Delegate = true;
|
||||
NotifyAccess = "all";
|
||||
KillMode = "mixed";
|
||||
};
|
||||
Install.WantedBy = [ "default.target" ];
|
||||
};
|
||||
|
||||
steamFrame.session.services.start = [ "docker.service" ];
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,8 @@
|
||||
# copies and links) are removed by steam-frame-nix-cleanup (on switch).
|
||||
# The entry shadows the Flatpak's (same ID), keeping MIME associations, and is
|
||||
# seen by the "+" menu (which reads only ~/.local/share/applications).
|
||||
# defaultBrowser: without a default the portal picks the first installed
|
||||
# https handler (e.g. Chromium) in both sessions.
|
||||
{ config, pkgs, lib, ... }:
|
||||
let
|
||||
cfg = config.steamFrame.firefox;
|
||||
@@ -98,9 +100,26 @@ in {
|
||||
default profile in both sessions.
|
||||
'';
|
||||
};
|
||||
defaultBrowser = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Make the launcher the default for http, https and text/html (in
|
||||
Home Manager's ~/.config/mimeapps.list). Without a default the
|
||||
portal opens links with the first installed https handler, in both
|
||||
sessions.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
xdg.mimeApps = lib.mkIf cfg.defaultBrowser {
|
||||
enable = true;
|
||||
defaultApplications = lib.genAttrs
|
||||
[ "x-scheme-handler/http" "x-scheme-handler/https" "text/html" ]
|
||||
(_: "org.mozilla.firefox.desktop");
|
||||
};
|
||||
|
||||
# stable: the branch the launcher runs (the extension point has no
|
||||
# version, so it takes the app's branch).
|
||||
xdg.dataFile = lib.optionalAttrs (defaultPrefs != { } || desktopFix) {
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# Launchers for apps that keep secrets in the KDE wallet, so both sessions
|
||||
# share them.
|
||||
# - One kwalletd6, on the outer bus: the nested desktop has its own D-Bus,
|
||||
# where an app would start a second kwalletd6 whose secrets the Steam
|
||||
# session never sees. The launchers run with session.busEnv.
|
||||
# - Electron picks its keyring from XDG_CURRENT_DESKTOP; in the Steam session
|
||||
# (gamescope) it falls back to "basic" (a local file) and the login made in
|
||||
# the desktop is gone. `electron` passes --password-store=kwallet6.
|
||||
# - Flatpaks: the wallet's D-Bus names (kwalletd6 and the Secret Service,
|
||||
# served by ksecretd) as `flatpak run --talk-name` options, not Flatpak
|
||||
# override files: they apply only to launches from the entry and are gone
|
||||
# with it.
|
||||
# - schemeHandlers: login callbacks (claude://, io.element.desktop://) go
|
||||
# through the portal, which opens an app chooser (invisible in VR) unless
|
||||
# the app is the default and a recommended handler (Added Associations).
|
||||
# Each entry is written to ~/.local/share/applications with the app's own
|
||||
# desktop ID: it shadows the Flatpak's/package's entry and is seen by the "+"
|
||||
# menu (which reads only that directory).
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.steamFrame.keyring;
|
||||
busEnv = config.steamFrame.session.busEnv;
|
||||
|
||||
walletNames = [ "org.kde.kwalletd6" "org.freedesktop.secrets" ];
|
||||
|
||||
actionModule = lib.types.submodule {
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Name of the action (Name=).";
|
||||
};
|
||||
args = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ ''"claude://claude.ai/new"'' ];
|
||||
description = ''
|
||||
Arguments after the app's (in desktop entry Exec syntax: quote
|
||||
yourself).
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Shared by flatpaks and programs; `flatpak` switches the defaults and the
|
||||
# command.
|
||||
appModule = flatpak: { name, config, ... }: {
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "Element";
|
||||
description = "Name shown in menus (Name=).";
|
||||
};
|
||||
genericName = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "GenericName=.";
|
||||
};
|
||||
comment = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Comment=.";
|
||||
};
|
||||
icon = lib.mkOption ({
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if flatpak then name else null;
|
||||
description = "Icon name (Icon=).";
|
||||
} // lib.optionalAttrs flatpak { defaultText = lib.literalMD "the app ID"; });
|
||||
categories = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "Network" "InstantMessaging" ];
|
||||
description = "Categories=.";
|
||||
};
|
||||
startupWMClass = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "StartupWMClass=.";
|
||||
};
|
||||
mimeTypes = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "x-scheme-handler/rdp" ];
|
||||
description = ''
|
||||
Further MIME types listed in the entry (MimeType=), without making
|
||||
the app their default.
|
||||
'';
|
||||
};
|
||||
schemeHandlers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "element" "io.element.desktop" ];
|
||||
description = ''
|
||||
URL schemes (without `x-scheme-handler/`) the app handles and is
|
||||
made the default and a recommended handler for, e.g. login
|
||||
callbacks: otherwise the portal asks with an app chooser, which
|
||||
isn't shown in VR. Listed in MimeType= too.
|
||||
'';
|
||||
};
|
||||
electron = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Pass `--password-store=kwallet6`: Electron picks its keyring from
|
||||
XDG_CURRENT_DESKTOP and in the Steam session (gamescope) would use
|
||||
an unencrypted local store instead of the wallet.
|
||||
'';
|
||||
};
|
||||
args = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Further arguments to the app (desktop entry Exec syntax), before
|
||||
the field code.
|
||||
'';
|
||||
};
|
||||
fieldCode = lib.mkOption {
|
||||
type = lib.types.enum [ "%U" "%u" "%F" "%f" "" ];
|
||||
default = "%U";
|
||||
description = ''
|
||||
How the entry passes URLs/files: `%U` several URLs, `%u` one,
|
||||
`%F`/`%f` local files, `""` none.
|
||||
'';
|
||||
};
|
||||
actions = lib.mkOption {
|
||||
type = lib.types.attrsOf actionModule;
|
||||
default = { };
|
||||
example = lib.literalExpression
|
||||
''{ NewChat = { name = "New Chat"; args = [ '''"claude://claude.ai/new"''' ]; }; }'';
|
||||
description = ''
|
||||
Desktop actions (right-click menu entries); each runs the command
|
||||
with its args.
|
||||
'';
|
||||
};
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
example = { StartupNotify = "true"; Keywords = "Matrix;chat;"; };
|
||||
description = "Further keys of the [Desktop Entry] group.";
|
||||
};
|
||||
command = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
description = ''
|
||||
The command line the entry runs, without args and field code (for
|
||||
a terminal).
|
||||
'';
|
||||
};
|
||||
} // lib.optionalAttrs flatpak {
|
||||
flatpakArgs = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "--branch=stable" "--command=/app/bin/element" ];
|
||||
description = "Further `flatpak run` options.";
|
||||
};
|
||||
} // lib.optionalAttrs (!flatpak) {
|
||||
executable = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = lib.literalExpression ''"''${pkgs.claude-desktop}/bin/claude-desktop"'';
|
||||
description = "The program to run (a path, e.g. into a package).";
|
||||
};
|
||||
};
|
||||
|
||||
config.command = lib.concatStringsSep " " ([ busEnv ]
|
||||
++ (if flatpak
|
||||
then [ "flatpak run" ] ++ map (n: "--talk-name=${n}") walletNames
|
||||
++ config.flatpakArgs ++ [ name ]
|
||||
else [ config.executable ])
|
||||
++ lib.optional config.electron "--password-store=kwallet6");
|
||||
};
|
||||
|
||||
entry = id: app: let
|
||||
exec = args: lib.concatStringsSep " " ([ app.command ] ++ args);
|
||||
mime = map (s: "x-scheme-handler/${s}") app.schemeHandlers ++ app.mimeTypes;
|
||||
list = xs: lib.concatMapStrings (x: "${x};") xs;
|
||||
line = k: v: lib.optionalString (v != null && v != "") "${k}=${v}\n";
|
||||
in ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
'' + line "Name" app.name
|
||||
+ line "GenericName" app.genericName
|
||||
+ line "Comment" app.comment
|
||||
+ line "Icon" app.icon
|
||||
+ line "Exec" (exec (app.args ++ lib.optional (app.fieldCode != "") app.fieldCode))
|
||||
+ line "StartupWMClass" app.startupWMClass
|
||||
+ line "Categories" (list app.categories)
|
||||
+ line "MimeType" (list mime)
|
||||
+ line "Actions" (list (lib.attrNames app.actions))
|
||||
+ line "X-Flatpak" (if app ? flatpakArgs then id else null)
|
||||
+ lib.concatStrings (lib.mapAttrsToList line app.settings)
|
||||
+ lib.concatStrings (lib.mapAttrsToList (a: act: ''
|
||||
|
||||
[Desktop Action ${a}]
|
||||
Name=${act.name}
|
||||
Exec=${exec act.args}
|
||||
'') app.actions);
|
||||
|
||||
apps = cfg.flatpaks // cfg.programs;
|
||||
handlers = lib.concatLists (lib.mapAttrsToList (id: app:
|
||||
map (s: lib.nameValuePair "x-scheme-handler/${s}" "${id}.desktop") app.schemeHandlers) apps);
|
||||
in {
|
||||
imports = [ ./cleanup.nix ];
|
||||
|
||||
options.steamFrame.keyring = {
|
||||
flatpaks = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule (appModule true));
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"im.riot.Riot" = {
|
||||
name = "Element";
|
||||
electron = true;
|
||||
schemeHandlers = [ "element" "io.element.desktop" ];
|
||||
};
|
||||
"org.kde.krdc" = { name = "KRDC"; fieldCode = "%u"; };
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Flatpaks (by app ID) that keep secrets in the KDE wallet: a desktop
|
||||
entry shadowing the Flatpak's runs it on the outer bus, allowed to
|
||||
talk to the wallet (`flatpak run --talk-name=...`).
|
||||
'';
|
||||
};
|
||||
programs = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule (appModule false));
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"com.anthropic.Claude" = {
|
||||
name = "Claude";
|
||||
executable = "''${pkgs.claude-desktop}/bin/claude-desktop";
|
||||
icon = "claude-desktop";
|
||||
electron = true;
|
||||
schemeHandlers = [ "claude" ];
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Other programs (e.g. from Nix packages), by desktop ID without
|
||||
`.desktop` (use the package's own, so the entry replaces it): a
|
||||
desktop entry that runs them on the outer bus.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (apps != { }) {
|
||||
assertions = map (id: {
|
||||
assertion = false;
|
||||
message = "steamFrame.keyring: \"${id}\" is in both flatpaks and programs.";
|
||||
}) (lib.intersectLists (lib.attrNames cfg.flatpaks) (lib.attrNames cfg.programs));
|
||||
|
||||
xdg.dataFile = lib.mapAttrs' (id: app:
|
||||
lib.nameValuePair "applications/${id}.desktop" { text = entry id app; }) apps;
|
||||
|
||||
xdg.mimeApps = lib.mkIf (handlers != [ ]) {
|
||||
enable = true;
|
||||
defaultApplications = lib.listToAttrs handlers;
|
||||
associations.added = lib.listToAttrs handlers;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -56,8 +56,17 @@
|
||||
# firefox.enable = true; # launcher for the Firefox Flatpak
|
||||
# # the Frame has no AV1 decoder: sites send VP9/H.264 (hardware):
|
||||
# firefox.disableAv1 = true;
|
||||
# firefox.defaultBrowser = true; # default for http/https links
|
||||
# # Hardware video decoding in the Jellyfin Desktop Flatpak (install
|
||||
# # org.jellyfin.JellyfinDesktop yourself); gives it devices=all:
|
||||
# jellyfin.hardwareDecoding.enable = true;
|
||||
# # Apps that keep logins in the KDE wallet: one wallet for both
|
||||
# # sessions (Element: install im.riot.Riot yourself):
|
||||
# keyring.flatpaks."im.riot.Riot" = {
|
||||
# name = "Element";
|
||||
# electron = true;
|
||||
# schemeHandlers = [ "element" "io.element.desktop" ]; # SSO callback
|
||||
# };
|
||||
# docker.enable = true; # rootless Docker, user service
|
||||
# };
|
||||
}
|
||||
Reference in new issue
Block a user