This commit is contained in:
mjallen18
2026-03-15 19:30:48 -05:00
parent d303a48a30
commit 2fbb4d1f1b
4 changed files with 842 additions and 536 deletions

View File

@@ -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}]";
};
};
}
}