From 2fbb4d1f1b9938de29af31c0f4a144996dfc7593 Mon Sep 17 00:00:00 2001 From: mjallen18 Date: Sun, 15 Mar 2026 19:30:48 -0500 Subject: [PATCH] upd --- flake.nix | 55 ++- modules/steam-rom-manager/default.nix | 517 +++++++++++----------- modules/steam-rom-manager/emulators.nix | 249 +++++++++++ modules/steam-rom-manager/options.nix | 557 +++++++++++++----------- 4 files changed, 842 insertions(+), 536 deletions(-) create mode 100644 modules/steam-rom-manager/emulators.nix diff --git a/flake.nix b/flake.nix index 617406f..de63179 100644 --- a/flake.nix +++ b/flake.nix @@ -3,19 +3,66 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + # home-manager is declared so consumers can do + # inputs.home-manager.follows = "steam-rom-manager/home-manager" + # and so nix flake check can instantiate the module correctly. home-manager = { url = "github:nix-community/home-manager"; inputs.nixpkgs.follows = "nixpkgs"; }; }; - outputs = { self, nixpkgs, home-manager, ... }: + outputs = + { + self, + nixpkgs, + home-manager, + }: let - supportedSystems = [ "x86_64-linux" "aarch64-linux" ]; - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + # Linux systems where Steam (and AppImages) are supported. + # Darwin is intentionally excluded: the SRM AppImage is Linux-only. + linuxSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forLinux = nixpkgs.lib.genAttrs linuxSystems; in { + # Home Manager module — available under both the conventional .default + # key and an explicit name for clarity in consumer flakes. homeManagerModules.default = import ./modules/steam-rom-manager; homeManagerModules.steam-rom-manager = import ./modules/steam-rom-manager; + + # `nix flake check` target: instantiate the module with a minimal config + # to catch evaluation errors before they reach users. + checks = forLinux ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + hmLib = home-manager.lib; + in + { + module-eval = + (hmLib.homeManagerConfiguration { + inherit pkgs; + modules = [ + self.homeManagerModules.default + { + home = { + username = "test"; + homeDirectory = "/home/test"; + stateVersion = "24.11"; + }; + programs.steam-rom-manager = { + enable = true; + steamUsername = "testuser"; + # retroarch is cross-platform; use it as the check target + emulators.retroarch.enable = true; + }; + } + ]; + }).activationPackage; + } + ); }; -} \ No newline at end of file +} diff --git a/modules/steam-rom-manager/default.nix b/modules/steam-rom-manager/default.nix index 73f0f2f..5616c51 100644 --- a/modules/steam-rom-manager/default.nix +++ b/modules/steam-rom-manager/default.nix @@ -1,276 +1,266 @@ -{ config, lib, pkgs, ... }: - -with lib; +{ + config, + lib, + pkgs, + ... +}: let + + version = "2.5.34"; + cfg = config.programs.steam-rom-manager; - # Function to find the main binary in a package - findMainBinary = pkg: + # Single source of truth for built-in emulator metadata (package, romFolder, + # fileTypes). options.nix imports the same file so both files stay in sync. + knownEmulators = import ./emulators.nix pkgs; + + # --------------------------------------------------------------------------- + # 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 - pkgName = pkg.pname or (builtins.parseDrvName pkg.name).name; - - commonVariants = [ - pkgName - "${pkgName}-qt" - "${pkgName}-gtk" - "${pkgName}-emu" - "Ryujinx" + known = knownEmulators.${name} or null; + romFolder = + if emu.romFolder != "" then + emu.romFolder + else if known != null 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 then + known.fileTypes + else + lib.warn "steam-rom-manager: unknown emulator '${name}', fileTypes not set" [ ]; + parserType = emu.parserType; + + # lib.getExe resolves the primary binary without IFD path probing. + exePath = lib.getExe emu.package; + + orderedConfig = [ + { + name = "parserType"; + value = parserType; + } + { + name = "configTitle"; + value = emu.configTitle; + } + { + name = "steamDirectory"; + value = "\${steamdirglobal}"; + } + { + name = "romDirectory"; + value = "${cfg.environmentVariables.romsDirectory}/${romFolder}"; + } + { + name = "steamCategories"; + value = emu.steamCategories; + } + { + name = "executableArgs"; + value = emu.extraArgs; + } + { + name = "executableModifier"; + value = emu.executableModifier; + } + { + name = "startInDirectory"; + value = "${cfg.environmentVariables.romsDirectory}/${romFolder}"; + } + { + name = "titleModifier"; + value = emu.titleModifier; + } + # fetchControllerTemplatesButton / removeControllersButton are UI-only + # action triggers in SRM; they have no meaningful config value and must + # be present as 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 = { + glob = "\${title}@(${lib.concatStringsSep "|" fileTypes})"; + }; + } + { + 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 = "parserId"; + value = name; + } + # SRM userConfigurations schema version — bump when upgrading past a + # breaking config format change in SRM itself. + { + name = "version"; + value = 25; + } ]; - - existingVariant = findFirst - (variant: builtins.pathExists "${pkg}/bin/${variant}") - null - commonVariants; + + makeOrderedJSON = + pairs: + let + joined = builtins.concatStringsSep "," ( + map (pair: "\"${pair.name}\":${builtins.toJSON pair.value}") pairs + ); + in + "{${joined}}"; in - if existingVariant != null - then existingVariant - else pkgName; - - # Common emulator configurations with default packages - commonEmulatorConfigs = { - ryujinx = { - romFolder = "switch"; - fileTypes = [ ".nca" ".NCA" ".nro" ".NRO" ".nso" ".NSO" ".nsp" ".NSP" ".xci" ".XCI" ]; - package = pkgs.ryujinx; - }; - yuzu = { - romFolder = "switch"; - fileTypes = [ ".nsp" ".NSP" ".xci" ".XCI" ]; - package = pkgs.yuzu; - }; - pcsx2 = { - romFolder = "ps2"; - fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".chd" ".CHD" ]; - package = pkgs.pcsx2; - }; - rpcs3 = { - romFolder = "ps3"; - fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".pkg" ".PKG" ]; - package = pkgs.rpcs3; - }; - dolphin-emu = { - romFolder = "gc"; - fileTypes = [ ".iso" ".ISO" ".gcm" ".GCM" ".ciso" ".CISO" ]; - package = pkgs.dolphin-emu; - }; - duckstation = { - romFolder = "psx"; - fileTypes = [ ".iso" ".ISO" ".bin" ".BIN" ".chd" ".CHD" ".pbp" ".PBP" ]; - package = pkgs.duckstation; - }; - melonDS = { - romFolder = "nds"; - fileTypes = [ ".nds" ".NDS" ]; - package = pkgs.melonDS; - }; - cemu = { - romFolder = "wiiu"; - fileTypes = [ ".wud" ".WUD" ".wux" ".WUX" ".rpx" ".RPX" ]; - package = pkgs.cemu; - }; - ppsspp = { - romFolder = "psp"; - fileTypes = [ ".iso" ".ISO" ".cso" ".CSO" ".pbp" ".PBP" ]; - package = pkgs.ppsspp; - }; - mame = { - romFolder = "arcade"; - fileTypes = [ ".zip" ".ZIP" ".7z" ".7Z" ]; - package = pkgs.mame; - }; - dosbox = { - romFolder = "dos"; - fileTypes = [ ".exe" ".EXE" ".bat" ".BAT" ".com" ".COM" ]; - package = pkgs.dosbox; - }; - snes9x = { - romFolder = "snes"; - fileTypes = [ ".smc" ".SMC" ".sfc" ".SFC" ".fig" ".FIG" ]; - package = pkgs.snes9x-gtk; - }; - mgba = { - romFolder = "gba"; - fileTypes = [ ".gba" ".GBA" ]; - package = pkgs.mgba; - }; - mupen64plus = { - romFolder = "n64"; - fileTypes = [ ".n64" ".N64" ".v64" ".V64" ".z64" ".Z64" ]; - package = pkgs.mupen64plus; - }; - retroarch = { - romFolder = "retroarch"; - fileTypes = [ ".zip" ".ZIP" ".7z" ".7Z" ".iso" ".ISO" ".bin" ".BIN" ".chd" ".CHD" ]; - package = pkgs.retroarch; - }; - flycast = { - romFolder = "dreamcast"; - fileTypes = [ ".gdi" ".GDI" ".cdi" ".CDI" ".chd" ".CHD" ]; - package = pkgs.flycast; - }; - citra = { - romFolder = "3ds"; - fileTypes = [ ".3ds" ".3DS" ".cia" ".CIA" ".cxi" ".CXI" ]; - package = pkgs.citra-nightly; - }; - "Non-SRM Shortcuts" = { - parserType = "Non-SRM Shortcuts"; - romFolder = ""; - fileTypes = [ ]; - package = pkgs.steam; - }; - }; - - # Create parser configuration - mkParserConfig = name: emu: - let - # Use the provided package or fall back to the default if available - package = emu.package; - # Get the binary name dynamically - binaryName = findMainBinary package; - - orderedConfig = [ - # Basic parser configuration - { name = "parserType"; value = emu.parserType; } - { name = "configTitle"; value = emu.configTitle; } - { name = "steamDirectory"; value = "\${steamdirglobal}"; } - { name = "romDirectory"; value = "${cfg.environmentVariables.romsDirectory}/${if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder}"; } - { name = "steamCategories"; value = emu.steamCategories; } - - # Executable configuration - { name = "executableArgs"; value = emu.extraArgs; } - { name = "executableModifier"; value = emu.executableModifier; } - { name = "startInDirectory"; value = "${cfg.environmentVariables.romsDirectory}/${if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder}"; } - { name = "titleModifier"; value = emu.titleModifier; } - - # Controller settings - { name = "fetchControllerTemplatesButton"; value = null; } - { name = "removeControllersButton"; value = null; } - { name = "steamInputEnabled"; value = if emu.steamInputEnabled then "1" else "0"; } - - # Image provider configuration - { name = "imageProviders"; value = cfg.enabledProviders; } - { name = "onlineImageQueries"; value = emu.onlineImageQueries; } - { name = "imagePool"; value = emu.imagePool; } - - # DRM and user account settings - { name = "drmProtect"; value = emu.drmProtected; } - { name = "userAccounts"; value = { - specifiedAccounts = emu.userAccounts; - }; } - - # Parser-specific settings - { name = "parserInputs"; value = { - glob = "\${title}@(${concatStringsSep "|" (if emu.fileTypes != [] then emu.fileTypes else commonEmulatorConfigs.${name}.fileTypes)})"; - }; } - - # Executable details - { name = "executable"; value = { - path = "${package}/bin/${binaryName}"; - shortcutPassthrough = emu.shortcutPassthrough; - appendArgsToExecutable = emu.appendArgsToExecutable; - }; } - - # 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 = cfg.imageProviderSettings.sgdb; - }; } - - # 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 - joined = builtins.concatStringsSep "," - (map (pair: "\"${pair.name}\":${builtins.toJSON pair.value}") pairs); - in - "{${joined}}"; - in makeOrderedJSON orderedConfig; - # Fetch the SVG icon file + # --------------------------------------------------------------------------- + # 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/master/src/assets/icons/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="; }; - # # Create Steam ROM Manager package + # --------------------------------------------------------------------------- + # 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-2.5.30.AppImage"; - url = "https://github.com/SteamGridDB/steam-rom-manager/releases/download/v2.5.30/Steam-ROM-Manager-2.5.30.AppImage"; - hash = "sha256-2prpPNgB8EYrswYc98RRrQtHc/s9asbtACRCDyyGQqg="; - }} "$@" -''; + 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="; + } + } "$@" + ''; -in { - imports = [ - ./options.nix - ]; +in +{ + imports = [ ./options.nix ]; - config = mkIf cfg.enable { - home.packages = [ pkgs.appimage-run steam-rom-manager-appimage ] - ++ mapAttrsToList (_: v: v.package) (filterAttrs (_: v: v.enable) cfg.emulators); + config = lib.mkIf cfg.enable { - xdg.dataFile = { - "icons/hicolor/scalable/apps/steam-rom-manager.svg".source = steam-rom-manager-icon; - }; + home.packages = [ + pkgs.appimage-run + steam-rom-manager-appimage + ] + ++ lib.mapAttrsToList (_: v: v.package) (lib.filterAttrs (_: v: v.enable) cfg.emulators); + + 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" ]; + categories = [ + "Game" + "Utility" + ]; type = "Application"; terminal = false; comment = "Add ROMs to Steam with artwork"; @@ -282,12 +272,10 @@ in { }; xdg.configFile = { + # userSettings schema version 8 corresponds to SRM ≥ 2.4.x. "steam-rom-manager/userData/userSettings.json".text = builtins.toJSON { fuzzyMatcher = { - timestamps = { - check = cfg.fuzzyMatcher.timestamps.check; - download = cfg.fuzzyMatcher.timestamps.download; - }; + timestamps = { inherit (cfg.fuzzyMatcher.timestamps) check download; }; verbose = cfg.fuzzyMatcher.verbose; filterProviders = cfg.fuzzyMatcher.filterProviders; }; @@ -316,18 +304,13 @@ in { version = 8; }; - "steam-rom-manager/userData/userConfigurations.json".text = - let - 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); - - configsJson = "[${concatStringsSep "," configs}]"; - in - configsJson; + "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}]"; }; }; -} \ No newline at end of file +} diff --git a/modules/steam-rom-manager/emulators.nix b/modules/steam-rom-manager/emulators.nix new file mode 100644 index 0000000..5b6f394 --- /dev/null +++ b/modules/steam-rom-manager/emulators.nix @@ -0,0 +1,249 @@ +# emulators.nix — single source of truth for built-in emulator metadata. +# +# Both default.nix (for romFolder/fileTypes fallbacks) and options.nix (for +# package defaults) import this file directly, preventing the two files from +# drifting out of sync. +# +# Each entry may contain: +# package — nixpkgs derivation (required) +# romFolder — sub-directory name under romsDirectory (required) +# fileTypes — list of file extensions matched by the glob parser (required) +# parserType — SRM parser type string; omit to use the default "Glob" + +pkgs: { + + ryujinx = { + # ryujinx was removed from nixpkgs; ryubing is the maintained community fork + package = pkgs.ryubing; + romFolder = "switch"; + fileTypes = [ + ".nca" + ".NCA" + ".nro" + ".NRO" + ".nso" + ".NSO" + ".nsp" + ".NSP" + ".xci" + ".XCI" + ]; + }; + + # yuzu was removed from nixpkgs after the Nintendo lawsuit; eden is the successor + yuzu = { + package = pkgs.eden; + romFolder = "switch"; + fileTypes = [ + ".nsp" + ".NSP" + ".xci" + ".XCI" + ]; + }; + + pcsx2 = { + package = pkgs.pcsx2; + romFolder = "ps2"; + fileTypes = [ + ".iso" + ".ISO" + ".bin" + ".BIN" + ".chd" + ".CHD" + ]; + }; + + rpcs3 = { + package = pkgs.rpcs3; + romFolder = "ps3"; + fileTypes = [ + ".iso" + ".ISO" + ".bin" + ".BIN" + ".pkg" + ".PKG" + ]; + }; + + dolphin-emu = { + # Use pkgs.dolphin-emu (the user-facing wrapper), not pkgs.dolphinEmu + package = pkgs.dolphin-emu; + romFolder = "gc"; + fileTypes = [ + ".iso" + ".ISO" + ".gcm" + ".GCM" + ".ciso" + ".CISO" + ".rvz" + ".RVZ" + ".wbfs" + ".WBFS" + ]; + }; + + duckstation = { + package = pkgs.duckstation; + romFolder = "psx"; + fileTypes = [ + ".iso" + ".ISO" + ".bin" + ".BIN" + ".chd" + ".CHD" + ".pbp" + ".PBP" + ]; + }; + + melonDS = { + package = pkgs.melonDS; + romFolder = "nds"; + fileTypes = [ + ".nds" + ".NDS" + ]; + }; + + cemu = { + package = pkgs.cemu; + romFolder = "wiiu"; + fileTypes = [ + ".wud" + ".WUD" + ".wux" + ".WUX" + ".rpx" + ".RPX" + ]; + }; + + ppsspp = { + package = pkgs.ppsspp; + romFolder = "psp"; + fileTypes = [ + ".iso" + ".ISO" + ".cso" + ".CSO" + ".pbp" + ".PBP" + ]; + }; + + mame = { + package = pkgs.mame; + romFolder = "arcade"; + fileTypes = [ + ".zip" + ".ZIP" + ".7z" + ".7Z" + ]; + }; + + dosbox = { + package = pkgs.dosbox; + romFolder = "dos"; + fileTypes = [ + ".exe" + ".EXE" + ".bat" + ".BAT" + ".com" + ".COM" + ]; + }; + + snes9x = { + package = pkgs.snes9x-gtk; + romFolder = "snes"; + fileTypes = [ + ".smc" + ".SMC" + ".sfc" + ".SFC" + ".fig" + ".FIG" + ]; + }; + + mgba = { + package = pkgs.mgba; + romFolder = "gba"; + fileTypes = [ + ".gba" + ".GBA" + ]; + }; + + mupen64plus = { + package = pkgs.mupen64plus; + romFolder = "n64"; + fileTypes = [ + ".n64" + ".N64" + ".v64" + ".V64" + ".z64" + ".Z64" + ]; + }; + + retroarch = { + package = pkgs.retroarch; + romFolder = "retroarch"; + fileTypes = [ + ".zip" + ".ZIP" + ".7z" + ".7Z" + ".iso" + ".ISO" + ".bin" + ".BIN" + ".chd" + ".CHD" + ]; + }; + + flycast = { + package = pkgs.flycast; + romFolder = "dreamcast"; + fileTypes = [ + ".gdi" + ".GDI" + ".cdi" + ".CDI" + ".chd" + ".CHD" + ]; + }; + + # citra-nightly was removed from nixpkgs; azahar is the maintained successor + citra = { + package = pkgs.azahar; + romFolder = "3ds"; + fileTypes = [ + ".3ds" + ".3DS" + ".cia" + ".CIA" + ".cxi" + ".CXI" + ]; + }; + + "Non-SRM Shortcuts" = { + package = pkgs.steam; + parserType = "Non-SRM Shortcuts"; + romFolder = ""; + fileTypes = [ ]; + }; + +} diff --git a/modules/steam-rom-manager/options.nix b/modules/steam-rom-manager/options.nix index 95411d2..293e174 100644 --- a/modules/steam-rom-manager/options.nix +++ b/modules/steam-rom-manager/options.nix @@ -1,360 +1,387 @@ -{ config, lib, pkgs, ... }: - -with lib; +{ + config, + lib, + pkgs, + ... +}: let cfg = config.programs.steam-rom-manager; -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"; - }; + # Single source of truth for built-in emulator metadata. + knownEmulators = import ./emulators.nix pkgs; + + # Valid image provider identifiers accepted by SRM. + validProviders = [ + "sgdb" + "steamCDN" + ]; +in +{ + options.programs.steam-rom-manager = { + + enable = lib.mkEnableOption "Steam ROM Manager"; + + # ------------------------------------------------------------------------- + # Fuzzy matcher + # ------------------------------------------------------------------------- fuzzyMatcher = { timestamps = { - check = mkOption { - type = types.int; + check = lib.mkOption { + type = lib.types.int; default = 0; - description = "Timestamp for fuzzy matcher check"; + description = "Epoch timestamp of the last fuzzy-matcher data check."; }; - download = mkOption { - type = types.int; + download = lib.mkOption { + type = lib.types.int; default = 0; - description = "Timestamp for fuzzy matcher download"; + description = "Epoch timestamp of the last fuzzy-matcher data download."; }; }; - verbose = mkOption { - type = types.bool; + verbose = lib.mkOption { + type = lib.types.bool; default = false; - description = "Enable verbose logging for fuzzy matcher"; + description = "Enable verbose logging for the fuzzy matcher."; }; - filterProviders = mkOption { - type = types.bool; + filterProviders = lib.mkOption { + type = lib.types.bool; default = true; - description = "Filter image providers"; + description = "Restrict image lookups to the configured providers."; }; }; + # ------------------------------------------------------------------------- + # Environment variables written into userSettings.json + # ------------------------------------------------------------------------- environmentVariables = { - steamDirectory = mkOption { - type = types.str; + steamDirectory = lib.mkOption { + type = lib.types.str; default = "${config.home.homeDirectory}/.local/share/Steam"; - description = "Steam installation directory"; + description = "Path to the Steam data directory."; }; - - romsDirectory = mkOption { - type = types.str; + romsDirectory = lib.mkOption { + type = lib.types.str; default = "${config.home.homeDirectory}/Emulation/roms"; - description = "Base directory for ROM files"; + description = "Root directory that contains per-system ROM sub-folders."; }; - - retroarchPath = mkOption { - type = types.str; + retroarchPath = lib.mkOption { + type = lib.types.str; default = ""; - description = "Path to RetroArch executable"; + description = "Path to the RetroArch executable (leave empty if not using RetroArch)."; }; - - raCoresDirectory = mkOption { - type = types.str; + raCoresDirectory = lib.mkOption { + type = lib.types.str; default = ""; - description = "RetroArch cores directory"; + description = "Directory containing RetroArch cores."; }; - - localImagesDirectory = mkOption { - type = types.str; + localImagesDirectory = lib.mkOption { + type = lib.types.str; default = ""; - description = "Directory for local images"; + description = "Directory for locally stored artwork."; }; }; + # ------------------------------------------------------------------------- + # Preview settings + # ------------------------------------------------------------------------- previewSettings = { - retrieveCurrentSteamImages = mkOption { - type = types.bool; + retrieveCurrentSteamImages = lib.mkOption { + type = lib.types.bool; default = true; - description = "Retrieve current Steam images"; + description = "Download existing Steam artwork when previewing."; }; - - disableCategories = mkOption { - type = types.bool; + disableCategories = lib.mkOption { + type = lib.types.bool; default = false; - description = "Disable Steam categories"; + description = "Do not apply Steam category tags to shortcuts."; }; - - deleteDisabledShortcuts = mkOption { - type = types.bool; + deleteDisabledShortcuts = lib.mkOption { + type = lib.types.bool; default = false; - description = "Delete disabled shortcuts"; + description = "Remove shortcuts for parsers that are disabled."; }; - - imageZoomPercentage = mkOption { - type = types.int; + imageZoomPercentage = lib.mkOption { + type = lib.types.int; default = 30; - description = "Image zoom percentage in preview"; + description = "Zoom level (%) used when displaying artwork in the preview pane."; }; - - preload = mkOption { - type = types.bool; + preload = lib.mkOption { + type = lib.types.bool; default = false; - description = "Preload images"; + description = "Pre-fetch artwork while the preview loads."; }; - - hideUserAccount = mkOption { - type = types.bool; + hideUserAccount = lib.mkOption { + type = lib.types.bool; default = false; - description = "Hide user account in preview"; + description = "Hide the Steam account name in the preview pane."; }; }; - enabledProviders = mkOption { - type = types.listOf types.str; - default = [ "sgdb" "steamCDN" ]; - description = "Enabled image providers"; + # ------------------------------------------------------------------------- + # Image providers + # ------------------------------------------------------------------------- + enabledProviders = lib.mkOption { + type = lib.types.listOf (lib.types.enum validProviders); + default = [ + "sgdb" + "steamCDN" + ]; + description = '' + Ordered list of image providers SRM should query. + Valid values: ${lib.concatStringsSep ", " (map (p: ''"${p}"'') validProviders)}. + ''; }; imageProviderSettings = { sgdb = { - nsfw = mkOption { - type = types.bool; + nsfw = lib.mkOption { + type = lib.types.bool; default = false; - description = "Allow NSFW content from SteamGridDB"; + description = "Include NSFW artwork from SteamGridDB."; }; - - humor = mkOption { - type = types.bool; + humor = lib.mkOption { + type = lib.types.bool; default = false; - description = "Allow humor content from SteamGridDB"; + description = "Include humour-tagged artwork from SteamGridDB."; }; - - styles = mkOption { - type = types.listOf types.str; - default = []; - description = "Preferred art styles for SteamGridDB"; + styles = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Preferred grid artwork styles (e.g. \"alternate\", \"blurred\")."; }; - - stylesHero = mkOption { - type = types.listOf types.str; - default = []; - description = "Preferred hero art styles for SteamGridDB"; + stylesHero = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Preferred hero artwork styles."; }; - - stylesLogo = mkOption { - type = types.listOf types.str; - default = []; - description = "Preferred logo styles for SteamGridDB"; + stylesLogo = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Preferred logo styles."; }; - - stylesIcon = mkOption { - type = types.listOf types.str; - default = []; - description = "Preferred icon styles for SteamGridDB"; + stylesIcon = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Preferred icon styles."; }; - - imageMotionTypes = mkOption { - type = types.listOf types.str; + imageMotionTypes = lib.mkOption { + type = lib.types.listOf lib.types.str; default = [ "static" ]; - description = "Allowed image motion types"; + description = "Allowed motion types (\"static\", \"animated\")."; }; - - sizes = mkOption { - type = types.listOf types.str; - default = []; - description = "Preferred image sizes"; + sizes = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Preferred grid image sizes."; }; - - sizesHero = mkOption { - type = types.listOf types.str; - default = []; - description = "Preferred hero image sizes"; + sizesHero = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Preferred hero image sizes."; }; - - sizesIcon = mkOption { - type = types.listOf types.str; - default = []; - description = "Preferred icon sizes"; + sizesIcon = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Preferred icon sizes."; }; }; }; - batchDownloadSize = mkOption { - type = types.int; + # ------------------------------------------------------------------------- + # Miscellaneous top-level settings + # ------------------------------------------------------------------------- + batchDownloadSize = lib.mkOption { + type = lib.types.int; default = 50; - description = "Number of images to download in a batch"; + description = "Number of images to download per batch."; }; - dnsServers = mkOption { - type = types.listOf types.str; - default = []; - description = "Custom DNS servers for image downloads"; + dnsServers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Custom DNS servers used for artwork downloads (leave empty for system DNS)."; }; - language = mkOption { - type = types.str; + language = lib.mkOption { + type = lib.types.str; default = "en-US"; - description = "Application language"; + example = "de-DE"; + description = "BCP-47 language tag for the SRM UI."; }; - theme = mkOption { - type = types.str; + theme = lib.mkOption { + type = lib.types.str; default = "Deck"; - description = "Application theme"; + example = "Default"; + description = "SRM UI theme name."; }; - emudeckInstall = mkOption { - type = types.bool; + emudeckInstall = lib.mkOption { + type = lib.types.bool; default = false; - description = "Is this an EmuDeck installation"; + description = "Set to true when SRM is managed alongside EmuDeck."; }; - autoUpdate = mkOption { - type = types.bool; + autoUpdate = lib.mkOption { + type = lib.types.bool; default = false; - description = "Enable automatic updates"; + description = "Allow SRM to update itself automatically (not recommended in a Nix-managed setup)."; }; - offlineMode = mkOption { - type = types.bool; + offlineMode = lib.mkOption { + type = lib.types.bool; default = false; - description = "Run in offline mode"; + description = "Run SRM without fetching remote artwork."; }; - navigationWidth = mkOption { - type = types.int; + navigationWidth = lib.mkOption { + type = lib.types.int; default = 0; - description = "Navigation panel width"; + description = "Width in pixels of the navigation sidebar (0 = default)."; }; - clearLogOnTest = mkOption { - type = types.bool; + clearLogOnTest = lib.mkOption { + type = lib.types.bool; default = true; - description = "Clear log when testing configuration"; + description = "Clear the log panel each time a parser test is run."; }; - steamUsername = mkOption { - type = types.str; - description = "Steam username for configuration"; + steamUsername = lib.mkOption { + type = lib.types.str; + example = "john"; + description = '' + Steam account username used to build the SRM environment-variable + reference ''${} in userSettings.json. + This must match the account name shown in Steam (not the display name). + ''; }; - emulators = mkOption { - type = types.attrsOf (types.submodule ({ name, ... }: { - options = { - enable = mkEnableOption "emulator configuration"; - package = mkOption { - type = types.package; - default = - if name == "pcsx2" then pkgs.pcsx2 - else if name == "citra" then pkgs.citra-nightly - else if name == "yuzu" then pkgs.yuzu - else if name == "ryujinx" then pkgs.ryubing - else if name == "rpcs3" then pkgs.rpcs3 - else if name == "dolphin-emu" then pkgs.dolphinEmu - else if name == "duckstation" then pkgs.duckstation - else if name == "melonDS" then pkgs.melonDS - else if name == "cemu" then pkgs.cemu - else if name == "ppsspp" then pkgs.ppsspp - else if name == "mame" then pkgs.mame - else if name == "dosbox" then pkgs.dosbox - else if name == "snes9x" then pkgs.snes9x-gtk - else if name == "mgba" then pkgs.mgba - else if name == "mupen64plus" then pkgs.mupen64plus - else if name == "retroarch" then pkgs.retroarch - else if name == "flycast" then pkgs.flycast - else if name == "Non-SRM Shortcuts" then pkgs.steam - else pkgs.${name}; - description = "Emulator package"; - }; - parserType = mkOption { - type = types.str; - default = "Glob"; - description = "Parser type"; - }; - configTitle = mkOption { - type = types.str; - default = name; - description = "Configuration title"; - }; - romFolder = mkOption { - type = types.str; - default = ""; - description = "Name of the ROM folder (defaults to common configuration)"; - }; - steamCategories = mkOption { - type = types.listOf types.str; - default = [""]; - description = "List of Steam categories"; - }; - extraArgs = mkOption { - type = types.str; - default = "--fullscreen \"\${filePath}\""; - description = "Additional emulator arguments"; - }; - executableModifier = mkOption { - type = types.str; - default = "\"\${exePath}\""; - description = "Executable modifier"; - }; - titleModifier = mkOption { - type = types.str; - default = "\${fuzzyTitle}"; - description = "Title modifier"; - }; - # fetchControllerTemplatesButton = mkOption { - # type = types.str; - # default = null; - # description = "Fetch controller templates button"; - # }; - # removeControllersButton = mkOption { - # type = types.str; - # default = null; - # description = "Remove controller templates button"; - # }; - steamInputEnabled = mkOption { - type = types.bool; - default = false; - description = "Enable Steam input"; - }; - onlineImageQueries = mkOption { - type = types.listOf types.str; - default = [ "\${fuzzyTitle}" ]; - description = "List of online image queries"; - }; - imagePool = mkOption { - type = types.str; - default = "\${fuzzyTitle}"; - description = "image pool"; - }; - drmProtected = mkOption { - type = types.bool; - default = false; - description = "DRM protected"; - }; - userAccounts = mkOption { - type = types.listOf types.str; - default = [ "Global" ]; - description = "List of user accounts"; - }; - fileTypes = mkOption { - type = types.listOf types.str; - default = []; - description = "List of ROM file types (defaults to common configuration)"; - }; - shortcutPassthrough = mkOption { - type = types.bool; - default = false; - description = "Enable shortcut passthrough"; - }; - appendArgsToExecutable = mkOption { - type = types.bool; - default = true; - description = "Append arguments to executable"; - }; - }; - })); - default = {}; - description = "Emulator configurations"; + # ------------------------------------------------------------------------- + # Emulator parser configurations + # ------------------------------------------------------------------------- + emulators = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + + enable = lib.mkEnableOption "emulator parser for ${name}"; + + package = lib.mkOption { + type = lib.types.package; + # Resolve against knownEmulators first; fall back to pkgs. so + # users can supply any arbitrary emulator key without a code change. + default = if knownEmulators ? ${name} then knownEmulators.${name}.package else pkgs.${name}; + description = "Package providing the emulator binary."; + }; + + parserType = lib.mkOption { + type = lib.types.str; + default = + if knownEmulators ? ${name} && knownEmulators.${name} ? parserType then + knownEmulators.${name}.parserType + else + "Glob"; + description = "SRM parser type (usually \"Glob\" for file-based ROMs)."; + }; + + configTitle = lib.mkOption { + type = lib.types.str; + default = name; + description = "Human-readable label shown inside SRM for this parser."; + }; + + romFolder = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Sub-folder under + that contains ROMs for this emulator. Defaults to the built-in + value for known emulators; must be set explicitly for custom ones. + ''; + }; + + steamCategories = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "" ]; + description = "Steam categories / collection tags to apply to shortcuts."; + }; + + extraArgs = lib.mkOption { + type = lib.types.str; + default = "--fullscreen \"\${filePath}\""; + description = "Command-line arguments passed to the emulator."; + }; + + executableModifier = lib.mkOption { + type = lib.types.str; + default = "\"\${exePath}\""; + description = "SRM executable modifier expression."; + }; + + titleModifier = lib.mkOption { + type = lib.types.str; + default = "\${fuzzyTitle}"; + description = "SRM title modifier expression."; + }; + + steamInputEnabled = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable Steam Input controller remapping for shortcuts."; + }; + + onlineImageQueries = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "\${fuzzyTitle}" ]; + description = "Query strings used to search for artwork online."; + }; + + imagePool = lib.mkOption { + type = lib.types.str; + default = "\${fuzzyTitle}"; + description = "SRM image pool identifier for artwork caching."; + }; + + drmProtected = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Mark shortcuts as DRM-protected."; + }; + + userAccounts = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "Global" ]; + description = "Steam accounts to apply this parser to (\"Global\" applies to all)."; + }; + + fileTypes = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + File extensions matched by the glob parser (e.g. ".iso"). + Defaults to the built-in list for known emulators; must be set for custom ones. + ''; + }; + + shortcutPassthrough = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Pass through existing Steam shortcuts rather than replacing them."; + }; + + appendArgsToExecutable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Append to the executable path in the shortcut."; + }; + }; + } + ) + ); + default = { }; + description = "Attribute set of emulator parser configurations keyed by emulator name."; }; }; -} \ No newline at end of file +}