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
+}