mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 04:04:16 +02:00
- session.services.stop: stop only units that are active; stopping a unit that isn't loaded printed "Failed to stop" on every switch (by default for steam-ui-patches, steam-keyboard-patch and vr-keyboard-relay). - keyring: a scheme in the schemeHandlers of two apps is an assertion instead of one app silently winning. - vr-keyboard-replay.mjs: recorded paths have no time; label them by index. - webpack-modules.mjs: skip page files that are missing instead of throwing.
265 lines
9.8 KiB
Nix
265 lines
9.8 KiB
Nix
# 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))
|
|
++ lib.mapAttrsToList (mime: ids: {
|
|
assertion = lib.length ids == 1;
|
|
message = "steamFrame.keyring: ${lib.removePrefix "x-scheme-handler/" mime} is in the schemeHandlers of ${lib.concatStringsSep " and " ids}.";
|
|
}) (lib.zipAttrs (map (h: { ${h.name} = lib.removeSuffix ".desktop" h.value; }) handlers));
|
|
|
|
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;
|
|
};
|
|
};
|
|
}
|