# steam-rom-manager.nix { config, lib, pkgs, ... }: with lib; let cfg = config.programs.steam-rom-manager; # Common emulator configurations commonEmulatorConfigs = { ryujinx = { romFolder = "switch"; binaryName = "Ryujinx"; fileTypes = [ ".nca" ".NCA" ".nro" ".NRO" ".nso" ".NSO" ".nsp" ".NSP" ".xci" ".XCI" ]; }; yuzu = { romFolder = "switch"; binaryName = "yuzu"; fileTypes = [ ".nsp" ".NSP" ".xci" ".XCI" ]; }; pcsx2 = { romFolder = "ps2"; binaryName = "pcsx2-qt"; fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".chd" ".CHD" ]; }; rpcs3 = { romFolder = "ps3"; binaryName = "rpcs3"; fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".pkg" ".PKG" ]; }; dolphin-emu = { romFolder = "gc"; binaryName = "dolphin-emu"; fileTypes = [ ".iso" ".ISO" ".gcm" ".GCM" ".ciso" ".CISO" ]; }; duckstation = { romFolder = "psx"; binaryName = "duckstation-qt"; fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".chd" ".CHD" ".pbp" ".PBP" ]; }; melonDS = { romFolder = "nds"; binaryName = "melonDS"; fileTypes = [ ".nds" ".NDS" ]; }; cemu = { romFolder = "wiiu"; binaryName = "cemu"; fileTypes = [ ".wud" ".WUD" ".wux" ".WUX" ".rpx" ".RPX" ]; }; }; getFileTypes = name: emu: if (emu.fileTypes or []) != [] then emu.fileTypes else commonEmulatorConfigs.${name}.fileTypes or []; mkParserConfig = name: emu: let # We create an ordered list of key-value pairs that will maintain the exact order # in the generated JSON output. Each field is documented for clarity. orderedConfig = [ # Basic parser configuration { name = "parserType"; value = "Glob"; } { name = "configTitle"; value = name; } { name = "steamDirectory"; value = "\${steamdirglobal}"; } { name = "romDirectory"; value = "${cfg.romsDirectory}/${if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder}"; } { name = "steamCategories"; value = [""]; } # Executable configuration { name = "executableArgs"; value = emu.extraArgs; } { name = "executableModifier"; value = "\"\${exePath}\""; } { name = "startInDirectory"; value = "${cfg.romsDirectory}/${if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder}"; } { name = "titleModifier"; value = "\${fuzzyTitle}"; } # Controller settings { name = "fetchControllerTemplatesButton"; value = null; } { name = "removeControllersButton"; value = null; } { name = "steamInputEnabled"; value = "1"; } # Image provider configuration { name = "imageProviders"; value = [ "sgdb" ]; } { name = "onlineImageQueries"; value = [ "\${fuzzyTitle}" ]; } { name = "imagePool"; value = "\${fuzzyTitle}"; } # DRM and user account settings { name = "drmProtect"; value = false; } { name = "userAccounts"; value = { specifiedAccounts = [ "Global" ]; }; } # Parser-specific settings { name = "parserInputs"; value = { glob = "\${title}@(${concatStringsSep "|" (getFileTypes name emu)})"; }; } # Executable details { name = "executable"; value = { path = "${emu.package}/bin/${if emu.binaryName != "" then emu.binaryName else commonEmulatorConfigs.${name}.binaryName}"; shortcutPassthrough = false; appendArgsToExecutable = true; }; } # Title and fuzzy matching configuration { name = "titleFromVariable"; value = { limitToGroups = []; caseInsensitiveVariables = false; skipFileIfVariableWasNotFound = false; }; } { name = "fuzzyMatch"; value = { replaceDiacritics = true; removeCharacters = true; removeBrackets = true; }; } # Controller configuration { 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; }; } # Image provider API configuration { name = "imageProviderAPIs"; value = { sgdb = { nsfw = false; humor = false; styles = []; stylesHero = []; stylesLogo = []; stylesIcon = []; imageMotionTypes = [ "static" ]; sizes = []; sizesHero = []; sizesTall = null; sizesIcon = []; }; }; } # Default and local image settings { name = "defaultImage"; value = { tall = ""; long = ""; hero = ""; logo = ""; icon = ""; }; } { name = "localImages"; value = { tall = ""; long = ""; hero = ""; logo = ""; icon = ""; }; } # Parser identification { name = "parserId"; value = name; } { name = "version"; value = 25; } ]; # Function to convert our ordered list into properly formatted JSON makeOrderedJSON = pairs: let # Create JSON key-value pairs while preserving order joined = builtins.concatStringsSep "," (map (pair: "\"${pair.name}\":${builtins.toJSON pair.value}") pairs); in "{${joined}}"; in makeOrderedJSON orderedConfig; # Create AppImage package steam-rom-manager = pkgs.fetchurl { name = "steam-rom-manager-2.5.29.AppImage"; url = "https://github.com/SteamGridDB/steam-rom-manager/releases/download/v2.5.29/Steam-ROM-Manager-2.5.29.AppImage"; hash = "sha256-6ZJ+MGIgr2osuQuqD6N9NnPiJFNq/HW6ivG8tyXUhvs="; }; in { options.programs.steam-rom-manager = { enable = mkEnableOption "Steam ROM Manager"; package = mkOption { type = types.package; default = steam-rom-manager; description = "Steam ROM Manager package"; }; steamUsername = mkOption { type = types.str; description = "Steam username for configuration"; }; romsDirectory = mkOption { type = types.str; default = "${config.home.homeDirectory}/Emulation/roms"; description = "Base directory for ROM files"; }; steamDirectory = mkOption { type = types.str; default = "${config.home.homeDirectory}/.local/share/Steam"; description = "Steam installation directory"; }; emulators = mkOption { type = types.attrsOf (types.submodule { options = { enable = mkEnableOption "emulator configuration"; package = mkOption { type = types.package; description = "Emulator package"; }; binaryName = mkOption { type = types.str; default = ""; description = "Name of the emulator binary (defaults to common configuration)"; }; romFolder = mkOption { type = types.str; default = ""; description = "Name of the ROM folder (defaults to common configuration)"; }; fileTypes = mkOption { type = types.listOf types.str; default = []; description = "List of ROM file types (defaults to common configuration)"; }; extraArgs = mkOption { type = types.str; default = "--fullscreen \"\${filePath}\""; description = "Additional emulator arguments"; }; }; }); default = {}; description = "Emulator configurations"; }; }; config = mkIf cfg.enable { home.packages = [ pkgs.appimage-run ] ++ mapAttrsToList (_: v: v.package) (filterAttrs (_: v: v.enable) cfg.emulators); xdg.desktopEntries.steam-rom-manager = { name = "Steam ROM Manager"; exec = "${pkgs.appimage-run}/bin/appimage-run ${cfg.package}"; icon = "steam"; categories = [ "Game" "Utility" ]; type = "Application"; }; # Configuration files using xdg.configFile xdg.configFile = { # User settings configuration "steam-rom-manager/userData/userSettings.json".text = builtins.toJSON { fuzzyMatcher = { timestamps = { check = 0; download = 0; }; verbose = false; filterProviders = true; }; environmentVariables = { steamDirectory = cfg.steamDirectory; userAccounts = "\${${cfg.steamUsername}}"; romsDirectory = cfg.romsDirectory; retroarchPath = ""; raCoresDirectory = ""; localImagesDirectory = ""; }; previewSettings = { retrieveCurrentSteamImages = true; disableCategories = false; deleteDisabledShortcuts = false; imageZoomPercentage = 30; preload = false; hideUserAccount = false; }; enabledProviders = [ "sgdb" "steamCDN" ]; batchDownloadSize = 50; dnsServers = []; language = "en-US"; theme = "Deck"; emudeckInstall = false; autoUpdate = false; offlineMode = false; navigationWidth = 0; clearLogOnTest = true; version = 8; }; # Parser configurations "steam-rom-manager/userData/userConfigurations.json".text = let # Generate configurations configs = mapAttrsToList (name: emu: mkParserConfig name (emu // { romFolder = if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder; binaryName = if emu.binaryName != "" then emu.binaryName else commonEmulatorConfigs.${name}.binaryName; }) ) (filterAttrs (_: v: v.enable) cfg.emulators); # Join configurations into a valid JSON array configsJson = "[${concatStringsSep "," configs}]"; in configsJson; }; }; }