Mesen 2, launched with a set of settings.json keys ENFORCED.
Mesen owns settings.json: it rewrites all ~220 KB of it (key maps, per-system config, window state) on startup and exit, so the file cannot be a store symlink. What can be declared is the handful of keys this project depends on, merged in before every launch. Everything else in the file stays Mesen's.
None of these keys has a command-line equivalent that works:
Video.UseSoftwareRendereris read in theMainWindowconstructor (UI/Windows/MainWindow.axaml.cs:81), before the command line is parsed, so a switch arrives after the renderer is already chosen. The GPU renderer gives a BLACK game surface with working audio whenever Mesa falls back to llvmpipe, which is what an ordinary WSLg session on this host gets (measured 2026-09-20, mesen 2.1.1; the same build renders correctly with the two variables the devshell sets — seeflake.nix). The software renderer works in both cases.Debug.ScriptWindow.AllowIoOsAccess/AllowNetworkAccessgate LuaSocket, default false, and have no switch at all (UI/Config/Debugger/ScriptWindowConfig.cs:29-30). Network needs I/O on as well (Core/Debugger/ScriptingContext.cpp:73).
THE SEED, when no settings file exists yet
ConfigUpgrade = 1isConfigUpgradeHint.FirstRun, the only value for whichConfiguration.InitializeDefaultspopulates controller mappings. A bare{}deserialises as 0 (Uninitialized) and leaves every port UNMAPPED.- A settings file existing at all is what suppresses the first-run
wizard — including under
--testRunner, which otherwise hangs on it (UI/Program.cs:56-76).
Mesen writes the file with a UTF-8 BOM, which jq rejects, hence the sed.
35{ 36 lib, 37 writeShellApplication, 38 writeText, 39 mesen, 40 jq, 41 gnused, 42 coreutils, 43 # Attrset deep-merged over settings.json before every launch. 44 enforce, 45}: 46let 47 enforced = writeText "mesen-enforced.json" (builtins.toJSON enforce); 48 seed = writeText "mesen-seed.json" ( 49 builtins.toJSON ( 50 lib.recursiveUpdate { 51 ConfigUpgrade = 1; 52 DefaultKeyMappings = "Xbox, ArrowKeys"; 53 } enforce 54 ) 55 ); 56in 57writeShellApplication { 58 # Same name as the upstream binary, which is deliberately NOT also put on 59 # PATH: there is then no unwrapped `Mesen` to launch by mistake. 60 name = "Mesen"; 61 runtimeInputs = [ 62 jq 63 gnused 64 coreutils 65 ]; 66 text = '' 67 settings="''${XDG_CONFIG_HOME:-$HOME/.config}/Mesen2/settings.json" 68 strip_bom() { sed '1s/^\xEF\xBB\xBF//' "$settings"; } 69 70 if [ ! -s "$settings" ]; then 71 mkdir -p "$(dirname "$settings")" 72 cat ${seed} > "$settings" 73 elif ! strip_bom | jq -e --slurpfile e ${enforced} '(. * $e[0]) == .' > /dev/null; then 74 tmp="$(mktemp "$settings.XXXXXX")" 75 trap 'rm -f "$tmp"' EXIT 76 strip_bom | jq --slurpfile e ${enforced} '. * $e[0]' > "$tmp" 77 mv "$tmp" "$settings" 78 trap - EXIT 79 fi 80 81 exec ${mesen}/bin/Mesen "$@" 82 ''; 83}