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

@@ -3,19 +3,66 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 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 = { home-manager = {
url = "github:nix-community/home-manager"; url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
outputs = { self, nixpkgs, home-manager, ... }: outputs =
{
self,
nixpkgs,
home-manager,
}:
let let
supportedSystems = [ "x86_64-linux" "aarch64-linux" ]; # Linux systems where Steam (and AppImages) are supported.
forAllSystems = nixpkgs.lib.genAttrs supportedSystems; # Darwin is intentionally excluded: the SRM AppImage is Linux-only.
linuxSystems = [
"x86_64-linux"
"aarch64-linux"
];
forLinux = nixpkgs.lib.genAttrs linuxSystems;
in 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.default = import ./modules/steam-rom-manager;
homeManagerModules.steam-rom-manager = 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;
}
);
}; };
} }

View File

@@ -1,192 +1,158 @@
{ config, lib, pkgs, ... }: {
config,
with lib; lib,
pkgs,
...
}:
let let
version = "2.5.34";
cfg = config.programs.steam-rom-manager; cfg = config.programs.steam-rom-manager;
# Function to find the main binary in a package # Single source of truth for built-in emulator metadata (package, romFolder,
findMainBinary = pkg: # 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 let
pkgName = pkg.pname or (builtins.parseDrvName pkg.name).name; 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;
commonVariants = [ # lib.getExe resolves the primary binary without IFD path probing.
pkgName exePath = lib.getExe emu.package;
"${pkgName}-qt"
"${pkgName}-gtk"
"${pkgName}-emu"
"Ryujinx"
];
existingVariant = findFirst
(variant: builtins.pathExists "${pkg}/bin/${variant}")
null
commonVariants;
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 = [ orderedConfig = [
# Basic parser configuration {
{ name = "parserType"; value = emu.parserType; } name = "parserType";
{ name = "configTitle"; value = emu.configTitle; } value = parserType;
{ 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; } name = "configTitle";
value = emu.configTitle;
# Executable configuration }
{ name = "executableArgs"; value = emu.extraArgs; } {
{ name = "executableModifier"; value = emu.executableModifier; } name = "steamDirectory";
{ name = "startInDirectory"; value = "${cfg.environmentVariables.romsDirectory}/${if emu.romFolder != "" then emu.romFolder else commonEmulatorConfigs.${name}.romFolder}"; } value = "\${steamdirglobal}";
{ name = "titleModifier"; value = emu.titleModifier; } }
{
# Controller settings name = "romDirectory";
{ name = "fetchControllerTemplatesButton"; value = null; } value = "${cfg.environmentVariables.romsDirectory}/${romFolder}";
{ name = "removeControllersButton"; value = null; } }
{ name = "steamInputEnabled"; value = if emu.steamInputEnabled then "1" else "0"; } {
name = "steamCategories";
# Image provider configuration value = emu.steamCategories;
{ name = "imageProviders"; value = cfg.enabledProviders; } }
{ name = "onlineImageQueries"; value = emu.onlineImageQueries; } {
{ name = "imagePool"; value = emu.imagePool; } name = "executableArgs";
value = emu.extraArgs;
# DRM and user account settings }
{ name = "drmProtect"; value = emu.drmProtected; } {
{ name = "userAccounts"; value = { 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; specifiedAccounts = emu.userAccounts;
}; } };
}
# Parser-specific settings {
{ name = "parserInputs"; value = { name = "parserInputs";
glob = "\${title}@(${concatStringsSep "|" (if emu.fileTypes != [] then emu.fileTypes else commonEmulatorConfigs.${name}.fileTypes)})"; value = {
}; } glob = "\${title}@(${lib.concatStringsSep "|" fileTypes})";
};
# Executable details }
{ name = "executable"; value = { {
path = "${package}/bin/${binaryName}"; name = "executable";
value = {
path = exePath;
shortcutPassthrough = emu.shortcutPassthrough; shortcutPassthrough = emu.shortcutPassthrough;
appendArgsToExecutable = emu.appendArgsToExecutable; appendArgsToExecutable = emu.appendArgsToExecutable;
}; } };
}
# Title and fuzzy matching configuration {
{ name = "titleFromVariable"; value = { name = "titleFromVariable";
value = {
limitToGroups = [ ]; limitToGroups = [ ];
caseInsensitiveVariables = false; caseInsensitiveVariables = false;
skipFileIfVariableWasNotFound = false; skipFileIfVariableWasNotFound = false;
}; } };
}
{ name = "fuzzyMatch"; value = { {
name = "fuzzyMatch";
value = {
replaceDiacritics = true; replaceDiacritics = true;
removeCharacters = true; removeCharacters = true;
removeBrackets = true; removeBrackets = true;
}; } };
}
# Controller configuration {
{ name = "controllers"; value = { name = "controllers";
value = {
ps4 = null; ps4 = null;
ps5 = null; ps5 = null;
ps5_edge = null; ps5_edge = null;
@@ -198,79 +164,103 @@ let
switch_pro = null; switch_pro = null;
neptune = null; neptune = null;
steamcontroller_gordon = null; steamcontroller_gordon = null;
}; } };
}
# Image provider API configuration {
{ name = "imageProviderAPIs"; value = { name = "imageProviderAPIs";
value = {
sgdb = cfg.imageProviderSettings.sgdb; sgdb = cfg.imageProviderSettings.sgdb;
}; } };
}
# Default and local image settings {
{ name = "defaultImage"; value = { name = "defaultImage";
value = {
tall = ""; tall = "";
long = ""; long = "";
hero = ""; hero = "";
logo = ""; logo = "";
icon = ""; icon = "";
}; } };
}
{ name = "localImages"; value = { {
name = "localImages";
value = {
tall = ""; tall = "";
long = ""; long = "";
hero = ""; hero = "";
logo = ""; logo = "";
icon = ""; icon = "";
}; } };
}
# Parser identification {
{ name = "parserId"; value = name; } name = "parserId";
{ name = "version"; value = 25; } value = name;
}
# SRM userConfigurations schema version — bump when upgrading past a
# breaking config format change in SRM itself.
{
name = "version";
value = 25;
}
]; ];
# Function to convert our ordered list into properly formatted JSON makeOrderedJSON =
makeOrderedJSON = pairs: pairs:
let let
joined = builtins.concatStringsSep "," joined = builtins.concatStringsSep "," (
(map (pair: "\"${pair.name}\":${builtins.toJSON pair.value}") pairs); map (pair: "\"${pair.name}\":${builtins.toJSON pair.value}") pairs
);
in in
"{${joined}}"; "{${joined}}";
in in
makeOrderedJSON orderedConfig; 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 { steam-rom-manager-icon = pkgs.fetchurl {
name = "steam-rom-manager.svg"; 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="; 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" '' steam-rom-manager-appimage = pkgs.writeShellScriptBin "steam-rom-manager" ''
exec ${pkgs.appimage-run}/bin/appimage-run ${pkgs.fetchurl { exec ${pkgs.appimage-run}/bin/appimage-run ${
name = "steam-rom-manager-2.5.30.AppImage"; pkgs.fetchurl {
url = "https://github.com/SteamGridDB/steam-rom-manager/releases/download/v2.5.30/Steam-ROM-Manager-2.5.30.AppImage"; name = "steam-rom-manager-${version}.AppImage";
hash = "sha256-2prpPNgB8EYrswYc98RRrQtHc/s9asbtACRCDyyGQqg="; url = "https://github.com/SteamGridDB/steam-rom-manager/releases/download/v${version}/Steam-ROM-Manager-${version}.AppImage";
}} "$@" hash = "sha256-QbXwfT91BQ15/DL3IYC3qZcahlsQvkUKTwMUUpZY+U8=";
}
} "$@"
''; '';
in { in
imports = [ {
./options.nix imports = [ ./options.nix ];
];
config = mkIf cfg.enable { config = lib.mkIf cfg.enable {
home.packages = [ pkgs.appimage-run steam-rom-manager-appimage ]
++ mapAttrsToList (_: v: v.package) (filterAttrs (_: v: v.enable) cfg.emulators);
xdg.dataFile = { home.packages = [
"icons/hicolor/scalable/apps/steam-rom-manager.svg".source = steam-rom-manager-icon; 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 = { xdg.desktopEntries.steam-rom-manager = {
name = "Steam ROM Manager"; name = "Steam ROM Manager";
exec = "${steam-rom-manager-appimage}/bin/steam-rom-manager"; exec = "${steam-rom-manager-appimage}/bin/steam-rom-manager";
icon = "steam-rom-manager"; icon = "steam-rom-manager";
categories = [ "Game" "Utility" ]; categories = [
"Game"
"Utility"
];
type = "Application"; type = "Application";
terminal = false; terminal = false;
comment = "Add ROMs to Steam with artwork"; comment = "Add ROMs to Steam with artwork";
@@ -282,12 +272,10 @@ in {
}; };
xdg.configFile = { xdg.configFile = {
# userSettings schema version 8 corresponds to SRM ≥ 2.4.x.
"steam-rom-manager/userData/userSettings.json".text = builtins.toJSON { "steam-rom-manager/userData/userSettings.json".text = builtins.toJSON {
fuzzyMatcher = { fuzzyMatcher = {
timestamps = { timestamps = { inherit (cfg.fuzzyMatcher.timestamps) check download; };
check = cfg.fuzzyMatcher.timestamps.check;
download = cfg.fuzzyMatcher.timestamps.download;
};
verbose = cfg.fuzzyMatcher.verbose; verbose = cfg.fuzzyMatcher.verbose;
filterProviders = cfg.fuzzyMatcher.filterProviders; filterProviders = cfg.fuzzyMatcher.filterProviders;
}; };
@@ -318,16 +306,11 @@ in {
"steam-rom-manager/userData/userConfigurations.json".text = "steam-rom-manager/userData/userConfigurations.json".text =
let let
configs = mapAttrsToList (name: emu: configs = lib.mapAttrsToList (name: emu: mkParserConfig name emu) (
mkParserConfig name (emu // { lib.filterAttrs (_: v: v.enable) cfg.emulators
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 in
configsJson; "[${lib.concatStringsSep "," configs}]";
}; };
}; };
} }

View File

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

View File

@@ -1,360 +1,387 @@
{ config, lib, pkgs, ... }: {
config,
with lib; lib,
pkgs,
...
}:
let let
cfg = config.programs.steam-rom-manager; cfg = config.programs.steam-rom-manager;
in {
# 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 = { options.programs.steam-rom-manager = {
enable = mkEnableOption "Steam ROM Manager";
package = mkOption { enable = lib.mkEnableOption "Steam ROM Manager";
type = types.package;
default = steam-rom-manager;
description = "Steam ROM Manager package";
};
# -------------------------------------------------------------------------
# Fuzzy matcher
# -------------------------------------------------------------------------
fuzzyMatcher = { fuzzyMatcher = {
timestamps = { timestamps = {
check = mkOption { check = lib.mkOption {
type = types.int; type = lib.types.int;
default = 0; default = 0;
description = "Timestamp for fuzzy matcher check"; description = "Epoch timestamp of the last fuzzy-matcher data check.";
}; };
download = mkOption { download = lib.mkOption {
type = types.int; type = lib.types.int;
default = 0; default = 0;
description = "Timestamp for fuzzy matcher download"; description = "Epoch timestamp of the last fuzzy-matcher data download.";
}; };
}; };
verbose = mkOption { verbose = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = false; default = false;
description = "Enable verbose logging for fuzzy matcher"; description = "Enable verbose logging for the fuzzy matcher.";
}; };
filterProviders = mkOption { filterProviders = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = true; default = true;
description = "Filter image providers"; description = "Restrict image lookups to the configured providers.";
}; };
}; };
# -------------------------------------------------------------------------
# Environment variables written into userSettings.json
# -------------------------------------------------------------------------
environmentVariables = { environmentVariables = {
steamDirectory = mkOption { steamDirectory = lib.mkOption {
type = types.str; type = lib.types.str;
default = "${config.home.homeDirectory}/.local/share/Steam"; default = "${config.home.homeDirectory}/.local/share/Steam";
description = "Steam installation directory"; description = "Path to the Steam data directory.";
}; };
romsDirectory = lib.mkOption {
romsDirectory = mkOption { type = lib.types.str;
type = types.str;
default = "${config.home.homeDirectory}/Emulation/roms"; default = "${config.home.homeDirectory}/Emulation/roms";
description = "Base directory for ROM files"; description = "Root directory that contains per-system ROM sub-folders.";
}; };
retroarchPath = lib.mkOption {
retroarchPath = mkOption { type = lib.types.str;
type = types.str;
default = ""; default = "";
description = "Path to RetroArch executable"; description = "Path to the RetroArch executable (leave empty if not using RetroArch).";
}; };
raCoresDirectory = lib.mkOption {
raCoresDirectory = mkOption { type = lib.types.str;
type = types.str;
default = ""; default = "";
description = "RetroArch cores directory"; description = "Directory containing RetroArch cores.";
}; };
localImagesDirectory = lib.mkOption {
localImagesDirectory = mkOption { type = lib.types.str;
type = types.str;
default = ""; default = "";
description = "Directory for local images"; description = "Directory for locally stored artwork.";
}; };
}; };
# -------------------------------------------------------------------------
# Preview settings
# -------------------------------------------------------------------------
previewSettings = { previewSettings = {
retrieveCurrentSteamImages = mkOption { retrieveCurrentSteamImages = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = true; default = true;
description = "Retrieve current Steam images"; description = "Download existing Steam artwork when previewing.";
}; };
disableCategories = lib.mkOption {
disableCategories = mkOption { type = lib.types.bool;
type = types.bool;
default = false; default = false;
description = "Disable Steam categories"; description = "Do not apply Steam category tags to shortcuts.";
}; };
deleteDisabledShortcuts = lib.mkOption {
deleteDisabledShortcuts = mkOption { type = lib.types.bool;
type = types.bool;
default = false; default = false;
description = "Delete disabled shortcuts"; description = "Remove shortcuts for parsers that are disabled.";
}; };
imageZoomPercentage = lib.mkOption {
imageZoomPercentage = mkOption { type = lib.types.int;
type = types.int;
default = 30; default = 30;
description = "Image zoom percentage in preview"; description = "Zoom level (%) used when displaying artwork in the preview pane.";
}; };
preload = lib.mkOption {
preload = mkOption { type = lib.types.bool;
type = types.bool;
default = false; default = false;
description = "Preload images"; description = "Pre-fetch artwork while the preview loads.";
}; };
hideUserAccount = lib.mkOption {
hideUserAccount = mkOption { type = lib.types.bool;
type = types.bool;
default = false; 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; # Image providers
default = [ "sgdb" "steamCDN" ]; # -------------------------------------------------------------------------
description = "Enabled 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 = { imageProviderSettings = {
sgdb = { sgdb = {
nsfw = mkOption { nsfw = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = false; default = false;
description = "Allow NSFW content from SteamGridDB"; description = "Include NSFW artwork from SteamGridDB.";
}; };
humor = lib.mkOption {
humor = mkOption { type = lib.types.bool;
type = types.bool;
default = false; default = false;
description = "Allow humor content from SteamGridDB"; description = "Include humour-tagged artwork from SteamGridDB.";
}; };
styles = lib.mkOption {
styles = mkOption { type = lib.types.listOf lib.types.str;
type = types.listOf types.str;
default = [ ]; default = [ ];
description = "Preferred art styles for SteamGridDB"; description = "Preferred grid artwork styles (e.g. \"alternate\", \"blurred\").";
}; };
stylesHero = lib.mkOption {
stylesHero = mkOption { type = lib.types.listOf lib.types.str;
type = types.listOf types.str;
default = [ ]; default = [ ];
description = "Preferred hero art styles for SteamGridDB"; description = "Preferred hero artwork styles.";
}; };
stylesLogo = lib.mkOption {
stylesLogo = mkOption { type = lib.types.listOf lib.types.str;
type = types.listOf types.str;
default = [ ]; default = [ ];
description = "Preferred logo styles for SteamGridDB"; description = "Preferred logo styles.";
}; };
stylesIcon = lib.mkOption {
stylesIcon = mkOption { type = lib.types.listOf lib.types.str;
type = types.listOf types.str;
default = [ ]; default = [ ];
description = "Preferred icon styles for SteamGridDB"; description = "Preferred icon styles.";
}; };
imageMotionTypes = lib.mkOption {
imageMotionTypes = mkOption { type = lib.types.listOf lib.types.str;
type = types.listOf types.str;
default = [ "static" ]; default = [ "static" ];
description = "Allowed image motion types"; description = "Allowed motion types (\"static\", \"animated\").";
}; };
sizes = lib.mkOption {
sizes = mkOption { type = lib.types.listOf lib.types.str;
type = types.listOf types.str;
default = [ ]; default = [ ];
description = "Preferred image sizes"; description = "Preferred grid image sizes.";
}; };
sizesHero = lib.mkOption {
sizesHero = mkOption { type = lib.types.listOf lib.types.str;
type = types.listOf types.str;
default = [ ]; default = [ ];
description = "Preferred hero image sizes"; description = "Preferred hero image sizes.";
}; };
sizesIcon = lib.mkOption {
sizesIcon = mkOption { type = lib.types.listOf lib.types.str;
type = types.listOf types.str;
default = [ ]; default = [ ];
description = "Preferred icon sizes"; description = "Preferred icon sizes.";
}; };
}; };
}; };
batchDownloadSize = mkOption { # -------------------------------------------------------------------------
type = types.int; # Miscellaneous top-level settings
# -------------------------------------------------------------------------
batchDownloadSize = lib.mkOption {
type = lib.types.int;
default = 50; default = 50;
description = "Number of images to download in a batch"; description = "Number of images to download per batch.";
}; };
dnsServers = mkOption { dnsServers = lib.mkOption {
type = types.listOf types.str; type = lib.types.listOf lib.types.str;
default = [ ]; default = [ ];
description = "Custom DNS servers for image downloads"; description = "Custom DNS servers used for artwork downloads (leave empty for system DNS).";
}; };
language = mkOption { language = lib.mkOption {
type = types.str; type = lib.types.str;
default = "en-US"; default = "en-US";
description = "Application language"; example = "de-DE";
description = "BCP-47 language tag for the SRM UI.";
}; };
theme = mkOption { theme = lib.mkOption {
type = types.str; type = lib.types.str;
default = "Deck"; default = "Deck";
description = "Application theme"; example = "Default";
description = "SRM UI theme name.";
}; };
emudeckInstall = mkOption { emudeckInstall = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = false; default = false;
description = "Is this an EmuDeck installation"; description = "Set to true when SRM is managed alongside EmuDeck.";
}; };
autoUpdate = mkOption { autoUpdate = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = false; default = false;
description = "Enable automatic updates"; description = "Allow SRM to update itself automatically (not recommended in a Nix-managed setup).";
}; };
offlineMode = mkOption { offlineMode = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = false; default = false;
description = "Run in offline mode"; description = "Run SRM without fetching remote artwork.";
}; };
navigationWidth = mkOption { navigationWidth = lib.mkOption {
type = types.int; type = lib.types.int;
default = 0; default = 0;
description = "Navigation panel width"; description = "Width in pixels of the navigation sidebar (0 = default).";
}; };
clearLogOnTest = mkOption { clearLogOnTest = lib.mkOption {
type = types.bool; type = lib.types.bool;
default = true; default = true;
description = "Clear log when testing configuration"; description = "Clear the log panel each time a parser test is run.";
}; };
steamUsername = mkOption { steamUsername = lib.mkOption {
type = types.str; type = lib.types.str;
description = "Steam username for configuration"; example = "john";
description = ''
Steam account username used to build the SRM environment-variable
reference <literal>''${<username>}</literal> in userSettings.json.
This must match the account name shown in Steam (not the display name).
'';
}; };
emulators = mkOption { # -------------------------------------------------------------------------
type = types.attrsOf (types.submodule ({ name, ... }: { # Emulator parser configurations
# -------------------------------------------------------------------------
emulators = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = { options = {
enable = mkEnableOption "emulator configuration";
package = mkOption { enable = lib.mkEnableOption "emulator parser for ${name}";
type = types.package;
package = lib.mkOption {
type = lib.types.package;
# Resolve against knownEmulators first; fall back to pkgs.<name> 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 = default =
if name == "pcsx2" then pkgs.pcsx2 if knownEmulators ? ${name} && knownEmulators.${name} ? parserType then
else if name == "citra" then pkgs.citra-nightly knownEmulators.${name}.parserType
else if name == "yuzu" then pkgs.yuzu else
else if name == "ryujinx" then pkgs.ryubing "Glob";
else if name == "rpcs3" then pkgs.rpcs3 description = "SRM parser type (usually \"Glob\" for file-based ROMs).";
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; configTitle = lib.mkOption {
default = "Glob"; type = lib.types.str;
description = "Parser type";
};
configTitle = mkOption {
type = types.str;
default = name; default = name;
description = "Configuration title"; description = "Human-readable label shown inside SRM for this parser.";
}; };
romFolder = mkOption {
type = types.str; romFolder = lib.mkOption {
type = lib.types.str;
default = ""; default = "";
description = "Name of the ROM folder (defaults to common configuration)"; description = ''
Sub-folder under <option>environmentVariables.romsDirectory</option>
that contains ROMs for this emulator. Defaults to the built-in
value for known emulators; must be set explicitly for custom ones.
'';
}; };
steamCategories = mkOption {
type = types.listOf types.str; steamCategories = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "" ]; default = [ "" ];
description = "List of Steam categories"; description = "Steam categories / collection tags to apply to shortcuts.";
}; };
extraArgs = mkOption {
type = types.str; extraArgs = lib.mkOption {
type = lib.types.str;
default = "--fullscreen \"\${filePath}\""; default = "--fullscreen \"\${filePath}\"";
description = "Additional emulator arguments"; description = "Command-line arguments passed to the emulator.";
}; };
executableModifier = mkOption {
type = types.str; executableModifier = lib.mkOption {
type = lib.types.str;
default = "\"\${exePath}\""; default = "\"\${exePath}\"";
description = "Executable modifier"; description = "SRM executable modifier expression.";
}; };
titleModifier = mkOption {
type = types.str; titleModifier = lib.mkOption {
type = lib.types.str;
default = "\${fuzzyTitle}"; default = "\${fuzzyTitle}";
description = "Title modifier"; description = "SRM title modifier expression.";
}; };
# fetchControllerTemplatesButton = mkOption {
# type = types.str; steamInputEnabled = lib.mkOption {
# default = null; type = lib.types.bool;
# description = "Fetch controller templates button";
# };
# removeControllersButton = mkOption {
# type = types.str;
# default = null;
# description = "Remove controller templates button";
# };
steamInputEnabled = mkOption {
type = types.bool;
default = false; default = false;
description = "Enable Steam input"; description = "Enable Steam Input controller remapping for shortcuts.";
}; };
onlineImageQueries = mkOption {
type = types.listOf types.str; onlineImageQueries = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "\${fuzzyTitle}" ]; default = [ "\${fuzzyTitle}" ];
description = "List of online image queries"; description = "Query strings used to search for artwork online.";
}; };
imagePool = mkOption {
type = types.str; imagePool = lib.mkOption {
type = lib.types.str;
default = "\${fuzzyTitle}"; default = "\${fuzzyTitle}";
description = "image pool"; description = "SRM image pool identifier for artwork caching.";
}; };
drmProtected = mkOption {
type = types.bool; drmProtected = lib.mkOption {
type = lib.types.bool;
default = false; default = false;
description = "DRM protected"; description = "Mark shortcuts as DRM-protected.";
}; };
userAccounts = mkOption {
type = types.listOf types.str; userAccounts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "Global" ]; default = [ "Global" ];
description = "List of user accounts"; description = "Steam accounts to apply this parser to (\"Global\" applies to all).";
}; };
fileTypes = mkOption {
type = types.listOf types.str; fileTypes = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ]; default = [ ];
description = "List of ROM file types (defaults to common configuration)"; description = ''
File extensions matched by the glob parser (e.g. <literal>".iso"</literal>).
Defaults to the built-in list for known emulators; must be set for custom ones.
'';
}; };
shortcutPassthrough = mkOption {
type = types.bool; shortcutPassthrough = lib.mkOption {
type = lib.types.bool;
default = false; default = false;
description = "Enable shortcut passthrough"; description = "Pass through existing Steam shortcuts rather than replacing them.";
}; };
appendArgsToExecutable = mkOption {
type = types.bool; appendArgsToExecutable = lib.mkOption {
type = lib.types.bool;
default = true; default = true;
description = "Append arguments to executable"; description = "Append <option>extraArgs</option> to the executable path in the shortcut.";
}; };
}; };
})); }
default = {}; )
description = "Emulator configurations"; );
default = { };
description = "Attribute set of emulator parser configurations keyed by emulator name.";
}; };
}; };
} }