Compare commits

9 Commits

11 changed files with 273 additions and 8 deletions

View File

@@ -94,6 +94,14 @@ in
} }
]; ];
registry.homepage.${config.networking.hostName}.romm = {
tab = "Apps";
section = "Media";
name = "RomM";
description = "Games";
icon = "romm.png";
};
guests.romm = { guests.romm = {
interior = { interior = {
modules.services.romm.enable = true; modules.services.romm.enable = true;

View File

@@ -104,6 +104,7 @@
domain = "games.alexion.dev"; domain = "games.alexion.dev";
protocol = "https"; protocol = "https";
port = 80; port = 80;
clientMaxBodySize = "0";
}; };
}; };
}; };

View File

@@ -44,6 +44,7 @@
modules.desktop.enable = true; modules.desktop.enable = true;
modules.desktop.apps.obsidian.enable = true; modules.desktop.apps.obsidian.enable = true;
modules.desktop.apps.r2modman.enable = true; modules.desktop.apps.r2modman.enable = true;
modules.desktop.emulation.enable = true;
modules.desktop.apps.signal.enable = true; modules.desktop.apps.signal.enable = true;
modules.desktop.apps.steam.enable = true; modules.desktop.apps.steam.enable = true;
modules.desktop.apps.vencord.enable = true; modules.desktop.apps.vencord.enable = true;

View File

@@ -0,0 +1,36 @@
{
config,
lib,
...
}:
# Shared directories for emulators.
let
user = config.user.name;
directory = "/home/${user}/emulation";
in
{
options.modules.desktop.emulation.enable = lib.mkEnableOption "shared emulation directories";
config = lib.mkIf config.modules.desktop.emulation.enable {
modules.desktop.emulation.retroarch = {
enable = lib.mkDefault true;
browserDirectory = "${directory}/roms";
};
home-manager.users.${user} = hm: {
home.file = {
".config/retroarch/saves".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/saves";
".config/retroarch/states".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/states";
".config/retroarch/system".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/bios";
};
systemd.user.tmpfiles.rules = map (path: "d ${path} 0700 - - -") [
directory
"${directory}/bios"
"${directory}/roms"
"${directory}/saves"
"${directory}/states"
];
};
};
}

View File

@@ -0,0 +1,62 @@
{
config,
lib,
pkgs,
util,
...
}:
# RetroArch with the selected PlayStation and Sega Saturn cores.
let
cfg = config.modules.desktop.emulation.retroarch;
user = config.user.name;
in
{
options.modules.desktop.emulation.retroarch = {
enable = lib.mkEnableOption "RetroArch with PlayStation and Sega Saturn cores";
browserDirectory = lib.mkOption {
type = lib.types.str;
default = "";
description = "Directory where RetroArch starts its content browser.";
};
};
config = lib.mkIf cfg.enable {
home-manager.users.${user} = {
home.packages = [
(pkgs.wrapRetroArch {
cores = with pkgs.libretro; [
beetle-psx-hw
beetle-saturn
];
settings = {
savefiles_in_content_dir = "false";
savestates_in_content_dir = "false";
system_directory = "/home/${user}/.config/retroarch/system";
video_fullscreen = "true";
sort_savefiles_enable = "true";
sort_savestates_enable = "true";
cloud_sync_enable = "true";
cloud_sync_driver = "webdav";
webdav_url = "https://games.alexion.dev/api/sync/retroarch/";
cloud_sync_sync_saves = "true";
cloud_sync_sync_configs = "true";
cloud_sync_sync_thumbs = "false";
cloud_sync_sync_system = "false";
cloud_sync_sync_mode = "0";
cloud_sync_destructive = "false";
}
// lib.optionalAttrs (cfg.browserDirectory != "") {
rgui_browser_directory = cfg.browserDirectory;
};
})
];
};
registry.window-icon.${config.networking.hostName}.retroarch = {
class = "com.libretro.RetroArch";
icon = util.glyph "f11b";
};
};
}

View File

@@ -71,7 +71,10 @@ let
extraConfig = lib.optionalString (route.clientMaxBodySize != null) '' extraConfig = lib.optionalString (route.clientMaxBodySize != null) ''
client_max_body_size ${route.clientMaxBodySize}; client_max_body_size ${route.clientMaxBodySize};
''; '';
locations."/".proxyPass = mkBackend address port; locations."/" = {
proxyPass = mkBackend address port;
proxyWebsockets = true;
};
}; };
}; };

View File

@@ -1,6 +1,7 @@
{ {
config, config,
lib, lib,
pkgs,
... ...
}: }:
let let
@@ -31,13 +32,33 @@ in
services.romm = { services.romm = {
enable = true; enable = true;
package = pkgs.alexion.romm;
group = "storage"; group = "storage";
database.createLocally = true; database.createLocally = true;
redis.createLocally = true; redis.createLocally = true;
watcher.enable = true; watcher.enable = true;
extraEnvironment = {
HASHEOUS_API_ENABLED = "true";
PLAYMATCH_API_ENABLED = "true";
ROM_CONVERTO_ENABLED = "false";
};
inherit (cfg) environmentFile; inherit (cfg) environmentFile;
}; };
systemd.services.romm-worker.serviceConfig.ExecStart =
lib.mkForce "${lib.getExe' config.services.romm.package "romm-worker"} scans";
systemd.tmpfiles.settings."10-romm"."${config.services.romm.dataDir}/config/config.yml"."L+".argument =
toString (
(pkgs.formats.yaml { }).generate "romm-config.yml" {
emulatorjs.auto_save_sync = false;
filesystem.structure = {
default = "roms/{platform}/{game}";
firmware = "bios/{platform}";
};
}
);
users.users = { users.users = {
romm.uid = config.registry."stable-uid".${config.networking.hostName}.romm; romm.uid = config.registry."stable-uid".${config.networking.hostName}.romm;
redis-romm.uid = config.registry."stable-uid".${config.networking.hostName}."redis-romm"; redis-romm.uid = config.registry."stable-uid".${config.networking.hostName}."redis-romm";

View File

@@ -3,8 +3,8 @@ aliases:
- "0015" - "0015"
- RomM 5.4 Package - RomM 5.4 Package
date-created: 2026-10-19 19:40 date-created: 2026-10-19 19:40
date-modified: 2026-10-19 19:40 date-modified: 2026-10-21 00:01
status: open status: closed
area: "[[homelab]]" area: "[[homelab]]"
blocked-by: blocked-by:
- "[[0014-romm-guest]]" - "[[0014-romm-guest]]"

View File

@@ -4,5 +4,7 @@
inherit root; inherit root;
}; };
romm = pkgs.callPackage ./romm.nix { };
waybar = pkgs.callPackage ./waybar.nix { }; waybar = pkgs.callPackage ./waybar.nix { };
} }

124
src/flake/packages/romm.nix Normal file
View File

@@ -0,0 +1,124 @@
{
buildNpmPackage,
fetchFromGitHub,
lib,
python3,
romm,
}:
romm.overrideAttrs (
finalAttrs: previousAttrs:
let
frontend = buildNpmPackage {
pname = "romm-frontend";
inherit (finalAttrs) version;
src = "${finalAttrs.src}/frontend";
npmDepsHash = "sha256-f5IcpOVv7JYFzUPkZIq5nbT3dS1f/KCSllh8Vkgl1P0=";
npmFlags = [ "--ignore-scripts" ];
makeCacheWritable = true;
installPhase = ''
runHook preInstall
cp -r dist $out
chmod u+w $out/assets
cp -r assets/. $out/assets/
runHook postInstall
'';
};
# RomM 5.4 requires packages absent from the Nixpkgs 5.2 environment.
pythonEnv = python3.withPackages (
ps: with ps; [
aiohttp
alembic
anyio
apprise
asyncssh
authlib
bcrypt
colorama
croniter
cryptography
defusedxml
email-validator
fastapi
gunicorn
httpx
httpx2
itsdangerous
jinja2
joserfc
mariadb
mutagen
mysql-connector-python
opentelemetry-distro
opentelemetry-exporter-otlp
opentelemetry-instrumentation-aiohttp-client
opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-httpx
opentelemetry-instrumentation-redis
opentelemetry-instrumentation-sqlalchemy
passlib
pillow
psycopg
pydantic
pydash
python-dotenv
python-magic
python-multipart
python-socketio
pyyaml
redis
rq
rq-scheduler
sentry-sdk
sqlalchemy
starlette
streaming-form-data
strsimpy
ua-parser
unidecode
uvicorn
uvicorn-worker
watchfiles
yarl
zipfile-inflate64
zstandard
]
);
in
{
version = "5.4.0";
src = fetchFromGitHub {
owner = "rommapp";
repo = "romm";
tag = finalAttrs.version;
hash = "sha256-oIYEdIAJgXkcqjZtWlWKD/NFiQ3gbo7gxwLhqZOCWpM=";
};
postPatch = previousAttrs.postPatch + ''
# Redis 8.0.1 removed this argument.
# RomM disables the related maintenance notifications.
substituteInPlace backend/handler/redis_handler.py \
--replace-fail 'from redis.maint_notifications import MaintNotificationsConfig' "" \
--replace-fail ' "maint_notifications_config": MaintNotificationsConfig(enabled=False),' ""
'';
installPhase =
lib.replaceStrings
[
"${previousAttrs.passthru.frontend}"
"${previousAttrs.passthru.pythonEnv}"
]
[
"${frontend}"
"${pythonEnv}"
]
previousAttrs.installPhase;
passthru = previousAttrs.passthru // {
inherit frontend pythonEnv;
};
}
)

View File

@@ -3,16 +3,20 @@ name: grill
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases. description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
--- ---
Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it. Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: each decision branches into the decisions it unblocks.
Maintain the design tree in a new Markdown file. Use the file as the source of truth. Maintain the design tree in a new Markdown file. Use the file as the source of truth.
Work the tree one question at a time. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask now without guessing at answers you have not heard yet. Record the next frontier question in the file, then ask it and wait for the user's answer. Build the complete **frontier** before the first question.
After each answer, recompute and display every frontier decision.
Ask one frontier decision per turn.
A clarification leaves that decision open.
Format a question like so: Format a question like so:
``` ```
❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices> ❓ **D1 — <decision title>**: <question body, might be multiple paragraphs, including multiple choices>
**Options** **Options**
@@ -22,7 +26,7 @@ Format a question like so:
--- ---
❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices> ❓ **D2 — <decision title>**: <question body, might be multiple paragraphs, including multiple choices>
**Options** **Options**
@@ -34,7 +38,10 @@ Format a question like so:
List every viable answer in an ordered list and mark your recommended answer. List every viable answer in an ordered list and mark your recommended answer.
Word each question so "yes" accepts your recommended answer. Word each question so "yes" accepts your recommended answer.
Each user answer reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier in the file and ask the next question. A question whose answer depends on another open question belongs later, not next. Before asking the next question, account for every decision that the response settles.
Record preferences as constraints.
Settle an implementation decision only after facts establish that it is viable.
A decision with unresolved prerequisites belongs outside the frontier.
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), find it with available tools; don't ask the user for anything you could look up yourself. The _decisions_ are the user's: put each to them and wait. Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), find it with available tools; don't ask the user for anything you could look up yourself. The _decisions_ are the user's: put each to them and wait.