{ config, lib, pkgs, ... }: let version = "2.5.34"; cfg = config.programs.steam-rom-manager; # Single source of truth for built-in emulator metadata (package, romFolder, # fileTypes, executableArgs, steamCategory, parserInputs). # options.nix imports the same file so both files stay in sync. knownEmulators = import ./emulators.nix pkgs; # Parser types that do not scan ROM files and have no executable path. platformParserTypes = [ "Epic" "Legendary" "GOG Galaxy" "Amazon Games" "UPlay" "itch.io" "UWP" "EA Desktop" "Battle.net" "Non-SRM Shortcuts" "Steam" "Manual" ]; isPlatformParser = type: builtins.elem type platformParserTypes; # --------------------------------------------------------------------------- # Build an ordered JSON object for one parser entry. # SRM requires keys in a specific order; builtins.toJSON does not guarantee # order, so we construct the JSON string manually from an ordered list. # # Schema version 25 corresponds to SRM ≥ 2.5.x. If you upgrade SRM and the # config format changes, bump `version` here and audit the field list. # --------------------------------------------------------------------------- mkParserConfig = name: emu: let known = knownEmulators.${name} or null; parserType = emu.parserType; platform = isPlatformParser parserType; romFolder = if emu.romFolder != "" then emu.romFolder else if known != null && known ? romFolder then known.romFolder else lib.warn "steam-rom-manager: unknown emulator '${name}', romFolder not set" ""; fileTypes = if emu.fileTypes != [ ] then emu.fileTypes else if known != null && known ? fileTypes then known.fileTypes else [ ]; # lib.getExe resolves the primary binary without IFD path probing. # For platform parsers there is no executable; use empty string. exePath = if platform then "" else if emu.package != null then lib.getExe emu.package else ""; # Build parserInputs: for Glob/Glob-regex parsers always include the # generated glob key; merge any extra user-supplied inputs on top. globKey = if parserType == "Glob" then "glob" else if parserType == "Glob-regex" then "glob-regex" else null; globPattern = if globKey != null && fileTypes != [ ] then { ${globKey} = "\${title}@(${lib.concatStringsSep "|" fileTypes})"; } else { }; resolvedParserInputs = globPattern // emu.parserInputs; orderedConfig = [ { name = "parserType"; value = parserType; } { name = "configTitle"; value = emu.configTitle; } { name = "steamDirectory"; value = "\${steamdirglobal}"; } { name = "romDirectory"; value = if platform then "" else "${cfg.environmentVariables.romsDirectory}/${romFolder}"; } { name = "steamCategories"; value = emu.steamCategories; } { name = "executableArgs"; value = if platform then "" else emu.extraArgs; } { name = "executableModifier"; value = emu.executableModifier; } { name = "startInDirectory"; value = if platform then "" else "${cfg.environmentVariables.romsDirectory}/${romFolder}"; } { name = "titleModifier"; value = emu.titleModifier; } # fetchControllerTemplatesButton / removeControllersButton are UI-only # action triggers in SRM; they carry no config value and must be null # for schema compatibility. { name = "fetchControllerTemplatesButton"; value = null; } { name = "removeControllersButton"; value = null; } { name = "steamInputEnabled"; value = if emu.steamInputEnabled then "1" else "0"; } { name = "imageProviders"; value = cfg.enabledProviders; } { name = "onlineImageQueries"; value = emu.onlineImageQueries; } { name = "imagePool"; value = emu.imagePool; } { name = "drmProtect"; value = emu.drmProtected; } { name = "userAccounts"; value = { specifiedAccounts = emu.userAccounts; }; } { name = "parserInputs"; value = resolvedParserInputs; } { name = "executable"; value = { path = exePath; shortcutPassthrough = emu.shortcutPassthrough; appendArgsToExecutable = emu.appendArgsToExecutable; }; } { name = "titleFromVariable"; value = { limitToGroups = [ ]; caseInsensitiveVariables = false; skipFileIfVariableWasNotFound = false; }; } { name = "fuzzyMatch"; value = { replaceDiacritics = true; removeCharacters = true; removeBrackets = true; }; } { name = "controllers"; value = { ps4 = null; ps5 = null; ps5_edge = null; xbox360 = null; xboxone = null; xboxelite = null; switch_joycon_left = null; switch_joycon_right = null; switch_pro = null; neptune = null; steamcontroller_gordon = null; }; } { name = "imageProviderAPIs"; value = { sgdb = cfg.imageProviderSettings.sgdb; }; } { name = "defaultImage"; value = { tall = ""; long = ""; hero = ""; logo = ""; icon = ""; }; } { name = "localImages"; value = { tall = ""; long = ""; hero = ""; logo = ""; icon = ""; }; } { name = "disabled"; value = emu.disabled; } { name = "parserId"; value = name; } # SRM userConfigurations schema version — bump when upgrading past a # breaking config format change in SRM itself. { name = "version"; value = 25; } ]; makeOrderedJSON = pairs: let joined = builtins.concatStringsSep "," ( map (pair: "\"${pair.name}\":${builtins.toJSON pair.value}") pairs ); in "{${joined}}"; in makeOrderedJSON orderedConfig; # --------------------------------------------------------------------------- # Icon — pinned to the commit that introduced the current SVG so the hash # does not break when unrelated files change on master. # Commit: 1670cbe8871a632708e0440ccef52c5c5c403ddc (2024-01-12) # --------------------------------------------------------------------------- steam-rom-manager-icon = pkgs.fetchurl { name = "steam-rom-manager.svg"; url = "https://raw.githubusercontent.com/SteamGridDB/steam-rom-manager/1670cbe8871a632708e0440ccef52c5c5c403ddc/src/assets/icons/steam-rom-manager.svg"; hash = "sha256-DKzNIs5UhIWAVRTfinvCb8WqeDniPWw9Z08/p/Zpa9E="; }; # --------------------------------------------------------------------------- # Wrap the upstream AppImage with appimage-run. # --------------------------------------------------------------------------- steam-rom-manager-appimage = pkgs.writeShellScriptBin "steam-rom-manager" '' exec ${pkgs.appimage-run}/bin/appimage-run ${ pkgs.fetchurl { name = "steam-rom-manager-${version}.AppImage"; url = "https://github.com/SteamGridDB/steam-rom-manager/releases/download/v${version}/Steam-ROM-Manager-${version}.AppImage"; hash = "sha256-QbXwfT91BQ15/DL3IYC3qZcahlsQvkUKTwMUUpZY+U8="; } } "$@" ''; # Collect only enabled emulators that have a non-null package (i.e. not # pure platform parsers like Epic/GOG that need no extra Nix package). enabledEmulatorsWithPackage = lib.filterAttrs (_: v: v.enable && v.package != null) cfg.emulators; in { imports = [ ./options.nix ]; config = lib.mkIf cfg.enable { home.packages = [ pkgs.appimage-run steam-rom-manager-appimage ] ++ lib.mapAttrsToList (_: v: v.package) enabledEmulatorsWithPackage; xdg.dataFile."icons/hicolor/scalable/apps/steam-rom-manager.svg".source = steam-rom-manager-icon; xdg.desktopEntries.steam-rom-manager = { name = "Steam ROM Manager"; exec = "${steam-rom-manager-appimage}/bin/steam-rom-manager"; icon = "steam-rom-manager"; categories = [ "Game" "Utility" ]; type = "Application"; terminal = false; comment = "Add ROMs to Steam with artwork"; settings = { "X-KDE-StartupNotify" = "true"; "X-KDE-SubstituteUID" = "false"; "X-DBUS-StartupType" = "Unique"; }; }; xdg.configFile = { # userSettings schema version 8 corresponds to SRM ≥ 2.4.x. "steam-rom-manager/userData/userSettings.json".text = builtins.toJSON { fuzzyMatcher = { timestamps = { inherit (cfg.fuzzyMatcher.timestamps) check download; }; verbose = cfg.fuzzyMatcher.verbose; filterProviders = cfg.fuzzyMatcher.filterProviders; }; environmentVariables = { steamDirectory = cfg.environmentVariables.steamDirectory; userAccounts = "\${${cfg.steamUsername}}"; romsDirectory = cfg.environmentVariables.romsDirectory; retroarchPath = cfg.environmentVariables.retroarchPath; raCoresDirectory = cfg.environmentVariables.raCoresDirectory; localImagesDirectory = cfg.environmentVariables.localImagesDirectory; }; previewSettings = cfg.previewSettings; enabledProviders = cfg.enabledProviders; imageProviderAPIs = { sgdb = cfg.imageProviderSettings.sgdb; }; batchDownloadSize = cfg.batchDownloadSize; dnsServers = cfg.dnsServers; language = cfg.language; theme = cfg.theme; emudeckInstall = cfg.emudeckInstall; autoUpdate = cfg.autoUpdate; offlineMode = cfg.offlineMode; navigationWidth = cfg.navigationWidth; clearLogOnTest = cfg.clearLogOnTest; version = 8; }; "steam-rom-manager/userData/userConfigurations.json".text = let configs = lib.mapAttrsToList (name: emu: mkParserConfig name emu) ( lib.filterAttrs (_: v: v.enable) cfg.emulators ); in "[${lib.concatStringsSep "," configs}]"; }; }; }