Compare commits

..

192 Commits

Author SHA1 Message Date
c053846adb fix(waybar): restore workspace clicks 2026-10-11 21:43:20 -04:00
2edad6c5df refactor(clipboard): simplify picker wiring 2026-10-11 21:36:54 -04:00
1d5acc09c6 refactor(audio): simplify media controls 2026-10-11 21:30:21 -04:00
ee29e53f7c refactor(waybar): register owned modules 2026-10-11 21:07:35 -04:00
3c9d0ec886 refactor(nix): share glyph decoding helper 2026-10-11 21:07:35 -04:00
f0c5544b10 refactor(waybar): register workspace window icons 2026-10-11 18:23:41 -04:00
4dd0a5a4f5 feat(rofi): increase launcher contrast 2026-10-11 17:49:05 -04:00
a4a795f7ee refactor(mako): use executable helper for history 2026-10-11 17:40:56 -04:00
ecdf877308 refactor(login): use configured uwsm executable 2026-10-11 17:39:37 -04:00
d1face10ec refactor(nix): apply executable helper rules 2026-10-11 17:38:15 -04:00
337d9a2014 docs(nix-style): add package command rules 2026-10-11 17:38:15 -04:00
f059068873 refactor(hyprlock): inline command paths 2026-10-11 17:25:18 -04:00
7662c04cda fix(hypridle): use Lua dpms dispatcher 2026-10-11 17:22:05 -04:00
33128fc3c7 feat(hyprland): migrate desktop utility bindings 2026-10-11 16:58:27 -04:00
68415ad47e fix(hyprland): place Zeus primary monitor at origin 2026-10-11 16:56:03 -04:00
6b7201e6fa feat(hyprland): migrate session feature bindings 2026-10-11 16:52:29 -04:00
4360f4863b feat(hyprland): migrate hardware key bindings 2026-10-11 16:40:07 -04:00
370cab8f83 feat(hyprland): render session env registry 2026-10-11 16:34:52 -04:00
e74a08b853 feat(dotfiles): add session env registry 2026-10-11 16:30:00 -04:00
250b0b71f9 feat(hyprland): migrate static Lua compositor settings 2026-10-11 16:24:48 -04:00
10299cec19 refactor(hyprland): place keybind definitions after config 2026-10-11 16:00:38 -04:00
91654b3068 feat(hyprland): add remaining window actions 2026-10-11 15:58:50 -04:00
be9c3c07a3 feat(hyprland): add window resize binds 2026-10-11 15:00:03 -04:00
a8bd4bf4cf feat(hyprland): add window movement binds 2026-10-11 14:52:39 -04:00
a2fc568704 feat(hyprland): add directional focus binds 2026-10-11 12:58:56 -04:00
0bf5ebae87 feat(hyprland): add workspace focus keybinds 2026-10-10 17:02:20 -04:00
fddec14128 refactor(hyprland): move terminal keybind into registry 2026-10-10 15:41:32 -04:00
85fadc5bfc feat(hyprland): start lua config migration 2026-10-10 13:57:33 -04:00
3992030949 docs: update notes 2026-10-10 11:06:05 -04:00
c8a596aa39 fix(hyprland): stop idle dpms poweroff 2026-10-10 08:29:01 -04:00
4908c4751f refactor(nixos): simplify Firefox configuration 2026-10-07 22:57:25 -04:00
79c56dfb1c refactor(nixos): remove Alacritty module 2026-10-07 22:53:09 -04:00
7140b6d258 refactor(nixos): simplify VPN client configuration 2026-10-07 22:44:45 -04:00
f173887228 refactor(nixos): consolidate VLAN networking 2026-10-07 21:29:52 -04:00
3a487050fd refactor(nixos): simplify SSH configuration 2026-10-07 20:27:48 -04:00
65c84f5b39 docs(nixos): clarify host module options 2026-10-07 18:20:53 -04:00
b3c4903c82 fix(fish): persist remote system deployments 2026-10-07 17:43:20 -04:00
ec1c0fba56 refactor(nixos): organize host modules 2026-10-07 16:59:03 -04:00
1f2f1a79ad refactor(nixos): derive ZFS host IDs 2026-10-07 16:51:56 -04:00
46a47fa061 feat(nixos): configure route body limits 2026-10-06 22:54:58 -04:00
8d357e960b feat(nixos): add unzip to host profiles 2026-10-06 22:45:49 -04:00
f48bac5ec1 refactor(nixos): inline module enable conditions 2026-10-06 22:26:47 -04:00
d53246caf0 docs(nixos): clarify guest option 2026-10-06 22:24:00 -04:00
92f7f0542d docs(nixos): clarify host option 2026-10-06 22:22:32 -04:00
4f7fbef35b feat(nixos): encapsulate CachyOS kernel configuration 2026-10-06 19:42:52 -04:00
c7c3257b9d docs(nixos): clarify primary user option 2026-10-06 19:12:58 -04:00
7d37b92e17 refactor(nixos): scope user profile to hosts 2026-10-05 23:15:01 -04:00
9ece182099 refactor(nixos): disable guest SSH by default 2026-10-05 23:05:55 -04:00
9aea13b8c2 refactor(dotfiles): inline module imports 2026-10-05 00:28:34 -04:00
369c7512c6 refactor(nixos): clarify stable UID validation 2026-10-05 00:26:28 -04:00
8c91a210b2 docs(nixos): clarify registry option descriptions 2026-10-05 00:13:31 -04:00
d602581a99 refactor(nixos): inline shell shortcut type 2026-10-05 00:11:51 -04:00
c3a6271582 refactor(nixos): model route protocols explicitly 2026-10-05 00:09:09 -04:00
a7db0b745c refactor(nixos): use Home Manager session variables 2026-10-04 22:22:55 -04:00
4a8e8920ea refactor(dotfiles): clarify registry validation 2026-10-04 22:01:03 -04:00
3e9295134e feat(natu): detect redundant let expressions 2026-10-04 21:23:35 -04:00
c91094234f refactor(natu): remove single-use binding rule 2026-10-04 14:08:13 -04:00
11b490d1e8 chore: apply generic updates 2026-10-04 13:56:23 -04:00
43b5d07a27 revert(nixos): undo bad system update
Restore the pre-update Pi packaging and Rofi option from PR 124 after the associated deployment exposed a performance regression.

Refs: 1b4dc7762
2026-10-04 13:50:10 -04:00
1b4dc77627 fix(nixos): build vendored pi independently 2026-10-03 07:57:41 -04:00
1cbdcee8ac feat(natu): ignore imported let bindings 2026-10-01 21:54:09 -04:00
3fbb0aca32 refactor(dotfiles): remove registry replication 2026-10-01 21:50:58 -04:00
87776c8994 chore(dotfiles): defer registry binding findings 2026-09-30 11:28:29 -04:00
17597526c5 refactor(dotfiles): inline MAC octet helper 2026-09-30 11:22:38 -04:00
14c9a418b6 feat(natu): support preceding suppressions 2026-09-30 11:03:32 -04:00
589e37f52e refactor(dotfiles): inline host configurations 2026-09-30 10:46:20 -04:00
78492739f3 feat(natu): detect single-use let bindings 2026-09-30 10:43:05 -04:00
2265dfc558 feat(natu): lint redundant parameter forwarding 2026-09-30 00:19:27 -04:00
becbb50491 docs(nix): clarify completed Nix prose 2026-09-29 22:59:14 -04:00
54335ead82 refactor(dotfiles): inline registry definitions 2026-09-29 20:16:15 -04:00
16c69e01e1 refactor(dotfiles): inline registry options 2026-09-29 00:27:15 -04:00
5af563d70f refactor(dotfiles): split registry responsibilities 2026-09-27 23:31:44 -04:00
0e5731145e feat(natu): lint direct listToAttrs maps 2026-09-27 18:05:16 -04:00
b6469567e1 fix(zeus): ignore power button presses 2026-09-27 14:47:00 -04:00
8f0837a4b5 feat(desktop): copy screenshots before annotation 2026-09-27 10:09:17 -04:00
0937f15260 fix(dotfiles): qualify stable UID helpers 2026-09-27 10:09:17 -04:00
61efef1a7d fix(dotfiles): stop passing unused guest libraries 2026-09-27 10:07:57 -04:00
8eabdee2a0 docs(nix-style): allow general basename use 2026-09-27 09:42:50 -04:00
43b3ff4b09 refactor(dotfiles): centralize Nix file names 2026-09-27 09:39:10 -04:00
3b2d16743e refactor(dotfiles): share Nix file discovery 2026-09-26 23:49:03 -04:00
11d11c7c02 feat(style-nix): require shared file discovery 2026-09-26 23:49:03 -04:00
869d6a2cbd feat(review): run natu for Nix targets 2026-09-26 23:49:03 -04:00
e5b76515f1 refactor(nix): order let bindings 2026-09-26 23:43:45 -04:00
324d40c85d style(nixos): suppress cfg order findings 2026-09-26 23:32:34 -04:00
dd82367dcb feat(natu): support inline suppressions 2026-09-26 23:30:01 -04:00
ac5cc8d659 fix(natu): preserve NixOS cfg bindings 2026-09-26 23:24:54 -04:00
5e5c3e2534 feat(natu): report expected let order 2026-09-26 23:20:00 -04:00
09a2093f90 feat(nvim): enable nixd 2026-09-26 22:36:03 -04:00
8000367613 refactor(dotfiles): order let bindings 2026-09-26 21:04:57 -04:00
38dff04ece fix(natu): resolve inherited attributes 2026-09-26 20:27:48 -04:00
0c1a1c1ffa refactor(natu): rename xatu 2026-09-26 20:12:57 -04:00
27d9194ac3 feat(xatu): check let binding order 2026-09-26 20:06:14 -04:00
c4ea12b3ce feat(xatu): add scope graph 2026-09-26 20:05:59 -04:00
16ce11fc52 chore(valheim): select butter-chicken world 2026-09-26 13:40:57 -04:00
a73636a84f feat(zeus): install r2modman 2026-09-25 13:21:40 -04:00
80db2b61e6 chore: update 2026-09-24 23:11:47 -04:00
7cc066b55d chore(nixos): update Pi default model 2026-09-24 23:10:12 -04:00
74e7d2d211 feat(skills): enable craft skill repo-wide 2026-09-23 23:38:30 -04:00
98d734e977 docs: add valheim progression note 2026-09-23 09:51:55 -04:00
dda05559a5 feat(nixos): add copyparty opds volume 2026-09-20 20:38:15 -04:00
594877c4ba fix(nixos): mirror copyparty volume paths 2026-09-20 12:14:45 -04:00
f9aff6839e feat(xatu): add manual inherit from lint 2026-09-19 23:30:56 -04:00
4b7d3834ab feat(xatu): add manual inherit lint 2026-09-19 21:27:23 -04:00
213cd84642 refactor(xatu): narrow lint rule set 2026-09-19 20:17:24 -04:00
6e19169e11 feat(xatu): report empty let expressions 2026-09-19 18:25:25 -04:00
be8744795c feat(xatu): report unquoted URI literals 2026-09-19 17:51:46 -04:00
ad00301ac9 docs(rust-style): order declarations by dependency 2026-09-19 17:38:09 -04:00
87dc93513b feat(xatu): set CLI exit statuses 2026-09-19 17:06:11 -04:00
58f2350d5f fix(xatu): ignore testdata directories 2026-09-19 16:49:50 -04:00
4b9beae501 refactor(flake): split root output wiring 2026-09-19 16:19:55 -04:00
addcf7ca8e feat(xatu): report deprecated toPath calls 2026-09-19 15:17:38 -04:00
6887b104d6 build(rust): vendor googletest crate 2026-09-19 14:53:22 -04:00
a6fe84190e test(xatu): cover future sight findings 2026-09-18 23:25:00 -04:00
1fa3962fed refactor(xatu): return structured reports 2026-09-18 22:43:17 -04:00
113018a62c feat(xatu): reject files with parse errors 2026-09-18 21:14:00 -04:00
11f1eba26f feat(xatu): add lint architecture 2026-09-18 20:57:39 -04:00
d974a46211 chore(vendor): remove unused anyhow crate 2026-09-17 15:14:18 -04:00
6b29f2c2b9 feat(status): add error context 2026-09-17 15:07:14 -04:00
49cf043035 feat(status): add status type 2026-09-17 14:57:50 -04:00
37539682d4 chore(vendor): add anyhow crate 2026-09-17 14:13:23 -04:00
7a3a3d6b90 feat(xatu): scaffold source collection 2026-09-17 14:09:47 -04:00
87916a8e17 docs(skills): add build style skill 2026-09-17 12:45:29 -04:00
498d0dd467 docs: update project todo list 2026-09-17 09:57:17 -04:00
b4b77faaa9 refactor(rust): align workspace with style rules 2026-09-17 09:57:11 -04:00
cf9a671fb8 feat(skills): add style review workflow 2026-09-17 09:56:58 -04:00
247712582a chore: remove unused Rust projects 2026-09-17 01:14:58 -04:00
ddc7952399 feat(xatu): scaffold project
Add the Xatu Rust package as a minimal hello-world skeleton with Cargo workspace metadata and Bazel library, binary, and test targets.

Include the Statix and Deadnix evaluation notes so the new linter work starts with the prior rule and architecture research.
2026-09-17 00:26:12 -04:00
885124c240 feat(nix): lint let binding order 2026-09-16 18:13:35 -04:00
7b09cea3af feat(nix): add style linter scaffold 2026-09-16 16:03:59 -04:00
82ad65a3a4 build(rust): vendor rnix parser crate 2026-09-16 15:51:43 -04:00
cbdf482422 refactor(dotfiles): order default let bindings 2026-09-16 14:50:19 -04:00
82b727e437 refactor(dotfiles): use formatter output in fix app 2026-09-16 14:19:28 -04:00
652b92d4f4 docs(skills): add nix let review rule 2026-09-16 13:44:35 -04:00
8154feba5a feat(skills): add nix style guidance 2026-09-16 11:27:24 -04:00
e15ce7f16b style(nix): apply style fixes 2026-09-16 00:16:08 -04:00
d24f58a4c9 feat(nix): add style tooling 2026-09-16 00:13:28 -04:00
5d40b43dd9 fix(nixos): avoid redundant deploy closure transfers 2026-09-15 21:16:30 -04:00
9a038d9821 feat(nixos): add pikachu homepage guest 2026-09-15 20:44:56 -04:00
fca37666d3 fix(nixos): keep palworld server stopped by default 2026-09-15 19:03:00 -04:00
aa1a330ef3 fix(nixos): decouple game startup from guest readiness 2026-09-15 18:04:51 -04:00
7be45ac44c fix(nixos): deploy from source checkout 2026-09-15 17:08:49 -04:00
4b3ca27637 docs(obsidian): process inbox notes 2026-09-15 09:44:36 -04:00
a73b59aa0a feat(copyparty): add read-only root volume 2026-09-14 22:50:45 -04:00
6159d87d10 fix(valheim): disable raids 2026-09-14 21:21:00 -04:00
e65eb8389f chore(pi): drop obsolete tool validation patch 2026-09-14 20:29:37 -04:00
212ed506a2 fix(pi): reject broken managed tool binaries 2026-09-14 10:57:23 -04:00
a8a18d27e9 fix(pi): discover ancestor project resources 2026-09-14 10:57:18 -04:00
aac222bee1 revert(pi): reset carried vendored source deltas 2026-09-14 10:57:14 -04:00
b6613b3a04 docs(pi): document linear vendor workflow 2026-09-14 10:49:19 -04:00
9e7759c9f5 feat(nixos): build vendored pi 2026-09-14 10:48:47 -04:00
4def357d09 chore(vendor): import pi 0.85.1
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: d981de1229ef899957bbe968bc8dcda02a21f477
2026-09-14 10:48:47 -04:00
5ebc7018c3 fix(nixos): add Steam game performance helpers 2026-09-13 21:45:00 -04:00
25ecd986b5 fix(nixos): skip herdr onboarding 2026-09-13 06:55:31 -04:00
b3bda4a739 feat(shell): manage herdr agent integrations 2026-09-13 06:47:25 -04:00
8fe2e6f844 feat(nixos): add OMP module 2026-09-13 00:16:30 -04:00
7625b031ec build: update flake inputs 2026-09-12 23:12:14 -04:00
6a364f5ce9 feat(nixos): migrate navidrome to pikachu 2026-09-12 21:17:36 -04:00
1a5edc1a5f fix(nixos): avoid blocking guest startup on SteamCMD 2026-09-12 20:00:20 -04:00
cd220b8030 feat(ariadne): deserialize Clew AST JSON 2026-09-12 11:55:06 -04:00
ec96a948b6 test(ariadne): cover pipeline prerequisites 2026-09-11 20:58:00 -04:00
a3ed3b0b9d feat(skills): add document drafting skill 2026-09-10 18:28:37 -04:00
9e7469796d feat(nixos): add Palworld Tracion guest 2026-09-10 14:37:50 -04:00
8bd9ff166e feat(nixos): run Valheim in a guest 2026-09-10 10:16:27 -04:00
5758a781d8 feat(nixos): add UDP route ingress 2026-09-10 09:08:47 -04:00
88762118b9 feat(nixos): add Valheim service module 2026-09-10 08:23:42 -04:00
baca50ceeb feat(nixos): add SteamCMD server support 2026-09-10 07:59:57 -04:00
c159115847 fix(nixos): deploy local worktree changes 2026-09-09 18:46:21 -04:00
3538297991 feat(ariadne): add CLI skeleton 2026-09-09 18:23:29 -04:00
01f47b419d chore(ariadne): remove obsolete extraction script 2026-09-08 23:03:03 -04:00
4f0512832b feat(nvim): add Rust LSP support 2026-09-08 22:59:57 -04:00
8855b6d379 build(ariadne): vendor clap 2026-09-08 19:48:37 -04:00
e6af135702 docs: update task inventory 2026-09-08 18:50:01 -04:00
e40652b1a0 refactor(nixos): simplify shared defaults 2026-09-08 18:41:23 -04:00
3a3c612a83 refactor(nixos): centralize physical host defaults 2026-09-08 16:58:20 -04:00
a7bc7c7667 refactor(nixos): centralize universal defaults 2026-09-08 16:35:35 -04:00
9cac3efe21 chore(shell): remove devtree helper 2026-09-08 15:40:21 -04:00
af244d1064 docs: add ariadne planning notes 2026-09-08 15:38:36 -04:00
c03becfbb9 docs: license monorepo 2026-09-08 15:25:19 -04:00
13d07efa1f feat(skills): add grill skill
Add a monorepo-owned grill skill and enable it at the repository root.

Remove stale craft-skill commit guidance.
2026-09-08 14:57:13 -04:00
db7da0e9c6 fix(pi): discover ancestor project resources 2026-09-07 22:46:14 -04:00
f940fc3fd2 docs: require pull request branches 2026-09-07 22:13:50 -04:00
115fdc570f fix(nixos): drop stale Pi layout patch 2026-09-07 22:11:29 -04:00
557be040f8 chore: update flake inputs 2026-09-07 21:35:17 -04:00
3e36db4d89 feat(clew): extract Ren'Py ASTs 2026-09-07 20:02:06 -04:00
df03bfbcbb feat(clew): extract Ren'Py game files 2026-09-07 15:39:34 -04:00
2463b7e787 feat(nixos): add neogaia WireGuard client 2026-09-07 12:18:52 -04:00
aab32198d1 build(python): simplify rpycdec vendor layout 2026-09-06 17:22:16 -04:00
884789d676 build(python): align toolchain with monorepo pattern 2026-09-06 17:18:14 -04:00
c85993c196 build(python): vendor rpycdec 2026-09-06 15:06:03 -04:00
64773e0948 build(ariadne): vendor serde crates 2026-09-05 20:21:54 -04:00
90f46026f0 feat(ariadne): add Rust binary skeleton 2026-09-05 20:02:28 -04:00
8d91550937 fix(ariadne): preserve season paths during extraction 2026-09-05 19:36:38 -04:00
4147 changed files with 1342636 additions and 3281 deletions

1
.agents Symbolic link
View File

@@ -0,0 +1 @@
.pi

3
.gitignore vendored
View File

@@ -20,6 +20,9 @@ Thumbs.db
# Bazel convenience symlinks
/bazel-*
# local generated output
/output/
# language package-manager caches and outputs
node_modules/
dist/

52
.pi/prompts/review.md Normal file
View File

@@ -0,0 +1,52 @@
---
description: Review changes against repository style rules
argument-hint: "[target]"
---
Review target: `${ARGUMENTS:-the staged diff}`.
Audit the target for violations of the repository style rules.
Choose skills from the reviewed files:
- Use the `nix-style` skill for `*.nix` files.
- Use the `rust-style` skill for `*.rs` files.
- Use the `build-style` skill for `Cargo.toml` files.
- Use the `build-style` skill for Bazel `BUILD` files.
If the target includes multiple matching file types, use all matching skills.
If the target is a diff, choose skills from the changed files.
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.
Order findings by severity.
Do not report preferences unless a loaded rule states them.
Use this format for each finding:
```text
`code-path:line`
> `smallest reviewed-code excerpt that shows the violation`
`skill::rule-file:line`: Finding title.
Summary: why the code breaks the rule
Fix: suggested fix
```
Keep the code excerpt to one to three lines when possible.
Use a longer excerpt only when the violation needs surrounding context.
Use `...` to omit unrelated lines from a multi-line excerpt.
Use a style-skill rule citation in the form `skill::rule-file:line`.
Cite Natu diagnostics as `natu::<rule-id>`.
Make the fix direct.
Do not include alternatives when one fix best follows the cited rule.
It is acceptable to report no findings when the target follows the loaded rules.

1
.pi/skills/build-style Symbolic link
View File

@@ -0,0 +1 @@
../../src/skills/style/build

1
.pi/skills/craft-skill Symbolic link
View File

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

1
.pi/skills/grill Symbolic link
View File

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

1
.pi/skills/nix-style Symbolic link
View File

@@ -0,0 +1 @@
../../src/skills/style/nix

1
.pi/skills/rust-style Symbolic link
View File

@@ -0,0 +1 @@
../../src/skills/style/rust

1
.pi/skills/to-document Symbolic link
View File

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

View File

@@ -19,6 +19,8 @@
## Pull requests
- Always create a new branch before opening a pull request.
Never open a pull request directly from a shared or existing work branch.
- During pull-request development, add follow-up commits instead of amending existing commits.
Preserve review history until the pull request is ready to merge.
- When the user says a pull request is merged, clean up the local branch without waiting for an explicit request.
@@ -93,8 +95,34 @@
committing a top-down rewrite.
- `rules_rust` `rust_test` targets cannot set both `crate` and `srcs`.
Use one input style per test target.
- Rust tests in this monorepo live in separate `*_test.rs` files, not inline `#[cfg(test)]` modules.
- `rnix::ast::HasEntry::entries()` includes assignments and `inherit` statements.
Use `attrpath_values()` when code needs only assignment entries.
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
`src/ariadne/Cargo.toml` unless explicitly requested.
- 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.
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.
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.
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.
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.
- Bazel Python targets require `PYTHON_TOOLCHAIN_ROOT` to point at a Python installation root.
- When checking whether a package exists in nixpkgs, check both this repository's pinned nixpkgs inputs
and the relevant current upstream channel. A package can exist on current `nixos-unstable` while being
absent from the pinned revision or from a different branch such as `nixpkgs-unstable`.
- 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`.
- Pi is vendored under `third_party/pi` as a linear vendor import.
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.
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.

405
Cargo.lock generated
View File

@@ -2,6 +2,411 @@
# It is not intended for manual editing.
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]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "ariadne"
version = "0.1.0"
dependencies = [
"clap",
"monorepo",
"serde",
"serde_json",
]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "clap"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "clap_lex"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "machoke"
version = "0.1.0"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "monorepo"
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]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"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]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "status"
version = "0.1.0"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "text-size"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

View File

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

251
LICENSE Normal file
View File

@@ -0,0 +1,251 @@
Repository license policy
=========================
Except as noted below, repository-owned content in this monorepo is
Copyright (C) 2026 Alexion and licensed under the GNU Affero General Public
License, version 3 or any later version.
Exceptions and exclusions:
- src/skills/ is licensed separately under the MIT license. See
src/skills/LICENSE.
- third_party/ is not covered by this license. Third-party content keeps its
upstream license.
The canonical AGPL-3.0 license text follows.
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based on the Program.
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see <http://www.gnu.org/licenses/>.

View File

@@ -4,9 +4,15 @@ module(
)
bazel_dep(name = "bazel_skylib", version = "1.8.2")
bazel_dep(name = "rules_python", version = "1.6.3")
bazel_dep(name = "rules_rust", version = "0.74.0")
python = use_extension("//bazel:python.bzl", "python")
use_repo(python, "python")
rust = use_extension("//bazel:rust.bzl", "rust")
use_repo(rust, "rust")
register_toolchains("@python//:python")
register_toolchains("@rust//:rust")

638
MODULE.bazel.lock generated
View File

@@ -6,35 +6,53 @@
"https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/source.json": "14892cc698e02ffedf4967546e6bedb7245015906888d3465fcf27c90a26da10",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
"https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8",
"https://bcr.bazel.build/modules/apple_support/1.24.1/source.json": "cf725267cbacc5f028ef13bb77e7f2c2e0066923a4dab1025e4a0511b1ed258a",
"https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef",
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
"https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_features/1.50.0/MODULE.bazel": "2083ef9c7a469f520890483ccf8e0189d6e71e2117e7752e15e6554433d5ae3e",
"https://bcr.bazel.build/modules/bazel_features/1.50.0/source.json": "e0ee3debde2789ff56e4452e612d126925ba9ab64d4bde79c67f099d2902df9b",
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
"https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
"https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
"https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
"https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
"https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
"https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4",
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
"https://bcr.bazel.build/modules/googletest/1.14.0/source.json": "2478949479000fdd7de9a3d0107ba2c85bb5f961c3ecb1aa448f52549ce310b5",
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
"https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
"https://bcr.bazel.build/modules/package_metadata/0.0.3/source.json": "742075a428ad12a3fa18a69014c2f57f01af910c6d9d18646c990200853e641a",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
"https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
"https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
@@ -45,10 +63,22 @@
"https://bcr.bazel.build/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344",
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
"https://bcr.bazel.build/modules/protobuf/27.0/source.json": "1acf3d080c728d42f423fde5422fd0a1a24f44c15908124ce12363a253384193",
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/source.json": "c16a6488fb279ef578da7098e605082d72ed85fc8d843eaae81e7d27d0f4625d",
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
"https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022",
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
"https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4",
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
"https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
"https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
"https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
@@ -56,41 +86,83 @@
"https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc",
"https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
"https://bcr.bazel.build/modules/rules_cc/0.2.8/source.json": "85087982aca15f31307bd52698316b28faa31bd2c3095a41f456afec0131344c",
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
"https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
"https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
"https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
"https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1",
"https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d",
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
"https://bcr.bazel.build/modules/rules_java/8.5.1/source.json": "db1a77d81b059e0f84985db67a22f3f579a529a86b7997605be3d214a0abe38e",
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/source.json": "5abb45cc9beb27b77aec6a65a11855ef2b55d95dfdc358e9f312b78ae0ba32d5",
"https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9",
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
"https://bcr.bazel.build/modules/rules_proto/6.0.2/source.json": "17a2e195f56cb28d6bbf763e49973d13890487c6945311ed141e196fb660426d",
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
"https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7",
"https://bcr.bazel.build/modules/rules_python/0.22.1/source.json": "57226905e783bae7c37c2dd662be078728e48fa28ee4324a7eabcafb5a43d014",
"https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f",
"https://bcr.bazel.build/modules/rules_python/1.6.3/source.json": "f0be74977e5604a6526c8a416cda22985093ff7d5d380d41722d7e44015cc419",
"https://bcr.bazel.build/modules/rules_rust/0.74.0/MODULE.bazel": "6e6fa04d7c8202f9cd63205ec9f96da39382a4ac971ee1d7a900fe8e1f68be17",
"https://bcr.bazel.build/modules/rules_rust/0.74.0/source.json": "04eac31b4536eb23dd8e8e8ca7e079d3c1c4ac99934d38aa54bbae36917c7507",
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
"https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b",
"https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c",
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
"https://bcr.bazel.build/modules/stardoc/0.5.3/source.json": "cd53fe968dc8cd98197c052db3db6d82562960c87b61e7a90ee96f8e4e0dda97",
"https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d"
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d",
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
},
"selectedYankedVersions": {},
"moduleExtensions": {
"//bazel:python.bzl%python": {
"general": {
"bzlTransitiveDigest": "CbGiMYJXjcea8dmoNxUWe/lWOjqRWfQPTXFHUUMheIY=",
"usagesDigest": "bcY43VsTrhTJ8N4Dj5FtsgMhZxLyKKjHP+wWK4ntJQ4=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"python": {
"bzlFile": "@@//bazel:python.bzl",
"ruleClassName": "python_repository",
"attributes": {}
}
},
"recordedRepoMappingEntries": []
}
},
"//bazel:rust.bzl%rust": {
"general": {
"bzlTransitiveDigest": "4kUzQyjRjzchWIBZJKkNs8LuO4F+g/DwuwtTa0RTPcU=",
@@ -108,6 +180,29 @@
"recordedRepoMappingEntries": []
}
},
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
"general": {
"bzlTransitiveDigest": "KIX40nDfygEWbU+rq3nYpt3tVgTK/iO8PKh5VMBlN7M=",
"usagesDigest": "pwHZ+26iLgQdwvdZeA5wnAjKnNI3y6XO2VbhOTeo5h8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"compatibility_proxy": {
"bzlFile": "@@rules_java~//java:rules_java_deps.bzl",
"ruleClassName": "_compatibility_proxy_repo_rule",
"attributes": {}
}
},
"recordedRepoMappingEntries": [
[
"rules_java~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_jvm_external~//:extensions.bzl%maven": {
"general": {
"bzlTransitiveDigest": "twF5CTH+cMNcSDOS8HgBaxaIQ+FsdzZBKM0EbgJoYGU=",
@@ -1123,47 +1218,100 @@
]
}
},
"@@rules_jvm_external~//:non-module-deps.bzl%non_module_deps": {
"@@rules_kotlin~//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
"general": {
"bzlTransitiveDigest": "ZOivBbbZUakRexeLO/N26oX4Bcph6HHnqNmfxt7yoCc=",
"usagesDigest": "SgmaWbjiY+8dgpUSia89kYJqNm3A67fr0hnvRaklrV4=",
"bzlTransitiveDigest": "fus14IFJ/1LGWWGKPH/U18VnJCoMjfDt1ckahqCnM0A=",
"usagesDigest": "aJF6fLy82rR95Ff5CZPAqxNoFgOMLMN5ImfBS0nhnkg=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"io_bazel_rules_kotlin": {
"com_github_jetbrains_kotlin_git": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
"ruleClassName": "kotlin_compiler_git_repository",
"attributes": {
"urls": [
"https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
],
"sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
}
},
"com_github_jetbrains_kotlin": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
"ruleClassName": "kotlin_capabilities_repository",
"attributes": {
"git_repository_name": "com_github_jetbrains_kotlin_git",
"compiler_version": "1.9.23"
}
},
"com_github_google_ksp": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:ksp.bzl",
"ruleClassName": "ksp_compiler_plugin_repository",
"attributes": {
"urls": [
"https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
],
"sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
"strip_version": "1.9.23-1.0.20"
}
},
"com_github_pinterest_ktlint": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_file",
"attributes": {
"sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
"urls": [
"https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
],
"executable": true
}
},
"rules_android": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "946747acdbeae799b085d12b240ec346f775ac65236dfcf18aa0cd7300f6de78",
"sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
"strip_prefix": "rules_android-0.1.1",
"urls": [
"https://github.com/bazelbuild/rules_kotlin/releases/download/v1.7.0-RC-2/rules_kotlin_release.tgz"
"https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
]
}
}
},
"recordedRepoMappingEntries": [
[
"rules_jvm_external~",
"rules_kotlin~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_python~//python/extensions:python.bzl%python": {
"@@rules_python~//python/uv:uv.bzl%uv": {
"general": {
"bzlTransitiveDigest": "lbXqTyC4ahBb81TIrIp+2d3sWnlurVNqSeAaLJknLUs=",
"usagesDigest": "1Y6kbygksx7wAtDStFoHnR90xr8Yeq00I91YcLMbxMI=",
"bzlTransitiveDigest": "mxPY/VBQrSC9LvYeRrlxD+0IkDTQ4+36NGMnGWlN/Vw=",
"usagesDigest": "qIeg3YEigU6oZD/nE0vQzliorEcL5dtbidAIP+ZhylM=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pythons_hub": {
"bzlFile": "@@rules_python~//python/extensions/private:interpreter_hub.bzl",
"ruleClassName": "hub_repo",
"uv": {
"bzlFile": "@@rules_python~//python/uv/private:uv_toolchains_repo.bzl",
"ruleClassName": "uv_toolchains_repo",
"attributes": {
"toolchains": []
"toolchain_type": "'@@rules_python~//python/uv:uv_toolchain_type'",
"toolchain_names": [
"none"
],
"toolchain_implementations": {
"none": "'@@rules_python~//python:none'"
},
"toolchain_compatible_with": {
"none": [
"@platforms//:incompatible"
]
},
"toolchain_target_settings": {}
}
}
},
@@ -1175,446 +1323,8 @@
],
[
"rules_python~",
"rules_python",
"rules_python~"
]
]
}
},
"@@rules_python~//python/extensions/private:internal_deps.bzl%internal_deps": {
"general": {
"bzlTransitiveDigest": "b6FMQSdoZ1QOssw14AW8bWDn2BvywI4FVkLbO2nTMsE=",
"usagesDigest": "KPNj8wxzOk7dXY9StqZ91MCKEIJSEnAyV0Q/dGFP5sw=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pypi__build": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/03/97/f58c723ff036a8d8b4d3115377c0a37ed05c1f68dd9a0d66dab5e82c5c1c/build-0.9.0-py3-none-any.whl",
"sha256": "38a7a2b7a0bdc61a42a0a67509d88c71ecfc37b393baba770fae34e20929ff69",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__click": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/76/0a/b6c5f311e32aeb3b406e03c079ade51e905ea630fc19d1262a46249c1c86/click-8.0.1-py3-none-any.whl",
"sha256": "fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__colorama": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",
"sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__installer": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl",
"sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__packaging": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/8f/7b/42582927d281d7cb035609cd3a543ffac89b74f3f4ee8e1c50914bcb57eb/packaging-22.0-py3-none-any.whl",
"sha256": "957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__pep517": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/ee/2f/ef63e64e9429111e73d3d6cbee80591672d16f2725e648ebc52096f3d323/pep517-0.13.0-py3-none-any.whl",
"sha256": "4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__pip": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/09/bd/2410905c76ee14c62baf69e3f4aa780226c1bbfc9485731ad018e35b0cb5/pip-22.3.1-py3-none-any.whl",
"sha256": "908c78e6bc29b676ede1c4d57981d490cb892eb45cd8c214ab6298125119e077",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__pip_tools": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/5e/e8/f6d7d1847c7351048da870417724ace5c4506e816b38db02f4d7c675c189/pip_tools-6.12.1-py3-none-any.whl",
"sha256": "f0c0c0ec57b58250afce458e2e6058b1f30a4263db895b7d72fd6311bf1dc6f7",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__setuptools": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/7c/5b/3d92b9f0f7ca1645cba48c080b54fe7d8b1033a4e5720091d1631c4266db/setuptools-60.10.0-py3-none-any.whl",
"sha256": "782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__tomli": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl",
"sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__wheel": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/bd/7c/d38a0b30ce22fc26ed7dbc087c6d00851fb3395e9d0dac40bec1f905030c/wheel-0.38.4-py3-none-any.whl",
"sha256": "b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__importlib_metadata": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/d7/31/74dcb59a601b95fce3b0334e8fc9db758f78e43075f22aeb3677dfb19f4c/importlib_metadata-1.4.0-py2.py3-none-any.whl",
"sha256": "bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__zipp": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/f4/50/cc72c5bcd48f6e98219fc4a88a5227e9e28b81637a99c49feba1d51f4d50/zipp-1.0.0-py2.py3-none-any.whl",
"sha256": "8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__more_itertools": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/bd/3f/c4b3dbd315e248f84c388bd4a72b131a29f123ecacc37ffb2b3834546e42/more_itertools-8.13.0-py3-none-any.whl",
"sha256": "c5122bffc5f104d37c1626b8615b511f3427aa5389b94d61e5ef8236bfbc3ddb",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__coverage_cp38_aarch64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/07/82/79fa21ceca9a9b091eb3c67e27eb648dade27b2c9e1eb23af47232a2a365/coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl"
]
}
},
"pypi__coverage_cp38_aarch64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/40/3b/cd68cb278c4966df00158811ec1e357b9a7d132790c240fc65da57e10013/coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
]
}
},
"pypi__coverage_cp38_x86_64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/05/63/a789b462075395d34f8152229dccf92b25ca73eac05b3f6cd75fa5017095/coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl"
]
}
},
"pypi__coverage_cp38_x86_64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/bd/a0/e263b115808226fdb2658f1887808c06ac3f1b579ef5dda02309e0d54459/coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
]
}
},
"pypi__coverage_cp39_aarch64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/63/e9/f23e8664ec4032d7802a1cf920853196bcbdce7b56408e3efe1b2da08f3c/coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl"
]
}
},
"pypi__coverage_cp39_aarch64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/18/95/27f80dcd8273171b781a19d109aeaed7f13d78ef6d1e2f7134a5826fd1b4/coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
]
}
},
"pypi__coverage_cp39_x86_64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/ea/52/c08080405329326a7ff16c0dfdb4feefaa8edd7446413df67386fe1bbfe0/coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl"
]
}
},
"pypi__coverage_cp39_x86_64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/6b/f2/919f0fdc93d3991ca074894402074d847be8ac1e1d78e7e9e1c371b69a6f/coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
]
}
},
"pypi__coverage_cp310_aarch64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/89/a2/cbf599e50bb4be416e0408c4cf523c354c51d7da39935461a9687e039481/coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl"
]
}
},
"pypi__coverage_cp310_aarch64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/15/b0/3639d84ee8a900da0cf6450ab46e22517e4688b6cec0ba8ab6f8166103a2/coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
]
}
},
"pypi__coverage_cp310_x86_64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/c4/8d/5ec7d08f4601d2d792563fe31db5e9322c306848fec1e65ec8885927f739/coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl"
]
}
},
"pypi__coverage_cp310_x86_64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/3c/7d/d5211ea782b193ab8064b06dc0cc042cf1a4ca9c93a530071459172c550f/coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
]
}
},
"pypi__coverage_cp311_aarch64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/36/f3/5cbd79cf4cd059c80b59104aca33b8d05af4ad5bf5b1547645ecee716378/coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
]
}
},
"pypi__coverage_cp311_x86_64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/50/cf/455930004231fa87efe8be06d13512f34e070ddfee8b8bf5a050cdc47ab3/coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl"
]
}
},
"pypi__coverage_cp311_x86_64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/6a/63/8e82513b7e4a1b8d887b4e85c1c2b6c9b754a581b187c0b084f3330ac479/coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
]
}
}
},
"recordedRepoMappingEntries": [
[
"rules_python~",
"bazel_skylib",
"bazel_skylib~"
],
[
"rules_python~",
"bazel_tools",
"bazel_tools"
],
[
"rules_python~",
"rules_python",
"rules_python~"
"platforms",
"platforms"
]
]
}

View File

@@ -13,7 +13,8 @@ dev/ Monorepo root
├── src/ Projects
│ └── dotfiles/ Home and system configuration
└── third_party/ External source and vendoring support
└── crates/ Generated Rust crate sources and Bazel targets
├── crates/ Generated Rust crate sources and Bazel targets
└── pi/ Vendored Pi source
```
## Rust dependencies
@@ -44,3 +45,45 @@ deps = ["//third_party/crates:crate_name"]
Commit the generated files under `third_party/crates/`.
Do not edit generated files manually.
## Vendored Pi
Pi source lives in `third_party/pi/`.
The directory is a linear vendor import from upstream Pi.
Local fork changes are carried directly in that vendored tree.
Use the upstream tag that matches the target CLI release.
The CLI version lives in `packages/coding-agent/package.json`.
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
Those commands create a synthetic upstream root commit.
Gitea rebase applies that root commit without the `third_party/pi` prefix.
That causes add/add conflicts on repository root files.
Update the vendor tree with a linear commit:
```sh
git fetch https://github.com/earendil-works/pi.git refs/tags/v<version>
split="$(git rev-parse FETCH_HEAD)"
rm -rf third_party/pi
mkdir -p third_party/pi
git archive --format=tar "$split" | tar -C third_party/pi -xf -
git add third_party/pi
git commit \
-m "chore(vendor): import pi <version>" \
-m "Imported as a linear vendor commit so forge rebases do not replay the upstream root tree." \
-m "git-subtree-dir: third_party/pi" \
-m "git-subtree-split: $split"
```
Keep the vendor import or update commit separate from local edits.
Do not remove the `git-subtree-dir` and `git-subtree-split` trailers.
They record the upstream source commit for future updates.
After the upstream update, resolve conflicts in `third_party/pi/`.
Keep local fork changes as ordinary commits after the vendor update.
Then verify the package:
```sh
nix build .#nixosConfigurations.zeus.config.home-manager.users.alexion.programs.pi-coding-agent.package --no-link --print-out-paths
```

View File

@@ -1,4 +1,6 @@
exports_files([
"python.BUILD.template",
"python.bzl",
"rust.BUILD.template",
"rust.bzl",
"utils.bzl",

View File

@@ -0,0 +1,25 @@
load("@rules_python//python:py_runtime.bzl", "py_runtime")
load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair")
py_runtime(
name = "runtime",
implementation_name = "cpython",
interpreter_path = "{interpreter_path}",
interpreter_version_info = {
"major": "{major}",
"minor": "{minor}",
"micro": "{micro}",
},
python_version = "PY3",
)
py_runtime_pair(
name = "runtime_pair",
py3_runtime = ":runtime",
)
toolchain(
name = "python",
toolchain = ":runtime_pair",
toolchain_type = "@rules_python//python:toolchain_type",
)

46
bazel/python.bzl Normal file
View File

@@ -0,0 +1,46 @@
load("//bazel:utils.bzl", "execute", "require_env")
def _python_repository_impl(repository_ctx):
root = repository_ctx.path(require_env(repository_ctx, "PYTHON_TOOLCHAIN_ROOT"))
python3 = root.get_child("bin/python3")
version = execute(
repository_ctx,
[
str(python3),
"-c",
"import sys; print('{}.{}.{}'.format(*sys.version_info[:3]))",
],
)
parts = version.split(".")
if len(parts) != 3:
fail("python3 did not report a major.minor.micro version")
repository_ctx.file("WORKSPACE.bazel", "")
repository_ctx.template(
"BUILD.bazel",
Label("//bazel:python.BUILD.template"),
{
"{interpreter_path}": str(python3),
"{major}": parts[0],
"{minor}": parts[1],
"{micro}": parts[2],
},
)
python_repository = repository_rule(
implementation = _python_repository_impl,
local = True,
environ = ["PYTHON_TOOLCHAIN_ROOT"],
)
def _python_impl(module_ctx):
python_repository(name = "python")
python = module_extension(
implementation = _python_impl,
)

View File

@@ -2,10 +2,10 @@
## Desktop
- TODO: Verify this path after the NixOS configuration is migrated.
When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/session/waybar.nix`.
- 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`.
@@ -32,21 +32,36 @@
- Use produced literals for Herdr key names that include shifted punctuation.
For example, use `prefix+<` instead of `prefix+shift+comma`.
- Put global Pi extension sources in `configs/nixos/modules/shell/pi/extensions/`.
Home Manager deploys them to `~/.pi/agent/extensions/`.
- Treat Home Manager-managed Pi extension, prompt, and skill directories as generated output.
Do not edit those directories except for throwaway development or local experiments.
Promote finished work into the NixOS configuration before treating it as deployed.
- 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.
- Pikachu deploys from the monorepo checkout copied to `~/dev`.
Do not use the old `~/wrk/dotfiles` path for remote rebuild commands.
- 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
cfg = config.guests.actualbudget;
cfg = config.guests.actualbudget; # NOLINT(let_order)
dataDir = "/data";
in
{
@@ -25,6 +25,14 @@ in
}
];
registry.homepage.${config.networking.hostName}.actualbudget = {
tab = "Apps";
section = "Other";
name = "Actual Budget";
description = "Finance";
icon = "actual-budget.png";
};
guests.actualbudget = {
interior = {
modules.services.actualbudget = {

View File

@@ -4,7 +4,7 @@
...
}:
let
cfg = config.guests.copyparty;
cfg = config.guests.copyparty; # NOLINT(let_order)
volumeType = lib.types.submodule {
options = {
@@ -24,6 +24,12 @@ let
description = "Copyparty access map from permission string to account names.";
};
readOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Serve this Copyparty volume read-only.";
};
flags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
@@ -84,6 +90,15 @@ in
}
];
registry.homepage.${config.networking.hostName}.copyparty = {
tab = "Apps";
section = "Files";
name = "Copyparty";
description = "Files on ${config.networking.hostName}";
icon = "copyparty.svg";
href = lib.mkIf cfg.reverseProxy.enable "https://${cfg.reverseProxy.route.domain}/?h";
};
guests.copyparty = {
interior.modules.services.copyparty = {
enable = true;
@@ -94,31 +109,36 @@ in
inherit (cfg.reverseProxy) trustedSources;
};
volumes = lib.mapAttrs (_urlPath: volume: {
inherit (volume) path access flags;
inherit (volume)
path
access
flags
readOnly
;
}) cfg.volumes;
};
mounts =
{
${cfg.stateDir} = {
directory = cfg.statePath;
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
group = "storage";
mode = "0770";
};
${"/run/secrets/${cfg.passwordSecret}"} = {
file = config.sops.secrets.${cfg.passwordSecret}.path;
readOnly = true;
};
mounts = {
${cfg.stateDir} = {
directory = cfg.statePath;
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
group = "storage";
mode = "0770";
};
${"/run/secrets/${cfg.passwordSecret}"} = {
file = config.sops.secrets.${cfg.passwordSecret}.path;
readOnly = true;
};
}
// lib.mapAttrs' (
_urlPath: volume:
lib.nameValuePair volume.path {
directory = volume.hostPath;
owner = "root";
group = "storage";
mode = "2770";
}
// lib.mapAttrs' (_urlPath: volume:
lib.nameValuePair volume.path {
directory = volume.hostPath;
owner = "root";
group = "storage";
mode = "2770";
}
) cfg.volumes;
) cfg.volumes;
limits = {
memory = lib.mkDefault "2G";
cpu = lib.mkDefault "200%";

View File

@@ -0,0 +1,46 @@
{
config,
lib,
...
}:
let
cfg = config.guests.homepage; # NOLINT(let_order)
environmentFile = "/run/secrets/homepage.env";
in
{
config = lib.mkIf cfg.enable {
guests.homepage = {
interior.modules.services.homepage = {
enable = true;
hostName = config.networking.hostName;
inherit environmentFile;
};
mounts.${environmentFile} = {
file = config.sops.templates."homepage.env".path;
readOnly = true;
};
limits = {
memory = lib.mkDefault "1G";
cpu = lib.mkDefault "100%";
tasksMax = lib.mkDefault 256;
};
};
sops.secrets = {
"services/metadata/latitude" = { };
"services/metadata/longitude" = { };
};
sops.templates."homepage.env" = {
owner = "root";
group = "storage";
mode = "0440";
content = ''
HOMEPAGE_VAR_LATITUDE=${config.sops.placeholder."services/metadata/latitude"}
HOMEPAGE_VAR_LONGITUDE=${config.sops.placeholder."services/metadata/longitude"}
'';
};
};
}

View File

@@ -0,0 +1,103 @@
{
config,
lib,
...
}:
let
guestCfg = config.guests.navidrome;
serviceCfg = config.containers.navidrome.config.modules.services.navidrome;
in
{
options.guests.navidrome = {
dataPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/navidrome";
description = "Host path mounted as Navidrome's data folder.";
};
libraryPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/music";
description = "Host path mounted as Navidrome's library folder.";
};
lastfmApiKeySecret = lib.mkOption {
type = lib.types.str;
default = "services/navidrome/lastfm/api-key";
description = "SOPS secret name containing the Navidrome Last.fm API key.";
};
lastfmSecretSecret = lib.mkOption {
type = lib.types.str;
default = "services/navidrome/lastfm/secret";
description = "SOPS secret name containing the Navidrome Last.fm secret.";
};
};
config = lib.mkIf guestCfg.enable {
assertions = [
{
assertion = guestCfg.dataPath != null;
message = "guests.navidrome.dataPath must point at the host directory that stores Navidrome data.";
}
{
assertion = guestCfg.libraryPath != null;
message = "guests.navidrome.libraryPath must point at the host directory that stores Navidrome's library folder.";
}
];
registry.homepage.${config.networking.hostName}.navidrome = {
tab = "Apps";
section = "Media";
name = "Navidrome";
description = "Music";
icon = "navidrome.png";
};
guests.navidrome = {
interior.modules.services.navidrome.enable = true;
mounts = {
${serviceCfg.dataFolder} = {
directory = guestCfg.dataPath;
owner = config.registry."stable-uid".${config.networking.hostName}.navidrome;
group = "storage";
mode = "2770";
};
${serviceCfg.musicFolder} = {
directory = guestCfg.libraryPath;
owner = "root";
group = "storage";
mode = "2770";
};
${serviceCfg.environmentFile} = {
file = config.sops.templates."navidrome.env".path;
readOnly = true;
};
};
limits = {
memory = lib.mkDefault "2G";
cpu = lib.mkDefault "200%";
tasksMax = lib.mkDefault 512;
};
};
sops.secrets = {
${guestCfg.lastfmApiKeySecret}.sopsFile = ../secrets + "/${config.networking.hostName}.yaml";
${guestCfg.lastfmSecretSecret}.sopsFile = ../secrets + "/${config.networking.hostName}.yaml";
};
sops.templates."navidrome.env" = {
owner = "root";
group = "storage";
mode = "0440";
content = ''
ND_LASTFM_APIKEY=${config.sops.placeholder.${guestCfg.lastfmApiKeySecret}}
ND_LASTFM_SECRET=${config.sops.placeholder.${guestCfg.lastfmSecretSecret}}
'';
};
};
}

View File

@@ -0,0 +1,155 @@
{
config,
lib,
...
}:
let
cfg = config.guests.palworld; # NOLINT(let_order)
steamDir = "/data/steamcmd";
installDir = "/data/server";
stateDir = "/data/state";
in
{
options.guests.palworld = {
steamPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/steamcmd";
description = "Host path bind-mounted as SteamCMD's metadata directory.";
};
installPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/palworld/server";
description = "Host path bind-mounted as Palworld's server install directory.";
};
statePath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/palworld/state";
description = "Host path bind-mounted as Palworld's durable state directory.";
};
serverName = lib.mkOption {
type = lib.types.str;
default = "palworld";
description = "Name shown for the Palworld server.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8211;
description = "UDP port for the Palworld server.";
};
publicIP = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Public IP address advertised by the Palworld server.";
};
autoStart = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Start the Palworld server after the guest reaches network readiness.";
};
passwordSecret = lib.mkOption {
type = lib.types.str;
default = "services/game-servers/password";
description = "SOPS secret name containing the Palworld server password.";
};
adminPasswordSecret = lib.mkOption {
type = lib.types.str;
default = "services/game-servers/password";
description = "SOPS secret name containing the Palworld admin password.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.steamPath != null;
message = "guests.palworld.steamPath must point at the host directory that stores SteamCMD metadata.";
}
{
assertion = cfg.installPath != null;
message = "guests.palworld.installPath must point at the host directory that stores Palworld server files.";
}
{
assertion = cfg.statePath != null;
message = "guests.palworld.statePath must point at the host directory that stores Palworld durable state.";
}
];
guests.palworld = {
interior.modules.services.games.palworld = {
enable = true;
inherit (cfg)
serverName
port
publicIP
autoStart
;
inherit steamDir installDir stateDir;
passwordFile = "/run/secrets/${cfg.passwordSecret}";
adminPasswordFile = "/run/secrets/${cfg.adminPasswordSecret}";
};
mounts = {
${steamDir} = {
directory = cfg.steamPath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${installDir} = {
directory = cfg.installPath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${stateDir} = {
directory = cfg.statePath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${"/run/secrets/${cfg.passwordSecret}"} = {
file = config.sops.secrets.${cfg.passwordSecret}.path;
readOnly = true;
};
}
// lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) {
${"/run/secrets/${cfg.adminPasswordSecret}"} = {
file = config.sops.secrets.${cfg.adminPasswordSecret}.path;
readOnly = true;
};
};
limits = {
memory = lib.mkDefault "8G";
cpu = lib.mkDefault "400%";
tasksMax = lib.mkDefault 2048;
};
};
sops.secrets = {
${cfg.passwordSecret} = {
owner = "root";
group = "storage";
mode = "0440";
};
}
// lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) {
${cfg.adminPasswordSecret} = {
owner = "root";
group = "storage";
mode = "0440";
};
};
};
}

View File

@@ -23,11 +23,6 @@ in
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"music.alexion.dev" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://10.23.20.32";
};
"photos.alexion.dev" = {
forceSSL = true;
enableACME = true;

View File

@@ -0,0 +1,116 @@
{
config,
lib,
...
}:
let
cfg = config.guests.valheim; # NOLINT(let_order)
steamDir = "/data/steamcmd";
installDir = "/data/server";
stateDir = "/data/state";
in
{
options.guests.valheim = {
steamPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/steamcmd";
description = "Host path bind-mounted as SteamCMD's metadata directory.";
};
installPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/valheim/server";
description = "Host path bind-mounted as Valheim's server install directory.";
};
statePath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/valheim/state";
description = "Host path bind-mounted as Valheim's world state directory.";
};
serverName = lib.mkOption {
type = lib.types.str;
default = "valheim";
description = "Name shown for the Valheim server.";
};
port = lib.mkOption {
type = lib.types.port;
default = 2456;
description = "Base UDP port for the Valheim server.";
};
passwordSecret = lib.mkOption {
type = lib.types.str;
default = "services/game-servers/password";
description = "SOPS secret name containing the Valheim server password.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.steamPath != null;
message = "guests.valheim.steamPath must point at the host directory that stores SteamCMD metadata.";
}
{
assertion = cfg.installPath != null;
message = "guests.valheim.installPath must point at the host directory that stores Valheim server files.";
}
{
assertion = cfg.statePath != null;
message = "guests.valheim.statePath must point at the host directory that stores Valheim world state.";
}
];
guests.valheim = {
interior.modules.services.games.valheim = {
enable = true;
inherit (cfg) serverName port;
inherit steamDir installDir stateDir;
passwordFile = "/run/secrets/${cfg.passwordSecret}";
};
mounts = {
${steamDir} = {
directory = cfg.steamPath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${installDir} = {
directory = cfg.installPath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${stateDir} = {
directory = cfg.statePath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${"/run/secrets/${cfg.passwordSecret}"} = {
file = config.sops.secrets.${cfg.passwordSecret}.path;
readOnly = true;
};
};
limits = {
memory = lib.mkDefault "4G";
cpu = lib.mkDefault "400%";
tasksMax = lib.mkDefault 1024;
};
};
sops.secrets.${cfg.passwordSecret} = {
owner = "root";
group = "storage";
mode = "0440";
};
};
}

View File

@@ -1,6 +1,6 @@
{
inputs,
pkgs,
util,
...
}:
# neogaia — Dell XPS 13 9380 laptop.
@@ -13,32 +13,22 @@
./disk.nix
];
system.stateVersion = "26.05";
modules.host.cachyos.enable = true;
# systemd-boot on the EFI system partition.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages_cachyos;
# Redistributable firmware for the QCA6174 wifi (ath10k blobs).
# Intel microcode updates follow from this, so none is declared here.
hardware.enableRedistributableFirmware = true;
# RAM-backed swap, no on-disk swap partition.
zramSwap.enable = true;
# So wifi can be joined from the console.
networking.networkmanager.enable = true;
modules.network.enable = true;
registry.address.neogaia.self = "10.23.50.146";
# So setup can be driven over the network.
# The matching host public keys sit beside this file in plaintext, since
# publishing them is their purpose.
modules.network.ssh.enable = true;
modules.network.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
modules.network.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
modules.network.vpn-client = {
enable = true;
sopsFile = ../../secrets/neogaia.yaml;
address = "10.23.100.3/32";
peer = {
publicKey = "sGEkSD+gEdIMXi4PBN3AhwHGc4tWMtcjOIey3s7yb3I=";
endpoint = "100.0.90.192:51820";
};
};
modules.monorepo.enable = true;
modules.shell.herdr.enable = true;
@@ -50,6 +40,23 @@
modules.desktop.apps.obsidian.enable = true;
modules.desktop.apps.steam.enable = true;
time.timeZone = "America/New_York";
i18n.defaultLocale = "en_GB.UTF-8";
registry.waybar.neogaia.battery = {
order = 70;
settings = {
format = "{icon} {capacity}%";
format-charging = "${util.glyph "f0e7"} {capacity}%";
format-icons = [
(util.glyph "f244")
(util.glyph "f243")
(util.glyph "f242")
(util.glyph "f241")
(util.glyph "f240")
];
states = {
warning = 20;
critical = 10;
};
};
};
}

View File

@@ -1,4 +1,4 @@
{ ... }:
_:
# neogaia's disk layout for disko: one NVMe disk, GPT, with an EFI system
# partition and a LUKS container holding btrfs subvolumes.
# No swap partition, since swap is zram.

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia

View File

@@ -13,70 +13,63 @@
./disk.nix
];
system.stateVersion = "26.05";
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
hardware.cpu.intel.updateMicrocode = true;
hardware.enableRedistributableFirmware = true;
zramSwap.enable = true;
systemd.network = {
enable = true;
netdevs = {
"40-br-vlan20".netdevConfig = {
Name = "br-vlan20";
Kind = "bridge";
};
"40-vlan20" = {
netdevConfig = {
Name = "vlan20";
Kind = "vlan";
};
vlanConfig.Id = 20;
};
};
networks = {
"10-uplink" = {
matchConfig = {
Name = "enp2s0";
MACAddress = "78:55:36:07:af:49";
};
networkConfig.DHCP = "yes";
vlan = [ "vlan20" ];
linkConfig.RequiredForOnline = "routable";
};
"40-vlan20" = {
matchConfig.Name = "vlan20";
networkConfig.Bridge = "br-vlan20";
linkConfig.RequiredForOnline = "no";
};
"40-br-vlan20" = {
matchConfig.Name = "br-vlan20";
linkConfig.RequiredForOnline = "no";
};
};
modules.network.vlans = {
interface = "enp2s0";
ids = [ 20 ];
};
networking.useDHCP = false;
modules.network.vlans.ids = [ 20 ];
boot.zfs.forceImportRoot = false;
modules.zfs = {
modules.host.zfs = {
enable = true;
hostId = "2346edbd";
pools.pikachu = { };
};
registry.address.pikachu.self = "10.23.10.102";
modules.network.ssh.enable = true;
modules.network.ssh.hostKeys.sopsFile = ../../secrets/pikachu.yaml;
modules.network.ssh.userKey.sopsFile = ../../secrets/pikachu.yaml;
guests.valheim = {
enable = true;
vlan = 20;
address = "10.23.20.197";
steamPath = "/pikachu/data/steamcmd";
installPath = "/pikachu/data/servers/valheim/main";
statePath = "/pikachu/media/games/servers/valheim/main";
serverName = "main";
reverseProxy = {
enable = true;
route = {
domain = "valheim.alexion.dev";
protocol = "udp";
ports = [
config.guests.valheim.port
(config.guests.valheim.port + 1)
];
};
};
};
guests.palworld = {
enable = true;
vlan = 20;
address = "10.23.20.250";
steamPath = "/pikachu/data/steamcmd";
installPath = "/pikachu/data/servers/palworld/tracion";
statePath = "/pikachu/media/games/servers/palworld/tracion";
serverName = "Tracion";
autoStart = false;
adminPasswordSecret = "services/game-servers/admin/password";
publicIP = config.guests.palworld.address;
reverseProxy = {
enable = true;
route = {
domain = "palworld.alexion.dev";
protocol = "udp";
ports = [ config.guests.palworld.port ];
};
};
};
guests.actualbudget = {
enable = true;
@@ -88,11 +81,29 @@
enable = true;
route = {
domain = "budget.alexion.dev";
protocol = "https";
port = 5006;
};
};
};
guests.navidrome = {
enable = true;
vlan = 20;
mac = "BC:24:11:C9:17:83";
dataPath = "/pikachu/data/navidrome";
libraryPath = "/pikachu/media/music";
address = "10.23.20.43";
reverseProxy = {
enable = true;
route = {
domain = "music.alexion.dev";
protocol = "https";
port = 4533;
};
};
};
guests.reverse-proxy = {
enable = true;
vlan = 20;
@@ -100,6 +111,20 @@
address = "10.23.20.109";
};
guests.homepage = {
enable = true;
vlan = 20;
address = "10.23.20.110";
reverseProxy = {
enable = true;
route = {
domain = "home";
protocol = "http";
port = 8082;
};
};
};
guests.copyparty = {
enable = true;
vlan = 20;
@@ -110,14 +135,22 @@
enable = true;
route = {
domain = "files.alexion.dev";
protocol = "https";
port = 3923;
clientMaxBodySize = "1g";
};
trustedSources = [ "${registry.address.${config.networking.hostName}."reverse-proxy"}/32" ];
};
volumes = {
"/" = {
hostPath = "/pikachu/data/copyparty/root";
path = "/srv";
access.r = [ "alexion" ];
readOnly = true;
};
"/inbox" = {
hostPath = "/pikachu/__inbox";
path = "/srv/inbox";
path = "/pikachu/__inbox";
access.A = [ "alexion" ];
flags = [
"d2d"
@@ -127,7 +160,7 @@
};
"/media" = {
hostPath = "/pikachu/media";
path = "/srv/media";
path = "/pikachu/media";
access.A = [ "alexion" ];
flags = [
"d2d"
@@ -135,6 +168,13 @@
"dthumb"
];
};
"/opds" = {
hostPath = "/pikachu/data/opds";
path = "/pikachu/data/opds";
access.r = [ "alexion" ];
readOnly = true;
flags = [ "opds" ];
};
};
};
@@ -144,6 +184,4 @@
usbutils
];
time.timeZone = "America/New_York";
i18n.defaultLocale = "en_GB.UTF-8";
}

View File

@@ -1,4 +1,4 @@
{ ... }:
_:
# pikachu's install layout for disko: one NVMe boot disk with an EFI system partition and ext4 root.
# The existing 8 TB ZFS mirror is imported by name and is never declared here.
{

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu

View File

@@ -1,41 +1,35 @@
{
config,
lib,
pkgs,
...
}:
# zeus — Ryzen desktop with AMD graphics.
# Disk layout is in ./disk.nix.
# `fileSystems` are derived from it, none declared here.
{
imports = [ ./disk.nix ] ++ lib.optional (builtins.pathExists ./hardware-configuration.nix) ./hardware-configuration.nix;
imports = [
./disk.nix
]
++ lib.optional (builtins.pathExists ./hardware-configuration.nix) ./hardware-configuration.nix;
system.stateVersion = "26.05";
nixpkgs.hostPlatform = "x86_64-linux";
# systemd-boot on the EFI system partition.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
modules.host.cachyos.enable = true;
boot.kernelPackages = pkgs.linuxPackages_cachyos;
hardware.enableRedistributableFirmware = true;
hardware.graphics = {
enable = true;
enable32Bit = true;
};
# RAM-backed swap, no on-disk swap partition.
zramSwap.enable = true;
modules.network.enable = true;
networking.networkmanager.enable = true;
services.logind.settings.Login = {
HandlePowerKey = "ignore";
HandlePowerKeyLongPress = "ignore";
};
registry.address.zeus.self = "10.23.50.221";
modules.network.ssh.enable = true;
modules.network.ssh.hostKeys.sopsFile = ../../secrets/zeus.yaml;
modules.network.ssh.userKey.sopsFile = ../../secrets/zeus.yaml;
hardware.bluetooth = {
enable = true;
powerOnBoot = true;
@@ -45,18 +39,29 @@
modules.shell.herdr.enable = true;
modules.shell.gitea-axi.enable = true;
modules.shell.pi.enable = true;
modules.shell.omp.enable = true;
modules.desktop.enable = true;
modules.desktop.apps.obsidian.enable = true;
modules.desktop.apps.r2modman.enable = true;
modules.desktop.apps.signal.enable = true;
modules.desktop.apps.steam.enable = true;
modules.desktop.apps.vencord.enable = true;
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
"HDMI-A-1,1920x1080@120,0x0,1,transform,3"
"DP-3,2560x1440@239.96,1080x0,1"
{
output = "HDMI-A-1";
mode = "1920x1080@120";
position = "-1080x0";
scale = 1;
transform = 3;
}
{
output = "DP-3";
mode = "2560x1440@239.96";
position = "0x0";
scale = 1;
}
];
time.timeZone = "America/New_York";
i18n.defaultLocale = "en_GB.UTF-8";
}

View File

@@ -1,4 +1,4 @@
{ ... }:
_:
# zeus's disk layout for disko: two NVMe disks with one EFI system partition
# and two LUKS containers joined into one btrfs filesystem.
# No swap partition, since swap is zram.

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBcHHuubptXCdvED/p2dR3MpjGN/AimqLasLY5984G alexion@zeus

View File

@@ -1,56 +1,35 @@
{
config,
lib,
pkgs,
inputs,
...
}:
# The shared foundation for every host and guest.
let
inherit (lib) mkOption types;
user = config.user;
pinArgs = prev: {
inherit (prev.stdenv.hostPlatform) system;
config.allowUnfree = true;
};
in
{
imports = [ inputs.home-manager.nixosModules.home-manager ];
options.user = {
name = mkOption {
type = types.str;
name = lib.mkOption {
type = lib.types.str;
default = "alexion";
description = ''
Primary interactive user for this system.
This drives the system account and Home Manager user together.
Names the primary user account.
Host configuration uses this name for the Home Manager user.
'';
};
description = mkOption {
type = types.str;
default = "Alexion";
description = "Human-readable description for the primary user.";
};
};
config = {
nixpkgs.overlays = [
(_final: prev: {
unstable = import inputs.nixpkgs-unstable (pinArgs prev);
stable = import inputs.nixpkgs-stable (pinArgs prev);
})
];
nixpkgs.config.allowUnfree = true;
system.stateVersion = "26.05";
nixpkgs.config.allowUnfree = true;
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
users.users.${user.name} = {
users.users.${config.user.name} = {
isNormalUser = true;
description = user.description;
extraGroups = [
"wheel"
"storage"
@@ -59,67 +38,7 @@ in
users.groups.storage.gid = 10000;
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
extraSpecialArgs = {
inherit inputs;
};
users.${user.name} = {
home.username = user.name;
home.homeDirectory = "/home/${user.name}";
home.stateVersion = "26.05";
home.packages = with pkgs; [
eza
wget
];
};
};
registry.shell-shortcut.${config.networking.hostName} = {
ls = {
command = "eza -alg --color=always --group-directories-first --icons=always";
abbreviation = false;
};
la = {
command = "eza -a --color=always --group-directories-first --icons=always";
abbreviation = false;
};
ll = {
command = "eza -lg --color=always --group-directories-first --icons=always";
abbreviation = false;
};
lt = {
command = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
abbreviation = false;
};
"l." = {
command = "eza -a | grep -e '^\\.'";
abbreviation = false;
};
".." = "cd ..";
"..." = "cd ../..";
"...." = "cd ../../..";
cp = "cp -v";
tarnow = "tar -acf ";
untar = "tar -zxvf ";
wget = "wget -c ";
grep = "grep --color=auto";
fgrep = "fgrep --color=auto";
egrep = "egrep --color=auto";
jctl = "journalctl -p 3 -xb";
update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}";
cleanup = "sudo nix-collect-garbage -d";
};
modules.shell.fish.enable = lib.mkDefault true;
modules.shell.fish.defaultShell = lib.mkDefault true;
modules.shell.tmux.enable = lib.mkDefault true;
modules.shell.nvim.enable = lib.mkDefault true;
modules.shell.git.enable = lib.mkDefault true;
modules.shell.yazi.enable = lib.mkDefault true;
time.timeZone = "America/New_York";
i18n.defaultLocale = "en_GB.UTF-8";
};
}

View File

@@ -1,18 +0,0 @@
{
config,
lib,
pkgs,
...
}:
# Alacritty as the desktop terminal.
let
cfg = config.modules.desktop.apps.alacritty;
user = config.user.name;
in
{
options.modules.desktop.apps.alacritty.enable = lib.mkEnableOption "Alacritty as the desktop terminal";
config = lib.mkIf cfg.enable {
home-manager.users.${user}.home.packages = [ pkgs.alacritty ];
};
}

View File

@@ -3,13 +3,12 @@
inputs,
lib,
pkgs,
util,
...
}:
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
# Configures Firefox as the desktop browser.
let
cfg = config.modules.desktop.apps.firefox;
user = config.user.name;
firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system};
cfg = config.modules.desktop.apps.firefox; # NOLINT(let_order)
in
{
options.modules.desktop.apps.firefox = {
@@ -19,19 +18,25 @@ in
type = lib.types.str;
default = "1.0";
description = ''
Firefox UI and page scale, passed to `layout.css.devPixelsPerPx`.
Override it on hosts that need a larger browser scale, such as small
high-DPI laptop panels.
Scales Firefox UI and page content.
Sets `layout.css.devPixelsPerPx`.
Use a larger value for high-DPI displays.
'';
};
};
config = lib.mkIf cfg.enable {
home-manager.users.${user} = hm: {
registry.window-icon.${config.networking.hostName}.firefox = {
class = ".*[Ff]irefox.*";
icon = util.glyph "f269";
};
home-manager.users.${config.user.name} = hm: {
programs.firefox = {
enable = true;
# Enterprise policies: enforced and unchangeable from the browser UI.
# Applies policies that users cannot change in Firefox.
policies = {
DisableTelemetry = true;
DisableFirefoxStudies = true;
@@ -40,7 +45,7 @@ in
DontCheckDefaultBrowser = true;
DisableFirefoxAccounts = true;
# Strip the monetized surfaces from the new-tab page.
# Removes sponsored content from the new-tab page.
FirefoxHome = {
SponsoredTopSites = false;
SponsoredPocket = false;
@@ -49,23 +54,19 @@ in
};
profiles.default = {
isDefault = true;
extensions = {
packages = with firefoxAddons; [
packages = with inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}; [
ublock-origin
proton-pass
sponsorblock
];
# The Nord chrome theme is a declared extension setting, so home-manager
# owns the extension-settings store, overwriting runtime changes to it.
# Home Manager overwrites Nord theme changes made in Firefox.
force = true;
};
# Stylix's Nord mapping paints the selected address-bar result a
# near-white grey, leaving its light text unreadable. Darken that one
# highlight to the Nord selection grey, from the same scheme.
# Stylix uses a near-white selection background.
# Use the Nord selection color because its text stays readable.
extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight =
let
c = hm.config.lib.stylix.colors;
@@ -77,32 +78,26 @@ in
};
settings = {
# Scale the UI and page by a fixed factor.
# Left at auto (-1), Firefox reads the panel's 1.5x and inflates its
# whole chrome while point-sized apps stay put.
# A shade under that brings it into line without dropping to true
# 1:1, which reads too small at this DPI.
"layout.css.devPixelsPerPx" = cfg.uiScale;
# Auto-enable the sideloaded Firefox Color add-on carrying the Nord
# chrome theme, which Firefox otherwise leaves disabled.
# Enables Firefox Color after Firefox sideloads the add-on.
"extensions.autoDisableScopes" = 0;
# Sponsored surfaces the policies above do not reach.
# Removes sponsored content outside Firefox policies.
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
"browser.newtabpage.activity-stream.showSponsored" = false;
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
};
search = {
# Declaring search overwrites Firefox's own engine list wholesale,
# so engines added later in the UI do not survive a rebuild.
# Home Manager replaces the engine list when this setting is enabled.
# Engines added in Firefox do not survive a rebuild.
force = true;
default = "ddg";
# The general-purpose commercial engines, hidden to leave a lean
# DuckDuckGo-and-Wikipedia list. They stay reachable through bangs.
# An engine carrying only metaData is treated as a builtin.
# Hides commercial search engines.
# Bangs still reach these engines.
# Entries with only `metaData` configure Firefox built-in engines.
engines = {
google.metaData.hidden = true;
bing.metaData.hidden = true;
@@ -114,8 +109,7 @@ in
};
};
# Nord chrome for the one profile, applied through the Stylix-managed
# Firefox Color add-on that colorTheme enables.
# Applies the Nord theme through Stylix's Firefox Color add-on.
stylix.targets.firefox = {
enable = true;
profileNames = [ "default" ];
@@ -133,8 +127,8 @@ in
};
};
# Firefox writes profiles.ini itself on first launch, so home-manager is
# told to own the file rather than fail activation refusing to clobber it.
# Firefox creates `profiles.ini` on its first launch.
# Force replacement during Home Manager activation.
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
};
};

View File

@@ -2,17 +2,30 @@
config,
lib,
pkgs,
util,
...
}:
# Ghostty as the desktop terminal.
let
cfg = config.modules.desktop.apps.ghostty;
user = config.user.name;
in
{
options.modules.desktop.apps.ghostty.enable = lib.mkEnableOption "Ghostty as the desktop terminal";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.apps.ghostty.enable {
registry.keybind.${config.networking.hostName}.ghostty = {
keys = "SUPER + Return";
action = {
kind = "exec";
command = lib.getExe pkgs.ghostty;
};
};
registry.window-icon.${config.networking.hostName}.ghostty = {
class = "com.mitchellh.ghostty";
icon = util.glyph "f120";
};
home-manager.users.${user}.home.packages = [ pkgs.ghostty ];
};
}

View File

@@ -2,17 +2,23 @@
config,
lib,
pkgs,
util,
...
}:
# Obsidian as the desktop note-taking vault.
let
cfg = config.modules.desktop.apps.obsidian;
user = config.user.name;
in
{
options.modules.desktop.apps.obsidian.enable = lib.mkEnableOption "Obsidian as the desktop note-taking vault";
options.modules.desktop.apps.obsidian.enable =
lib.mkEnableOption "Obsidian as the desktop note-taking vault";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.apps.obsidian.enable {
home-manager.users.${user}.home.packages = [ pkgs.obsidian ];
registry.window-icon.${config.networking.hostName}.obsidian = {
class = "obsidian";
icon = util.glyph "f02d";
};
};
}

View File

@@ -0,0 +1,18 @@
{
config,
lib,
pkgs,
...
}:
# r2modman, a Thunderstore mod manager.
let
user = config.user.name;
in
{
options.modules.desktop.apps.r2modman.enable =
lib.mkEnableOption "r2modman, a Thunderstore mod manager";
config = lib.mkIf config.modules.desktop.apps.r2modman.enable {
home-manager.users.${user}.home.packages = [ pkgs.r2modman ];
};
}

View File

@@ -2,17 +2,22 @@
config,
lib,
pkgs,
util,
...
}:
# Signal desktop messenger.
let
cfg = config.modules.desktop.apps.signal;
user = config.user.name;
in
{
options.modules.desktop.apps.signal.enable = lib.mkEnableOption "Signal desktop messenger";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.apps.signal.enable {
home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ];
registry.window-icon.${config.networking.hostName}.signal = {
class = "[Ss]ignal";
icon = util.glyph "f4ad";
};
};
}

View File

@@ -2,16 +2,16 @@
config,
lib,
pkgs,
util,
...
}:
# Steam game launcher and runtime integration.
let
cfg = config.modules.desktop.apps.steam;
in
{
options.modules.desktop.apps.steam.enable = lib.mkEnableOption "Steam game launcher";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.apps.steam.enable {
programs.gamemode.enable = true;
programs.steam = {
enable = true;
package = pkgs.steam.override {
@@ -20,5 +20,14 @@ in
};
hardware.steam-hardware.enable = true;
environment.systemPackages = [
pkgs.gamescope
];
registry.window-icon.${config.networking.hostName}.steam = {
class = "[Ss]team";
icon = util.glyph "f1b6";
};
};
}

View File

@@ -2,17 +2,23 @@
config,
lib,
pkgs,
util,
...
}:
# Vesktop, a Discord client with Vencord built in.
let
cfg = config.modules.desktop.apps.vencord;
user = config.user.name;
in
{
options.modules.desktop.apps.vencord.enable = lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
options.modules.desktop.apps.vencord.enable =
lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.apps.vencord.enable {
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
registry.window-icon.${config.networking.hostName}.vesktop = {
class = "[Vv]esktop";
icon = util.glyph "f1ff";
};
};
}

View File

@@ -1,14 +1,11 @@
{ config, lib, ... }:
# The desktop aggregator: one flag brings up the whole graphical session.
let
cfg = config.modules.desktop;
in
{
options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop";
# Each piece is turned on at default priority, so a host can still override
# any one of them while the single flag above enables the whole desktop.
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.enable {
modules.desktop.apps.firefox.enable = lib.mkDefault true;
modules.desktop.apps.ghostty.enable = lib.mkDefault true;
modules.desktop.session.hyprland.enable = lib.mkDefault true;
@@ -26,5 +23,6 @@ in
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
modules.desktop.utilities.theming.enable = lib.mkDefault true;
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
registry.env.${config.networking.hostName}.NIXOS_OZONE_WL = "1";
};
}

View File

@@ -1,41 +1,15 @@
{
config,
lib,
pkgs,
util,
...
}:
# The Hyprland compositor.
let
cfg = config.modules.desktop.session.hyprland;
user = config.user.name;
cursor = config.stylix.cursor;
# Numbered-workspace switch and move for 1..9, the operator's i3 muscle memory.
workspaceBinds = lib.concatMap (n: [
"$mod, ${toString n}, workspace, ${toString n}"
"$mod SHIFT, ${toString n}, movetoworkspace, ${toString n}"
]) (lib.range 1 9);
# Flip the tiling strategy between the two built-in layouts, since neither a
# dispatcher nor a keyword toggles it on its own.
toggleLayout = pkgs.writeShellScript "hypr-toggle-layout" ''
if [ "$(hyprctl getoption -j general:layout | ${pkgs.jq}/bin/jq -r .str)" = dwindle ]; then
hyprctl keyword general:layout master
else
hyprctl keyword general:layout dwindle
fi
'';
in
{
options.modules.desktop.session.hyprland = {
enable = lib.mkEnableOption "the Hyprland compositor";
options.modules.desktop.session.hyprland.enable = lib.mkEnableOption "the Hyprland compositor";
blur = lib.mkEnableOption ''
window blur. Off by default as the single biggest battery cost on a
laptop, left on for a host with the headroom to spend it'';
};
config = lib.mkIf config.modules.desktop.session.hyprland.enable {
config = lib.mkIf cfg.enable {
# This program integration owns the session, portals, and polkit, launched
# through the universal Wayland session manager.
programs.hyprland = {
@@ -43,7 +17,7 @@ in
withUWSM = true;
};
home-manager.users.${user}.wayland.windowManager.hyprland = {
home-manager.users.${config.user.name}.wayland.windowManager.hyprland = {
enable = true;
# One package drives the whole session, so there is never a version split.
@@ -55,136 +29,216 @@ in
# uwsm owns the systemd graphical-session targets.
systemd.enable = false;
# Write the native hyprlang hyprland.conf, whose variable and bind syntax
# the settings below are expressed in.
configType = "hyprlang";
# Write the native Lua Hyprland config. Home Manager renders the Nix
# settings below as hl.* calls.
configType = "lua";
settings = {
"$mod" = "SUPER";
"$terminal" = "ghostty";
# Session variables handed to the compositor directly.
# UWSM launches the session without the shell profile that would carry
# them, so a variable the compositor or its children must see is set
# here rather than through home-manager's sessionVariables.
env =
[
# Chromium and Electron apps read this to select native Wayland;
# nixpkgs wrappers (Obsidian's included) gate their Wayland flags on
# it, so without it they run under XWayland and blur at this DPI.
"NIXOS_OZONE_WL,1"
]
# Bibata ships XCursor only.
# The hyprcursor variables name the same theme, which Hyprland
# resolves through its XCursor fallback.
++ lib.optionals (cursor != null) [
"XCURSOR_THEME,${cursor.name}"
"XCURSOR_SIZE,${toString cursor.size}"
"HYPRCURSOR_THEME,${cursor.name}"
"HYPRCURSOR_SIZE,${toString cursor.size}"
];
input = {
kb_layout = "us";
# Caps is a second Escape.
# Shift+Caps still toggles a real CapsLock.
kb_options = "caps:escape_shifted_capslock";
# Snappy key repeat.
repeat_delay = 250;
repeat_rate = 45;
accel_profile = "flat";
touchpad = {
natural_scroll = true;
tap-to-click = true;
disable_while_typing = true;
config = {
input = {
kb_layout = config.services.xserver.xkb.layout;
repeat_delay = 250;
repeat_rate = 45;
accel_profile = "flat";
touchpad = {
natural_scroll = true;
tap_to_click = true;
disable_while_typing = true;
};
};
general = {
gaps_in = 4;
gaps_out = 8;
border_size = 2;
layout = "dwindle";
};
decoration = {
rounding = 6;
blur.enabled = false;
};
xwayland.force_zero_scaling = true;
dwindle.preserve_split = true;
};
general = {
gaps_in = 4;
gaps_out = 8;
border_size = 2;
layout = "dwindle";
};
decoration = {
rounding = 6;
blur.enabled = cfg.blur;
};
# XWayland clients render at the panel's native resolution instead of
# being raster-scaled by the compositor at the fractional monitor scale.
xwayland.force_zero_scaling = true;
animations = {
enabled = true;
bezier = [ "ease, 0.25, 0.1, 0.25, 1.0" ];
# Durations are in centiseconds.
# Short values keep the motion subtle.
animation = [
"windows, 1, 3, ease"
"fade, 1, 3, ease"
# Layer surfaces like the launcher fade in a touch quicker than windows.
"layersIn, 1, 2, ease"
"fadeLayersIn, 1, 2, ease"
"workspaces, 1, 3, ease"
"border, 1, 3, ease"
env = lib.mapAttrsToList (name: value: {
_args = [
name
value
];
}) (lib.attrByPath [ config.networking.hostName ] { } config.registry.env);
bind = lib.mapAttrsToList (_name: keybind: {
_args = [
keybind.keys
(lib.generators.mkLuaInline (
{
exec = "hl.dsp.exec_cmd(${lib.generators.toLua { } keybind.action.command})";
focus = "hl.dsp.focus(${
lib.generators.toLua { } { inherit (keybind.action) direction workspace; }
})";
"move-window" = "hl.dsp.window.move(${
lib.generators.toLua { } { inherit (keybind.action) direction workspace; }
})";
"resize-window" = "hl.dsp.window.resize(${
lib.generators.toLua { } (
{
down = {
x = 0;
y = 10;
relative = true;
};
left = {
x = -10;
y = 0;
relative = true;
};
right = {
x = 10;
y = 0;
relative = true;
};
up = {
x = 0;
y = -10;
relative = true;
};
}
.${keybind.action.direction}
)
})";
"toggle-floating" = ''hl.dsp.window.float({ action = "toggle" })'';
"toggle-fullscreen" = "hl.dsp.window.fullscreen()";
"close-window" = "hl.dsp.window.close()";
}
.${keybind.action.kind}
))
]
++ lib.optional keybind.repeating {
repeating = true;
}
++ lib.optional keybind.locked {
locked = true;
};
}) (lib.attrByPath [ config.networking.hostName ] { } config.registry.keybind);
};
};
registry.keybind.${config.networking.hostName} = lib.mkMerge [
# Generate numeric workspace keybinds.
(lib.listToAttrs (
lib.concatMap (workspace: [
{
name = "hyprland-workspace-${toString workspace}";
value = {
keys = "SUPER + ${toString workspace}";
action = {
kind = "focus";
inherit workspace;
};
};
}
{
name = "hyprland-move-to-workspace-${toString workspace}";
value = {
keys = "SUPER + SHIFT + ${toString workspace}";
action = {
kind = "move-window";
inherit workspace;
};
};
}
]) (lib.range 1 9)
))
# Generate directional keybinds.
(lib.listToAttrs (
lib.concatMap
(
{ direction, key }:
[
{
name = "hyprland-focus-${direction}";
value = {
keys = "SUPER + ${key}";
action = {
kind = "focus";
inherit direction;
};
};
}
{
name = "hyprland-move-${direction}";
value = {
keys = "SUPER + SHIFT + ${key}";
action = {
kind = "move-window";
inherit direction;
};
};
}
{
name = "hyprland-resize-${direction}";
value = {
keys = "SUPER + ALT + ${key}";
repeating = true;
action = {
kind = "resize-window";
inherit direction;
};
};
}
]
)
[
{
key = "H";
direction = "left";
}
{
key = "J";
direction = "down";
}
{
key = "K";
direction = "up";
}
{
key = "L";
direction = "right";
}
]
))
{
hyprland-toggle-floating = {
keys = "SUPER + Space";
action.kind = "toggle-floating";
};
dwindle = {
preserve_split = true;
hyprland-toggle-fullscreen = {
keys = "SUPER + F";
action.kind = "toggle-fullscreen";
};
bind = [
"$mod, Return, exec, $terminal"
hyprland-close-window = {
keys = "SUPER + SHIFT + Q";
action.kind = "close-window";
};
}
];
# Move focus.
"$mod, H, movefocus, l"
"$mod, J, movefocus, d"
"$mod, K, movefocus, u"
"$mod, L, movefocus, r"
# Move the window within the layout.
"$mod SHIFT, H, movewindow, l"
"$mod SHIFT, J, movewindow, d"
"$mod SHIFT, K, movewindow, u"
"$mod SHIFT, L, movewindow, r"
# Resize the active window.
"$mod ALT, H, resizeactive, -40 0"
"$mod ALT, J, resizeactive, 0 40"
"$mod ALT, K, resizeactive, 0 -40"
"$mod ALT, L, resizeactive, 40 0"
"$mod, Space, togglefloating,"
"$mod, F, fullscreen,"
# togglesplit is a dwindle layout message, reached through layoutmsg.
"$mod, T, layoutmsg, togglesplit"
"$mod SHIFT, T, exec, ${toggleLayout}"
"$mod SHIFT, Q, killactive,"
"$mod CTRL, Q, forcekillactive,"
]
++ workspaceBinds;
# Volume and brightness keys repeat while held, each raising a popup
# through the OSD client.
# Volume is capped at 100 percent.
# The client floors brightness so a full hold cannot black the screen out.
binde = [
", XF86AudioRaiseVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume raise --max-volume 100"
", XF86AudioLowerVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume lower"
", XF86MonBrightnessUp, exec, ${pkgs.swayosd}/bin/swayosd-client --brightness raise"
", XF86MonBrightnessDown, exec, ${pkgs.swayosd}/bin/swayosd-client --brightness lower"
];
# Mute and media transport still fire while the session is locked.
bindl = [
", XF86AudioMute, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume mute-toggle"
", XF86AudioPlay, exec, ${pkgs.playerctl}/bin/playerctl play-pause"
", XF86AudioNext, exec, ${pkgs.playerctl}/bin/playerctl next"
", XF86AudioPrev, exec, ${pkgs.playerctl}/bin/playerctl previous"
];
registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
position = "left";
order = 10;
settings = {
format = "{id} {windows}";
format-window-separator = " ";
window-rewrite-default = util.glyph "f2d0";
window-rewrite = lib.mapAttrs' (
_name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" windowIcon.icon
) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon);
};
};
};

View File

@@ -5,62 +5,34 @@
...
}:
# hypridle: idle-triggered locking and display power management.
let
cfg = config.modules.desktop.session.hyprland.hypridle;
user = config.user.name;
hyprctl = "${config.programs.hyprland.package}/bin/hyprctl";
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
# The guard drops the call when a locker is already up, so no idle trigger
# stacks a second hyprlock over the first.
lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}";
in
{
options.modules.desktop.session.hyprland.hypridle = {
enable = lib.mkEnableOption "hypridle idle management";
options.modules.desktop.session.hyprland.hypridle.enable =
lib.mkEnableOption "hypridle idle management";
lockTimeout = lib.mkOption {
type = lib.types.ints.positive;
default = 300;
description = "Seconds of inactivity before the screen locks.";
};
screenOffTimeout = lib.mkOption {
type = lib.types.ints.positive;
default = 360;
description = "Seconds of inactivity before the displays are powered off.";
};
};
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.session.hyprland.hypridle.enable {
# Closing the lid suspends, and every suspend locks first through the
# before_sleep_cmd below, so the lid always lands at a locked screen.
services.logind.settings.Login.HandleLidSwitch = "suspend";
home-manager.users.${user}.services.hypridle = {
home-manager.users.${config.user.name}.services.hypridle = {
enable = true;
settings = {
general = {
lock_cmd = lockCmd;
# The guard drops the call when a locker is already up, so no idle
# trigger stacks a second hyprlock over the first.
lock_cmd = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
before_sleep_cmd = "loginctl lock-session";
# Waking restores the displays the screen-off listener may have cut.
after_sleep_cmd = "${hyprctl} dispatch dpms on";
# Waking pokes the compositor in case firmware leaves displays dark.
after_sleep_cmd = "${lib.getExe' config.programs.hyprland.package "hyprctl"} dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
};
listener = [
# Lock on idle.
{
timeout = cfg.lockTimeout;
timeout = 300;
on-timeout = "loginctl lock-session";
}
# Power the displays off a little later, restoring them on any activity.
{
timeout = cfg.screenOffTimeout;
on-timeout = "${hyprctl} dispatch dpms off";
on-resume = "${hyprctl} dispatch dpms on";
}
];
};
};

View File

@@ -2,53 +2,63 @@
config,
lib,
pkgs,
util,
...
}:
# The lock screen: a hyprlock session-lock surface the compositor owns.
let
cfg = config.modules.desktop.session.hyprland.hyprlock;
user = config.user.name;
# The hyprlock this module installs, used by the lock keybind below.
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
sleepIcon = util.glyph [
"db81"
"dcb2"
];
sleepOffIcon = util.glyph [
"db81"
"dcb3"
];
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
unit=lockscreen-idle-inhibit.service
refresh_waybar() {
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
${lib.getExe' pkgs.procps "pkill"} -RTMIN+9 waybar >/dev/null 2>&1 || true
}
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
${pkgs.systemd}/bin/systemctl --user stop "$unit"
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet "$unit"; then
${lib.getExe' pkgs.systemd "systemctl"} --user stop "$unit"
refresh_waybar
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepIcon}"
exit 0
fi
if ${pkgs.systemd}/bin/systemd-run \
if ${lib.getExe' pkgs.systemd "systemd-run"} \
--user \
--collect \
--unit=lockscreen-idle-inhibit \
--description="Disable idle-triggered screen locking" \
${pkgs.systemd}/bin/systemd-inhibit \
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
--what=idle \
--who=lockscreen-hotkey \
--why="lock screen disabled by hotkey" \
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
${lib.getExe' pkgs.coreutils "sleep"} infinity >/dev/null; then
refresh_waybar
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}"
fi
'';
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet lockscreen-idle-inhibit.service; then
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
else
printf '{"text":"","class":"idle"}\n'
fi
'';
in
{
options.modules.desktop.session.hyprland.hyprlock.enable = lib.mkEnableOption "the hyprlock lock screen";
options.modules.desktop.session.hyprland.hyprlock.enable =
lib.mkEnableOption "the hyprlock lock screen";
config = lib.mkIf cfg.enable {
home-manager.users.${user} = {
config = lib.mkIf config.modules.desktop.session.hyprland.hyprlock.enable {
home-manager.users.${config.user.name} = {
# Colors and the lock-screen background come from Stylix's hyprlock target,
# so only geometry and behaviour are set here.
programs.hyprlock = {
@@ -81,14 +91,36 @@ in
};
};
};
};
# Lock on Super+X.
# The guard drops the keypress when a locker is already up, so a second
# hyprlock never stacks over the first.
wayland.windowManager.hyprland.settings.bind = [
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
"$mod CTRL, X, exec, ${idleLockToggle}"
];
registry.keybind.${config.networking.hostName} = {
hyprlock-lock = {
keys = "SUPER + X";
action = {
kind = "exec";
# The guard drops the call when a locker is already up, so no manual
# trigger stacks a second hyprlock over the first.
command = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
};
};
hyprlock-toggle-idle-inhibit = {
keys = "SUPER + CTRL + X";
action = {
kind = "exec";
command = "${idleLockToggle}";
};
};
};
registry.waybar.${config.networking.hostName}."custom/idle-lock" = {
order = 20;
settings = {
return-type = "json";
exec = "${idleLockStatus}";
interval = "once";
signal = 9;
};
};
};
}

View File

@@ -6,18 +6,20 @@
}:
# Text login: greetd running the tuigreet greeter, which starts the session
# through the universal Wayland session manager.
let
cfg = config.modules.desktop.session.login;
in
{
options.modules.desktop.session.login.enable = lib.mkEnableOption "greetd with the tuigreet text greeter";
options.modules.desktop.session.login.enable =
lib.mkEnableOption "greetd with the tuigreet text greeter";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.session.login.enable {
services.greetd = {
enable = true;
settings.default_session.command =
"${lib.getExe pkgs.tuigreet} --time --remember "
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
settings.default_session.command = lib.escapeShellArgs [
(lib.getExe pkgs.tuigreet)
"--time"
"--remember"
"--cmd"
"${lib.getExe config.programs.uwsm.package} start -e -D Hyprland hyprland.desktop"
];
};
};
}

View File

@@ -2,20 +2,29 @@
config,
lib,
pkgs,
util,
...
}:
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
let
cfg = config.modules.desktop.session.mako;
user = config.user.name;
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
if ${lib.getExe' pkgs.mako "makoctl"} mode 2>/dev/null | ${lib.getExe pkgs.gnugrep} -qx dnd; then
printf '{"text":"${util.glyph "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n'
else
printf '{"text":"${util.glyph "f0f3"}","tooltip":"Notifications on","class":"active"}\n'
fi
'';
makoctl = "${pkgs.mako}/bin/makoctl";
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
${lib.getExe' pkgs.mako "makoctl"} mode -t dnd >/dev/null 2>&1
${lib.getExe' pkgs.procps "pkill"} -RTMIN+8 waybar
'';
in
{
options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
config = lib.mkIf cfg.enable {
home-manager.users.${user} = {
config = lib.mkIf config.modules.desktop.session.mako.enable {
home-manager.users.${config.user.name} = {
# Colors and the popup font come from Stylix's mako target, so only
# behaviour is set here.
services.mako = {
@@ -32,13 +41,25 @@ in
"mode=dnd".invisible = true;
};
};
};
# Recall the last notification from history, keyboard-only like the rest
# of the session.
# $mod is defined by the compositor config these binds share.
wayland.windowManager.hyprland.settings.bind = [
"$mod, N, exec, ${makoctl} restore"
];
registry.keybind.${config.networking.hostName}.mako-history = {
keys = "SUPER + N";
action = {
kind = "exec";
command = "${lib.getExe' pkgs.mako "makoctl"} restore";
};
};
registry.waybar.${config.networking.hostName}."custom/dnd" = {
order = 80;
settings = {
return-type = "json";
exec = "${dndStatus}";
on-click = "${dndToggle}";
interval = "once";
signal = 8;
};
};
};
}

View File

@@ -1,13 +1,10 @@
{ config, lib, ... }:
# XDG desktop portal routing for in-app screen sharing and file dialogs.
let
cfg = config.modules.desktop.session.portals;
in
{
options.modules.desktop.session.portals.enable =
lib.mkEnableOption "XDG desktop portals for in-app screen sharing and file dialogs";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.session.portals.enable {
# The backend packages arrive with the compositor, so only the routing is set
# here.
# Screen sharing, screenshots, and global shortcuts need the compositor's

View File

@@ -6,31 +6,29 @@
}:
# rofi as the desktop launcher and dmenu-style menu frontend.
let
cfg = config.modules.desktop.session.rofi;
user = config.user.name;
# A session menu built on the same themed rofi, so the power actions look
# like every other menu the launcher drives.
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
| ${pkgs.rofi}/bin/rofi -dmenu -i -p Power)
| ${lib.getExe pkgs.rofi} -dmenu -i -p Power)
case "$chosen" in
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;;
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;;
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;;
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;;
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;;
Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
'Shut down') ${lib.getExe' pkgs.systemd "systemctl"} poweroff ;;
esac
'';
in
{
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
config = lib.mkIf cfg.enable {
home-manager.users.${user} = {
# No theme is set here: Stylix's rofi target supplies the Nord one.
config = lib.mkIf config.modules.desktop.session.rofi.enable {
home-manager.users.${config.user.name} = hm: {
# Stylix supplies the Nord theme.
programs.rofi = {
enable = true;
font = lib.mkForce "JetBrains Mono 12";
# Application launch is the only mode, kept minimal so the prompt opens
# as fast as rofi can read the desktop entries.
@@ -39,14 +37,31 @@ in
modi = "drun";
show-icons = false;
};
theme.window = {
border = hm.config.lib.formats.rasi.mkLiteral "2px";
border-color = hm.config.lib.formats.rasi.mkLiteral "#FFFFFF";
border-radius = hm.config.lib.formats.rasi.mkLiteral "8px";
};
};
};
registry.keybind.${config.networking.hostName} = {
rofi-launcher = {
keys = "SUPER + R";
action = {
kind = "exec";
command = "${lib.getExe pkgs.rofi} -show drun";
};
};
# The launcher and the session menu, kept next to the tool they drive.
# $mod is defined by the compositor config these binds share.
wayland.windowManager.hyprland.settings.bind = [
"$mod, R, exec, ${pkgs.rofi}/bin/rofi -show drun"
"$mod SHIFT, X, exec, ${powerMenu}"
];
rofi-power-menu = {
keys = "SUPER + SHIFT + X";
action = {
kind = "exec";
command = "${powerMenu}";
};
};
};
};
}

View File

@@ -6,144 +6,56 @@
}:
# Waybar: a top status bar reading system state at a glance.
let
cfg = config.modules.desktop.session.waybar;
user = config.user.name;
moduleNames =
position:
map (entry: entry.name) (
lib.sort (left: right: left.module.order < right.module.order) (
lib.filter (entry: entry.module.position == position) moduleEntries
)
);
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
# through JSON so the glyph survives as bytes rather than an editor paste.
g = code: builtins.fromJSON ''"\u${code}"'';
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
moduleEntries = lib.mapAttrsToList (name: module: {
inherit name module;
}) modules;
# Renders and toggles mako's do-not-disturb mode.
# With no mako daemon answering, the status reads "notifications on", keeping
# the widget inert rather than broken.
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
if ${pkgs.mako}/bin/makoctl mode 2>/dev/null | ${pkgs.gnugrep}/bin/grep -qx dnd; then
printf '{"text":"${g "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n'
else
printf '{"text":"${g "f0f3"}","tooltip":"Notifications on","class":"active"}\n'
fi
'';
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
${pkgs.mako}/bin/makoctl mode -t dnd >/dev/null 2>&1
${pkgs.procps}/bin/pkill -RTMIN+8 waybar
'';
# Lit while a wf-recorder capture is running.
# Empty text collapses the widget when idle, so it shows nothing until then.
recStatus = pkgs.writeShellScript "waybar-recording-status" ''
if ${pkgs.procps}/bin/pgrep -x wf-recorder >/dev/null; then
printf '{"text":"${g "f03d"}","tooltip":"Recording","class":"recording"}\n'
else
printf '{"text":"","class":"idle"}\n'
fi
'';
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet lockscreen-idle-inhibit.service; then
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
else
printf '{"text":"","class":"idle"}\n'
fi
'';
networkStatus = pkgs.writeShellScript "waybar-network-status" ''
active=$(
${pkgs.networkmanager}/bin/nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status \
| ${pkgs.gawk}/bin/awk -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
)
if [ -z "$active" ]; then
${pkgs.jq}/bin/jq -cn \
--arg text "${g "f127"} off" \
--arg tooltip "Network disconnected" \
--arg class "disconnected" \
'{ text: $text, tooltip: $tooltip, class: $class }'
exit 0
fi
IFS=: read -r device type _state connection <<< "$active"
case "$type" in
wifi) icon="${g "f1eb"}" ;;
ethernet) icon="LAN" ;;
*) icon="NET" ;;
esac
ipaddr=$(${pkgs.iproute2}/bin/ip -4 -o addr show dev "$device" | ${pkgs.gawk}/bin/awk '{ print $4; exit }')
${pkgs.jq}/bin/jq -cn \
--arg text "$icon $connection" \
--arg tooltip "$device: ''${ipaddr:-no IPv4}" \
--arg class "$type" \
'{ text: $text, tooltip: $tooltip, class: $class }'
'';
modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
in
{
options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.session.waybar.enable {
# A symbols font backs the module glyphs, so pango falls back to it for the
# codepoints Stylix's monospace font does not carry.
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
home-manager.users.${user}.programs.waybar = {
home-manager.users.${config.user.name}.programs.waybar = {
enable = true;
# A user service bound to graphical-session.target, which uwsm activates,
# so the bar comes up with the session and no compositor exec-once.
systemd.enable = true;
style = ''
.modules-right {
margin-right: 8px;
settings.mainBar = lib.mkMerge [
{
layer = "top";
position = "top";
height = 30;
spacing = 8;
margin-right = 8;
modules-left = moduleNames "left";
modules-center = moduleNames "center";
modules-right = moduleNames "right";
}
(lib.mapAttrs (_name: module: module.settings) modules)
];
};
#custom-dnd {
margin-right: 8px;
}
'';
settings.mainBar = {
layer = "top";
position = "top";
height = 30;
spacing = 8;
modules-left = [ "hyprland/workspaces" ];
modules-center = [ "clock" ];
modules-right = [
"custom/recording"
"custom/idle-lock"
"mpris"
"wireplumber"
"custom/network"
"battery"
"custom/dnd"
];
# Per-application icons for the windows on each workspace, mapped from
# window class with a neutral glyph for anything unlisted.
"hyprland/workspaces" = {
on-click = "activate";
format = "{id} {windows}";
format-window-separator = " ";
window-rewrite-default = g "f2d0";
window-rewrite = {
"class<Alacritty>" = g "f120";
"class<com.mitchellh.ghostty>" = g "f120";
"class<.*[Ff]irefox.*>" = g "f269";
"class<chromium.*>" = g "f268";
"class<[Cc]ode>" = g "f121";
"class<obsidian>" = g "f02d";
"class<[Ss]ignal>" = g "f4ad";
"class<[Ss]team>" = g "f1b6";
"class<[Vv]esktop>" = g "f1ff";
};
};
clock = {
registry.waybar.${config.networking.hostName} = {
clock = {
position = "center";
order = 10;
settings = {
format = "{:%a %d %b %H:%M}";
tooltip-format = "<tt>{calendar}</tt>";
calendar = {
@@ -152,71 +64,6 @@ in
format.today = "<b><u>{}</u></b>";
};
};
mpris = {
format = "${g "f001"} {title}";
format-paused = "${g "f04c"} {title}";
max-length = 40;
on-click = "play-pause";
on-scroll-up = "next";
on-scroll-down = "previous";
};
wireplumber = {
format = "{icon} {volume}%";
format-muted = "${g "f026"} muted";
format-icons = [
(g "f026")
(g "f027")
(g "f028")
];
on-click = "${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
};
"custom/network" = {
return-type = "json";
exec = "${networkStatus}";
interval = 5;
};
battery = {
format = "{icon} {capacity}%";
format-charging = "${g "f0e7"} {capacity}%";
format-icons = [
(g "f244")
(g "f243")
(g "f242")
(g "f241")
(g "f240")
];
states = {
warning = 20;
critical = 10;
};
};
"custom/dnd" = {
return-type = "json";
exec = "${dndStatus}";
on-click = "${dndToggle}";
interval = "once";
signal = 8;
};
# Recording state has no event to hook, so the widget samples the
# wf-recorder process once a second.
"custom/recording" = {
return-type = "json";
exec = "${recStatus}";
interval = 1;
};
"custom/idle-lock" = {
return-type = "json";
exec = "${idleLockStatus}";
interval = "once";
signal = 9;
};
};
};
};

View File

@@ -1,12 +1,25 @@
{ config, lib, ... }:
{
config,
lib,
pkgs,
util,
...
}:
# PipeWire as the desktop audio server.
let
cfg = config.modules.desktop.utilities.audio;
mediaKey = keys: command: {
inherit keys;
locked = true;
action = {
kind = "exec";
command = "${lib.getExe pkgs.playerctl} ${command}";
};
};
in
{
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.utilities.audio.enable {
# Realtime scheduling for the audio threads, so playback survives load.
security.rtkit.enable = true;
@@ -15,5 +28,39 @@ in
alsa.enable = true;
pulse.enable = true;
};
registry.keybind.${config.networking.hostName} = {
media-play = mediaKey "XF86AudioPlay" "play-pause";
media-next = mediaKey "XF86AudioNext" "next";
media-previous = mediaKey "XF86AudioPrev" "previous";
};
registry.waybar.${config.networking.hostName} = {
mpris = {
order = 30;
settings = {
format = "${util.glyph "f001"} {title}";
format-paused = "${util.glyph "f04c"} {title}";
max-length = 40;
on-click = "play-pause";
on-scroll-up = "next";
on-scroll-down = "previous";
};
};
wireplumber = {
order = 40;
settings = {
format = "{icon} {volume}%";
format-muted = "${util.glyph "f026"} muted";
format-icons = [
(util.glyph "f026")
(util.glyph "f027")
(util.glyph "f028")
];
on-click = "${lib.getExe' pkgs.wireplumber "wpctl"} set-mute @DEFAULT_AUDIO_SINK@ toggle";
};
};
};
};
}

View File

@@ -6,29 +6,23 @@
}:
# Clipboard history: cliphist records every copy, picked back through rofi.
let
cfg = config.modules.desktop.utilities.clipboard;
user = config.user.name;
cliphist = "${pkgs.cliphist}/bin/cliphist";
rofi = "${pkgs.rofi}/bin/rofi";
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
# The picker reuses the themed rofi, so history looks like every other menu
# the launcher drives.
# decode is needed because list emits id-prefixed lines rather than the copied
# bytes, so the chosen id has to be resolved back before it can be re-copied.
picker = pkgs.writeShellScript "clipboard-picker" ''
${cliphist} list \
| ${rofi} -dmenu -i -p Clipboard \
| ${cliphist} decode \
| ${wl-copy}
${lib.getExe pkgs.cliphist} list \
| ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
| ${lib.getExe pkgs.cliphist} decode \
| ${lib.getExe' pkgs.wl-clipboard "wl-copy"}
'';
in
{
options.modules.desktop.utilities.clipboard.enable = lib.mkEnableOption "cliphist clipboard history";
options.modules.desktop.utilities.clipboard.enable =
lib.mkEnableOption "cliphist clipboard history";
config = lib.mkIf cfg.enable {
home-manager.users.${user} = {
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
home-manager.users.${config.user.name} = {
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
# clipboard.
# The watchers and picker above reach wl-clipboard by store path, so this
@@ -38,11 +32,14 @@ in
# Two watchers record text and images to history, bound to the graphical
# session so uwsm starts and stops them with it.
services.cliphist.enable = true;
};
# $mod is defined by the compositor config these binds share.
wayland.windowManager.hyprland.settings.bind = [
"$mod SHIFT, V, exec, ${picker}"
];
registry.keybind.${config.networking.hostName}.clipboard-history = {
keys = "SUPER + SHIFT + V";
action = {
kind = "exec";
command = "${picker}";
};
};
};
}

View File

@@ -6,13 +6,60 @@
}:
# SwayOSD: a transient on-screen popup for volume and brightness changes.
let
cfg = config.modules.desktop.utilities.osd;
user = config.user.name;
in
{
options.modules.desktop.utilities.osd.enable = lib.mkEnableOption "the SwayOSD volume and brightness popup";
options.modules.desktop.utilities.osd.enable =
lib.mkEnableOption "the SwayOSD volume and brightness popup";
config = lib.mkIf config.modules.desktop.utilities.osd.enable {
registry.keybind.${config.networking.hostName} = {
volume-up = {
keys = "XF86AudioRaiseVolume";
repeating = true;
action = {
kind = "exec";
command = "${pkgs.swayosd}/bin/swayosd-client --output-volume raise --max-volume 100";
};
};
volume-down = {
keys = "XF86AudioLowerVolume";
repeating = true;
action = {
kind = "exec";
command = "${pkgs.swayosd}/bin/swayosd-client --output-volume lower";
};
};
brightness-up = {
keys = "XF86MonBrightnessUp";
repeating = true;
action = {
kind = "exec";
command = "${pkgs.swayosd}/bin/swayosd-client --brightness raise";
};
};
brightness-down = {
keys = "XF86MonBrightnessDown";
repeating = true;
action = {
kind = "exec";
command = "${pkgs.swayosd}/bin/swayosd-client --brightness lower";
};
};
mute = {
keys = "XF86AudioMute";
locked = true;
action = {
kind = "exec";
command = "${pkgs.swayosd}/bin/swayosd-client --output-volume mute-toggle";
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.swayosd ];
# The udev rule chgrps each backlight's brightness node to `video` and adds

View File

@@ -2,22 +2,11 @@
config,
lib,
pkgs,
util,
...
}:
# Keybound screen recording: pick a region, then toggle a video-only capture.
let
cfg = config.modules.desktop.utilities.recording;
user = config.user.name;
slurp = "${pkgs.slurp}/bin/slurp";
wf-recorder = "${pkgs.wf-recorder}/bin/wf-recorder";
notify-send = "${pkgs.libnotify}/bin/notify-send";
pgrep = "${pkgs.procps}/bin/pgrep";
pkill = "${pkgs.procps}/bin/pkill";
date = "${pkgs.coreutils}/bin/date";
mkdir = "${pkgs.coreutils}/bin/mkdir";
xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir";
# A single key both starts and stops.
# A running wf-recorder is stopped with SIGINT so it finalises the file.
# Otherwise slurp picks a region and wf-recorder runs in the foreground until
@@ -25,30 +14,51 @@ let
# reports it saved.
# No -a means no audio is captured.
toggle = pkgs.writeShellScript "screen-record-toggle" ''
if ${pgrep} -x wf-recorder >/dev/null; then
${pkill} -INT -x wf-recorder
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
${lib.getExe' pkgs.procps "pkill"} -INT -x wf-recorder
exit 0
fi
region=$(${slurp}) || exit 0
dir="$(${xdgUserDir} VIDEOS)/Recordings"
${mkdir} -p "$dir"
file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4"
region=$(${lib.getExe pkgs.slurp}) || exit 0
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
file="$dir/recording-$(${lib.getExe' pkgs.coreutils "date"} +%Y%m%d-%H%M%S).mp4"
${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio."
${wf-recorder} -g "$region" -f "$file"
${notify-send} -a "Screen recording" "Recording saved" "$file"
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file"
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording saved" "$file"
'';
# Lit while a wf-recorder capture is running.
# Empty text collapses the widget when idle, so it shows nothing until then.
recordingStatus = pkgs.writeShellScript "waybar-recording-status" ''
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
printf '{"text":"${util.glyph "f03d"}","tooltip":"Recording","class":"recording"}\n'
else
printf '{"text":"","class":"idle"}\n'
fi
'';
in
{
options.modules.desktop.utilities.recording.enable = lib.mkEnableOption "wf-recorder screen recording";
options.modules.desktop.utilities.recording.enable =
lib.mkEnableOption "wf-recorder screen recording";
config = lib.mkIf cfg.enable {
home-manager.users.${user} = {
# $mod is defined by the compositor config these binds share.
wayland.windowManager.hyprland.settings.bind = [
"$mod SHIFT, R, exec, ${toggle}"
];
config = lib.mkIf config.modules.desktop.utilities.recording.enable {
registry.keybind.${config.networking.hostName}.recording-toggle = {
keys = "SUPER + SHIFT + R";
action = {
kind = "exec";
command = "${toggle}";
};
};
registry.waybar.${config.networking.hostName}."custom/recording" = {
order = 10;
settings = {
return-type = "json";
exec = "${recordingStatus}";
interval = 1;
};
};
};
}

View File

@@ -4,25 +4,26 @@
pkgs,
...
}:
# Keyboard-driven screenshots that open in satty for annotation, then land in
# both the clipboard and a dated file.
# Keyboard-driven screenshots copy to the clipboard before Satty opens for
# annotation. Satty copies annotated screenshots when it saves them.
let
cfg = config.modules.desktop.utilities.screenshot;
user = config.user.name;
grimblast = "${pkgs.grimblast}/bin/grimblast";
satty = "${pkgs.satty}/bin/satty";
tee = "${pkgs.coreutils}/bin/tee";
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir";
# satty's copy action also saves, so one keystroke lands the shot in both the
# clipboard and a file.
# Copy the original screenshot before Satty opens.
# Satty copies and saves annotated screenshots when the user presses Enter.
capture =
target:
pkgs.writeShellScript "screenshot-${target}" ''
dir="$(${xdgUserDir} PICTURES)/Screenshots"
mkdir -p "$dir"
${grimblast} save ${target} - \
| ${tee} >(${wl-copy}) \
| ${satty} --filename - \
--output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \
--copy-command ${wl-copy} \
@@ -32,26 +33,46 @@ let
'';
in
{
options.modules.desktop.utilities.screenshot.enable = lib.mkEnableOption "screenshots via grimblast and satty";
options.modules.desktop.utilities.screenshot.enable =
lib.mkEnableOption "screenshots via grimblast and satty";
config = lib.mkIf config.modules.desktop.utilities.screenshot.enable {
registry.keybind.${config.networking.hostName} = {
screenshot-area = {
keys = "Print";
action = {
kind = "exec";
command = "${capture "area"}";
};
};
screenshot-active = {
keys = "SHIFT + Print";
action = {
kind = "exec";
command = "${capture "active"}";
};
};
screenshot-screen = {
keys = "CTRL + Print";
action = {
kind = "exec";
command = "${capture "screen"}";
};
};
};
config = lib.mkIf cfg.enable {
home-manager.users.${user} = {
# satty is a one-shot annotation surface, so float it rather than letting
# it claim a tile in the layout.
# windowrule v3 syntax: space-separated field/value tokens, comma
# separated, with matchers under a match: prefix and effects bare.
wayland.windowManager.hyprland.settings.windowrule = [
"float 1, match:class ^(com\\.gabm\\.satty)$"
wayland.windowManager.hyprland.settings.window_rule = [
{
match.class = "^(com\\.gabm\\.satty)$";
float = true;
}
];
# Print with plain/Shift/Ctrl for region/window/full.
# Super+L is already the hjkl focus and movement bind, so screenshots take
# the Print key instead.
wayland.windowManager.hyprland.settings.bind = [
", Print, exec, ${capture "area"}"
"SHIFT, Print, exec, ${capture "active"}"
"CTRL, Print, exec, ${capture "screen"}"
];
};
};
}

View File

@@ -6,8 +6,6 @@
}:
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
let
cfg = config.modules.desktop.utilities.theming;
user = config.user.name;
# A single static Nord wallpaper, drawn as a vertical gradient across the
# Polar Night shades so it needs no committed binary and no network fetch.
@@ -16,12 +14,13 @@ let
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
"$out"
'';
user = config.user.name;
in
{
options.modules.desktop.utilities.theming.enable =
lib.mkEnableOption "Nord theming of the graphical layer via Stylix";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.utilities.theming.enable {
stylix = {
enable = true;
@@ -59,6 +58,10 @@ in
};
};
};
registry.env.${config.networking.hostName} = {
HYPRCURSOR_THEME = config.stylix.cursor.name;
HYPRCURSOR_SIZE = toString config.stylix.cursor.size;
};
home-manager.users.${user} = {
# Stylix drives the cursor through home-manager's pointer-cursor config,

View File

@@ -2,13 +2,12 @@
# Declares the XDG user directories, so a tool that queries them lands in a
# well-known folder a host can relocate from one place.
let
cfg = config.modules.desktop.utilities.userdirs;
user = config.user.name;
in
{
options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.desktop.utilities.userdirs.enable {
# Write ~/.config/user-dirs.dirs from the option defaults.
home-manager.users.${user}.xdg.userDirs.enable = true;
};

View File

@@ -1,14 +1,9 @@
{ config, lib, ... }:
let
cfg = config.modules.guest;
in
{
options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration";
options.modules.guest.enable = lib.mkEnableOption "nested guest configuration";
config = lib.mkIf cfg.enable {
system.stateVersion = "26.05";
modules.network.ssh.enable = lib.mkDefault true;
config = lib.mkIf config.modules.guest.enable {
modules.network.ssh.enable = lib.mkDefault false;
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
};
}

View File

@@ -1,42 +0,0 @@
{
config,
lib,
...
}:
let
cfg = config.modules.host;
user = config.user;
passwordSecret = "${user.name}-password";
in
{
options.modules.host.enable = lib.mkEnableOption "physical-host base configuration";
config = lib.mkIf cfg.enable {
nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ];
nix.settings.extra-trusted-public-keys = [
"nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk="
];
nix.gc.automatic = true;
nix.gc.dates = "Mon 03:15";
nix.gc.options = "--delete-older-than 30d";
nix.optimise.automatic = true;
nix.optimise.dates = [ "Thu 03:45" ];
boot.loader.systemd-boot.configurationLimit = 15;
services.xserver.xkb.layout = "us";
services.xserver.xkb.options = "caps:escape_shifted_capslock";
console.useXkbConfig = true;
sops.defaultSopsFile = ../secrets/shared.yaml;
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
sops.age.sshKeyPaths = [ ];
sops.gnupg.sshKeyPaths = [ ];
users.mutableUsers = false;
sops.secrets.${passwordSecret}.neededForUsers = true;
users.users.${user.name}.hashedPasswordFile = config.sops.secrets.${passwordSecret}.path;
};
}

View File

@@ -0,0 +1,18 @@
{
config,
lib,
pkgs,
...
}:
{
options.modules.host.cachyos.enable = lib.mkEnableOption "the CachyOS kernel";
config = lib.mkIf config.modules.host.cachyos.enable {
boot.kernelPackages = pkgs.linuxPackages_cachyos;
nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ];
nix.settings.extra-trusted-public-keys = [
"nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk="
];
};
}

View File

@@ -0,0 +1,106 @@
{
config,
lib,
pkgs,
...
}:
let
hostSecrets = ../../secrets + "/${config.networking.hostName}.yaml";
in
{
options.modules.host.enable = lib.mkEnableOption "physical host configuration";
config = lib.mkIf config.modules.host.enable {
nix.gc.automatic = true;
nix.gc.dates = "Mon 03:15";
nix.gc.options = "--delete-older-than 30d";
nix.optimise.automatic = true;
nix.optimise.dates = [ "Thu 03:45" ];
boot.loader.systemd-boot.enable = true;
boot.loader.systemd-boot.configurationLimit = 15;
boot.loader.efi.canTouchEfiVariables = true;
hardware.enableRedistributableFirmware = true;
zramSwap.enable = true;
modules.network.ssh.enable = true;
modules.network.ssh.hostKeys.sopsFile = hostSecrets;
modules.network.ssh.userKey.sopsFile = hostSecrets;
services.xserver.xkb.layout = "us";
console.useXkbConfig = true;
sops.defaultSopsFile = ../../secrets/shared.yaml;
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
sops.age.sshKeyPaths = [ ];
sops.gnupg.sshKeyPaths = [ ];
users.mutableUsers = false;
sops.secrets."${config.user.name}-password".neededForUsers = true;
users.users.${config.user.name}.hashedPasswordFile =
config.sops.secrets."${config.user.name}-password".path;
security.sudo.extraConfig = ''
Defaults env_keep += "EDITOR VISUAL"
'';
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.${config.user.name} = {
home.username = config.user.name;
home.homeDirectory = "/home/${config.user.name}";
home.stateVersion = config.system.stateVersion;
home.sessionVariables = lib.attrByPath [ config.networking.hostName ] { } config.registry.env;
home.packages = with pkgs; [
eza
unzip
wget
];
};
};
registry.shell-shortcut.${config.networking.hostName} = {
ls = {
command = "eza -alg --color=always --group-directories-first --icons=always";
abbreviation = false;
};
ll = {
command = "eza -lg --color=always --group-directories-first --icons=always";
abbreviation = false;
};
lt = {
command = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
abbreviation = false;
};
".." = "cd ..";
"..." = "cd ../..";
"...." = "cd ../../..";
cp = "cp -v";
tarnow = "tar -acf ";
untar = "tar -zxvf ";
wget = "wget -c ";
grep = "grep --color=auto";
fgrep = "fgrep --color=auto";
egrep = "egrep --color=auto";
jctl = "journalctl -p 3 -xb";
update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}";
cleanup = "sudo nix-collect-garbage -d";
sops-host = {
command = "sudo --preserve-env=EDITOR,VISUAL env SOPS_AGE_KEY_FILE=/var/lib/sops-nix/key.txt nix shell nixpkgs#sops nixpkgs#neovim -c sops";
abbreviation = false;
};
};
modules.shell.fish.enable = lib.mkDefault true;
modules.shell.fish.defaultShell = lib.mkDefault true;
modules.shell.tmux.enable = lib.mkDefault true;
modules.shell.nvim.enable = lib.mkDefault true;
modules.shell.git.enable = lib.mkDefault true;
modules.shell.yazi.enable = lib.mkDefault true;
};
}

View File

@@ -0,0 +1,57 @@
{
config,
lib,
...
}:
# Imports existing ZFS pools that hold durable service state.
let
cfg = config.modules.host.zfs; # NOLINT(let_order)
in
{
options.modules.host.zfs = {
enable = lib.mkEnableOption "ZFS pool imports for durable service state";
pools = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.path);
default = { };
example = lib.literalExpression ''
{
tank = {
media = "/srv/media";
downloads = "/srv/downloads";
};
}
'';
description = ''
Lists pools that NixOS imports at boot.
Maps each dataset name to its mount point.
NixOS does not create or destroy these pools during rebuilds.
An empty map imports the pool without declaring file systems.
'';
};
};
config = lib.mkIf cfg.enable {
boot.supportedFilesystems = [ "zfs" ];
# This module derives a stable host ID from the configured hostname.
networking.hostId = builtins.substring 0 8 (
builtins.hashString "sha256" config.networking.hostName
);
boot.zfs.extraPools = lib.attrNames cfg.pools;
fileSystems = lib.mkMerge (
lib.mapAttrsToList (
pool: mounts:
lib.mapAttrs' (
dataset: mountpoint:
lib.nameValuePair mountpoint {
device = "${pool}/${dataset}";
fsType = "zfs";
}
) mounts
) cfg.pools
);
};
}

View File

@@ -4,14 +4,12 @@
...
}:
# Tools for developing this monorepo.
let
cfg = config.modules.monorepo;
in
{
options.modules.monorepo.enable = lib.mkEnableOption "tools for developing this monorepo";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.monorepo.enable {
modules.shell.bazel.enable = true;
modules.shell.python.enable = true;
modules.shell.rust.enable = true;
};
}

View File

@@ -0,0 +1,57 @@
{
config,
lib,
pkgs,
util,
...
}:
# NetworkManager-managed networking.
let
networkStatus = pkgs.writeShellScript "waybar-network-status" ''
active=$(
${lib.getExe' pkgs.networkmanager "nmcli"} -t -f DEVICE,TYPE,STATE,CONNECTION device status \
| ${lib.getExe' pkgs.gawk "awk"} -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
)
if [ -z "$active" ]; then
${lib.getExe pkgs.jq} -cn \
--arg text "${util.glyph "f127"} off" \
--arg tooltip "Network disconnected" \
--arg class "disconnected" \
'{ text: $text, tooltip: $tooltip, class: $class }'
exit 0
fi
IFS=: read -r device type _state connection <<< "$active"
case "$type" in
wifi) icon="${util.glyph "f1eb"}" ;;
ethernet) icon="LAN" ;;
*) icon="NET" ;;
esac
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$device" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
${lib.getExe pkgs.jq} -cn \
--arg text "$icon $connection" \
--arg tooltip "$device: ''${ipaddr:-no IPv4}" \
--arg class "$type" \
'{ text: $text, tooltip: $tooltip, class: $class }'
'';
in
{
options.modules.network.enable = lib.mkEnableOption "NetworkManager-managed networking";
config = lib.mkIf config.modules.network.enable {
networking.networkmanager.enable = true;
registry.waybar.${config.networking.hostName}."custom/network" = {
order = 60;
settings = {
return-type = "json";
exec = "${networkStatus}";
interval = 5;
};
};
};
}

View File

@@ -4,343 +4,207 @@
registry,
...
}:
# SSH on this machine, in both directions.
# Configures inbound and outbound SSH access.
let
cfg = config.modules.network.ssh;
user = config.user.name;
user = config.user.name; # NOLINT(let_order)
inherit (lib)
concatLists
concatStringsSep
elem
filter
genAttrs
hasAttr
imap0
listToAttrs
mapAttrs
mapAttrsToList
mkIf
mkMerge
mkOption
nameValuePair
optional
optionalAttrs
types
unique
;
# NOLINT(let_order)
workstationNames = [
"neogaia"
"zeus"
];
serverNames = [ "pikachu" ];
hostKeySecret = type: "ssh-host-${type}-key";
userKeySecret = "ssh-user-ed25519-key";
isWorkstation = lib.elem config.networking.hostName workstationNames; # NOLINT(let_order)
isServer = lib.elem config.networking.hostName serverNames; # NOLINT(let_order)
targetType = types.submodule (
{ name, ... }:
{
options = {
hostName = mkOption {
type = types.str;
default = name;
description = ''
The network address OpenSSH connects to for this target.
'';
};
# NOLINT(let_order)
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
hostKeysFor =
host:
map builtins.readFile [
(../../hosts + "/${host}/ssh_host_ed25519_key.pub")
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
];
user = mkOption {
type = types.str;
default = config.user.name;
description = ''
The remote login name OpenSSH uses for this target.
'';
};
# NOLINT(let_order)
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
hostName = registry.address.${name}.self;
clientKey = clientKeyFor name;
hostKeys = hostKeysFor name;
});
port = mkOption {
type = types.port;
default = 22;
description = ''
The TCP port OpenSSH uses for this target.
'';
};
extraTargets = {
# TODO: Pin Gitea's host key after migrating it from Proxmox.
gitea = {
hostName = "git.alexion.dev";
port = 2022;
user = "gitea";
aliases = [
"gitea"
"git.alexion.dev"
];
};
};
aliases = mkOption {
type = types.listOf types.str;
default = [ name ];
description = ''
Host patterns written into the generated OpenSSH client block.
'';
};
clientKey = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The public key this target offers when it connects outward.
Other machines admit this key according to the host groups below.
'';
};
hostKeys = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
The public keys this target presents when it accepts inbound SSH.
These keys generate system-wide known-host entries.
'';
};
};
}
);
keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key));
targetNamesIn = names: filter (name: hasAttr name cfg.targets) names;
workstationNames = targetNamesIn cfg.hosts.workstations;
serverNames = targetNamesIn cfg.hosts.servers;
clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names);
currentHost = config.networking.hostName;
isServer = elem currentHost cfg.hosts.servers;
isWorkstation = elem currentHost cfg.hosts.workstations;
# NOLINT(let_order)
mkTarget = name: target: {
hostName = target.hostName or name;
user = target.user or user;
port = target.port or 22;
aliases = target.aliases or [ name ];
clientKey = target.clientKey or null;
hostKeys = target.hostKeys or [ ];
};
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets); # NOLINT(let_order)
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names); # NOLINT(let_order)
# NOLINT(let_order)
defaultAuthorizedKeys = lib.flatten (
clientKeysFor workstationNames
++ optional isServer (clientKeysFor serverNames)
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
);
outboundTargetNames =
outboundTargetNames = # NOLINT(let_order)
let
groupTargets =
if isServer then
[ "gitea" ] ++ cfg.hosts.servers
builtins.attrNames extraTargets ++ serverNames
else if isWorkstation then
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
builtins.attrNames extraTargets ++ serverNames ++ workstationNames
else
[ "gitea" ];
builtins.attrNames extraTargets;
in
filter (name: name != currentHost) (targetNamesIn groupTargets);
lib.filter (name: name != config.networking.hostName) groupTargets;
sshSettingsFor = name:
let
target = cfg.targets.${name};
in
{
header = "Host ${concatStringsSep " " target.aliases}";
HostName = target.hostName;
User = target.user;
}
// optionalAttrs (target.port != 22) { Port = target.port; };
knownHostNamesFor = target:
let
names = unique (target.aliases ++ [ target.hostName ]);
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
in
map withPort names;
knownHosts = listToAttrs (
concatLists (
mapAttrsToList (
knownHosts = lib.listToAttrs (
lib.concatLists (
lib.mapAttrsToList (
targetName: target:
imap0 (i: key:
nameValuePair "${targetName}-${toString i}" {
lib.imap0 (
i: key:
lib.nameValuePair "${targetName}-${toString i}" {
hostNames = knownHostNamesFor target;
publicKey = keyWithoutComment key;
}
) target.hostKeys
) cfg.targets
) targets
)
);
knownHostNamesFor =
target:
let
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
names = lib.unique (target.aliases ++ [ target.hostName ]);
in
map withPort names;
keyWithoutComment = key: lib.concatStringsSep " " (lib.take 2 (lib.splitString " " key));
sshSettingsFor =
name:
let
target = targets.${name};
in
{
header = "Host ${lib.concatStringsSep " " target.aliases}";
HostName = target.hostName;
User = target.user;
}
// lib.optionalAttrs (target.port != 22) { Port = target.port; };
hostKeySecret = type: "ssh-host-${type}-key";
hostKeyTypes = [
"ed25519"
"rsa"
];
userKeySecret = "ssh-user-ed25519-key";
in
{
options.modules.network.ssh = {
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
enable = lib.mkEnableOption "the OpenSSH daemon";
hosts = {
servers = mkOption {
type = types.listOf types.str;
default = [ "pikachu" ];
description = ''
Hosts that serve durable services.
They admit workstation keys and server keys, and they receive aliases for other servers and the forge.
'';
};
workstations = mkOption {
type = types.listOf types.str;
default = [
"neogaia"
"zeus"
];
description = ''
Hosts the operator works from.
Their keys are admitted by every host, and they receive aliases for the whole fleet and the forge.
'';
};
};
targets = mkOption {
type = types.attrsOf targetType;
default = {
gitea = {
hostName = "git.alexion.dev";
port = 2022;
user = "gitea";
aliases = [
"gitea"
"git.alexion.dev"
];
};
neogaia = {
hostName = registry.address.neogaia.self;
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia";
hostKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJS+wp7K123+4BT6G4f954R6WyrbWveY7VlpoBUf6I5p neogaia"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCo2wWUKxyAS4J5TqbWf8glDhJvS5XmdRqFhMeJwG3pOB+4AccZ1T8LU7ZN+RjtRi3j2qXBJvIHuzhtQNtmT59TxocvfobYiqOgJpvVO5K6yD8ZoUJs6ziDkIduI9w9mdRIESoi+dBbVu8n24r61cKDVh+jWX+yjzkOcWcOzqDyQhhkjqblZ1WMAdujEMuEPvif1i2LCxStUaZqRGcx09m/ME2fYcaJrpuxxxvX2+CPJNicoo6Rx9i7ZjAoNuvH+jui4KT62DzlQtQtCl2CFUOM0gCPSa+MbNQ9elfHPvGzEcwOIMo2cuy9KURUkQu+sAgaG8S1PEniDDTecskHtuRdmPZawnQGpIhzo919Q6wUgjT8scK4mmSXRWmGmkMt0GNA2tfj5tDks6r5Q8XsYqtWs4rsOEvfmxVSdM771w+fqDBAil99Jsh0ksPK9+Bwgg8cMDzLLFDn8JA5y2G1HocMMom+u5DYKwPXEKnCILkasB8y24+O3PhSu1EuWw277w6EUEXvU03rCf0Ak/ULjxp9a00EGlloEwSmFI7Aub9XHDr87IdbGInEn+PMqyBYADiN+3h6nE2JO+nMa6i/CHdebmT+T7YJvuTKHD9sjFmQsYaghlq03DZrhHcm4hgUvE1dqGojHrhk/WgA3EWTWtK/+BP0Vy2jXaaz+qAx+EGnhQ== neogaia"
];
};
zeus = {
hostName = registry.address.zeus.self;
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBcHHuubptXCdvED/p2dR3MpjGN/AimqLasLY5984G alexion@zeus";
hostKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ4GBPt966LR4LpmXgiMMkepIqNIlXTUBh4zdNeek/lN root@zeus"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC3xOxGAJCG9PE0FVufgJX9VRO64IUUlRGHSOfYfAI4Mit46xKsr7z/OvqPPE9z8rOIcVPbLkc/mkMuHbDgolleunAFCIeuIIYpkL4o4/TZECh9BtN2WdPhjcD7S0rV+rR7p24Qy5bTCSwZmsQq8QxHTVPjjBngtPGRSpKVrZAuHCcdCJ13hBWAvnaav5KysKC89nMj/7FIh02v8kipGfq5LGCZ20Qp2lmv2DS24NhBPXzcw8pLN47I5GYVkZh/0RWOByteo3eXtKCF+JCwfUPCTP+zHGD+1tbO8//B2fkt3Pe9lUrn5eM2ZnX1emXSWfQng3cMqkftzWqvBwv0sSDU+9YV6H2Ury+GrrfukpBtQVe9PQcBWqbRPhxuagncR+af/tx0ZohZS0t74m3WgLtZ4r2mpTZS4oBmRsfvWzMFGSgwtva+3Y+NsLjPPM1Cz5Dn05JJlidzcABfsUg2H23vkmO2otaQI01dUni+szlRn4w6sVyxNcvY2fQGjmt9k7jmVI5D2A3yl+OLJguAAlw3ia335X1tNUG8FoI/VyZNhOckIihmbTtihNIg37Ro4yiuPRhnUWFvg2hcBCa9hzNP7XDUPnoCRQyJ4adQHQA/x4Kh+HJ6pZqtGcdqU80fzmWtrLkWG6v5/NRKs6soYDup7oQIKbJo9TtXvqtzmVmovQ== root@zeus"
];
};
pikachu = {
hostName = registry.address.pikachu.self;
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu";
hostKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKljRf4pJO+pqEqjpPz08gOYq3g1PpxvE66xVw7uMEnA root@pikachu"
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCy/riwm7dflA3mT+3a0/2CIoS2LbAsK/vn35kOoNeuzn0yhiF+imexP6tkB3S2t+H5ybRzkbbuNZcynFfeCqthFc8kvbdCnt8Diqoeg96fZ6ecvh5QE5yH9op8534EySetZ/exakFLnF+6EiWMuWUW3DFwsc2kcgDJObqSE8gTx/d7JK953MiTFmSJBFyg1RtQ3ZnMT+iCrvY2dyCLQai7VeF8koVKF2c0leAq2Hc75rb/L9md8MoJa64iPiz7hwTCin3xoFyaY/5hNVvyqFd5PivgR69gLdJkuVsUYO2mJzhur8cYmJD+pGjJ0U45hyE9TMrCFjeJHHuvSt3+2kph62wv95jLNk0WmMlwgyunISxENCSVVtNYdBMXhUh8VhEAW17QpVUg9EnPvxOdTKEjrvfOZYASWUa51JKbgBgexVgFbxdjDZR88DZa31AVBts/cx/59gXTUahFXMYLdZgssx+5uibZQWnvCyfUV9WLbfmK1lgL6hzReg1VkQ87iGr6skjtQYemJxRaFNA1+Q5f3kmG3KncuK/594a3qXYP4gC6A2blf8om1YZ4aXXh6f+GFKLjoEw1vvM2rJ+rjzfymwDX+pxVQ9L13OEtVZc9Ez76pOkbm1hqdbL0gY45+0cpxodhWV0wMQJBDXL1MHP8qcs+/vw0GxVK5l1SnWBGlw== root@pikachu"
];
};
};
description = ''
SSH targets known to the fleet.
The inventory holds connection details plus public keys used for authorization and host verification.
'';
};
extraAuthorizedKeys = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Additional client public keys admitted by this host.
'';
};
authorizedKeys = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
description = ''
Complete override for client public keys admitted by this host.
Leave null to derive access from `modules.network.ssh.hosts` and `modules.network.ssh.targets`.
'';
};
extraSettings = mkOption {
type = types.attrsOf types.anything;
default = { };
description = ''
Additional OpenSSH client settings merged into the generated Home Manager configuration.
'';
};
hostKeys.restore = mkOption {
type = types.bool;
hostKeys.restore = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Restore the host keys from secrets rather than letting the daemon generate its own.
A machine with its own identity keeps its fingerprint across a reimage by restoring committed keys.
A guest carries no host identity, so it turns this off and presents a self-generated key instead.
Restores host keys from secrets.
Preserves the fingerprint across reimages.
Guests disable this option and generate their own host keys.
'';
};
hostKeys.sopsFile = mkOption {
type = types.nullOr types.path;
hostKeys.sopsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Encrypted file holding this host's SSH host private keys, one entry per key type, named `ssh-host-<type>-key`.
Required when `restore` is on.
Sets encrypted file that contains host private keys.
Names each secret `ssh-host-<type>-key`.
Requires a value when `restore` is enabled.
'';
};
hostKeys.types = mkOption {
type = types.listOf types.str;
default = [
"ed25519"
"rsa"
];
description = ''
Key types to restore, naming both the entries read from the encrypted file and the algorithms the daemon offers.
'';
};
userKey.sopsFile = mkOption {
type = types.nullOr types.path;
userKey.sopsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Encrypted file holding this machine's SSH client private key, under the entry `ssh-user-ed25519-key`.
Left unset on a machine that authenticates to no remote server, such as a guest.
Sets encrypted file that contains the SSH client private key.
Names the secret `ssh-user-ed25519-key`.
Guests leave this unset because they do not authenticate to remote servers.
'';
};
};
config = mkIf cfg.enable (
mkMerge [
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.openssh.enable = true;
users.users.${user}.openssh.authorizedKeys.keys =
if cfg.authorizedKeys != null then
cfg.authorizedKeys
else
defaultAuthorizedKeys ++ cfg.extraAuthorizedKeys;
users.users.${user}.openssh.authorizedKeys.keys = defaultAuthorizedKeys;
programs.ssh.knownHosts = knownHosts;
home-manager.users.${user}.programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings =
mapAttrs (name: _: sshSettingsFor name) (genAttrs outboundTargetNames (name: name))
// cfg.extraSettings;
settings = lib.mapAttrs (name: _: sshSettingsFor name) (
lib.genAttrs outboundTargetNames (name: name)
);
};
}
(mkIf cfg.hostKeys.restore {
(lib.mkIf cfg.hostKeys.restore {
assertions = [
{
assertion = cfg.hostKeys.sopsFile != null;
message = "modules.network.ssh.hostKeys.restore requires modules.network.ssh.hostKeys.sopsFile to name the encrypted host keys.";
message = "Set `modules.network.ssh.hostKeys.sopsFile` when host-key restoration is enabled.";
}
];
sops.secrets = genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
sops.secrets = lib.genAttrs (map hostKeySecret hostKeyTypes) (_: {
inherit (cfg.hostKeys) sopsFile;
mode = "0400";
restartUnits = [ "sshd.service" ];
});
services.openssh.hostKeys = [ ];
services.openssh.extraConfig = concatStringsSep "" (
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") cfg.hostKeys.types
services.openssh.extraConfig = lib.concatStringsSep "" (
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") hostKeyTypes
);
})
(mkIf (cfg.userKey.sopsFile != null) {
(lib.mkIf (cfg.userKey.sopsFile != null) {
sops.secrets.${userKeySecret} = {
inherit (cfg.userKey) sopsFile;
mode = "0400";
owner = user;
};
home-manager.users.${user}.programs.ssh.settings."*".IdentityFile =
config.sops.secrets.${userKeySecret}.path;
home-manager.users.${user}.programs.ssh.settings."*" = {
IdentityFile = config.sops.secrets.${userKeySecret}.path;
IdentitiesOnly = true;
};
})
]
);

View File

@@ -4,24 +4,17 @@
util,
...
}:
# The host networking foundation: per-VLAN bridges over a tagged trunk.
# Configures bridges for tagged VLANs on a physical interface that uses DHCP.
let
cfg = config.modules.network.vlans;
inherit (util) bridgeName;
vlanName = id: "${cfg.trunk}.${toString id}";
cfg = config.modules.network.vlans; # NOLINT(let_order)
in
{
options.modules.network.vlans = {
enable = lib.mkEnableOption "host VLAN bridges over a tagged trunk";
trunk = lib.mkOption {
interface = lib.mkOption {
type = lib.types.str;
example = "enp1s0";
description = ''
Physical interface that carries 802.1Q-tagged traffic for every VLAN.
Each declared VLAN attaches to its own bridge.
Names the physical interface that carries untagged DHCP traffic and tagged VLANs.
'';
};
@@ -34,66 +27,37 @@ in
30
];
description = ''
Tagged VLAN ids to materialize.
Each id N creates one bridge named `br-vlanN`.
Creates bridges for the specified tagged VLAN IDs.
VLAN ID N creates bridge `br-vlanN`.
'';
};
management = {
vlan = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 1 4094);
default = null;
description = ''
VLAN whose bridge carries the host's own management address.
The value must be one of `ids`.
Left null, the host takes no address on any bridge.
'';
};
address = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "10.0.10.2/24";
description = ''
Host static management address in CIDR form.
Left null, the host takes its management address by DHCP.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.ids;
message = "modules.network.vlans.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.ids.";
}
{
assertion = cfg.management.address == null || cfg.management.vlan != null;
message = "modules.network.vlans.management.address needs modules.network.vlans.management.vlan to say which bridge carries it.";
}
];
# networkd owns these interfaces so NetworkManager does not contend for them.
networking.networkmanager.unmanaged =
[ "interface-name:${cfg.trunk}" ]
++ map (id: "interface-name:${bridgeName id}") cfg.ids
++ map (id: "interface-name:${vlanName id}") cfg.ids;
config = lib.mkIf (cfg.ids != [ ]) {
networking = {
useDHCP = false;
networkmanager.unmanaged = [
"interface-name:${cfg.interface}"
]
++ map (id: "interface-name:${util.bridgeName id}") cfg.ids
++ map (id: "interface-name:vlan${toString id}") cfg.ids;
};
systemd.network = {
enable = true;
netdevs = lib.mkMerge (
map (id: {
"40-${bridgeName id}" = {
"40-${util.bridgeName id}" = {
netdevConfig = {
Name = bridgeName id;
Name = util.bridgeName id;
Kind = "bridge";
};
};
"40-${vlanName id}" = {
"40-vlan${toString id}" = {
netdevConfig = {
Name = vlanName id;
Name = "vlan${toString id}";
Kind = "vlan";
};
vlanConfig.Id = id;
@@ -103,37 +67,25 @@ in
networks = lib.mkMerge (
[
# The trunk carries only tagged frames and takes no address.
{
"30-${cfg.trunk}" = {
matchConfig.Name = cfg.trunk;
networkConfig.LinkLocalAddressing = "no";
linkConfig.RequiredForOnline = "no";
vlan = map vlanName cfg.ids;
"10-${cfg.interface}" = {
matchConfig.Name = cfg.interface;
networkConfig.DHCP = "yes";
vlan = map (id: "vlan${toString id}") cfg.ids;
linkConfig.RequiredForOnline = "routable";
};
}
]
++ map (id: {
# Enslaved to its bridge, carrying no address itself.
"40-${vlanName id}" = {
matchConfig.Name = vlanName id;
networkConfig.Bridge = bridgeName id;
"40-vlan${toString id}" = {
matchConfig.Name = "vlan${toString id}";
networkConfig.Bridge = util.bridgeName id;
linkConfig.RequiredForOnline = "no";
};
"40-${util.bridgeName id}" = {
matchConfig.Name = util.bridgeName id;
linkConfig.RequiredForOnline = "no";
};
# The management bridge carries the host address.
# Other bridges provide L2 segments for guests.
"40-${bridgeName id}" = lib.mkMerge [
{
matchConfig.Name = bridgeName id;
linkConfig.RequiredForOnline = "no";
}
(lib.mkIf (cfg.management.vlan == id) (
if cfg.management.address == null then
{ networkConfig.DHCP = "yes"; }
else
{ address = [ cfg.management.address ]; }
))
];
}) cfg.ids
);
};

View File

@@ -0,0 +1,109 @@
{
config,
lib,
pkgs,
util,
...
}:
let
cfg = config.modules.network.vpn-client; # NOLINT(let_order)
vpnIcon = util.glyph [
"db83"
"dc9d"
];
vpnStatus = pkgs.writeShellScript "waybar-vpn-status" ''
interface=${lib.escapeShellArg cfg.interface}
if ! ${lib.getExe' pkgs.iproute2 "ip"} link show dev "$interface" >/dev/null 2>&1; then
printf '{"text":"","class":"idle"}\n'
exit 0
fi
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$interface" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
${lib.getExe pkgs.jq} -cn \
--arg text "${vpnIcon}" \
--arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \
--arg class "active" \
'{ text: $text, tooltip: $tooltip, class: $class }'
'';
in
{
options.modules.network.vpn-client = {
enable = lib.mkEnableOption "a WireGuard VPN client";
interface = lib.mkOption {
type = lib.types.str;
default = "wg-home";
description = "Names the WireGuard interface.";
};
address = lib.mkOption {
type = lib.types.str;
example = "10.23.100.3/32";
description = "Sets the client tunnel address in CIDR notation.";
};
sopsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Names the encrypted file that contains the WireGuard private key.";
};
peer = {
publicKey = lib.mkOption {
type = lib.types.str;
description = "Sets the WireGuard public key for the remote peer.";
};
endpoint = lib.mkOption {
type = lib.types.str;
example = "203.0.113.10:51820";
description = "Sets the WireGuard endpoint for the remote peer.";
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.sopsFile != null;
message = "Set modules.network.vpn-client.sopsFile when the VPN client is enabled.";
}
];
sops.secrets.wireguard-home-private-key.sopsFile = cfg.sopsFile;
registry.shell-shortcut.${config.networking.hostName} = {
vpn-on = "sudo systemctl start wireguard-${cfg.interface}.target";
vpn-off = "sudo systemctl stop 'wireguard-${cfg.interface}*.service' wireguard-${cfg.interface}.target";
};
registry.waybar.${config.networking.hostName}."custom/vpn" = {
order = 50;
settings = {
return-type = "json";
exec = "${vpnStatus}";
interval = 5;
};
};
systemd.targets."wireguard-${cfg.interface}".wantedBy = lib.mkForce [ ];
networking.wireguard.interfaces.${cfg.interface} = {
ips = [ cfg.address ];
mtu = 1280;
privateKeyFile = config.sops.secrets.wireguard-home-private-key.path;
peers = [
{
inherit (cfg.peer) publicKey endpoint;
allowedIPs = [ "10.23.0.0/16" ];
persistentKeepalive = 25;
}
];
};
};
}

View File

@@ -4,7 +4,7 @@
...
}:
let
cfg = config.modules.services.actualbudget;
cfg = config.modules.services.actualbudget; # NOLINT(let_order)
in
{
options.modules.services.actualbudget = {
@@ -45,14 +45,14 @@ in
users.users.${cfg.user} = {
isSystemUser = true;
uid = config.registry."stable-uid".${config.networking.hostName}.actualbudget;
group = cfg.group;
inherit (cfg) group;
};
services.actual = {
enable = true;
openFirewall = true;
user = cfg.user;
group = cfg.group;
inherit (cfg) user;
inherit (cfg) group;
settings = {
inherit (cfg) port dataDir;
};

View File

@@ -5,7 +5,7 @@
...
}:
let
cfg = config.modules.services.copyparty;
cfg = config.modules.services.copyparty; # NOLINT(let_order)
volumeType = lib.types.submodule {
options = {
@@ -23,6 +23,12 @@ let
description = "Copyparty access map from permission string to account names.";
};
readOnly = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Serve this volume read-only.";
};
flags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
@@ -32,37 +38,6 @@ let
};
};
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
hasFlag = name: flags: lib.any (flag: lib.hasPrefix "${name}:" flag || lib.hasPrefix "${name}=" flag) flags;
defaultPermissionFlags = flags:
lib.optionals (!(hasFlag "chmod_d" flags)) [ "chmod_d: 2770" ]
++ lib.optionals (!(hasFlag "chmod_f" flags)) [ "chmod_f: 660" ];
renderFlags = flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") (flags ++ defaultPermissionFlags flags);
renderVolume = urlPath: volume: ''
[${urlPath}]
${volume.path}
accs:
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)}
${lib.optionalString (volume.flags != [ ]) ''
flags:
${renderFlags volume.flags}
''}
'';
renderTrustedSource = source: " xff-src: ${source}";
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
rproxy: 1
xff-hdr: x-forwarded-for
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
'';
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
configScript = pkgs.writeShellScript "copyparty-config" ''
set -eu
@@ -88,6 +63,40 @@ let
${renderedVolumes}
EOF
'';
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
hasFlag =
name: flags: lib.any (flag: lib.hasPrefix "${name}:" flag || lib.hasPrefix "${name}=" flag) flags;
defaultPermissionFlags =
flags:
lib.optionals (!(hasFlag "chmod_d" flags)) [ "chmod_d: 2770" ]
++ lib.optionals (!(hasFlag "chmod_f" flags)) [ "chmod_f: 660" ];
renderFlags =
flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") (flags ++ defaultPermissionFlags flags);
renderVolume = urlPath: volume: ''
[${urlPath}]
${volume.path}
accs:
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)}
${lib.optionalString (volume.flags != [ ]) ''
flags:
${renderFlags volume.flags}
''}
'';
renderTrustedSource = source: " xff-src: ${source}";
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
rproxy: 1
xff-hdr: x-forwarded-for
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
'';
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
in
{
options.modules.services.copyparty = {
@@ -167,7 +176,7 @@ in
users.users.${cfg.user} = {
isSystemUser = true;
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
group = cfg.group;
inherit (cfg) group;
home = cfg.stateDir;
};
@@ -196,7 +205,10 @@ in
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.stateDir ] ++ map (volume: volume.path) (lib.attrValues cfg.volumes);
ReadWritePaths = [
cfg.stateDir
]
++ map (volume: volume.path) (lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes));
};
};

View File

@@ -0,0 +1,154 @@
{
config,
lib,
pkgs,
...
}:
{
options.modules.services.games.palworld = {
enable = lib.mkEnableOption "Palworld dedicated server";
steamDir = lib.mkOption {
type = lib.types.str;
example = "/data/steamcmd";
description = "Directory where SteamCMD stores metadata and cache files.";
};
installDir = lib.mkOption {
type = lib.types.str;
example = "/data/servers/palworld/tracion";
description = "Directory where SteamCMD installs the Palworld server files.";
};
stateDir = lib.mkOption {
type = lib.types.str;
example = "/data/state/palworld/tracion";
description = "Directory where Palworld stores durable server state.";
};
serverName = lib.mkOption {
type = lib.types.str;
default = "palworld";
description = "Name shown for the Palworld server.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8211;
description = "UDP port for the Palworld server.";
};
publicIP = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Public IP address advertised by the Palworld server.";
};
maxPlayers = lib.mkOption {
type = lib.types.ints.between 1 32;
default = 32;
description = "Maximum player count for the Palworld server.";
};
publicLobby = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Advertise the server in Palworld's public lobby.";
};
autoStart = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Start the Palworld server after the guest reaches network readiness.";
};
rcon = {
enable = lib.mkEnableOption "Palworld RCON" // {
default = true;
};
port = lib.mkOption {
type = lib.types.port;
default = 25575;
description = "TCP port for Palworld RCON.";
};
};
passwordFile = lib.mkOption {
type = lib.types.path;
description = "Runtime file containing the Palworld server password.";
};
adminPasswordFile = lib.mkOption {
type = lib.types.path;
description = "Runtime file containing the Palworld admin password.";
};
};
config = lib.mkIf config.modules.services.games.palworld.enable (
let
service = config.modules.services.games.palworld;
savedDir = "${service.installDir}/Pal/Saved";
startScript = pkgs.writeShellScript "palworld-start" ''
set -eu
${pkgs.coreutils}/bin/install -d \
-m 0770 \
${lib.escapeShellArg service.stateDir} \
${lib.escapeShellArg (builtins.dirOf savedDir)}
if [ -L ${lib.escapeShellArg savedDir} ]; then
current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})"
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir}
fi
fi
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
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
else
${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
fi
fi
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
fi
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
exec ./PalServer.sh \
"$@" \
"-serverpassword=$password" \
"-adminpassword=$admin_password"
'';
in
{
modules.services.steamcmd = {
enable = true;
inherit (service) steamDir installDir stateDir;
serviceName = "palworld-${lib.toLower service.serverName}";
appId = 2394010;
inherit (service) autoStart;
executable = "${startScript}";
arguments = [
"-useperfthreads"
"-NoAsyncLoadingThread"
"-UseMultithreadForDS"
"-port=${toString service.port}"
"-publicport=${toString service.port}"
"-servername=${service.serverName}"
"-players=${toString service.maxPlayers}"
]
++ lib.optional service.publicLobby "-publiclobby"
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
++ lib.optional service.rcon.enable "-rcon";
udpPorts = [ service.port ];
tcpPorts = [ service.port ];
};
}
);
}

View File

@@ -0,0 +1,89 @@
{
config,
lib,
pkgs,
...
}:
{
options.modules.services.games.valheim = {
enable = lib.mkEnableOption "Valheim dedicated server";
steamDir = lib.mkOption {
type = lib.types.str;
example = "/data/steamcmd";
description = "Directory where SteamCMD stores metadata and cache files.";
};
installDir = lib.mkOption {
type = lib.types.str;
example = "/data/servers/valheim/main";
description = "Directory where SteamCMD installs the Valheim server files.";
};
stateDir = lib.mkOption {
type = lib.types.str;
example = "/data/state/valheim/main";
description = "Directory where Valheim stores world data.";
};
serverName = lib.mkOption {
type = lib.types.str;
default = "valheim";
description = "Name shown for the Valheim server.";
};
port = lib.mkOption {
type = lib.types.port;
default = 2456;
description = "Base UDP port for the Valheim server.";
};
passwordFile = lib.mkOption {
type = lib.types.path;
description = "Runtime file containing the Valheim server password.";
};
};
config = lib.mkIf config.modules.services.games.valheim.enable (
let
service = config.modules.services.games.valheim;
startScript = pkgs.writeShellScript "valheim-start" ''
set -eu
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
exec ./valheim_server.x86_64 "$@" -password "$password"
'';
in
{
modules.services.steamcmd = {
enable = true;
inherit (service) steamDir installDir stateDir;
serviceName = "valheim";
appId = 896660;
executable = "${startScript}";
arguments = [
"-nographics"
"-batchmode"
"-name"
service.serverName
"-port"
(toString service.port)
"-world"
service.serverName
"-savedir"
service.stateDir
"-public"
"0"
"-modifier"
"raids"
"none"
];
udpPorts = [
service.port
(service.port + 1)
];
};
}
);
}

View File

@@ -0,0 +1,227 @@
{
config,
lib,
registry,
...
}:
let
cfg = config.modules.services.homepage; # NOLINT(let_order)
derivedAllowedHosts = lib.unique (
lib.optional (selfRoute != null) selfRoute.domain
++ lib.optionals (selfAddress != null) [
selfAddress
"${selfAddress}:${toString cfg.listenPort}"
]
);
selfRoute = selfEntry.route or null;
selfEntry = registry.join cfg.hostName cfg.entryName;
selfAddress = selfEntry.address or null;
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
mkTab =
tab:
let
sections = entriesByTab.${tab};
in
{
${tab} = map (section: { ${section} = sections.${section}; }) (orderedSections tab sections);
};
entriesByTab = lib.foldl' insertEntry { } sortedEntries;
insertEntry =
acc: item:
acc
// {
${item.homepage.tab} = (acc.${item.homepage.tab} or { }) // {
${item.homepage.section} = (acc.${item.homepage.tab}.${item.homepage.section} or [ ]) ++ [
{ ${item.homepage.name} = mkServiceAttrs item; }
];
};
};
sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries;
homepageEntries = lib.concatLists (
lib.mapAttrsToList (
host: entries:
lib.mapAttrsToList (entry: homepage: {
inherit host entry homepage;
composite = registry.join host entry;
}) entries
) registry.homepage
);
orderedTabs = tabOrder ++ lib.filter (tab: !lib.elem tab tabOrder) (lib.attrNames entriesByTab);
tabOrder = [
"Apps"
"Download"
"System"
];
mkRegistryPath =
registryName: host: entry:
"registry.${registryName}.${host}.${entry}";
mkHref =
item:
if item.homepage.href != null then
item.homepage.href
else if (item.composite.route or null) != null then
if item.composite.route.protocol == "https" then
"https://${item.composite.route.domain}"
else if item.composite.route.protocol == "http" then
"http://${item.composite.route.domain}"
else
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching HTTP route entry"
else
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching route entry";
mkServiceAttrs =
item:
lib.filterAttrs (_: value: value != null) {
href = mkHref item;
inherit (item.homepage) description icon;
};
sectionOrderByTab = {
Apps = [
"Media"
"Files"
"Other"
];
Download = [
"Library Managers"
"Other Utilities"
"Download Clients"
];
System = [
"Monitoring"
"Network"
"Machines"
"Docker"
];
};
orderedSections =
tab: sections:
let
sectionOrder = sectionOrderByTab.${tab} or [ ];
in
sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
in
{
options.modules.services.homepage = {
enable = lib.mkEnableOption "Homepage Dashboard";
listenPort = lib.mkOption {
type = lib.types.port;
default = 8082;
description = "Port for Homepage to bind to.";
};
hostName = lib.mkOption {
type = lib.types.str;
description = "Host registry key that owns the Homepage guest entry.";
};
entryName = lib.mkOption {
type = lib.types.str;
default = "homepage";
description = "Registry entry name for this Homepage service.";
};
allowedHosts = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = "Host headers accepted by Homepage. Defaults to matching address and route registry entries.";
};
environmentFile = lib.mkOption {
type = lib.types.path;
default = "/run/secrets/homepage.env";
description = "Environment file passed to Homepage.";
};
};
config = lib.mkIf cfg.enable {
services.homepage-dashboard = {
enable = true;
openFirewall = true;
inherit (cfg) listenPort;
environmentFiles = [ cfg.environmentFile ];
allowedHosts = lib.concatStringsSep "," (
if cfg.allowedHosts == null then derivedAllowedHosts else cfg.allowedHosts
);
services = homepageServices;
widgets = [
{
greeting = {
text_size = "xl";
text = "tracion";
};
}
{
openmeteo = {
units = "metric";
latitude = "{{HOMEPAGE_VAR_LATITUDE}}";
longitude = "{{HOMEPAGE_VAR_LONGITUDE}}";
cache = 5;
};
}
{
datetime = {
text_size = "xl";
locale = "en";
format = {
dateStyle = "full";
timeStyle = "medium";
hour12 = false;
};
};
}
];
settings = {
title = "Tracion";
theme = "dark";
color = "slate";
showStats = false;
hideVersion = true;
bookmarksStyle = "icons";
useEqualHeights = true;
layout = {
Apps = {
style = "row";
columns = 1;
tab = "Apps";
Media = {
style = "row";
columns = 2;
};
Files = {
style = "row";
columns = 2;
};
Other = {
style = "row";
columns = 3;
iconsOnly = true;
};
};
Download = {
style = "row";
columns = 1;
tab = "Download";
};
System = {
tab = "System";
};
};
};
};
};
}

View File

@@ -0,0 +1,64 @@
{
config,
lib,
...
}:
let
cfg = config.modules.services.navidrome; # NOLINT(let_order)
in
{
options.modules.services.navidrome = {
enable = lib.mkEnableOption "Navidrome";
dataFolder = lib.mkOption {
type = lib.types.str;
default = "/var/lib/navidrome";
readOnly = true;
description = "Path for Navidrome's `DataFolder` setting.";
};
musicFolder = lib.mkOption {
type = lib.types.str;
default = "/music";
readOnly = true;
description = "Path for Navidrome's `MusicFolder` setting.";
};
environmentFile = lib.mkOption {
type = lib.types.path;
default = "/run/secrets/navidrome.env";
readOnly = true;
description = "Path to Navidrome's environment file.";
};
};
config = lib.mkMerge [
{
registry."stable-uid".${config.networking.hostName}.navidrome = 20004;
}
(lib.mkIf cfg.enable {
users.users.navidrome = {
isSystemUser = true;
uid = config.registry."stable-uid".${config.networking.hostName}.navidrome;
group = "storage";
home = cfg.dataFolder;
};
services.navidrome = {
enable = true;
user = "navidrome";
group = "storage";
openFirewall = true;
inherit (cfg) environmentFile;
settings = {
Address = "0.0.0.0";
Port = 4533;
DataFolder = cfg.dataFolder;
MusicFolder = cfg.musicFolder;
EnableSharing = true;
};
};
})
];
}

View File

@@ -5,35 +5,63 @@
...
}:
let
cfg = config.modules.services.reverse-proxy;
mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}";
mkBackend = address: route: "http://${address}:${toString route.port}";
mkVirtualHost = host: entry:
let
compositeEntry = registry.join host entry;
route = compositeEntry.route;
address = compositeEntry.address or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
in
{
inherit host entry;
name = route.domain;
value = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = mkBackend address route;
};
};
generatedVirtualHostList = lib.concatLists (
generatedUdpStreams = lib.concatLists (
lib.mapAttrsToList (
host: routes:
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry) routes
lib.concatLists (
lib.mapAttrsToList (
entry: route: lib.optionals (route.protocol == "udp") (map (mkUdpStream host entry) route.ports)
) routes
)
) registry.route
);
mkUdpStream =
host: entry: port:
let
address =
compositeEntry.address
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
compositeEntry = registry.join host entry;
in
{
inherit
host
entry
port
address
;
};
generatedStreamConfig = lib.concatMapStringsSep "\n" (stream: ''
server {
listen ${toString stream.port} udp;
proxy_pass ${stream.address}:${toString stream.port};
}
'') generatedUdpStreams;
generatedVirtualHosts =
if duplicateDomainMessages != [ ] then
throw (builtins.head duplicateDomainMessages)
else
lib.listToAttrs generatedVirtualHostList;
duplicateDomainMessages = lib.concatLists (
lib.mapAttrsToList (
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
@@ -42,34 +70,69 @@ let
}
) { } generatedVirtualHostList;
duplicateDomainMessages = lib.concatLists (
generatedVirtualHostList = generatedHttpsVirtualHostList ++ generatedHttpVirtualHostList;
generatedHttpsVirtualHostList = lib.concatLists (
lib.mapAttrsToList (
domain: virtualHosts:
if lib.length virtualHosts > 1 then
[
"duplicate route domain ${domain} in ${lib.concatMapStringsSep " and " (virtualHost: mkRegistryPath "route" virtualHost.host virtualHost.entry) virtualHosts}"
]
else
[ ]
) virtualHostsByDomain
host: routes:
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") (
lib.filterAttrs (_entry: route: route.protocol == "https") routes
)
) registry.route
);
generatedVirtualHosts =
if duplicateDomainMessages != [ ] then
throw (builtins.head duplicateDomainMessages)
else
lib.listToAttrs generatedVirtualHostList;
mkVirtualHost =
host: entry: mode:
let
isHttp = mode == "http";
address =
compositeEntry.address
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
compositeEntry = registry.join host entry;
inherit (compositeEntry) route;
inherit (route) port;
in
{
inherit host entry;
registryName = "route";
name = route.domain;
value = {
forceSSL = !isHttp;
enableACME = !isHttp;
extraConfig = lib.optionalString (route.clientMaxBodySize != null) ''
client_max_body_size ${route.clientMaxBodySize};
'';
locations."/".proxyPass = mkBackend address 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 =
registryName: host: entry:
"registry.${registryName}.${host}.${entry}";
in
{
options.modules.services.reverse-proxy = {
enable = lib.mkEnableOption "the public edge reverse proxy";
};
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.services.reverse-proxy.enable {
networking.firewall.allowedTCPPorts = [
80
443
];
networking.firewall.allowedUDPPorts = map (stream: stream.port) generatedUdpStreams;
security.acme = {
acceptTerms = true;
@@ -89,6 +152,7 @@ in
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
streamConfig = lib.mkIf (generatedUdpStreams != [ ]) generatedStreamConfig;
virtualHosts = generatedVirtualHosts;
};
};

View File

@@ -0,0 +1,190 @@
{
config,
lib,
pkgs,
...
}:
{
options.modules.services.steamcmd = {
enable = lib.mkEnableOption "SteamCMD-managed game server";
steamDir = lib.mkOption {
type = lib.types.str;
example = "/data/steamcmd";
description = "Directory where SteamCMD stores metadata and cache files.";
};
serviceName = lib.mkOption {
type = lib.types.str;
example = "valheim";
description = "Name of the systemd service that runs the game server.";
};
appId = lib.mkOption {
type = lib.types.ints.positive;
example = 896660;
description = "Steam app ID for the dedicated server package.";
};
installDir = lib.mkOption {
type = lib.types.str;
example = "/data/servers/valheim/main";
description = "Directory where SteamCMD installs the server files and where the game service starts.";
};
stateDir = lib.mkOption {
type = lib.types.str;
example = "/data/state/valheim/main";
description = "Writable directory for durable game server state.";
};
executable = lib.mkOption {
type = lib.types.str;
example = "./valheim_server.x86_64";
description = "Server executable, relative to installDir or absolute.";
};
arguments = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"-name"
"main"
];
description = "Arguments passed to the server executable.";
};
udpPorts = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ ];
description = "UDP ports opened for the game service.";
};
tcpPorts = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ ];
description = "TCP ports opened for the game service.";
};
autoStart = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Start the game service after the guest reaches network readiness.";
};
};
config = lib.mkMerge [
{
registry."stable-uid".${config.networking.hostName}.steamcmd = 20003;
}
(lib.mkIf config.modules.services.steamcmd.enable (
let
group = "storage";
service = config.modules.services.steamcmd;
user = "steamcmd";
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
set -eu
${pkgs.coreutils}/bin/install -d \
-o ${user} \
-g ${group} \
-m 0770 \
${lib.escapeShellArg service.steamDir} \
${lib.escapeShellArg service.installDir} \
${lib.escapeShellArg service.stateDir}
'';
updateScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-update" ''
set -eu
export HOME=${lib.escapeShellArg service.steamDir}
exec ${lib.getExe pkgs.steamcmd} \
+force_install_dir ${lib.escapeShellArg service.installDir} \
+login anonymous \
+app_update ${toString service.appId} validate \
+quit
'';
startScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-start" ''
set -eu
cd ${lib.escapeShellArg service.installDir}
export LD_LIBRARY_PATH=linux64:''${LD_LIBRARY_PATH-}
exec ${lib.getExe pkgs.steam-run} ${lib.escapeShellArg service.executable} ${lib.escapeShellArgs service.arguments}
'';
in
{
users.users.${user} = {
isSystemUser = true;
uid = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
inherit group;
home = service.steamDir;
};
systemd.tmpfiles.rules = [
"d ${service.steamDir} 0770 ${user} ${group} - -"
"Z ${service.steamDir} 0770 ${user} ${group} - -"
"d ${service.installDir} 0770 ${user} ${group} - -"
"Z ${service.installDir} 0770 ${user} ${group} - -"
"d ${service.stateDir} 0770 ${user} ${group} - -"
"Z ${service.stateDir} 0770 ${user} ${group} - -"
];
systemd.services = {
"steamcmd-${service.serviceName}-update" = {
description = "Update ${service.serviceName} with SteamCMD";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
User = user;
Group = group;
WorkingDirectory = "-${service.steamDir}";
ExecStartPre = "+${prepareScript}";
ExecStart = updateScript;
TimeoutStartSec = "15min";
};
};
"${service.serviceName}-autostart" = {
description = "Queue ${service.serviceName} game server startup";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.systemd}/bin/systemctl --no-block start ${service.serviceName}.service";
};
};
${service.serviceName} = {
description = "${service.serviceName} game server";
after = [
"network-online.target"
"steamcmd-${service.serviceName}-update.service"
];
wants = [
"network-online.target"
"steamcmd-${service.serviceName}-update.service"
];
serviceConfig = {
Type = "simple";
User = user;
Group = group;
WorkingDirectory = "-${service.installDir}";
ExecStartPre = "+${prepareScript}";
ExecStart = startScript;
Restart = "on-failure";
TimeoutStartSec = "15min";
};
};
};
networking.firewall.allowedUDPPorts = service.udpPorts;
networking.firewall.allowedTCPPorts = service.tcpPorts;
}
))
];
}

View File

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

View File

@@ -5,14 +5,13 @@
}:
# direnv for the primary user, configured through home-manager.
let
cfg = config.modules.shell.direnv;
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 cfg.enable {
config = lib.mkIf config.modules.shell.direnv.enable {
home-manager.users.${user}.programs.direnv = {
enable = true;
silent = true;

View File

@@ -6,14 +6,10 @@
}:
# fish for the primary user, configured through home-manager.
let
cfg = config.modules.shell.fish;
user = config.user.name;
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
envVars = lib.attrByPath [ config.networking.hostName ] { } config.registry.env-var;
cfg = config.modules.shell.fish; # NOLINT(let_order)
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
exportEnvVar = name: value: ''
set -gx ${name} ${lib.escapeShellArg value}
'';
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
user = config.user.name;
in
{
options.modules.shell.fish = {
@@ -99,21 +95,14 @@ in
};
deploy-to = {
description = "Deploy the current dotfiles branch to a host through target-local nixos-rebuild";
description = "Build the current checkout locally, import it on a host, and activate it";
body = builtins.readFile ./functions/deploy-to.fish;
};
devtree = {
description = "Manage alexion/dev git worktrees";
body = builtins.readFile ./functions/devtree.fish;
};
};
# Rendered by home-manager into ~/.config/fish/config.fish.
interactiveShellInit = lib.concatStringsSep "\n" (
(lib.mapAttrsToList exportEnvVar envVars)
++ [ (builtins.readFile ./config.fish) ]
);
interactiveShellInit = builtins.readFile ./config.fish;
};
};
};

View File

@@ -1,19 +1,9 @@
if test (count $argv) -lt 1; or test (count $argv) -gt 2
echo "usage: deploy-to <host> [branch]" >&2
if test (count $argv) -ne 1
echo "usage: deploy-to <host>" >&2
return 2
end
set host $argv[1]
set branch $argv[2]
if test -z "$branch"
set branch (git branch --show-current)
end
if test -z "$branch"
echo "deploy-to: could not determine the current branch" >&2
return 2
end
git rev-parse --show-toplevel >/dev/null 2>&1
or begin
@@ -21,33 +11,85 @@ or begin
return 2
end
set dirty_lines (git status --porcelain)
if test (count $dirty_lines) -gt 0
echo "deploy-to: refusing to deploy with a dirty local worktree" >&2
return 1
end
git fetch origin
set repository_root (git rev-parse --show-toplevel)
or return $status
set remote_ref refs/remotes/origin/$branch
git rev-parse --verify --quiet $remote_ref >/dev/null
set untracked_nix_files (git -C $repository_root ls-files --others --exclude-standard -- '*.nix' flake.nix flake.lock)
if test (count $untracked_nix_files) -gt 0
echo "deploy-to: warning: untracked Nix files are not part of Git flake evaluation" >&2
printf ' %s\n' $untracked_nix_files >&2
end
set installable "$repository_root#nixosConfigurations.$host.config.system.build.toplevel"
echo "deploy-to: building $installable" >&2
set system_path (nix build --no-link --print-out-paths $installable)
or begin
echo "deploy-to: origin/$branch does not exist" >&2
echo "deploy-to: build failed for $host" >&2
return $status
end
if test (count $system_path) -ne 1
echo "deploy-to: expected one build output, got "(count $system_path) >&2
return 1
end
set local_head (git rev-parse HEAD)
set remote_head (git rev-parse $remote_ref)
if test "$local_head" != "$remote_head"
echo "deploy-to: local HEAD differs from origin/$branch" >&2
echo "deploy-to: push or reset the branch before deploying" >&2
return 1
set closure_paths (nix-store -qR $system_path)
or begin
echo "deploy-to: closure query failed for $host" >&2
return $status
end
set escaped_branch (string escape -- $branch)
set escaped_host (string escape -- $host)
set remote_command "cd ~/dev && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs"
echo "deploy-to: checking which closure paths $host is missing" >&2
set missing_paths (printf '%s\n' $closure_paths | ssh $host 'xargs -r nix-store --check-validity --print-invalid')
or begin
echo "deploy-to: remote closure check failed for $host" >&2
return $status
end
ssh -t $host $remote_command
if test (count $missing_paths) -gt 0
set nar_file (mktemp --suffix=.nar)
or return $status
set remote_nar "/tmp/deploy-to-$host-"(date +%s)"-$fish_pid.nar"
echo "deploy-to: exporting "(count $missing_paths)" missing closure paths for $system_path" >&2
nix-store --export $missing_paths >$nar_file
or begin
set export_status $status
rm -f $nar_file
echo "deploy-to: closure export failed for $host" >&2
return $export_status
end
echo "deploy-to: copying closure archive to $host:$remote_nar" >&2
scp $nar_file $host:$remote_nar
set scp_status $status
rm -f $nar_file
if test $scp_status -ne 0
echo "deploy-to: closure archive copy failed for $host" >&2
return $scp_status
end
echo "deploy-to: importing closure on $host" >&2
ssh -t $host "sudo sh -c 'nix-store --import < $remote_nar' && rm -f $remote_nar"
or begin
echo "deploy-to: closure import failed for $host" >&2
return $status
end
else
echo "deploy-to: $host already has the full closure for $system_path" >&2
end
echo "deploy-to: recording $system_path as the system profile on $host" >&2
ssh -t $host sudo nix-env --profile /nix/var/nix/profiles/system --set $system_path
or begin
echo "deploy-to: system profile update failed for $host" >&2
return $status
end
echo "deploy-to: activating $system_path on $host" >&2
ssh -t $host sudo "$system_path/bin/switch-to-configuration" switch
or begin
echo "deploy-to: activation failed for $host" >&2
return $status
end

View File

@@ -1,273 +0,0 @@
set main_checkout ~/dev
set worktree_root ~/worktrees/dev
set base_branch main
set remote origin
function __devtree_usage --no-scope-shadowing
echo "usage: devtree <command> [args]" >&2
echo "commands:" >&2
echo " new <branch> create a branch worktree from main and cd into it" >&2
echo " cd <branch> cd into an existing worktree" >&2
echo " list list worktrees" >&2
echo " sync [<branch>] fast-forward main, then rebase branch onto main" >&2
echo " sync --all fast-forward main, then rebase clean topic worktrees onto main" >&2
echo " remove <branch> remove a clean worktree, keeping the branch" >&2
echo " prune [--dry-run] prune worktree metadata and stale merged local branches" >&2
end
function __devtree_path_for_branch --no-scope-shadowing --argument-names branch
set current_path ""
for line in (git -C $main_checkout worktree list --porcelain)
if string match -q 'worktree *' -- $line
set current_path (string replace 'worktree ' '' -- $line)
else if test "$line" = "branch refs/heads/$branch"
echo $current_path
return 0
end
end
return 1
end
function __devtree_checked_out_branches --no-scope-shadowing
for line in (git -C $main_checkout worktree list --porcelain)
if string match -q 'branch refs/heads/*' -- $line
string replace 'branch refs/heads/' '' -- $line
end
end
end
function __devtree_update_main --no-scope-shadowing
if not test -d $main_checkout/.git
echo "devtree: main checkout not found at $main_checkout" >&2
return 1
end
set dirty_lines (git -C $main_checkout status --porcelain)
if test (count $dirty_lines) -gt 0
echo "devtree: refusing to update dirty main checkout: $main_checkout" >&2
return 1
end
git -C $main_checkout fetch --prune $remote
or return $status
git -C $main_checkout switch $base_branch
or return $status
git -C $main_checkout merge --ff-only $remote/$base_branch
end
function __devtree_sync_branch --no-scope-shadowing --argument-names branch
if test "$branch" = "$base_branch"
echo "$branch: already updated"
return 0
end
set path (__devtree_path_for_branch $branch)
if test -z "$path"
echo "devtree: no worktree found for branch '$branch'" >&2
return 1
end
set dirty_lines (git -C $path status --porcelain)
if test (count $dirty_lines) -gt 0
echo "$branch: skipped: dirty ($path)"
return 0
end
echo "$branch: rebasing onto $base_branch"
git -C $path rebase $base_branch
end
if test (count $argv) -eq 0
__devtree_usage
return 2
end
set command $argv[1]
set args $argv[2..-1]
switch $command
case new
if test (count $args) -ne 1
echo "usage: devtree new <branch>" >&2
return 2
end
set branch $args[1]
set path $worktree_root/(string replace -a / __ $branch)
if git -C $main_checkout show-ref --verify --quiet refs/heads/$branch
echo "devtree: branch already exists: $branch" >&2
return 1
end
if test -e $path
echo "devtree: worktree path already exists: $path" >&2
return 1
end
__devtree_update_main
or return $status
mkdir -p $worktree_root
or return $status
git -C $main_checkout worktree add -b $branch $path $base_branch
or return $status
cd $path
case cd
if test (count $args) -ne 1
echo "usage: devtree cd <branch>" >&2
return 2
end
set path (__devtree_path_for_branch $args[1])
if test -z "$path"
echo "devtree: no worktree found for branch '$args[1]'" >&2
return 1
end
cd $path
case list
if test (count $args) -ne 0
echo "usage: devtree list" >&2
return 2
end
git -C $main_checkout worktree list
case sync
if test (count $args) -gt 1
echo "usage: devtree sync [<branch>]" >&2
echo " devtree sync --all" >&2
return 2
end
__devtree_update_main
or return $status
if test (count $args) -eq 1; and test "$args[1]" = --all
set current_path ""
set branches
for line in (git -C $main_checkout worktree list --porcelain)
if string match -q 'worktree *' -- $line
set current_path (string replace 'worktree ' '' -- $line)
else if string match -q 'branch refs/heads/*' -- $line
set branch (string replace 'branch refs/heads/' '' -- $line)
if test "$branch" != "$base_branch"
set --append branches $branch
end
end
end
for branch in $branches
__devtree_sync_branch $branch
or return $status
end
else
if test (count $args) -eq 1
set branch $args[1]
else
git rev-parse --show-toplevel >/dev/null 2>&1
or begin
echo "devtree: run 'devtree sync' from a git checkout, or pass a branch" >&2
return 2
end
set branch (git branch --show-current)
if test -z "$branch"
echo "devtree: could not determine the current branch" >&2
return 2
end
end
__devtree_sync_branch $branch
end
case remove
if test (count $args) -ne 1
echo "usage: devtree remove <branch>" >&2
return 2
end
set branch $args[1]
if test "$branch" = "$base_branch"
echo "devtree: refusing to remove $base_branch" >&2
return 1
end
set path (__devtree_path_for_branch $branch)
if test -z "$path"
echo "devtree: no worktree found for branch '$branch'" >&2
return 1
end
set dirty_lines (git -C $path status --porcelain)
if test (count $dirty_lines) -gt 0
echo "devtree: refusing to remove dirty worktree: $path" >&2
return 1
end
git -C $main_checkout worktree remove $path
case prune
set dry_run false
if test (count $args) -gt 1
echo "usage: devtree prune [--dry-run]" >&2
return 2
else if test (count $args) -eq 1
if test "$args[1]" = --dry-run
set dry_run true
else
echo "usage: devtree prune [--dry-run]" >&2
return 2
end
end
git -C $main_checkout worktree prune
or return $status
git -C $main_checkout fetch --prune $remote
or return $status
set checked_out (__devtree_checked_out_branches)
for line in (git -C $main_checkout for-each-ref --format='%(refname:short)|%(upstream:track)' refs/heads)
set parts (string split '|' -- $line)
set branch $parts[1]
set upstream_track $parts[2]
if test "$branch" = "$base_branch"
continue
end
if contains -- $branch $checked_out
continue
end
if not string match -q '*gone*' -- $upstream_track
continue
end
if not git -C $main_checkout merge-base --is-ancestor $branch $base_branch
echo "$branch: skipped: not merged into $base_branch"
continue
end
if test "$dry_run" = true
echo "$branch: would delete"
else
git -C $main_checkout branch -d $branch
or return $status
end
end
case '*'
echo "devtree: unknown command: $command" >&2
__devtree_usage
return 2
end

View File

@@ -5,14 +5,13 @@
}:
# git for the primary user, configured through home-manager.
let
cfg = config.modules.shell.git;
user = config.user.name;
in
{
options.modules.shell.git.enable =
lib.mkEnableOption "git for the primary user, carrying the operator's commit identity";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.shell.git.enable {
home-manager.users.${user}.programs.git = {
enable = true;

View File

@@ -6,14 +6,13 @@
}:
# gitea-axi for the primary user, installed through its own home-manager module.
let
cfg = config.modules.shell.gitea-axi;
user = config.user.name;
in
{
options.modules.shell.gitea-axi.enable =
lib.mkEnableOption "gitea-axi, an agent-ergonomic CLI for Gitea issues and pull requests";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.shell.gitea-axi.enable {
home-manager.sharedModules = [ inputs.gitea-axi.homeModules.default ];
home-manager.users.${user}.programs.gitea-axi.enable = true;
};

View File

@@ -6,42 +6,58 @@
}:
# Herdr, a terminal multiplexer for coding agents.
let
cfg = config.modules.shell.herdr;
user = config.user.name;
in
{
options.modules.shell.herdr.enable = lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
options.modules.shell.herdr.enable =
lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.shell.herdr.enable {
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";
home-manager.users.${user} = {
home.packages = [ pkgs.herdr ];
home-manager.users.${user} = lib.mkMerge [
{
home.packages = [ pkgs.herdr ];
xdg.configFile."herdr/config.toml".text = ''
[keys]
prefix = "ctrl+space"
detach = "prefix+d"
reload_config = "prefix+r"
new_workspace = "prefix+c"
new_tab = "prefix+shift+c"
rename_workspace = "prefix+comma"
rename_tab = "prefix+<"
split_vertical = "prefix+backslash"
split_horizontal = "prefix+minus"
switch_workspace = "prefix+1..9"
switch_tab = "prefix+shift+1..9"
focus_pane_left = "prefix+h"
focus_pane_down = "prefix+j"
focus_pane_up = "prefix+k"
focus_pane_right = "prefix+l"
xdg.configFile."herdr/config.toml".text = ''
onboarding = false
[ui]
prompt_new_tab_name = false
[keys]
prefix = "ctrl+space"
detach = "prefix+d"
reload_config = "prefix+r"
new_workspace = "prefix+c"
new_tab = "prefix+shift+c"
rename_workspace = "prefix+comma"
rename_tab = "prefix+<"
split_vertical = "prefix+backslash"
split_horizontal = "prefix+minus"
switch_workspace = "prefix+1..9"
switch_tab = "prefix+shift+1..9"
focus_pane_left = "prefix+h"
focus_pane_down = "prefix+j"
focus_pane_up = "prefix+k"
focus_pane_right = "prefix+l"
[experimental]
kitty_graphics = true
'';
};
[ui]
prompt_new_tab_name = false
[experimental]
kitty_graphics = true
'';
}
(lib.mkIf config.modules.shell.pi.enable {
home.file.".pi/agent/extensions/herdr-agent-state.ts" = {
source = "${pkgs.herdr.src}/src/integration/assets/pi/herdr-agent-state.ts";
force = true;
};
})
(lib.mkIf config.modules.shell.omp.enable {
home.file.".omp/agent/extensions/herdr-omp-agent-state.ts" = {
source = "${pkgs.herdr.src}/src/integration/assets/omp/herdr-agent-state.ts";
force = true;
};
})
];
};
}

View File

@@ -6,6 +6,31 @@ require("nord").setup({
})
vim.cmd.colorscheme("nord")
-- Keep floating windows distinct from the current line highlight.
vim.api.nvim_set_hl(0, "NormalFloat", { bg = "#2e3440" })
vim.api.nvim_set_hl(0, "FloatBorder", { fg = "#88c0d0", bg = "#2e3440" })
local open_floating_preview = vim.lsp.util.open_floating_preview
vim.lsp.util.open_floating_preview = function(contents, syntax, opts, ...)
opts = opts or {}
opts.border = opts.border or "rounded"
return open_floating_preview(contents, syntax, opts, ...)
end
-- Format Rust buffers before writing them.
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.rs",
callback = function(args)
vim.lsp.buf.format({
bufnr = args.buf,
timeout_ms = 2000,
filter = function(client)
return client.name == "rust_analyzer"
end,
})
end,
})
-- Conceal markdown syntax in markdown buffers.
-- conceallevel is window-local, so it is set with opt_local when the filetype
-- is applied to the buffer's window.

View File

@@ -7,13 +7,13 @@
}:
# Neovim for the primary user, configured declaratively through nixvim.
let
cfg = config.modules.shell.nvim;
user = config.user.name;
in
{
options.modules.shell.nvim.enable = lib.mkEnableOption "Neovim, configured declaratively via nixvim";
options.modules.shell.nvim.enable =
lib.mkEnableOption "Neovim, configured declaratively via nixvim";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.shell.nvim.enable {
registry.shell-shortcut.${config.networking.hostName} = {
vi = "nvim";
vim = "nvim";
@@ -31,6 +31,7 @@ in
ripgrep # backs the picker's live grep
fd # backs the picker's file finder
wl-clipboard # provides a non-blocking clipboard provider on Wayland
nixd # provides Nix language-server support
];
globals.mapleader = " ";
@@ -142,6 +143,56 @@ in
];
plugins = {
blink-cmp = {
enable = true;
setupLspCapabilities = true;
settings = {
keymap = {
preset = "default";
"<M-Space>" = [
"show"
"show_documentation"
"hide_documentation"
];
};
completion.documentation.auto_show = true;
signature.enabled = true;
sources.default = [
"lsp"
"path"
"snippets"
"buffer"
];
};
};
lsp = {
enable = true;
inlayHints = true;
keymaps = {
silent = true;
diagnostic = {
"[d" = "goto_prev";
"]d" = "goto_next";
};
lspBuf = {
gd = "definition";
gD = "references";
K = "hover";
"<leader>ca" = "code_action";
"<leader>rn" = "rename";
};
};
servers.rust_analyzer = lib.mkIf config.modules.shell.rust.enable {
enable = true;
installCargo = false;
installRustc = false;
installRustfmt = false;
};
servers.nixd.enable = true;
};
gitsigns = {
enable = true;
settings.current_line_blame = false;

View File

@@ -0,0 +1,19 @@
{
config,
lib,
pkgs,
...
}:
# Oh My Pi, installed alongside upstream Pi as the `omp` command.
let
user = config.user.name;
in
{
options.modules.shell.omp = {
enable = lib.mkEnableOption "Oh My Pi, a batteries-included fork of Pi";
};
config = lib.mkIf config.modules.shell.omp.enable {
home-manager.users.${user}.home.packages = [ pkgs.omp ];
};
}

View File

@@ -8,35 +8,31 @@
# home-manager, which ships the package and manages global settings.
# The login credential is left unmanaged, so it survives rebuilds.
let
cfg = config.modules.shell.pi;
user = config.user.name;
piDir = "${config.users.users.${user}.home}/.pi/agent";
patchedPi = pkgs.pi-coding-agent.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
./patches/pi-flex-spacer.patch
./patches/pi-tool-lookup-validation.patch
];
});
in
{
options.modules.shell.pi = {
enable = lib.mkEnableOption ''
Pi, a terminal coding agent, configured through home-manager'';
enable = lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
};
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.shell.pi.enable {
home-manager.users.${user} = {
programs.pi-coding-agent = {
enable = true;
package = patchedPi;
package = pkgs.pi-coding-agent.overrideAttrs {
src = ../../../../../third_party/pi;
patches = [ ];
};
settings = {
defaultProvider = "openai-codex";
defaultModel = "gpt-5.5";
defaultModel = "gpt-5.6-terra";
defaultThinkingLevel = "medium";
theme = "dark";
enableInstallTelemetry = false;
enableAnalytics = false;
terminal.clearOnShrink = true;
};
};
@@ -44,12 +40,6 @@ in
# The first declarative rollout replaces the interactive settings file.
# Login state stays in auth.json, which this module does not manage.
"${piDir}/settings.json".force = true;
"${piDir}/extensions" = {
source = ./extensions;
recursive = true;
force = true;
};
};
};
};

View File

@@ -1,287 +0,0 @@
// installed by herdr
// managed by herdr; reinstalling or updating the integration overwrites this file.
// add custom hooks/plugins beside this file instead of editing it.
// HERDR_INTEGRATION_ID=pi
// HERDR_INTEGRATION_VERSION=6
// @ts-nocheck
import net from "node:net";
const HERDR_ENV = process.env.HERDR_ENV;
const socketPath = process.env.HERDR_SOCKET_PATH;
const socketEndpoint =
process.platform === "win32" && socketPath ? `\\\\.\\pipe\\${socketPath}` : socketPath;
const paneId = process.env.HERDR_PANE_ID;
const source = "herdr:pi";
function enabled() {
return HERDR_ENV === "1" && !!socketPath && !!paneId;
}
function sendRequestAttempt(request: unknown, timeoutMs: number): Promise<boolean> {
if (!enabled()) {
return Promise.resolve(true);
}
return new Promise((resolve) => {
let done = false;
let timeout: ReturnType<typeof setTimeout> | undefined;
const finish = (delivered: boolean) => {
if (done) return;
done = true;
if (timeout) {
clearTimeout(timeout);
}
socket.destroy();
resolve(delivered);
};
const socket = net.createConnection(socketEndpoint!);
socket.on("error", () => finish(false));
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
socket.on("data", () => finish(true));
socket.on("end", () => finish(false));
timeout = setTimeout(() => finish(false), timeoutMs);
timeout.unref?.();
});
}
async function sendRequest(request: unknown): Promise<void> {
if (await sendRequestAttempt(request, 500)) {
return;
}
await sendRequestAttempt(request, 1500);
}
type AgentState = "working" | "blocked" | "idle";
type QueuedState = {
state: AgentState;
message?: string;
seq: number;
};
let reportSeq = Date.now() * 1000;
let currentAgentSessionId: string | undefined;
let currentAgentSessionPath: string | undefined;
function nextReportSeq(): number {
reportSeq += 1;
return reportSeq;
}
function updateSessionRef(ctx: any): void {
try {
const file = ctx?.sessionManager?.getSessionFile?.();
currentAgentSessionPath =
typeof file === "string" && file.startsWith("/") ? file : undefined;
} catch {
currentAgentSessionPath = undefined;
}
try {
const id = ctx?.sessionManager?.getSessionId?.();
currentAgentSessionId = typeof id === "string" && id.length > 0 ? id : undefined;
} catch {
currentAgentSessionId = undefined;
}
}
function withSessionRef(params: Record<string, unknown>): Record<string, unknown> {
if (currentAgentSessionPath) {
return { ...params, agent_session_path: currentAgentSessionPath };
}
if (currentAgentSessionId) {
return { ...params, agent_session_id: currentAgentSessionId };
}
return params;
}
function currentSessionRef(): Record<string, unknown> | undefined {
if (currentAgentSessionPath) {
return { agent_session_path: currentAgentSessionPath };
}
if (currentAgentSessionId) {
return { agent_session_id: currentAgentSessionId };
}
return undefined;
}
function reportSession(sessionStartSource?: string): Promise<void> {
const sessionRef = currentSessionRef();
if (!sessionRef) {
return Promise.resolve();
}
return sendRequest({
id: `${source}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`,
method: "pane.report_agent_session",
params: {
pane_id: paneId,
source,
agent: "pi",
seq: nextReportSeq(),
session_start_source: sessionStartSource,
...sessionRef,
},
});
}
function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise<void> {
return sendRequest({
id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
method: "pane.report_agent",
params: withSessionRef({
pane_id: paneId,
source,
agent: "pi",
state,
message,
seq,
}),
});
}
function releaseAgent(): Promise<void> {
return sendRequest({
id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`,
method: "pane.release_agent",
params: {
pane_id: paneId,
source,
agent: "pi",
seq: nextReportSeq(),
},
});
}
function shouldReleaseOnSessionShutdown(event: any): boolean {
// Pi tears down and rebinds extension runtimes for internal lifecycle actions
// such as /reload, /new, /resume, and /fork. Those do not mean the pane's
// agent process has exited, and releasing hook authority there can suppress
// legitimate reports from the replacement runtime. Only a user/process quit
// should release Herdr's full-lifecycle authority.
const reason = event?.reason;
return reason === "quit";
}
let sendInFlight = false;
let queuedState: QueuedState | undefined;
function queueState(state: AgentState, message?: string): void {
queuedState = { state, message, seq: nextReportSeq() };
if (!sendInFlight) {
void drainStateQueue();
}
}
async function drainStateQueue(): Promise<void> {
if (sendInFlight) {
return;
}
sendInFlight = true;
try {
while (queuedState) {
const next = queuedState;
queuedState = undefined;
await sendState(next.state, next.message, next.seq);
}
} finally {
sendInFlight = false;
if (queuedState) {
void drainStateQueue();
}
}
}
export default function (pi) {
if (!enabled()) {
return;
}
let agentActive = false;
let blockedCount = 0;
let blockedMessage: string | undefined;
let lastState: AgentState | undefined;
let lastMessage: string | undefined;
let rootSession = false;
function desiredState() {
if (blockedCount > 0) {
return { state: "blocked" as const, message: blockedMessage };
}
if (agentActive) {
return { state: "working" as const, message: undefined };
}
return { state: "idle" as const, message: undefined };
}
function publishState(force = false) {
const next = desiredState();
if (!force && next.state === lastState && next.message === lastMessage) {
return;
}
lastState = next.state;
lastMessage = next.message;
queueState(next.state, next.message);
}
pi.events.on("herdr:blocked", (data) => {
if (!rootSession) {
return;
}
if (!data?.active) {
blockedCount = Math.max(0, blockedCount - 1);
if (blockedCount === 0) {
blockedMessage = undefined;
}
publishState();
return;
}
blockedCount += 1;
blockedMessage = data.label;
publishState();
});
pi.on("session_start", async (event, ctx) => {
if (ctx?.hasUI !== true) {
return;
}
rootSession = true;
updateSessionRef(ctx);
await reportSession(event?.reason);
// A reload can replace this extension mid-run without emitting another agent_start.
agentActive = ctx?.isIdle?.() === false;
publishState(true);
});
pi.on("agent_start", (_event, ctx) => {
if (!rootSession) {
return;
}
updateSessionRef(ctx);
void reportSession();
agentActive = true;
publishState();
});
pi.on("agent_settled", (_event, ctx) => {
if (!rootSession || ctx?.isIdle?.() !== true) {
return;
}
agentActive = false;
publishState();
});
pi.on("session_shutdown", async (event) => {
if (!rootSession) {
return;
}
if (shouldReleaseOnSessionShutdown(event)) {
await releaseAgent();
}
});
}

View File

@@ -1,23 +0,0 @@
diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts
--- a/packages/coding-agent/src/utils/tools-manager.ts 2026-08-01 18:41:36.970496010 -0400
+++ b/packages/coding-agent/src/utils/tools-manager.ts 2026-08-01 18:41:37.028186009 -0400
@@ -74,8 +74,8 @@
function commandExists(cmd: string): boolean {
try {
const result = spawnSync(cmd, ["--version"], { stdio: "pipe" });
- // Check for ENOENT error (command not found)
- return result.error === undefined || result.error === null;
+ // Downloaded generic Linux binaries can exist but fail on NixOS before they print a version.
+ return (result.error === undefined || result.error === null) && result.status === 0;
} catch {
return false;
}
@@ -88,7 +87,7 @@
// Check our tools directory first
const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : ""));
- if (existsSync(localPath)) {
+ if (existsSync(localPath) && commandExists(localPath)) {
return localPath;
}

View File

@@ -0,0 +1,20 @@
{
config,
lib,
pkgs,
...
}:
# Python tools for the primary user's shell environment.
let
user = config.user.name;
in
{
options.modules.shell.python.enable =
lib.mkEnableOption "Python tools for the primary user's shell environment";
config = lib.mkIf config.modules.shell.python.enable {
registry.env.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
};
}

View File

@@ -6,8 +6,6 @@
}:
# Rust tools for the primary user's shell environment.
let
cfg = config.modules.shell.rust;
user = config.user.name;
toolchain = pkgs.symlinkJoin {
name = "rust-toolchain";
paths = with pkgs; [
@@ -18,12 +16,14 @@ let
rustfmt
];
};
user = config.user.name;
in
{
options.modules.shell.rust.enable = lib.mkEnableOption "Rust tools for the primary user's shell environment";
options.modules.shell.rust.enable =
lib.mkEnableOption "Rust tools for the primary user's shell environment";
config = lib.mkIf cfg.enable {
registry.env-var.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
config = lib.mkIf config.modules.shell.rust.enable {
registry.env.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
home-manager.users.${user} = {
home.packages = [

View File

@@ -6,13 +6,12 @@
# tmux for the primary user, configured through home-manager.
# Settings without a home-manager option are read from ./extra.conf.
let
cfg = config.modules.shell.tmux;
user = config.user.name;
in
{
options.modules.shell.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.shell.tmux.enable {
registry.shell-shortcut.${config.networking.hostName}.tmx = "tmux new-session -A -s";
home-manager.users.${user}.programs.tmux = {

View File

@@ -6,13 +6,12 @@
}:
# Yazi, a terminal file manager.
let
cfg = config.modules.shell.yazi;
user = config.user.name;
in
{
options.modules.shell.yazi.enable = lib.mkEnableOption "Yazi, a terminal file manager";
config = lib.mkIf cfg.enable {
config = lib.mkIf config.modules.shell.yazi.enable {
home-manager.users.${user}.programs.yazi = {
enable = true;
enableFishIntegration = true;

View File

@@ -1,72 +0,0 @@
{
config,
lib,
...
}:
# The host-level ZFS pool import: durable service state a host mounts, never rebuilds.
let
cfg = config.modules.zfs;
in
{
options.modules.zfs = {
enable = lib.mkEnableOption "importing durable ZFS pools that hold service state";
hostId = lib.mkOption {
type = lib.types.strMatching "[0-9a-f]{8}";
example = "deadbeef";
description = ''
This host's 8-hex-digit ZFS host id, written to `networking.hostId`. ZFS
stamps an imported pool with the importing host's id, so a pool still
held by another machine is refused rather than silently dual-mounted. It
must be fixed for the machine and distinct across machines that can reach
the same pool.
'';
};
pools = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.path);
default = { };
example = lib.literalExpression ''
{
tank = {
media = "/srv/media";
downloads = "/srv/downloads";
};
}
'';
description = ''
The ZFS pools to import at boot, keyed by pool name, each pool mapping a
dataset path relative to it to that dataset's mountpoint. A pool is
durable state imported as it stands, never created or destroyed by a
rebuild, so a service's data survives any rebuild or reimage. A pool
with an empty map is still imported, leaving each dataset to its own ZFS
`mountpoint` property.
'';
};
};
config = lib.mkIf cfg.enable {
# The ZFS stack in the kernel and boot, needed even where the root filesystem is another kind.
boot.supportedFilesystems = [ "zfs" ];
# ZFS refuses to import a pool without a host id to stamp its ownership onto.
networking.hostId = cfg.hostId;
# The declared pools are imported at boot, distinct from any pool backing the root filesystem.
boot.zfs.extraPools = lib.attrNames cfg.pools;
# Each declared dataset is mounted at its host path as a native ZFS filesystem.
fileSystems = lib.mkMerge (
lib.mapAttrsToList (
pool: mounts:
lib.mapAttrs' (
dataset: mountpoint:
lib.nameValuePair mountpoint {
device = "${pool}/${dataset}";
fsType = "zfs";
}
) mounts
) cfg.pools
);
};
}

View File

@@ -1,10 +1,10 @@
{ lib, ... }:
{ lib, util, ... }:
{
type = lib.types.str;
checks = [
{
assertion = lib.registry.predicates.uniqueValue (entry: entry.value);
assertion = util.registry.predicates.uniqueValue (entry: entry.value);
message = "Address registry values must be unique.";
}
];

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