Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5bc1d5303 | ||
|
|
d342370b89 | ||
|
|
c370d635e5 | ||
|
|
8756bc087f | ||
|
|
bb22a90053 | ||
|
|
5cf7109eaf | ||
|
|
4baafb7094 | ||
|
|
0166547091 |
No files matched your search
Generated
+4
@@ -164,6 +164,10 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scaffolding"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ members = [
|
|||||||
"src/machoke",
|
"src/machoke",
|
||||||
"src/monorepo",
|
"src/monorepo",
|
||||||
"src/status",
|
"src/status",
|
||||||
|
"tools/scaffolding",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ Confirm you have connectivity (`ping -c1 github.com`) before continuing.
|
|||||||
Clone the monorepo onto the live ISO and work from that local checkout:
|
Clone the monorepo onto the live ISO and work from that local checkout:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ git clone ssh://gitea@git.alexion.dev:2022/alexion/dev.git
|
$ git clone ssh://gitea@git.alexion.dev/alexion/dev.git
|
||||||
$ cd dev
|
$ cd dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
registry,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.gitea;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.guests.gitea.dataPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/gitea";
|
||||||
|
description = "Host path mounted as Gitea's state directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.dataPath != null;
|
||||||
|
message = "guests.gitea.dataPath must point at the host directory that stores Gitea state.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
registry.homepage.${config.networking.hostName}.gitea = {
|
||||||
|
tab = "Apps";
|
||||||
|
section = "Other";
|
||||||
|
name = "Gitea";
|
||||||
|
description = "Git forge";
|
||||||
|
icon = "gitea.png";
|
||||||
|
};
|
||||||
|
|
||||||
|
guests.gitea = {
|
||||||
|
interior.modules.services.gitea = {
|
||||||
|
enable = true;
|
||||||
|
reverseProxyAddress = registry.address.${config.networking.hostName}."reverse-proxy";
|
||||||
|
};
|
||||||
|
|
||||||
|
mounts."/var/lib/gitea" = {
|
||||||
|
directory = cfg.dataPath;
|
||||||
|
owner = config.registry."stable-uid".${config.networking.hostName}.gitea;
|
||||||
|
group = "storage";
|
||||||
|
mode = "2770";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
registry,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
giteaSshBackend = "10.23.20.45:2022";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
config = lib.mkIf config.guests.reverse-proxy.enable {
|
config = lib.mkIf config.guests.reverse-proxy.enable {
|
||||||
guests.reverse-proxy = {
|
guests.reverse-proxy = {
|
||||||
@@ -13,11 +11,6 @@ in
|
|||||||
modules.services.reverse-proxy.enable = true;
|
modules.services.reverse-proxy.enable = true;
|
||||||
|
|
||||||
services.nginx.virtualHosts = {
|
services.nginx.virtualHosts = {
|
||||||
"git.alexion.dev" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/".proxyPass = "http://10.23.20.32";
|
|
||||||
};
|
|
||||||
"photos.alexion.dev" = {
|
"photos.alexion.dev" = {
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
@@ -45,12 +38,12 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 2022 ];
|
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||||
|
|
||||||
services.nginx.streamConfig = ''
|
services.nginx.streamConfig = ''
|
||||||
server {
|
server {
|
||||||
listen 2022;
|
listen 22;
|
||||||
proxy_pass ${giteaSshBackend};
|
proxy_pass ${registry.address.${config.networking.hostName}.gitea}:2222;
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ in
|
|||||||
"services/romm/igdb/client-id"
|
"services/romm/igdb/client-id"
|
||||||
"services/romm/igdb/client-secret"
|
"services/romm/igdb/client-secret"
|
||||||
"services/romm/retroachievements/api-key"
|
"services/romm/retroachievements/api-key"
|
||||||
|
"services/romm/screenscraper/dev/id"
|
||||||
|
"services/romm/screenscraper/dev/password"
|
||||||
"services/romm/screenscraper/user"
|
"services/romm/screenscraper/user"
|
||||||
"services/romm/screenscraper/password"
|
"services/romm/screenscraper/password"
|
||||||
"services/romm/steamgriddb/api-key"
|
"services/romm/steamgriddb/api-key"
|
||||||
@@ -179,6 +181,8 @@ in
|
|||||||
content = ''
|
content = ''
|
||||||
IGDB_CLIENT_ID=${config.sops.placeholder."services/romm/igdb/client-id"}
|
IGDB_CLIENT_ID=${config.sops.placeholder."services/romm/igdb/client-id"}
|
||||||
IGDB_CLIENT_SECRET=${config.sops.placeholder."services/romm/igdb/client-secret"}
|
IGDB_CLIENT_SECRET=${config.sops.placeholder."services/romm/igdb/client-secret"}
|
||||||
|
SCREENSCRAPER_DEV_ID=${config.sops.placeholder."services/romm/screenscraper/dev/id"}
|
||||||
|
SCREENSCRAPER_DEV_PASSWORD=${config.sops.placeholder."services/romm/screenscraper/dev/password"}
|
||||||
SCREENSCRAPER_USER=${config.sops.placeholder."services/romm/screenscraper/user"}
|
SCREENSCRAPER_USER=${config.sops.placeholder."services/romm/screenscraper/user"}
|
||||||
SCREENSCRAPER_PASSWORD=${config.sops.placeholder."services/romm/screenscraper/password"}
|
SCREENSCRAPER_PASSWORD=${config.sops.placeholder."services/romm/screenscraper/password"}
|
||||||
RETROACHIEVEMENTS_API_KEY=${config.sops.placeholder."services/romm/retroachievements/api-key"}
|
RETROACHIEVEMENTS_API_KEY=${config.sops.placeholder."services/romm/retroachievements/api-key"}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
modules.desktop.enable = true;
|
modules.desktop.enable = true;
|
||||||
modules.desktop.apps.firefox.uiScale = "1.25";
|
modules.desktop.apps.firefox.uiScale = "1.25";
|
||||||
modules.desktop.apps.obsidian.enable = true;
|
modules.desktop.apps.obsidian.enable = true;
|
||||||
modules.desktop.apps.steam.enable = true;
|
|
||||||
|
|
||||||
registry.waybar.neogaia.battery = {
|
registry.waybar.neogaia.battery = {
|
||||||
order = 70;
|
order = 70;
|
||||||
|
|||||||
@@ -96,6 +96,21 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
guests.gitea = {
|
||||||
|
enable = true;
|
||||||
|
vlan = 20;
|
||||||
|
address = "10.23.20.206";
|
||||||
|
dataPath = "/pikachu/data/gitea";
|
||||||
|
reverseProxy = {
|
||||||
|
enable = true;
|
||||||
|
route = {
|
||||||
|
domain = "git.alexion.dev";
|
||||||
|
protocol = "https";
|
||||||
|
port = 3000;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
guests.romm = {
|
guests.romm = {
|
||||||
enable = true;
|
enable = true;
|
||||||
vlan = 20;
|
vlan = 20;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
modules.desktop.emulation.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.discord.enable = true;
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
|
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
util,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# Official Discord provides chat, voice, and screen sharing.
|
||||||
|
let
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.desktop.apps.discord.enable = lib.mkEnableOption "official Discord";
|
||||||
|
|
||||||
|
config = lib.mkIf config.modules.desktop.apps.discord.enable {
|
||||||
|
home-manager.users.${user}.home.packages = [ pkgs.discord ];
|
||||||
|
|
||||||
|
registry.window-icon.${config.networking.hostName}.discord = {
|
||||||
|
class = "[Dd]iscord";
|
||||||
|
icon = util.glyph "f1ff";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
programs.steam = {
|
programs.steam = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
remotePlay.openFirewall = true;
|
||||||
package = pkgs.steam.override {
|
package = pkgs.steam.override {
|
||||||
extraEnv.STEAM_FORCE_DESKTOPUI_SCALING = "1.5";
|
extraEnv.STEAM_FORCE_DESKTOPUI_SCALING = "1.5";
|
||||||
};
|
};
|
||||||
@@ -21,9 +22,10 @@
|
|||||||
|
|
||||||
hardware.steam-hardware.enable = true;
|
hardware.steam-hardware.enable = true;
|
||||||
|
|
||||||
environment.systemPackages = [
|
programs.gamescope = {
|
||||||
pkgs.gamescope
|
enable = true;
|
||||||
];
|
capSysNice = true;
|
||||||
|
};
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.steam = {
|
registry.window-icon.${config.networking.hostName}.steam = {
|
||||||
class = "[Ss]team";
|
class = "[Ss]team";
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Vesktop, a Discord client with Vencord built in.
|
|
||||||
let
|
|
||||||
user = config.user.name;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.desktop.apps.vencord.enable =
|
|
||||||
lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.vencord.enable {
|
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
|
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.vesktop = {
|
|
||||||
class = "[Vv]esktop";
|
|
||||||
icon = util.glyph "f1ff";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
File renamed without changes.
@@ -22,7 +22,38 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
sops.secrets =
|
||||||
|
lib.genAttrs
|
||||||
|
[
|
||||||
|
"retroarch/romm/user"
|
||||||
|
"retroarch/romm/password"
|
||||||
|
"retroarch/retroachievements/user"
|
||||||
|
"retroarch/retroachievements/password"
|
||||||
|
]
|
||||||
|
(_: {
|
||||||
|
sopsFile = ../../../secrets + "/${config.networking.hostName}.yaml";
|
||||||
|
});
|
||||||
|
|
||||||
|
sops.templates.retroarch-credentials = {
|
||||||
|
owner = user;
|
||||||
|
mode = "0400";
|
||||||
|
content = ''
|
||||||
|
webdav_username = "${config.sops.placeholder."retroarch/romm/user"}"
|
||||||
|
webdav_password = "${config.sops.placeholder."retroarch/romm/password"}"
|
||||||
|
cheevos_username = "${config.sops.placeholder."retroarch/retroachievements/user"}"
|
||||||
|
cheevos_password = "${config.sops.placeholder."retroarch/retroachievements/password"}"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
|
home.file.".config/retroarch/retroarch.cfg" = {
|
||||||
|
force = true;
|
||||||
|
text = ''
|
||||||
|
config_save_on_exit = "false"
|
||||||
|
#include "${config.sops.templates.retroarch-credentials.path}"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
home.packages = [
|
home.packages = [
|
||||||
(pkgs.wrapRetroArch {
|
(pkgs.wrapRetroArch {
|
||||||
cores = with pkgs.libretro; [
|
cores = with pkgs.libretro; [
|
||||||
@@ -45,13 +76,29 @@ in
|
|||||||
cloud_sync_sync_system = "false";
|
cloud_sync_sync_system = "false";
|
||||||
cloud_sync_sync_mode = "0";
|
cloud_sync_sync_mode = "0";
|
||||||
cloud_sync_destructive = "false";
|
cloud_sync_destructive = "false";
|
||||||
|
cheevos_enable = "true";
|
||||||
|
cheevos_hardcore_mode_enable = "false";
|
||||||
|
menu_driver = "xmb";
|
||||||
|
menu_shader_pipeline = "1";
|
||||||
|
xmb_menu_color_theme = "19";
|
||||||
|
content_show_add_entry = "0";
|
||||||
|
content_show_contentless_cores = "0";
|
||||||
|
content_show_explore = "false";
|
||||||
|
content_show_favorites = "false";
|
||||||
|
content_show_history = "false";
|
||||||
|
content_show_images = "false";
|
||||||
|
content_show_playlists = "false";
|
||||||
|
content_show_video = "false";
|
||||||
|
menu_icon_thumbnails = "3";
|
||||||
|
menu_thumbnails = "0";
|
||||||
|
network_on_demand_thumbnails = "true";
|
||||||
|
netplay_nickname = user;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (cfg.browserDirectory != "") {
|
// lib.optionalAttrs (cfg.browserDirectory != "") {
|
||||||
rgui_browser_directory = cfg.browserDirectory;
|
rgui_browser_directory = cfg.browserDirectory;
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.retroarch = {
|
registry.window-icon.${config.networking.hostName}.retroarch = {
|
||||||
|
|||||||
@@ -33,16 +33,23 @@ let
|
|||||||
});
|
});
|
||||||
|
|
||||||
extraTargets = {
|
extraTargets = {
|
||||||
# TODO: Pin Gitea's host key after migrating it from Proxmox.
|
|
||||||
gitea = {
|
gitea = {
|
||||||
hostName = "git.alexion.dev";
|
hostName = "git.alexion.dev";
|
||||||
port = 2022;
|
|
||||||
user = "gitea";
|
user = "gitea";
|
||||||
|
hostKeys = [
|
||||||
|
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFcUl17AcZz1wS8ek2yS/bu5NRTX4ViYTuFOSnp8leHmirP3Zk5QlC33hodQp09J6EA6Vl8zUowy4qLud7BMurQ="
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICkK2W2Ev+9RXa2PNXiNPPCPF5ln2+M2jHiGoZ4cPYxX"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDZSeU1iRHoaIXgbYe0kAiOAASw9LrQhu54RNCoTQ2O1rbqgnnKO2qbYJvfmUWL7jqEkEotgUBy4Xqxgfl7rXGQ9viWZe+i2tKwJdGsabNLJ+QTDCNyGCd22s1Hhl7n68JvuLTENXl4WpH56MB3yjFmK9onLHHcLG+rWiR0I6Yn264LfNVX0RuZK7TlG0tUKJR5bQBXGT6PxAE1IceYRgbgM0BYq4nakNg/s4ZSpmJFrrGfCHNsnHiEmFAXRFrGwf+0ef2STDukM/ADLziPXu0Ge7k72/UPvtEdcpgvzIDMJ2EznKTnxIoZnFkrDqhGOo8DnXK+cFVKAkkC6NgWQpO6fNnY+MEDgSb0fr/rGcwzUa1p+fON5/zEYxsAtKyAeP4D7OEAo65lsSKqi1tbt521miTobp0NOT8p4tGgHTwmshbqlmUZz0DVexN6uc7yzoHu4V/oYomEWP/j/E2uuvLi57AaJbM3Z/oAHUtrHFK9A9FFggyzpETZE7qWA9BhO/pJQUsjdWRm0pUHsPlf9Uqqit2BKVERiMSlHsBKch4tIk4ryZBazWunDD+QFcllKG74nMVISZ7i8q9IfNy+PWQuJIEm5XrT9W5pnkBIWrePlcAcPo080ALigmyhH+WOPiyO+dfueeq6PVdu2Voc+6FvtylRcn4u8O+E5Wq5amvQAQ=="
|
||||||
|
];
|
||||||
aliases = [
|
aliases = [
|
||||||
"gitea"
|
"gitea"
|
||||||
"git.alexion.dev"
|
"git.alexion.dev"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
steammachine = {
|
||||||
|
hostName = "10.23.50.234";
|
||||||
|
user = "deck";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
mkTarget = name: target: {
|
mkTarget = name: target: {
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ in
|
|||||||
bind_hosts = [ "0.0.0.0" ];
|
bind_hosts = [ "0.0.0.0" ];
|
||||||
port = 53;
|
port = 53;
|
||||||
anonymize_client_ip = false;
|
anonymize_client_ip = false;
|
||||||
ratelimit = 20;
|
ratelimit = 0;
|
||||||
ratelimit_subnet_len_ipv4 = 24;
|
|
||||||
ratelimit_subnet_len_ipv6 = 56;
|
|
||||||
ratelimit_whitelist = [ ];
|
|
||||||
refuse_any = true;
|
refuse_any = true;
|
||||||
upstream_dns = [
|
upstream_dns = [
|
||||||
"https://dns.cloudflare.com/dns-query"
|
"https://dns.cloudflare.com/dns-query"
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.modules.services.gitea;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.services.gitea = {
|
||||||
|
enable = lib.mkEnableOption "Gitea";
|
||||||
|
|
||||||
|
reverseProxyAddress = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "IP address of the reverse proxy trusted to forward client headers.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkMerge [
|
||||||
|
{
|
||||||
|
registry."stable-uid".${config.networking.hostName}.gitea = 20007;
|
||||||
|
}
|
||||||
|
|
||||||
|
(lib.mkIf cfg.enable {
|
||||||
|
users.users.gitea = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = config.registry."stable-uid".${config.networking.hostName}.gitea;
|
||||||
|
group = "storage";
|
||||||
|
home = "/var/lib/gitea";
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
2222
|
||||||
|
3000
|
||||||
|
];
|
||||||
|
|
||||||
|
services.gitea = {
|
||||||
|
enable = true;
|
||||||
|
user = "gitea";
|
||||||
|
group = "storage";
|
||||||
|
stateDir = "/var/lib/gitea";
|
||||||
|
repositoryRoot = "/var/lib/gitea/repositories";
|
||||||
|
lfs = {
|
||||||
|
enable = true;
|
||||||
|
contentDir = "/var/lib/gitea/data/lfs";
|
||||||
|
};
|
||||||
|
database.type = "sqlite3";
|
||||||
|
settings = {
|
||||||
|
server = {
|
||||||
|
HTTP_ADDR = "0.0.0.0";
|
||||||
|
HTTP_PORT = 3000;
|
||||||
|
ROOT_URL = "https://git.alexion.dev/";
|
||||||
|
SSH_DOMAIN = "git.alexion.dev";
|
||||||
|
SSH_PORT = 22;
|
||||||
|
START_SSH_SERVER = true;
|
||||||
|
SSH_LISTEN_HOST = "0.0.0.0";
|
||||||
|
SSH_LISTEN_PORT = 2222;
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.reverseProxyAddress != null) {
|
||||||
|
REVERSE_PROXY_TRUSTED_PROXIES = "${cfg.reverseProxyAddress}/32";
|
||||||
|
};
|
||||||
|
service.DISABLE_REGISTRATION = true;
|
||||||
|
session.COOKIE_SECURE = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -56,6 +56,13 @@ in
|
|||||||
default = "roms/{platform}/{game}";
|
default = "roms/{platform}/{game}";
|
||||||
firmware = "bios/{platform}";
|
firmware = "bios/{platform}";
|
||||||
};
|
};
|
||||||
|
scan.media = [
|
||||||
|
"box2d"
|
||||||
|
"screenshot"
|
||||||
|
"title_screen"
|
||||||
|
"logo"
|
||||||
|
"manual"
|
||||||
|
];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ services:
|
|||||||
steamgriddb:
|
steamgriddb:
|
||||||
api-key: ENC[AES256_GCM,data:+kBRieIB/dHbvQLbH2gQTQh2j6DQhybVWEGkSCRZrZY=,iv:3h6dhfiV7HfaZhMv93x8+7F3utzYG6fIbTDScA14PVI=,tag:Wo24YnvsIxe7iMRLyxrj2w==,type:str]
|
api-key: ENC[AES256_GCM,data:+kBRieIB/dHbvQLbH2gQTQh2j6DQhybVWEGkSCRZrZY=,iv:3h6dhfiV7HfaZhMv93x8+7F3utzYG6fIbTDScA14PVI=,tag:Wo24YnvsIxe7iMRLyxrj2w==,type:str]
|
||||||
screenscraper:
|
screenscraper:
|
||||||
|
dev:
|
||||||
|
id: ENC[AES256_GCM,data:nVPM7ijeVQ==,iv:noknaNqxnTx3E8cKtwY9MVkg7Xeq0C1O+7EpdXvz90g=,tag:f/iHVW1Tu93J3VdvURC7Gg==,type:str]
|
||||||
|
password: ENC[AES256_GCM,data:6v+we9O7PXExTmFZ,iv:DUEj1pei0ryh4AzTRrB9YDVArLCMvyKPbNfvB2IiyzA=,tag:nPPqB/tbsRqv9W7zFNOsdg==,type:str]
|
||||||
user: ENC[AES256_GCM,data:7hGcFF4xqg==,iv:SFOdYej94Thfn+Te0kXsC9Kq568A1dsnaR+JB90MgBQ=,tag:inIWuFY7f5Tvv8EyV71dYA==,type:str]
|
user: ENC[AES256_GCM,data:7hGcFF4xqg==,iv:SFOdYej94Thfn+Te0kXsC9Kq568A1dsnaR+JB90MgBQ=,tag:inIWuFY7f5Tvv8EyV71dYA==,type:str]
|
||||||
password: ENC[AES256_GCM,data:eUWwDDQUSytm/6QZlG5XjbSi41nsBQbm4w==,iv:82S2a3rAVjaF0CXcjXUXErNHMY7MEkjpDL7EVX9cIHw=,tag:uGuVJCv8ThsaNnA0C0WR2Q==,type:str]
|
password: ENC[AES256_GCM,data:eUWwDDQUSytm/6QZlG5XjbSi41nsBQbm4w==,iv:82S2a3rAVjaF0CXcjXUXErNHMY7MEkjpDL7EVX9cIHw=,tag:uGuVJCv8ThsaNnA0C0WR2Q==,type:str]
|
||||||
sops:
|
sops:
|
||||||
@@ -37,7 +40,7 @@ sops:
|
|||||||
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
|
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
||||||
lastmodified: "2026-10-20T01:27:06Z"
|
lastmodified: "2026-10-22T16:11:38Z"
|
||||||
mac: ENC[AES256_GCM,data:NCt6e/ctZwaBADfrhu1RXtILlClbHEqOAnIu8rE/zMPSwMIPB/kTtfd/ujf+1iRszoHaX4Se1xc0wz1th6NVToFVNarxBBgTk4JMUU8gdMweOjmwlh3GXU3ZcYY/xAXxWF1hxyXSEW78j3S2844H9XXENAq/Bf3mjdCYQ/qr/d4=,iv:4nFyBSmRvF4zSAwvL+xccMwtWrVbHtFKNoTb7f3rSH4=,tag:eCV/7P0TjyMZXKcWBUPXQA==,type:str]
|
mac: ENC[AES256_GCM,data:pkDbhEFKC8fl4SzF3U7F0jqzil9CB803/vIV2mKIDO1MXC8GAOGPW3Q7qfHkCXFkVmXvBvowmJYdR3d/zMO3L4XJPRYR+NpUEpsHjRLq/uSJszWMeCjXQA8DIZxT2fWcp1FGcdpHecrs9QkWTy1OtSDGD+xr0WlHTbLX7bTrtfo=,iv:OfhVrGco4/gBE3V8U7CKDfBGvaCizcEq6x6p/y4X7NY=,tag:/+G5ojgzOlhXW2QM/irWOA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.3
|
version: 3.13.3
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
ssh-host-ed25519-key: ENC[AES256_GCM,data:NMlzM7E9bXUhNqIbtyERuuPShnlx00ftHyRnyMiLSSzp1R/80n8Hdvv1q3uNmZdSgmBYPKdM34fAHuXgwREBcWlqMXptUNnXMngtqN9vxYcfWxJfcN67Fsw2xG8v1h/L0vqwPSn24RAeB6HTExqvr/5egrfQX0yylWytZLMbSIUeAsY+GGD67TDp3W7RdMfXKDHgvRubRR2YdHu8BNQ+BXgzOWCqAbrnHAk0AUaeUloGYr72+zBAhXmWfrXBJjJoU1H7CZELNtcFYczC0wgKUoTTdx9MiPGN2SEx5hCkqOd6iT4MyuY6ZM0SuXMwl7Lw/U12wOzhqZy/1orzQcyRNzcDYcBepAknth/08631fEX4PJdjzZMnI1VTdGSHs/Adl3PQoXv/WGY+QWc6Pb2I+PyAQTCkX9eCr/Mf+VFnOzu0mDBSK/8i1dGlf1lBul4o9L+x/Gz0Fj5kaA1SJdbqCcgKPtEDvLjvCvwYh215BNXv2UCyIrcYf1Y712/sAAcrHmnViocBbTo/m3FkbdGX,iv:jwHCjzmPWv1IXpTonmohIz7LftotOTG0utynf1PKobQ=,tag:1RjXmbJAPvuSUdtCuNXTlg==,type:str]
|
ssh-host-ed25519-key: ENC[AES256_GCM,data:NMlzM7E9bXUhNqIbtyERuuPShnlx00ftHyRnyMiLSSzp1R/80n8Hdvv1q3uNmZdSgmBYPKdM34fAHuXgwREBcWlqMXptUNnXMngtqN9vxYcfWxJfcN67Fsw2xG8v1h/L0vqwPSn24RAeB6HTExqvr/5egrfQX0yylWytZLMbSIUeAsY+GGD67TDp3W7RdMfXKDHgvRubRR2YdHu8BNQ+BXgzOWCqAbrnHAk0AUaeUloGYr72+zBAhXmWfrXBJjJoU1H7CZELNtcFYczC0wgKUoTTdx9MiPGN2SEx5hCkqOd6iT4MyuY6ZM0SuXMwl7Lw/U12wOzhqZy/1orzQcyRNzcDYcBepAknth/08631fEX4PJdjzZMnI1VTdGSHs/Adl3PQoXv/WGY+QWc6Pb2I+PyAQTCkX9eCr/Mf+VFnOzu0mDBSK/8i1dGlf1lBul4o9L+x/Gz0Fj5kaA1SJdbqCcgKPtEDvLjvCvwYh215BNXv2UCyIrcYf1Y712/sAAcrHmnViocBbTo/m3FkbdGX,iv:jwHCjzmPWv1IXpTonmohIz7LftotOTG0utynf1PKobQ=,tag:1RjXmbJAPvuSUdtCuNXTlg==,type:str]
|
||||||
ssh-host-rsa-key: ENC[AES256_GCM,data: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,iv:i1s5hIhvWr+0Oood4Kftw+zH1sHoyKG7PnCME9zxATM=,tag:/83WXGYbnxRDjJZMB3rQtA==,type:str]
|
ssh-host-rsa-key: ENC[AES256_GCM,data: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,iv:i1s5hIhvWr+0Oood4Kftw+zH1sHoyKG7PnCME9zxATM=,tag:/83WXGYbnxRDjJZMB3rQtA==,type:str]
|
||||||
ssh-user-ed25519-key: ENC[AES256_GCM,data:xu3YkQnuaVC2YWGPrXfxbgY3PruxfwKMaeBSwyf6HiGOQFn3rlBpcfjtktBayVJkQzdhjK0sMqEBpvljrIOKRDdUC13jCXlvbT9D9P4AtGjzdLO672WZxZ/e+B2jtJGP0qfOo9SSPgsWijbNTy0SXkQoaO5zQe3HUWSH0CSNiNtY88YRodIDcJQVZrwVJndGsHnC5ELVZ60sdzh7rZQeKz+cvigMDpkORcs3QhUe9PFpsdl9emjSlWq+8zJVas6h2mp+ajgGMl8qrb+9mVJMDQIpEmW/HEQIHYLAuIPxyZ43iIeQZC5oPYy518tGZNJUC3KHceTQJgGWHPM707TU1/M7/W47i79TvjWccyQpIv9SQ7G9RJXhzMD15judb5cyQfwobqJ0w4VjTxao3hESZUho8ZMFAyNtXf7IqUgKMmbJ+o4pZTS8Mqyp0ZfjddP7FxnHkPiORmxQ3v2fGqtGTlJ4ODZO/vYZTmwsampWJA2J9FkMihqggI74yyoS8006mkY9kNpeTzNUeF5flFS/,iv:op7zSdx7335l8n/Q3pCM/9enb00Hbs+m3F5NUynNUXw=,tag:F5AE+gB1H1iVXSQWaliWKg==,type:str]
|
ssh-user-ed25519-key: ENC[AES256_GCM,data:xu3YkQnuaVC2YWGPrXfxbgY3PruxfwKMaeBSwyf6HiGOQFn3rlBpcfjtktBayVJkQzdhjK0sMqEBpvljrIOKRDdUC13jCXlvbT9D9P4AtGjzdLO672WZxZ/e+B2jtJGP0qfOo9SSPgsWijbNTy0SXkQoaO5zQe3HUWSH0CSNiNtY88YRodIDcJQVZrwVJndGsHnC5ELVZ60sdzh7rZQeKz+cvigMDpkORcs3QhUe9PFpsdl9emjSlWq+8zJVas6h2mp+ajgGMl8qrb+9mVJMDQIpEmW/HEQIHYLAuIPxyZ43iIeQZC5oPYy518tGZNJUC3KHceTQJgGWHPM707TU1/M7/W47i79TvjWccyQpIv9SQ7G9RJXhzMD15judb5cyQfwobqJ0w4VjTxao3hESZUho8ZMFAyNtXf7IqUgKMmbJ+o4pZTS8Mqyp0ZfjddP7FxnHkPiORmxQ3v2fGqtGTlJ4ODZO/vYZTmwsampWJA2J9FkMihqggI74yyoS8006mkY9kNpeTzNUeF5flFS/,iv:op7zSdx7335l8n/Q3pCM/9enb00Hbs+m3F5NUynNUXw=,tag:F5AE+gB1H1iVXSQWaliWKg==,type:str]
|
||||||
|
retroarch:
|
||||||
|
romm:
|
||||||
|
user: ENC[AES256_GCM,data:Jc1/7lhAhw==,iv:1yFG+9LFbyOHT+xQ3+ZlIiiG4VY7h/FKlgKzgTLV2FA=,tag:6nyway08U8DxFISZQnpmTA==,type:str]
|
||||||
|
password: ENC[AES256_GCM,data:10KB8nD2HN2FQhWGMoCwxUjzwxA=,iv:xxge3Y7N3T7B7OBIBs7PtmuPXQfrda/bU4LxldGjzuE=,tag:J0Ik7KNZH5mAlHeidZjSYg==,type:str]
|
||||||
|
retroachievements:
|
||||||
|
user: ENC[AES256_GCM,data:G6+e2BW8,iv:BJD3nFLWyWX8pFFCKZENOSi6FxCy+BAXOmgdjrCCIak=,tag:yb3VNgBQEN5x7Guqlh8eyg==,type:str]
|
||||||
|
password: ENC[AES256_GCM,data:kOfDyJK+3WxGDFhu,iv:q/lWpCuuCeyC61QwYQ85oYrXxtiVGyF3UccOVkXt8+w=,tag:k2pwo7R6nmNLiTUp7f70nA==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
@@ -21,7 +28,7 @@ sops:
|
|||||||
ZQg0XgccFlZjIBjp5jg2rDq0qG/tFkvIuX9C3udSxyGX+1MRutyhpw==
|
ZQg0XgccFlZjIBjp5jg2rDq0qG/tFkvIuX9C3udSxyGX+1MRutyhpw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age199atsykyh0hp0yuxymznxs0mqv4ram9ttaddk6jrsjjc43rmvu7snh9dq8
|
recipient: age199atsykyh0hp0yuxymznxs0mqv4ram9ttaddk6jrsjjc43rmvu7snh9dq8
|
||||||
lastmodified: "2026-09-05T00:59:23Z"
|
lastmodified: "2026-10-22T16:53:08Z"
|
||||||
mac: ENC[AES256_GCM,data:zYZoZ3vxcIHTJu2rdEqhsmkCz0ylRo6xXTEQnfBbyeAOP0QD72czo0VfUPHVcZVnLhT3yfwMXjotM2ScwTkUinouOXdW0+jIgHd9U3Q949an0IJGNJVljd3/rN5SknEoTqbA/GRk/uTNUu4A1usWqrp1gSPHFxzALKg/CFJoIag=,iv:SXamTie6plAiFOlZ2rGkpAlRYPzAKfqht0V+stt23jE=,tag:w8xq4orRj9SbDaZ+Y46Epw==,type:str]
|
mac: ENC[AES256_GCM,data:bfXTV8SOfZ88nofmvrzU6CXa/apXHsMvLh0yz3oI9QWyvVmBzW8iv5oUmItQ/GBBM3t3WkFwqqo9sHmImT0GOOg4C8DUUwn/3DP1ehOA6xy/ZDBGNmVZuwqM0boJMRkJBeIYoDX5bChRo7Eg+hMPconLCoqmfZhaIldy6IMOFlY=,iv:4gzsrRKlbcqgkugRdXvtH7KKCUh03oYZyokTtGV8egQ=,tag:+mgqrd+j9pvyFgTwZj5CEw==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.3
|
version: 3.13.3
|
||||||
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"livePreview": false,
|
"livePreview": false,
|
||||||
"readableLineLength": false,
|
"readableLineLength": false,
|
||||||
"strictLineBreaks": true,
|
"strictLineBreaks": true,
|
||||||
"propertiesInDocument": "visible",
|
"propertiesInDocument": "hidden",
|
||||||
"showLineNumber": true,
|
"showLineNumber": true,
|
||||||
"autoPairMarkdown": false,
|
"autoPairMarkdown": false,
|
||||||
"useTab": false,
|
"useTab": false,
|
||||||
|
|||||||
Vendored
+24
-24
@@ -4,21 +4,21 @@
|
|||||||
"type": "split",
|
"type": "split",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "81c4860f495113bf",
|
"id": "880908ac22302761",
|
||||||
"type": "tabs",
|
"type": "tabs",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "daab69fc3faab88f",
|
"id": "10ea163afc7dbc62",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "00 INBOX/credits.md",
|
"file": "TODO.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": true
|
"source": true
|
||||||
},
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "credits"
|
"title": "TODO"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "localgraph",
|
"type": "localgraph",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "00 INBOX/credits.md",
|
"file": "TODO.md",
|
||||||
"options": {
|
"options": {
|
||||||
"collapse-filter": true,
|
"collapse-filter": true,
|
||||||
"search": "",
|
"search": "",
|
||||||
@@ -217,34 +217,34 @@
|
|||||||
"table-editor-obsidian:Advanced Tables Toolbar": false
|
"table-editor-obsidian:Advanced Tables Toolbar": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "daab69fc3faab88f",
|
"active": "10ea163afc7dbc62",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"00 INBOX/ai/tldr-design.md",
|
"00 INBOX/ai/adguard-pikachu-migration-design-tree.md",
|
||||||
"00 INBOX/ai/caps-escape-design.md",
|
"00 INBOX/ai/caps-escape-design.md",
|
||||||
|
"03 wrk/tasks/0016-romm-manual-rom-migration.md",
|
||||||
|
"03 wrk/tasks/0015-romm-5-4-package.md",
|
||||||
|
"00 INBOX/ai/retroarch-zeus-design-tree.md",
|
||||||
|
"TODO.md",
|
||||||
|
"00 INBOX/ai/romm-5-4-deployment-changes-design-tree.md",
|
||||||
|
"00 INBOX/ai/romm-migration-design-tree.md",
|
||||||
|
"00 INBOX/ai/romm-5-4-package-design-tree.md",
|
||||||
|
"00 INBOX/ai/steamvr-quest-3-design-tree.md",
|
||||||
"AGENTS.md",
|
"AGENTS.md",
|
||||||
|
"03 wrk/homelab.md",
|
||||||
|
"00 INBOX/ai/nixos-agents-style-scope-design-tree.md",
|
||||||
|
"03 wrk/tasks/0014-romm-guest.md",
|
||||||
"00 INBOX/ai/tldr-design-tree.md",
|
"00 INBOX/ai/tldr-design-tree.md",
|
||||||
|
"00 INBOX/ai/tldr-design.md",
|
||||||
|
"00 INBOX/ai/valheim-items.md",
|
||||||
|
"03 wrk/tasks/0013-oidc-provider.md",
|
||||||
|
"03 wrk/tasks/0002-nixos-refactor.md",
|
||||||
|
"03 wrk/tasks/0011-gitea-axi-rust-migration.md",
|
||||||
|
"00 INBOX/credits.md",
|
||||||
"00 INBOX/ai/grill-skill-design.md",
|
"00 INBOX/ai/grill-skill-design.md",
|
||||||
"03 wrk/tasks/0012-gitea-axi-pr-merge-guardrail.md",
|
"03 wrk/tasks/0012-gitea-axi-pr-merge-guardrail.md",
|
||||||
"03 wrk/tasks/0011-gitea-axi-rust-migration.md",
|
|
||||||
"00 INBOX/ai/gitea-axi-pr-merge-guardrail.md",
|
"00 INBOX/ai/gitea-axi-pr-merge-guardrail.md",
|
||||||
"00 INBOX/ai/gitea-axi-rust-migration.md",
|
"00 INBOX/ai/gitea-axi-rust-migration.md",
|
||||||
"00 INBOX/valheim progression.md",
|
"00 INBOX/valheim progression.md",
|
||||||
"03 wrk/tasks/0010-fix-main-monitor-detection.md",
|
|
||||||
"00 INBOX/credits.md",
|
|
||||||
"03 wrk/tasks/0009-bazel-project-scaffolding.md",
|
|
||||||
"03 wrk/tasks/0005-rust-cli-scaffolding.md",
|
|
||||||
"03 wrk/tasks/0008-vim-obsidian-markdown-renderer.md",
|
|
||||||
"03 wrk/tasks/0002-nixos-refactor.md",
|
|
||||||
"TODO.md",
|
|
||||||
"technical-writing.md",
|
|
||||||
"dictionary.md",
|
|
||||||
"03 wrk/tasks/0007-obsidian-source-notes.md",
|
|
||||||
"03 wrk/tasks/0004-dictionary-cli.md",
|
|
||||||
"03 wrk/tasks/0006-wallpaper-randomizer.md",
|
|
||||||
"03 wrk/tasks/0003-whisper-dictation.md",
|
|
||||||
"03 wrk/monorepo.md",
|
|
||||||
"03 wrk/tasks/0001-todo-cli.md",
|
|
||||||
"03 wrk/productivity.md",
|
|
||||||
"03 wrk/tasks",
|
"03 wrk/tasks",
|
||||||
"00 INBOX/let order name resolution.md~",
|
"00 INBOX/let order name resolution.md~",
|
||||||
"00 INBOX/nix name resolution.md~",
|
"00 INBOX/nix name resolution.md~",
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
# Caps Escape Design
|
|
||||||
|
|
||||||
## Facts
|
|
||||||
|
|
||||||
- Commit `250b0b71f` removed `services.xserver.xkb.options` from the shared host module.
|
|
||||||
- The removed option was `caps:escape_shifted_capslock`.
|
|
||||||
- That XKB option maps Caps Lock to Escape.
|
|
||||||
- Shift+Caps Lock still enables Caps Lock.
|
|
||||||
- The shared host module configures Neogaia, Zeus, and Pikachu.
|
|
||||||
- Zeus has QMK firmware that provides equivalent keyboard behavior.
|
|
||||||
- Neogaia and Zeus enable `modules.host.cachyos` and `modules.desktop`.
|
|
||||||
- Pikachu enables neither module.
|
|
||||||
- `configs/nixos/hosts/neogaia/default.nix` directly sets Neogaia-specific hardware, networking, desktop application, and Waybar settings.
|
|
||||||
- The current configuration has no Caps Lock or Escape remapping.
|
|
||||||
- This session runs on Zeus, not Neogaia.
|
|
||||||
|
|
||||||
## Design Tree
|
|
||||||
|
|
||||||
- Restore Caps Lock behavior
|
|
||||||
- Scope the configuration
|
|
||||||
- Configure Neogaia only. ✓
|
|
||||||
- Select the mapping behavior.
|
|
||||||
- Restore `caps:escape_shifted_capslock`. ✓
|
|
||||||
- Set `services.xserver.xkb.options` in `configs/nixos/hosts/neogaia/default.nix`. ✓
|
|
||||||
- Define validation for Neogaia.
|
|
||||||
- Run static checks and manually verify both keys after deployment. ✓
|
|
||||||
- Stop after source and static validation. ✓
|
|
||||||
|
|
||||||
## Settled Decisions
|
|
||||||
|
|
||||||
1. Configure the mapping on Neogaia only.
|
|
||||||
2. Restore `caps:escape_shifted_capslock`.
|
|
||||||
3. Set `services.xserver.xkb.options` directly in Neogaia's host configuration.
|
|
||||||
4. Run static checks and manually verify both keys on Neogaia after deployment.
|
|
||||||
5. Stop after source and static validation.
|
|
||||||
|
|
||||||
## Frontier
|
|
||||||
|
|
||||||
None.
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
date-created: 2026-10-21 08:47
|
||||||
|
date-modified: 2026-10-21 09:26
|
||||||
|
---
|
||||||
|
# RetroArch on Zeus design tree
|
||||||
|
|
||||||
|
## Facts
|
||||||
|
|
||||||
|
- Zeus is an `x86_64-linux` desktop with AMD graphics, 32-bit graphics support, Bluetooth, Hyprland, and Home Manager.
|
||||||
|
- Nixpkgs provides `pkgs.retroarch.withCores` for an explicit set of Libretro cores.
|
||||||
|
- Nixpkgs provides `pkgs.retroarch-free` and `pkgs.retroarch-full` for broad core sets.
|
||||||
|
- The packaged RetroArch version is 1.22.2 and includes Cloud Sync on Linux.
|
||||||
|
- Linux Cloud Sync uses a WebDAV endpoint.
|
||||||
|
- RomM requires RetroArch 1.22 or newer for this integration.
|
||||||
|
- RomM documents configuration synchronization as part of its Cloud Sync setup.
|
||||||
|
- Cloud Sync supports saves, configuration, thumbnails, and system files as separate categories.
|
||||||
|
- RomM matches a save or state to a ROM by its file name without the extension.
|
||||||
|
- Local ROM files must use the same names as their RomM library records.
|
||||||
|
- RomM 5.4 provides a WebDAV endpoint for RetroArch Cloud Sync.
|
||||||
|
- The endpoint is `https://games.alexion.dev/api/sync/retroarch/`.
|
||||||
|
- RomM stores its ROMs and BIOS files on Pikachu.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Install RetroArch on Zeus.
|
||||||
|
- Start with PlayStation and Sega Saturn cores.
|
||||||
|
- Evaluate RetroArch Cloud Sync.
|
||||||
|
- Keep package selection declarative.
|
||||||
|
- Keep credentials out of the Nix store and version control.
|
||||||
|
|
||||||
|
## Decision tree
|
||||||
|
|
||||||
|
- D1 — Initial core selection [settled]
|
||||||
|
- Use `beetle-psx-hw` for PlayStation.
|
||||||
|
- Use `beetle-saturn` for Sega Saturn.
|
||||||
|
- D4 — Core package viability [settled]
|
||||||
|
- Both core packages support `x86_64-linux`.
|
||||||
|
- Neither package is marked broken or unfree.
|
||||||
|
- D5 — Package composition [settled]
|
||||||
|
- Build a RetroArch wrapper with `beetle-psx-hw` and `beetle-saturn` only.
|
||||||
|
- D2 — Cloud Sync data scope [settled]
|
||||||
|
- Sync saves, states, and configuration.
|
||||||
|
- Do not sync thumbnails or system files.
|
||||||
|
- D6 — Sync endpoint [settled]
|
||||||
|
- Use RomM's RetroArch WebDAV endpoint.
|
||||||
|
- D7 — Sync identity [settled]
|
||||||
|
- Use the existing RomM account.
|
||||||
|
- D15 — Credential injection [settled]
|
||||||
|
- Store the RomM password in RetroArch's writable user configuration.
|
||||||
|
- D8 — Synchronization mode [settled]
|
||||||
|
- Synchronize automatically at startup and after play sessions.
|
||||||
|
- D16 — Launch order [settled]
|
||||||
|
- Do not add a game-launching frontend.
|
||||||
|
- Start RetroArch before loading local ROM content.
|
||||||
|
- D9 — Conflict and destructive-sync policy [settled]
|
||||||
|
- Use non-destructive synchronization.
|
||||||
|
- RetroArch backs up a local file when RomM reports its deletion.
|
||||||
|
- D3 — ROM and firmware access [settled]
|
||||||
|
- Keep ROMs and BIOS files locally on Zeus.
|
||||||
|
- D10 — Mount or replication method [settled]
|
||||||
|
- Do not configure a remote mount or replication method.
|
||||||
|
- D11 — RetroArch directory settings [settled]
|
||||||
|
- Use `~/.local/share/retroarch/`.
|
||||||
|
- Use `roms/`, `system/`, `saves/`, and `states/` subdirectories.
|
||||||
|
- Keep saves and states outside the content directory.
|
||||||
|
- Sort saves and states into per-core folders.
|
||||||
|
- D12 — User-configuration policy [settled]
|
||||||
|
- Nix declares the package, cores, and directory defaults.
|
||||||
|
- RetroArch manages and synchronizes its writable configuration.
|
||||||
|
- D13 — User-facing integration [settled]
|
||||||
|
- Do not add a user-facing integration beyond RetroArch's desktop entry.
|
||||||
|
- D14 — Validation [settled]
|
||||||
|
- Verify the NixOS evaluation and a manual Cloud Sync round trip.
|
||||||
|
|
||||||
|
## Frontier
|
||||||
|
|
||||||
|
No decisions remain open.
|
||||||
|
|
||||||
|
## Decision log
|
||||||
|
|
||||||
|
- D13: Do not add a user-facing integration beyond RetroArch's desktop entry.
|
||||||
|
- D16: Do not add a game-launching frontend. Start RetroArch before loading local ROM content.
|
||||||
|
- D14: Verify the NixOS evaluation and a manual Cloud Sync round trip.
|
||||||
|
- D12: Nix declares the package, cores, and directory defaults. RetroArch manages and synchronizes its writable configuration.
|
||||||
|
- D11: Use `~/.local/share/retroarch/` with `roms/`, `system/`, `saves/`, and `states/` subdirectories.
|
||||||
|
- D9: Use non-destructive synchronization.
|
||||||
|
- D8: Synchronize automatically at startup and after play sessions.
|
||||||
|
- D15: Store the RomM password in RetroArch's writable user configuration.
|
||||||
|
- D5: Build a RetroArch wrapper with `beetle-psx-hw` and `beetle-saturn` only.
|
||||||
|
- D3: Keep ROMs and BIOS files locally on Zeus.
|
||||||
|
- D10: Do not configure a remote mount or replication method.
|
||||||
|
- D7: Use the existing RomM account.
|
||||||
|
- D7 note: RomM accepts API client tokens as Bearer authentication. RetroArch Cloud Sync only supports WebDAV Basic authentication.
|
||||||
|
- D6: Use RomM's RetroArch WebDAV endpoint.
|
||||||
|
- D2: Sync saves, states, and configuration. Do not sync thumbnails or system files.
|
||||||
|
- D11: Keep saves and states outside the content directory. Sort them into per-core folders.
|
||||||
|
- D1: Use `beetle-psx-hw` for PlayStation and `beetle-saturn` for Sega Saturn.
|
||||||
|
- D4: Both selected core packages support Zeus and have no broken or unfree package status.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
date-created: 2026-10-20 19:15
|
||||||
|
date-modified: 2026-10-20 19:20
|
||||||
|
---
|
||||||
|
# RomM 5.4 deployment changes
|
||||||
|
|
||||||
|
## Known facts
|
||||||
|
|
||||||
|
- The deployed Nixpkgs package is RomM 5.2.0.
|
||||||
|
- RomM 5.4.0 requires Python 3.14 or later.
|
||||||
|
- The pinned Nixpkgs contains Python 3.14.
|
||||||
|
- The pinned Python package set lacks some RomM 5.4.0 dependency versions.
|
||||||
|
- For example, it provides RQ 2.10 while RomM requires RQ 2.12 or later.
|
||||||
|
- In this design, upstream means the Nixpkgs repository.
|
||||||
|
- Nixpkgs upstream still packages RomM 5.2.0.
|
||||||
|
- Two open Nixpkgs pull requests update RomM to 5.4.0.
|
||||||
|
- Pull request 574089 adds a required compatibility patch for Nixpkgs Redis 8.0.1.
|
||||||
|
- Pull request 574089 has not run the NixOS module test.
|
||||||
|
- The existing `romm` derivation already uses Python 3.14 from the pin.
|
||||||
|
- The package can override the current derivation without copying it.
|
||||||
|
- The override evaluates successfully with the RomM 5.4.0 source and frontend hashes.
|
||||||
|
- RomM reads `config.yml` from `/var/lib/romm/config/config.yml`.
|
||||||
|
- Systemd tmpfiles `L+` replaces that path with a generated Nix-store symlink.
|
||||||
|
- NixOS modules use `L+` symlinks for declaratively managed files in persistent application directories.
|
||||||
|
- The existing Nixpkgs RomM NixOS test starts the application with PostgreSQL, Redis, nginx, and the filesystem watcher in a local VM.
|
||||||
|
- The test does not deploy to Pikachu.
|
||||||
|
- The automated update changes only the version, source hash, and frontend dependency hash.
|
||||||
|
- The manual update also removes a Redis maintenance-notification import and argument.
|
||||||
|
- RomM does not require the `rom-converto` executable while `ROM_CONVERTO_ENABLED` is false.
|
||||||
|
- As checked on 2026-10-20, both pull requests remain open.
|
||||||
|
- Neither pull request has a review or a discussion comment.
|
||||||
|
- Both pull requests request review from the RomM package maintainers.
|
||||||
|
- Both pull requests pass evaluation and lint checks.
|
||||||
|
- Their package-test checks have a neutral result.
|
||||||
|
- The Darwin package-test jobs remain queued.
|
||||||
|
- RomM 5.3.0 added a required `filesystem.structure` declaration.
|
||||||
|
- RomM will not start without that declaration.
|
||||||
|
- The existing mounts match Structure A.
|
||||||
|
- Structure A maps ROMs to `roms/{platform}/{game}`.
|
||||||
|
- Structure A maps firmware to `bios/{platform}`.
|
||||||
|
- RomM 5.4.0 enables `emulatorjs.auto_save_sync` by default.
|
||||||
|
- RomM 5.4.0 adds optional in-place library conversion through `rom-converto`.
|
||||||
|
- Library conversion requires `ROM_CONVERTO_ENABLED=true`.
|
||||||
|
- Conversion deletes original files after conversion.
|
||||||
|
- The task requires declarative `config.yml` management.
|
||||||
|
- The task requires disabled browser save synchronization.
|
||||||
|
- The task requires disabled library conversion.
|
||||||
|
- The task retains the IGDB, ScreenScraper, and SteamGridDB configuration.
|
||||||
|
- The task reuses the Nixpkgs `services.romm` module.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [RomM 5.3.0 release](https://github.com/rommapp/romm/releases/tag/5.3.0)
|
||||||
|
- [RomM 5.4.0 release](https://github.com/rommapp/romm/releases/tag/5.4.0)
|
||||||
|
- [RomM 5.4.0 configuration example](https://github.com/rommapp/romm/blob/5.4.0/examples/config.example.yml)
|
||||||
|
- [RomM 5.4.0 Python requirement](https://github.com/rommapp/romm/blob/5.4.0/pyproject.toml)
|
||||||
|
- [[0015-romm-5-4-package]]
|
||||||
|
|
||||||
|
## Decision tree
|
||||||
|
|
||||||
|
- RomM 5.4 deployment
|
||||||
|
- Package runtime
|
||||||
|
- Settled: Build the local package with Python 3.14.
|
||||||
|
- Package source
|
||||||
|
- Settled: Maintain a local package.
|
||||||
|
- Constraint: Do not rely on either unreviewed Nixpkgs pull request.
|
||||||
|
- Package exposure
|
||||||
|
- Settled: Expose the package as `pkgs.alexion.romm`.
|
||||||
|
- Library layout
|
||||||
|
- Settled: Manage `config.yml` declaratively with Nix.
|
||||||
|
- Configuration materialization
|
||||||
|
- Settled: Replace the configuration file with a Nix-store symlink on activation.
|
||||||
|
- Settled: Declare Structure A.
|
||||||
|
- Settled: Set `filesystem.structure.default` to `roms/{platform}/{game}`.
|
||||||
|
- Settled: Set `filesystem.structure.firmware` to `bios/{platform}`.
|
||||||
|
- File safety
|
||||||
|
- Settled: Keep library conversion disabled.
|
||||||
|
- Settled: Do not enable `ROM_CONVERTO_ENABLED`.
|
||||||
|
- Browser emulation
|
||||||
|
- Settled: Set `emulatorjs.auto_save_sync` to `false`.
|
||||||
|
- Existing integration
|
||||||
|
- Settled: Reuse `services.romm`.
|
||||||
|
- Settled: Keep IGDB, ScreenScraper, and SteamGridDB configuration unchanged.
|
||||||
|
- Validation
|
||||||
|
- Settled: Build the local package and the Pikachu configuration.
|
||||||
|
- Settled: Do not run a local NixOS VM test.
|
||||||
|
- Settled: The operator validates the deployment on Pikachu.
|
||||||
|
|
||||||
|
## Settled decisions
|
||||||
|
|
||||||
|
- Build a local RomM 5.4.0 package.
|
||||||
|
- Do not rely on either unreviewed Nixpkgs pull request.
|
||||||
|
- Expose the package as `pkgs.alexion.romm`.
|
||||||
|
- Reuse `services.romm`.
|
||||||
|
- Generate `config.yml` with Nix.
|
||||||
|
- Replace `config.yml` with a Nix-store symlink on activation.
|
||||||
|
- Declare Structure A with `roms/{platform}/{game}` and `bios/{platform}`.
|
||||||
|
- Keep `ROM_CONVERTO_ENABLED` disabled.
|
||||||
|
- Set `emulatorjs.auto_save_sync` to `false`.
|
||||||
|
- Keep IGDB, ScreenScraper, and SteamGridDB configuration unchanged.
|
||||||
|
- Build the local package and Pikachu configuration.
|
||||||
|
- Do not run a local NixOS VM test.
|
||||||
|
- Let the operator validate the deployment on Pikachu.
|
||||||
|
|
||||||
|
## Frontier
|
||||||
|
|
||||||
|
No in-scope decisions remain.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
date-created: 2026-10-20 17:45
|
||||||
|
date-modified: 2026-10-20 17:52
|
||||||
|
---
|
||||||
|
# SteamVR to Meta Quest 3 design tree
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Stream PCVR from Zeus to a Meta Quest 3.
|
||||||
|
|
||||||
|
The design must produce a repeatable and diagnosable SteamVR session.
|
||||||
|
|
||||||
|
## Observed facts
|
||||||
|
|
||||||
|
- Zeus runs NixOS with a CachyOS kernel.
|
||||||
|
- Zeus uses an AMD Radeon RX 9070-series GPU with the `amdgpu` driver.
|
||||||
|
- Zeus runs Hyprland on Wayland.
|
||||||
|
- Zeus has Steam and Gamescope configured.
|
||||||
|
- SteamVR 2.17.10 is installed.
|
||||||
|
- SteamVR includes Valve's Linux `vrlink` driver.
|
||||||
|
- A previous `vrlink` session detected the Quest 3 and its Touch controllers.
|
||||||
|
- That session selected 90 Hz from the Quest's 72, 80, 90, and 120 Hz modes.
|
||||||
|
- SteamVR setup lacks `cap_sys_nice` on `vrcompositor-launcher`.
|
||||||
|
- NixOS can declare host packages, permissions, services, and diagnostics.
|
||||||
|
- Steam owns the Quest pairing and SteamVR user settings.
|
||||||
|
- Zeus connects by Ethernet at `10.23.50.221`.
|
||||||
|
- The local Wi-Fi scan finds the `Tracion` 5 GHz network at 5220 MHz.
|
||||||
|
- The scan reports a 1170 Mbit/s link rate.
|
||||||
|
- The operator has no external tracked devices.
|
||||||
|
|
||||||
|
## Decision tree
|
||||||
|
|
||||||
|
- **D1: Host platform commitment** — Settled: Linux-native on the current NixOS installation.
|
||||||
|
- Linux-native on the current NixOS installation.
|
||||||
|
- **D2: Transport** — Blocked by D1
|
||||||
|
- Wireless LAN. — Settled: primary transport.
|
||||||
|
- **D3: Streaming stack** — Settled: SteamVR with its built-in VRLink driver.
|
||||||
|
- SteamVR with its built-in VRLink driver.
|
||||||
|
- **D3a: SteamVR acceptance criteria** — Settled: reliable session test.
|
||||||
|
- Run a SteamVR game for 30 minutes at 90 Hz.
|
||||||
|
- Verify visuals, audio, and Touch controllers.
|
||||||
|
- Verify one reconnect.
|
||||||
|
- ALVR only after SteamVR fails the agreed acceptance criteria.
|
||||||
|
- **D4: Wi-Fi topology** — Settled: keep the current access point.
|
||||||
|
- Keep the current access point.
|
||||||
|
- Add a dedicated access point.
|
||||||
|
- Use a direct Wi-Fi link.
|
||||||
|
- **D5: Session performance target** — Settled: selectable refresh-rate profiles.
|
||||||
|
- Selectable refresh-rate profiles.
|
||||||
|
- **D5a: Default refresh rate** — Settled: 90 Hz.
|
||||||
|
- **D5b: Included refresh rates** — Settled: 72, 80, 90, and 120 Hz.
|
||||||
|
- **D6: Network isolation** — Settled: existing trusted LAN.
|
||||||
|
- Existing trusted LAN.
|
||||||
|
- Dedicated VR VLAN or SSID.
|
||||||
|
- **D7: Controller and tracking scope** — Settled: Quest Touch controllers only.
|
||||||
|
- Quest Touch controllers only.
|
||||||
|
- Also support external tracked devices.
|
||||||
|
- **D8: Launch workflow** — Settled: automatic service and headset connection.
|
||||||
|
- Start SteamVR when Steam Link connects from the Quest.
|
||||||
|
- **D9: Diagnostics and recovery** — Settled: capture health checks and logs.
|
||||||
|
- Capture SteamVR logs and health checks.
|
||||||
|
- Recover failed sessions manually.
|
||||||
|
- USB tether.
|
||||||
|
- **D10: USB streaming stack** — Blocked by D2
|
||||||
|
- ALVR over USB.
|
||||||
|
- Meta Quest Link through a Windows installation.
|
||||||
|
- **D11: USB role** — Blocked by D2
|
||||||
|
- Primary transport.
|
||||||
|
- Fallback transport.
|
||||||
|
- Both wireless LAN and USB tether.
|
||||||
|
- **D12: Primary transport** — Blocked by D2
|
||||||
|
- **D13: Fallback behavior** — Blocked by D12
|
||||||
|
- **D14: System integration** — Settled: hybrid configuration.
|
||||||
|
- NixOS manages system integration.
|
||||||
|
- Steam manages Quest pairing and user settings.
|
||||||
|
- Dual-boot or replace NixOS with Windows.
|
||||||
|
- **D15: Windows streaming stack** — Blocked by D1
|
||||||
|
- Steam Link.
|
||||||
|
- Virtual Desktop.
|
||||||
|
- Meta Quest Link or Air Link.
|
||||||
|
- ALVR.
|
||||||
|
- **D16: Windows installation scope** — Blocked by D1
|
||||||
|
- Dedicated VR installation.
|
||||||
|
- General-use Windows installation.
|
||||||
|
- **D2: Transport** — Blocked by D1
|
||||||
|
- Keep NixOS and add a separate Windows VR host.
|
||||||
|
- **D17: Host placement** — Blocked by D1
|
||||||
|
- **D15: Windows streaming stack** — Blocked by D17
|
||||||
|
- **D2: Transport** — Blocked by D17
|
||||||
|
|
||||||
|
## Frontier
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Settled decisions
|
||||||
|
|
||||||
|
- **D1: Host platform commitment** — Linux-native on the current NixOS installation.
|
||||||
|
- **D2: Transport** — Wireless LAN as the primary transport.
|
||||||
|
- **D4: Wi-Fi topology** — Keep the current access point.
|
||||||
|
- **D5: Session performance target** — Selectable profiles at 72, 80, 90, and 120 Hz.
|
||||||
|
- **D5a: Default refresh rate** — 90 Hz.
|
||||||
|
- **D5b: Included refresh rates** — 72, 80, 90, and 120 Hz.
|
||||||
|
- **D6: Network isolation** — Existing trusted LAN.
|
||||||
|
- **D7: Controller and tracking scope** — Quest Touch controllers only.
|
||||||
|
- **D3: Streaming stack** — SteamVR with its built-in VRLink driver.
|
||||||
|
- **D3a: SteamVR acceptance criteria** — 30-minute session at 90 Hz, with visuals, audio, Touch controllers, and one reconnect.
|
||||||
|
- **D8: Launch workflow** — Start SteamVR when Steam Link connects from the Quest.
|
||||||
|
- **D9: Diagnostics and recovery** — Capture health checks and logs. Recover failed sessions manually.
|
||||||
|
- **D14: System integration** — NixOS manages system integration. Steam manages Quest pairing and user settings.
|
||||||
Whitespace-only changes.
+4
-3
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
date-created: 2026-10-19 02:39
|
||||||
|
date-modified: 2026-10-20 17:45
|
||||||
|
---
|
||||||
# Brain Agent Instructions
|
# Brain Agent Instructions
|
||||||
|
|
||||||
This repository is an Obsidian vault for an execution-focused second brain.
|
This repository is an Obsidian vault for an execution-focused second brain.
|
||||||
@@ -32,6 +36,3 @@ Include negative decisions only when they prevent a likely, harmful, or repeated
|
|||||||
|
|
||||||
Remove template instruction text when writing a note from a template.
|
Remove template instruction text when writing a note from a template.
|
||||||
|
|
||||||
## Gotchas
|
|
||||||
|
|
||||||
|
|
||||||
@@ -51,11 +51,8 @@
|
|||||||
|
|
||||||
- lidarr (requires raichu)
|
- lidarr (requires raichu)
|
||||||
- immich
|
- immich
|
||||||
- romm
|
|
||||||
- gitea
|
- gitea
|
||||||
- nginxproxymanager (gradual migration to nginx guest)
|
- nginxproxymanager (gradual migration to nginx guest)
|
||||||
- adguard
|
|
||||||
- manage DNS rewrites for local service names such as `home`
|
|
||||||
|
|
||||||
## Raichu Proxmox -> Nixos
|
## Raichu Proxmox -> Nixos
|
||||||
|
|
||||||
@@ -110,8 +107,3 @@
|
|||||||
- Investigate Rust Bazel target structure.
|
- Investigate Rust Bazel target structure.
|
||||||
Prefer clear per-file dependency edges, but `rust_library` maps each target to a crate.
|
Prefer clear per-file dependency edges, but `rust_library` maps each target to a crate.
|
||||||
Decide whether wrapper rules or another pattern can improve graph clarity without forcing artificial crate boundaries.
|
Decide whether wrapper rules or another pattern can improve graph clarity without forcing artificial crate boundaries.
|
||||||
|
|
||||||
## Nix Refactor
|
|
||||||
|
|
||||||
Review Nix files in entrypoint order:
|
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||||
|
|
||||||
|
rust_library(
|
||||||
|
name = "rust",
|
||||||
|
crate_name = "rust",
|
||||||
|
srcs = [
|
||||||
|
"rust.rs",
|
||||||
|
"rust_test.rs",
|
||||||
|
],
|
||||||
|
compile_data = glob(["templates/*"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_test(
|
||||||
|
name = "rust_test",
|
||||||
|
crate = ":rust",
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_binary(
|
||||||
|
name = "rust_cli",
|
||||||
|
crate_name = "rust_cli",
|
||||||
|
srcs = [
|
||||||
|
"rust_main.rs",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
":rust",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "scaffolding"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "rust"
|
||||||
|
path = "rust.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rust_cli"
|
||||||
|
path = "rust_main.rs"
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
const BUILD_TEMPLATE: &str = include_str!("templates/BUILD.template");
|
||||||
|
const CARGO_TEMPLATE: &str = include_str!("templates/Cargo.toml.template");
|
||||||
|
const LIBRARY_TEMPLATE: &str = include_str!("templates/library.rs.template");
|
||||||
|
const BINARY_TEMPLATE: &str = include_str!("templates/binary.rs.template");
|
||||||
|
const TEST_TEMPLATE: &str = include_str!("templates/test.rs.template");
|
||||||
|
const CRATE_NAME_PLACEHOLDER: &str = "{CRATE_NAME}";
|
||||||
|
|
||||||
|
/// Creates a Rust package at `destination`.
|
||||||
|
pub fn run(destination: &Path) -> io::Result<PathBuf> {
|
||||||
|
scaffold(&workspace_root()?, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scaffold(workspace_root: &Path, destination: &Path) -> io::Result<PathBuf> {
|
||||||
|
let workspace_root = fs::canonicalize(workspace_root)?;
|
||||||
|
let destination = resolve_destination(&workspace_root, destination)?;
|
||||||
|
let crate_name = crate_name(&destination)?;
|
||||||
|
let workspace_member = workspace_member(&workspace_root, &destination)?;
|
||||||
|
let files = render_files(&crate_name);
|
||||||
|
let cargo_path = workspace_root.join("Cargo.toml");
|
||||||
|
let cargo_manifest = fs::read_to_string(&cargo_path)?;
|
||||||
|
let cargo_manifest = update_workspace_members(&cargo_manifest, &workspace_member)?;
|
||||||
|
|
||||||
|
fs::create_dir(&destination)?;
|
||||||
|
for file in files {
|
||||||
|
fs::write(destination.join(file.name), file.contents)?;
|
||||||
|
}
|
||||||
|
fs::write(cargo_path, cargo_manifest)?;
|
||||||
|
|
||||||
|
Ok(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root() -> io::Result<PathBuf> {
|
||||||
|
match env::var_os("BUILD_WORKSPACE_DIRECTORY") {
|
||||||
|
Some(path) => Ok(PathBuf::from(path)),
|
||||||
|
None => env::current_dir(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_destination(workspace_root: &Path, destination: &Path) -> io::Result<PathBuf> {
|
||||||
|
let destination = if destination.is_absolute() {
|
||||||
|
destination.to_path_buf()
|
||||||
|
} else {
|
||||||
|
workspace_root.join(destination)
|
||||||
|
};
|
||||||
|
let parent = destination.parent().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination must have a parent directory",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let parent = fs::canonicalize(parent)?;
|
||||||
|
|
||||||
|
if !parent.starts_with(workspace_root) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!(
|
||||||
|
"the destination is outside the workspace: {}",
|
||||||
|
destination.display()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let destination = parent.join(destination.file_name().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination must name a directory",
|
||||||
|
)
|
||||||
|
})?);
|
||||||
|
if destination.try_exists()? {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::AlreadyExists,
|
||||||
|
format!("the destination already exists: {}", destination.display()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn crate_name(destination: &Path) -> io::Result<String> {
|
||||||
|
let crate_name = destination
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination name must be valid UTF-8",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if crate_name.is_empty()
|
||||||
|
|| !crate_name.bytes().all(|character| {
|
||||||
|
character.is_ascii_lowercase() || character.is_ascii_digit() || character == b'_'
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid crate name: {crate_name}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(crate_name.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_member(workspace_root: &Path, destination: &Path) -> io::Result<String> {
|
||||||
|
let member = destination.strip_prefix(workspace_root).map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination is outside the workspace",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let member = member.to_str().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination must be valid UTF-8",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(member.replace(std::path::MAIN_SEPARATOR, "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_files(crate_name: &str) -> Vec<RenderedFile> {
|
||||||
|
vec![
|
||||||
|
render_file("BUILD", BUILD_TEMPLATE, crate_name),
|
||||||
|
render_file("Cargo.toml", CARGO_TEMPLATE, crate_name),
|
||||||
|
render_file(&format!("{crate_name}.rs"), LIBRARY_TEMPLATE, crate_name),
|
||||||
|
render_file(
|
||||||
|
&format!("{crate_name}_main.rs"),
|
||||||
|
BINARY_TEMPLATE,
|
||||||
|
crate_name,
|
||||||
|
),
|
||||||
|
render_file(&format!("{crate_name}_test.rs"), TEST_TEMPLATE, crate_name),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_file(name: &str, template: &str, crate_name: &str) -> RenderedFile {
|
||||||
|
RenderedFile {
|
||||||
|
name: name.to_owned(),
|
||||||
|
contents: template.replace(CRATE_NAME_PLACEHOLDER, crate_name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_workspace_members(manifest: &str, member: &str) -> io::Result<String> {
|
||||||
|
let workspace_start = manifest.find("[workspace]").ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"the root Cargo.toml has no [workspace] section",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let members_start = manifest[workspace_start..]
|
||||||
|
.find("members = [")
|
||||||
|
.map(|offset| workspace_start + offset)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"the root Cargo.toml has no workspace members array",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let members_end = manifest[members_start..]
|
||||||
|
.find(']')
|
||||||
|
.map(|offset| members_start + offset)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"the root Cargo.toml has an unclosed workspace members array",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if manifest[members_start..members_end].contains(&format!("\"{member}\"")) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::AlreadyExists,
|
||||||
|
format!("the workspace already has member: {member}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut manifest = manifest.to_owned();
|
||||||
|
manifest.insert_str(members_end, &format!(" \"{member}\",\n"));
|
||||||
|
Ok(manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RenderedFile {
|
||||||
|
name: String,
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "rust_test.rs"]
|
||||||
|
mod rust_test;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let mut arguments = env::args_os();
|
||||||
|
let program = arguments.next().unwrap_or_else(|| "rust_cli".into());
|
||||||
|
let Some(destination) = arguments.next() else {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("usage: {} <destination>", PathBuf::from(program).display()),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if destination == "--help" {
|
||||||
|
println!("usage: {} <destination>", PathBuf::from(program).display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if arguments.next().is_some() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"expected exactly one destination",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let destination = rust::run(&PathBuf::from(destination))?;
|
||||||
|
println!("created {}", destination.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use super::{crate_name, render_files, scaffold, update_workspace_members};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_valid_crate_names() {
|
||||||
|
assert_eq!(crate_name(Path::new("foo_2")).unwrap(), "foo_2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_crate_names() {
|
||||||
|
assert!(crate_name(Path::new("Foo")).is_err());
|
||||||
|
assert!(crate_name(Path::new("foo-bar")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renders_crate_name_into_every_file() {
|
||||||
|
for file in render_files("example") {
|
||||||
|
assert!(!file.contents.contains("{CRATE_NAME}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adds_workspace_member() {
|
||||||
|
let manifest = "[workspace]\nmembers = [\n \"src/status\",\n]\n";
|
||||||
|
let updated = update_workspace_members(manifest, "src/example").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
updated,
|
||||||
|
"[workspace]\nmembers = [\n \"src/status\",\n \"src/example\",\n]\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_a_rust_package() {
|
||||||
|
let workspace = std::env::temp_dir().join(format!(
|
||||||
|
"scaffolding-test-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos(),
|
||||||
|
));
|
||||||
|
fs::create_dir_all(workspace.join("src")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
workspace.join("Cargo.toml"),
|
||||||
|
"[workspace]\nmembers = [\n]\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let destination = scaffold(&workspace, &workspace.join("src/example")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
destination,
|
||||||
|
fs::canonicalize(workspace.join("src/example")).unwrap()
|
||||||
|
);
|
||||||
|
assert!(destination.join("BUILD").exists());
|
||||||
|
assert!(destination.join("Cargo.toml").exists());
|
||||||
|
assert!(destination.join("example.rs").exists());
|
||||||
|
assert!(destination.join("example_main.rs").exists());
|
||||||
|
assert!(destination.join("example_test.rs").exists());
|
||||||
|
assert!(
|
||||||
|
fs::read_to_string(workspace.join("Cargo.toml"))
|
||||||
|
.unwrap()
|
||||||
|
.contains(" \"src/example\",")
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_dir_all(workspace).unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||||
|
|
||||||
|
rust_library(
|
||||||
|
name = "{CRATE_NAME}",
|
||||||
|
crate_name = "{CRATE_NAME}",
|
||||||
|
srcs = [
|
||||||
|
"{CRATE_NAME}.rs",
|
||||||
|
"{CRATE_NAME}_test.rs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_test(
|
||||||
|
name = "{CRATE_NAME}_test",
|
||||||
|
crate = ":{CRATE_NAME}",
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_binary(
|
||||||
|
name = "{CRATE_NAME}_cli",
|
||||||
|
crate_name = "{CRATE_NAME}_cli",
|
||||||
|
srcs = [
|
||||||
|
"{CRATE_NAME}_main.rs",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
":{CRATE_NAME}",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "{CRATE_NAME}"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "{CRATE_NAME}.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "{CRATE_NAME}_cli"
|
||||||
|
path = "{CRATE_NAME}_main.rs"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
{CRATE_NAME}::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
#[path = "{CRATE_NAME}_test.rs"]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub fn run() {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use super::run;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runs() {
|
||||||
|
run();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user