Compare commits

...

30 Commits

Author SHA1 Message Date
7186d6a3a7 refactor(nixos): scope agent guidance 2026-10-20 04:40:14 -04:00
c836396cbe docs(tasks): close RomM guest task 2026-10-19 23:36:01 -04:00
3493519085 feat(nixos): deploy RomM guest 2026-10-19 23:35:07 -04:00
f160fd15a2 fix(neogaia): restore Caps Escape swap 2026-10-19 20:41:12 -04:00
f1a93199c7 docs: plan RomM migration 2026-10-19 20:04:50 -04:00
591144df33 docs(tasks): close NixOS refactor task 2026-10-19 16:45:48 -04:00
d7760ef84d docs(tasks): track gitea-axi packaging migration 2026-10-19 16:45:48 -04:00
042238476a feat(pi): add tldr prompt command 2026-10-19 16:04:30 -04:00
e41b8e3eb4 docs: capture agent workflow decisions 2026-10-19 02:41:41 -04:00
63017f8e33 feat(skills): add grill skill 2026-10-19 02:32:34 -04:00
52f31fd44d docs(tasks): add Gitea Axi tasks 2026-10-18 15:44:55 -04:00
898a6df333 fix(skills): clarify skill guidance 2026-10-18 15:40:23 -04:00
5a5543633c fix(agents): preserve review-only requests 2026-10-18 15:05:55 -04:00
9c501a79ad feat(skills): add retrospective skill 2026-10-18 14:51:59 -04:00
30e5b1e677 docs: add project task notes 2026-10-18 01:16:20 -04:00
45a840002f refactor(skills): remove redundant skills 2026-10-17 22:04:31 -04:00
a9db1f8b7c feat(skills): add agent writing guide 2026-10-17 22:04:31 -04:00
dcc4efcc75 fix(rofi): migrate Home Manager options 2026-10-15 18:16:47 -04:00
a23a317335 build(nix): update flake inputs 2026-10-15 18:16:47 -04:00
6e545c0a15 build(pi): integrate vendored Pi 1.3.0 2026-10-15 18:05:57 -04:00
6081e3e325 chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
2026-10-15 17:58:48 -04:00
719370c8f7 refactor(bazel): inline the primary user 2026-10-15 14:23:18 -04:00
2b4e66a75c fix(palworld): configure the RCON port 2026-10-15 14:18:27 -04:00
c44769ff70 refactor(steamcmd): constrain service names 2026-10-15 14:13:07 -04:00
1397576538 refactor(reverse-proxy): simplify virtual host generation 2026-10-15 13:57:45 -04:00
5fbe1850e0 docs(tasks): organize task areas 2026-10-14 18:18:18 -04:00
57bb21af1a feat(vault): add task workflow 2026-10-14 01:24:22 -04:00
ffc40e547b feat(skills): add Conventional Commits skill 2026-10-13 22:49:44 -04:00
55f61f71d8 refactor(natu): remove custom Nix linter 2026-10-13 21:59:41 -04:00
08563c6769 refactor(nixos): remove unused direnv module 2026-10-13 21:50:37 -04:00
2933 changed files with 244569 additions and 488936 deletions

View File

@@ -17,15 +17,6 @@ If the target includes multiple matching file types, use all matching skills.
If the target is a diff, choose skills from the changed files. If the target is a diff, choose skills from the changed files.
Do not use a style skill when no reviewed file matches it. Do not use a style skill when no reviewed file matches it.
For every reviewed `*.nix` file, run:
```bash
bazel run //src/natu:natu -- <reviewed-nix-paths>
```
Pass every reviewed Nix path to Natu.
Report every Natu diagnostic as an actionable finding.
Report only actionable findings. Report only actionable findings.
Order findings by severity. Order findings by severity.
Do not report preferences unless a loaded rule states them. Do not report preferences unless a loaded rule states them.
@@ -45,7 +36,6 @@ Keep the code excerpt to one to three lines when possible.
Use a longer excerpt only when the violation needs surrounding context. Use a longer excerpt only when the violation needs surrounding context.
Use `...` to omit unrelated lines from a multi-line excerpt. Use `...` to omit unrelated lines from a multi-line excerpt.
Use a style-skill rule citation in the form `skill::rule-file:line`. Use a style-skill rule citation in the form `skill::rule-file:line`.
Cite Natu diagnostics as `natu::<rule-id>`.
Make the fix direct. Make the fix direct.
Do not include alternatives when one fix best follows the cited rule. Do not include alternatives when one fix best follows the cited rule.

8
.pi/prompts/tldr.md Normal file
View File

@@ -0,0 +1,8 @@
---
description: Summarize an agent response
argument-hint: "[source request]"
---
Summarize `${ARGUMENTS:-the preceding agent response}`.
Keep the summary compact.
Use no fixed format.

View File

@@ -0,0 +1 @@
../../src/skills/agent-writing-guide

View File

@@ -0,0 +1 @@
../../src/skills/conventional-commits

View File

@@ -1 +0,0 @@
../../src/skills/craft-skill

1
.pi/skills/retrospect Symbolic link
View File

@@ -0,0 +1 @@
../../src/skills/retrospect

View File

@@ -1 +0,0 @@
../../src/skills/to-document

View File

@@ -3,7 +3,6 @@
## Commit convention ## Commit convention
- Use Conventional Commits for all commit messages. - Use Conventional Commits for all commit messages.
Before writing a commit message, read `docs/conventional-commits.md`.
- Keep commit messages about the Git history, not the forge workflow. - Keep commit messages about the Git history, not the forge workflow.
Include external references only when they are stable context for the change. Include external references only when they are stable context for the change.
- When writing commit messages, NEVER auto-add your agent name as co-author. - When writing commit messages, NEVER auto-add your agent name as co-author.
@@ -49,6 +48,8 @@
- NEVER ask the user a question using the `AskUserQuestion` tool. - NEVER ask the user a question using the `AskUserQuestion` tool.
Ask in plain prose, in your own message, instead, with no exceptions. Ask in plain prose, in your own message, instead, with no exceptions.
This overrides any default instruction to use it. This overrides any default instruction to use it.
- For review requests, inspect the requested material and report findings.
Edit files after the operator accepts a proposed change.
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated. - Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
Detect auto-generated files with a layered check. Detect auto-generated files with a layered check.
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`. Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
@@ -68,20 +69,6 @@
after applying an accepted change. after applying an accepted change.
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next". If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
## Corrections
- When you discover that your belief about a project fact or convention was wrong, record the correction.
Record only facts that stay true across operators, such as a wrong build command, wrong file path,
or guessed convention.
Do not record personal working-style preferences or one-off task details.
- Record the correction in the nearest project `AGENTS.md`.
Use a dedicated `## Gotchas` section.
Create that section when the file lacks one.
- If no project `AGENTS.md` exists, ask before creating one.
Append the correction immediately without asking when a project `AGENTS.md` exists.
Briefly mention the edit in your response.
If an existing entry is wrong or stale, correct or remove it the same way.
## Gotchas ## Gotchas
- When materializing an external specification, standard, or project-maintained convention, use the - When materializing an external specification, standard, or project-maintained convention, use the
@@ -101,17 +88,12 @@
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to - Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
`src/ariadne/Cargo.toml` unless explicitly requested. `src/ariadne/Cargo.toml` unless explicitly requested.
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`. - In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
- The flake does not currently define `formatter.x86_64-linux`.
Do not use `nix fmt` as a validation command until a formatter is added.
- The `fix` app passes its current directory to Deadnix. - The `fix` app passes its current directory to Deadnix.
Run `nix run .#fix` from the repository root so path exclusions match. Run `nix run .#fix` from the repository root so path exclusions match.
- During the Nix refactor, address each issue that comes up by adding either a Natu rule or a new Nix style rule.
- Git pathspec searches started in a subdirectory default to that subtree. - Git pathspec searches started in a subdirectory default to that subtree.
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches. Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
- A `**/*.nix` Git pathspec does not include Nix files directly in its starting directory. - A `**/*.nix` Git pathspec does not include Nix files directly in its starting directory.
Include both `*.nix` and `**/*.nix`, or use a recursive `*.nix` pathspec, when linting every Nix file. Include both `*.nix` and `**/*.nix`, or use a recursive `*.nix` pathspec, when linting every Nix file.
- Nix `let` bindings are recursive; their source order does not determine binding availability.
Treat Natu `let_order` as a readability rule, not an evaluation dependency.
- Ariadne extraction must preserve the game-root season prefix for archived source. - Ariadne extraction must preserve the game-root season prefix for archived source.
Different game installs can contain compiled scripts with the same internal path. Different game installs can contain compiled scripts with the same internal path.
- Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data. - Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data.
@@ -122,7 +104,13 @@
- OMP does not discover project skills from `.pi/skills`. - OMP does not discover project skills from `.pi/skills`.
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`. Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
- Pi is vendored under `third_party/pi` as a linear vendor import. - Pi is vendored under `third_party/pi` as a linear vendor import.
`pkgs.alexion.pi-coding-agent` packages that source and does not use Nixpkgs Pi.
Do not use `git subtree add` or `git subtree pull` for Pi PR branches. Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix. Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits. Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source. - The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
- The `monorepo` and `status` libraries do not have `README.md` files.
- Check root and `src/skills/.pi/skills/` for enabled symlinks before removing a monorepo skill.
- Ariadne Rust source files live directly under `src/ariadne/`. The binary entry point is `ariadne_main.rs.
- Bazel build-style rules live under `src/skills/style/build/rules/`, not `src/skills/style/rules/`.
- Bazel passes arguments after `--` to the executed binary verbatim. A scaffold can define `//path` as a destination-path syntax.

154
Cargo.lock generated
View File

@@ -2,15 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "1.0.0" version = "1.0.0"
@@ -71,12 +62,6 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.7" version = "4.6.7"
@@ -108,7 +93,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.6", "syn",
] ]
[[package]] [[package]]
@@ -123,41 +108,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "countme"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
[[package]]
name = "googletest"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b5e2f2b556b7b90297a5a35c8267dd43a537923d2b329beefdba2b4ec19d94"
dependencies = [
"googletest_macro",
"num-traits",
"regex",
"rustversion",
]
[[package]]
name = "googletest_macro"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ae6abc96141edd26bf5aeec0f119c129c44de3ced09e5073711a02cb74725d0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -190,27 +140,6 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
name = "monorepo" name = "monorepo"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "natu"
version = "0.1.0"
dependencies = [
"clap",
"googletest",
"monorepo",
"rnix",
"rowan",
"status",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "once_cell_polyfill" name = "once_cell_polyfill"
version = "1.70.2" version = "1.70.2"
@@ -235,68 +164,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "regex"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rnix"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c163bd17372eecdf10d351c34584b7de7c1a33be4e92a32f3fb3f5a7fe3f579b"
dependencies = [
"rowan",
]
[[package]]
name = "rowan"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
dependencies = [
"countme",
"hashbrown",
"rustc-hash",
"text-size",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.229" version = "1.0.229"
@@ -324,7 +191,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.6", "syn",
] ]
[[package]] [[package]]
@@ -350,17 +217,6 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "3.0.6" version = "3.0.6"
@@ -372,12 +228,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "text-size"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.26" version = "1.0.26"

View File

@@ -4,16 +4,12 @@ members = [
"src/machoke", "src/machoke",
"src/monorepo", "src/monorepo",
"src/status", "src/status",
"src/natu",
] ]
resolver = "3" resolver = "3"
[workspace.dependencies] [workspace.dependencies]
clap = "4" clap = "4"
googletest = "0.14"
monorepo = { path = "src/monorepo" } monorepo = { path = "src/monorepo" }
rnix = "=0.14.0"
rowan = "0.16"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
status = { path = "src/status" } status = { path = "src/status" }

View File

@@ -1,67 +0,0 @@
# NixOS configuration instructions
## Desktop
- When adding a graphical application, register its workspace icon in `registry.window-icon`.
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
Match the mapping on the window class.
Store the icon as the rendered string.
Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session.
- TODO: Verify this path after the NixOS configuration is migrated.
Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.
`home.sessionVariables` do **not** reach the Hyprland session because UWSM does not source `hm-session-vars.sh`.
Source cursor values from `config.stylix.cursor`.
Set `XCURSOR_*` and `HYPRCURSOR_*` to the same theme for Bibata.
## Firefox
- Manage Firefox search engines through `programs.firefox.search` with `force = true`.
Hide built-in engines with `<engine>.metaData.hidden = true` because omission does not remove them.
Use current engine ids, such as `default = "ddg"` instead of `"DuckDuckGo"`.
Decode the built `search.json.mozlz4` with `mozlz4a -d <search.json.mozlz4>` to check the result.
- Avoid Home Manager Firefox extension settings unless the whole profile can use the legacy extension-storage backend.
Any non-empty `profiles.<name>.extensions.settings.<id>.settings` sets `extensions.webextensions.ExtensionStorageIDB.enabled = false` for the whole profile.
## Neovim
- Nixvim's flake input follows the root nixpkgs source, but its Home Manager module does not reuse the host's `pkgs` instance.
Keep `programs.nixvim.nixpkgs.useGlobalPackages = true`.
That setting makes Nixvim use the shared package set without warning that its source default changed.
## AI tooling
- Use produced literals for Herdr key names that include shifted punctuation.
For example, use `prefix+<` instead of `prefix+shift+comma`.
- Put monorepo Pi extension sources in `.pi/extensions/`.
Pi discovers them from subdirectories through the local ancestor-resource patch.
- Treat Home Manager-managed Pi settings as generated output.
Do not edit them except for throwaway development or local experiments.
Promote finished settings into the NixOS configuration before treating them as deployed.
## Gotchas
- `configs/nixos/registry/stable-uid.nix` receives library functions through `lib`.
Qualify its library calls with `lib.`.
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
- `sops.secrets.<name>.owner` must name a user known on the decrypting host.
Do not set the owner to a guest-only service user unless the host also declares that user.
Use a host-visible owner plus a shared group mode when the guest service can read the decrypted secret file through its group.
- A read-only guest bind mount cannot contain nested writable bind mounts unless their mount-point
directories already exist inside the read-only source.
For aggregate Copyparty volumes such as `/srv` with nested `/srv/inbox`, keep the guest bind mount
writable and enforce read-only behavior through Copyparty access plus service `ReadWritePaths`.
- `deploy-to` builds from the source checkout, records the imported closure as the target system profile,
and activates it. Do not rely on a target-local monorepo checkout for remote deployment.
- On Pikachu, `nix copy --to ssh://pikachu` accepted a small `nix store add-file` probe,
but rejected locally built system paths that lacked trusted signatures.
Use a sudo `nix-store --import` path for unsigned deploy closures.
- Public DNS entries are not managed through the NixOS configuration.
Do not describe NixOS route choices as public-DNS management.
- Do not hand-author `hardware-configuration.nix` for a host that has not been generated on that machine.
Generate it on the target system or live ISO, then stage it before evaluating the flake.
- Configure the Chaotic Nyx cache only on hosts that select `linuxPackages_cachyos` (currently Neogaia
and Zeus), not in the generic host module.
- Module-relative paths resolve from the module file. Update them when moving a module into a subdirectory.
- On Pikachu, `uptime` does not support `-s`. Use `who -b` to report the boot time.

View File

@@ -4,7 +4,7 @@
... ...
}: }:
let let
cfg = config.guests.copyparty; # NOLINT(let_order) cfg = config.guests.copyparty;
in in
{ {
options.guests.copyparty = { options.guests.copyparty = {

View File

@@ -4,7 +4,7 @@
... ...
}: }:
let let
cfg = config.guests.homepage; # NOLINT(let_order) cfg = config.guests.homepage;
environmentFile = "/run/secrets/homepage.env"; environmentFile = "/run/secrets/homepage.env";
in in
{ {

View File

@@ -4,7 +4,7 @@
... ...
}: }:
let let
cfg = config.guests.palworld; # NOLINT(let_order) cfg = config.guests.palworld;
steamDir = "/data/steamcmd"; steamDir = "/data/steamcmd";
installDir = "/data/server"; installDir = "/data/server";
stateDir = "/data/state"; stateDir = "/data/state";

View File

@@ -13,11 +13,6 @@ in
modules.services.reverse-proxy.enable = true; modules.services.reverse-proxy.enable = true;
services.nginx.virtualHosts = { services.nginx.virtualHosts = {
"games.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"git.alexion.dev" = { "git.alexion.dev" = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;

View File

@@ -0,0 +1,181 @@
{
config,
lib,
...
}:
let
cfg = config.guests.romm;
rommUid = config.registry."stable-uid".${config.networking.hostName}.romm;
redisUid = config.registry."stable-uid".${config.networking.hostName}."redis-romm";
in
{
options.guests.romm = {
dataPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/romm";
description = "Host path mounted as RomM application data.";
};
postgresqlPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/romm/postgresql";
description = "Host path mounted as RomM's PostgreSQL data.";
};
redisPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/romm/redis";
description = "Host path mounted as RomM's Redis data.";
};
romsPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/roms";
description = "Host path mounted as RomM's ROM library.";
};
biosPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/bios";
description = "Host path mounted as RomM's BIOS library.";
};
usersPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/romm/users";
description = "Host path mounted as RomM's user assets.";
};
};
config = lib.mkIf cfg.enable {
assertions =
map
(path: {
assertion = path.value != null;
message = "guests.romm.${path.name} must point at the host directory that stores ${path.description}.";
})
[
{
name = "dataPath";
value = cfg.dataPath;
description = "RomM application data";
}
{
name = "postgresqlPath";
value = cfg.postgresqlPath;
description = "RomM PostgreSQL data";
}
{
name = "redisPath";
value = cfg.redisPath;
description = "RomM Redis data";
}
{
name = "romsPath";
value = cfg.romsPath;
description = "RomM's ROM library";
}
{
name = "biosPath";
value = cfg.biosPath;
description = "RomM's BIOS library";
}
{
name = "usersPath";
value = cfg.usersPath;
description = "RomM's user assets";
}
];
guests.romm = {
interior = {
modules.services.romm.enable = true;
services.romm.nginx.virtualHost = cfg.reverseProxy.route.domain;
};
mounts = {
"/var/lib/romm" = {
directory = cfg.dataPath;
owner = rommUid;
group = "storage";
mode = "2770";
};
"/var/lib/postgresql" = {
directory = cfg.postgresqlPath;
owner = config.ids.uids.postgres;
group = "storage";
mode = "0700";
};
"/var/lib/redis-romm" = {
directory = cfg.redisPath;
owner = redisUid;
group = "storage";
mode = "0700";
};
"/var/lib/romm/library/roms" = {
directory = cfg.romsPath;
owner = rommUid;
group = "storage";
mode = "2770";
};
"/var/lib/romm/library/bios" = {
directory = cfg.biosPath;
owner = rommUid;
group = "storage";
mode = "2770";
};
"/var/lib/romm/assets/users" = {
directory = cfg.usersPath;
owner = rommUid;
group = "storage";
mode = "2770";
};
"/run/secrets/romm.env" = {
file = config.sops.templates."romm.env".path;
readOnly = true;
};
};
limits = {
memory = lib.mkDefault "4G";
cpu = lib.mkDefault "200%";
tasksMax = lib.mkDefault 512;
};
};
sops.secrets =
lib.genAttrs
[
"services/romm/igdb/client-id"
"services/romm/igdb/client-secret"
"services/romm/retroachievements/api-key"
"services/romm/screenscraper/user"
"services/romm/screenscraper/password"
"services/romm/steamgriddb/api-key"
]
(_: {
sopsFile = ../secrets + "/${config.networking.hostName}.yaml";
});
sops.templates."romm.env" = {
owner = "root";
group = "storage";
mode = "0440";
content = ''
IGDB_CLIENT_ID=${config.sops.placeholder."services/romm/igdb/client-id"}
IGDB_CLIENT_SECRET=${config.sops.placeholder."services/romm/igdb/client-secret"}
SCREENSCRAPER_USER=${config.sops.placeholder."services/romm/screenscraper/user"}
SCREENSCRAPER_PASSWORD=${config.sops.placeholder."services/romm/screenscraper/password"}
RETROACHIEVEMENTS_API_KEY=${config.sops.placeholder."services/romm/retroachievements/api-key"}
STEAMGRIDDB_API_KEY=${config.sops.placeholder."services/romm/steamgriddb/api-key"}
'';
};
};
}

View File

@@ -4,7 +4,7 @@
... ...
}: }:
let let
cfg = config.guests.valheim; # NOLINT(let_order) cfg = config.guests.valheim;
steamDir = "/data/steamcmd"; steamDir = "/data/steamcmd";
installDir = "/data/server"; installDir = "/data/server";
stateDir = "/data/state"; stateDir = "/data/state";

View File

@@ -15,6 +15,8 @@
modules.host.cachyos.enable = true; modules.host.cachyos.enable = true;
services.xserver.xkb.options = "caps:swapescape";
modules.network.enable = true; modules.network.enable = true;
registry.address.neogaia.self = "10.23.50.146"; registry.address.neogaia.self = "10.23.50.146";

View File

@@ -88,6 +88,26 @@
}; };
}; };
guests.romm = {
enable = true;
vlan = 20;
address = "10.23.20.112";
dataPath = "/pikachu/data/romm";
postgresqlPath = "/pikachu/data/romm/postgresql";
redisPath = "/pikachu/data/romm/redis";
romsPath = "/pikachu/media/games/emulation/roms";
biosPath = "/pikachu/media/games/emulation/bios";
usersPath = "/pikachu/media/games/emulation/users";
reverseProxy = {
enable = true;
route = {
domain = "games.alexion.dev";
protocol = "https";
port = 80;
};
};
};
guests.reverse-proxy = { guests.reverse-proxy = {
enable = true; enable = true;
vlan = 20; vlan = 20;

View File

@@ -8,7 +8,7 @@
}: }:
# Configures Firefox as the desktop browser. # Configures Firefox as the desktop browser.
let let
cfg = config.modules.desktop.apps.firefox; # NOLINT(let_order) cfg = config.modules.desktop.apps.firefox;
in in
{ {
options.modules.desktop.apps.firefox = { options.modules.desktop.apps.firefox = {
@@ -67,6 +67,8 @@ in
# Stylix uses a near-white selection background. # Stylix uses a near-white selection background.
# Use the Nord selection color because its text stays readable. # Use the Nord selection color because its text stays readable.
# Home Manager writes extension settings through Firefox's legacy storage backend.
# Extension settings disable `ExtensionStorageIDB` for the whole profile.
extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight = extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight =
let let
c = hm.config.lib.stylix.colors; c = hm.config.lib.stylix.colors;

View File

@@ -37,6 +37,7 @@
config = { config = {
input = { input = {
kb_layout = config.services.xserver.xkb.layout; kb_layout = config.services.xserver.xkb.layout;
kb_options = config.services.xserver.xkb.options;
repeat_delay = 250; repeat_delay = 250;
repeat_rate = 45; repeat_rate = 45;
accel_profile = "flat"; accel_profile = "flat";

View File

@@ -28,12 +28,11 @@ in
# Stylix supplies the Nord theme. # Stylix supplies the Nord theme.
programs.rofi = { programs.rofi = {
enable = true; enable = true;
font = lib.mkForce "JetBrains Mono 12";
# Application launch is the only mode, kept minimal so the prompt opens # Application launch is the only mode, kept minimal so the prompt opens
# as fast as rofi can read the desktop entries. # as fast as rofi can read the desktop entries.
# Icons are off: resolving one per entry is the bulk of drun's startup. # Icons are off: resolving one per entry is the bulk of drun's startup.
extraConfig = { settings = {
font = lib.mkForce "JetBrains Mono 12";
modi = "drun"; modi = "drun";
show-icons = false; show-icons = false;
}; };

View File

@@ -5,7 +5,7 @@
}: }:
# Imports existing ZFS pools that hold durable service state. # Imports existing ZFS pools that hold durable service state.
let let
cfg = config.modules.host.zfs; # NOLINT(let_order) cfg = config.modules.host.zfs;
in in
{ {
options.modules.host.zfs = { options.modules.host.zfs = {

View File

@@ -7,19 +7,17 @@
# Configures inbound and outbound SSH access. # Configures inbound and outbound SSH access.
let let
cfg = config.modules.network.ssh; cfg = config.modules.network.ssh;
user = config.user.name; # NOLINT(let_order) user = config.user.name;
# NOLINT(let_order)
workstationNames = [ workstationNames = [
"neogaia" "neogaia"
"zeus" "zeus"
]; ];
serverNames = [ "pikachu" ]; serverNames = [ "pikachu" ];
isWorkstation = lib.elem config.networking.hostName workstationNames; # NOLINT(let_order) isWorkstation = lib.elem config.networking.hostName workstationNames;
isServer = lib.elem config.networking.hostName serverNames; # NOLINT(let_order) isServer = lib.elem config.networking.hostName serverNames;
# NOLINT(let_order)
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub"); clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
hostKeysFor = hostKeysFor =
host: host:
@@ -28,7 +26,6 @@ let
(../../hosts + "/${host}/ssh_host_rsa_key.pub") (../../hosts + "/${host}/ssh_host_rsa_key.pub")
]; ];
# NOLINT(let_order)
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: { managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
hostName = registry.address.${name}.self; hostName = registry.address.${name}.self;
clientKey = clientKeyFor name; clientKey = clientKeyFor name;
@@ -48,7 +45,6 @@ let
}; };
}; };
# NOLINT(let_order)
mkTarget = name: target: { mkTarget = name: target: {
hostName = target.hostName or name; hostName = target.hostName or name;
user = target.user or user; user = target.user or user;
@@ -57,15 +53,14 @@ let
clientKey = target.clientKey or null; clientKey = target.clientKey or null;
hostKeys = target.hostKeys or [ ]; hostKeys = target.hostKeys or [ ];
}; };
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets); # NOLINT(let_order) targets = lib.mapAttrs mkTarget (managedTargets // extraTargets);
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names); # NOLINT(let_order) clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names);
# NOLINT(let_order)
defaultAuthorizedKeys = lib.flatten ( defaultAuthorizedKeys = lib.flatten (
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames) clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
); );
outboundTargetNames = # NOLINT(let_order) outboundTargetNames =
let let
groupTargets = groupTargets =
if isServer then if isServer then

View File

@@ -6,7 +6,7 @@
}: }:
# Configures bridges for tagged VLANs on a physical interface that uses DHCP. # Configures bridges for tagged VLANs on a physical interface that uses DHCP.
let let
cfg = config.modules.network.vlans; # NOLINT(let_order) cfg = config.modules.network.vlans;
in in
{ {
options.modules.network.vlans = { options.modules.network.vlans = {

View File

@@ -6,7 +6,7 @@
... ...
}: }:
let let
cfg = config.modules.network.vpn-client; # NOLINT(let_order) cfg = config.modules.network.vpn-client;
vpnIcon = util.glyph [ vpnIcon = util.glyph [
"db83" "db83"

View File

@@ -6,7 +6,7 @@
... ...
}: }:
let let
cfg = config.modules.services.copyparty; # NOLINT(let_order) cfg = config.modules.services.copyparty;
accountName = config.user.name; accountName = config.user.name;

View File

@@ -92,33 +92,33 @@
startScript = pkgs.writeShellScript "palworld-start" '' startScript = pkgs.writeShellScript "palworld-start" ''
set -eu set -eu
${pkgs.coreutils}/bin/install -d \ ${lib.getExe' pkgs.coreutils "install"} -d \
-m 0770 \ -m 0770 \
${lib.escapeShellArg service.stateDir} \ ${lib.escapeShellArg service.stateDir} \
${lib.escapeShellArg (builtins.dirOf savedDir)} ${lib.escapeShellArg (builtins.dirOf savedDir)}
if [ -L ${lib.escapeShellArg savedDir} ]; then if [ -L ${lib.escapeShellArg savedDir} ]; then
current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})" current_target="$(${lib.getExe' pkgs.coreutils "readlink"} ${lib.escapeShellArg savedDir})"
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir} ${lib.getExe' pkgs.coreutils "rm"} ${lib.escapeShellArg savedDir}
fi fi
fi fi
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
if ${pkgs.findutils}/bin/find ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${pkgs.gnugrep}/bin/grep -q .; then if ${lib.getExe' pkgs.findutils "find"} ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${lib.getExe' pkgs.gnugrep "grep"} -q .; then
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir} ${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
else else
${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/ ${lib.getExe' pkgs.coreutils "cp"} -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir} ${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
fi fi
fi fi
if [ ! -e ${lib.escapeShellArg savedDir} ]; then if [ ! -e ${lib.escapeShellArg savedDir} ]; then
${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir} ${lib.getExe' pkgs.coreutils "ln"} -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
fi fi
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})" password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})" admin_password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
exec ./PalServer.sh \ exec ./PalServer.sh \
"$@" \ "$@" \
@@ -145,9 +145,12 @@
] ]
++ lib.optional service.publicLobby "-publiclobby" ++ lib.optional service.publicLobby "-publiclobby"
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ] ++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
++ lib.optional service.rcon.enable "-rcon"; ++ lib.optionals service.rcon.enable [
"-rcon"
"-RCONPort=${toString service.rcon.port}"
];
udpPorts = [ service.port ]; udpPorts = [ service.port ];
tcpPorts = [ service.port ]; tcpPorts = lib.optional service.rcon.enable service.rcon.port;
}; };
} }
); );

View File

@@ -51,7 +51,7 @@
startScript = pkgs.writeShellScript "valheim-start" '' startScript = pkgs.writeShellScript "valheim-start" ''
set -eu set -eu
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})" password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
exec ./valheim_server.x86_64 "$@" -password "$password" exec ./valheim_server.x86_64 "$@" -password "$password"
''; '';
in in

View File

@@ -5,7 +5,7 @@
... ...
}: }:
let let
cfg = config.modules.services.homepage; # NOLINT(let_order) cfg = config.modules.services.homepage;
derivedAllowedHosts = lib.unique ( derivedAllowedHosts = lib.unique (
lib.optional (selfRoute != null) selfRoute.domain lib.optional (selfRoute != null) selfRoute.domain

View File

@@ -4,7 +4,7 @@
... ...
}: }:
let let
cfg = config.modules.services.navidrome; # NOLINT(let_order) cfg = config.modules.services.navidrome;
in in
{ {
options.modules.services.navidrome = { options.modules.services.navidrome = {

View File

@@ -40,43 +40,14 @@ let
proxy_pass ${stream.address}:${toString stream.port}; proxy_pass ${stream.address}:${toString stream.port};
} }
'') generatedUdpStreams; '') generatedUdpStreams;
generatedVirtualHosts = generatedVirtualHosts = lib.listToAttrs generatedVirtualHostList;
if duplicateDomainMessages != [ ] then
throw (builtins.head duplicateDomainMessages)
else
lib.listToAttrs generatedVirtualHostList;
duplicateDomainMessages = lib.concatLists ( # Route registry validation already requires unique domains.
lib.mapAttrsToList ( generatedVirtualHostList = lib.concatLists (
domain: virtualHosts:
if lib.length virtualHosts > 1 then
[
"duplicate route domain ${domain} in ${
lib.concatMapStringsSep " and " (
virtualHost: mkRegistryPath virtualHost.registryName virtualHost.host virtualHost.entry
) virtualHosts
}"
]
else
[ ]
) virtualHostsByDomain
);
virtualHostsByDomain = lib.foldl' (
acc: virtualHost:
acc
// {
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
}
) { } generatedVirtualHostList;
generatedVirtualHostList = generatedHttpsVirtualHostList ++ generatedHttpVirtualHostList;
generatedHttpsVirtualHostList = lib.concatLists (
lib.mapAttrsToList ( lib.mapAttrsToList (
host: routes: host: routes:
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") ( lib.mapAttrsToList (entry: route: mkVirtualHost host entry route.protocol) (
lib.filterAttrs (_entry: route: route.protocol == "https") routes lib.filterAttrs (_entry: route: route.protocol != "udp") routes
) )
) registry.route ) registry.route
); );
@@ -93,8 +64,6 @@ let
inherit (route) port; inherit (route) port;
in in
{ {
inherit host entry;
registryName = "route";
name = route.domain; name = route.domain;
value = { value = {
forceSSL = !isHttp; forceSSL = !isHttp;
@@ -108,15 +77,6 @@ let
mkBackend = address: port: "http://${address}:${toString port}"; mkBackend = address: port: "http://${address}:${toString port}";
generatedHttpVirtualHostList = lib.concatLists (
lib.mapAttrsToList (
host: routes:
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "http") (
lib.filterAttrs (_entry: route: route.protocol == "http") routes
)
) registry.route
);
mkRegistryPath = mkRegistryPath =
registryName: host: entry: registryName: host: entry:
"registry.${registryName}.${host}.${entry}"; "registry.${registryName}.${host}.${entry}";

View File

@@ -0,0 +1,47 @@
{
config,
lib,
...
}:
let
cfg = config.modules.services.romm;
in
{
options.modules.services.romm = {
enable = lib.mkEnableOption "RomM";
environmentFile = lib.mkOption {
type = lib.types.path;
default = "/run/secrets/romm.env";
readOnly = true;
description = "Path to RomM's environment file.";
};
};
config = lib.mkMerge [
{
registry."stable-uid".${config.networking.hostName} = {
romm = 20005;
"redis-romm" = 20006;
};
}
(lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [ 80 ];
services.romm = {
enable = true;
group = "storage";
database.createLocally = true;
redis.createLocally = true;
watcher.enable = true;
inherit (cfg) environmentFile;
};
users.users = {
romm.uid = config.registry."stable-uid".${config.networking.hostName}.romm;
redis-romm.uid = config.registry."stable-uid".${config.networking.hostName}."redis-romm";
};
})
];
}

View File

@@ -15,9 +15,9 @@
}; };
serviceName = lib.mkOption { serviceName = lib.mkOption {
type = lib.types.str; type = lib.types.strMatching "[a-z0-9][a-z0-9-]*";
example = "valheim"; example = "valheim";
description = "Name of the systemd service that runs the game server."; description = "Lowercase systemd service basename for the game server.";
}; };
appId = lib.mkOption { appId = lib.mkOption {
@@ -87,7 +87,7 @@
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" '' prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
set -eu set -eu
${pkgs.coreutils}/bin/install -d \ ${lib.getExe' pkgs.coreutils "install"} -d \
-o ${user} \ -o ${user} \
-g ${group} \ -g ${group} \
-m 0770 \ -m 0770 \
@@ -155,7 +155,7 @@
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ]; wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
ExecStart = "${pkgs.systemd}/bin/systemctl --no-block start ${service.serviceName}.service"; ExecStart = "${lib.getExe' pkgs.systemd "systemctl"} --no-block start ${service.serviceName}.service";
}; };
}; };

View File

@@ -5,14 +5,11 @@
... ...
}: }:
# Bazel for the primary user's shell environment. # Bazel for the primary user's shell environment.
let
user = config.user.name;
in
{ {
options.modules.shell.bazel.enable = options.modules.shell.bazel.enable =
lib.mkEnableOption "Bazel for the primary user's shell environment"; lib.mkEnableOption "Bazel for the primary user's shell environment";
config = lib.mkIf config.modules.shell.bazel.enable { config = lib.mkIf config.modules.shell.bazel.enable {
home-manager.users.${user}.home.packages = [ pkgs.bazel ]; home-manager.users.${config.user.name}.home.packages = [ pkgs.bazel ];
}; };
} }

View File

@@ -1,25 +0,0 @@
{
config,
lib,
...
}:
# direnv for the primary user, configured through home-manager.
let
user = config.user.name;
in
{
options.modules.shell.direnv.enable =
lib.mkEnableOption "direnv for the primary user, with nix-direnv shell caching";
config = lib.mkIf config.modules.shell.direnv.enable {
home-manager.users.${user}.programs.direnv = {
enable = true;
silent = true;
# nix-direnv caches the evaluated shell so re-entering a directory is instant
# instead of re-running `nix develop`, and adds the `use flake` stdlib helper.
nix-direnv.enable = true;
stdlib = builtins.readFile ./repo-flake.sh;
};
};
}

View File

@@ -1,33 +0,0 @@
if declare -f use_flake >/dev/null; then
__dotfiles_use_flake_definition="$(declare -f use_flake)"
eval "${__dotfiles_use_flake_definition/use_flake/__dotfiles_nix_direnv_use_flake}"
unset __dotfiles_use_flake_definition
use_flake() {
local flake_expr="${1:-.}"
if [[ "$flake_expr" == //* ]]; then
local git_root
if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
log_error 'use flake // requires a Git repository'
return 1
fi
local flake_suffix="${flake_expr#//}"
if [[ -n "$flake_suffix" && "$flake_suffix" != \#* && "$flake_suffix" != /* ]]; then
flake_expr="path:${git_root}/${flake_suffix}"
else
flake_expr="path:${git_root}${flake_suffix}"
fi
shift
__dotfiles_nix_direnv_use_flake "$flake_expr" "$@"
return
fi
__dotfiles_nix_direnv_use_flake "$@"
}
else
log_error 'nix-direnv must define use_flake before the // wrapper loads'
return 1
fi

View File

@@ -6,7 +6,7 @@
}: }:
# fish for the primary user, configured through home-manager. # fish for the primary user, configured through home-manager.
let let
cfg = config.modules.shell.fish; # NOLINT(let_order) cfg = config.modules.shell.fish;
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts; commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut; shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
user = config.user.name; user = config.user.name;

View File

@@ -24,6 +24,9 @@ in
home-manager.users.${user} = hm: { home-manager.users.${user} = hm: {
programs.nixvim = { programs.nixvim = {
enable = true; enable = true;
# Aligns Nixvim plugins with this flake's `Nixpkgs`.
# `nixpkgs.useGlobalPackages` prevents Home Manager from creating a separate package set.
nixpkgs.useGlobalPackages = true; nixpkgs.useGlobalPackages = true;
extraPackages = with pkgs; [ extraPackages = with pkgs; [

View File

@@ -12,7 +12,7 @@ let
in in
{ {
# Next stable UID: 20005 # Next stable UID: 20007
type = lib.types.ints.positive; type = lib.types.ints.positive;

View File

@@ -6,6 +6,17 @@ services:
lastfm: lastfm:
api-key: ENC[AES256_GCM,data:Zb0I5TpdEqOxLAD6ed2XywFGZp7dfY69vcGiWOUJkJQ=,iv:pO53pHr09jWfzJyTsQbTHtzTmiQ9+RQkDwZJmInIiX8=,tag:io9jkSN7TF2I8+qlSymaYw==,type:str] api-key: ENC[AES256_GCM,data:Zb0I5TpdEqOxLAD6ed2XywFGZp7dfY69vcGiWOUJkJQ=,iv:pO53pHr09jWfzJyTsQbTHtzTmiQ9+RQkDwZJmInIiX8=,tag:io9jkSN7TF2I8+qlSymaYw==,type:str]
secret: ENC[AES256_GCM,data:rKhd9i0/tbJlXhc9Wf81IDydjpD8khSdwJEEMK260E4=,iv:btBXWBKJp9br5a3yZsRVegN8Ytzc45Qbk2iNlPbENkQ=,tag:LUbu+6gxfqQvVpddZto7Tg==,type:str] secret: ENC[AES256_GCM,data:rKhd9i0/tbJlXhc9Wf81IDydjpD8khSdwJEEMK260E4=,iv:btBXWBKJp9br5a3yZsRVegN8Ytzc45Qbk2iNlPbENkQ=,tag:LUbu+6gxfqQvVpddZto7Tg==,type:str]
romm:
igdb:
client-id: ENC[AES256_GCM,data:tme7NzzbOlCmkMwVObWz793mOsCyVIzSjGoFvxpe,iv:5aOMvsDi8Z9aBymbqhbK79X4kJsqaET1y2Py6YUZZ/U=,tag:ih3GghlxiLsVyeTJoqU9/Q==,type:str]
client-secret: ENC[AES256_GCM,data:AqF7B9VaxM/X3sRqY6C5JVWPWqgRIoqy09P9ZUFj,iv:D+vDPUuGGdzKYnTVTRkI9TV/EOJgVECIeNDt9g8NGl0=,tag:MRaTT7TdEZSd+98xthVw1A==,type:str]
retroachievements:
api-key: ENC[AES256_GCM,data:LMzETQCMi9Ib783W0BZDe2PaSUSTVM5a61502RtZLg8=,iv:Hf5nBoVtZdJszQuc2n0LVNMtJCCpJrh40y4g+l+az70=,tag:esfKLpI1W8BG3Pfsg3xxKQ==,type:str]
steamgriddb:
api-key: ENC[AES256_GCM,data:+kBRieIB/dHbvQLbH2gQTQh2j6DQhybVWEGkSCRZrZY=,iv:3h6dhfiV7HfaZhMv93x8+7F3utzYG6fIbTDScA14PVI=,tag:Wo24YnvsIxe7iMRLyxrj2w==,type:str]
screenscraper:
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]
sops: sops:
age: age:
- enc: | - enc: |
@@ -26,7 +37,7 @@ sops:
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA== dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
lastmodified: "2026-09-12T22:54:25Z" lastmodified: "2026-10-20T01:27:06Z"
mac: ENC[AES256_GCM,data:4R3a0ytf4i8aM8iBQ9vRC/q+hvtjLrCTNgbpCB5DbqBMBYmhtGsBmO/0i1yT0N4tydyLaGOMkNfuuuVi3ASLEOKnViy9w3uD9IN0F79W4f2vguWUVIp/T8skvN9wbV9o9xd0LkJzmeVVgzb1H6HKw+rTGjbquleoIHEBDcDDM+k=,iv:0NlckMXlS+idY6UGBF+F0KFLt+pzD7Q/YEQOdXraAYg=,tag:JBBQduV8uhnKz9gPf6HfAQ==,type:str] mac: ENC[AES256_GCM,data:NCt6e/ctZwaBADfrhu1RXtILlClbHEqOAnIu8rE/zMPSwMIPB/kTtfd/ujf+1iRszoHaX4Se1xc0wz1th6NVToFVNarxBBgTk4JMUU8gdMweOjmwlh3GXU3ZcYY/xAXxWF1hxyXSEW78j3S2844H9XXENAq/Bf3mjdCYQ/qr/d4=,iv:4nFyBSmRvF4zSAwvL+xccMwtWrVbHtFKNoTb7f3rSH4=,tag:eCV/7P0TjyMZXKcWBUPXQA==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.13.3 version: 3.13.3

View File

@@ -5,7 +5,7 @@
"livePreview": false, "livePreview": false,
"readableLineLength": false, "readableLineLength": false,
"strictLineBreaks": true, "strictLineBreaks": true,
"propertiesInDocument": "hidden", "propertiesInDocument": "visible",
"showLineNumber": true, "showLineNumber": true,
"autoPairMarkdown": false, "autoPairMarkdown": false,
"useTab": false, "useTab": false,

View File

@@ -5,5 +5,6 @@
"obsidian-style-settings", "obsidian-style-settings",
"custom-font-loader", "custom-font-loader",
"obsidian-icon-folder", "obsidian-icon-folder",
"table-editor-obsidian" "table-editor-obsidian",
"obsidian-linter"
] ]

View File

@@ -28,6 +28,20 @@
"rgb": 5366249 "rgb": 5366249
} }
}, },
{
"query": "path:\"03 wrk/tasks\"[\"status\":open]",
"color": {
"a": 1,
"rgb": 14725458
}
},
{
"query": "path:\"03 wrk/tasks\" [\"status\":closed]",
"color": {
"a": 1,
"rgb": 11657298
}
},
{ {
"query": "path:\"03 wrk\" ", "query": "path:\"03 wrk\" ",
"color": { "color": {
@@ -60,6 +74,6 @@
"repelStrength": 20, "repelStrength": 20,
"linkStrength": 1, "linkStrength": 1,
"linkDistance": 250, "linkDistance": 250,
"scale": 0.3645607001522557, "scale": 0.16202697784544698,
"close": false "close": false
} }

View File

@@ -0,0 +1,326 @@
{
"ruleConfigs": {
"add-blank-line-after-yaml": {
"enabled": true
},
"dedupe-yaml-array-values": {
"enabled": true,
"dedupe-alias-key": true,
"dedupe-tag-key": true,
"dedupe-array-keys": true,
"ignore-keys": []
},
"escape-yaml-special-characters": {
"enabled": true,
"try-to-escape-single-line-arrays": true
},
"force-yaml-escape": {
"enabled": true,
"force-yaml-escape-keys": []
},
"format-tags-in-yaml": {
"enabled": true
},
"format-yaml-array": {
"enabled": true,
"alias-key": true,
"tag-key": true,
"default-array-keys": true,
"force-single-line-array-style": [],
"force-multi-line-array-style": []
},
"insert-yaml-attributes": {
"enabled": true,
"text-to-insert": []
},
"move-inline-fields-to-yaml": {
"enabled": true,
"how-to-handle-full-line-fields": "Move and remove",
"how-to-handle-bracketed-fields": "Leave in place",
"how-to-handle-existing-keys": "Skip",
"inline-keys-to-ignore": []
},
"move-tags-to-yaml": {
"enabled": true,
"how-to-handle-existing-tags": "Nothing",
"tags-to-ignore": []
},
"remove-yaml-keys": {
"enabled": true,
"yaml-keys-to-remove": []
},
"sort-yaml-array-values": {
"enabled": true,
"sort-alias-key": true,
"sort-tag-key": true,
"sort-array-keys": true,
"ignore-keys": [],
"sort-order": "Ascending Alphabetical"
},
"yaml-key-sort": {
"enabled": true,
"yaml-key-priority-sort-order": [
"aliases",
"date-created",
"date-modified"
],
"priority-keys-at-start-of-yaml": true,
"yaml-sort-order-for-other-keys": "None"
},
"yaml-timestamp": {
"enabled": true,
"date-created": true,
"date-created-key": "date-created",
"date-created-source-of-truth": "file system",
"date-modified": true,
"date-modified-key": "date-modified",
"date-modified-source-of-truth": "file system",
"format": "YYYY-MM-DD HH:mm",
"convert-to-utc": false,
"update-on-file-contents-updated": "after 5 seconds"
},
"yaml-title": {
"enabled": false,
"title-key": "title",
"mode": "first-h1-or-filename-if-h1-missing"
},
"yaml-title-alias": {
"enabled": true,
"preserve-existing-alias-section-style": true,
"keep-alias-that-matches-the-filename": false,
"use-yaml-key-to-keep-track-of-old-filename-or-heading": false,
"alias-helper-key": "linter-yaml-title-alias",
"remove-alias-if-empty": true
},
"capitalize-headings": {
"enabled": true,
"style": "Title Case",
"ignore-case-words": [],
"lowercase-words": [],
"starting-word-ignore-characters": "'\"(‘“-",
"ending-word-ignore-characters": ".?!,:;'\")”’0123456789-",
"ignore-words": []
},
"file-name-heading": {
"enabled": true
},
"header-increment": {
"enabled": true,
"start-at-h2": false
},
"headings-start-line": {
"enabled": true
},
"remove-trailing-punctuation-in-heading": {
"enabled": true,
"punctuation-to-remove": ".,;:!。,;:!"
},
"footnote-after-punctuation": {
"enabled": true
},
"move-footnotes-to-the-bottom": {
"enabled": true,
"include-blank-line-between-footnotes": false
},
"re-index-footnotes": {
"enabled": true
},
"auto-correct-common-misspellings": {
"enabled": true,
"ignore-words": [],
"skip-words-with-multiple-capitals": true,
"extra-auto-correct-files": []
},
"blockquote-style": {
"enabled": true,
"style": "space"
},
"convert-bullet-list-markers": {
"enabled": true
},
"default-language-for-code-fences": {
"enabled": true,
"default-language": ""
},
"emphasis-style": {
"enabled": true,
"style": "consistent"
},
"no-bare-urls": {
"enabled": true,
"no-bare-uris": true
},
"ordered-list-style": {
"enabled": true,
"number-style": "ascending",
"list-end-style": ".",
"preserve-start": false
},
"proper-ellipsis": {
"enabled": true
},
"quote-style": {
"enabled": true,
"single-quote-enabled": true,
"single-quote-style": "''",
"double-quote-enabled": true,
"double-quote-style": "\"\""
},
"remove-consecutive-list-markers": {
"enabled": true
},
"remove-empty-list-markers": {
"enabled": true
},
"remove-hyphenated-line-breaks": {
"enabled": true
},
"remove-multiple-spaces": {
"enabled": true
},
"strong-style": {
"enabled": true,
"style": "consistent"
},
"two-spaces-between-lines-with-content": {
"enabled": false,
"line-break-indicator": " "
},
"unordered-list-style": {
"enabled": true,
"list-style": "consistent"
},
"align-table-columns": {
"enabled": true
},
"compact-yaml": {
"enabled": true,
"inner-new-lines": false
},
"consecutive-blank-lines": {
"enabled": true
},
"convert-spaces-to-tabs": {
"enabled": true,
"tabsize": 4
},
"empty-line-around-blockquotes": {
"enabled": true
},
"empty-line-around-code-fences": {
"enabled": true
},
"empty-line-around-horizontal-rules": {
"enabled": true
},
"empty-line-around-math-blocks": {
"enabled": true
},
"empty-line-around-tables": {
"enabled": true
},
"heading-blank-lines": {
"enabled": true,
"bottom": true,
"empty-line-after-yaml": true
},
"line-break-at-document-end": {
"enabled": true
},
"move-math-block-indicators-to-their-own-line": {
"enabled": true
},
"paragraph-blank-lines": {
"enabled": true
},
"remove-empty-lines-between-list-markers-and-checklists": {
"enabled": true
},
"remove-link-spacing": {
"enabled": true
},
"remove-space-around-characters": {
"enabled": true,
"include-fullwidth-forms": true,
"include-cjk-symbols-and-punctuation": true,
"include-dashes": true,
"other-symbols": ""
},
"remove-space-before-or-after-characters": {
"enabled": true,
"characters-to-remove-space-before": ",!?;:).’”]",
"characters-to-remove-space-after": "¿¡‘“(["
},
"space-after-list-markers": {
"enabled": true
},
"space-between-chinese-japanese-or-korean-and-english-or-numbers": {
"enabled": true,
"english-symbols-punctuation-before": "-+;:'\"°%$)]",
"english-symbols-punctuation-after": "-+'\"([¥$"
},
"trailing-spaces": {
"enabled": true,
"two-space-line-break": false
},
"add-blockquote-indentation-on-paste": {
"enabled": true
},
"prevent-double-checklist-indicator-on-paste": {
"enabled": true
},
"prevent-double-list-item-indicator-on-paste": {
"enabled": true
},
"proper-ellipsis-on-paste": {
"enabled": true
},
"remove-hyphens-on-paste": {
"enabled": true
},
"remove-leading-or-trailing-whitespace-on-paste": {
"enabled": true
},
"remove-leftover-footnotes-from-quote-on-paste": {
"enabled": true
},
"remove-multiple-blank-lines-on-paste": {
"enabled": true
}
},
"lintOnSave": true,
"recordLintOnSaveLogs": false,
"displayChanged": true,
"suppressMessageWhenNoChange": true,
"enableDiffPreviewView": true,
"suppressLintAllFilesConfirmationModal": false,
"suppressLintAllFilesInFolderConfirmationModal": false,
"lintOnFileChange": false,
"displayLintOnFileChangeNotice": false,
"settingsConvertedToConfigKeyValues": true,
"textAreaSettingsConvertedToListItemSettings": true,
"defaultArrayStyleMovedToCommonStyles": true,
"additionalFileExtensions": [],
"foldersToIgnore": [
"99 meta/templates"
],
"filesToIgnore": [
{
"label": "todo",
"match": "TODO.md",
"flags": "i"
}
],
"linterLocale": "system-default",
"logLevel": "ERROR",
"lintCommands": [],
"customRegexes": [],
"commonStyles": {
"aliasArrayStyle": "multi-line",
"tagArrayStyle": "single-line",
"defaultArrayStyle": "multi-line",
"minimumNumberOfDollarSignsToBeAMathBlock": 2,
"escapeCharacter": "\"",
"removeUnnecessaryEscapeCharsForMultiLineArrays": true
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
{
"id": "obsidian-linter",
"name": "Linter",
"version": "1.34.1",
"minAppVersion": "1.13.0",
"description": "Formats and styles your notes. It can be used to format YAML tags, aliases, arrays, and metadata; footnotes; headings; spacing; math blocks; regular markdown contents like list, italics, and bold styles; and more with the use of custom rule options as well.",
"author": "Victor Tao",
"authorUrl": "https://github.com/platers",
"helpUrl": "https://platers.github.io/obsidian-linter/",
"isDesktopOnly": false
}

View File

@@ -0,0 +1 @@
.linter-no-border{border:none}.linter-border-bottom{border-bottom:1px solid var(--background-modifier-border);border-top:0;margin-bottom:.75em}.custom-row-description{margin-top:0}.modal-warn,.search-zero-state{font-weight:700}.modal-heading,.search-zero-state{text-align:center}.confirm-modal-checkbox-container{align-items:center;display:flex;gap:6px;margin-top:8px}.disabled-list-entry{color:var(--text-faint)}.linter-diff-preview-view{display:flex;flex-direction:column;height:100%;min-width:0;overflow:hidden}.linter-diff-preview-empty{color:var(--text-muted);padding:16px}.linter-diff-preview-header{align-items:flex-start;border-bottom:1px solid var(--background-modifier-border);display:flex;flex:0 0 auto;gap:12px;justify-content:space-between;padding:12px 14px}.linter-diff-preview-title-group{min-width:0}.linter-diff-preview-header h3{font-size:var(--font-ui-medium);line-height:1.3;margin:0 0 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.linter-diff-preview-actions{display:flex;flex:0 0 auto;gap:8px}.linter-diff-preview-body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px 16px}.linter-diff-preview-view .linter-diff-summary{color:var(--text-normal);display:inline-block;font-size:var(--font-ui-small);margin:0 0 10px;padding:5px 8px}.linter-diff-preview-view .linter-diff,.linter-diff-preview-view .linter-diff-summary{background:var(--background-secondary);border:1px solid var(--background-modifier-border);border-radius:6px}.linter-diff-preview-view .linter-diff{font-family:var(--font-monospace);font-size:var(--font-smaller);line-height:1.45;margin:0;overflow:hidden;padding:6px 0}.linter-diff-preview-view .linter-diff-line{display:grid;grid-template-columns:26px minmax(0,1fr);min-height:20px;padding:1px 10px 1px 0}.linter-diff-preview-view .linter-diff-marker{color:var(--text-muted);padding-right:8px;text-align:right;user-select:none}.linter-diff-preview-view .linter-diff-text{min-width:0;overflow-wrap:anywhere;white-space:pre-wrap}.linter-diff-preview-view .linter-diff-skip{border-bottom:1px solid var(--background-modifier-border);border-top:1px solid var(--background-modifier-border);color:var(--text-faint);font-family:var(--font-interface);font-size:var(--font-ui-smaller);padding:4px 12px;text-align:center}.linter-diff-preview-view .linter-diff-added{background:rgba(var(--color-green-rgb),.12);color:var(--text-normal)}.linter-diff-preview-view .linter-diff-removed{background:rgba(var(--color-red-rgb),.12);color:var(--text-normal)}.linter-diff-preview-view .linter-diff-context{color:var(--text-muted)}

View File

@@ -4,21 +4,21 @@
"type": "split", "type": "split",
"children": [ "children": [
{ {
"id": "6d9088d392cb8604", "id": "81c4860f495113bf",
"type": "tabs", "type": "tabs",
"children": [ "children": [
{ {
"id": "22a4c1f201a1b00e", "id": "daab69fc3faab88f",
"type": "leaf", "type": "leaf",
"state": { "state": {
"type": "markdown", "type": "markdown",
"state": { "state": {
"file": "00 INBOX/ai/valheim-items.md", "file": "00 INBOX/credits.md",
"mode": "preview", "mode": "source",
"source": true "source": true
}, },
"icon": "lucide-file", "icon": "lucide-file",
"title": "valheim-items" "title": "credits"
} }
} }
] ]
@@ -91,7 +91,7 @@
"state": { "state": {
"type": "localgraph", "type": "localgraph",
"state": { "state": {
"file": "00 INBOX/ai/valheim-items.md", "file": "00 INBOX/credits.md",
"options": { "options": {
"collapse-filter": true, "collapse-filter": true,
"search": "", "search": "",
@@ -115,7 +115,7 @@
"linkStrength": 1, "linkStrength": 1,
"linkDistance": 250, "linkDistance": 250,
"scale": 0.6666666666666666, "scale": 0.6666666666666666,
"close": true "close": false
} }
}, },
"icon": "lucide-git-fork", "icon": "lucide-git-fork",
@@ -184,20 +184,6 @@
"title": "tag" "title": "tag"
} }
}, },
{
"id": "8918bf52a32e4a8e",
"type": "leaf",
"state": {
"type": "all-properties",
"state": {
"sortOrder": "frequency",
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-ghost",
"title": "all-properties"
}
},
{ {
"id": "c10908b3bd97f5fa", "id": "c10908b3bd97f5fa",
"type": "leaf", "type": "leaf",
@@ -231,43 +217,43 @@
"table-editor-obsidian:Advanced Tables Toolbar": false "table-editor-obsidian:Advanced Tables Toolbar": false
} }
}, },
"active": "22a4c1f201a1b00e", "active": "daab69fc3faab88f",
"lastOpenFiles": [ "lastOpenFiles": [
"00 INBOX/ai/hyprland-lua-migration-plan.md", "00 INBOX/ai/tldr-design.md",
"00 INBOX/ai/deadnix-architecture.md", "00 INBOX/ai/caps-escape-design.md",
"00 INBOX/ai/valheim-items.md",
"TODO.md",
"AGENTS.md", "AGENTS.md",
"00 INBOX/ai/tldr-design-tree.md",
"00 INBOX/ai/grill-skill-design.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-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", "technical-writing.md",
"rust-module-prototype.md", "dictionary.md",
"00 INBOX/ai/statix-architecture.md", "03 wrk/tasks/0007-obsidian-source-notes.md",
"00 INBOX/ai/nix-statix-deadnix-rules.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",
"00 INBOX/let order name resolution.md~", "00 INBOX/let order name resolution.md~",
"00 INBOX/nix name resolution.md",
"00 INBOX/let order name resolution.md",
"00 INBOX/nix name resolution.md~", "00 INBOX/nix name resolution.md~",
"TODO.md~", "TODO.md~",
"00 INBOX/ai/xatu-rule-implementation-order.md",
"00 INBOX/ai/xatu-architecture-design.md",
"00 INBOX/ai/nix-style-linter-plan.md",
"00 INBOX/ai/nix-style-guide-agent-customization.md",
"00 INBOX/ai/game-guest-readiness-plan.md",
"00 INBOX/ai/xatu-node-rule-static-dispatch-plan.md",
"00 INBOX/ai/nix-style-linter-pr-1-vendor-rnix.md",
"00 INBOX/ai/nix-style-linter-pr-3-let-order.md",
"00 INBOX/ai/nix-style-linter-pr-2-scaffold-cli.md",
"00 INBOX/ai/homepage-pikachu-setup-plan.md",
"00 INBOX/ai/source-driven-remote-deploy-plan.md",
"00 INBOX/ai/monorepo-work-report.md",
"00 INBOX/ai/clew-ast-exploration.md",
"00 INBOX/ai/steamcmd servers.md",
"00 INBOX/ai/clew-ast-exploration.md~", "00 INBOX/ai/clew-ast-exploration.md~",
"03 wrk/machoke.md~", "03 wrk/machoke.md~",
"03 wrk/falinks.md~", "03 wrk/falinks.md~",
"99 meta/attachments/bases/unlinked mentions.base", "99 meta/attachments/bases/unlinked mentions.base",
"00 INBOX/ai", "00 INBOX/ai",
"03 wrk/ai", "03 wrk/ai"
"99 meta/Untitled"
] ]
} }

View File

@@ -0,0 +1,39 @@
# 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.

View File

@@ -1,304 +0,0 @@
# Deadnix Architecture
## Purpose
This note explains how `deadnix` finds unused Nix code.
It focuses on architecture, not whether each check belongs in our linter.
Use this note when deciding whether our linter needs scope analysis.
## What Deadnix Is
`deadnix` is a dead-code scanner for Nix.
It finds bindings that introduce names which no live code uses.
It is not a general style linter.
It does not have many independent rules.
It implements one analysis family: unused declarations.
Like `statix`, it parses source text with `rnix`.
It does not evaluate Nix.
It does not import files.
It reasons over local syntax and local scope only.
## Minimal Dead-Code Vocabulary
- A binding introduces a name.
- A scope defines where introduced names are visible.
- A use is an identifier occurrence that refers to a binding.
- Shadowing happens when an inner scope reuses an outer name.
- Dead code is a binding that no live code uses.
- A fixed point is the final result after repeated analysis no longer changes.
Dead-code analysis needs more state than a simple syntax lint.
It must know which names a scope introduces and where those names can be used.
## Component Map
`deadnix` has fewer layers than `statix`.
The command-line entry point, file walk, parsing, analysis, reporting, and editing live in one small tool.
The important concepts are separate modules:
- settings control analysis options;
- scopes describe where names are introduced;
- bindings describe individual declared names;
- usage search answers whether a name appears in a live body;
- dead-code results connect a binding to its scope;
- reports render findings;
- edits remove or rename unused declarations.
## Main Pipeline
```mermaid
flowchart TD
A[Parse CLI options] --> B[Collect input files]
B --> C[Read each file]
C --> D[Parse with rnix]
D --> E[Skip file on parse errors]
D --> F[Run dead-code analysis]
F --> G[Print reports unless quiet]
G --> H[Apply edits when --edit is set]
H --> I[Exit 1 when --fail and findings exist]
```
The CLI accepts files or directories.
Directory mode walks `.nix` files.
Hidden files require an explicit option.
Excludes filter paths before analysis.
The tool skips dead-code diagnostics when a file has parse errors.
It reports parse errors to the user and continues with other files.
## Analysis Settings
The analysis has four important switches.
`no_lambda_arg` disables checks for single lambda arguments.
This can avoid noise when an API requires an argument name.
`no_lambda_pattern_names` disables checks for names in attribute-set function patterns.
This matters for `callPackage`-style code, where argument names can be external API.
`no_underscore` ignores bindings that start with `_`.
This treats `_name` as an intentional unused placeholder.
`warn_used_underscore` reports underscore-prefixed bindings that code actually uses.
This enforces the convention that `_name` means unused.
These settings are architecture-relevant because dead-code policy depends on API context.
A repository linter needs a way to express that context.
## Scope Model
`deadnix` recognizes four scope types.
### Lambda pattern scope
A lambda pattern is a function argument written as an attribute set pattern.
It can introduce field names and an optional alias for the whole argument set.
Example shape:
```nix
args @ { pkgs, lib, ... }: body
```
This scope can introduce `args`, `pkgs`, and `lib`.
### Lambda argument scope
A single lambda argument introduces one name.
Example shape:
```nix
pkgs: body
```
This scope introduces `pkgs`.
### Let scope
A `let` expression introduces bindings and inherited names.
Those names are available in binding expressions and in the body.
Example shape:
```nix
let
inherit (pkgs) hello;
value = hello;
in
value
```
This scope introduces `hello` and `value`.
### Recursive attribute-set scope
A `rec` attribute set introduces names that sibling attributes can reference.
`deadnix` models this scope, but treats its bindings as non-removable.
That conservative choice avoids deleting public-looking attributes from recursive sets.
## Binding Model
A binding stores three facts.
It stores the identifier name.
It stores the syntax node that declared the name.
It stores whether the binding is mortal.
A mortal binding can become a dead-code finding.
A non-mortal binding participates in name lookup but will not be removed.
Single lambda arguments that already start with `_` are non-mortal.
Recursive attribute-set bindings are also non-mortal.
A binding also checks for the `deadnix: skip` pragma.
The pragma applies to a nearby following declaration.
The scanner searches backward from the declaration and stops after a blank-line boundary.
## Usage Search
Usage search asks whether a given name appears inside a syntax subtree.
The search is scope-aware.
When it enters a child scope that binds the same name, it stops following that branch.
That prevents an inner binding from counting as a use of an outer binding.
Attribute paths get special handling.
Ordinary attribute keys do not count as uses.
Dynamic or string attribute names can contain expressions, so the search looks inside those parts.
`inherit` also gets special handling.
A scope can inherit names from an outer scope.
The analysis checks whether the inherited expression uses the searched name.
## Fixed-Point Analysis
`deadnix` repeats the scan until the result set stops changing.
This matters because dead code can depend on other dead code.
A binding can appear used during the first pass because another binding references it.
If the referring binding is dead, the referenced binding is also dead.
The loop tracks dead declaration nodes.
During later scans, uses inside already-dead bodies do not keep other bindings alive.
This fixed-point loop is the central architectural difference from simple local syntax lints.
It makes dead-code results transitive.
## Reporting Model
A result stores the scope and binding for one finding.
The display text combines both parts.
Examples include unused lambda pattern, unused lambda argument, and unused let binding.
Human output uses `ariadne`.
The reporter groups ranges by line and labels the exact binding names.
It uses different colors for different scope kinds.
JSON output exists behind a feature flag.
It emits file, message, line, column, and end column.
`NO_COLOR` disables colored human output.
## Edit Model
`deadnix --edit` converts findings into text edits.
Each edit has a start offset, end offset, and replacement string.
The edit behavior depends on scope kind.
For unused lambda pattern names, it removes the name and nearby comma or whitespace.
For unused pattern aliases, it removes the alias syntax.
For unused lambda arguments, it renames the argument with an underscore prefix.
For unused `let` assignments, it deletes the assignment.
For unused inherited names, it deletes only the inherited name.
After those edits, it parses the edited source again.
Then it removes empty `let in` constructs and empty `inherit` statements.
This cleanup pass prevents the first edit batch from leaving invalid or useless scaffolding.
## Error Handling
If a path cannot be stated or read, the CLI reports the file error and continues.
If a file has parse errors, the CLI reports those errors and skips dead-code analysis for that file.
The `--fail` flag changes exit status when findings exist.
Without `--fail`, findings do not make the command fail.
This design separates report generation from CI policy.
## Architectural Strengths
The scope model is explicit.
Each scope type declares which bindings it introduces and where those bindings are visible.
The fixed-point loop catches transitive dead code.
A binding used only by dead code also becomes dead.
The usage search handles shadowing.
That prevents common false positives in nested functions and nested `let` expressions.
The edit model is tied to analysis results.
It knows whether a binding came from a lambda, pattern, `let`, or `inherit`.
The tool has few moving parts.
That makes the analysis easy to follow compared to a general multi-rule linter.
## Architectural Limits
`deadnix` does not evaluate Nix.
It cannot know which names external callers depend on.
This matters for module arguments, package function arguments, and public attribute sets.
It has global switches rather than project-specific rule contexts.
For example, all lambda pattern names can be checked or skipped.
The tool does not know that one directory contains `callPackage` functions.
The analysis is local to one file.
It cannot see that another file imports a function and relies on its argument names.
The fixer performs text edits with byte ranges.
It must handle punctuation and whitespace carefully.
Pattern edits are harder than simple assignment deletion.
The tool has one analysis family.
It is not a framework for many unrelated rules.
## Lessons for Our Linter
Dead-code checks need a scope pass.
A per-node rule interface is not enough when a diagnostic depends on name visibility.
Represent scopes explicitly.
Each scope type should provide introduced bindings and visible bodies.
Track shadowing before reporting unused names.
A text search for an identifier is not sufficient.
Use fixed-point analysis for transitive dead code.
One pass misses names used only by dead declarations.
Separate policy from mechanics.
The mechanics can find unused names.
Repository policy decides whether module arguments, package arguments, or underscore names count.
Treat autofix as scope-specific.
Removing a pattern name is not the same operation as deleting a `let` assignment.
Run cleanup after edits.
Dead-code fixes can create empty `let` expressions and empty `inherit` statements.
## Open Design Questions for Our Linter
- Should dead-code analysis be part of `nix-style-linter`, or a separate command?
- Should policy vary by path, file kind, or suppression comment?
- Should recursive attribute-set members ever become removable findings?
- Should underscore names mean ignored, or should used underscore names be errors?
- Should the linter report unused names without offering autofix first?
- Should cleanup rules run as ordinary rules or as a post-fix pass?

View File

@@ -1,102 +0,0 @@
# Hyprland Lua Migration Plan
## Problem Statement
The Hyprland compositor config uses Home Manager's hyprlang output.
Hyprland deprecates hyprlang for the main compositor config.
The repository needs to generate a Lua config without changing desktop behavior.
## Solution
Keep the config owned by Nix and Home Manager.
Switch the Hyprland Home Manager config output to Lua.
Convert only the settings whose shape changes under Lua.
This is not a desktop redesign.
The change preserves the current monitors, binds, window rules, environment variables, and UWSM startup.
## User Stories
1. As the operator, I want Hyprland to load `hyprland.lua`, so that startup no longer warns about hyprlang deprecation.
2. As the operator, I want existing keyboard binds to keep working, so that the desktop behavior stays unchanged.
3. As the operator, I want host monitor layout to stay unchanged, so that the physical display setup does not move.
4. As the operator, I want session environment variables to stay in the compositor config, so that Wayland and cursor behavior stays unchanged.
5. As the operator, I want auxiliary module binds to keep working, so that launcher, lock, screenshot, clipboard, notifications, and recording flows stay usable.
## Implementation Decisions
1. Use Home Manager's Lua config generator.
Set the Hyprland config type to Lua and keep package ownership unchanged.
2. Keep ordinary compositor options declarative in Nix.
Move input, general, decoration, xwayland, animation, and layout settings into the Lua config structure.
3. Remove hyprlang-only variables from generated config.
Keep modifier and terminal values as Nix locals or helper inputs.
4. Add minimal local helpers for Lua calls.
Use helpers only for repeated bind, dispatcher, command, environment, and rule shapes.
5. Convert all bind strings to Lua bind call data.
Preserve key combinations, dispatchers, commands, repeat behavior, and locked-session behavior.
6. Convert contributed binds in their owning modules.
Keep each bind near the module that owns the tool it launches.
7. Convert monitor strings to Lua monitor records.
Preserve output names, modes, positions, scale values, and rotation.
8. Convert the screenshot window rule to the Lua rule shape.
Preserve the Satty class matcher and floating behavior.
9. Convert environment strings to Lua environment calls.
Preserve the Wayland flag and cursor variables.
10. Delete or update comments that describe hyprlang syntax.
Keep comments that explain behavior or non-obvious invariants.
## Testing Decisions
1. Run the repository Nix formatting and style checks for modified Nix files.
2. Evaluate or build the affected host configuration.
This proves Home Manager accepts the Lua settings structure.
3. Confirm the generated config file uses Lua.
The expected output is `hyprland.lua`, not `hyprland.conf`.
4. Verify Hyprland accepts the generated Lua config.
Use Hyprland config verification when the installed build supports it.
Otherwise reload the compositor and inspect errors.
5. Smoke test the changed desktop surface.
Test one bind from each source area after reload.
6. Check the explicit monitor layout on the host that declares it.
Confirm resolution, position, scale, and rotation stayed unchanged.
## Out of Scope
- Do not split the config into Lua modules.
- Do not hand-write a full Lua config.
- Do not add new Hyprland behavior.
- Do not change UWSM startup.
- Do not redesign binds, monitor layout, or visual settings.
- Do not change hyprlock, hypridle, waybar, mako, or rofi settings beyond their Hyprland binds.
## Further Notes
Home Manager documents `configType = "lua"` for Hyprland.
It maps settings to `hl.*` calls.
It supports `_args` for multi-argument calls.
It supports raw Lua expressions with `lib.generators.mkLuaInline`.
The riskiest parts are binds, monitor declarations, and window rules.
Those parts changed shape more than scalar config sections.
Assumption: the pinned Home Manager input contains the documented Lua Hyprland generator.
Validate this during the configuration build.

View File

@@ -0,0 +1,130 @@
# NixOS AGENTS and style-skill scope design tree
This tree reviews `configs/nixos/AGENTS.md`.
It decides which instructions belong in Nix style rules.
## Known facts
- `configs/nixos/AGENTS.md` has 70 lines.
- Each frontier question quotes the complete instruction under review.
- This review decides instruction placement only.
- It supplies guidance for desktop, Firefox, Neovim, AI tooling, and NixOS gotchas.
- The `/review` prompt loads `nix-style` for Nix files.
- `nix-style` loads every rule under `src/skills/style/nix/rules/`.
- The current Nix rules cover Nix syntax and flake structure.
- `configs/nixos/AGENTS.md` includes a guest-module ownership rule.
- The RomM session missed that ownership rule during implementation.
- `registry.window-icon` exists in `configs/nixos/registry/window-icon.nix`.
- Desktop application modules declare window classes and rendered icons in that registry.
- Hyprland reads the registry to generate workspace icon mappings.
- The AGENTS instruction named the registry after its introduction on 2026-10-11.
- The `class` option describes its window-class match.
- The `icon` option describes a string icon value.
- The registry schema does not document the rendered-string requirement or the class-discovery command.
- Hyprland renders `config.registry.env` as its `env` configuration.
- The `env` registry replaced direct Hyprland environment declarations in commit `370cab8f8`.
- `configs/nixos/modules/desktop/apps/r2modman.nix` enables a graphical application without a `registry.window-icon` declaration.
- The current registry modules already validate value uniqueness and field combinations.
- They do not validate required producer declarations.
- The registry collector is specific to the NixOS dotfiles configuration.
- A non-discoverable environment fact records local behavior that configuration, source code, and normal tool help do not reveal.
- The current instruction groups are:
- Desktop: window icons, Hyprland environment variables, and cursor settings.
- Firefox: search-engine declarations and extension-storage compatibility.
- Neovim: Nixvim package-set selection.
- AI tooling: Herdr key names and Pi configuration locations.
- Gotchas: registry APIs, guest boundaries, SOPS ownership, guest mounts, deployment, DNS, hardware, module paths, and host commands.
## Decision tree
- Instruction review
- Settled: Review each complete instruction in source order.
- Registry conventions
- Settled: Create `src/skills/style/nix/rules/dotfiles.md`.
- Graphical application icon instruction
- Settled: Move the complete instruction to `src/skills/style/nix/rules/dotfiles.md`.
- Hyprland session environment instruction
- Settled: Remove it because `registry.env` superseded it.
- Firefox search-engine instruction
- Settled: Remove it because the Firefox module documents the configuration.
- Firefox extension-storage instruction
- Settled: Move it to a comment above `extensions.settings` in `modules/desktop/apps/firefox.nix`.
- Neovim package-set instruction
- Settled: Move it to a comment above `nixpkgs.useGlobalPackages` in `modules/shell/nvim/default.nix`.
- Herdr key-name instruction
- Settled: Remove it.
- Pi extension location instruction
- Settled: Remove it. It recorded a completed migration from NixOS-managed sources.
- Pi generated-settings instruction
- Settled: Remove it because the Pi module documents the managed boundary.
- Library-function qualification instruction
- Settled: Generalize it and move it to `src/skills/style/nix/rules/general.md`.
- Guest ownership instruction
- Settled: Move it to `src/skills/style/nix/rules/dotfiles.md`.
- SOPS guest-owner instruction
- Settled: Move it to `src/skills/style/nix/rules/dotfiles.md`.
- Nested guest-mount instruction
- Settled: Remove it because the example describes no active layout.
- `deploy-to` instruction
- Settled: Remove it. Agents cannot complete its remote sudo steps.
- Pikachu deploy-closure instruction
- Settled: Remove it because `deploy-to` encodes the required import path.
- Public-DNS instruction
- Settled: Remove it.
- Hardware-configuration instruction
- Settled: Remove it because the root auto-generated-file rule owns the policy.
- Chaotic-cache instruction
- Settled: Remove it because `modules.host.cachyos.enable` owns the cache configuration.
- Module-relative-path instruction
- Settled: Remove it because Nix evaluation reports invalid paths.
- Pikachu `uptime` instruction
- Settled: Remove it because the command exposes its supported options.
- Guest-ACME-evaluation instruction
- Settled: Remove it because current direct guest evaluation succeeds.
- systemd executable instruction
- Settled: Remove it because `general.md` already requires `lib.getExe'` for non-main executables.
## Frontier
The frontier is empty.
## Shared understanding
Remove these instructions from `configs/nixos/AGENTS.md`:
- Hyprland session environment
- Firefox search engines
- Herdr shifted punctuation
- Pi extension source location
- Pi managed settings
- Nested guest mounts
- `deploy-to` behavior
- Pikachu deploy closures
- Public DNS boundary
- Hardware configuration
- Chaotic cache selection
- Module-relative paths
- Pikachu boot time
- Guest ACME evaluation
- systemd executable selection
Move these instructions intact to `src/skills/style/nix/rules/dotfiles.md`:
- Graphical application icons
- Guest ownership
- SOPS guest ownership
Move the generalized library-function qualification convention to
`src/skills/style/nix/rules/general.md`.
Move these instructions to code comments:
- Firefox extension storage: above `extensions.settings` in
`configs/nixos/modules/desktop/apps/firefox.nix`
- Nixvim package set: above `nixpkgs.useGlobalPackages` in
`configs/nixos/modules/shell/nvim/default.nix`
The review does not decide how to restore the auto-generated status of
`configs/nixos/hosts/pikachu/hardware-configuration.nix`.
The operator confirmed this design and authorized the edits.

View File

@@ -0,0 +1,106 @@
# RomM migration design tree
## Known facts
- RomM is listed under the Pikachu and Pichu Proxmox migration.
- Pikachu already runs VLAN 20 guests with persistent storage.
- Nixpkgs provides a tested `services.romm` module at version 5.2.0.
- `services.romm.dataDir` defaults to `/var/lib/romm`.
- RomM stores the game library below `/var/lib/romm/library`.
- RomM stores saves, states, user-uploaded screenshots, and profile avatars below `/var/lib/romm/assets/users/<user-id>/`.
- RomM derives `<user-id>` from its database ID, so it is not a stable configured username.
- RomM stores scraped covers and provider-sourced game screenshots below `/var/lib/romm/resources/roms/<platform-id>/<rom-id>/`.
- RomM 5.4.0 exists upstream but Nixpkgs does not package it.
- RomM 5.3.0 requires an explicit library-structure declaration in `config.yml`.
- The 5.4.0 package phase must add the Structure A declaration for `roms/{platform}/{game}` and `bios/{platform}`.
- RomM 5.4.0 can modify ROM files in place only when library conversion is explicitly enabled.
- RomM 5.3.0 identifies hashed ROMs by CRC, MD5, and SHA-1 after a scan.
- RomM 5.3.0 matches moved or renamed hashed ROMs to their existing database records.
- This migration is a fresh installation.
- The new library will receive selected ROMs from the old installation.
- Copyparty serves `/pikachu/media` as its writable `/media` volume.
## Decision tree
- RomM deployment
- Target location
- Settled: Deploy a dedicated guest on Pikachu.
- Package source
- Settled: Start with Nixpkgs's RomM 5.2.0 package.
- Settled: Package RomM 5.4.0 locally as follow-up work.
- Persistent storage
- Settled: Use `/var/lib/romm` in the guest.
- Settled: Mount `/pikachu/data/romm` at `/var/lib/romm`.
- Settled: Mount the ROM library separately at `/var/lib/romm/library`.
- Settled: Mount user assets separately for Copyparty browsing.
- Settled: Keep scraped resources in `/pikachu/data/romm`.
- Settled: Mount `/pikachu/media/games/emulation/roms` at `/var/lib/romm/library/roms`.
- Settled: Mount `/pikachu/media/games/emulation/bios` at `/var/lib/romm/library/bios`.
- Settled: Mount `/pikachu/media/games/emulation/users` at `/var/lib/romm/assets/users`.
- Settled: Mount `/pikachu/data/romm/postgresql` at `/var/lib/postgresql`.
- Settled: Mount `/pikachu/data/romm/redis` at `/var/lib/redis-romm`.
- Guest networking
- Settled: Follow the standard guest-provisioning protocol, including its VLAN 20 DHCP reservation.
- Public route
- Settled: Retain the old installation's public domain.
- Metadata and authentication secrets
- Settled: Configure IGDB, ScreenScraper, and SteamGridDB.
- Settled: Use RomM's default metadata and artwork priority.
- Settled: Manage the 5.4.0 `config.yml` declaratively with Nix.
- Settled: Use RomM's default region and English-language priority.
- Save synchronization
- Settled: Disable automatic browser save synchronization in 5.4.0.
- Authentication
- Settled: Use RomM's built-in accounts initially.
- Deferred: Configure RomM as an OpenID Connect client after an identity provider exists.
- ROM selection and import
- Settled: The operator manually selects and copies ROMs after deployment.
- Settled: Do not create automatic import tooling or a ROM manifest.
- Capacity and service limits
- Settled: Limit the guest to `4G` memory, `200%` CPU, and `512` tasks.
- Library scanning
- Settled: Enable the filesystem watcher for library changes.
- Cutover and retirement
- Out of scope: The operator manages the old installation and public-domain cutover outside this monorepo.
- Work phases
- Settled: Create a RomM guest module with Nixpkgs's package in [[0014-romm-guest]].
- Settled: Package RomM 5.4.0 locally and reuse `services.romm` in [[0015-romm-5-4-package]].
- Settled: Move selected ROMs after the service deployment is ready in [[0016-romm-manual-rom-migration]].
## Settled decisions
- **Migration model:** Start with a fresh RomM installation.
- **Data scope:** Copy selected ROMs from the old installation.
- **Old application state:** Do not migrate the database, users, sessions, or job queue.
- **Deployment target:** Deploy a dedicated guest on Pikachu.
- **Initial package:** Use Nixpkgs's RomM 5.2.0 package.
- **Package follow-up:** Package RomM 5.4.0 locally.
- **Work phases:** Deploy the guest, update the package, then import selected ROMs.
- **Guest data directory:** Use RomM's default `/var/lib/romm`.
- **Host application data:** Mount `/pikachu/data/romm` at `/var/lib/romm`.
- **ROM library:** Mount it separately at `/var/lib/romm/library`.
- **Storage classification:** Preserve the distinction between application data and browsable media.
- **User assets:** Mount `/pikachu/media/games/emulation/users` at `/var/lib/romm/assets/users` to expose saves, states, user-uploaded screenshots, and profile avatars through Copyparty.
- **ROM paths:** Mount `/pikachu/media/games/emulation/roms` and `/pikachu/media/games/emulation/bios` at the corresponding children of `/var/lib/romm/library`.
- **Guest networking:** Follow the standard guest-provisioning protocol.
- **Public route:** Retain the old installation's public domain.
- **Metadata providers:** Configure IGDB, ScreenScraper, and SteamGridDB in both package phases.
- **Metadata priority:** Use RomM's default metadata and artwork priority.
- **RomM configuration:** Manage the required 5.4.0 `config.yml` declaratively with Nix.
- **Scan region and language priority:** Use RomM's defaults.
- **Initial authentication:** Use RomM's built-in accounts.
- **OIDC follow-up:** Track it in [[0013-oidc-provider]].
- **ROM migration:** The operator manually selects and copies ROMs after deployment.
- **Guest limits:** Cap the guest at `4G` memory, `200%` CPU, and `512` tasks.
- **Library watcher:** Enable the filesystem watcher.
- **PostgreSQL:** Mount `/pikachu/data/romm/postgresql` at `/var/lib/postgresql`.
- **Redis:** Mount `/pikachu/data/romm/redis` at `/var/lib/redis-romm`.
- **Browser save synchronization:** Disable it in 5.4.0 until browser emulation is needed.
## Frontier
No in-scope decisions remain.
## Next question
None. Present the settled design for confirmation before implementation.

View File

@@ -0,0 +1,36 @@
# TL;DR command design tree
This tree reviews the implementation decisions in `tldr-design.md`.
## Settled decisions
- **Command form and scope:** Add the project prompt template `.pi/prompts/tldr.md`.
- **Explicit source interface:** Interpret command arguments as a free-form source request.
- **Default source:** Summarize the preceding agent response when no source request exists.
- **Summary shape:** Produce a compact summary without a fixed format.
- **Unavailable or unclear sources:** Add no special handling policy.
## Decision tree
- Command behavior
- Source selection
- Settled: Use the preceding agent response without arguments.
- Settled: Interpret arguments as a free-form source request.
- Output
- Settled: Produce a compact summary without a fixed format.
- Summary language
- Settled: Do not prescribe a language policy.
- Error behavior
- Settled: Add no special handling policy for unavailable or unclear sources.
## Frontier
The frontier is empty.
## Shared understanding pending confirmation
The project gets a `/tldr` prompt command in `.pi/prompts/tldr.md`.
Without arguments, it summarizes the preceding agent response.
With arguments, it interprets them as a free-form source request.
It produces a compact summary with no fixed format or language policy.
The prompt adds no special policy for unavailable or unclear sources.

View File

@@ -0,0 +1,39 @@
# TL;DR command design
## Goal
Add a Pi command that summarizes the preceding agent response by default.
An explicit command argument identifies the source to summarize.
## Decision tree
- Command form and scope
- Settled: Create a project prompt template at `.pi/prompts/tldr.md`.
- Command interface
- Settled: Pass arguments through as a free-form source request.
- Default source selection
- Settled: Summarize the preceding agent response when no source request exists.
- Summary format and length
- Settled: Use a compact, format-free summary.
- Ambiguous-source behavior
- Settled: Add no special handling policy.
## Settled decisions
- **Q1 — Command form and scope:** Create a project prompt template.
- **Q2 — Explicit source interface:** Accept a free-form source request.
- **Default source selection:** Summarize the preceding agent response when the command has no arguments.
- **Q3 — Summary shape:** Use a compact, format-free summary.
- **Q4 — Unavailable source:** Add no special handling policy.
## Frontier
The frontier is empty.
## Shared understanding pending confirmation
The project gets a `/tldr` prompt command in `.pi/prompts/tldr.md`.
Without arguments, it summarizes the preceding agent response.
With arguments, it interprets them as a free-form source request.
It produces a compact summary with no fixed format.
The prompt adds no special policy for unavailable or unclear sources.

14
docs/00 INBOX/credits.md Normal file
View File

@@ -0,0 +1,14 @@
---
aliases:
- Credits
date-created: 2026-10-17 18:08
date-modified: 2026-10-17 23:33
---
# Credits
| Path | Original | Notes | |
|-------------------------------------|-----------------------------------------------------------------------------------------|-------|---|
| `//src/skills/agent-writing-skills` | <https://github.com/mattpocock/skills/tree/main/skills/productivity/writing-for-agents> | | |
| `//src/skills/retrospect` | <https://github.com/mattpocock/skills/tree/main/skills/engineering/retro> | | |
| | | | |

8
docs/03 wrk/dotfiles.md Normal file
View File

@@ -0,0 +1,8 @@
---
aliases:
- Dotfiles
date-created: 2026-10-14 16:27
date-modified: 2026-10-14 16:28
---
# Dotfiles

8
docs/03 wrk/monorepo.md Normal file
View File

@@ -0,0 +1,8 @@
---
aliases:
- Monorepo
date-created: 2026-10-14 22:51
date-modified: 2026-10-14 22:54
---
# Monorepo

View File

@@ -0,0 +1,8 @@
---
aliases:
- Productivity
date-created: 2026-10-14 16:32
date-modified: 2026-10-14 16:33
---
# Productivity

View File

@@ -0,0 +1,16 @@
---
aliases:
- "0001"
- TODO CLI
date-created: 2026-10-14 00:49
date-modified: 2026-10-14 22:52
status: open
area: "[[productivity]]"
blocked-by:
- "[[0005-rust-cli-scaffolding]]"
subtasks: []
---
# TODO CLI
Write a TODO CLI that enforces the task workflow in this vault deterministically. This allows agents to participate in task management with minimal cost instead of relying on markdown generation and parsing arbitrary metadata while maintaining human readability.

View File

@@ -0,0 +1,103 @@
---
aliases:
- "0002"
- NixOS Refactor
date-created: 2026-10-14 16:07
date-modified: 2026-10-19 16:44
status: closed
area: "[[dotfiles]]"
blocked-by: []
subtasks: []
---
# NixOS Refactor
Complete the remaining Nix refactor work.
## Files
- [x] `flake.nix`
- [x] `src/flake/default.nix`
- [x] `src/flake/formatter.nix`
- [x] `src/flake/apps.nix`
- [x] `src/dotfiles/default.nix`
- [x] `src/dotfiles/util.nix`
- [x] `src/dotfiles/registry.nix`
- [x] `src/dotfiles/registry/schema.nix`
- [x] `src/dotfiles/registry/validation.nix`
- [x] `configs/nixos/registry/address.nix`
- [>] `configs/nixos/registry/homepage.nix`
- [x] `configs/nixos/registry/route.nix`
- [x] `configs/nixos/registry/shell-shortcut.nix`
- [x] `configs/nixos/registry/stable-uid.nix`
- [x] `configs/nixos/modules/default.nix`
- [x] `configs/nixos/modules/host.nix`
- [x] `configs/nixos/modules/guest.nix`
- [x] `configs/nixos/modules/monorepo.nix`
- [x] `configs/nixos/modules/zfs.nix`
- [x] `configs/nixos/modules/network/ssh.nix`
- [x] `configs/nixos/modules/network/vlans.nix`
- [x] `configs/nixos/modules/network/vpn-client.nix`
- [x] `configs/nixos/modules/desktop/default.nix`
- [x] `configs/nixos/modules/desktop/apps/alacritty.nix`
- [x] `configs/nixos/modules/desktop/apps/firefox.nix`
- [x] `configs/nixos/modules/desktop/apps/ghostty.nix`
- [x] `configs/nixos/modules/desktop/apps/obsidian.nix`
- [x] `configs/nixos/modules/desktop/apps/r2modman.nix`
- [x] `configs/nixos/modules/desktop/apps/signal.nix`
- [x] `configs/nixos/modules/desktop/apps/steam.nix`
- [x] `configs/nixos/modules/desktop/apps/vencord.nix`
- [x] `configs/nixos/modules/desktop/session/hyprland/default.nix`
- [x] `configs/nixos/modules/desktop/session/hyprland/hypridle.nix`
- [x] `configs/nixos/modules/desktop/session/hyprland/hyprlock.nix`
- [x] `configs/nixos/modules/desktop/session/login.nix`
- [x] `configs/nixos/modules/desktop/session/mako.nix`
- [x] `configs/nixos/modules/desktop/session/portals.nix`
- [x] `configs/nixos/modules/desktop/session/rofi.nix`
- [x] `configs/nixos/modules/desktop/session/waybar.nix`
- [x] `configs/nixos/modules/desktop/utilities/audio.nix`
- [x] `configs/nixos/modules/desktop/utilities/clipboard.nix`
- [x] `configs/nixos/modules/desktop/utilities/osd.nix`
- [x] `configs/nixos/modules/desktop/utilities/recording.nix`
- [x] `configs/nixos/modules/desktop/utilities/screenshot.nix`
- [x] `configs/nixos/modules/desktop/utilities/theming.nix`
- [x] `configs/nixos/modules/desktop/utilities/userdirs.nix`
- [x] `configs/nixos/modules/services/actualbudget.nix`
- [x] `configs/nixos/modules/services/copyparty.nix`
- [x] `configs/nixos/modules/services/homepage.nix`
- [x] `configs/nixos/modules/services/navidrome.nix`
- [x] `configs/nixos/modules/services/reverse-proxy.nix`
- [x] `configs/nixos/modules/services/steamcmd.nix`
- [x] `configs/nixos/modules/services/games/palworld.nix`
- [x] `configs/nixos/modules/services/games/valheim.nix`
- [x] `configs/nixos/modules/shell/bazel.nix`
- [x] `configs/nixos/modules/shell/direnv/default.nix`
- [x] `configs/nixos/modules/shell/fish/default.nix`
- [x] `configs/nixos/modules/shell/gitea-axi.nix`
- [x] `configs/nixos/modules/shell/git.nix`
- [x] `configs/nixos/modules/shell/herdr.nix`
- [x] `configs/nixos/modules/shell/nvim/default.nix`
- [x] `configs/nixos/modules/shell/omp.nix`
- [x] `configs/nixos/modules/shell/pi/default.nix`
- [x] `configs/nixos/modules/shell/python.nix`
- [x] `configs/nixos/modules/shell/rust.nix`
- [x] `configs/nixos/modules/shell/tmux/default.nix`
- [x] `configs/nixos/modules/shell/yazi.nix`
- [x] `src/dotfiles/guests.nix`
- [x] `configs/nixos/guests/actualbudget.nix`
- [x] `configs/nixos/guests/copyparty.nix`
- [x] `configs/nixos/guests/homepage.nix`
- [x] `configs/nixos/guests/navidrome.nix`
- [x] `configs/nixos/guests/palworld.nix`
- [x] `configs/nixos/guests/reverse-proxy.nix`
- [x] `configs/nixos/guests/valheim.nix`
- [x] `src/dotfiles/hosts.nix`
- [x] `configs/nixos/hosts/neogaia/default.nix`
- [x] `configs/nixos/hosts/neogaia/disk.nix`
- [x] `configs/nixos/hosts/neogaia/hardware-configuration.nix`
- [x] `configs/nixos/hosts/pikachu/default.nix`
- [x] `configs/nixos/hosts/pikachu/disk.nix`
- [x] `configs/nixos/hosts/pikachu/hardware-configuration.nix`
- [x] `configs/nixos/hosts/zeus/default.nix`
- [x] `configs/nixos/hosts/zeus/disk.nix`
- [x] `configs/nixos/hosts/zeus/hardware-configuration.nix`

View File

@@ -0,0 +1,37 @@
---
aliases:
- "0003"
- Whisper dictation
date-created: 2026-10-14 16:31
date-modified: 2026-10-14 16:33
status: open
area: "[[productivity]]"
blocked-by:
- "[[0002-nixos-refactor]]"
subtasks: []
---
# Whisper dictation
Provide reliable local dictation on Zeus.
## Prototype findings
- `hyprwhspr-rs` provides PipeWire capture, local `whisper.cpp` transcription, clipboard copying, and Hyprland integration.
- The prototype used `small.en` from `~/dwn/ggml-small.en.bin`.
- The initial CPU prototype transcribed accurately but added about 2.3 seconds after a 2.9-second recording.
- Vulkan acceleration uses `whisper-cpp-vulkan` and `gpu_layers = 999`.
- `SUPER + ALT + D` starts recording on press and stops recording on release.
- Hyprland Lua represents release bindings with `hl.bind(..., { release = true })`.
- `hyprwhspr-rs` uses Hyprland's removed `sendshortcut` dispatcher for automatic paste.
- Its virtual-keyboard fallback can drop modifiers and insert only `v`.
- Treat the prototype configuration as disposable because it no longer works reliably.
## Completion
- [ ] Choose a maintained local speech-to-text backend.
- [ ] Record microphone audio through PipeWire.
- [ ] Transcribe locally with GPU acceleration when available.
- [ ] Insert transcribed text reliably into the focused application.
- [ ] Copy transcribed text to the clipboard.
- [ ] Test dictation with representative technical speech.

View File

@@ -0,0 +1,18 @@
---
aliases:
- "0004"
- Dictionary CLI
date-created: 2026-10-14 22:47
date-modified: 2026-10-14 22:57
status: open
area: "[[productivity]]"
blocked-by:
- "[[0005-rust-cli-scaffolding]]"
subtasks: []
---
# Dictionary CLI
A CLI tool that basically works as a strict allowlist for words used in prose such as markdown files or strings passed to the tool.
The basic idea can be found in [[dictionary]], but as it would be a deterministic tool, a strict allowlist might be more beneficial.

View File

@@ -0,0 +1,21 @@
---
aliases:
- "0005"
- Rust CLI Scaffolding
date-created: 2026-10-14 22:50
date-modified: 2026-10-15 23:48
# Allowed: open, closed
status: open
area: "[[monorepo]]"
blocked-by:
- "[[0009-bazel-project-scaffolding]]"
subtasks: []
---
# Rust CLI Scaffolding
One of the biggest patterns I noticed when working on Rust projects in the monorepo was that they were mostly CLI tools. I found myself doing the same boilerplate over and over again.
I think much like Google, having a function that handles sensible defaults would be a good idea.
But also, maybe a Rust project set up to avoid the common pitfalls?

View File

@@ -0,0 +1,23 @@
---
aliases:
- "0006"
- Wallpaper Randomizer
date-created: 2026-10-14 23:00
date-modified: 2026-10-14 23:05
status: open
area: "[[dotfiles]]"
blocked-by:
- "[[0005-rust-cli-scaffolding]]"
subtasks: []
---
# Wallpaper Randomizer
This is kind of two ideas in one:
1. Randomizing the desktop wallpaper from a select list.
2. A web service endpoint that provides a random image from a configured directory/set.
The first idea might not need any special code, so it wouldn't be a CLI.
I think that any tools I write should have both a non-interactive mode and a TUI or web server mode.

View File

@@ -0,0 +1,20 @@
---
aliases:
- "0007"
- Obsidian Source Notes
date-created: 2026-10-14 23:11
date-modified: 2026-10-14 23:14
status: open
area: "[[productivity]]"
blocked-by: []
subtasks: []
---
# Obsidian Source Notes
This is more of a note to collect ideas for `ingest-*` skills.
## Skill Ideas
- `ingest-youtube-transcript`
- `ingest-youtube-video`

View File

@@ -0,0 +1,15 @@
---
aliases:
- "0008"
- Vim Obsidian Markdown Renderer
date-created: 2026-10-14 23:18
date-modified: 2026-10-14 23:20
status: open
area: "[[dotfiles]]"
blocked-by: []
subtasks: []
---
# Vim Obsidian Markdown Renderer
The current plugin I use for Markdown rendering in Vim has some issues, but most importantly it doesn't have full feature parity with Obsidian. I think from the top of my head, wikilinks can't be followed for example.

View File

@@ -0,0 +1,17 @@
---
aliases:
- "0009"
- Bazel Project Scaffolding
date-created: 2026-10-15 23:46
date-modified: 2026-10-16 00:32
status: open
area: "[[monorepo]]"
blocked-by: []
subtasks: []
---
# Bazel Project Scaffolding
A small utility library that sets up projects when needed.
The first goal would be to scaffold a basic Rust project with a library, tests and binary to avoid manual toil.

View File

@@ -0,0 +1,18 @@
---
aliases:
- "0010"
- Fix Main Monitor Detection
date-created: 2026-10-17 09:24
date-modified: 2026-10-17 09:25
# Allowed: open, closed
status: open
area: "[[dotfiles]]"
blocked-by: []
subtasks: []
---
# Fix Main Monitor Detection
Most of the time, games detect the HDMI monitor as the main monitor and show the wrong resolution options and/or have display issues.
The HDMI monitor is a vertical monitor with 1080x1920 resolution so that leads to a lot of problems I can't fix from within the game because it gets cut off.

View File

@@ -0,0 +1,20 @@
---
aliases:
- "0011"
- Gitea Axi Rust Migration
date-created: 2026-10-18 15:43
date-modified: 2026-10-19 16:46
status: open
area: "[[monorepo]]"
blocked-by:
- "[[0005-rust-cli-scaffolding]]"
subtasks: []
---
# Gitea Axi Rust Migration
Rewrite `gitea-axi` in Rust and migrate it into this monorepo.
Preserve its Gitea workflow and command behavior.
Replace the external Home Manager module in `configs/nixos/modules/shell/gitea-axi.nix`.

View File

@@ -0,0 +1,19 @@
---
aliases:
- "0012"
- Gitea Axi PR Merge Guardrail
date-created: 2026-10-18 15:43
date-modified: 2026-10-18 15:43
status: open
area: "[[monorepo]]"
blocked-by:
- "[[0011-gitea-axi-rust-migration]]"
subtasks: []
---
# Gitea Axi PR Merge Guardrail
Guard `gitea-axi pr merge` against an unpushed local rewrite.
Compare the checked-out matching branch with the remote pull-request head.
Require explicit confirmation when local commits are absent from that head.

View File

@@ -0,0 +1,17 @@
---
aliases:
- "0013"
- OIDC Provider
date-created: 2026-10-19 19:40
date-modified: 2026-10-19 19:40
status: open
area: "[[homelab]]"
blocked-by: []
subtasks: []
---
# OIDC Provider
Deploy an OpenID Connect provider for homelab services.
Configure a RomM client after the provider is available.

View File

@@ -0,0 +1,42 @@
---
aliases:
- "0014"
- RomM Guest
date-created: 2026-10-19 19:40
date-modified: 2026-10-19 19:40
status: closed
area: "[[homelab]]"
blocked-by: []
subtasks: []
---
# RomM Guest
Deploy a dedicated RomM guest on Pikachu with Nixpkgs RomM 5.2.0.
Follow the standard guest-provisioning protocol.
Keep the old installation's public domain.
Use the local PostgreSQL and Redis instances from `services.romm`.
Set guest limits to `4G` memory, `200%` CPU, and `512` tasks.
Enable the library watcher.
Use RomM built-in accounts.
Mount persistent application data:
```text
/pikachu/data/romm -> /var/lib/romm
/pikachu/data/romm/postgresql -> /var/lib/postgresql
/pikachu/data/romm/redis -> /var/lib/redis-romm
```
Mount browsable media:
```text
/pikachu/media/games/emulation/roms -> /var/lib/romm/library/roms
/pikachu/media/games/emulation/bios -> /var/lib/romm/library/bios
/pikachu/media/games/emulation/users -> /var/lib/romm/assets/users
```
Configure IGDB, ScreenScraper, and SteamGridDB from secrets.
Use RomM's default metadata, artwork, region, and language priorities.

View File

@@ -0,0 +1,31 @@
---
aliases:
- "0015"
- RomM 5.4 Package
date-created: 2026-10-19 19:40
date-modified: 2026-10-19 19:40
status: open
area: "[[homelab]]"
blocked-by:
- "[[0014-romm-guest]]"
subtasks: []
---
# RomM 5.4 Package
Package RomM 5.4.0 locally.
Reuse `services.romm`.
Manage `config.yml` declaratively with Nix.
Declare the Structure A library layout:
```yaml
filesystem:
structure:
default: "roms/{platform}/{game}"
firmware: "bios/{platform}"
```
Keep library conversion disabled.
Disable automatic browser save synchronization until browser emulation is needed.
Keep the IGDB, ScreenScraper, and SteamGridDB configuration unchanged.

View File

@@ -0,0 +1,24 @@
---
aliases:
- "0016"
- RomM Manual ROM Migration
date-created: 2026-10-19 19:40
date-modified: 2026-10-19 19:40
status: open
area: "[[homelab]]"
blocked-by:
- "[[0014-romm-guest]]"
subtasks: []
---
# RomM Manual ROM Migration
Manually copy selected ROMs and BIOS files from the old installation.
Copy ROMs to `/pikachu/media/games/emulation/roms`.
Copy BIOS files to `/pikachu/media/games/emulation/bios`.
Do not migrate the old database, users, sessions, job queue, or other application state.
Do not add automatic import tooling or a ROM manifest.
This task does not change monorepo configuration.

View File

@@ -1,5 +1,5 @@
--- ---
date-created: {{date}} {{time}} date-created: "{{date}} {{time}}"
--- ---
# {{title}} # {{title}}

View File

@@ -0,0 +1,12 @@
---
date-created: "{{date}} {{time}}"
# Allowed: open, closed
status: open
area:
blocked-by:
-
subtasks:
-
---
# {{title}}

View File

@@ -1,37 +1,34 @@
# active projects # Active Projects
- dotfiles
- nix-style / refactor
## Inactive For Now
- ariadne - ariadne
- machoke - machoke
- falinks - falinks
- dotfiles
- natu
- nix-style / refactor
# project ideas # Project Ideas
- obsidian-source-notes (`ingest-*` skills) - onepacerr (requires raichu migration)
- obsidian-game-backlog - obsidian-game-backlog
- whisper dictation
- breadcrumbs - breadcrumbs
- gingko - gingko
- pokemon-classifier-ml - pokemon-classifier-ml
- home assistant agent integration - home assistant agent integration
- wallpaper randomizer - wallpaper randomizer
- ebook tool (might be an overlap with `ingest-ebook`) - ebook tool (might be an overlap with `ingest-ebook`)
- amazon / actualbudget exporter
- vim-obsidian-markdown
- dictionary cli tool
- kitchen.md - kitchen.md
- Rust project creator
- random number generator utility - random number generator utility
- reading material helper - reading material helper
- valheim progression DSL + graph - valheim progression DSL + graph
- Pi:
- vim bindings
# migration # Migration
- [[conventional-commits]] -> skill?
- [[dictionary]] -> cli tool
- [[technical-writing]] -> skill? - [[technical-writing]] -> skill?
- gitea-axi
- skills - skills
- claude-code/codebase-design - claude-code/codebase-design
- claude-code/implement - claude-code/implement
@@ -50,7 +47,7 @@
- subagents - subagents
- test-driven-development - test-driven-development
## pikachu + pichu proxmox -> nixos ## Pikachu + Pichu Proxmox -> Nixos
- lidarr (requires raichu) - lidarr (requires raichu)
- immich - immich
@@ -60,7 +57,8 @@
- adguard - adguard
- manage DNS rewrites for local service names such as `home` - manage DNS rewrites for local service names such as `home`
## raichu proxmox -> nixos ## Raichu Proxmox -> Nixos
- dwn torrent (vpn) - dwn torrent (vpn)
- dwn usenet (vpn) - dwn usenet (vpn)
- dwn-torrent-novpn - dwn-torrent-novpn
@@ -77,39 +75,34 @@
- seerr - seerr
- plex - plex
- pterodactyl - pterodactyl
- palworld old - palworld old
- project zomboid - project zomboid
- minecraft - minecraft
- cobblemon - cobblemon
# Learning
# learning
- YouTube - YouTube
- EeroAlvar - EeroAlvar
- https://www.youtube.com/watch?v=DzhLFiblPUk - <https://www.youtube.com/watch?v=DzhLFiblPUk>
- https://www.youtube.com/watch?v=hQGAwD9vkLo - <https://www.youtube.com/watch?v=hQGAwD9vkLo>
- https://www.youtube.com/watch?v=hhtExS5UQBI - <https://www.youtube.com/watch?v=hhtExS5UQBI>
- https://www.youtube.com/watch?v=Y1xO9OsCBC4 - <https://www.youtube.com/watch?v=Y1xO9OsCBC4>
- https://www.youtube.com/watch?v=DWWrLlM3gwQ - <https://www.youtube.com/watch?v=DWWrLlM3gwQ>
- https://www.youtube.com/watch?v=yHL-gmkzC-4 - <https://www.youtube.com/watch?v=yHL-gmkzC-4>
- https://www.youtube.com/watch?v=KRVYUkM16hE - <https://www.youtube.com/watch?v=KRVYUkM16hE>
- https://www.youtube.com/watch?v=RxdvzenLcl4 - <https://www.youtube.com/watch?v=RxdvzenLcl4>
- https://www.youtube.com/watch?v=mdbpIpPSPwQ - <https://www.youtube.com/watch?v=mdbpIpPSPwQ>
- https://www.youtube.com/watch?v=GHsq0klC_4g - <https://www.youtube.com/watch?v=GHsq0klC_4g>
- https://www.youtube.com/watch?v=ciC6ffUqI8k - <https://www.youtube.com/watch?v=ciC6ffUqI8k>
- https://www.youtube.com/watch?v=gYxZt9Qe0fk - <https://www.youtube.com/watch?v=gYxZt9Qe0fk>
- https://www.youtube.com/watch?v=0tqty8ltKDA - <https://www.youtube.com/watch?v=0tqty8ltKDA>
- https://www.youtube.com/watch?v=kzcI5F4tGiU - <https://www.youtube.com/watch?v=kzcI5F4tGiU>
- https://www.youtube.com/watch?v=iKwPaB5TUdI - <https://www.youtube.com/watch?v=iKwPaB5TUdI>
- https://www.youtube.com/watch?v=A-CtdQFRX64 - <https://www.youtube.com/watch?v=A-CtdQFRX64>
- natu literature - <https://www.youtube.com/watch?v=MHPRnM38Dyc>
- https://rust-analyzer.github.io/book/contributing/syntax.html?utm_source=chatgpt.com
- https://github.com/dotnet/roslyn/blob/main/docs/compilers/Design/Red-Green%20Trees.md?utm_source=chatgpt.com
- https://github.com/rust-analyzer/rowan/blob/master/examples/s_expressions.rs?utm_source=chatgpt.com
- https://dl.acm.org/doi/10.1145/964001.964011
- https://web.cecs.pdx.edu/~apt/esop15.pdf?utm_source=chatgpt.com
- https://www.researchgate.net/publication/2377179_Efficient_and_flexible_incremental_parsing?utm_source=chatgpt.com
# tech debt # Tech Debt
- better remote deployment - better remote deployment
- expose remote deployment as a flake app, replacing the Fish-only `deploy-to` workflow - expose remote deployment as a flake app, replacing the Fish-only `deploy-to` workflow
@@ -118,92 +111,7 @@
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 ## Nix Refactor
Review Nix files in entrypoint order: Review Nix files in entrypoint order:
- [x] `flake.nix`
- [x] `src/flake/default.nix`
- [x] `src/flake/formatter.nix`
- [x] `src/flake/apps.nix`
- [x] `src/dotfiles/default.nix`
- [x] `src/dotfiles/util.nix`
- [x] `src/dotfiles/registry.nix`
- [x] `src/dotfiles/registry/schema.nix`
- [x] `src/dotfiles/registry/validation.nix`
- [x] `configs/nixos/registry/address.nix`
- [>] `configs/nixos/registry/homepage.nix`
- [x] `configs/nixos/registry/route.nix`
- [x] `configs/nixos/registry/shell-shortcut.nix`
- [x] `configs/nixos/registry/stable-uid.nix`
- [x] `configs/nixos/modules/default.nix`
- [x] `configs/nixos/modules/host.nix`
- [x] `configs/nixos/modules/guest.nix`
- [x] `configs/nixos/modules/monorepo.nix`
- [x] `configs/nixos/modules/zfs.nix`
- [x] `configs/nixos/modules/network/ssh.nix`
- [x] `configs/nixos/modules/network/vlans.nix`
- [x] `configs/nixos/modules/network/vpn-client.nix`
- [x] `configs/nixos/modules/desktop/default.nix`
- [x] `configs/nixos/modules/desktop/apps/alacritty.nix`
- [x] `configs/nixos/modules/desktop/apps/firefox.nix`
- [x] `configs/nixos/modules/desktop/apps/ghostty.nix`
- [x] `configs/nixos/modules/desktop/apps/obsidian.nix`
- [x] `configs/nixos/modules/desktop/apps/r2modman.nix`
- [x] `configs/nixos/modules/desktop/apps/signal.nix`
- [x] `configs/nixos/modules/desktop/apps/steam.nix`
- [x] `configs/nixos/modules/desktop/apps/vencord.nix`
- [ ] `configs/nixos/modules/desktop/session/hyprland/default.nix`
- [ ] `configs/nixos/modules/desktop/session/hyprland/hypridle.nix`
- [ ] `configs/nixos/modules/desktop/session/hyprland/hyprlock.nix`
- [ ] `configs/nixos/modules/desktop/session/login.nix`
- [ ] `configs/nixos/modules/desktop/session/mako.nix`
- [ ] `configs/nixos/modules/desktop/session/portals.nix`
- [ ] `configs/nixos/modules/desktop/session/rofi.nix`
- [ ] `configs/nixos/modules/desktop/session/waybar.nix`
- [ ] `configs/nixos/modules/desktop/utilities/audio.nix`
- [ ] `configs/nixos/modules/desktop/utilities/clipboard.nix`
- [ ] `configs/nixos/modules/desktop/utilities/osd.nix`
- [ ] `configs/nixos/modules/desktop/utilities/recording.nix`
- [ ] `configs/nixos/modules/desktop/utilities/screenshot.nix`
- [ ] `configs/nixos/modules/desktop/utilities/theming.nix`
- [ ] `configs/nixos/modules/desktop/utilities/userdirs.nix`
- [ ] `configs/nixos/modules/services/actualbudget.nix`
- [ ] `configs/nixos/modules/services/copyparty.nix`
- [ ] `configs/nixos/modules/services/homepage.nix`
- [ ] `configs/nixos/modules/services/navidrome.nix`
- [ ] `configs/nixos/modules/services/reverse-proxy.nix`
- [ ] `configs/nixos/modules/services/steamcmd.nix`
- [ ] `configs/nixos/modules/services/games/palworld.nix`
- [ ] `configs/nixos/modules/services/games/valheim.nix`
- [ ] `configs/nixos/modules/shell/bazel.nix`
- [ ] `configs/nixos/modules/shell/direnv/default.nix`
- [ ] `configs/nixos/modules/shell/fish/default.nix`
- [ ] `configs/nixos/modules/shell/gitea-axi.nix`
- [ ] `configs/nixos/modules/shell/git.nix`
- [ ] `configs/nixos/modules/shell/herdr.nix`
- [ ] `configs/nixos/modules/shell/nvim/default.nix`
- [ ] `configs/nixos/modules/shell/omp.nix`
- [ ] `configs/nixos/modules/shell/pi/default.nix`
- [ ] `configs/nixos/modules/shell/python.nix`
- [ ] `configs/nixos/modules/shell/rust.nix`
- [ ] `configs/nixos/modules/shell/tmux/default.nix`
- [ ] `configs/nixos/modules/shell/yazi.nix`
- [ ] `src/dotfiles/guests.nix`
- [ ] `configs/nixos/guests/actualbudget.nix`
- [ ] `configs/nixos/guests/copyparty.nix`
- [ ] `configs/nixos/guests/homepage.nix`
- [ ] `configs/nixos/guests/navidrome.nix`
- [ ] `configs/nixos/guests/palworld.nix`
- [ ] `configs/nixos/guests/reverse-proxy.nix`
- [ ] `configs/nixos/guests/valheim.nix`
- [ ] `src/dotfiles/hosts.nix`
- [ ] `configs/nixos/hosts/neogaia/default.nix`
- [ ] `configs/nixos/hosts/neogaia/disk.nix`
- [ ] `configs/nixos/hosts/neogaia/hardware-configuration.nix`
- [ ] `configs/nixos/hosts/pikachu/default.nix`
- [ ] `configs/nixos/hosts/pikachu/disk.nix`
- [ ] `configs/nixos/hosts/pikachu/hardware-configuration.nix`
- [ ] `configs/nixos/hosts/zeus/default.nix`
- [ ] `configs/nixos/hosts/zeus/disk.nix`
- [ ] `configs/nixos/hosts/zeus/hardware-configuration.nix`

View File

@@ -1,141 +0,0 @@
# Conventional Commits 1.0.0
## Summary
The Conventional Commits specification is a lightweight convention on top of commit messages.
It provides an easy set of rules for creating an explicit commit history;
which makes it easier to write automated tools on top of.
This convention dovetails with [SemVer](http://semver.org),
by describing the features, fixes, and breaking changes made in commit messages.
The commit message should be structured as follows:
---
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
---
The commit contains the following structural elements, to communicate intent to the
consumers of your library:
1. **fix:** a commit of the _type_ `fix` patches a bug in your codebase (this correlates with [`PATCH`](http://semver.org/#summary) in Semantic Versioning).
2. **feat:** a commit of the _type_ `feat` introduces a new feature to the codebase (this correlates with [`MINOR`](http://semver.org/#summary) in Semantic Versioning).
3. **BREAKING CHANGE:** a commit that has a footer `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with [`MAJOR`](http://semver.org/#summary) in Semantic Versioning).
A BREAKING CHANGE can be part of commits of any _type_.
4. _types_ other than `fix:` and `feat:` are allowed, for example [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) (based on the [Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type)) recommends `build:`, `chore:`,
`ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others.
5. _footers_ other than `BREAKING CHANGE: <description>` may be provided and follow a convention similar to
[git trailer format](https://git-scm.com/docs/git-interpret-trailers).
Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE).
A scope may be provided to a commit’s type, to provide additional contextual information and is contained within parenthesis, e.g., `feat(parser): add ability to parse arrays`.
## Examples
### Commit message with description and breaking change footer
```
feat: allow provided config object to extend other configs
BREAKING CHANGE: `extends` key in config file is now used for extending other config files
```
### Commit message with `!` to draw attention to breaking change
```
feat!: send an email to the customer when a product is shipped
```
### Commit message with scope and `!` to draw attention to breaking change
```
feat(api)!: send an email to the customer when a product is shipped
```
### Commit message with both `!` and BREAKING CHANGE footer
```
chore!: drop support for Node 6
BREAKING CHANGE: use JavaScript features not available in Node 6.
```
### Commit message with no body
```
docs: correct spelling of CHANGELOG
```
### Commit message with scope
```
feat(lang): add Polish language
```
### Commit message with multi-paragraph body and multiple footers
```
fix: prevent racing of requests
Introduce a request id and a reference to latest request. Dismiss
incoming responses other than from latest request.
Remove timeouts which were used to mitigate the racing issue but are
obsolete now.
Reviewed-by: Z
Refs: #123
```
## Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
1. Commits MUST be prefixed with a type, which consists of a noun, `feat`, `fix`, etc.,
followed by the OPTIONAL scope, OPTIONAL `!`, and REQUIRED terminal colon and space.
2. The type `feat` MUST be used when a commit adds a new feature to your application or library.
3. The type `fix` MUST be used when a commit represents a bug fix for your application.
4. A scope MAY be provided after a type.
A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., `fix(parser):`
5. A description MUST immediately follow the colon and space after the type/scope prefix.
The description is a short summary of the code changes, e.g., _fix: array parsing issue when multiple spaces were contained in string_.
6. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes.
The body MUST begin one blank line after the description.
7. A commit body is free-form and MAY consist of any number of newline separated paragraphs.
8. One or more footers MAY be provided one blank line after the body.
Each footer MUST consist of a word token, followed by either a `:<space>` or `<space>#` separator, followed by a string value (this is inspired by the [git trailer convention](https://git-scm.com/docs/git-interpret-trailers)).
9. A footer’s token MUST use `-` in place of whitespace characters, e.g., `Acked-by` (this helps differentiate the footer section from a multi-paragraph body).
An exception is made for `BREAKING CHANGE`, which MAY also be used as a token.
10. A footer’s value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer token/separator pair is observed.
11. Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the footer.
12. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., _BREAKING CHANGE: environment variables now take precedence over config files_.
13. If included in the type/scope prefix, breaking changes MUST be indicated by a `!` immediately before the `:`.
If `!` is used, `BREAKING CHANGE:` MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change.
14. Types other than `feat` and `fix` MAY be used in your commit messages, e.g., _docs: updated ref docs._
15. The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase.
16. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer.
## FAQ
### How does Conventional Commits handle revert commits?
Reverting code can be complicated: are you reverting multiple commits?
if you are reverting a feature, should the next release instead be a patch?
Conventional Commits does not make an explicit effort to define revert behavior.
Instead we leave it to tooling authors to use the flexibility of _types_ and _footers_ to develop their logic for handling reverts.
One recommendation is to use the `revert` type, and a footer that references the commit SHAs that are being reverted:
```
revert: let us never again speak of the noodle incident
Refs: 676104e, a215868
```

View File

@@ -38,7 +38,7 @@ Use `REPLACE` for terms that have a preferred substitute.
| host | USE | | A machine with a NixOS configuration built by this repository's flake. | | host | USE | | A machine with a NixOS configuration built by this repository's flake. |
| module | USE | | A self-contained NixOS configuration under `configs/nixos/modules/`. | | module | USE | | A self-contained NixOS configuration under `configs/nixos/modules/`. |
| registry | USE | | A typed configuration namespace whose entries can be collected across hosts and reused by other configuration. | | registry | USE | | A typed configuration namespace whose entries can be collected across hosts and reused by other configuration. |
| Conventional Commits | USE | | A commit-message format defined in `docs/conventional-commits.md`. | | Conventional Commits | USE | | A format for commit messages. |
| forge | USE | | A web service that hosts Git repositories, pull requests, issues, and code review comments. Use when the exact provider does not matter. | | forge | USE | | A web service that hosts Git repositories, pull requests, issues, and code review comments. Use when the exact provider does not matter. |
| flake | USE | | A Nix project with pinned inputs and declared outputs. Use for the repository-level build unit defined by `flake.nix`. | | flake | USE | | A Nix project with pinned inputs and declared outputs. Use for the repository-level build unit defined by `flake.nix`. |
| NixOS module | USE | | A Nix file evaluated by `nixosSystem` for a NixOS configuration. | | NixOS module | USE | | A Nix file evaluated by `nixosSystem` for a NixOS configuration. |

107
flake.lock generated
View File

@@ -75,11 +75,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1789232619, "lastModified": 1792048969,
"narHash": "sha256-8pyRIm67V5dtVN3A4Mu0e2a7DIhUhWp6y3NSVS6WBwU=", "narHash": "sha256-+Scy7etDouWzvEV9d6zEDz/TxOoGzfSJau9+yp/zWM4=",
"owner": "chaotic-cx", "owner": "chaotic-cx",
"repo": "nyx", "repo": "nyx",
"rev": "0265d6932168aef12c47f03a565786410ba877e0", "rev": "c283697bfcc3770232139b86c4ad19fd8a62cd6f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -96,11 +96,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1781152676, "lastModified": 1789770686,
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=", "narHash": "sha256-uZkBR7yHdIKUFB5SZdfgh1qkGfI3XmYmI/lTiquxbck=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1", "rev": "725ea35e410ad83be4931d1bff7e090eacaf3563",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -117,11 +117,11 @@
}, },
"locked": { "locked": {
"dir": "pkgs/firefox-addons", "dir": "pkgs/firefox-addons",
"lastModified": 1789185767, "lastModified": 1792036980,
"narHash": "sha256-jaWL9beN3ifjNzNEBxo0abA+TSi0n7Q8LRQvZTxEqaM=", "narHash": "sha256-y28wLbzsIbBDeToXHhAdoTs+lcH/d7/1Pky40N6/CoI=",
"owner": "rycee", "owner": "rycee",
"repo": "nur-expressions", "repo": "nur-expressions",
"rev": "811170bb74b11f3b9489618ce77487cc58c79067", "rev": "cbf0380669792dd8f6dbb891d3be33aae8fa4625",
"type": "gitlab" "type": "gitlab"
}, },
"original": { "original": {
@@ -155,11 +155,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1788450739, "lastModified": 1790898470,
"narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=", "narHash": "sha256-zTwuwezD0w3hpga6a6P8emidzAZFyWqlNeRBlpWOOl8=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993", "rev": "024633cd702b10285db5cb19b40ad48d2399ba60",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -267,11 +267,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1789183968, "lastModified": 1791940130,
"narHash": "sha256-pEWnYdIF1pBMZwarp/rEPzl+Lknhm5hhjSfeLxH8nAA=", "narHash": "sha256-hRCCi4fp5IjLz4cA8+RkUsaYAHzbd7M5FSAjKOq4E+k=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "cd1c9e552f41894aeb5cc5cb353d5a1d61550357", "rev": "92a0ed1be12f1e20093faf8af63bae2c74ad521e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -308,11 +308,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1789267719, "lastModified": 1792098797,
"narHash": "sha256-FIlwGdFjtpGYyL8g/RZZvIxshT9gdwJfcMjHRWHrc4U=", "narHash": "sha256-u4DIIZgznEGzByxXrxWCXK19VcMtTBmi/M9gZ7Jk9bM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "10d0acd7f81e3c035b452daa39ac7d1673880729", "rev": "640725978b99076e18e069f6e7d2959124584f8f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -328,11 +328,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1789127271, "lastModified": 1791721963,
"narHash": "sha256-vL/Fpo7Zxa7wvO8V5FAToUBvP+K3XsMB1wUglTtd/J0=", "narHash": "sha256-BYhx89XJltnRyQeuDKHVL5eSqbbfAcOKuDts7QRsrxQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixos-hardware", "repo": "nixos-hardware",
"rev": "24cfdc1f9344b90a1eee329a3906e2f39f4d0f1e", "rev": "60a624466c08787ee8303393b7d4752d1826f14f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -343,27 +343,24 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1789006805, "lastModified": 1791734076,
"narHash": "sha256-xB8mKMOx1IA9vTDNLmJZ6n4wCMq/cuWBBOzGCRnqxrU=", "narHash": "sha256-0dNcyVeoD61zhucIpt5BAQ+ytd6JzF641Z+A9bgltLo=",
"owner": "NixOS", "rev": "7fd4004d0511991fff3a0524856a9be09e28c5f0",
"repo": "nixpkgs", "type": "tarball",
"rev": "8ce4ef6cb6f871616146b9fe26d2a5ae594e94fe", "url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1090327.7fd4004d0511/nixexprs.tar.zst"
"type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "type": "tarball",
"ref": "nixos-unstable", "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.zst"
"repo": "nixpkgs",
"type": "github"
} }
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1789114715, "lastModified": 1791883866,
"narHash": "sha256-ugpsyk3NM2s87vXfUiIIiibbJ4Pp0JPS5p/3mfs+q+c=", "narHash": "sha256-yvkDdToRZ4IgPc9tSBzzCYJvTLf8n5eE3rgUkpCuD5A=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "21a67dc470149f337cecafbe965d8d252a390518", "rev": "a033e66ab94a6bedf2c3d58d93a36319d3cbbbaf",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -375,11 +372,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1789073787, "lastModified": 1791966044,
"narHash": "sha256-xfX/toC2QV707s06GbP4II/TxYF0fNQj7s5/LClNDKc=", "narHash": "sha256-t1hT8UW97rFCWCJO7/pClvu0AhLN5V3uir2M0ic4w/Q=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "aff8a0b28396750446e5537a96461bc4facdb287", "rev": "319e4cebef10cbb636c6ea392137b52ad105336a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -391,11 +388,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1789149629, "lastModified": 1791956154,
"narHash": "sha256-H6GwaZzZf+4npqv0tph94w9tZddSjFjmQrVsW0z78uk=", "narHash": "sha256-jBYGPi4udQ9uo4KQp9m2J0546nJVQitG15gvZSaVVAs=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "eaad089433ca2bb662274377d33df3d0e51ef28b", "rev": "55bfb4d7437bd4e09ab7cba58a43bd68eb310044",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -414,11 +411,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1789067777, "lastModified": 1792012326,
"narHash": "sha256-gN3OnYgJwCK0HX01dQGWusnirJuVHE9WigmSqOnyuCE=", "narHash": "sha256-DApZIjaKdU4XzglZ56yrPYLhG/ajoyuQ4g4s8tkHqxU=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixvim", "repo": "nixvim",
"rev": "16baff93297c63b6d4c497f00ffcf0b844b06fc6", "rev": "8485794923eb406ce3728af320392226608f724b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -476,11 +473,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1788914643, "lastModified": 1792056300,
"narHash": "sha256-4GuMPW90JSxXWDPUB9M+1m7fYbe3H0apOd86/zBQ2Kw=", "narHash": "sha256-YU1LjDe1o2Ymg5zEiCHbmMXLkDCtxHQRiVqZ3nC5GGk=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "13616fff713a9f94055c66f15687ebdc17a335df", "rev": "44931708c62937c2b1bf7953902989fa5bb9ee1b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -509,11 +506,11 @@
"tinted-zed": "tinted-zed" "tinted-zed": "tinted-zed"
}, },
"locked": { "locked": {
"lastModified": 1787771653, "lastModified": 1791574276,
"narHash": "sha256-DkkJSBOXWV/mja5Vy8az5g1KpZlsbUwlmH0BsQhowaQ=", "narHash": "sha256-UMnLnZ3JlA7UTBNNNx1rNsXYoPP/cirXLCKGiQ/yDx8=",
"owner": "danth", "owner": "danth",
"repo": "stylix", "repo": "stylix",
"rev": "5e3809851f486e7fc7e84b40f174c74b60ecc784", "rev": "2856c4c6a845b9bec98828ef197abe176e4806bb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -525,11 +522,11 @@
"systems": { "systems": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1774449309, "lastModified": 1791244623,
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=", "narHash": "sha256-uuanbkFk8a313Zfrxdcz8Us/yQtaxNnKJtX8v0ZPpd8=",
"owner": "nix-systems", "owner": "nix-systems",
"repo": "default", "repo": "default",
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150", "rev": "d36eeed142f88b5a636f2e3856a53c61654cf29b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -626,11 +623,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1786901030, "lastModified": 1791885023,
"narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=", "narHash": "sha256-C3alQ2MEt0zVrUSho5NUDo3jmhbff72++bfwoqT9Omo=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa", "rev": "0d6d739a833ba9a9c93034862e4a7b061a062a1c",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -24,7 +24,6 @@ in
configs/nixos/hosts/neogaia/hardware-configuration.nix \ configs/nixos/hosts/neogaia/hardware-configuration.nix \
configs/nixos/hosts/pikachu/hardware-configuration.nix \ configs/nixos/hosts/pikachu/hardware-configuration.nix \
configs/nixos/hosts/zeus/hardware-configuration.nix \ configs/nixos/hosts/zeus/hardware-configuration.nix \
src/natu/testdata \
third_party \ third_party \
-- . -- .
treefmt treefmt

View File

@@ -6,7 +6,6 @@ inputs.treefmt-nix.lib.evalModule inputs.nixpkgs.legacyPackages.${system} {
settings.formatter.nixfmt.excludes = [ settings.formatter.nixfmt.excludes = [
"configs/nixos/hosts/*/hardware-configuration.nix" "configs/nixos/hosts/*/hardware-configuration.nix"
"src/natu/testdata/*.nix"
"third_party/**" "third_party/**"
]; ];
} }

View File

@@ -1,8 +1,4 @@
{ { pkgs, root }:
pi-coding-agent, pkgs.callPackage (root + "/third_party/pi/nix/package.nix") {
root, source = root + "/third_party/pi";
}:
pi-coding-agent.overrideAttrs {
src = root + "/third_party/pi";
patches = [ ];
} }

View File

@@ -1,59 +0,0 @@
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
rust_library(
name = "natu_lib",
crate_name = "natu",
srcs = [
"ast.rs",
"file_store.rs",
"natu.rs",
"parser.rs",
"rules.rs",
"rules/let_order.rs",
"rules/list_to_attrs_map.rs",
"rules/manual_inherit.rs",
"rules/manual_inherit_from.rs",
"rules/redundant_let.rs",
"rules/redundant_parameter_forwarding.rs",
"rules/unquoted_uri.rs",
"scope_graph.rs",
],
deps = [
"//src/status:status",
"//third_party/crates:rnix",
"//third_party/crates/rowan-0.16.1:rowan",
],
)
rust_test(
name = "file_store_test",
srcs = ["file_store_test.rs"],
deps = [":natu_lib"],
)
rust_test(
name = "natu_test",
srcs = ["natu_test.rs"],
data = [
"testdata/clean.nix",
"testdata/invalid.nix",
"testdata/lint_findings.nix",
],
deps = [
":natu_lib",
"//src/status:status",
"//third_party/crates:googletest",
],
)
rust_binary(
name = "natu",
crate_name = "natu_cli",
srcs = ["natu_main.rs"],
deps = [
":natu_lib",
"//src/monorepo:monorepo",
"//src/status:status",
"//third_party/crates:clap",
],
)

View File

@@ -1,21 +0,0 @@
[package]
name = "natu"
version = "0.1.0"
edition = "2024"
[lib]
path = "natu.rs"
[[bin]]
name = "natu"
path = "natu_main.rs"
[dependencies]
clap = { workspace = true, features = ["derive"] }
monorepo = { workspace = true }
rnix = { workspace = true }
rowan = { workspace = true }
status = { workspace = true }
[dev-dependencies]
googletest = { workspace = true }

View File

@@ -1,92 +0,0 @@
use crate::FileId;
#[derive(Debug)]
pub struct ParsedFile {
pub file_id: FileId,
pub root: Expr,
pub nodes: Vec<Node>,
pub tokens: Vec<Token>,
pub errors: Vec<ParseError>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
pub range: SourceRange,
pub data: NodeData,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceRange {
pub start: usize,
pub end: usize,
}
impl std::fmt::Display for SourceRange {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}..{}", self.start, self.end)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeData {
Binding(Binding),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Binding {
pub range: SourceRange,
pub key: Ident,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ident {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Ident(Ident),
Select(Select),
Apply(Apply),
Let {
range: SourceRange,
bindings: Vec<Binding>,
body: Box<Expr>,
},
AttrSet(Vec<Binding>),
Lambda {
parameters: Vec<Ident>,
body: Box<Expr>,
},
Other(Vec<Expr>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Select {
pub receiver: String,
pub attr: Ident,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Apply {
pub lambda: Box<Expr>,
pub argument: Box<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
pub range: SourceRange,
pub data: TokenData,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenData {
UnquotedUri(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub range: SourceRange,
pub message: String,
}

View File

@@ -1,33 +0,0 @@
use std::path::PathBuf;
#[derive(Debug, Default)]
pub struct FileStore {
files: Vec<SourceFile>,
}
impl FileStore {
pub fn add(&mut self, file: SourceFile) -> FileId {
let id = FileId(self.files.len());
self.files.push(file);
id
}
pub fn get(&self, id: FileId) -> &SourceFile {
&self.files[id.0]
}
}
impl From<Vec<SourceFile>> for FileStore {
fn from(files: Vec<SourceFile>) -> Self {
Self { files }
}
}
#[derive(Debug)]
pub struct SourceFile {
pub path: PathBuf,
pub source: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileId(usize);

View File

@@ -1,33 +0,0 @@
use std::path::PathBuf;
use natu::{FileStore, SourceFile};
fn source_file(path: &str, source: &str) -> SourceFile {
SourceFile {
path: PathBuf::from(path),
source: source.to_owned(),
}
}
#[test]
fn added_file_can_be_retrieved_by_id() {
let mut store = FileStore::default();
let id = store.add(source_file("first.nix", "{ inherit foo; }"));
let file = store.get(id);
assert_eq!(PathBuf::from("first.nix"), file.path);
assert_eq!("{ inherit foo; }", file.source);
}
#[test]
fn file_ids_refer_to_their_added_files() {
let mut store = FileStore::default();
let first = store.add(source_file("first.nix", "first"));
let second = store.add(source_file("second.nix", "second"));
assert_ne!(first, second);
assert_eq!("first", store.get(first).source);
assert_eq!("second", store.get(second).source);
}

View File

@@ -1,121 +0,0 @@
use std::collections::HashSet;
mod ast;
mod file_store;
mod parser;
mod rules;
mod scope_graph;
use scope_graph::ScopeGraph;
pub use file_store::{FileId, FileStore, SourceFile};
pub use rules::{Diagnostic, Finding};
#[derive(Debug)]
pub struct Report {
pub files: FileStore,
pub diagnostics: Vec<Diagnostic>,
}
impl Report {
pub fn is_clean(&self) -> bool {
self.diagnostics.is_empty()
}
}
impl std::fmt::Display for Report {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut reported_let_order_files = HashSet::new();
for diagnostic in &self.diagnostics {
if diagnostic.finding.rule_id == "let_order" {
if !reported_let_order_files.insert(diagnostic.file_id) {
continue;
}
let file = self.files.get(diagnostic.file_id);
writeln!(
formatter,
"{} [let_order] Let bindings must follow dependency order",
file.path.display()
)?;
let mut findings = self
.diagnostics
.iter()
.filter(|candidate| {
candidate.file_id == diagnostic.file_id
&& candidate.finding.rule_id == "let_order"
})
.collect::<Vec<_>>();
findings.sort_by_key(|finding| finding.finding.range.start);
for finding in findings {
let (start, _) = line_range(&file.source, finding.finding.range);
let expected_order =
finding.finding.expected_order.as_ref().expect("let order");
writeln!(
formatter,
" let at line {start}: {}",
expected_order.join(", ")
)?;
}
continue;
}
let file = self.files.get(diagnostic.file_id);
let (start, end) = line_range(&file.source, diagnostic.finding.range);
let location = if start == end {
start.to_string()
} else {
format!("{start}-{end}")
};
writeln!(formatter, "{}:{location} {diagnostic}", file.path.display())?;
}
Ok(())
}
}
fn line_range(source: &str, range: ast::SourceRange) -> (usize, usize) {
let line_at = |offset| {
source
.bytes()
.take(offset)
.filter(|byte| *byte == b'\n')
.count()
+ 1
};
let start = line_at(range.start);
let end = line_at(range.end);
(start, end)
}
/// Analyzes source files and returns the diagnostics Natu finds.
pub fn future_sight(files: Vec<SourceFile>) -> status::Result<Report> {
let mut store = FileStore::default();
let mut diagnostics = Vec::new();
for file in files {
let file_id = store.add(file);
let file = store.get(file_id);
let parsed = parser::parse_file(file_id, &file.source);
if !parsed.errors.is_empty() {
return Err(status::Status::failed_precondition(format!(
"failed to parse {}",
file.path.display()
)));
}
let graph = ScopeGraph::from_file(&parsed);
diagnostics.extend(rules::run_rules(&parsed, &graph, &file.source));
}
Ok(Report {
files: store,
diagnostics,
})
}

View File

@@ -1,105 +0,0 @@
use clap::Parser;
use natu::{Report, SourceFile};
use status::ResultExt;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
fn main() -> ExitCode {
let args = Args::parse();
match analyze(args) {
Ok(report) if report.is_clean() => {
println!("No findings.");
ExitCode::SUCCESS
}
Ok(report) => {
print!("{report}");
ExitCode::from(1)
}
Err(error) => {
eprintln!("{error}");
ExitCode::from(2)
}
}
}
#[derive(Debug, Parser)]
struct Args {
#[arg(value_name = "PATH", default_value = ".")]
paths: Vec<PathBuf>,
}
fn analyze(args: Args) -> status::Result<Report> {
let paths: Vec<PathBuf> = args
.paths
.into_iter()
.map(monorepo::workspace::resolve_workspace_path)
.collect();
let files: Vec<SourceFile> = paths
.iter()
.map(|path| collect_source_files(path))
.collect::<status::Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect();
natu::future_sight(files)
}
fn collect_source_files(path: &Path) -> status::Result<Vec<SourceFile>> {
let metadata = std::fs::symlink_metadata(path)
.with_context(|| format!("failed to read metadata for {}", path.display()))?;
if metadata.file_type().is_dir() {
if is_excluded_dir(path) {
return Ok(Vec::new());
}
let mut files = Vec::new();
for entry in std::fs::read_dir(path)
.with_context(|| format!("failed to read directory {}", path.display()))?
{
let entry =
entry.with_context(|| format!("failed to read entry in {}", path.display()))?;
files.extend(collect_source_files(&entry.path())?);
}
return Ok(files);
}
if metadata.file_type().is_file() && is_nix_file(path) {
return Ok(vec![SourceFile {
path: path.to_path_buf(),
source: std::fs::read_to_string(path)
.with_context(|| format!("failed to read source file {}", path.display()))?,
}]);
}
Ok(Vec::new())
}
// TODO: Add configurable exclusions.
fn is_excluded_dir(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
matches!(
name,
".git"
| "bazel-bin"
| "bazel-dev"
| "bazel-out"
| "bazel-testlogs"
| "target"
| "testdata"
| "third_party"
) || name == "result"
|| name.starts_with("result-")
})
}
fn is_nix_file(path: &Path) -> bool {
path.extension().is_some_and(|extension| extension == "nix")
}

View File

@@ -1,303 +0,0 @@
use std::path::{Path, PathBuf};
use googletest::matchers::{contains, eq, is_true};
use googletest::{OrFail, expect_that, gtest};
use status::Code;
use natu::SourceFile;
fn fixture_source(path: &str) -> googletest::Result<SourceFile> {
Ok(SourceFile {
path: PathBuf::from(path),
source: std::fs::read_to_string(fixture_path(path)?).or_fail()?,
})
}
fn fixture_path(path: &str) -> googletest::Result<PathBuf> {
if Path::new(path).exists() {
return Ok(PathBuf::from(path));
}
let runfiles = PathBuf::from(std::env::var("TEST_SRCDIR").or_fail()?);
let workspace = std::env::var("TEST_WORKSPACE").or_fail()?;
Ok(runfiles.join(workspace).join("src/natu").join(path))
}
#[gtest]
fn natu_accepts_clean_source() -> googletest::Result<()> {
let report = natu::future_sight(vec![fixture_source("testdata/clean.nix")?]).or_fail()?;
expect_that!(report.is_clean(), is_true());
Ok(())
}
#[gtest]
fn natu_rejects_invalid_source() -> googletest::Result<()> {
let error = natu::future_sight(vec![fixture_source("testdata/invalid.nix")?])
.err()
.or_fail()?;
expect_that!(error.code(), eq(Code::FailedPrecondition));
Ok(())
}
#[gtest]
fn natu_reports_out_of_order_let_bindings() -> googletest::Result<()> {
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;
let let_order_count = report
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "let_order")
.count();
expect_that!(let_order_count, eq(2));
expect_that!(
report.to_string(),
eq(concat!(
"testdata/lint_findings.nix:11 [manual_inherit] Use `inherit` for this assignment\n",
"testdata/lint_findings.nix:12 [manual_inherit_from] Use `inherit` for this selected attribute\n",
"testdata/lint_findings.nix:13 [unquoted_uri] URI literals should be quoted\n",
"testdata/lint_findings.nix [let_order] Let bindings must follow dependency order\n",
" let at line 1: first, second, nested\n",
" let at line 4: nestedSecond, nestedFirst\n",
))
);
Ok(())
}
#[gtest]
fn natu_preserves_suppressed_bindings() -> googletest::Result<()> {
let report = natu::future_sight(vec![SourceFile {
path: PathBuf::from("testdata/nolint.nix"),
source: "{ config, ... }: let # NOLINT(redundant_let)\n cfg = config.example; # NOLINT(let_order)\n value = cfg;\nin value\n".into(),
}])
.or_fail()?;
expect_that!(report.is_clean(), is_true());
Ok(())
}
#[gtest]
fn natu_reports_select_dependency_order() -> googletest::Result<()> {
let report = natu::future_sight(vec![SourceFile {
path: PathBuf::from("testdata/select_dependencies.nix"),
source: r#"
let
dependency = { value = 1; };
entry = dependency.value;
in
entry
"#
.into(),
}])
.or_fail()?;
let count = report
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "let_order")
.count();
expect_that!(count, eq(1));
Ok(())
}
#[gtest]
fn natu_reports_redundant_let() -> googletest::Result<()> {
let report = natu::future_sight(vec![SourceFile {
path: PathBuf::from("testdata/redundant_let.nix"),
source: r#"
let
helper = 1;
value = helper;
in
value
"#
.into(),
}])
.or_fail()?;
let count = report
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "redundant_let")
.count();
expect_that!(count, eq(1));
Ok(())
}
#[gtest]
fn natu_accepts_nonredundant_let() -> googletest::Result<()> {
let report = natu::future_sight(vec![
SourceFile {
path: PathBuf::from("testdata/recursive_let.nix"),
source: r#"
let
recursive = recursive;
in
recursive
"#
.into(),
},
SourceFile {
path: PathBuf::from("testdata/dependent_let.nix"),
source: r#"
let
value = 1;
dependent = value;
in
value
"#
.into(),
},
])
.or_fail()?;
let count = report
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "redundant_let")
.count();
expect_that!(count, eq(0));
Ok(())
}
#[gtest]
fn natu_reports_redundant_parameter_forwarding() -> googletest::Result<()> {
let report = natu::future_sight(vec![
SourceFile {
path: PathBuf::from("testdata/attribute_parameter.nix"),
source: r#"
{ hosts, ... }:
let
collect = { hosts }: hosts;
in
collect { inherit hosts; }
"#
.into(),
},
SourceFile {
path: PathBuf::from("testdata/plain_parameter.nix"),
source: r#"
value:
let
transform = value: value;
in
transform value
"#
.into(),
},
SourceFile {
path: PathBuf::from("testdata/curried_parameter.nix"),
source: r#"
{ hosts, ... }:
let
transform = prefix: hosts: suffix: hosts;
in
transform "prefix" hosts "suffix"
"#
.into(),
},
SourceFile {
path: PathBuf::from("testdata/local_parameter.nix"),
source: r#"
let
transform = value: value;
in
transform value
"#
.into(),
},
])
.or_fail()?;
let count = report
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "redundant_parameter_forwarding")
.count();
expect_that!(count, eq(3));
Ok(())
}
#[gtest]
fn natu_reports_list_to_attrs_after_map() -> googletest::Result<()> {
let report = natu::future_sight(vec![SourceFile {
path: PathBuf::from("testdata/list_to_attrs.nix"),
source: r#"
let
generated = lib.listToAttrs (
map (item: lib.nameValuePair item item) items
);
generated_pairs = lib.listToAttrs (
concatLists (
mapAttrsToList (name: _: lib.nameValuePair name name) attrs
)
);
precomputed = lib.listToAttrs pairs;
in
{
inherit generated generated_pairs precomputed;
}
"#
.into(),
}])
.or_fail()?;
let count = report
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "list_to_attrs_map")
.count();
expect_that!(count, eq(1));
Ok(())
}
#[gtest]
fn natu_reports_unquoted_uri() -> googletest::Result<()> {
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;
let findings = report
.diagnostics
.iter()
.map(|diagnostic| {
(
report.files.get(diagnostic.file_id).path.clone(),
diagnostic.finding.rule_id,
)
})
.collect::<Vec<_>>();
expect_that!(
findings,
contains(eq(&(
PathBuf::from("testdata/lint_findings.nix"),
"unquoted_uri"
)))
);
Ok(())
}
#[gtest]
fn natu_reports_manual_inherit() -> googletest::Result<()> {
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;
let manual_inherit_count = report
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "manual_inherit")
.count();
expect_that!(manual_inherit_count, eq(1));
Ok(())
}
#[gtest]
fn natu_reports_manual_inherit_from() -> googletest::Result<()> {
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;
let manual_inherit_from_count = report
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.finding.rule_id == "manual_inherit_from")
.count();
expect_that!(manual_inherit_from_count, eq(1));
Ok(())
}

View File

@@ -1,295 +0,0 @@
use rnix::ast::{Attr, HasEntry};
use rowan::ast::AstNode;
use crate::{FileId, ast};
pub fn parse_file(file_id: FileId, source: &str) -> ast::ParsedFile {
let parse = rnix::Root::parse(source);
let source_len = source.len();
let syntax = parse.syntax();
let root = rnix::ast::Root::cast(syntax.clone()).expect("parser root is a root node");
let root = convert_expr(root.expr().expect("parser root has an expression"));
let (nodes, tokens) = convert_syntax(syntax);
ast::ParsedFile {
file_id,
root,
nodes,
tokens,
errors: parse
.errors()
.iter()
.map(|error| convert_parse_error(error, source_len))
.collect(),
}
}
fn convert_syntax(syntax: rnix::SyntaxNode) -> (Vec<ast::Node>, Vec<ast::Token>) {
let mut nodes = Vec::new();
let mut tokens = Vec::new();
for element in syntax.descendants_with_tokens() {
match element {
rnix::NodeOrToken::Node(node) => nodes.extend(convert_node(node)),
rnix::NodeOrToken::Token(token) => tokens.extend(convert_token(token)),
}
}
(nodes, tokens)
}
fn convert_expr(expr: rnix::ast::Expr) -> ast::Expr {
match expr {
rnix::ast::Expr::Ident(ident) => ast::Expr::Ident(ident.into()),
rnix::ast::Expr::Select(select) => convert_select_expr(select),
rnix::ast::Expr::Apply(apply) => ast::Expr::Apply(ast::Apply {
lambda: Box::new(convert_expr(apply.lambda().expect("applications have a lambda"))),
argument: Box::new(convert_expr(
apply.argument().expect("applications have an argument"),
)),
}),
rnix::ast::Expr::LetIn(let_in) => ast::Expr::Let {
range: let_in.syntax().text_range().into(),
bindings: let_in
.attrpath_values()
.filter_map(convert_binding_ast)
.collect(),
body: Box::new(convert_expr(
let_in.body().expect("let expressions have bodies"),
)),
},
rnix::ast::Expr::AttrSet(attr_set) => ast::Expr::AttrSet(convert_attr_set(attr_set)),
rnix::ast::Expr::Lambda(lambda) => ast::Expr::Lambda {
parameters: lambda_parameters(&lambda),
body: Box::new(convert_expr(lambda.body().expect("lambdas have bodies"))),
},
expr => ast::Expr::Other(
expr.syntax()
.children()
.filter_map(rnix::ast::Expr::cast)
.map(convert_expr)
.collect(),
),
}
}
fn convert_select_expr(select: rnix::ast::Select) -> ast::Expr {
select.clone().try_into().map(ast::Expr::Select).unwrap_or_else(|_| {
ast::Expr::Other(
select
.syntax()
.children()
.filter_map(rnix::ast::Expr::cast)
.map(convert_expr)
.collect(),
)
})
}
fn convert_binding_ast(binding: rnix::ast::AttrpathValue) -> Option<ast::Binding> {
let mut attrs = binding.attrpath()?.attrs();
let Attr::Ident(key) = attrs.next()? else {
return None;
};
Some(ast::Binding {
range: binding.syntax().text_range().into(),
key: key.into(),
value: convert_expr(binding.value()?),
})
}
fn convert_attr_set(attr_set: rnix::ast::AttrSet) -> Vec<ast::Binding> {
attr_set
.entries()
.flat_map(|entry| match entry {
rnix::ast::Entry::AttrpathValue(binding) => {
convert_binding_ast(binding).into_iter().collect()
}
rnix::ast::Entry::Inherit(inherit) => convert_inherit(inherit),
})
.collect()
}
fn convert_inherit(inherit: rnix::ast::Inherit) -> Vec<ast::Binding> {
let value = inherit
.from()
.and_then(|from| from.expr())
.map(convert_expr);
inherit
.attrs()
.filter_map(|attr| match attr {
Attr::Ident(ident) => Some(ident),
_ => None,
})
.map(|key| ast::Binding {
range: key.syntax().text_range().into(),
value: value
.clone()
.unwrap_or_else(|| ast::Expr::Ident(key.clone().into())),
key: key.into(),
})
.collect()
}
fn lambda_parameters(lambda: &rnix::ast::Lambda) -> Vec<ast::Ident> {
match lambda.param() {
Some(rnix::ast::Param::IdentParam(parameter)) => {
parameter.ident().map(Into::into).into_iter().collect()
}
Some(rnix::ast::Param::Pattern(pattern)) => pattern
.pat_entries()
.filter_map(|entry| entry.ident())
.chain(pattern.pat_bind().and_then(|binding| binding.ident()))
.map(Into::into)
.collect(),
None => Vec::new(),
}
}
fn convert_node(node: rnix::SyntaxNode) -> Option<ast::Node> {
match node.kind() {
rnix::SyntaxKind::NODE_ATTRPATH_VALUE => convert_binding(node),
_ => None,
}
}
fn convert_binding(node: rnix::SyntaxNode) -> Option<ast::Node> {
let binding = rnix::ast::AttrpathValue::cast(node)?;
let range = binding.syntax().text_range().into();
let mut attrs = binding.attrpath()?.attrs();
let Attr::Ident(key) = attrs.next()? else {
return None;
};
if attrs.next().is_some() {
return None;
}
Some(ast::Node {
range,
data: ast::NodeData::Binding(ast::Binding {
range,
key: key.into(),
value: convert_expr(binding.value()?),
}),
})
}
impl TryFrom<rnix::ast::AttrpathValue> for ast::Binding {
type Error = ();
fn try_from(binding: rnix::ast::AttrpathValue) -> Result<Self, Self::Error> {
let mut attrs = binding.attrpath().ok_or(())?.attrs();
let key = match attrs.next().ok_or(())? {
Attr::Ident(ident) => ident.into(),
_ => return Err(()),
};
if attrs.next().is_some() {
return Err(());
}
let value = binding.value().ok_or(())?.try_into()?;
Ok(Self {
range: binding.syntax().text_range().into(),
key,
value,
})
}
}
impl TryFrom<rnix::ast::Expr> for ast::Expr {
type Error = ();
fn try_from(expr: rnix::ast::Expr) -> Result<Self, Self::Error> {
match expr {
rnix::ast::Expr::Ident(ident) => Ok(Self::Ident(ident.into())),
rnix::ast::Expr::Select(select) => Ok(Self::Select(select.try_into()?)),
_ => Err(()),
}
}
}
impl From<rnix::ast::Ident> for ast::Ident {
fn from(ident: rnix::ast::Ident) -> Self {
Self {
name: ident.to_string(),
}
}
}
impl TryFrom<rnix::ast::Select> for ast::Select {
type Error = ();
fn try_from(select: rnix::ast::Select) -> Result<Self, Self::Error> {
if select.or_token().is_some() {
return Err(());
}
let mut attrs = select.attrpath().ok_or(())?.attrs();
let attr = match attrs.next().ok_or(())? {
Attr::Ident(ident) => ident.into(),
_ => return Err(()),
};
if attrs.next().is_some() {
return Err(());
}
Ok(Self {
receiver: select.expr().ok_or(())?.syntax().to_string(),
attr,
})
}
}
fn convert_token(token: rnix::SyntaxToken) -> Option<ast::Token> {
match token.kind() {
rnix::SyntaxKind::TOKEN_URI => Some(ast::Token {
range: token.text_range().into(),
data: ast::TokenData::UnquotedUri(token.text().to_string()),
}),
_ => None,
}
}
fn convert_parse_error(error: &rnix::ParseError, source_len: usize) -> ast::ParseError {
ast::ParseError {
range: parse_error_range(error, source_len),
message: error.to_string(),
}
}
fn parse_error_range(error: &rnix::ParseError, source_len: usize) -> ast::SourceRange {
match error {
rnix::ParseError::Unexpected(range)
| rnix::ParseError::UnexpectedExtra(range)
| rnix::ParseError::UnexpectedWanted(_, range, _)
| rnix::ParseError::UnexpectedDoubleBind(range)
| rnix::ParseError::DuplicatedArgs(range, _) => (*range).into(),
rnix::ParseError::UnexpectedEOF
| rnix::ParseError::UnexpectedEOFWanted(_)
| rnix::ParseError::RecursionLimitExceeded => ast::SourceRange {
start: source_len,
end: source_len,
},
_ => ast::SourceRange {
start: source_len,
end: source_len,
},
}
}
impl From<rnix::TextRange> for ast::SourceRange {
fn from(range: rnix::TextRange) -> Self {
Self {
start: usize::from(range.start()),
end: usize::from(range.end()),
}
}
}

View File

@@ -1,187 +0,0 @@
use crate::FileId;
use crate::ast;
use crate::rules::list_to_attrs_map::ListToAttrsMap;
use crate::rules::manual_inherit::ManualInherit;
use crate::rules::manual_inherit_from::ManualInheritFrom;
use crate::rules::unquoted_uri::UnquotedUri;
use crate::scope_graph::ScopeGraph;
pub mod let_order;
pub mod list_to_attrs_map;
pub mod manual_inherit;
pub mod manual_inherit_from;
pub mod redundant_let;
pub mod redundant_parameter_forwarding;
pub mod unquoted_uri;
#[derive(Debug)]
pub struct Diagnostic {
pub file_id: FileId,
pub finding: Finding,
}
#[derive(Debug)]
pub struct Finding {
pub rule_id: &'static str,
pub range: ast::SourceRange,
pub message: &'static str,
pub binding: Option<String>,
pub expected_order: Option<Vec<String>>,
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"[{}] {}",
self.finding.rule_id, self.finding.message
)?;
if let Some(binding) = &self.finding.binding {
write!(formatter, ": {binding}")?;
}
Ok(())
}
}
pub trait NodeRule {
type Node;
const ID: &'static str;
fn select(data: &ast::NodeData) -> Option<&Self::Node>;
fn check(node: &Self::Node) -> Option<&'static str>;
}
pub trait TokenRule {
type Token;
const ID: &'static str;
fn select(data: &ast::TokenData) -> Option<&Self::Token>;
fn check(token: &Self::Token) -> Option<&'static str>;
}
pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, source: &str) -> Vec<Diagnostic> {
let node_diagnostics = file
.nodes
.iter()
.flat_map(|node| {
[
run_node_rule::<ListToAttrsMap>(node),
run_node_rule::<ManualInherit>(node),
run_node_rule::<ManualInheritFrom>(node),
]
})
.flatten();
let token_diagnostics = file
.tokens
.iter()
.flat_map(|token| [run_token_rule::<UnquotedUri>(token)])
.flatten();
let let_order_diagnostics =
let_order::check(graph, source)
.into_iter()
.map(|finding| Finding {
rule_id: "let_order",
range: finding.range,
message: "Let bindings must follow dependency order",
binding: None,
expected_order: Some(finding.expected_order),
});
let redundant_let_diagnostics =
redundant_let::check(&file.root)
.into_iter()
.map(|finding| Finding {
rule_id: "redundant_let",
range: finding.range,
message: "Inline this returned let binding",
binding: None,
expected_order: None,
});
let redundant_parameter_forwarding_diagnostics =
redundant_parameter_forwarding::check(&file.root)
.into_iter()
.map(|finding| Finding {
rule_id: "redundant_parameter_forwarding",
range: finding.range,
message: "Use the outer parameter instead of forwarding it",
binding: None,
expected_order: None,
});
node_diagnostics
.chain(token_diagnostics)
.chain(let_order_diagnostics)
.chain(redundant_let_diagnostics)
.chain(redundant_parameter_forwarding_diagnostics)
.filter(|finding| !is_suppressed(source, finding.range, finding.rule_id))
.map(|finding| Diagnostic {
file_id: file.file_id,
finding,
})
.collect()
}
pub fn is_suppressed(source: &str, range: ast::SourceRange, rule_id: &str) -> bool {
let line = source[range.start..].lines().next().unwrap_or_default();
line_suppresses(line, rule_id) || preceding_line_suppresses(source, range, rule_id)
}
fn preceding_line_suppresses(source: &str, range: ast::SourceRange, rule_id: &str) -> bool {
let Some((preceding_source, _current_line)) = source[..range.start].rsplit_once('\n') else {
return false;
};
let preceding_line = preceding_source.lines().next_back().unwrap_or_default();
line_suppresses(preceding_line, rule_id)
}
fn line_suppresses(line: &str, rule_id: &str) -> bool {
let Some(comment) = line.split_once('#').map(|(_, comment)| comment.trim()) else {
return false;
};
if comment == "NOLINT" {
return true;
}
comment
.strip_prefix("NOLINT(")
.and_then(|rules| rules.strip_suffix(')'))
.is_some_and(|rules| {
rules
.split(',')
.any(|candidate| candidate.trim() == rule_id)
})
}
fn run_node_rule<R>(node: &ast::Node) -> Option<Finding>
where
R: NodeRule,
{
let typed_node = R::select(&node.data)?;
let message = R::check(typed_node)?;
Some(Finding {
rule_id: R::ID,
range: node.range,
message,
binding: None,
expected_order: None,
})
}
fn run_token_rule<R>(token: &ast::Token) -> Option<Finding>
where
R: TokenRule,
{
let typed_token = R::select(&token.data)?;
let message = R::check(typed_token)?;
Some(Finding {
rule_id: R::ID,
range: token.range,
message,
binding: None,
expected_order: None,
})
}

View File

@@ -1,87 +0,0 @@
use crate::{
ast,
rules::is_suppressed,
scope_graph::{LetScope, NodeId, ScopeGraph},
};
pub struct Finding {
pub range: ast::SourceRange,
pub expected_order: Vec<String>,
}
pub fn check(graph: &ScopeGraph, source: &str) -> Vec<Finding> {
graph
.let_scopes()
.iter()
.flat_map(|let_scope| {
let expected = expected_order(graph, let_scope)
.into_iter()
.filter(|declaration| !binding_suppressed(let_scope, *declaration, source))
.collect::<Vec<_>>();
let actual = let_scope
.bindings
.iter()
.filter(|binding| !is_suppressed(source, binding.range, "let_order"))
.map(|binding| binding.declaration)
.collect::<Vec<_>>();
(!actual.starts_with(&expected)).then(|| Finding {
range: let_scope.range,
expected_order: expected
.iter()
.map(|declaration| binding_name(let_scope, *declaration).to_owned())
.collect(),
})
})
.collect()
}
fn binding_suppressed(let_scope: &LetScope, declaration: NodeId, source: &str) -> bool {
let binding = let_scope
.bindings
.iter()
.find(|binding| binding.declaration == declaration)
.expect("let declaration");
is_suppressed(source, binding.range, "let_order")
}
fn binding_name(let_scope: &LetScope, declaration: NodeId) -> &str {
&let_scope
.bindings
.iter()
.find(|binding| binding.declaration == declaration)
.expect("let declaration")
.name
}
fn expected_order(graph: &ScopeGraph, let_scope: &LetScope) -> Vec<NodeId> {
let mut order = Vec::new();
visit_references(graph, &let_scope.entry_references, let_scope, &mut order);
order
}
fn visit_references(
graph: &ScopeGraph,
references: &[NodeId],
let_scope: &LetScope,
order: &mut Vec<NodeId>,
) {
for reference in references {
for declaration in graph.resolve(*reference) {
let Some(binding) = let_scope
.bindings
.iter()
.find(|binding| binding.declaration == declaration)
else {
continue;
};
if order.contains(&declaration) {
continue;
}
order.push(declaration);
visit_references(graph, &binding.references, let_scope, order);
}
}
}

View File

@@ -1,58 +0,0 @@
use crate::{ast, rules::NodeRule};
pub struct ListToAttrsMap;
impl NodeRule for ListToAttrsMap {
type Node = ast::Binding;
const ID: &'static str = "list_to_attrs_map";
fn select(data: &ast::NodeData) -> Option<&Self::Node> {
let ast::NodeData::Binding(binding) = data;
Some(binding)
}
fn check(node: &Self::Node) -> Option<&'static str> {
matches_list_to_attrs_map(&node.value)
.then_some("Use `lib.genAttrs'` instead of `lib.listToAttrs` after `map`")
}
}
fn matches_list_to_attrs_map(expr: &ast::Expr) -> bool {
let ast::Expr::Apply(application) = unwrap_expr(expr) else {
return false;
};
is_lib_list_to_attrs(&application.lambda) && is_map_application(&application.argument)
}
fn unwrap_expr(expr: &ast::Expr) -> &ast::Expr {
match expr {
ast::Expr::Other(expressions) if expressions.len() == 1 => unwrap_expr(&expressions[0]),
_ => expr,
}
}
fn is_lib_list_to_attrs(expr: &ast::Expr) -> bool {
matches!(
unwrap_expr(expr),
ast::Expr::Select(select) if select.receiver == "lib" && select.attr.name == "listToAttrs"
)
}
fn is_map_application(expr: &ast::Expr) -> bool {
let ast::Expr::Apply(application) = unwrap_expr(expr) else {
return false;
};
is_map(&application.lambda)
}
fn is_map(expr: &ast::Expr) -> bool {
match unwrap_expr(expr) {
ast::Expr::Ident(ident) => ident.name == "map",
ast::Expr::Apply(application) => is_map(&application.lambda),
_ => false,
}
}

View File

@@ -1,24 +0,0 @@
use crate::{ast, rules::NodeRule};
pub struct ManualInherit;
impl NodeRule for ManualInherit {
type Node = ast::Binding;
const ID: &'static str = "manual_inherit";
fn select(data: &ast::NodeData) -> Option<&Self::Node> {
let ast::NodeData::Binding(binding) = data;
Some(binding)
}
fn check(node: &Self::Node) -> Option<&'static str> {
match &node.value {
ast::Expr::Ident(value) if node.key == *value => {
Some("Use `inherit` for this assignment")
}
_ => None,
}
}
}

View File

@@ -1,24 +0,0 @@
use crate::{ast, rules::NodeRule};
pub struct ManualInheritFrom;
impl NodeRule for ManualInheritFrom {
type Node = ast::Binding;
const ID: &'static str = "manual_inherit_from";
fn select(data: &ast::NodeData) -> Option<&Self::Node> {
let ast::NodeData::Binding(binding) = data;
Some(binding)
}
fn check(node: &Self::Node) -> Option<&'static str> {
match &node.value {
ast::Expr::Select(select) if node.key == select.attr => {
Some("Use `inherit` for this selected attribute")
}
_ => None,
}
}
}

Some files were not shown because too many files have changed in this diff Show More