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:
Pierre Kisters committed 2026-09-29 01:13:47 +02:00
1 parent aa9ca183f1
commit b0131338de
10 files changed
+617 -14

No files matched your search

+50 -10
View File
@@ -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
+57
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, ... }: {
+2
View File
@@ -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});
+81
View File
@@ -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" ];
};
}
+19
View File
@@ -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) {
+260
View File
@@ -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;
};
};
}
+9
View File
@@ -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
# };
}