Files
Pierre Kisters 59afc2f616 Launchers: neutral examples and made-up fixtures
Examples showed specific apps as if the repository installed or
configured them.

- Docs, README, template and option examples: the Signal Flatpak
  (org.signal.Signal, sgnl:// and signalcaptcha://) and a made-up
  org.example.App instead of the previous chat and remote-desktop apps;
  no install lines (the app is installed by the user, as noted).
- Portal fix: the file-dialog symptom phrased generically.
- Launcher check: made-up Flatpak entries org.example.Chat (Electron,
  --file-forwarding @@u %U @@, an action, an SSO scheme) and
  org.example.Remote (Qt, -qwindowtitle %c @@u %u @@, localized keys)
  replace the copied ones; Firefox, gedit and Jellyfin stay.
2026-10-01 18:16:37 +02:00

242 lines
9.8 KiB
Nix

# Pieces of steamFrame.launchers (launchers.nix) that the check
# (check.nix) uses too: the per-launcher option type, Exec quoting, the
# rewrite environment, the build-time entry of a package and the runtime
# generator.
{ lib, pkgs }:
let
walletNames = [ "org.kde.kwalletd6" "org.freedesktop.secrets" ];
# One argument in desktop entry Exec syntax (key-file escaping of the whole
# value is rewrite.awk's): quoted when it has characters outside a safe
# set, with " ` $ \ escaped; % doubled (field codes).
quoteExec = s:
let
q = if builtins.match "[A-Za-z0-9_/.:,=+@-]+" s != null then s
else "\"" + lib.replaceStrings [ "\\" "\"" "`" "$" ] [ "\\\\" "\\\"" "\\`" "\\$" ] s + "\"";
in lib.replaceStrings [ "%" ] [ "%%" ] q;
execLine = args: lib.concatMapStringsSep " " quoteExec args;
envArgs = env: lib.mapAttrsToList (k: v: "${k}=${v}") env;
envPrefix = env: lib.optionals (env != { }) ([ "env" ] ++ envArgs env);
# "flatpak" | "package" | "file"
kindOf = l: if l.source.package != null then "package"
else if l.source.file != null then "file" else "flatpak";
in rec {
inherit quoteExec execLine walletNames kindOf;
# The option type of one launcher; `bus` is session.bus.
launcherModule = { bus }: { name, config, ... }: let
flatpak = kindOf config == "flatpak";
in {
options = {
source = {
flatpak = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
defaultText = lib.literalMD "the desktop ID, when neither `package` nor `file` is set";
example = "org.mozilla.firefox";
description = ''
Flatpak app ID whose exported entry
(`~/.local/share/flatpak/exports/share/applications/<ID>.desktop`,
then `/var/lib/flatpak/...`) is rewritten at runtime.
'';
};
package = lib.mkOption {
type = lib.types.nullOr lib.types.package;
default = null;
example = lib.literalExpression "pkgs.signal-desktop";
description = ''
Nix package whose `share/applications/<desktop ID>.desktop` is
rewritten at build time (the build fails if it has none).
'';
};
file = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/usr/share/applications/org.kde.konsole.desktop";
description = "Desktop entry on the host, rewritten at runtime.";
};
};
hostEnv = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = { DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/user/1000/bus"; };
description = ''
Environment of the process the entry starts (`env K=V` before the
command): for a Flatpak, of the `flatpak` client, so e.g. its
sandbox's D-Bus proxy and portals use that bus.
'';
};
env = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = { MOZ_ENABLE_WAYLAND = "0"; };
description = ''
Environment of the app: `flatpak run --env=K=V` for a Flatpak, like
`hostEnv` otherwise.
'';
};
flatpakArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "--device=all" ];
description = "Further `flatpak run` options, before the app ID (Flatpaks only).";
};
args = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "--ozone-platform=x11" ];
description = ''
Arguments right after the app ID / program, before the entry's own
(plain strings, quoted for you).
'';
};
wrappers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = lib.literalExpression ''[ "''${pkgs.writeShellScript "wrap" "exec \"$@\""}" ]'';
description = ''
Commands the entry's command line is passed to, outermost first:
`Exec=<wrappers> <original command>` (each gets the full command
line as its arguments).
'';
};
mimeTypes = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "x-scheme-handler/rdp" ];
description = "Added to the entry's `MimeType=`, without making the app their default.";
};
defaultFor = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "x-scheme-handler/sgnl" "x-scheme-handler/signalcaptcha" ];
description = ''
MIME types / `x-scheme-handler/<scheme>` the app becomes the default
and a recommended handler for (`xdg.mimeApps`, Added Associations:
the portal then opens them without an app chooser, which VR doesn't
show); added to `MimeType=` too.
'';
};
settings = lib.mkOption {
type = lib.types.attrsOf (lib.types.nullOr lib.types.str);
default = { };
example = { Name = "Signal (wallet)"; Keywords = null; };
description = ''
`[Desktop Entry]` keys to set or override (value in key-file syntax,
as given); `null` removes a key. Either way the key's localized
variants (`Name[de]=`) are dropped.
'';
};
keyring = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Keep the app's secrets in the one KDE wallet of both sessions: run
it on the outer bus (`hostEnv.DBUS_SESSION_BUS_ADDRESS`); a
Flatpak also gets `--talk-name=org.kde.kwalletd6
--talk-name=org.freedesktop.secrets`.
'';
};
electron = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Pass `--password-store=kwallet6` (Electron apps: in the Steam
session they'd use an unencrypted local store). Needs
`keyring.enable`.
'';
};
};
command = lib.mkOption {
type = lib.types.str;
readOnly = true;
description = ''
Approximately the command the entry runs, for a terminal (without
the entry's own options such as `--branch` or `--command`).
'';
};
};
config = {
hostEnv = lib.mkIf config.keyring.enable { DBUS_SESSION_BUS_ADDRESS = bus; };
flatpakArgs = lib.mkIf (config.keyring.enable && flatpak)
(lib.mkBefore (map (n: "--talk-name=${n}") walletNames));
args = lib.mkIf config.keyring.electron (lib.mkBefore [ "--password-store=kwallet6" ]);
command = command (config // { id = name; });
};
};
flatpakIdOf = l: if l.source.flatpak != null then l.source.flatpak else l.id;
# Approximate shell command of a launcher (attrs as the option, plus `id`).
command = l: let
kind = kindOf l;
pkg = l.source.package;
in lib.escapeShellArgs (
envPrefix (l.hostEnv // lib.optionalAttrs (kind != "flatpak") l.env)
++ l.wrappers
++ (if kind == "flatpak" then
[ "flatpak" "run" ] ++ l.flatpakArgs ++ map (e: "--env=${e}") (envArgs l.env) ++ [ (flatpakIdOf l) ]
else if kind == "package" then
[ "${pkg}/bin/${pkg.meta.mainProgram or (lib.getName pkg)}" ]
else [ "<program of ${l.source.file}>" ])
++ l.args);
# The environment of rewrite.awk for a launcher (without SFN_SOURCE).
rewriteEnv = l: let flatpak = kindOf l == "flatpak"; in {
SFN_MODE = if flatpak then "flatpak" else "program";
SFN_FLATPAK_ID = if flatpak then flatpakIdOf l else "";
SFN_PREFIX = execLine (envPrefix (l.hostEnv // lib.optionalAttrs (!flatpak) l.env) ++ l.wrappers);
SFN_FLATPAK_ARGS = lib.optionalString flatpak
(execLine (l.flatpakArgs ++ map (e: "--env=${e}") (envArgs l.env)));
SFN_ARGS = execLine l.args;
SFN_MIME = lib.concatMapStrings (m: "${m};") (lib.unique (l.mimeTypes ++ l.defaultFor));
SFN_SETTINGS = lib.concatStrings (lib.mapAttrsToList
(k: v: if v == null then "${k}\n" else "${k}=${v}\n") l.settings);
};
# Build time: the package's entry, rewritten.
packageEntry = l: let
src = "${l.source.package}/share/applications/${l.id}.desktop";
in pkgs.runCommand "${l.id}.desktop" ({
nativeBuildInputs = [ pkgs.gawk ];
preferLocalBuild = true;
allowSubstitutes = false;
SFN_SOURCE = src;
} // rewriteEnv l) ''
if [ ! -f ${lib.escapeShellArg src} ]; then
echo "steamFrame.launchers.\"${l.id}\": ${l.source.package} has no share/applications/${l.id}.desktop" >&2
exit 1
fi
LC_ALL=C gawk -f ${./rewrite.awk} "$SFN_SOURCE" "$SFN_SOURCE" > $out || {
echo "steamFrame.launchers.\"${l.id}\": can't rewrite $SFN_SOURCE" >&2
exit 1
}
'';
# Runtime: the steam-frame-nix-launchers command for the Flatpak/file
# `launchers` (attrs of launchers with `id`); the dirs can be overridden
# with STEAM_FRAME_NIX_{RUNTIME_DIR,APPLICATIONS_DIR,FLATPAK_EXPORTS}.
generator = { launchers, runtimeDir, exportDirs, applicationsDir }: let
spec = pkgs.linkFarm "steam-frame-nix-launchers-spec" (lib.mapAttrsToList (id: l: {
name = id;
path = pkgs.writeText "steam-frame-nix-launcher-${id}" (lib.toShellVars (rewriteEnv l // {
SFN_FILE = if kindOf l == "file" then l.source.file else "";
}));
}) launchers);
in pkgs.writeShellApplication {
name = "steam-frame-nix-launchers";
runtimeInputs = [ pkgs.coreutils pkgs.gawk pkgs.util-linux ];
text = ''
spec=${spec}
awk_script=${./rewrite.awk}
runtime=''${STEAM_FRAME_NIX_RUNTIME_DIR:-${lib.escapeShellArg runtimeDir}}
apps=''${STEAM_FRAME_NIX_APPLICATIONS_DIR:-${lib.escapeShellArg applicationsDir}}
exports=''${STEAM_FRAME_NIX_FLATPAK_EXPORTS:-${lib.escapeShellArg (lib.concatStringsSep ":" exportDirs)}}
'' + builtins.readFile ./generate.sh;
};
}