Portal: KDE file dialog for Flatpak apps in the Steam session

Neither Valve backend (gamescope, holo) implements FileChooser, so sandboxed
apps fell back to an in-sandbox dialog without the home dir (e.g. Element's
Attachments). session.portalFix.fileChooser (on) links kde.portal into the
Steam session's portal dir and selects kde for FileChooser;
xdg-desktop-portal-kde is D-Bus activated with DISPLAY=:0. Flake check
for the portal config.
This commit is contained in:
Pierre Kisters committed 2026-09-29 03:20:12 +02:00
1 parent 356dcb9012
commit 50e5991c59
6 files changed
+92 -17

No files matched your search

+2 -1
View File
@@ -26,7 +26,7 @@ configuration, limitations and how it works.
**Session** ([docs/session.md](docs/session.md)): **Session** ([docs/session.md](docs/session.md)):
- [Session settings](docs/session.md#session-settings-and-services) (`session.*`): outer bus and user services, for launchers and the other modules. - [Session settings](docs/session.md#session-settings-and-services) (`session.*`): outer bus and user services, for launchers and the other modules.
- [Portal fix](docs/session.md#portal-fix) (`session.portalFix`, on): apps in the Steam session can open links. - [Portal fix](docs/session.md#portal-fix) (`session.portalFix`, on): apps in the Steam session can open links, and Flatpak apps get KDE's file dialog.
- [Applications menu](docs/session.md#applications-menu) (`session.applicationsMenu`, on): KDE apps in the Steam session (Dolphin from the "+" menu) know the installed apps. - [Applications menu](docs/session.md#applications-menu) (`session.applicationsMenu`, on): KDE apps in the Steam session (Dolphin from the "+" menu) know the installed apps.
- [Keyboard layout](docs/session.md#keyboard-layout) (`keyboard.layout`, `keyboard.variant`): XKB layout for the Steam session. - [Keyboard layout](docs/session.md#keyboard-layout) (`keyboard.layout`, `keyboard.variant`): XKB layout for the Steam session.
- [Clipboard sync](docs/session.md#clipboard-sync) (`clipboardSync`, on): one clipboard for the Steam session and the nested desktop. - [Clipboard sync](docs/session.md#clipboard-sync) (`clipboardSync`, on): one clipboard for the Steam session and the nested desktop.
@@ -232,6 +232,7 @@ which: [Repository layout](docs/development.md).
| `steamFrame.session.services.restart` | list of str | `[ ]` | User units restarted on every switch. | | `steamFrame.session.services.restart` | list of str | `[ ]` | User units restarted on every switch. |
| `steamFrame.session.services.stop` | list of str | `[ ]` | User units stopped on switch if running (e.g. of a disabled feature). | | `steamFrame.session.services.stop` | list of str | `[ ]` | User units stopped on switch if running (e.g. of a disabled feature). |
| `steamFrame.session.portalFix.enable` | bool | `true` | Working portal config (OpenURI) for the Steam session. | | `steamFrame.session.portalFix.enable` | bool | `true` | Working portal config (OpenURI) for the Steam session. |
| `steamFrame.session.portalFix.fileChooser` | bool | `true` | KDE's file dialog as the Steam session's FileChooser portal (Flatpak apps can open and save files outside their sandbox). Needs `portalFix.enable`. |
| `steamFrame.session.applicationsMenu.enable` | bool | `true` | `~/.config/menus/applications.menu` linked to Plasma's, for KDE apps in the Steam session. | | `steamFrame.session.applicationsMenu.enable` | bool | `true` | `~/.config/menus/applications.menu` linked to Plasma's, for KDE apps in the Steam session. |
| `steamFrame.keyboard.layout` | null or str | `null` | XKB layout for the Steam session, e.g. `"de"`; `null`: US. | | `steamFrame.keyboard.layout` | null or str | `null` | XKB layout for the Steam session, e.g. `"de"`; `null`: US. |
| `steamFrame.keyboard.variant` | null or str | `null` | XKB variant for the Steam session, e.g. `"nodeadkeys"`; see [Keyboard layout](docs/session.md#keyboard-layout). | | `steamFrame.keyboard.variant` | null or str | `null` | XKB variant for the Steam session, e.g. `"nodeadkeys"`; see [Keyboard layout](docs/session.md#keyboard-layout). |
+1
View File
@@ -50,6 +50,7 @@ modules/
cleanup/package.nix build: install.sh as a command (cleanup, steamvr-debugger-arm) cleanup/package.nix build: install.sh as a command (cleanup, steamvr-debugger-arm)
cleanup/check.nix test: install.sh cleanup on fake home/runtime dirs cleanup/check.nix test: install.sh cleanup on fake home/runtime dirs
portal.nix Steam session portal config (session.portalFix) portal.nix Steam session portal config (session.portalFix)
portal/check.nix test: KDE FileChooser on by default, absent when disabled
applications-menu.nix applications.menu link for KDE apps (session.applicationsMenu) applications-menu.nix applications.menu link for KDE apps (session.applicationsMenu)
applications-menu/check.nix test: the link on by default, absent when disabled applications-menu/check.nix test: the link on by default, absent when disabled
keyboard-layout.nix gamescope-session drop-in with XKB_DEFAULT_* (keyboard.layout) keyboard-layout.nix gamescope-session drop-in with XKB_DEFAULT_* (keyboard.layout)
+22 -6
View File
@@ -40,21 +40,37 @@ launcher's `hostEnv`; a hand-written entry uses the prefix `session.busEnv`:
## Portal fix ## Portal fix
`session.portalFix.enable`, module `portal`. On by default. `session.portalFix.enable` and `session.portalFix.fileChooser`, module
`portal`. Both on by default.
**Problem:** the Frame image (SteamOS 0.3.0, build 20260922) points the Steam **Problem:** the Frame image (SteamOS 0.3.0, build 20260922) points the Steam
session's `xdg-desktop-portal` at `/usr/share/xdg-desktop-portal/gamescope-portals`, session's `xdg-desktop-portal` at `/usr/share/xdg-desktop-portal/gamescope-portals`,
which lacks `gamescope-portals.conf`: no backend, no OpenURI, so no app in which lacks `gamescope-portals.conf`: no backend, no OpenURI, so no app in
the Steam session can open links. the Steam session can open links. Its two backends (gamescope, holo) also
have no FileChooser, so Flatpak apps fall back to a dialog inside their
sandbox: e.g. Element's Attachments shows an empty file selector without
your home dir.
**What you get:** a working OpenURI portal in the Steam session; the **What you get:** a working OpenURI portal in the Steam session and, with
desktop's portal is unaffected. `fileChooser`, KDE's file dialog there (a normal window in VR) with access
to all your files; the app gets only the files you pick. The desktop's
portal is unaffected.
**Remove when** SteamOS ships `gamescope-portals.conf`. **Configuration:** takes effect when the portal restarts (next Steam session
start, or `systemctl --user restart xdg-desktop-portal`) and the app
restarts (Electron apps check for the portal at startup). To keep the old
dialog: `steamFrame.session.portalFix.fileChooser = false;`.
**Remove when** SteamOS ships `gamescope-portals.conf` (and a FileChooser
backend, for `fileChooser`).
**How it works:** a portal dir in `~/.local/share` linking Valve's `.portal` **How it works:** a portal dir in `~/.local/share` linking Valve's `.portal`
files plus a config (`default=holo;gamescope`), and a drop-in on files plus a config (`default=holo;gamescope`), and a drop-in on
`xdg-desktop-portal.service`. `xdg-desktop-portal.service`. `fileChooser` adds a link to
`/usr/share/xdg-desktop-portal/portals/kde.portal` and
`org.freedesktop.impl.portal.FileChooser=kde`; the preinstalled
`xdg-desktop-portal-kde` starts on demand (D-Bus activation) with the Steam
session's `DISPLAY=:0`.
## Applications menu ## Applications menu
+3 -2
View File
@@ -66,14 +66,15 @@
# Tests of the VR keyboard (text model, corrector, swipe decoder on the # Tests of the VR keyboard (text model, corrector, swipe decoder on the
# default German + English dictionary), the Jellyfin mpv shim, the # default German + English dictionary), the Jellyfin mpv shim, the
# Firefox wrapper, the launchers, install.sh cleanup and the # Firefox wrapper, the launchers, install.sh cleanup, the
# applications.menu link: nix flake check # applications.menu link and the portal config: nix flake check
checks = forSystems (pkgs: { checks = forSystems (pkgs: {
applications-menu = import ./modules/applications-menu/check.nix { inherit pkgs; }; applications-menu = import ./modules/applications-menu/check.nix { inherit pkgs; };
cleanup = import ./modules/cleanup/check.nix { inherit pkgs; }; cleanup = import ./modules/cleanup/check.nix { inherit pkgs; };
firefox = import ./modules/firefox/check.nix { inherit pkgs; }; firefox = import ./modules/firefox/check.nix { inherit pkgs; };
jellyfin = import ./modules/jellyfin/check.nix { inherit pkgs; }; jellyfin = import ./modules/jellyfin/check.nix { inherit pkgs; };
launchers = import ./modules/launchers/check.nix { inherit pkgs; }; launchers = import ./modules/launchers/check.nix { inherit pkgs; };
portal = import ./modules/portal/check.nix { inherit pkgs; };
vr-keyboard = import ./modules/vr-keyboard/check.nix { inherit pkgs; }; vr-keyboard = import ./modules/vr-keyboard/check.nix { inherit pkgs; };
}); });
+28 -8
View File
@@ -3,8 +3,11 @@
# gamescope-portals.conf, so no backend is selected and there is no OpenURI # gamescope-portals.conf, so no backend is selected and there is no OpenURI
# (links don't open). We point it at our own dir: Valve's .portal files + a # (links don't open). We point it at our own dir: Valve's .portal files + a
# config. Remove once SteamOS ships the .conf. # config. Remove once SteamOS ships the .conf.
# Neither Valve backend has a FileChooser, so sandboxed apps fall back to an
# in-sandbox dialog that can't see home; fileChooser adds KDE's for it.
{ config, lib, ... }: { config, lib, ... }:
let let
cfg = config.steamFrame.session.portalFix;
sys = "/usr/share/xdg-desktop-portal/gamescope-portals"; sys = "/usr/share/xdg-desktop-portal/gamescope-portals";
dir = "xdg-desktop-portal/gamescope-portals"; dir = "xdg-desktop-portal/gamescope-portals";
in { in {
@@ -15,23 +18,40 @@ in {
[ "steamFrame" "session" "portalFix" "enable" ]) [ "steamFrame" "session" "portalFix" "enable" ])
]; ];
options.steamFrame.session.portalFix.enable = lib.mkOption { options.steamFrame.session.portalFix = {
type = lib.types.bool; enable = lib.mkOption {
default = true; type = lib.types.bool;
description = '' default = true;
Give the Steam session's xdg-desktop-portal a config so apps there can description = ''
open links (OpenURI). Give the Steam session's xdg-desktop-portal a config so apps there can
''; open links (OpenURI).
'';
};
fileChooser = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Use KDE's file dialog (xdg-desktop-portal-kde) as the Steam session's
FileChooser portal, so sandboxed apps (Flatpak) can open and save
files outside their sandbox. Needs `enable`.
'';
};
}; };
config = lib.mkIf config.steamFrame.session.portalFix.enable { config = lib.mkIf cfg.enable {
xdg.dataFile."${dir}/holo.portal".source = xdg.dataFile."${dir}/holo.portal".source =
config.lib.file.mkOutOfStoreSymlink "${sys}/holo.portal"; config.lib.file.mkOutOfStoreSymlink "${sys}/holo.portal";
xdg.dataFile."${dir}/gamescope.portal".source = xdg.dataFile."${dir}/gamescope.portal".source =
config.lib.file.mkOutOfStoreSymlink "${sys}/gamescope.portal"; config.lib.file.mkOutOfStoreSymlink "${sys}/gamescope.portal";
xdg.dataFile."${dir}/kde.portal" = lib.mkIf cfg.fileChooser {
source = config.lib.file.mkOutOfStoreSymlink
"/usr/share/xdg-desktop-portal/portals/kde.portal";
};
xdg.dataFile."${dir}/gamescope-portals.conf".text = '' xdg.dataFile."${dir}/gamescope-portals.conf".text = ''
[preferred] [preferred]
default=holo;gamescope default=holo;gamescope
'' + lib.optionalString cfg.fileChooser ''
org.freedesktop.impl.portal.FileChooser=kde
''; '';
# Outer (systemd) portal only; the nested desktop's keeps kde-portals.conf. # Outer (systemd) portal only; the nested desktop's keeps kde-portals.conf.
+36
View File
@@ -0,0 +1,36 @@
# Evaluates the portal module against a stub of Home Manager's options: on by
# default the Steam session's portal config picks KDE's FileChooser, with
# `fileChooser = false` it has neither the kde.portal link nor that line.
{ pkgs }:
let
inherit (pkgs) lib;
dir = "xdg-desktop-portal/gamescope-portals";
eval = cfg: (lib.evalModules {
modules = [
../portal.nix
{
options.xdg.dataFile = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.anything);
default = { };
};
options.lib = lib.mkOption { type = lib.types.attrsOf lib.types.anything; };
config = {
_module.check = false; # Home Manager options the stub doesn't declare
_module.args.pkgs = pkgs;
lib.file.mkOutOfStoreSymlink = p: "out-of-store:${p}";
};
}
cfg
];
}).config.xdg.dataFile;
on = eval { };
off = eval { steamFrame.session.portalFix.fileChooser = false; };
kde = "org.freedesktop.impl.portal.FileChooser=kde";
in
assert on."${dir}/kde.portal".source
== "out-of-store:/usr/share/xdg-desktop-portal/portals/kde.portal";
assert lib.hasInfix kde on."${dir}/gamescope-portals.conf".text;
assert !(off ? "${dir}/kde.portal");
assert !(lib.hasInfix kde off."${dir}/gamescope-portals.conf".text);
assert off ? "${dir}/holo.portal";
pkgs.runCommand "portal-check" { } "touch $out"