{ lib, config, pkgs, ... }: let # Extend lib with play utilities playLib = import ../../lib { inherit lib; }; cfg = config.play.wrappers; # Use shared lib function inherit (playLib) toCliArgs; # Function to create a wrapper for an application mkWrapper = name: wrapperCfg: let # Determine the command to execute baseCommand = if wrapperCfg.command != null then wrapperCfg.command else lib.getExe wrapperCfg.package; # Convert wrapper-specific environment to a single string for gamescoperun gamescopeWrapperEnv = lib.optionalString (wrapperCfg.environment != { }) ( let envList = lib.mapAttrsToList (name: value: "${name}=${toString value}") wrapperCfg.environment; in # Set as a global exported variable so gamescoperun can see it "set -gx GAMESCOPE_WRAPPER_ENV '${lib.concatStringsSep ";" envList}'" ); # Convert extraOptions to CLI args extraArgs = lib.optionalString ( wrapperCfg.extraOptions != { } ) "-x \"${toCliArgs wrapperCfg.extraOptions}\""; wrapperScript = pkgs.writeScriptBin name '' #!${lib.getExe pkgs.fish} # Set environment for gamescoperun to consume ${gamescopeWrapperEnv} # Execute with gamescoperun exec ${lib.getExe config.play.gamescoperun.package} ${extraArgs} ${baseCommand} $argv ''; in wrapperScript; in { options.play.wrappers = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule ( { name, ... }: { options = { enable = lib.mkEnableOption "wrapper for this application"; package = lib.mkOption { type = lib.types.nullOr lib.types.package; default = null; description = "The package to wrap (used with lib.getExe if command is not specified)"; }; command = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "\${lib.getExe osConfig.programs.steam.package} -tenfoot -bigpicture"; description = '' The exact command to execute after gamescoperun and its options. If specified, this takes precedence over the package option. Can include arguments and flags. ''; }; extraOptions = lib.mkOption { type = with lib.types; attrsOf (oneOf [ str int bool ]); default = { }; example = { "fsr-upscaling" = true; "force-windows-fullscreen" = true; "fsr-upscaling-sharpness" = 5; }; description = '' Additional gamescope command-line options for this specific wrapper. Option names must match gamescope's flags exactly (e.g., "hdr-enabled"). These will be passed via the -x flag to gamescoperun. ''; }; environment = lib.mkOption { type = with lib.types; attrsOf (oneOf [ str int ]); default = { }; example = { STEAM_FORCE_DESKTOPUI_SCALING = 1; STEAM_GAMEPADUI = 1; }; description = "Additional environment variables for this specific wrapper"; }; # Readonly package option that exposes the configured wrapper wrappedPackage = lib.mkOption { type = lib.types.package; readOnly = true; default = mkWrapper name config.play.wrappers.${name}; description = "The configured wrapper package for this application"; }; }; } ) ); default = { }; description = "Application wrappers that run through gamescoperun"; }; config = { home.packages = lib.mapAttrsToList mkWrapper ( lib.filterAttrs (name: wrapperCfg: wrapperCfg.enable) cfg ); # Ensure gamescoperun is enabled if any wrappers are enabled play.gamescoperun.enable = lib.mkIf (lib.length (lib.attrNames cfg) > 0) true; assertions = lib.mapAttrsToList (name: wrapperCfg: { assertion = wrapperCfg.enable -> (wrapperCfg.command != null || wrapperCfg.package != null); message = "play.wrappers.${name}: Either 'command' or 'package' must be specified when enabled"; }) cfg; }; }