Compare commits
6 Commits
main
...
b65efd8b24
| Author | SHA1 | Date | |
|---|---|---|---|
| b65efd8b24 | |||
| 34174d9424 | |||
| e49d4c7bed | |||
| 539891037b | |||
| e4ccf58296 | |||
| 0cfd7c260c |
@@ -17,6 +17,15 @@ If the target includes multiple matching file types, use all matching skills.
|
|||||||
If the target is a diff, choose skills from the changed files.
|
If the target is a diff, choose skills from the changed files.
|
||||||
Do not use a style skill when no reviewed file matches it.
|
Do not use a style skill when no reviewed file matches it.
|
||||||
|
|
||||||
|
For every reviewed `*.nix` file, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bazel run //src/natu:natu -- <reviewed-nix-paths>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass every reviewed Nix path to Natu.
|
||||||
|
Report every Natu diagnostic as an actionable finding.
|
||||||
|
|
||||||
Report only actionable findings.
|
Report only actionable findings.
|
||||||
Order findings by severity.
|
Order findings by severity.
|
||||||
Do not report preferences unless a loaded rule states them.
|
Do not report preferences unless a loaded rule states them.
|
||||||
@@ -36,6 +45,7 @@ Keep the code excerpt to one to three lines when possible.
|
|||||||
Use a longer excerpt only when the violation needs surrounding context.
|
Use a longer excerpt only when the violation needs surrounding context.
|
||||||
Use `...` to omit unrelated lines from a multi-line excerpt.
|
Use `...` to omit unrelated lines from a multi-line excerpt.
|
||||||
Use a style-skill rule citation in the form `skill::rule-file:line`.
|
Use a style-skill rule citation in the form `skill::rule-file:line`.
|
||||||
|
Cite Natu diagnostics as `natu::<rule-id>`.
|
||||||
Make the fix direct.
|
Make the fix direct.
|
||||||
Do not include alternatives when one fix best follows the cited rule.
|
Do not include alternatives when one fix best follows the cited rule.
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/agent-writing-guide
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/conventional-commits
|
|
||||||
1
.pi/skills/craft-skill
Symbolic link
1
.pi/skills/craft-skill
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/craft-skill
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/retrospect
|
|
||||||
1
.pi/skills/to-document
Symbolic link
1
.pi/skills/to-document
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/to-document
|
||||||
28
AGENTS.md
28
AGENTS.md
@@ -3,6 +3,7 @@
|
|||||||
## Commit convention
|
## Commit convention
|
||||||
|
|
||||||
- Use Conventional Commits for all commit messages.
|
- Use Conventional Commits for all commit messages.
|
||||||
|
Before writing a commit message, read `docs/conventional-commits.md`.
|
||||||
- Keep commit messages about the Git history, not the forge workflow.
|
- Keep commit messages about the Git history, not the forge workflow.
|
||||||
Include external references only when they are stable context for the change.
|
Include external references only when they are stable context for the change.
|
||||||
- When writing commit messages, NEVER auto-add your agent name as co-author.
|
- When writing commit messages, NEVER auto-add your agent name as co-author.
|
||||||
@@ -48,8 +49,6 @@
|
|||||||
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
||||||
Ask in plain prose, in your own message, instead, with no exceptions.
|
Ask in plain prose, in your own message, instead, with no exceptions.
|
||||||
This overrides any default instruction to use it.
|
This overrides any default instruction to use it.
|
||||||
- For review requests, inspect the requested material and report findings.
|
|
||||||
Edit files after the operator accepts a proposed change.
|
|
||||||
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
|
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
|
||||||
Detect auto-generated files with a layered check.
|
Detect auto-generated files with a layered check.
|
||||||
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
|
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
|
||||||
@@ -69,6 +68,20 @@
|
|||||||
after applying an accepted change.
|
after applying an accepted change.
|
||||||
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
|
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
|
||||||
|
|
||||||
|
## Corrections
|
||||||
|
|
||||||
|
- When you discover that your belief about a project fact or convention was wrong, record the correction.
|
||||||
|
Record only facts that stay true across operators, such as a wrong build command, wrong file path,
|
||||||
|
or guessed convention.
|
||||||
|
Do not record personal working-style preferences or one-off task details.
|
||||||
|
- Record the correction in the nearest project `AGENTS.md`.
|
||||||
|
Use a dedicated `## Gotchas` section.
|
||||||
|
Create that section when the file lacks one.
|
||||||
|
- If no project `AGENTS.md` exists, ask before creating one.
|
||||||
|
Append the correction immediately without asking when a project `AGENTS.md` exists.
|
||||||
|
Briefly mention the edit in your response.
|
||||||
|
If an existing entry is wrong or stale, correct or remove it the same way.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- When materializing an external specification, standard, or project-maintained convention, use the
|
- When materializing an external specification, standard, or project-maintained convention, use the
|
||||||
@@ -88,12 +101,13 @@
|
|||||||
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
||||||
`src/ariadne/Cargo.toml` unless explicitly requested.
|
`src/ariadne/Cargo.toml` unless explicitly requested.
|
||||||
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
||||||
|
- The flake does not currently define `formatter.x86_64-linux`.
|
||||||
|
Do not use `nix fmt` as a validation command until a formatter is added.
|
||||||
- The `fix` app passes its current directory to Deadnix.
|
- The `fix` app passes its current directory to Deadnix.
|
||||||
Run `nix run .#fix` from the repository root so path exclusions match.
|
Run `nix run .#fix` from the repository root so path exclusions match.
|
||||||
|
- During the Nix refactor, address each issue that comes up by adding either a Natu rule or a new Nix style rule.
|
||||||
- Git pathspec searches started in a subdirectory default to that subtree.
|
- Git pathspec searches started in a subdirectory default to that subtree.
|
||||||
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
|
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
|
||||||
- A `**/*.nix` Git pathspec does not include Nix files directly in its starting directory.
|
|
||||||
Include both `*.nix` and `**/*.nix`, or use a recursive `*.nix` pathspec, when linting every Nix file.
|
|
||||||
- Ariadne extraction must preserve the game-root season prefix for archived source.
|
- Ariadne extraction must preserve the game-root season prefix for archived source.
|
||||||
Different game installs can contain compiled scripts with the same internal path.
|
Different game installs can contain compiled scripts with the same internal path.
|
||||||
- Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data.
|
- Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data.
|
||||||
@@ -104,13 +118,7 @@
|
|||||||
- OMP does not discover project skills from `.pi/skills`.
|
- OMP does not discover project skills from `.pi/skills`.
|
||||||
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
|
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
|
||||||
- Pi is vendored under `third_party/pi` as a linear vendor import.
|
- Pi is vendored under `third_party/pi` as a linear vendor import.
|
||||||
`pkgs.alexion.pi-coding-agent` packages that source and does not use Nixpkgs Pi.
|
|
||||||
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
||||||
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
||||||
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
||||||
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
|
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
|
||||||
- The `monorepo` and `status` libraries do not have `README.md` files.
|
|
||||||
- Check root and `src/skills/.pi/skills/` for enabled symlinks before removing a monorepo skill.
|
|
||||||
- Ariadne Rust source files live directly under `src/ariadne/`. The binary entry point is `ariadne_main.rs.
|
|
||||||
- Bazel build-style rules live under `src/skills/style/build/rules/`, not `src/skills/style/rules/`.
|
|
||||||
- Bazel passes arguments after `--` to the executed binary verbatim. A scaffold can define `//path` as a destination-path syntax.
|
|
||||||
|
|||||||
154
Cargo.lock
generated
154
Cargo.lock
generated
@@ -2,6 +2,15 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -62,6 +71,12 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -93,7 +108,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -108,6 +123,41 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "countme"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "googletest"
|
||||||
|
version = "0.14.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6b5e2f2b556b7b90297a5a35c8267dd43a537923d2b329beefdba2b4ec19d94"
|
||||||
|
dependencies = [
|
||||||
|
"googletest_macro",
|
||||||
|
"num-traits",
|
||||||
|
"regex",
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "googletest_macro"
|
||||||
|
version = "0.14.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ae6abc96141edd26bf5aeec0f119c129c44de3ced09e5073711a02cb74725d0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -140,6 +190,27 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|||||||
name = "monorepo"
|
name = "monorepo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "natu"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"googletest",
|
||||||
|
"monorepo",
|
||||||
|
"rnix",
|
||||||
|
"rowan",
|
||||||
|
"status",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -164,6 +235,68 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rnix"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c163bd17372eecdf10d351c34584b7de7c1a33be4e92a32f3fb3f5a7fe3f579b"
|
||||||
|
dependencies = [
|
||||||
|
"rowan",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rowan"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
|
||||||
|
dependencies = [
|
||||||
|
"countme",
|
||||||
|
"hashbrown",
|
||||||
|
"rustc-hash",
|
||||||
|
"text-size",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -191,7 +324,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -217,6 +350,17 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.6"
|
version = "3.0.6"
|
||||||
@@ -228,6 +372,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "text-size"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ members = [
|
|||||||
"src/machoke",
|
"src/machoke",
|
||||||
"src/monorepo",
|
"src/monorepo",
|
||||||
"src/status",
|
"src/status",
|
||||||
|
"src/natu",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clap = "4"
|
clap = "4"
|
||||||
|
googletest = "0.14"
|
||||||
monorepo = { path = "src/monorepo" }
|
monorepo = { path = "src/monorepo" }
|
||||||
|
rnix = "=0.14.0"
|
||||||
|
rowan = "0.16"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
status = { path = "src/status" }
|
status = { path = "src/status" }
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
## Desktop
|
## Desktop
|
||||||
|
|
||||||
- When adding a graphical application, register its workspace icon in `registry.window-icon`.
|
- 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`.
|
||||||
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
|
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
|
||||||
Match the mapping on the window class.
|
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.
|
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.
|
- 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`.
|
Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.
|
||||||
@@ -40,8 +40,6 @@
|
|||||||
|
|
||||||
## Gotchas
|
## 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.
|
- 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.
|
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.
|
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
|
||||||
@@ -52,8 +50,8 @@
|
|||||||
directories already exist inside the read-only source.
|
directories already exist inside the read-only source.
|
||||||
For aggregate Copyparty volumes such as `/srv` with nested `/srv/inbox`, keep the guest bind mount
|
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`.
|
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,
|
- `deploy-to` builds from the source checkout and activates an imported closure on the target.
|
||||||
and activates it. Do not rely on a target-local monorepo checkout for remote deployment.
|
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,
|
- 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.
|
but rejected locally built system paths that lacked trusted signatures.
|
||||||
Use a sudo `nix-store --import` path for unsigned deploy closures.
|
Use a sudo `nix-store --import` path for unsigned deploy closures.
|
||||||
@@ -61,10 +59,3 @@
|
|||||||
Do not describe NixOS route choices as public-DNS management.
|
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.
|
- 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.
|
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.
|
|
||||||
- Evaluating a NixOS guest's inner `config.services.nginx` can force incomplete ACME values.
|
|
||||||
Evaluate the parent host's `config.system.build.toplevel` to validate an ACME-enabled guest.
|
|
||||||
- `pkgs.systemd` does not set `meta.mainProgram`. Use `lib.getExe' pkgs.systemd "systemctl"` instead of `lib.getExe pkgs.systemd`.
|
|
||||||
|
|||||||
51
configs/nixos/guests/actualbudget.nix
Normal file
51
configs/nixos/guests/actualbudget.nix
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.actualbudget;
|
||||||
|
dataDir = "/data";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.guests.actualbudget = {
|
||||||
|
dataPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/actualbudget";
|
||||||
|
description = "Host path bind-mounted as Actual Budget's data directory.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.dataPath != null;
|
||||||
|
message = "guests.actualbudget.dataPath must point at the host directory that stores Actual Budget data.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
enable = true;
|
||||||
|
inherit dataDir;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
mounts.${dataDir}.directory = cfg.dataPath;
|
||||||
|
limits = {
|
||||||
|
memory = lib.mkDefault "2G";
|
||||||
|
cpu = lib.mkDefault "200%";
|
||||||
|
tasksMax = lib.mkDefault 512;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,13 +5,57 @@
|
|||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.copyparty;
|
cfg = config.guests.copyparty;
|
||||||
|
|
||||||
|
volumeType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
hostPath = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Host path bind-mounted into the Copyparty guest for this volume.";
|
||||||
|
};
|
||||||
|
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Guest path served by this Copyparty volume.";
|
||||||
|
};
|
||||||
|
|
||||||
|
access = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||||
|
default = { };
|
||||||
|
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 = [ ];
|
||||||
|
description = "Copyparty volume flags.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.guests.copyparty = {
|
options.guests.copyparty = {
|
||||||
|
accountName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "alexion";
|
||||||
|
description = "Copyparty account name configured for password authentication.";
|
||||||
|
};
|
||||||
|
|
||||||
passwordSecret = lib.mkOption {
|
passwordSecret = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "services/copyparty/users/${config.user.name}/password";
|
default = "services/copyparty/users/alexion/password";
|
||||||
description = "SOPS secret name that contains the Copyparty account password.";
|
description = "SOPS secret name containing the Copyparty account password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/var/lib/copyparty";
|
||||||
|
description = "Guest path where Copyparty stores service state.";
|
||||||
};
|
};
|
||||||
|
|
||||||
statePath = lib.mkOption {
|
statePath = lib.mkOption {
|
||||||
@@ -28,39 +72,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf volumeType;
|
||||||
lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
hostPath = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Host path bind-mounted into the Copyparty guest for this volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Guest path served by this Copyparty volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
access = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
||||||
default = { };
|
|
||||||
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.attrs;
|
|
||||||
default = { };
|
|
||||||
description = "Copyparty volume flags.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default = { };
|
default = { };
|
||||||
description = "Copyparty volumes keyed by their URL path.";
|
description = "Copyparty volumes keyed by their URL path.";
|
||||||
};
|
};
|
||||||
@@ -90,26 +102,24 @@ in
|
|||||||
guests.copyparty = {
|
guests.copyparty = {
|
||||||
interior.modules.services.copyparty = {
|
interior.modules.services.copyparty = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
inherit (cfg) accountName stateDir;
|
||||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
enable = cfg.reverseProxy.enable;
|
enable = cfg.reverseProxy.enable;
|
||||||
inherit (cfg.reverseProxy) trustedSources;
|
inherit (cfg.reverseProxy) trustedSources;
|
||||||
};
|
};
|
||||||
volumes = lib.mapAttrs (
|
volumes = lib.mapAttrs (_urlPath: volume: {
|
||||||
_urlPath: volume:
|
inherit (volume)
|
||||||
{
|
path
|
||||||
inherit (volume)
|
access
|
||||||
path
|
flags
|
||||||
readOnly
|
readOnly
|
||||||
;
|
;
|
||||||
}
|
}) cfg.volumes;
|
||||||
// lib.optionalAttrs (volume.access != { }) { inherit (volume) access; }
|
|
||||||
// lib.optionalAttrs (volume.flags != { }) { inherit (volume) flags; }
|
|
||||||
) cfg.volumes;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mounts = {
|
mounts = {
|
||||||
"/var/lib/copyparty" = {
|
${cfg.stateDir} = {
|
||||||
directory = cfg.statePath;
|
directory = cfg.statePath;
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
group = "storage";
|
group = "storage";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
inputs,
|
inputs,
|
||||||
util,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# neogaia — Dell XPS 13 9380 laptop.
|
# neogaia — Dell XPS 13 9380 laptop.
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
./disk.nix
|
./disk.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
modules.host.cachyos.enable = true;
|
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
||||||
|
|
||||||
modules.network.enable = true;
|
# So wifi can be joined from the console.
|
||||||
|
networking.networkmanager.enable = true;
|
||||||
|
|
||||||
registry.address.neogaia.self = "10.23.50.146";
|
registry.address.neogaia.self = "10.23.50.146";
|
||||||
|
|
||||||
@@ -27,9 +28,15 @@
|
|||||||
peer = {
|
peer = {
|
||||||
publicKey = "sGEkSD+gEdIMXi4PBN3AhwHGc4tWMtcjOIey3s7yb3I=";
|
publicKey = "sGEkSD+gEdIMXi4PBN3AhwHGc4tWMtcjOIey3s7yb3I=";
|
||||||
endpoint = "100.0.90.192:51820";
|
endpoint = "100.0.90.192:51820";
|
||||||
|
allowedIPs = [ "10.23.0.0/16" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
registry.shell-shortcut.neogaia = {
|
||||||
|
vpn-on = "sudo systemctl start wireguard-wg-home.target";
|
||||||
|
vpn-off = "sudo systemctl stop 'wireguard-wg-home*.service' wireguard-wg-home.target";
|
||||||
|
};
|
||||||
|
|
||||||
modules.monorepo.enable = true;
|
modules.monorepo.enable = true;
|
||||||
modules.shell.herdr.enable = true;
|
modules.shell.herdr.enable = true;
|
||||||
modules.shell.gitea-axi.enable = true;
|
modules.shell.gitea-axi.enable = true;
|
||||||
@@ -40,23 +47,4 @@
|
|||||||
modules.desktop.apps.obsidian.enable = true;
|
modules.desktop.apps.obsidian.enable = true;
|
||||||
modules.desktop.apps.steam.enable = true;
|
modules.desktop.apps.steam.enable = true;
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia
|
|
||||||
@@ -15,15 +15,52 @@
|
|||||||
|
|
||||||
hardware.cpu.intel.updateMicrocode = true;
|
hardware.cpu.intel.updateMicrocode = true;
|
||||||
|
|
||||||
modules.network.vlans = {
|
systemd.network = {
|
||||||
interface = "enp2s0";
|
enable = true;
|
||||||
ids = [ 20 ];
|
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";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
networking.useDHCP = false;
|
||||||
|
|
||||||
|
modules.network.vlans.ids = [ 20 ];
|
||||||
|
|
||||||
boot.zfs.forceImportRoot = false;
|
boot.zfs.forceImportRoot = false;
|
||||||
|
|
||||||
modules.host.zfs = {
|
modules.zfs = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
hostId = "2346edbd";
|
||||||
pools.pikachu = { };
|
pools.pikachu = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,13 +73,12 @@
|
|||||||
steamPath = "/pikachu/data/steamcmd";
|
steamPath = "/pikachu/data/steamcmd";
|
||||||
installPath = "/pikachu/data/servers/valheim/main";
|
installPath = "/pikachu/data/servers/valheim/main";
|
||||||
statePath = "/pikachu/media/games/servers/valheim/main";
|
statePath = "/pikachu/media/games/servers/valheim/main";
|
||||||
serverName = "main";
|
serverName = "butter-chicken";
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "valheim.alexion.dev";
|
domain = "valheim.alexion.dev";
|
||||||
protocol = "udp";
|
udp.ports = [
|
||||||
ports = [
|
|
||||||
config.guests.valheim.port
|
config.guests.valheim.port
|
||||||
(config.guests.valheim.port + 1)
|
(config.guests.valheim.port + 1)
|
||||||
];
|
];
|
||||||
@@ -65,8 +101,22 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "palworld.alexion.dev";
|
domain = "palworld.alexion.dev";
|
||||||
protocol = "udp";
|
udp.ports = [ config.guests.palworld.port ];
|
||||||
ports = [ config.guests.palworld.port ];
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
guests.actualbudget = {
|
||||||
|
enable = true;
|
||||||
|
vlan = 20;
|
||||||
|
mac = "BC:24:11:C1:CD:28";
|
||||||
|
dataPath = "/pikachu/data/actualbudget";
|
||||||
|
address = "10.23.20.42";
|
||||||
|
reverseProxy = {
|
||||||
|
enable = true;
|
||||||
|
route = {
|
||||||
|
domain = "budget.alexion.dev";
|
||||||
|
port = 5006;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -82,7 +132,6 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "music.alexion.dev";
|
domain = "music.alexion.dev";
|
||||||
protocol = "https";
|
|
||||||
port = 4533;
|
port = 4533;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -103,8 +152,7 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "home";
|
domain = "home";
|
||||||
protocol = "http";
|
http.port = 8082;
|
||||||
port = 8082;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -119,9 +167,7 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "files.alexion.dev";
|
domain = "files.alexion.dev";
|
||||||
protocol = "https";
|
|
||||||
port = 3923;
|
port = 3923;
|
||||||
clientMaxBodySize = "1g";
|
|
||||||
};
|
};
|
||||||
trustedSources = [ "${registry.address.${config.networking.hostName}."reverse-proxy"}/32" ];
|
trustedSources = [ "${registry.address.${config.networking.hostName}."reverse-proxy"}/32" ];
|
||||||
};
|
};
|
||||||
@@ -129,21 +175,35 @@
|
|||||||
"/" = {
|
"/" = {
|
||||||
hostPath = "/pikachu/data/copyparty/root";
|
hostPath = "/pikachu/data/copyparty/root";
|
||||||
path = "/srv";
|
path = "/srv";
|
||||||
|
access.r = [ "alexion" ];
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
};
|
};
|
||||||
"/inbox" = {
|
"/inbox" = {
|
||||||
hostPath = "/pikachu/__inbox";
|
hostPath = "/pikachu/__inbox";
|
||||||
path = "/pikachu/__inbox";
|
path = "/pikachu/__inbox";
|
||||||
|
access.A = [ "alexion" ];
|
||||||
|
flags = [
|
||||||
|
"d2d"
|
||||||
|
"d2t"
|
||||||
|
"dthumb"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"/media" = {
|
"/media" = {
|
||||||
hostPath = "/pikachu/media";
|
hostPath = "/pikachu/media";
|
||||||
path = "/pikachu/media";
|
path = "/pikachu/media";
|
||||||
|
access.A = [ "alexion" ];
|
||||||
|
flags = [
|
||||||
|
"d2d"
|
||||||
|
"d2t"
|
||||||
|
"dthumb"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"/opds" = {
|
"/opds" = {
|
||||||
hostPath = "/pikachu/data/opds";
|
hostPath = "/pikachu/data/opds";
|
||||||
path = "/pikachu/data/opds";
|
path = "/pikachu/data/opds";
|
||||||
|
access.r = [ "alexion" ];
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
flags.opds = true;
|
flags = [ "opds" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# zeus — Ryzen desktop with AMD graphics.
|
# zeus — Ryzen desktop with AMD graphics.
|
||||||
@@ -14,19 +15,14 @@
|
|||||||
|
|
||||||
nixpkgs.hostPlatform = "x86_64-linux";
|
nixpkgs.hostPlatform = "x86_64-linux";
|
||||||
|
|
||||||
modules.host.cachyos.enable = true;
|
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
||||||
|
|
||||||
hardware.graphics = {
|
hardware.graphics = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enable32Bit = true;
|
enable32Bit = 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";
|
registry.address.zeus.self = "10.23.50.221";
|
||||||
|
|
||||||
@@ -49,19 +45,8 @@
|
|||||||
modules.desktop.apps.vencord.enable = true;
|
modules.desktop.apps.vencord.enable = true;
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
|
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
|
||||||
{
|
"HDMI-A-1,1920x1080@120,0x0,1,transform,3"
|
||||||
output = "HDMI-A-1";
|
"DP-3,2560x1440@239.96,1080x0,1"
|
||||||
mode = "1920x1080@120";
|
|
||||||
position = "-1080x0";
|
|
||||||
scale = 1;
|
|
||||||
transform = 3;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
output = "DP-3";
|
|
||||||
mode = "2560x1440@239.96";
|
|
||||||
position = "0x0";
|
|
||||||
scale = 1;
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBcHHuubptXCdvED/p2dR3MpjGN/AimqLasLY5984G alexion@zeus
|
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
inputs,
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The shared foundation for every host and guest.
|
# The shared foundation for every host and guest.
|
||||||
|
let
|
||||||
|
stateVersion = "26.05";
|
||||||
|
in
|
||||||
{
|
{
|
||||||
imports = [ inputs.home-manager.nixosModules.home-manager ];
|
imports = [ inputs.home-manager.nixosModules.home-manager ];
|
||||||
|
|
||||||
@@ -13,14 +17,14 @@
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "alexion";
|
default = "alexion";
|
||||||
description = ''
|
description = ''
|
||||||
Names the primary user account.
|
Primary interactive user for this system.
|
||||||
Host configuration uses this name for the Home Manager user.
|
This drives the system account and Home Manager user together.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
system.stateVersion = "26.05";
|
system.stateVersion = stateVersion;
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
nix.settings.experimental-features = [
|
nix.settings.experimental-features = [
|
||||||
@@ -38,7 +42,66 @@
|
|||||||
|
|
||||||
users.groups.storage.gid = 10000;
|
users.groups.storage.gid = 10000;
|
||||||
|
|
||||||
|
security.sudo.extraConfig = ''
|
||||||
|
Defaults env_keep += "EDITOR VISUAL"
|
||||||
|
'';
|
||||||
|
|
||||||
time.timeZone = "America/New_York";
|
time.timeZone = "America/New_York";
|
||||||
i18n.defaultLocale = "en_GB.UTF-8";
|
i18n.defaultLocale = "en_GB.UTF-8";
|
||||||
|
|
||||||
|
home-manager = {
|
||||||
|
useGlobalPkgs = true;
|
||||||
|
useUserPackages = true;
|
||||||
|
users.${config.user.name} = {
|
||||||
|
home.username = config.user.name;
|
||||||
|
home.homeDirectory = "/home/${config.user.name}";
|
||||||
|
home.stateVersion = stateVersion;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
19
configs/nixos/modules/desktop/apps/alacritty.nix
Normal file
19
configs/nixos/modules/desktop/apps/alacritty.nix
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
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 ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
inputs,
|
inputs,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Configures Firefox as the desktop browser.
|
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.firefox;
|
cfg = config.modules.desktop.apps.firefox;
|
||||||
|
user = config.user.name;
|
||||||
|
firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.firefox = {
|
options.modules.desktop.apps.firefox = {
|
||||||
@@ -18,25 +19,19 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "1.0";
|
default = "1.0";
|
||||||
description = ''
|
description = ''
|
||||||
Scales Firefox UI and page content.
|
Firefox UI and page scale, passed to `layout.css.devPixelsPerPx`.
|
||||||
|
Override it on hosts that need a larger browser scale, such as small
|
||||||
Sets `layout.css.devPixelsPerPx`.
|
high-DPI laptop panels.
|
||||||
Use a larger value for high-DPI displays.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.window-icon.${config.networking.hostName}.firefox = {
|
home-manager.users.${user} = hm: {
|
||||||
class = ".*[Ff]irefox.*";
|
|
||||||
icon = util.glyph "f269";
|
|
||||||
};
|
|
||||||
|
|
||||||
home-manager.users.${config.user.name} = hm: {
|
|
||||||
programs.firefox = {
|
programs.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Applies policies that users cannot change in Firefox.
|
# Enterprise policies: enforced and unchangeable from the browser UI.
|
||||||
policies = {
|
policies = {
|
||||||
DisableTelemetry = true;
|
DisableTelemetry = true;
|
||||||
DisableFirefoxStudies = true;
|
DisableFirefoxStudies = true;
|
||||||
@@ -45,7 +40,7 @@ in
|
|||||||
DontCheckDefaultBrowser = true;
|
DontCheckDefaultBrowser = true;
|
||||||
DisableFirefoxAccounts = true;
|
DisableFirefoxAccounts = true;
|
||||||
|
|
||||||
# Removes sponsored content from the new-tab page.
|
# Strip the monetized surfaces from the new-tab page.
|
||||||
FirefoxHome = {
|
FirefoxHome = {
|
||||||
SponsoredTopSites = false;
|
SponsoredTopSites = false;
|
||||||
SponsoredPocket = false;
|
SponsoredPocket = false;
|
||||||
@@ -54,19 +49,23 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
profiles.default = {
|
profiles.default = {
|
||||||
|
isDefault = true;
|
||||||
|
|
||||||
extensions = {
|
extensions = {
|
||||||
packages = with inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}; [
|
packages = with firefoxAddons; [
|
||||||
ublock-origin
|
ublock-origin
|
||||||
proton-pass
|
proton-pass
|
||||||
sponsorblock
|
sponsorblock
|
||||||
];
|
];
|
||||||
|
|
||||||
# Home Manager overwrites Nord theme changes made in Firefox.
|
# The Nord chrome theme is a declared extension setting, so home-manager
|
||||||
|
# owns the extension-settings store, overwriting runtime changes to it.
|
||||||
force = true;
|
force = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Stylix uses a near-white selection background.
|
# Stylix's Nord mapping paints the selected address-bar result a
|
||||||
# Use the Nord selection color because its text stays readable.
|
# near-white grey, leaving its light text unreadable. Darken that one
|
||||||
|
# highlight to the Nord selection grey, from the same scheme.
|
||||||
extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight =
|
extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight =
|
||||||
let
|
let
|
||||||
c = hm.config.lib.stylix.colors;
|
c = hm.config.lib.stylix.colors;
|
||||||
@@ -78,26 +77,32 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
settings = {
|
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;
|
"layout.css.devPixelsPerPx" = cfg.uiScale;
|
||||||
|
|
||||||
# Enables Firefox Color after Firefox sideloads the add-on.
|
# Auto-enable the sideloaded Firefox Color add-on carrying the Nord
|
||||||
|
# chrome theme, which Firefox otherwise leaves disabled.
|
||||||
"extensions.autoDisableScopes" = 0;
|
"extensions.autoDisableScopes" = 0;
|
||||||
|
|
||||||
# Removes sponsored content outside Firefox policies.
|
# Sponsored surfaces the policies above do not reach.
|
||||||
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
|
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
|
||||||
"browser.newtabpage.activity-stream.showSponsored" = false;
|
"browser.newtabpage.activity-stream.showSponsored" = false;
|
||||||
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
|
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
search = {
|
search = {
|
||||||
# Home Manager replaces the engine list when this setting is enabled.
|
# Declaring search overwrites Firefox's own engine list wholesale,
|
||||||
# Engines added in Firefox do not survive a rebuild.
|
# so engines added later in the UI do not survive a rebuild.
|
||||||
force = true;
|
force = true;
|
||||||
default = "ddg";
|
default = "ddg";
|
||||||
|
|
||||||
# Hides commercial search engines.
|
# The general-purpose commercial engines, hidden to leave a lean
|
||||||
# Bangs still reach these engines.
|
# DuckDuckGo-and-Wikipedia list. They stay reachable through bangs.
|
||||||
# Entries with only `metaData` configure Firefox built-in engines.
|
# An engine carrying only metaData is treated as a builtin.
|
||||||
engines = {
|
engines = {
|
||||||
google.metaData.hidden = true;
|
google.metaData.hidden = true;
|
||||||
bing.metaData.hidden = true;
|
bing.metaData.hidden = true;
|
||||||
@@ -109,7 +114,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Applies the Nord theme through Stylix's Firefox Color add-on.
|
# Nord chrome for the one profile, applied through the Stylix-managed
|
||||||
|
# Firefox Color add-on that colorTheme enables.
|
||||||
stylix.targets.firefox = {
|
stylix.targets.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
profileNames = [ "default" ];
|
profileNames = [ "default" ];
|
||||||
@@ -127,8 +133,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Firefox creates `profiles.ini` on its first launch.
|
# Firefox writes profiles.ini itself on first launch, so home-manager is
|
||||||
# Force replacement during Home Manager activation.
|
# told to own the file rather than fail activation refusing to clobber it.
|
||||||
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,30 +2,17 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Ghostty as the desktop terminal.
|
# Ghostty as the desktop terminal.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.ghostty;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.ghostty.enable = lib.mkEnableOption "Ghostty as the desktop terminal";
|
options.modules.desktop.apps.ghostty.enable = lib.mkEnableOption "Ghostty as the desktop terminal";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.ghostty.enable {
|
config = lib.mkIf cfg.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 ];
|
home-manager.users.${user}.home.packages = [ pkgs.ghostty ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,18 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Obsidian as the desktop note-taking vault.
|
# Obsidian as the desktop note-taking vault.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.obsidian;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.obsidian.enable =
|
options.modules.desktop.apps.obsidian.enable =
|
||||||
lib.mkEnableOption "Obsidian as the desktop note-taking vault";
|
lib.mkEnableOption "Obsidian as the desktop note-taking vault";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.obsidian.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.obsidian ];
|
home-manager.users.${user}.home.packages = [ pkgs.obsidian ];
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.obsidian = {
|
|
||||||
class = "obsidian";
|
|
||||||
icon = util.glyph "f02d";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
}:
|
}:
|
||||||
# r2modman, a Thunderstore mod manager.
|
# r2modman, a Thunderstore mod manager.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.r2modman;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.r2modman.enable =
|
options.modules.desktop.apps.r2modman.enable =
|
||||||
lib.mkEnableOption "r2modman, a Thunderstore mod manager";
|
lib.mkEnableOption "r2modman, a Thunderstore mod manager";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.r2modman.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.r2modman ];
|
home-manager.users.${user}.home.packages = [ pkgs.r2modman ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,17 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Signal desktop messenger.
|
# Signal desktop messenger.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.signal;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.signal.enable = lib.mkEnableOption "Signal desktop messenger";
|
options.modules.desktop.apps.signal.enable = lib.mkEnableOption "Signal desktop messenger";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.signal.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ];
|
home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ];
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.signal = {
|
|
||||||
class = "[Ss]ignal";
|
|
||||||
icon = util.glyph "f4ad";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Steam game launcher and runtime integration.
|
# 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";
|
options.modules.desktop.apps.steam.enable = lib.mkEnableOption "Steam game launcher";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.steam.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
programs.gamemode.enable = true;
|
programs.gamemode.enable = true;
|
||||||
|
|
||||||
programs.steam = {
|
programs.steam = {
|
||||||
@@ -24,10 +26,5 @@
|
|||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
pkgs.gamescope
|
pkgs.gamescope
|
||||||
];
|
];
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.steam = {
|
|
||||||
class = "[Ss]team";
|
|
||||||
icon = util.glyph "f1b6";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,18 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Vesktop, a Discord client with Vencord built in.
|
# Vesktop, a Discord client with Vencord built in.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.vencord;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.vencord.enable =
|
options.modules.desktop.apps.vencord.enable =
|
||||||
lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
|
lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.vencord.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
|
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.vesktop = {
|
|
||||||
class = "[Vv]esktop";
|
|
||||||
icon = util.glyph "f1ff";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# The desktop aggregator: one flag brings up the whole graphical session.
|
# 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";
|
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
|
# 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.
|
# any one of them while the single flag above enables the whole desktop.
|
||||||
config = lib.mkIf config.modules.desktop.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
modules.desktop.apps.firefox.enable = lib.mkDefault true;
|
modules.desktop.apps.firefox.enable = lib.mkDefault true;
|
||||||
modules.desktop.apps.ghostty.enable = lib.mkDefault true;
|
modules.desktop.apps.ghostty.enable = lib.mkDefault true;
|
||||||
modules.desktop.session.hyprland.enable = lib.mkDefault true;
|
modules.desktop.session.hyprland.enable = lib.mkDefault true;
|
||||||
@@ -23,6 +26,5 @@
|
|||||||
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
|
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
|
||||||
modules.desktop.utilities.theming.enable = lib.mkDefault true;
|
modules.desktop.utilities.theming.enable = lib.mkDefault true;
|
||||||
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
|
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
|
||||||
registry.env.${config.networking.hostName}.NIXOS_OZONE_WL = "1";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
util,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The Hyprland compositor.
|
# 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";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.enable {
|
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 cfg.enable {
|
||||||
# This program integration owns the session, portals, and polkit, launched
|
# This program integration owns the session, portals, and polkit, launched
|
||||||
# through the universal Wayland session manager.
|
# through the universal Wayland session manager.
|
||||||
programs.hyprland = {
|
programs.hyprland = {
|
||||||
@@ -17,7 +43,7 @@
|
|||||||
withUWSM = true;
|
withUWSM = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.wayland.windowManager.hyprland = {
|
home-manager.users.${user}.wayland.windowManager.hyprland = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# One package drives the whole session, so there is never a version split.
|
# One package drives the whole session, so there is never a version split.
|
||||||
@@ -29,216 +55,135 @@
|
|||||||
# uwsm owns the systemd graphical-session targets.
|
# uwsm owns the systemd graphical-session targets.
|
||||||
systemd.enable = false;
|
systemd.enable = false;
|
||||||
|
|
||||||
# Write the native Lua Hyprland config. Home Manager renders the Nix
|
# Write the native hyprlang hyprland.conf, whose variable and bind syntax
|
||||||
# settings below as hl.* calls.
|
# the settings below are expressed in.
|
||||||
configType = "lua";
|
configType = "hyprlang";
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
config = {
|
"$mod" = "SUPER";
|
||||||
input = {
|
"$terminal" = "ghostty";
|
||||||
kb_layout = config.services.xserver.xkb.layout;
|
|
||||||
repeat_delay = 250;
|
# Session variables handed to the compositor directly.
|
||||||
repeat_rate = 45;
|
# UWSM launches the session without the shell profile that would carry
|
||||||
accel_profile = "flat";
|
# them, so a variable the compositor or its children must see is set
|
||||||
touchpad = {
|
# here rather than through home-manager's sessionVariables.
|
||||||
natural_scroll = true;
|
env = [
|
||||||
tap_to_click = true;
|
# Chromium and Electron apps read this to select native Wayland;
|
||||||
disable_while_typing = true;
|
# 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
env = lib.mapAttrsToList (name: value: {
|
general = {
|
||||||
_args = [
|
gaps_in = 4;
|
||||||
name
|
gaps_out = 8;
|
||||||
value
|
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"
|
||||||
];
|
];
|
||||||
}) (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";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hyprland-toggle-fullscreen = {
|
dwindle = {
|
||||||
keys = "SUPER + F";
|
preserve_split = true;
|
||||||
action.kind = "toggle-fullscreen";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hyprland-close-window = {
|
bind = [
|
||||||
keys = "SUPER + SHIFT + Q";
|
"$mod, Return, exec, $terminal"
|
||||||
action.kind = "close-window";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
|
# Move focus.
|
||||||
position = "left";
|
"$mod, H, movefocus, l"
|
||||||
order = 10;
|
"$mod, J, movefocus, d"
|
||||||
settings = {
|
"$mod, K, movefocus, u"
|
||||||
format = "{id} {windows}";
|
"$mod, L, movefocus, r"
|
||||||
format-window-separator = " ";
|
|
||||||
window-rewrite-default = util.glyph "f2d0";
|
# Move the window within the layout.
|
||||||
window-rewrite = lib.mapAttrs' (
|
"$mod SHIFT, H, movewindow, l"
|
||||||
_name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" windowIcon.icon
|
"$mod SHIFT, J, movewindow, d"
|
||||||
) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon);
|
"$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"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,34 +5,62 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# hypridle: idle-triggered locking and display power management.
|
# hypridle: idle-triggered locking and display power management.
|
||||||
{
|
let
|
||||||
options.modules.desktop.session.hyprland.hypridle.enable =
|
cfg = config.modules.desktop.session.hyprland.hypridle;
|
||||||
lib.mkEnableOption "hypridle idle management";
|
user = config.user.name;
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.hypridle.enable {
|
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";
|
||||||
|
|
||||||
|
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 {
|
||||||
# Closing the lid suspends, and every suspend locks first through the
|
# Closing the lid suspends, and every suspend locks first through the
|
||||||
# before_sleep_cmd below, so the lid always lands at a locked screen.
|
# before_sleep_cmd below, so the lid always lands at a locked screen.
|
||||||
services.logind.settings.Login.HandleLidSwitch = "suspend";
|
services.logind.settings.Login.HandleLidSwitch = "suspend";
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.services.hypridle = {
|
home-manager.users.${user}.services.hypridle = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
general = {
|
general = {
|
||||||
# The guard drops the call when a locker is already up, so no idle
|
lock_cmd = lockCmd;
|
||||||
# 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";
|
before_sleep_cmd = "loginctl lock-session";
|
||||||
# Waking pokes the compositor in case firmware leaves displays dark.
|
# Waking restores the displays the screen-off listener may have cut.
|
||||||
after_sleep_cmd = "${lib.getExe' config.programs.hyprland.package "hyprctl"} dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
after_sleep_cmd = "${hyprctl} dispatch dpms on";
|
||||||
};
|
};
|
||||||
|
|
||||||
listener = [
|
listener = [
|
||||||
# Lock on idle.
|
# Lock on idle.
|
||||||
{
|
{
|
||||||
timeout = 300;
|
timeout = cfg.lockTimeout;
|
||||||
on-timeout = "loginctl lock-session";
|
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";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,54 +2,45 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
||||||
let
|
let
|
||||||
sleepIcon = util.glyph [
|
cfg = config.modules.desktop.session.hyprland.hyprlock;
|
||||||
"db81"
|
user = config.user.name;
|
||||||
"dcb2"
|
|
||||||
];
|
# The hyprlock this module installs, used by the lock keybind below.
|
||||||
sleepOffIcon = util.glyph [
|
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||||
"db81"
|
|
||||||
"dcb3"
|
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
|
||||||
];
|
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||||
|
|
||||||
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
|
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
|
||||||
unit=lockscreen-idle-inhibit.service
|
unit=lockscreen-idle-inhibit.service
|
||||||
|
|
||||||
refresh_waybar() {
|
refresh_waybar() {
|
||||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+9 waybar >/dev/null 2>&1 || true
|
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet "$unit"; then
|
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
||||||
${lib.getExe' pkgs.systemd "systemctl"} --user stop "$unit"
|
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepIcon}"
|
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.systemd "systemd-run"} \
|
if ${pkgs.systemd}/bin/systemd-run \
|
||||||
--user \
|
--user \
|
||||||
--collect \
|
--collect \
|
||||||
--unit=lockscreen-idle-inhibit \
|
--unit=lockscreen-idle-inhibit \
|
||||||
--description="Disable idle-triggered screen locking" \
|
--description="Disable idle-triggered screen locking" \
|
||||||
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
|
${pkgs.systemd}/bin/systemd-inhibit \
|
||||||
--what=idle \
|
--what=idle \
|
||||||
--who=lockscreen-hotkey \
|
--who=lockscreen-hotkey \
|
||||||
--why="lock screen disabled by hotkey" \
|
--why="lock screen disabled by hotkey" \
|
||||||
${lib.getExe' pkgs.coreutils "sleep"} infinity >/dev/null; then
|
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}"
|
${pkgs.swayosd}/bin/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
|
fi
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
@@ -57,8 +48,8 @@ in
|
|||||||
options.modules.desktop.session.hyprland.hyprlock.enable =
|
options.modules.desktop.session.hyprland.hyprlock.enable =
|
||||||
lib.mkEnableOption "the hyprlock lock screen";
|
lib.mkEnableOption "the hyprlock lock screen";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.hyprlock.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Colors and the lock-screen background come from Stylix's hyprlock target,
|
# Colors and the lock-screen background come from Stylix's hyprlock target,
|
||||||
# so only geometry and behaviour are set here.
|
# so only geometry and behaviour are set here.
|
||||||
programs.hyprlock = {
|
programs.hyprlock = {
|
||||||
@@ -91,36 +82,14 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
# Lock on Super+X.
|
||||||
hyprlock-lock = {
|
# The guard drops the keypress when a locker is already up, so a second
|
||||||
keys = "SUPER + X";
|
# hyprlock never stacks over the first.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
||||||
# The guard drops the call when a locker is already up, so no manual
|
"$mod CTRL, X, exec, ${idleLockToggle}"
|
||||||
# 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;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,19 @@
|
|||||||
}:
|
}:
|
||||||
# Text login: greetd running the tuigreet greeter, which starts the session
|
# Text login: greetd running the tuigreet greeter, which starts the session
|
||||||
# through the universal Wayland session manager.
|
# through the universal Wayland session manager.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.session.login;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.login.enable =
|
options.modules.desktop.session.login.enable =
|
||||||
lib.mkEnableOption "greetd with the tuigreet text greeter";
|
lib.mkEnableOption "greetd with the tuigreet text greeter";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.login.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
services.greetd = {
|
services.greetd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings.default_session.command = lib.escapeShellArgs [
|
settings.default_session.command =
|
||||||
(lib.getExe pkgs.tuigreet)
|
"${lib.getExe pkgs.tuigreet} --time --remember "
|
||||||
"--time"
|
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
|
||||||
"--remember"
|
|
||||||
"--cmd"
|
|
||||||
"${lib.getExe config.programs.uwsm.package} start -e -D Hyprland hyprland.desktop"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,20 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
||||||
let
|
let
|
||||||
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
|
cfg = config.modules.desktop.session.mako;
|
||||||
if ${lib.getExe' pkgs.mako "makoctl"} mode 2>/dev/null | ${lib.getExe pkgs.gnugrep} -qx dnd; then
|
user = config.user.name;
|
||||||
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
|
|
||||||
'';
|
|
||||||
|
|
||||||
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
|
makoctl = "${pkgs.mako}/bin/makoctl";
|
||||||
${lib.getExe' pkgs.mako "makoctl"} mode -t dnd >/dev/null 2>&1
|
|
||||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+8 waybar
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
|
options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.mako.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Colors and the popup font come from Stylix's mako target, so only
|
# Colors and the popup font come from Stylix's mako target, so only
|
||||||
# behaviour is set here.
|
# behaviour is set here.
|
||||||
services.mako = {
|
services.mako = {
|
||||||
@@ -41,25 +32,13 @@ in
|
|||||||
"mode=dnd".invisible = true;
|
"mode=dnd".invisible = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName}.mako-history = {
|
# Recall the last notification from history, keyboard-only like the rest
|
||||||
keys = "SUPER + N";
|
# of the session.
|
||||||
action = {
|
# $mod is defined by the compositor config these binds share.
|
||||||
kind = "exec";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
command = "${lib.getExe' pkgs.mako "makoctl"} restore";
|
"$mod, N, exec, ${makoctl} restore"
|
||||||
};
|
];
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/dnd" = {
|
|
||||||
order = 80;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${dndStatus}";
|
|
||||||
on-click = "${dndToggle}";
|
|
||||||
interval = "once";
|
|
||||||
signal = 8;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# XDG desktop portal routing for in-app screen sharing and file dialogs.
|
# 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 =
|
options.modules.desktop.session.portals.enable =
|
||||||
lib.mkEnableOption "XDG desktop portals for in-app screen sharing and file dialogs";
|
lib.mkEnableOption "XDG desktop portals for in-app screen sharing and file dialogs";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.portals.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# The backend packages arrive with the compositor, so only the routing is set
|
# The backend packages arrive with the compositor, so only the routing is set
|
||||||
# here.
|
# here.
|
||||||
# Screen sharing, screenshots, and global shortcuts need the compositor's
|
# Screen sharing, screenshots, and global shortcuts need the compositor's
|
||||||
|
|||||||
@@ -6,61 +6,47 @@
|
|||||||
}:
|
}:
|
||||||
# rofi as the desktop launcher and dmenu-style menu frontend.
|
# rofi as the desktop launcher and dmenu-style menu frontend.
|
||||||
let
|
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
|
# A session menu built on the same themed rofi, so the power actions look
|
||||||
# like every other menu the launcher drives.
|
# like every other menu the launcher drives.
|
||||||
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
|
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
|
||||||
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
|
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
|
||||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Power)
|
| ${pkgs.rofi}/bin/rofi -dmenu -i -p Power)
|
||||||
case "$chosen" in
|
case "$chosen" in
|
||||||
Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
|
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;;
|
||||||
Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
|
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;;
|
||||||
'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
|
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;;
|
||||||
Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
|
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;;
|
||||||
'Shut down') ${lib.getExe' pkgs.systemd "systemctl"} poweroff ;;
|
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;;
|
||||||
esac
|
esac
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
|
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.rofi.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = hm: {
|
home-manager.users.${user} = {
|
||||||
# Stylix supplies the Nord theme.
|
# No theme is set here: Stylix's rofi target supplies the Nord one.
|
||||||
programs.rofi = {
|
programs.rofi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Application launch is the only mode, kept minimal so the prompt opens
|
# Application launch is the only mode, kept minimal so the prompt opens
|
||||||
# as fast as rofi can read the desktop entries.
|
# as fast as rofi can read the desktop entries.
|
||||||
# Icons are off: resolving one per entry is the bulk of drun's startup.
|
# Icons are off: resolving one per entry is the bulk of drun's startup.
|
||||||
settings = {
|
extraConfig = {
|
||||||
font = lib.mkForce "JetBrains Mono 12";
|
|
||||||
modi = "drun";
|
modi = "drun";
|
||||||
show-icons = false;
|
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";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rofi-power-menu = {
|
# The launcher and the session menu, kept next to the tool they drive.
|
||||||
keys = "SUPER + SHIFT + X";
|
# $mod is defined by the compositor config these binds share.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod, R, exec, ${pkgs.rofi}/bin/rofi -show drun"
|
||||||
command = "${powerMenu}";
|
"$mod SHIFT, X, exec, ${powerMenu}"
|
||||||
};
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,57 +6,165 @@
|
|||||||
}:
|
}:
|
||||||
# Waybar: a top status bar reading system state at a glance.
|
# Waybar: a top status bar reading system state at a glance.
|
||||||
let
|
let
|
||||||
moduleNames =
|
cfg = config.modules.desktop.session.waybar;
|
||||||
position:
|
vpnClient = config.modules.network.vpn-client;
|
||||||
map (entry: entry.name) (
|
user = config.user.name;
|
||||||
lib.sort (left: right: left.module.order < right.module.order) (
|
|
||||||
lib.filter (entry: entry.module.position == position) moduleEntries
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
moduleEntries = lib.mapAttrsToList (name: module: {
|
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
||||||
inherit name module;
|
# through JSON so the glyph survives as bytes rather than an editor paste.
|
||||||
}) modules;
|
g = code: builtins.fromJSON ''"\u${code}"'';
|
||||||
|
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||||
|
|
||||||
modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
|
# 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
|
||||||
|
'';
|
||||||
|
|
||||||
|
vpnStatus = pkgs.writeShellScript "waybar-vpn-status" ''
|
||||||
|
interface=${lib.escapeShellArg vpnClient.interface}
|
||||||
|
|
||||||
|
if ! ${pkgs.iproute2}/bin/ip link show dev "$interface" >/dev/null 2>&1; then
|
||||||
|
printf '{"text":"","class":"idle"}\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ipaddr=$(${pkgs.iproute2}/bin/ip -4 -o addr show dev "$interface" | ${pkgs.gawk}/bin/awk '{ print $4; exit }')
|
||||||
|
|
||||||
|
${pkgs.jq}/bin/jq -cn \
|
||||||
|
--arg text "" \
|
||||||
|
--arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \
|
||||||
|
--arg class "active" \
|
||||||
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||||
|
'';
|
||||||
|
|
||||||
|
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 }'
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.waybar.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# A symbols font backs the module glyphs, so pango falls back to it for the
|
# A symbols font backs the module glyphs, so pango falls back to it for the
|
||||||
# codepoints Stylix's monospace font does not carry.
|
# codepoints Stylix's monospace font does not carry.
|
||||||
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
|
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.programs.waybar = {
|
home-manager.users.${user}.programs.waybar = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.alexion.waybar;
|
|
||||||
|
|
||||||
# A user service bound to graphical-session.target, which uwsm activates,
|
# A user service bound to graphical-session.target, which uwsm activates,
|
||||||
# so the bar comes up with the session and no compositor exec-once.
|
# so the bar comes up with the session and no compositor exec-once.
|
||||||
systemd.enable = true;
|
systemd.enable = true;
|
||||||
|
|
||||||
settings.mainBar = lib.mkMerge [
|
style = ''
|
||||||
{
|
.modules-right {
|
||||||
layer = "top";
|
margin-right: 8px;
|
||||||
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)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName} = {
|
#custom-dnd {
|
||||||
clock = {
|
margin-right: 8px;
|
||||||
position = "center";
|
}
|
||||||
order = 10;
|
'';
|
||||||
settings = {
|
|
||||||
|
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"
|
||||||
|
]
|
||||||
|
++ lib.optional vpnClient.enable "custom/vpn"
|
||||||
|
++ [
|
||||||
|
"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 = {
|
||||||
format = "{:%a %d %b %H:%M}";
|
format = "{:%a %d %b %H:%M}";
|
||||||
tooltip-format = "<tt>{calendar}</tt>";
|
tooltip-format = "<tt>{calendar}</tt>";
|
||||||
calendar = {
|
calendar = {
|
||||||
@@ -65,6 +173,77 @@ in
|
|||||||
format.today = "<b><u>{}</u></b>";
|
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/vpn" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${vpnStatus}";
|
||||||
|
interval = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
"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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +1,12 @@
|
|||||||
{
|
{ config, lib, ... }:
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# PipeWire as the desktop audio server.
|
# PipeWire as the desktop audio server.
|
||||||
let
|
let
|
||||||
volumeKey = keys: command: {
|
cfg = config.modules.desktop.utilities.audio;
|
||||||
inherit keys;
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --output-volume ${command}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaKey = keys: command: {
|
|
||||||
inherit keys;
|
|
||||||
locked = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe pkgs.playerctl} ${command}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
|
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.audio.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# Realtime scheduling for the audio threads, so playback survives load.
|
# Realtime scheduling for the audio threads, so playback survives load.
|
||||||
security.rtkit.enable = true;
|
security.rtkit.enable = true;
|
||||||
|
|
||||||
@@ -37,50 +15,5 @@ in
|
|||||||
alsa.enable = true;
|
alsa.enable = true;
|
||||||
pulse.enable = true;
|
pulse.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
|
||||||
volume-up = volumeKey "XF86AudioRaiseVolume" "raise --max-volume 100";
|
|
||||||
volume-down = volumeKey "XF86AudioLowerVolume" "lower";
|
|
||||||
mute = {
|
|
||||||
keys = "XF86AudioMute";
|
|
||||||
locked = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --output-volume mute-toggle";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,30 @@
|
|||||||
}:
|
}:
|
||||||
# Clipboard history: cliphist records every copy, picked back through rofi.
|
# Clipboard history: cliphist records every copy, picked back through rofi.
|
||||||
let
|
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 picker reuses the themed rofi, so history looks like every other menu
|
||||||
# the launcher drives.
|
# the launcher drives.
|
||||||
# decode is needed because list emits id-prefixed lines rather than the copied
|
# 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.
|
# bytes, so the chosen id has to be resolved back before it can be re-copied.
|
||||||
picker = pkgs.writeShellScript "clipboard-picker" ''
|
picker = pkgs.writeShellScript "clipboard-picker" ''
|
||||||
${lib.getExe pkgs.cliphist} list \
|
${cliphist} list \
|
||||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
|
| ${rofi} -dmenu -i -p Clipboard \
|
||||||
| ${lib.getExe pkgs.cliphist} decode \
|
| ${cliphist} decode \
|
||||||
| ${lib.getExe' pkgs.wl-clipboard "wl-copy"}
|
| ${wl-copy}
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.clipboard.enable =
|
options.modules.desktop.utilities.clipboard.enable =
|
||||||
lib.mkEnableOption "cliphist clipboard history";
|
lib.mkEnableOption "cliphist clipboard history";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
|
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
|
||||||
# clipboard.
|
# clipboard.
|
||||||
# The watchers and picker above reach wl-clipboard by store path, so this
|
# The watchers and picker above reach wl-clipboard by store path, so this
|
||||||
@@ -32,14 +39,11 @@ in
|
|||||||
# Two watchers record text and images to history, bound to the graphical
|
# Two watchers record text and images to history, bound to the graphical
|
||||||
# session so uwsm starts and stops them with it.
|
# session so uwsm starts and stops them with it.
|
||||||
services.cliphist.enable = true;
|
services.cliphist.enable = true;
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName}.clipboard-history = {
|
# $mod is defined by the compositor config these binds share.
|
||||||
keys = "SUPER + SHIFT + V";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
action = {
|
"$mod SHIFT, V, exec, ${picker}"
|
||||||
kind = "exec";
|
];
|
||||||
command = "${picker}";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,52 +5,36 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# SwayOSD: a transient on-screen popup for volume and brightness changes.
|
# 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 =
|
options.modules.desktop.utilities.osd.enable =
|
||||||
lib.mkEnableOption "the SwayOSD volume and brightness popup";
|
lib.mkEnableOption "the SwayOSD volume and brightness popup";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.osd.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
environment.systemPackages = [ pkgs.swayosd ];
|
environment.systemPackages = [ pkgs.swayosd ];
|
||||||
|
|
||||||
# The udev rule chgrps each backlight's brightness node to `video` and adds
|
# The udev rule chgrps each backlight's brightness node to `video` and adds
|
||||||
# group write, so the server dims the panel without root.
|
# group write, so the server dims the panel without root.
|
||||||
# Membership below grants the running session that access.
|
# Membership below grants the running session that access.
|
||||||
services.udev.packages = [ pkgs.swayosd ];
|
services.udev.packages = [ pkgs.swayosd ];
|
||||||
users.users.${config.user.name}.extraGroups = [ "video" ];
|
users.users.${user}.extraGroups = [ "video" ];
|
||||||
|
|
||||||
# The server draws the popups, so it runs for the whole graphical session.
|
# The server draws the popups, so it runs for the whole graphical session.
|
||||||
# It binds to the target uwsm activates.
|
# It binds to the target uwsm activates.
|
||||||
home-manager.users.${config.user.name}.systemd.user.services.swayosd = {
|
home-manager.users.${user}.systemd.user.services.swayosd = {
|
||||||
Unit = {
|
Unit = {
|
||||||
Description = "SwayOSD on-screen display server";
|
Description = "SwayOSD on-screen display server";
|
||||||
PartOf = [ "graphical-session.target" ];
|
PartOf = [ "graphical-session.target" ];
|
||||||
After = [ "graphical-session.target" ];
|
After = [ "graphical-session.target" ];
|
||||||
};
|
};
|
||||||
Service = {
|
Service = {
|
||||||
ExecStart = lib.getExe' pkgs.swayosd "swayosd-server";
|
ExecStart = "${pkgs.swayosd}/bin/swayosd-server";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
Install.WantedBy = [ "graphical-session.target" ];
|
Install.WantedBy = [ "graphical-session.target" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
|
||||||
brightness-up = {
|
|
||||||
keys = "XF86MonBrightnessUp";
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --brightness raise";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
brightness-down = {
|
|
||||||
keys = "XF86MonBrightnessDown";
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --brightness lower";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,67 +2,54 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
||||||
let
|
let
|
||||||
# A single key starts and stops recording.
|
cfg = config.modules.desktop.utilities.recording;
|
||||||
# Waybar refreshes on state changes instead of polling.
|
user = config.user.name;
|
||||||
recordingToggle = pkgs.writeShellScript "screen-record-toggle" ''
|
|
||||||
refresh_waybar() {
|
|
||||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+10 waybar >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
slurp = "${pkgs.slurp}/bin/slurp";
|
||||||
${lib.getExe' pkgs.procps "pkill"} -INT -x wf-recorder
|
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
|
||||||
|
# that stop arrives, so this instance lives for the whole recording and then
|
||||||
|
# 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
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
region=$(${lib.getExe pkgs.slurp}) || exit 0
|
region=$(${slurp}) || exit 0
|
||||||
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
|
dir="$(${xdgUserDir} VIDEOS)/Recordings"
|
||||||
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
${mkdir} -p "$dir"
|
||||||
file="$dir/recording-$(${lib.getExe' pkgs.coreutils "date"} +%Y%m%d-%H%M%S).mp4"
|
file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4"
|
||||||
|
|
||||||
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
|
${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio."
|
||||||
${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file" &
|
${wf-recorder} -g "$region" -f "$file"
|
||||||
recorder=$!
|
${notify-send} -a "Screen recording" "Recording saved" "$file"
|
||||||
refresh_waybar
|
|
||||||
wait "$recorder" || true
|
|
||||||
refresh_waybar
|
|
||||||
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording saved" "$file"
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Empty text collapses the widget when recording is idle.
|
|
||||||
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
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.recording.enable =
|
options.modules.desktop.utilities.recording.enable =
|
||||||
lib.mkEnableOption "wf-recorder screen recording";
|
lib.mkEnableOption "wf-recorder screen recording";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.recording.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.keybind.${config.networking.hostName}.recording-toggle = {
|
home-manager.users.${user} = {
|
||||||
keys = "SUPER + SHIFT + R";
|
# $mod is defined by the compositor config these binds share.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod SHIFT, R, exec, ${toggle}"
|
||||||
command = "${recordingToggle}";
|
];
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/recording" = {
|
|
||||||
order = 10;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${recordingStatus}";
|
|
||||||
interval = "once";
|
|
||||||
signal = 10;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,28 @@
|
|||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Keyboard-driven screenshots copy to the clipboard before Satty opens for
|
# Keyboard-driven screenshots that open in satty for annotation, then land in
|
||||||
# annotation. Satty copies annotated screenshots when it saves them.
|
# both the clipboard and a dated file.
|
||||||
let
|
let
|
||||||
screenshotKey = keys: target: {
|
cfg = config.modules.desktop.utilities.screenshot;
|
||||||
inherit keys;
|
user = config.user.name;
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${capture target}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Copy the original screenshot before Satty opens.
|
grimblast = "${pkgs.grimblast}/bin/grimblast";
|
||||||
# Satty copies and saves annotated screenshots when the user presses Enter.
|
satty = "${pkgs.satty}/bin/satty";
|
||||||
|
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.
|
||||||
capture =
|
capture =
|
||||||
target:
|
target:
|
||||||
pkgs.writeShellScript "screenshot-${target}" ''
|
pkgs.writeShellScript "screenshot-${target}" ''
|
||||||
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} PICTURES)/Screenshots"
|
dir="$(${xdgUserDir} PICTURES)/Screenshots"
|
||||||
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
mkdir -p "$dir"
|
||||||
${lib.getExe pkgs.grimblast} save ${target} - \
|
${grimblast} save ${target} - \
|
||||||
| ${lib.getExe' pkgs.coreutils "tee"} >(${lib.getExe' pkgs.wl-clipboard "wl-copy"}) \
|
| ${satty} --filename - \
|
||||||
| ${lib.getExe pkgs.satty} --filename - \
|
|
||||||
--output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \
|
--output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \
|
||||||
--copy-command ${lib.getExe' pkgs.wl-clipboard "wl-copy"} \
|
--copy-command ${wl-copy} \
|
||||||
--save-after-copy \
|
--save-after-copy \
|
||||||
--early-exit \
|
--early-exit \
|
||||||
--actions-on-enter save-to-clipboard
|
--actions-on-enter save-to-clipboard
|
||||||
@@ -36,22 +35,24 @@ in
|
|||||||
options.modules.desktop.utilities.screenshot.enable =
|
options.modules.desktop.utilities.screenshot.enable =
|
||||||
lib.mkEnableOption "screenshots via grimblast and satty";
|
lib.mkEnableOption "screenshots via grimblast and satty";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.screenshot.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# satty is a one-shot annotation surface, so float it rather than letting
|
# satty is a one-shot annotation surface, so float it rather than letting
|
||||||
# it claim a tile in the layout.
|
# it claim a tile in the layout.
|
||||||
wayland.windowManager.hyprland.settings.window_rule = [
|
# windowrule v3 syntax: space-separated field/value tokens, comma
|
||||||
{
|
# separated, with matchers under a match: prefix and effects bare.
|
||||||
match.class = "^(com\\.gabm\\.satty)$";
|
wayland.windowManager.hyprland.settings.windowrule = [
|
||||||
float = true;
|
"float 1, match:class ^(com\\.gabm\\.satty)$"
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
# Print with plain/Shift/Ctrl for region/window/full.
|
||||||
screenshot-area = screenshotKey "Print" "area";
|
# Super+L is already the hjkl focus and movement bind, so screenshots take
|
||||||
screenshot-active = screenshotKey "SHIFT + Print" "active";
|
# the Print key instead.
|
||||||
screenshot-screen = screenshotKey "CTRL + Print" "screen";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
|
", Print, exec, ${capture "area"}"
|
||||||
|
"SHIFT, Print, exec, ${capture "active"}"
|
||||||
|
"CTRL, Print, exec, ${capture "screen"}"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
}:
|
}:
|
||||||
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
|
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.utilities.theming;
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
# A single static Nord wallpaper, drawn as a vertical gradient across the
|
# 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.
|
# Polar Night shades so it needs no committed binary and no network fetch.
|
||||||
wallpaper = pkgs.runCommand "nord-wallpaper.png" { } ''
|
wallpaper = pkgs.runCommand "nord-wallpaper.png" { } ''
|
||||||
${lib.getExe pkgs.imagemagick} \
|
${pkgs.imagemagick}/bin/magick \
|
||||||
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
||||||
"$out"
|
"$out"
|
||||||
'';
|
'';
|
||||||
@@ -18,7 +21,7 @@ in
|
|||||||
options.modules.desktop.utilities.theming.enable =
|
options.modules.desktop.utilities.theming.enable =
|
||||||
lib.mkEnableOption "Nord theming of the graphical layer via Stylix";
|
lib.mkEnableOption "Nord theming of the graphical layer via Stylix";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.theming.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
stylix = {
|
stylix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
@@ -57,7 +60,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Stylix drives the cursor through home-manager's pointer-cursor config,
|
# Stylix drives the cursor through home-manager's pointer-cursor config,
|
||||||
# whose generation must be switched on explicitly.
|
# whose generation must be switched on explicitly.
|
||||||
home.pointerCursor.enable = true;
|
home.pointerCursor.enable = true;
|
||||||
@@ -66,10 +69,5 @@ in
|
|||||||
# base16 mapping Stylix would apply, so its target stays off.
|
# base16 mapping Stylix would apply, so its target stays off.
|
||||||
stylix.targets.nixvim.enable = false;
|
stylix.targets.nixvim.enable = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.env.${config.networking.hostName} = {
|
|
||||||
HYPRCURSOR_THEME = config.stylix.cursor.name;
|
|
||||||
HYPRCURSOR_SIZE = toString config.stylix.cursor.size;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,15 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# Declares XDG user directories, so tools that query them use predictable
|
# Declares the XDG user directories, so a tool that queries them lands in a
|
||||||
# folders a host can relocate from one place.
|
# well-known folder a host can relocate from one place.
|
||||||
let
|
let
|
||||||
homeDirectory = "/home/${config.user.name}";
|
cfg = config.modules.desktop.utilities.userdirs;
|
||||||
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
|
options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.userdirs.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name}.xdg.userDirs = {
|
# Write ~/.config/user-dirs.dirs from the option defaults.
|
||||||
enable = true;
|
home-manager.users.${user}.xdg.userDirs.enable = true;
|
||||||
createDirectories = true;
|
|
||||||
setSessionVariables = true;
|
|
||||||
|
|
||||||
desktop = "${homeDirectory}/.ignoreme";
|
|
||||||
documents = "${homeDirectory}/docs";
|
|
||||||
download = "${homeDirectory}/dwn";
|
|
||||||
music = "${homeDirectory}/mus";
|
|
||||||
pictures = "${homeDirectory}/pic";
|
|
||||||
publicShare = "${homeDirectory}/.ignoreme";
|
|
||||||
templates = "${homeDirectory}/.ignoreme";
|
|
||||||
videos = "${homeDirectory}/vid";
|
|
||||||
projects = "${homeDirectory}/.ignoreme";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.modules.guest;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.guest.enable = lib.mkEnableOption "nested guest configuration";
|
options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.guest.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
modules.network.ssh.enable = lib.mkDefault false;
|
modules.network.ssh.enable = lib.mkDefault true;
|
||||||
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
|
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
52
configs/nixos/modules/host.nix
Normal file
52
configs/nixos/modules/host.nix
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.modules.host;
|
||||||
|
inherit (config) user;
|
||||||
|
hostSecrets = ../secrets + "/${config.networking.hostName}.yaml";
|
||||||
|
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.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";
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
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="
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
{
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Imports existing ZFS pools that hold durable service state.
|
|
||||||
let
|
|
||||||
cfg = config.modules.host.zfs;
|
|
||||||
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
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,13 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Tools for developing this monorepo.
|
# Tools for developing this monorepo.
|
||||||
|
let
|
||||||
|
cfg = config.modules.monorepo;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.monorepo.enable = lib.mkEnableOption "tools for developing this monorepo";
|
options.modules.monorepo.enable = lib.mkEnableOption "tools for developing this monorepo";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.monorepo.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
modules.shell.bazel.enable = true;
|
modules.shell.bazel.enable = true;
|
||||||
modules.shell.python.enable = true;
|
modules.shell.python.enable = true;
|
||||||
modules.shell.rust.enable = true;
|
modules.shell.rust.enable = true;
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,203 +4,344 @@
|
|||||||
registry,
|
registry,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Configures inbound and outbound SSH access.
|
# SSH on this machine, in both directions.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.ssh;
|
cfg = config.modules.network.ssh;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
workstationNames = [
|
inherit (lib)
|
||||||
"neogaia"
|
concatLists
|
||||||
"zeus"
|
concatStringsSep
|
||||||
];
|
elem
|
||||||
serverNames = [ "pikachu" ];
|
filter
|
||||||
|
genAttrs
|
||||||
|
hasAttr
|
||||||
|
imap0
|
||||||
|
listToAttrs
|
||||||
|
mapAttrs
|
||||||
|
mapAttrsToList
|
||||||
|
mkIf
|
||||||
|
mkMerge
|
||||||
|
mkOption
|
||||||
|
nameValuePair
|
||||||
|
optional
|
||||||
|
optionalAttrs
|
||||||
|
types
|
||||||
|
unique
|
||||||
|
;
|
||||||
|
|
||||||
isWorkstation = lib.elem config.networking.hostName workstationNames;
|
hostKeySecret = type: "ssh-host-${type}-key";
|
||||||
isServer = lib.elem config.networking.hostName serverNames;
|
userKeySecret = "ssh-user-ed25519-key";
|
||||||
|
|
||||||
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
|
targetType = types.submodule (
|
||||||
hostKeysFor =
|
{ name, ... }:
|
||||||
host:
|
{
|
||||||
map builtins.readFile [
|
options = {
|
||||||
(../../hosts + "/${host}/ssh_host_ed25519_key.pub")
|
hostName = mkOption {
|
||||||
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
|
type = types.str;
|
||||||
];
|
default = name;
|
||||||
|
description = ''
|
||||||
|
The network address OpenSSH connects to for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
|
user = mkOption {
|
||||||
hostName = registry.address.${name}.self;
|
type = types.str;
|
||||||
clientKey = clientKeyFor name;
|
default = config.user.name;
|
||||||
hostKeys = hostKeysFor name;
|
description = ''
|
||||||
});
|
The remote login name OpenSSH uses for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
extraTargets = {
|
port = mkOption {
|
||||||
# TODO: Pin Gitea's host key after migrating it from Proxmox.
|
type = types.port;
|
||||||
gitea = {
|
default = 22;
|
||||||
hostName = "git.alexion.dev";
|
description = ''
|
||||||
port = 2022;
|
The TCP port OpenSSH uses for this target.
|
||||||
user = "gitea";
|
'';
|
||||||
aliases = [
|
};
|
||||||
"gitea"
|
|
||||||
"git.alexion.dev"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkTarget = name: target: {
|
aliases = mkOption {
|
||||||
hostName = target.hostName or name;
|
type = types.listOf types.str;
|
||||||
user = target.user or user;
|
default = [ name ];
|
||||||
port = target.port or 22;
|
description = ''
|
||||||
aliases = target.aliases or [ name ];
|
Host patterns written into the generated OpenSSH client block.
|
||||||
clientKey = target.clientKey or null;
|
'';
|
||||||
hostKeys = target.hostKeys or [ ];
|
};
|
||||||
};
|
|
||||||
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets);
|
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;
|
||||||
|
|
||||||
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names);
|
|
||||||
defaultAuthorizedKeys = lib.flatten (
|
defaultAuthorizedKeys = lib.flatten (
|
||||||
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
|
clientKeysFor workstationNames ++ optional isServer (clientKeysFor serverNames)
|
||||||
);
|
);
|
||||||
|
|
||||||
outboundTargetNames =
|
outboundTargetNames =
|
||||||
let
|
let
|
||||||
groupTargets =
|
groupTargets =
|
||||||
if isServer then
|
if isServer then
|
||||||
builtins.attrNames extraTargets ++ serverNames
|
[ "gitea" ] ++ cfg.hosts.servers
|
||||||
else if isWorkstation then
|
else if isWorkstation then
|
||||||
builtins.attrNames extraTargets ++ serverNames ++ workstationNames
|
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
|
||||||
else
|
else
|
||||||
builtins.attrNames extraTargets;
|
[ "gitea" ];
|
||||||
in
|
in
|
||||||
lib.filter (name: name != config.networking.hostName) groupTargets;
|
filter (name: name != currentHost) (targetNamesIn groupTargets);
|
||||||
|
|
||||||
knownHosts = lib.listToAttrs (
|
|
||||||
lib.concatLists (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
targetName: target:
|
|
||||||
lib.imap0 (
|
|
||||||
i: key:
|
|
||||||
lib.nameValuePair "${targetName}-${toString i}" {
|
|
||||||
hostNames = knownHostNamesFor target;
|
|
||||||
publicKey = keyWithoutComment key;
|
|
||||||
}
|
|
||||||
) target.hostKeys
|
|
||||||
) 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 =
|
sshSettingsFor =
|
||||||
name:
|
name:
|
||||||
let
|
let
|
||||||
target = targets.${name};
|
target = cfg.targets.${name};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
header = "Host ${lib.concatStringsSep " " target.aliases}";
|
header = "Host ${concatStringsSep " " target.aliases}";
|
||||||
HostName = target.hostName;
|
HostName = target.hostName;
|
||||||
User = target.user;
|
User = target.user;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (target.port != 22) { Port = target.port; };
|
// optionalAttrs (target.port != 22) { Port = target.port; };
|
||||||
|
|
||||||
hostKeySecret = type: "ssh-host-${type}-key";
|
knownHostNamesFor =
|
||||||
hostKeyTypes = [
|
target:
|
||||||
"ed25519"
|
let
|
||||||
"rsa"
|
names = unique (target.aliases ++ [ target.hostName ]);
|
||||||
];
|
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
||||||
userKeySecret = "ssh-user-ed25519-key";
|
in
|
||||||
|
map withPort names;
|
||||||
|
|
||||||
|
knownHosts = listToAttrs (
|
||||||
|
concatLists (
|
||||||
|
mapAttrsToList (
|
||||||
|
targetName: target:
|
||||||
|
imap0 (
|
||||||
|
i: key:
|
||||||
|
nameValuePair "${targetName}-${toString i}" {
|
||||||
|
hostNames = knownHostNamesFor target;
|
||||||
|
publicKey = keyWithoutComment key;
|
||||||
|
}
|
||||||
|
) target.hostKeys
|
||||||
|
) cfg.targets
|
||||||
|
)
|
||||||
|
);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.ssh = {
|
options.modules.network.ssh = {
|
||||||
enable = lib.mkEnableOption "the OpenSSH daemon";
|
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
|
||||||
|
|
||||||
hostKeys.restore = lib.mkOption {
|
hosts = {
|
||||||
type = lib.types.bool;
|
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;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = ''
|
||||||
Restores host keys from secrets.
|
Restore the host keys from secrets rather than letting the daemon generate its own.
|
||||||
Preserves the fingerprint across reimages.
|
A machine with its own identity keeps its fingerprint across a reimage by restoring committed keys.
|
||||||
Guests disable this option and generate their own host keys.
|
A guest carries no host identity, so it turns this off and presents a self-generated key instead.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.sopsFile = lib.mkOption {
|
hostKeys.sopsFile = mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Sets encrypted file that contains host private keys.
|
Encrypted file holding this host's SSH host private keys, one entry per key type, named `ssh-host-<type>-key`.
|
||||||
Names each secret `ssh-host-<type>-key`.
|
Required when `restore` is on.
|
||||||
Requires a value when `restore` is enabled.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
userKey.sopsFile = lib.mkOption {
|
hostKeys.types = mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
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;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Sets encrypted file that contains the SSH client private key.
|
Encrypted file holding this machine's SSH client private key, under the entry `ssh-user-ed25519-key`.
|
||||||
Names the secret `ssh-user-ed25519-key`.
|
Left unset on a machine that authenticates to no remote server, such as a guest.
|
||||||
Guests leave this unset because they do not authenticate to remote servers.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (
|
config = mkIf cfg.enable (mkMerge [
|
||||||
lib.mkMerge [
|
{
|
||||||
{
|
services.openssh.enable = true;
|
||||||
services.openssh.enable = true;
|
|
||||||
|
|
||||||
users.users.${user}.openssh.authorizedKeys.keys = defaultAuthorizedKeys;
|
users.users.${user}.openssh.authorizedKeys.keys =
|
||||||
|
if cfg.authorizedKeys != null then
|
||||||
|
cfg.authorizedKeys
|
||||||
|
else
|
||||||
|
defaultAuthorizedKeys ++ cfg.extraAuthorizedKeys;
|
||||||
|
|
||||||
programs.ssh.knownHosts = knownHosts;
|
programs.ssh.knownHosts = knownHosts;
|
||||||
|
|
||||||
home-manager.users.${user}.programs.ssh = {
|
home-manager.users.${user}.programs.ssh = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableDefaultConfig = false;
|
enableDefaultConfig = false;
|
||||||
settings = lib.mapAttrs (name: _: sshSettingsFor name) (
|
settings =
|
||||||
lib.genAttrs outboundTargetNames (name: name)
|
mapAttrs (name: _: sshSettingsFor name) (genAttrs outboundTargetNames (name: name))
|
||||||
);
|
// cfg.extraSettings;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
(lib.mkIf cfg.hostKeys.restore {
|
(mkIf cfg.hostKeys.restore {
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.hostKeys.sopsFile != null;
|
assertion = cfg.hostKeys.sopsFile != null;
|
||||||
message = "Set `modules.network.ssh.hostKeys.sopsFile` when host-key restoration is enabled.";
|
message = "modules.network.ssh.hostKeys.restore requires modules.network.ssh.hostKeys.sopsFile to name the encrypted host keys.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets = lib.genAttrs (map hostKeySecret hostKeyTypes) (_: {
|
sops.secrets = genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
||||||
inherit (cfg.hostKeys) sopsFile;
|
inherit (cfg.hostKeys) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
restartUnits = [ "sshd.service" ];
|
restartUnits = [ "sshd.service" ];
|
||||||
});
|
});
|
||||||
|
|
||||||
services.openssh.hostKeys = [ ];
|
services.openssh.hostKeys = [ ];
|
||||||
services.openssh.extraConfig = lib.concatStringsSep "" (
|
services.openssh.extraConfig = concatStringsSep "" (
|
||||||
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") hostKeyTypes
|
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") cfg.hostKeys.types
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
(lib.mkIf (cfg.userKey.sopsFile != null) {
|
(mkIf (cfg.userKey.sopsFile != null) {
|
||||||
sops.secrets.${userKeySecret} = {
|
sops.secrets.${userKeySecret} = {
|
||||||
inherit (cfg.userKey) sopsFile;
|
inherit (cfg.userKey) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = user;
|
owner = user;
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${user}.programs.ssh.settings."*" = {
|
home-manager.users.${user}.programs.ssh.settings."*".IdentityFile =
|
||||||
IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
config.sops.secrets.${userKeySecret}.path;
|
||||||
IdentitiesOnly = true;
|
})
|
||||||
};
|
]);
|
||||||
})
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,24 @@
|
|||||||
util,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Configures bridges for tagged VLANs on a physical interface that uses DHCP.
|
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vlans;
|
cfg = config.modules.network.vlans;
|
||||||
|
|
||||||
|
inherit (util) bridgeName;
|
||||||
|
|
||||||
|
vlanName = id: "${cfg.trunk}.${toString id}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.vlans = {
|
options.modules.network.vlans = {
|
||||||
interface = lib.mkOption {
|
enable = lib.mkEnableOption "host VLAN bridges over a tagged trunk";
|
||||||
|
|
||||||
|
trunk = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "enp1s0";
|
example = "enp1s0";
|
||||||
description = ''
|
description = ''
|
||||||
Names the physical interface that carries untagged DHCP traffic and tagged VLANs.
|
Physical interface that carries 802.1Q-tagged traffic for every VLAN.
|
||||||
|
Each declared VLAN attaches to its own bridge.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,37 +34,67 @@ in
|
|||||||
30
|
30
|
||||||
];
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Creates bridges for the specified tagged VLAN IDs.
|
Tagged VLAN ids to materialize.
|
||||||
|
Each id N creates one bridge named `br-vlanN`.
|
||||||
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.ids != [ ]) {
|
config = lib.mkIf cfg.enable {
|
||||||
networking = {
|
assertions = [
|
||||||
useDHCP = false;
|
{
|
||||||
networkmanager.unmanaged = [
|
assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.ids;
|
||||||
"interface-name:${cfg.interface}"
|
message = "modules.network.vlans.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.ids.";
|
||||||
]
|
}
|
||||||
++ map (id: "interface-name:${util.bridgeName id}") cfg.ids
|
{
|
||||||
++ map (id: "interface-name:vlan${toString id}") cfg.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;
|
||||||
|
|
||||||
systemd.network = {
|
systemd.network = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
netdevs = lib.mkMerge (
|
netdevs = lib.mkMerge (
|
||||||
map (id: {
|
map (id: {
|
||||||
"40-${util.bridgeName id}" = {
|
"40-${bridgeName id}" = {
|
||||||
netdevConfig = {
|
netdevConfig = {
|
||||||
Name = util.bridgeName id;
|
Name = bridgeName id;
|
||||||
Kind = "bridge";
|
Kind = "bridge";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"40-vlan${toString id}" = {
|
"40-${vlanName id}" = {
|
||||||
netdevConfig = {
|
netdevConfig = {
|
||||||
Name = "vlan${toString id}";
|
Name = vlanName id;
|
||||||
Kind = "vlan";
|
Kind = "vlan";
|
||||||
};
|
};
|
||||||
vlanConfig.Id = id;
|
vlanConfig.Id = id;
|
||||||
@@ -67,25 +104,37 @@ in
|
|||||||
|
|
||||||
networks = lib.mkMerge (
|
networks = lib.mkMerge (
|
||||||
[
|
[
|
||||||
|
# The trunk carries only tagged frames and takes no address.
|
||||||
{
|
{
|
||||||
"10-${cfg.interface}" = {
|
"30-${cfg.trunk}" = {
|
||||||
matchConfig.Name = cfg.interface;
|
matchConfig.Name = cfg.trunk;
|
||||||
networkConfig.DHCP = "yes";
|
networkConfig.LinkLocalAddressing = "no";
|
||||||
vlan = map (id: "vlan${toString id}") cfg.ids;
|
linkConfig.RequiredForOnline = "no";
|
||||||
linkConfig.RequiredForOnline = "routable";
|
vlan = map vlanName cfg.ids;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
++ map (id: {
|
++ map (id: {
|
||||||
"40-vlan${toString id}" = {
|
# Enslaved to its bridge, carrying no address itself.
|
||||||
matchConfig.Name = "vlan${toString id}";
|
"40-${vlanName id}" = {
|
||||||
networkConfig.Bridge = util.bridgeName id;
|
matchConfig.Name = vlanName id;
|
||||||
linkConfig.RequiredForOnline = "no";
|
networkConfig.Bridge = bridgeName id;
|
||||||
};
|
|
||||||
"40-${util.bridgeName id}" = {
|
|
||||||
matchConfig.Name = util.bridgeName id;
|
|
||||||
linkConfig.RequiredForOnline = "no";
|
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
|
}) cfg.ids
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +1,10 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vpn-client;
|
cfg = config.modules.network.vpn-client;
|
||||||
|
|
||||||
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
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.vpn-client = {
|
options.modules.network.vpn-client = {
|
||||||
@@ -37,31 +13,61 @@ in
|
|||||||
interface = lib.mkOption {
|
interface = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "wg-home";
|
default = "wg-home";
|
||||||
description = "Names the WireGuard interface.";
|
description = "WireGuard interface name.";
|
||||||
};
|
};
|
||||||
|
|
||||||
address = lib.mkOption {
|
address = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "10.23.100.3/32";
|
example = "10.23.100.3/32";
|
||||||
description = "Sets the client tunnel address in CIDR notation.";
|
description = "Client tunnel address in CIDR notation.";
|
||||||
|
};
|
||||||
|
|
||||||
|
privateKeySecret = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "wireguard-home-private-key";
|
||||||
|
description = "SOPS secret containing the WireGuard client private key.";
|
||||||
|
};
|
||||||
|
|
||||||
|
autoStart = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to start the VPN tunnel during boot.";
|
||||||
|
};
|
||||||
|
|
||||||
|
mtu = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = 1280;
|
||||||
|
description = "Tunnel MTU, or null to use the WireGuard default.";
|
||||||
};
|
};
|
||||||
|
|
||||||
sopsFile = lib.mkOption {
|
sopsFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = "Names the encrypted file that contains the WireGuard private key.";
|
description = "Encrypted secrets file that contains privateKeySecret.";
|
||||||
};
|
};
|
||||||
|
|
||||||
peer = {
|
peer = {
|
||||||
publicKey = lib.mkOption {
|
publicKey = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Sets the WireGuard public key for the remote peer.";
|
description = "WireGuard public key for the remote peer.";
|
||||||
};
|
};
|
||||||
|
|
||||||
endpoint = lib.mkOption {
|
endpoint = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "203.0.113.10:51820";
|
example = "203.0.113.10:51820";
|
||||||
description = "Sets the WireGuard endpoint for the remote peer.";
|
description = "WireGuard endpoint for the remote peer.";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowedIPs = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ "10.23.0.0/16" ];
|
||||||
|
description = "Routes to send through the VPN tunnel.";
|
||||||
|
};
|
||||||
|
|
||||||
|
persistentKeepalive = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = 25;
|
||||||
|
description = "Persistent keepalive interval in seconds, or null to disable it.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -70,39 +76,30 @@ in
|
|||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.sopsFile != null;
|
assertion = cfg.sopsFile != null;
|
||||||
message = "Set modules.network.vpn-client.sopsFile when the VPN client is enabled.";
|
message = "modules.network.vpn-client.sopsFile must name the encrypted secrets file containing the WireGuard private key.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets.wireguard-home-private-key.sopsFile = cfg.sopsFile;
|
sops.secrets.${cfg.privateKeySecret}.sopsFile = cfg.sopsFile;
|
||||||
|
|
||||||
registry.shell-shortcut.${config.networking.hostName} = {
|
systemd.targets."wireguard-${cfg.interface}" = lib.mkIf (!cfg.autoStart) {
|
||||||
vpn-on = "sudo systemctl start wireguard-${cfg.interface}.target";
|
wantedBy = lib.mkForce [ ];
|
||||||
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} = {
|
networking.wireguard.interfaces.${cfg.interface} = {
|
||||||
ips = [ cfg.address ];
|
ips = [ cfg.address ];
|
||||||
mtu = 1280;
|
inherit (cfg) mtu;
|
||||||
privateKeyFile = config.sops.secrets.wireguard-home-private-key.path;
|
privateKeyFile = config.sops.secrets.${cfg.privateKeySecret}.path;
|
||||||
|
|
||||||
peers = [
|
peers = [
|
||||||
{
|
(
|
||||||
inherit (cfg.peer) publicKey endpoint;
|
{
|
||||||
allowedIPs = [ "10.23.0.0/16" ];
|
inherit (cfg.peer) publicKey endpoint allowedIPs;
|
||||||
persistentKeepalive = 25;
|
}
|
||||||
}
|
// lib.optionalAttrs (cfg.peer.persistentKeepalive != null) {
|
||||||
|
inherit (cfg.peer) persistentKeepalive;
|
||||||
|
}
|
||||||
|
)
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
62
configs/nixos/modules/services/actualbudget.nix
Normal file
62
configs/nixos/modules/services/actualbudget.nix
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.modules.services.actualbudget;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.services.actualbudget = {
|
||||||
|
enable = lib.mkEnableOption "Actual Budget";
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 5006;
|
||||||
|
description = "Port Actual Budget listens on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/data";
|
||||||
|
description = "Directory where Actual Budget stores server and user files.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "actual";
|
||||||
|
description = "User account that runs Actual Budget.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "storage";
|
||||||
|
description = "Group account that owns Actual Budget's writable state.";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkMerge [
|
||||||
|
{
|
||||||
|
registry."stable-uid".${config.networking.hostName}.actualbudget = 20001;
|
||||||
|
}
|
||||||
|
|
||||||
|
(lib.mkIf cfg.enable {
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = config.registry."stable-uid".${config.networking.hostName}.actualbudget;
|
||||||
|
inherit (cfg) group;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.actual = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
inherit (cfg) user;
|
||||||
|
inherit (cfg) group;
|
||||||
|
settings = {
|
||||||
|
inherit (cfg) port dataDir;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -2,135 +2,147 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.copyparty;
|
cfg = config.modules.services.copyparty;
|
||||||
|
|
||||||
accountName = config.user.name;
|
volumeType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Filesystem path served by this volume.";
|
||||||
|
};
|
||||||
|
|
||||||
serviceGroup = "storage";
|
access = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
A = [ "alexion" ];
|
||||||
|
};
|
||||||
|
description = "Copyparty access map from permission string to account names.";
|
||||||
|
};
|
||||||
|
|
||||||
serviceStateDir = "/var/lib/copyparty";
|
readOnly = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Serve this volume read-only.";
|
||||||
|
};
|
||||||
|
|
||||||
serviceUser = "copyparty";
|
flags = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes);
|
default = [ ];
|
||||||
|
example = [ "dthumb" ];
|
||||||
port = 3923;
|
description = "Copyparty volume flags.";
|
||||||
defaultAccess = {
|
};
|
||||||
A = [ accountName ];
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultWritableFlags = {
|
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
|
||||||
d2d = true;
|
|
||||||
d2t = true;
|
|
||||||
dthumb = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
credentialHash = builtins.substring 0 16 (builtins.hashString "sha256" accountName);
|
hasFlag =
|
||||||
|
name: flags: lib.any (flag: lib.hasPrefix "${name}:" flag || lib.hasPrefix "${name}=" flag) flags;
|
||||||
|
|
||||||
credentialName = "password-${credentialHash}";
|
defaultPermissionFlags =
|
||||||
|
|
||||||
passwordPlaceholder = "@COPYPARTY_PASSWORD_${credentialHash}@";
|
|
||||||
|
|
||||||
globalSettings = {
|
|
||||||
i = "0.0.0.0";
|
|
||||||
usernames = true;
|
|
||||||
"http-only" = true;
|
|
||||||
"no-crt" = true;
|
|
||||||
"no-thumb" = true;
|
|
||||||
"no-snap" = true;
|
|
||||||
"no-rescan" = true;
|
|
||||||
"ses-db" = "${serviceStateDir}/sessions.db";
|
|
||||||
"chpw-db" = "${serviceStateDir}/chpw.json";
|
|
||||||
"shr-db" = "${serviceStateDir}/shares.db";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs cfg.reverseProxy.enable {
|
|
||||||
rproxy = 1;
|
|
||||||
"xff-hdr" = "x-forwarded-for";
|
|
||||||
};
|
|
||||||
|
|
||||||
globalExtraConfig = lib.optionalString cfg.reverseProxy.enable (
|
|
||||||
lib.concatMapStringsSep "\n" (source: "xff-src: ${source}") cfg.reverseProxy.trustedSources
|
|
||||||
);
|
|
||||||
|
|
||||||
volumeFlags =
|
|
||||||
flags:
|
flags:
|
||||||
lib.optionalAttrs (flags != { }) (
|
lib.optionals (!(hasFlag "chmod_d" flags)) [ "chmod_d: 2770" ]
|
||||||
{
|
++ lib.optionals (!(hasFlag "chmod_f" flags)) [ "chmod_f: 660" ];
|
||||||
chmod_d = "2770";
|
|
||||||
chmod_f = "660";
|
|
||||||
}
|
|
||||||
// flags
|
|
||||||
);
|
|
||||||
|
|
||||||
renderedVolumes = lib.mapAttrs (_urlPath: volume: {
|
renderFlags =
|
||||||
inherit (volume) path;
|
flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") (flags ++ defaultPermissionFlags flags);
|
||||||
access = if volume.readOnly then { r = [ accountName ]; } else defaultAccess // volume.access;
|
|
||||||
flags = volumeFlags (lib.optionalAttrs (!volume.readOnly) defaultWritableFlags // volume.flags);
|
|
||||||
}) cfg.volumes;
|
|
||||||
|
|
||||||
configText = util.formats.copyparty.render {
|
renderVolume = urlPath: volume: ''
|
||||||
inherit globalExtraConfig;
|
[${urlPath}]
|
||||||
accounts.${accountName} = passwordPlaceholder;
|
${volume.path}
|
||||||
global = globalSettings;
|
accs:
|
||||||
volumes = renderedVolumes;
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)}
|
||||||
};
|
${lib.optionalString (volume.flags != [ ]) ''
|
||||||
|
flags:
|
||||||
|
${renderFlags volume.flags}
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
|
||||||
configFile = pkgs.writeText "copyparty.conf" configText;
|
renderTrustedSource = source: " xff-src: ${source}";
|
||||||
|
|
||||||
replacePassword = ''
|
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
|
||||||
${lib.getExe pkgs.replace-secret} ${lib.escapeShellArg passwordPlaceholder} "$CREDENTIALS_DIRECTORY/${credentialName}" /run/copyparty/copyparty.conf
|
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
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")"
|
||||||
|
|
||||||
|
cat > /run/copyparty/copyparty.conf <<EOF
|
||||||
|
[global]
|
||||||
|
p: ${toString cfg.port}
|
||||||
|
usernames
|
||||||
|
http-only
|
||||||
|
no-crt
|
||||||
|
no-thumb
|
||||||
|
no-snap
|
||||||
|
no-rescan
|
||||||
|
ses-db: ${cfg.stateDir}/sessions.db
|
||||||
|
chpw-db: ${cfg.stateDir}/chpw.json
|
||||||
|
shr-db: ${cfg.stateDir}/shares.db
|
||||||
|
${renderedReverseProxy}
|
||||||
|
[accounts]
|
||||||
|
${cfg.accountName}: $password
|
||||||
|
|
||||||
|
${renderedVolumes}
|
||||||
|
EOF
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.copyparty = {
|
options.modules.services.copyparty = {
|
||||||
enable = lib.mkEnableOption "Copyparty";
|
enable = lib.mkEnableOption "Copyparty";
|
||||||
|
|
||||||
passwordFile = lib.mkOption {
|
package = lib.mkPackageOption pkgs "copyparty-min" { };
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 3923;
|
||||||
|
description = "Port Copyparty listens on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Sets the runtime file that contains the Copyparty account password.";
|
default = "copyparty";
|
||||||
|
description = "User account that runs Copyparty.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "storage";
|
||||||
|
description = "Group account that owns Copyparty's writable state and served files.";
|
||||||
|
};
|
||||||
|
|
||||||
|
accountName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "alexion";
|
||||||
|
description = "Copyparty account name configured for password authentication.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Runtime file containing the Copyparty account password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/var/lib/copyparty";
|
||||||
|
description = "Directory where Copyparty stores service state.";
|
||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf volumeType;
|
||||||
lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.externalPath;
|
|
||||||
description = "Sets the filesystem path served by this volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
access = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
A = [ "alexion" ];
|
|
||||||
};
|
|
||||||
description = "Maps Copyparty permission strings to account names.";
|
|
||||||
};
|
|
||||||
|
|
||||||
readOnly = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Serves this volume read-only.";
|
|
||||||
};
|
|
||||||
|
|
||||||
flags = lib.mkOption {
|
|
||||||
type = lib.types.attrs;
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
dthumb = true;
|
|
||||||
};
|
|
||||||
description = "Sets Copyparty volume flags.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default = { };
|
default = { };
|
||||||
description = "Defines Copyparty volumes by URL path.";
|
description = "Copyparty volumes keyed by their URL path.";
|
||||||
};
|
};
|
||||||
|
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
@@ -139,7 +151,7 @@ in
|
|||||||
trustedSources = lib.mkOption {
|
trustedSources = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = "Lists proxy sources that can set Copyparty's forwarded client IP header.";
|
description = "IP ranges that may supply Copyparty's forwarded client IP header.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -161,49 +173,46 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
users.users.${serviceUser} = {
|
users.users.${cfg.user} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
group = serviceGroup;
|
inherit (cfg) group;
|
||||||
home = serviceStateDir;
|
home = cfg.stateDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.copyparty = {
|
systemd.services.copyparty = {
|
||||||
description = "Copyparty file server";
|
description = "Copyparty file server";
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
preStart = ''
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -m 600 ${configFile} /run/copyparty/copyparty.conf
|
|
||||||
${replacePassword}
|
|
||||||
'';
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ReloadSignal = "SIGUSR1";
|
ReloadSignal = "SIGUSR1";
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
User = serviceUser;
|
User = cfg.user;
|
||||||
Group = serviceGroup;
|
Group = cfg.group;
|
||||||
WorkingDirectory = serviceStateDir;
|
WorkingDirectory = cfg.stateDir;
|
||||||
Environment = [
|
Environment = [
|
||||||
"HOME=${serviceStateDir}"
|
"HOME=${cfg.stateDir}"
|
||||||
"XDG_CONFIG_HOME=${serviceStateDir}"
|
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
||||||
];
|
];
|
||||||
StateDirectory = "copyparty";
|
StateDirectory = "copyparty";
|
||||||
RuntimeDirectory = "copyparty";
|
RuntimeDirectory = "copyparty";
|
||||||
RuntimeDirectoryMode = "0700";
|
RuntimeDirectoryMode = "0700";
|
||||||
LoadCredential = [ "${credentialName}:${cfg.passwordFile}" ];
|
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
||||||
ExecStart = "${lib.getExe pkgs.copyparty-min} -c /run/copyparty/copyparty.conf";
|
ExecStartPre = configScript;
|
||||||
|
ExecStart = "${lib.getExe cfg.package} -c /run/copyparty/copyparty.conf";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
ProtectHome = true;
|
ProtectHome = true;
|
||||||
ReadWritePaths = [
|
ReadWritePaths = [
|
||||||
serviceStateDir
|
cfg.stateDir
|
||||||
]
|
]
|
||||||
++ map (volume: volume.path) writableVolumes;
|
++ map (volume: volume.path) (lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ port ];
|
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,33 +92,33 @@
|
|||||||
startScript = pkgs.writeShellScript "palworld-start" ''
|
startScript = pkgs.writeShellScript "palworld-start" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -d \
|
${pkgs.coreutils}/bin/install -d \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
${lib.escapeShellArg service.stateDir} \
|
${lib.escapeShellArg service.stateDir} \
|
||||||
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
||||||
|
|
||||||
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
current_target="$(${lib.getExe' pkgs.coreutils "readlink"} ${lib.escapeShellArg savedDir})"
|
current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})"
|
||||||
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
||||||
${lib.getExe' pkgs.coreutils "rm"} ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
if ${lib.getExe' pkgs.findutils "find"} ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${lib.getExe' pkgs.gnugrep "grep"} -q .; then
|
if ${pkgs.findutils}/bin/find ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${pkgs.gnugrep}/bin/grep -q .; then
|
||||||
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
||||||
else
|
else
|
||||||
${lib.getExe' pkgs.coreutils "cp"} -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
||||||
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
||||||
${lib.getExe' pkgs.coreutils "ln"} -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
admin_password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
||||||
|
|
||||||
exec ./PalServer.sh \
|
exec ./PalServer.sh \
|
||||||
"$@" \
|
"$@" \
|
||||||
@@ -145,12 +145,9 @@
|
|||||||
]
|
]
|
||||||
++ lib.optional service.publicLobby "-publiclobby"
|
++ lib.optional service.publicLobby "-publiclobby"
|
||||||
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
||||||
++ lib.optionals service.rcon.enable [
|
++ lib.optional service.rcon.enable "-rcon";
|
||||||
"-rcon"
|
|
||||||
"-RCONPort=${toString service.rcon.port}"
|
|
||||||
];
|
|
||||||
udpPorts = [ service.port ];
|
udpPorts = [ service.port ];
|
||||||
tcpPorts = lib.optional service.rcon.enable service.rcon.port;
|
tcpPorts = [ service.port ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
startScript = pkgs.writeShellScript "valheim-start" ''
|
startScript = pkgs.writeShellScript "valheim-start" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
exec ./valheim_server.x86_64 "$@" -password "$password"
|
exec ./valheim_server.x86_64 "$@" -password "$password"
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -7,29 +7,42 @@
|
|||||||
let
|
let
|
||||||
cfg = config.modules.services.homepage;
|
cfg = config.modules.services.homepage;
|
||||||
|
|
||||||
derivedAllowedHosts = lib.unique (
|
mkRegistryPath =
|
||||||
lib.optional (selfRoute != null) selfRoute.domain
|
registryName: host: entry:
|
||||||
++ lib.optionals (selfAddress != null) [
|
"registry.${registryName}.${host}.${entry}";
|
||||||
selfAddress
|
|
||||||
"${selfAddress}:${toString cfg.listenPort}"
|
homepageEntries = lib.concatLists (
|
||||||
]
|
lib.mapAttrsToList (
|
||||||
|
host: entries:
|
||||||
|
lib.mapAttrsToList (entry: homepage: {
|
||||||
|
inherit host entry homepage;
|
||||||
|
composite = registry.join host entry;
|
||||||
|
}) entries
|
||||||
|
) registry.homepage
|
||||||
);
|
);
|
||||||
selfRoute = selfEntry.route or null;
|
|
||||||
|
|
||||||
selfEntry = registry.join cfg.hostName cfg.entryName;
|
mkHref =
|
||||||
selfAddress = selfEntry.address or null;
|
item:
|
||||||
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
|
if item.homepage.href != null then
|
||||||
|
item.homepage.href
|
||||||
|
else if (item.composite.route or null) != null then
|
||||||
|
if item.composite.route.port != null then
|
||||||
|
"https://${item.composite.route.domain}"
|
||||||
|
else if item.composite.route.http.port != null then
|
||||||
|
"http://${item.composite.route.domain}"
|
||||||
|
else
|
||||||
|
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching TCP route entry"
|
||||||
|
else
|
||||||
|
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching route entry";
|
||||||
|
|
||||||
mkTab =
|
mkServiceAttrs =
|
||||||
tab:
|
item:
|
||||||
let
|
lib.filterAttrs (_: value: value != null) {
|
||||||
sections = entriesByTab.${tab};
|
href = mkHref item;
|
||||||
in
|
inherit (item.homepage) description icon;
|
||||||
{
|
|
||||||
${tab} = map (section: { ${section} = sections.${section}; }) (orderedSections tab sections);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
entriesByTab = lib.foldl' insertEntry { } sortedEntries;
|
sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries;
|
||||||
|
|
||||||
insertEntry =
|
insertEntry =
|
||||||
acc: item:
|
acc: item:
|
||||||
@@ -42,19 +55,7 @@ let
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries;
|
entriesByTab = lib.foldl' insertEntry { } sortedEntries;
|
||||||
|
|
||||||
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 = [
|
tabOrder = [
|
||||||
"Apps"
|
"Apps"
|
||||||
@@ -62,30 +63,7 @@ let
|
|||||||
"System"
|
"System"
|
||||||
];
|
];
|
||||||
|
|
||||||
mkRegistryPath =
|
orderedTabs = tabOrder ++ lib.filter (tab: !lib.elem tab tabOrder) (lib.attrNames entriesByTab);
|
||||||
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 = {
|
sectionOrderByTab = {
|
||||||
Apps = [
|
Apps = [
|
||||||
@@ -111,9 +89,30 @@ let
|
|||||||
let
|
let
|
||||||
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
||||||
in
|
in
|
||||||
lib.filter (section: lib.hasAttr section sections) sectionOrder
|
sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
||||||
++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
|
||||||
|
|
||||||
|
mkTab =
|
||||||
|
tab:
|
||||||
|
let
|
||||||
|
sections = entriesByTab.${tab};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
${tab} = map (section: { ${section} = sections.${section}; }) (orderedSections tab sections);
|
||||||
|
};
|
||||||
|
|
||||||
|
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
|
||||||
|
|
||||||
|
selfEntry = registry.join cfg.hostName cfg.entryName;
|
||||||
|
selfAddress = selfEntry.address or null;
|
||||||
|
selfRoute = selfEntry.route or null;
|
||||||
|
|
||||||
|
derivedAllowedHosts = lib.unique (
|
||||||
|
lib.optional (selfRoute != null) selfRoute.domain
|
||||||
|
++ lib.optionals (selfAddress != null) [
|
||||||
|
selfAddress
|
||||||
|
"${selfAddress}:${toString cfg.listenPort}"
|
||||||
|
]
|
||||||
|
);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.homepage = {
|
options.modules.services.homepage = {
|
||||||
|
|||||||
@@ -5,25 +5,93 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.services.reverse-proxy;
|
||||||
|
|
||||||
generatedUdpStreams = lib.concatLists (
|
mkRegistryPath =
|
||||||
|
registryName: host: entry:
|
||||||
|
"registry.${registryName}.${host}.${entry}";
|
||||||
|
|
||||||
|
mkBackend = address: port: "http://${address}:${toString port}";
|
||||||
|
|
||||||
|
mkVirtualHost =
|
||||||
|
host: entry: mode:
|
||||||
|
let
|
||||||
|
compositeEntry = registry.join host entry;
|
||||||
|
inherit (compositeEntry) route;
|
||||||
|
address =
|
||||||
|
compositeEntry.address
|
||||||
|
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||||
|
isHttp = mode == "http";
|
||||||
|
port = if isHttp then route.http.port else route.port;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit host entry;
|
||||||
|
registryName = "route";
|
||||||
|
name = route.domain;
|
||||||
|
value = {
|
||||||
|
forceSSL = !isHttp;
|
||||||
|
enableACME = !isHttp;
|
||||||
|
locations."/".proxyPass = mkBackend address port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
generatedHttpsVirtualHostList = lib.concatLists (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
host: routes:
|
host: routes:
|
||||||
lib.concatLists (
|
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") (
|
||||||
lib.mapAttrsToList (
|
lib.filterAttrs (_entry: route: route.port != null) routes
|
||||||
entry: route: lib.optionals (route.protocol == "udp") (map (mkUdpStream host entry) route.ports)
|
|
||||||
) routes
|
|
||||||
)
|
)
|
||||||
) registry.route
|
) registry.route
|
||||||
);
|
);
|
||||||
|
|
||||||
|
generatedHttpVirtualHostList = lib.concatLists (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
host: routes:
|
||||||
|
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "http") (
|
||||||
|
lib.filterAttrs (_entry: route: route.http.port != null) routes
|
||||||
|
)
|
||||||
|
) registry.route
|
||||||
|
);
|
||||||
|
|
||||||
|
generatedVirtualHostList = generatedHttpsVirtualHostList ++ generatedHttpVirtualHostList;
|
||||||
|
|
||||||
|
virtualHostsByDomain = lib.foldl' (
|
||||||
|
acc: virtualHost:
|
||||||
|
acc
|
||||||
|
// {
|
||||||
|
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
|
||||||
|
}
|
||||||
|
) { } 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
|
||||||
|
);
|
||||||
|
|
||||||
|
generatedVirtualHosts =
|
||||||
|
if duplicateDomainMessages != [ ] then
|
||||||
|
throw (builtins.head duplicateDomainMessages)
|
||||||
|
else
|
||||||
|
lib.listToAttrs generatedVirtualHostList;
|
||||||
|
|
||||||
mkUdpStream =
|
mkUdpStream =
|
||||||
host: entry: port:
|
host: entry: port:
|
||||||
let
|
let
|
||||||
|
compositeEntry = registry.join host entry;
|
||||||
address =
|
address =
|
||||||
compositeEntry.address
|
compositeEntry.address
|
||||||
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||||
compositeEntry = registry.join host entry;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit
|
inherit
|
||||||
@@ -34,60 +102,28 @@ let
|
|||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
generatedUdpStreams = lib.concatLists (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
host: routes:
|
||||||
|
lib.concatLists (
|
||||||
|
lib.mapAttrsToList (entry: route: map (mkUdpStream host entry) route.udp.ports) routes
|
||||||
|
)
|
||||||
|
) registry.route
|
||||||
|
);
|
||||||
|
|
||||||
generatedStreamConfig = lib.concatMapStringsSep "\n" (stream: ''
|
generatedStreamConfig = lib.concatMapStringsSep "\n" (stream: ''
|
||||||
server {
|
server {
|
||||||
listen ${toString stream.port} udp;
|
listen ${toString stream.port} udp;
|
||||||
proxy_pass ${stream.address}:${toString stream.port};
|
proxy_pass ${stream.address}:${toString stream.port};
|
||||||
}
|
}
|
||||||
'') generatedUdpStreams;
|
'') generatedUdpStreams;
|
||||||
generatedVirtualHosts = lib.listToAttrs generatedVirtualHostList;
|
|
||||||
|
|
||||||
# Route registry validation already requires unique domains.
|
|
||||||
generatedVirtualHostList = lib.concatLists (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
host: routes:
|
|
||||||
lib.mapAttrsToList (entry: route: mkVirtualHost host entry route.protocol) (
|
|
||||||
lib.filterAttrs (_entry: route: route.protocol != "udp") routes
|
|
||||||
)
|
|
||||||
) registry.route
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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}";
|
|
||||||
|
|
||||||
mkRegistryPath =
|
|
||||||
registryName: host: entry:
|
|
||||||
"registry.${registryName}.${host}.${entry}";
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.reverse-proxy = {
|
options.modules.services.reverse-proxy = {
|
||||||
enable = lib.mkEnableOption "the public edge reverse proxy";
|
enable = lib.mkEnableOption "the public edge reverse proxy";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf config.modules.services.reverse-proxy.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
networking.firewall.allowedTCPPorts = [
|
networking.firewall.allowedTCPPorts = [
|
||||||
80
|
80
|
||||||
443
|
443
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
serviceName = lib.mkOption {
|
serviceName = lib.mkOption {
|
||||||
type = lib.types.strMatching "[a-z0-9][a-z0-9-]*";
|
type = lib.types.str;
|
||||||
example = "valheim";
|
example = "valheim";
|
||||||
description = "Lowercase systemd service basename for the game server.";
|
description = "Name of the systemd service that runs the game server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
appId = lib.mkOption {
|
appId = lib.mkOption {
|
||||||
@@ -80,14 +80,14 @@
|
|||||||
|
|
||||||
(lib.mkIf config.modules.services.steamcmd.enable (
|
(lib.mkIf config.modules.services.steamcmd.enable (
|
||||||
let
|
let
|
||||||
group = "storage";
|
|
||||||
|
|
||||||
service = config.modules.services.steamcmd;
|
service = config.modules.services.steamcmd;
|
||||||
user = "steamcmd";
|
user = "steamcmd";
|
||||||
|
group = "storage";
|
||||||
|
|
||||||
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -d \
|
${pkgs.coreutils}/bin/install -d \
|
||||||
-o ${user} \
|
-o ${user} \
|
||||||
-g ${group} \
|
-g ${group} \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
|
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
ExecStart = "${lib.getExe' pkgs.systemd "systemctl"} --no-block start ${service.serviceName}.service";
|
ExecStart = "${pkgs.systemd}/bin/systemctl --no-block start ${service.serviceName}.service";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,15 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Bazel for the primary user's shell environment.
|
# Bazel for the primary user's shell environment.
|
||||||
|
let
|
||||||
|
cfg = config.modules.shell.bazel;
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.bazel.enable =
|
options.modules.shell.bazel.enable =
|
||||||
lib.mkEnableOption "Bazel for the primary user's shell environment";
|
lib.mkEnableOption "Bazel for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.bazel.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name}.home.packages = [ pkgs.bazel ];
|
home-manager.users.${user}.home.packages = [ pkgs.bazel ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
26
configs/nixos/modules/shell/direnv/default.nix
Normal file
26
configs/nixos/modules/shell/direnv/default.nix
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# 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 {
|
||||||
|
home-manager.users.${user}.programs.direnv = {
|
||||||
|
enable = true;
|
||||||
|
silent = true;
|
||||||
|
|
||||||
|
# nix-direnv caches the evaluated shell so re-entering a directory is instant
|
||||||
|
# instead of re-running `nix develop`, and adds the `use flake` stdlib helper.
|
||||||
|
nix-direnv.enable = true;
|
||||||
|
stdlib = builtins.readFile ./repo-flake.sh;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
33
configs/nixos/modules/shell/direnv/repo-flake.sh
Normal file
33
configs/nixos/modules/shell/direnv/repo-flake.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
if declare -f use_flake >/dev/null; then
|
||||||
|
__dotfiles_use_flake_definition="$(declare -f use_flake)"
|
||||||
|
eval "${__dotfiles_use_flake_definition/use_flake/__dotfiles_nix_direnv_use_flake}"
|
||||||
|
unset __dotfiles_use_flake_definition
|
||||||
|
|
||||||
|
use_flake() {
|
||||||
|
local flake_expr="${1:-.}"
|
||||||
|
|
||||||
|
if [[ "$flake_expr" == //* ]]; then
|
||||||
|
local git_root
|
||||||
|
if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
||||||
|
log_error 'use flake // requires a Git repository'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local flake_suffix="${flake_expr#//}"
|
||||||
|
if [[ -n "$flake_suffix" && "$flake_suffix" != \#* && "$flake_suffix" != /* ]]; then
|
||||||
|
flake_expr="path:${git_root}/${flake_suffix}"
|
||||||
|
else
|
||||||
|
flake_expr="path:${git_root}${flake_suffix}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
__dotfiles_nix_direnv_use_flake "$flake_expr" "$@"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
__dotfiles_nix_direnv_use_flake "$@"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
log_error 'nix-direnv must define use_flake before the // wrapper loads'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
@@ -7,9 +7,13 @@
|
|||||||
# fish for the primary user, configured through home-manager.
|
# fish for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.fish;
|
cfg = config.modules.shell.fish;
|
||||||
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
|
||||||
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
||||||
|
envVars = lib.attrByPath [ config.networking.hostName ] { } config.registry.env-var;
|
||||||
|
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
||||||
|
exportEnvVar = name: value: ''
|
||||||
|
set -gx ${name} ${lib.escapeShellArg value}
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.fish = {
|
options.modules.shell.fish = {
|
||||||
@@ -102,7 +106,9 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
# Rendered by home-manager into ~/.config/fish/config.fish.
|
# Rendered by home-manager into ~/.config/fish/config.fish.
|
||||||
interactiveShellInit = builtins.readFile ./config.fish;
|
interactiveShellInit = lib.concatStringsSep "\n" (
|
||||||
|
(lib.mapAttrsToList exportEnvVar envVars) ++ [ (builtins.readFile ./config.fish) ]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,13 +80,6 @@ else
|
|||||||
echo "deploy-to: $host already has the full closure for $system_path" >&2
|
echo "deploy-to: $host already has the full closure for $system_path" >&2
|
||||||
end
|
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
|
echo "deploy-to: activating $system_path on $host" >&2
|
||||||
ssh -t $host sudo "$system_path/bin/switch-to-configuration" switch
|
ssh -t $host sudo "$system_path/bin/switch-to-configuration" switch
|
||||||
or begin
|
or begin
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
}:
|
}:
|
||||||
# git for the primary user, configured through home-manager.
|
# git for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.git;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.git.enable =
|
options.modules.shell.git.enable =
|
||||||
lib.mkEnableOption "git for the primary user, carrying the operator's commit identity";
|
lib.mkEnableOption "git for the primary user, carrying the operator's commit identity";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.git.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.programs.git = {
|
home-manager.users.${user}.programs.git = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
}:
|
}:
|
||||||
# gitea-axi for the primary user, installed through its own home-manager module.
|
# gitea-axi for the primary user, installed through its own home-manager module.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.gitea-axi;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.gitea-axi.enable =
|
options.modules.shell.gitea-axi.enable =
|
||||||
lib.mkEnableOption "gitea-axi, an agent-ergonomic CLI for Gitea issues and pull requests";
|
lib.mkEnableOption "gitea-axi, an agent-ergonomic CLI for Gitea issues and pull requests";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.gitea-axi.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.sharedModules = [ inputs.gitea-axi.homeModules.default ];
|
home-manager.sharedModules = [ inputs.gitea-axi.homeModules.default ];
|
||||||
home-manager.users.${user}.programs.gitea-axi.enable = true;
|
home-manager.users.${user}.programs.gitea-axi.enable = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
}:
|
}:
|
||||||
# Herdr, a terminal multiplexer for coding agents.
|
# Herdr, a terminal multiplexer for coding agents.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.herdr;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.herdr.enable =
|
options.modules.shell.herdr.enable =
|
||||||
lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
|
lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.herdr.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";
|
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";
|
||||||
|
|
||||||
home-manager.users.${user} = lib.mkMerge [
|
home-manager.users.${user} = lib.mkMerge [
|
||||||
|
|||||||
@@ -7,13 +7,14 @@
|
|||||||
}:
|
}:
|
||||||
# Neovim for the primary user, configured declaratively through nixvim.
|
# Neovim for the primary user, configured declaratively through nixvim.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.nvim;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.nvim.enable =
|
options.modules.shell.nvim.enable =
|
||||||
lib.mkEnableOption "Neovim, configured declaratively via nixvim";
|
lib.mkEnableOption "Neovim, configured declaratively via nixvim";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.nvim.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName} = {
|
registry.shell-shortcut.${config.networking.hostName} = {
|
||||||
vi = "nvim";
|
vi = "nvim";
|
||||||
vim = "nvim";
|
vim = "nvim";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Oh My Pi, installed alongside upstream Pi as the `omp` command.
|
# Oh My Pi, installed alongside upstream Pi as the `omp` command.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.omp;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -13,7 +14,7 @@ in
|
|||||||
enable = lib.mkEnableOption "Oh My Pi, a batteries-included fork of Pi";
|
enable = lib.mkEnableOption "Oh My Pi, a batteries-included fork of Pi";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.omp.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.omp ];
|
home-manager.users.${user}.home.packages = [ pkgs.omp ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,24 @@
|
|||||||
# home-manager, which ships the package and manages global settings.
|
# home-manager, which ships the package and manages global settings.
|
||||||
# The login credential is left unmanaged, so it survives rebuilds.
|
# The login credential is left unmanaged, so it survives rebuilds.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.pi;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
||||||
|
vendoredPi = pkgs.pi-coding-agent.overrideAttrs {
|
||||||
|
src = ../../../../../third_party/pi;
|
||||||
|
patches = [ ];
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.pi.enable =
|
options.modules.shell.pi = {
|
||||||
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 config.modules.shell.pi.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
programs.pi-coding-agent = {
|
programs.pi-coding-agent = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.alexion.pi-coding-agent;
|
package = vendoredPi;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
defaultProvider = "openai-codex";
|
defaultProvider = "openai-codex";
|
||||||
|
|||||||
@@ -6,14 +6,15 @@
|
|||||||
}:
|
}:
|
||||||
# Python tools for the primary user's shell environment.
|
# Python tools for the primary user's shell environment.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.python;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.python.enable =
|
options.modules.shell.python.enable =
|
||||||
lib.mkEnableOption "Python tools for the primary user's shell environment";
|
lib.mkEnableOption "Python tools for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.python.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.env.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
registry.env-var.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
||||||
|
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
|
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
}:
|
}:
|
||||||
# Rust tools for the primary user's shell environment.
|
# Rust tools for the primary user's shell environment.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.rust;
|
||||||
|
user = config.user.name;
|
||||||
toolchain = pkgs.symlinkJoin {
|
toolchain = pkgs.symlinkJoin {
|
||||||
name = "rust-toolchain";
|
name = "rust-toolchain";
|
||||||
paths = with pkgs; [
|
paths = with pkgs; [
|
||||||
@@ -16,14 +18,13 @@ let
|
|||||||
rustfmt
|
rustfmt
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
user = config.user.name;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.rust.enable =
|
options.modules.shell.rust.enable =
|
||||||
lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.rust.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.env.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
registry.env-var.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
home.packages = [
|
home.packages = [
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
# tmux for the primary user, configured through home-manager.
|
# tmux for the primary user, configured through home-manager.
|
||||||
# Settings without a home-manager option are read from ./extra.conf.
|
# Settings without a home-manager option are read from ./extra.conf.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.tmux;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager";
|
options.modules.shell.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.tmux.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName}.tmx = "tmux new-session -A -s";
|
registry.shell-shortcut.${config.networking.hostName}.tmx = "tmux new-session -A -s";
|
||||||
|
|
||||||
home-manager.users.${user}.programs.tmux = {
|
home-manager.users.${user}.programs.tmux = {
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
}:
|
}:
|
||||||
# Yazi, a terminal file manager.
|
# Yazi, a terminal file manager.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.yazi;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.yazi.enable = lib.mkEnableOption "Yazi, a terminal file manager";
|
options.modules.shell.yazi.enable = lib.mkEnableOption "Yazi, a terminal file manager";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.yazi.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.programs.yazi = {
|
home-manager.users.${user}.programs.yazi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableFishIntegration = true;
|
enableFishIntegration = true;
|
||||||
|
|||||||
72
configs/nixos/modules/zfs.nix
Normal file
72
configs/nixos/modules/zfs.nix
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{ lib, util, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = util.registry.predicates.uniqueValue (entry: entry.value);
|
assertion = lib.registry.predicates.uniqueValue (entry: entry.value);
|
||||||
message = "Address registry values must be unique.";
|
message = "Address registry values must be unique.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,40 +5,40 @@
|
|||||||
tab = lib.mkOption {
|
tab = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "Apps";
|
example = "Apps";
|
||||||
description = "Groups the entry in a Homepage tab.";
|
description = "Homepage tab that contains this entry.";
|
||||||
};
|
};
|
||||||
|
|
||||||
section = lib.mkOption {
|
section = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "Media";
|
example = "Media";
|
||||||
description = "Groups the entry in a Homepage section.";
|
description = "Homepage section that contains this entry.";
|
||||||
};
|
};
|
||||||
|
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "Navidrome";
|
example = "Navidrome";
|
||||||
description = "Sets the display name in Homepage.";
|
description = "Homepage display name for this entry.";
|
||||||
};
|
};
|
||||||
|
|
||||||
description = lib.mkOption {
|
description = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "Music";
|
example = "Music";
|
||||||
description = "Sets the description in Homepage.";
|
description = "Homepage description for this entry.";
|
||||||
};
|
};
|
||||||
|
|
||||||
icon = lib.mkOption {
|
icon = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "navidrome.png";
|
example = "navidrome.png";
|
||||||
description = "Sets the icon in Homepage.";
|
description = "Homepage icon for this entry.";
|
||||||
};
|
};
|
||||||
|
|
||||||
href = lib.mkOption {
|
href = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = "https://music.alexion.dev";
|
example = "https://music.alexion.dev";
|
||||||
description = "Overrides the generated Homepage link.";
|
description = "Homepage link override for this entry.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
{ lib, util, ... }:
|
|
||||||
{
|
|
||||||
checks = [
|
|
||||||
{
|
|
||||||
assertion = util.registry.predicates.uniquePerHost (entry: entry.value.keys);
|
|
||||||
message = "Desktop keybinds must use unique key chords per host.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
entries:
|
|
||||||
lib.all (
|
|
||||||
entry:
|
|
||||||
(entry.value.action.kind == "exec" && entry.value.action.command != null)
|
|
||||||
|| (
|
|
||||||
builtins.elem entry.value.action.kind [
|
|
||||||
"focus"
|
|
||||||
"move-window"
|
|
||||||
]
|
|
||||||
&& lib.xor (entry.value.action.direction != null) (entry.value.action.workspace != null)
|
|
||||||
)
|
|
||||||
|| (
|
|
||||||
entry.value.action.kind == "resize-window"
|
|
||||||
&& entry.value.action.direction != null
|
|
||||||
&& entry.value.action.workspace == null
|
|
||||||
)
|
|
||||||
|| (
|
|
||||||
builtins.elem entry.value.action.kind [
|
|
||||||
"toggle-floating"
|
|
||||||
"toggle-fullscreen"
|
|
||||||
"close-window"
|
|
||||||
]
|
|
||||||
&& entry.value.action.command == null
|
|
||||||
&& entry.value.action.direction == null
|
|
||||||
&& entry.value.action.workspace == null
|
|
||||||
)
|
|
||||||
) entries;
|
|
||||||
message = "Desktop keybind actions must set the fields their kind requires.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
keys = lib.mkOption {
|
|
||||||
description = "Sets the key chord for this desktop keybind.";
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "SUPER + Return";
|
|
||||||
};
|
|
||||||
|
|
||||||
repeating = lib.mkOption {
|
|
||||||
description = "Repeats the keybind action while the key chord is held.";
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
locked = lib.mkOption {
|
|
||||||
description = "Allows the keybind action while the session is locked.";
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
action = lib.mkOption {
|
|
||||||
description = "Declares the action this keybind runs.";
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
kind = lib.mkOption {
|
|
||||||
description = "Selects the keybind action kind.";
|
|
||||||
type = lib.types.enum [
|
|
||||||
"exec"
|
|
||||||
"focus"
|
|
||||||
"move-window"
|
|
||||||
"resize-window"
|
|
||||||
"toggle-floating"
|
|
||||||
"toggle-fullscreen"
|
|
||||||
"close-window"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
command = lib.mkOption {
|
|
||||||
description = "Command executed by an exec keybind.";
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "ghostty";
|
|
||||||
};
|
|
||||||
|
|
||||||
direction = lib.mkOption {
|
|
||||||
description = "Direction targeted by a keybind action.";
|
|
||||||
type = lib.types.nullOr (
|
|
||||||
lib.types.enum [
|
|
||||||
"down"
|
|
||||||
"left"
|
|
||||||
"right"
|
|
||||||
"up"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
default = null;
|
|
||||||
example = "left";
|
|
||||||
};
|
|
||||||
|
|
||||||
workspace = lib.mkOption {
|
|
||||||
description = "Workspace targeted by a keybind action.";
|
|
||||||
type = lib.types.nullOr lib.types.int;
|
|
||||||
default = null;
|
|
||||||
example = 1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,39 +1,22 @@
|
|||||||
{ lib, util, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = util.registry.predicates.uniqueValue (entry: entry.value.domain);
|
assertion = lib.registry.predicates.uniqueValue (entry: entry.value.domain);
|
||||||
message = "Route domains must be unique.";
|
message = "Route domains must be unique.";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
entries:
|
entries:
|
||||||
util.registry.predicates.uniqueValue (entry: entry.value) (
|
lib.registry.predicates.uniqueValue (entry: entry.value) (
|
||||||
lib.concatMap (entry: map (port: entry // { value = port; }) entry.value.ports) entries
|
lib.concatMap (entry: map (port: entry // { value = port; }) entry.value.udp.ports) entries
|
||||||
);
|
);
|
||||||
message = "Route UDP ports must be unique.";
|
message = "Route UDP ports must be unique.";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
entries:
|
entries: lib.all (entry: entry.value.port == null || entry.value.http.port == null) entries;
|
||||||
lib.all (entry: entry.value.port != null && entry.value.ports == [ ]) (
|
message = "Route entries must not set both port and http.port.";
|
||||||
lib.filter (entry: entry.value.protocol != "udp") entries
|
|
||||||
);
|
|
||||||
message = "HTTP and HTTPS routes must set port and not ports.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
entries:
|
|
||||||
lib.all (entry: entry.value.port == null && entry.value.ports != [ ]) (
|
|
||||||
lib.filter (entry: entry.value.protocol == "udp") entries
|
|
||||||
);
|
|
||||||
message = "UDP routes must set ports and not port.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
entries:
|
|
||||||
lib.all (entry: entry.value.protocol != "udp" || entry.value.clientMaxBodySize == null) entries;
|
|
||||||
message = "UDP routes must not set clientMaxBodySize.";
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -42,40 +25,31 @@
|
|||||||
domain = lib.mkOption {
|
domain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "files.alexion.dev";
|
example = "files.alexion.dev";
|
||||||
description = "Records the public domain for this route.";
|
description = "Domain served by this route entry.";
|
||||||
};
|
|
||||||
|
|
||||||
protocol = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"http"
|
|
||||||
"https"
|
|
||||||
"udp"
|
|
||||||
];
|
|
||||||
description = "Selects the public protocol for this route.";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.port;
|
type = lib.types.nullOr lib.types.port;
|
||||||
default = null;
|
default = null;
|
||||||
example = 3923;
|
example = 3923;
|
||||||
description = "Sets the HTTP backend port for an HTTP or HTTPS route.";
|
description = "HTTPS backend TCP port served by the matching address registry entry.";
|
||||||
};
|
};
|
||||||
|
|
||||||
ports = lib.mkOption {
|
http.port = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.port;
|
||||||
|
default = null;
|
||||||
|
example = 8082;
|
||||||
|
description = "HTTP backend TCP port served by the matching address registry entry.";
|
||||||
|
};
|
||||||
|
|
||||||
|
udp.ports = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.port;
|
type = lib.types.listOf lib.types.port;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [
|
example = [
|
||||||
2456
|
2456
|
||||||
2457
|
2457
|
||||||
];
|
];
|
||||||
description = "Sets the UDP ports for a UDP route.";
|
description = "UDP ports served by the matching address registry entry.";
|
||||||
};
|
|
||||||
|
|
||||||
clientMaxBodySize = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "1g";
|
|
||||||
description = "Maximum HTTP request body the reverse proxy accepts.";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
let
|
||||||
type = lib.types.coercedTo lib.types.str (command: { inherit command; }) (
|
entryType = lib.types.submodule {
|
||||||
lib.types.submodule {
|
options = {
|
||||||
options = {
|
command = lib.mkOption {
|
||||||
command = lib.mkOption {
|
type = lib.types.str;
|
||||||
type = lib.types.str;
|
example = "nvim";
|
||||||
example = "nvim";
|
description = "Command expanded by this shell shortcut.";
|
||||||
description = "Sets the command expanded by this shortcut.";
|
|
||||||
};
|
|
||||||
|
|
||||||
abbreviation = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Uses a Fish abbreviation instead of an alias.";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
);
|
abbreviation = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether fish realizes this shell shortcut as an abbreviation instead of an alias.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
type = lib.types.coercedTo lib.types.str (command: { inherit command; }) entryType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
|
inherit (lib)
|
||||||
|
filterAttrs
|
||||||
|
groupBy
|
||||||
|
mapAttrs
|
||||||
|
unique
|
||||||
|
;
|
||||||
|
|
||||||
|
serviceNamesByUid =
|
||||||
|
entries:
|
||||||
|
mapAttrs (_uid: uidEntries: unique (map (entry: entry.entry) uidEntries)) (
|
||||||
|
groupBy (entry: toString entry.value) entries
|
||||||
|
);
|
||||||
|
|
||||||
duplicateUids =
|
duplicateUids =
|
||||||
entries:
|
entries:
|
||||||
lib.filterAttrs (_uid: entryNames: builtins.length entryNames > 1) (entryNamesByUid entries);
|
filterAttrs (_uid: serviceNames: builtins.length serviceNames > 1) (serviceNamesByUid entries);
|
||||||
|
|
||||||
entryNamesByUid =
|
|
||||||
entries:
|
|
||||||
lib.mapAttrs (_uid: uidEntries: lib.unique (map (entry: entry.entry) uidEntries)) (
|
|
||||||
lib.groupBy (entry: toString entry.value) entries
|
|
||||||
);
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Next stable UID: 20005
|
# Next stable UID: 20005
|
||||||
|
replicate = true;
|
||||||
|
|
||||||
type = lib.types.ints.positive;
|
type = lib.types.ints.positive;
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = entries: duplicateUids entries == { };
|
assertion = entries: duplicateUids entries == { };
|
||||||
message = "Multiple entries use the same stable UID.";
|
message = "Stable UID allocations must be unique.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
position = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"left"
|
|
||||||
"center"
|
|
||||||
"right"
|
|
||||||
];
|
|
||||||
default = "right";
|
|
||||||
description = "Selects the Waybar section that shows the module.";
|
|
||||||
};
|
|
||||||
|
|
||||||
order = lib.mkOption {
|
|
||||||
type = lib.types.int;
|
|
||||||
default = 100;
|
|
||||||
example = 50;
|
|
||||||
description = "Sorts the module within its Waybar section.";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf lib.types.anything;
|
|
||||||
default = { };
|
|
||||||
description = "Sets the Waybar module configuration.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
class = lib.mkOption {
|
|
||||||
description = "Matches the window class reported by the compositor.";
|
|
||||||
type = lib.types.str;
|
|
||||||
example = ".*[Ff]irefox.*";
|
|
||||||
};
|
|
||||||
|
|
||||||
icon = lib.mkOption {
|
|
||||||
description = "Sets the icon shown for matching windows.";
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
2
docs/.obsidian/app.json
vendored
2
docs/.obsidian/app.json
vendored
@@ -5,7 +5,7 @@
|
|||||||
"livePreview": false,
|
"livePreview": false,
|
||||||
"readableLineLength": false,
|
"readableLineLength": false,
|
||||||
"strictLineBreaks": true,
|
"strictLineBreaks": true,
|
||||||
"propertiesInDocument": "visible",
|
"propertiesInDocument": "hidden",
|
||||||
"showLineNumber": true,
|
"showLineNumber": true,
|
||||||
"autoPairMarkdown": false,
|
"autoPairMarkdown": false,
|
||||||
"useTab": false,
|
"useTab": false,
|
||||||
|
|||||||
3
docs/.obsidian/community-plugins.json
vendored
3
docs/.obsidian/community-plugins.json
vendored
@@ -5,6 +5,5 @@
|
|||||||
"obsidian-style-settings",
|
"obsidian-style-settings",
|
||||||
"custom-font-loader",
|
"custom-font-loader",
|
||||||
"obsidian-icon-folder",
|
"obsidian-icon-folder",
|
||||||
"table-editor-obsidian",
|
"table-editor-obsidian"
|
||||||
"obsidian-linter"
|
|
||||||
]
|
]
|
||||||
16
docs/.obsidian/graph.json
vendored
16
docs/.obsidian/graph.json
vendored
@@ -28,20 +28,6 @@
|
|||||||
"rgb": 5366249
|
"rgb": 5366249
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"query": "path:\"03 wrk/tasks\"[\"status\":open]",
|
|
||||||
"color": {
|
|
||||||
"a": 1,
|
|
||||||
"rgb": 14725458
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"query": "path:\"03 wrk/tasks\" [\"status\":closed]",
|
|
||||||
"color": {
|
|
||||||
"a": 1,
|
|
||||||
"rgb": 11657298
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"query": "path:\"03 wrk\" ",
|
"query": "path:\"03 wrk\" ",
|
||||||
"color": {
|
"color": {
|
||||||
@@ -74,6 +60,6 @@
|
|||||||
"repelStrength": 20,
|
"repelStrength": 20,
|
||||||
"linkStrength": 1,
|
"linkStrength": 1,
|
||||||
"linkDistance": 250,
|
"linkDistance": 250,
|
||||||
"scale": 0.16202697784544698,
|
"scale": 0.3645607001522557,
|
||||||
"close": false
|
"close": false
|
||||||
}
|
}
|
||||||
326
docs/.obsidian/plugins/obsidian-linter/data.json
vendored
326
docs/.obsidian/plugins/obsidian-linter/data.json
vendored
@@ -1,326 +0,0 @@
|
|||||||
{
|
|
||||||
"ruleConfigs": {
|
|
||||||
"add-blank-line-after-yaml": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"dedupe-yaml-array-values": {
|
|
||||||
"enabled": true,
|
|
||||||
"dedupe-alias-key": true,
|
|
||||||
"dedupe-tag-key": true,
|
|
||||||
"dedupe-array-keys": true,
|
|
||||||
"ignore-keys": []
|
|
||||||
},
|
|
||||||
"escape-yaml-special-characters": {
|
|
||||||
"enabled": true,
|
|
||||||
"try-to-escape-single-line-arrays": true
|
|
||||||
},
|
|
||||||
"force-yaml-escape": {
|
|
||||||
"enabled": true,
|
|
||||||
"force-yaml-escape-keys": []
|
|
||||||
},
|
|
||||||
"format-tags-in-yaml": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"format-yaml-array": {
|
|
||||||
"enabled": true,
|
|
||||||
"alias-key": true,
|
|
||||||
"tag-key": true,
|
|
||||||
"default-array-keys": true,
|
|
||||||
"force-single-line-array-style": [],
|
|
||||||
"force-multi-line-array-style": []
|
|
||||||
},
|
|
||||||
"insert-yaml-attributes": {
|
|
||||||
"enabled": true,
|
|
||||||
"text-to-insert": []
|
|
||||||
},
|
|
||||||
"move-inline-fields-to-yaml": {
|
|
||||||
"enabled": true,
|
|
||||||
"how-to-handle-full-line-fields": "Move and remove",
|
|
||||||
"how-to-handle-bracketed-fields": "Leave in place",
|
|
||||||
"how-to-handle-existing-keys": "Skip",
|
|
||||||
"inline-keys-to-ignore": []
|
|
||||||
},
|
|
||||||
"move-tags-to-yaml": {
|
|
||||||
"enabled": true,
|
|
||||||
"how-to-handle-existing-tags": "Nothing",
|
|
||||||
"tags-to-ignore": []
|
|
||||||
},
|
|
||||||
"remove-yaml-keys": {
|
|
||||||
"enabled": true,
|
|
||||||
"yaml-keys-to-remove": []
|
|
||||||
},
|
|
||||||
"sort-yaml-array-values": {
|
|
||||||
"enabled": true,
|
|
||||||
"sort-alias-key": true,
|
|
||||||
"sort-tag-key": true,
|
|
||||||
"sort-array-keys": true,
|
|
||||||
"ignore-keys": [],
|
|
||||||
"sort-order": "Ascending Alphabetical"
|
|
||||||
},
|
|
||||||
"yaml-key-sort": {
|
|
||||||
"enabled": true,
|
|
||||||
"yaml-key-priority-sort-order": [
|
|
||||||
"aliases",
|
|
||||||
"date-created",
|
|
||||||
"date-modified"
|
|
||||||
],
|
|
||||||
"priority-keys-at-start-of-yaml": true,
|
|
||||||
"yaml-sort-order-for-other-keys": "None"
|
|
||||||
},
|
|
||||||
"yaml-timestamp": {
|
|
||||||
"enabled": true,
|
|
||||||
"date-created": true,
|
|
||||||
"date-created-key": "date-created",
|
|
||||||
"date-created-source-of-truth": "file system",
|
|
||||||
"date-modified": true,
|
|
||||||
"date-modified-key": "date-modified",
|
|
||||||
"date-modified-source-of-truth": "file system",
|
|
||||||
"format": "YYYY-MM-DD HH:mm",
|
|
||||||
"convert-to-utc": false,
|
|
||||||
"update-on-file-contents-updated": "after 5 seconds"
|
|
||||||
},
|
|
||||||
"yaml-title": {
|
|
||||||
"enabled": false,
|
|
||||||
"title-key": "title",
|
|
||||||
"mode": "first-h1-or-filename-if-h1-missing"
|
|
||||||
},
|
|
||||||
"yaml-title-alias": {
|
|
||||||
"enabled": true,
|
|
||||||
"preserve-existing-alias-section-style": true,
|
|
||||||
"keep-alias-that-matches-the-filename": false,
|
|
||||||
"use-yaml-key-to-keep-track-of-old-filename-or-heading": false,
|
|
||||||
"alias-helper-key": "linter-yaml-title-alias",
|
|
||||||
"remove-alias-if-empty": true
|
|
||||||
},
|
|
||||||
"capitalize-headings": {
|
|
||||||
"enabled": true,
|
|
||||||
"style": "Title Case",
|
|
||||||
"ignore-case-words": [],
|
|
||||||
"lowercase-words": [],
|
|
||||||
"starting-word-ignore-characters": "'\"(‘“-",
|
|
||||||
"ending-word-ignore-characters": ".?!,:;'\")”’0123456789-",
|
|
||||||
"ignore-words": []
|
|
||||||
},
|
|
||||||
"file-name-heading": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"header-increment": {
|
|
||||||
"enabled": true,
|
|
||||||
"start-at-h2": false
|
|
||||||
},
|
|
||||||
"headings-start-line": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-trailing-punctuation-in-heading": {
|
|
||||||
"enabled": true,
|
|
||||||
"punctuation-to-remove": ".,;:!。,;:!"
|
|
||||||
},
|
|
||||||
"footnote-after-punctuation": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"move-footnotes-to-the-bottom": {
|
|
||||||
"enabled": true,
|
|
||||||
"include-blank-line-between-footnotes": false
|
|
||||||
},
|
|
||||||
"re-index-footnotes": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"auto-correct-common-misspellings": {
|
|
||||||
"enabled": true,
|
|
||||||
"ignore-words": [],
|
|
||||||
"skip-words-with-multiple-capitals": true,
|
|
||||||
"extra-auto-correct-files": []
|
|
||||||
},
|
|
||||||
"blockquote-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"style": "space"
|
|
||||||
},
|
|
||||||
"convert-bullet-list-markers": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"default-language-for-code-fences": {
|
|
||||||
"enabled": true,
|
|
||||||
"default-language": ""
|
|
||||||
},
|
|
||||||
"emphasis-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"style": "consistent"
|
|
||||||
},
|
|
||||||
"no-bare-urls": {
|
|
||||||
"enabled": true,
|
|
||||||
"no-bare-uris": true
|
|
||||||
},
|
|
||||||
"ordered-list-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"number-style": "ascending",
|
|
||||||
"list-end-style": ".",
|
|
||||||
"preserve-start": false
|
|
||||||
},
|
|
||||||
"proper-ellipsis": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"quote-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"single-quote-enabled": true,
|
|
||||||
"single-quote-style": "''",
|
|
||||||
"double-quote-enabled": true,
|
|
||||||
"double-quote-style": "\"\""
|
|
||||||
},
|
|
||||||
"remove-consecutive-list-markers": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-empty-list-markers": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-hyphenated-line-breaks": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-multiple-spaces": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"strong-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"style": "consistent"
|
|
||||||
},
|
|
||||||
"two-spaces-between-lines-with-content": {
|
|
||||||
"enabled": false,
|
|
||||||
"line-break-indicator": " "
|
|
||||||
},
|
|
||||||
"unordered-list-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"list-style": "consistent"
|
|
||||||
},
|
|
||||||
"align-table-columns": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"compact-yaml": {
|
|
||||||
"enabled": true,
|
|
||||||
"inner-new-lines": false
|
|
||||||
},
|
|
||||||
"consecutive-blank-lines": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"convert-spaces-to-tabs": {
|
|
||||||
"enabled": true,
|
|
||||||
"tabsize": 4
|
|
||||||
},
|
|
||||||
"empty-line-around-blockquotes": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"empty-line-around-code-fences": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"empty-line-around-horizontal-rules": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"empty-line-around-math-blocks": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"empty-line-around-tables": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"heading-blank-lines": {
|
|
||||||
"enabled": true,
|
|
||||||
"bottom": true,
|
|
||||||
"empty-line-after-yaml": true
|
|
||||||
},
|
|
||||||
"line-break-at-document-end": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"move-math-block-indicators-to-their-own-line": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"paragraph-blank-lines": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-empty-lines-between-list-markers-and-checklists": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-link-spacing": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-space-around-characters": {
|
|
||||||
"enabled": true,
|
|
||||||
"include-fullwidth-forms": true,
|
|
||||||
"include-cjk-symbols-and-punctuation": true,
|
|
||||||
"include-dashes": true,
|
|
||||||
"other-symbols": ""
|
|
||||||
},
|
|
||||||
"remove-space-before-or-after-characters": {
|
|
||||||
"enabled": true,
|
|
||||||
"characters-to-remove-space-before": ",!?;:).’”]",
|
|
||||||
"characters-to-remove-space-after": "¿¡‘“(["
|
|
||||||
},
|
|
||||||
"space-after-list-markers": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"space-between-chinese-japanese-or-korean-and-english-or-numbers": {
|
|
||||||
"enabled": true,
|
|
||||||
"english-symbols-punctuation-before": "-+;:'\"°%$)]",
|
|
||||||
"english-symbols-punctuation-after": "-+'\"([¥$"
|
|
||||||
},
|
|
||||||
"trailing-spaces": {
|
|
||||||
"enabled": true,
|
|
||||||
"two-space-line-break": false
|
|
||||||
},
|
|
||||||
"add-blockquote-indentation-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"prevent-double-checklist-indicator-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"prevent-double-list-item-indicator-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"proper-ellipsis-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-hyphens-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-leading-or-trailing-whitespace-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-leftover-footnotes-from-quote-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-multiple-blank-lines-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lintOnSave": true,
|
|
||||||
"recordLintOnSaveLogs": false,
|
|
||||||
"displayChanged": true,
|
|
||||||
"suppressMessageWhenNoChange": true,
|
|
||||||
"enableDiffPreviewView": true,
|
|
||||||
"suppressLintAllFilesConfirmationModal": false,
|
|
||||||
"suppressLintAllFilesInFolderConfirmationModal": false,
|
|
||||||
"lintOnFileChange": false,
|
|
||||||
"displayLintOnFileChangeNotice": false,
|
|
||||||
"settingsConvertedToConfigKeyValues": true,
|
|
||||||
"textAreaSettingsConvertedToListItemSettings": true,
|
|
||||||
"defaultArrayStyleMovedToCommonStyles": true,
|
|
||||||
"additionalFileExtensions": [],
|
|
||||||
"foldersToIgnore": [
|
|
||||||
"99 meta/templates"
|
|
||||||
],
|
|
||||||
"filesToIgnore": [
|
|
||||||
{
|
|
||||||
"label": "todo",
|
|
||||||
"match": "TODO.md",
|
|
||||||
"flags": "i"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"linterLocale": "system-default",
|
|
||||||
"logLevel": "ERROR",
|
|
||||||
"lintCommands": [],
|
|
||||||
"customRegexes": [],
|
|
||||||
"commonStyles": {
|
|
||||||
"aliasArrayStyle": "multi-line",
|
|
||||||
"tagArrayStyle": "single-line",
|
|
||||||
"defaultArrayStyle": "multi-line",
|
|
||||||
"minimumNumberOfDollarSignsToBeAMathBlock": 2,
|
|
||||||
"escapeCharacter": "\"",
|
|
||||||
"removeUnnecessaryEscapeCharsForMultiLineArrays": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
399
docs/.obsidian/plugins/obsidian-linter/main.js
vendored
399
docs/.obsidian/plugins/obsidian-linter/main.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "obsidian-linter",
|
|
||||||
"name": "Linter",
|
|
||||||
"version": "1.34.1",
|
|
||||||
"minAppVersion": "1.13.0",
|
|
||||||
"description": "Formats and styles your notes. It can be used to format YAML tags, aliases, arrays, and metadata; footnotes; headings; spacing; math blocks; regular markdown contents like list, italics, and bold styles; and more with the use of custom rule options as well.",
|
|
||||||
"author": "Victor Tao",
|
|
||||||
"authorUrl": "https://github.com/platers",
|
|
||||||
"helpUrl": "https://platers.github.io/obsidian-linter/",
|
|
||||||
"isDesktopOnly": false
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.linter-no-border{border:none}.linter-border-bottom{border-bottom:1px solid var(--background-modifier-border);border-top:0;margin-bottom:.75em}.custom-row-description{margin-top:0}.modal-warn,.search-zero-state{font-weight:700}.modal-heading,.search-zero-state{text-align:center}.confirm-modal-checkbox-container{align-items:center;display:flex;gap:6px;margin-top:8px}.disabled-list-entry{color:var(--text-faint)}.linter-diff-preview-view{display:flex;flex-direction:column;height:100%;min-width:0;overflow:hidden}.linter-diff-preview-empty{color:var(--text-muted);padding:16px}.linter-diff-preview-header{align-items:flex-start;border-bottom:1px solid var(--background-modifier-border);display:flex;flex:0 0 auto;gap:12px;justify-content:space-between;padding:12px 14px}.linter-diff-preview-title-group{min-width:0}.linter-diff-preview-header h3{font-size:var(--font-ui-medium);line-height:1.3;margin:0 0 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.linter-diff-preview-actions{display:flex;flex:0 0 auto;gap:8px}.linter-diff-preview-body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px 16px}.linter-diff-preview-view .linter-diff-summary{color:var(--text-normal);display:inline-block;font-size:var(--font-ui-small);margin:0 0 10px;padding:5px 8px}.linter-diff-preview-view .linter-diff,.linter-diff-preview-view .linter-diff-summary{background:var(--background-secondary);border:1px solid var(--background-modifier-border);border-radius:6px}.linter-diff-preview-view .linter-diff{font-family:var(--font-monospace);font-size:var(--font-smaller);line-height:1.45;margin:0;overflow:hidden;padding:6px 0}.linter-diff-preview-view .linter-diff-line{display:grid;grid-template-columns:26px minmax(0,1fr);min-height:20px;padding:1px 10px 1px 0}.linter-diff-preview-view .linter-diff-marker{color:var(--text-muted);padding-right:8px;text-align:right;user-select:none}.linter-diff-preview-view .linter-diff-text{min-width:0;overflow-wrap:anywhere;white-space:pre-wrap}.linter-diff-preview-view .linter-diff-skip{border-bottom:1px solid var(--background-modifier-border);border-top:1px solid var(--background-modifier-border);color:var(--text-faint);font-family:var(--font-interface);font-size:var(--font-ui-smaller);padding:4px 12px;text-align:center}.linter-diff-preview-view .linter-diff-added{background:rgba(var(--color-green-rgb),.12);color:var(--text-normal)}.linter-diff-preview-view .linter-diff-removed{background:rgba(var(--color-red-rgb),.12);color:var(--text-normal)}.linter-diff-preview-view .linter-diff-context{color:var(--text-muted)}
|
|
||||||
83
docs/.obsidian/workspace.json
vendored
83
docs/.obsidian/workspace.json
vendored
@@ -4,21 +4,21 @@
|
|||||||
"type": "split",
|
"type": "split",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "81c4860f495113bf",
|
"id": "6d9088d392cb8604",
|
||||||
"type": "tabs",
|
"type": "tabs",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "daab69fc3faab88f",
|
"id": "22a4c1f201a1b00e",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "00 INBOX/credits.md",
|
"file": "00 INBOX/valheim progression.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": true
|
"source": true
|
||||||
},
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "credits"
|
"title": "valheim progression"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "localgraph",
|
"type": "localgraph",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "00 INBOX/credits.md",
|
"file": "00 INBOX/valheim progression.md",
|
||||||
"options": {
|
"options": {
|
||||||
"collapse-filter": true,
|
"collapse-filter": true,
|
||||||
"search": "",
|
"search": "",
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"linkStrength": 1,
|
"linkStrength": 1,
|
||||||
"linkDistance": 250,
|
"linkDistance": 250,
|
||||||
"scale": 0.6666666666666666,
|
"scale": 0.6666666666666666,
|
||||||
"close": false
|
"close": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"icon": "lucide-git-fork",
|
"icon": "lucide-git-fork",
|
||||||
@@ -184,6 +184,20 @@
|
|||||||
"title": "tag"
|
"title": "tag"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "8918bf52a32e4a8e",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "all-properties",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-ghost",
|
||||||
|
"title": "all-properties"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "c10908b3bd97f5fa",
|
"id": "c10908b3bd97f5fa",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
@@ -217,43 +231,42 @@
|
|||||||
"table-editor-obsidian:Advanced Tables Toolbar": false
|
"table-editor-obsidian:Advanced Tables Toolbar": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "daab69fc3faab88f",
|
"active": "22a4c1f201a1b00e",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"00 INBOX/ai/tldr-design.md",
|
"00 INBOX/ai/statix-architecture.md",
|
||||||
"00 INBOX/ai/caps-escape-design.md",
|
"00 INBOX/ai/nix-statix-deadnix-rules.md",
|
||||||
"AGENTS.md",
|
|
||||||
"00 INBOX/ai/tldr-design-tree.md",
|
|
||||||
"00 INBOX/ai/grill-skill-design.md",
|
|
||||||
"03 wrk/tasks/0012-gitea-axi-pr-merge-guardrail.md",
|
|
||||||
"03 wrk/tasks/0011-gitea-axi-rust-migration.md",
|
|
||||||
"00 INBOX/ai/gitea-axi-pr-merge-guardrail.md",
|
|
||||||
"00 INBOX/ai/gitea-axi-rust-migration.md",
|
|
||||||
"00 INBOX/valheim progression.md",
|
|
||||||
"03 wrk/tasks/0010-fix-main-monitor-detection.md",
|
|
||||||
"00 INBOX/credits.md",
|
|
||||||
"03 wrk/tasks/0009-bazel-project-scaffolding.md",
|
|
||||||
"03 wrk/tasks/0005-rust-cli-scaffolding.md",
|
|
||||||
"03 wrk/tasks/0008-vim-obsidian-markdown-renderer.md",
|
|
||||||
"03 wrk/tasks/0002-nixos-refactor.md",
|
|
||||||
"TODO.md",
|
|
||||||
"technical-writing.md",
|
|
||||||
"dictionary.md",
|
|
||||||
"03 wrk/tasks/0007-obsidian-source-notes.md",
|
|
||||||
"03 wrk/tasks/0004-dictionary-cli.md",
|
|
||||||
"03 wrk/tasks/0006-wallpaper-randomizer.md",
|
|
||||||
"03 wrk/tasks/0003-whisper-dictation.md",
|
|
||||||
"03 wrk/monorepo.md",
|
|
||||||
"03 wrk/tasks/0001-todo-cli.md",
|
|
||||||
"03 wrk/productivity.md",
|
|
||||||
"03 wrk/tasks",
|
|
||||||
"00 INBOX/let order name resolution.md~",
|
"00 INBOX/let order name resolution.md~",
|
||||||
|
"00 INBOX/nix name resolution.md",
|
||||||
|
"00 INBOX/let order name resolution.md",
|
||||||
"00 INBOX/nix name resolution.md~",
|
"00 INBOX/nix name resolution.md~",
|
||||||
|
"00 INBOX/valheim progression.md",
|
||||||
|
"TODO.md",
|
||||||
|
"00 INBOX/ai/deadnix-architecture.md",
|
||||||
"TODO.md~",
|
"TODO.md~",
|
||||||
|
"00 INBOX/ai/xatu-rule-implementation-order.md",
|
||||||
|
"00 INBOX/ai/xatu-architecture-design.md",
|
||||||
|
"00 INBOX/ai/nix-style-linter-plan.md",
|
||||||
|
"00 INBOX/ai/nix-style-guide-agent-customization.md",
|
||||||
|
"00 INBOX/ai/game-guest-readiness-plan.md",
|
||||||
|
"00 INBOX/ai/xatu-node-rule-static-dispatch-plan.md",
|
||||||
|
"00 INBOX/ai/nix-style-linter-pr-1-vendor-rnix.md",
|
||||||
|
"00 INBOX/ai/nix-style-linter-pr-3-let-order.md",
|
||||||
|
"00 INBOX/ai/nix-style-linter-pr-2-scaffold-cli.md",
|
||||||
|
"00 INBOX/ai/homepage-pikachu-setup-plan.md",
|
||||||
|
"00 INBOX/ai/source-driven-remote-deploy-plan.md",
|
||||||
|
"00 INBOX/ai/monorepo-work-report.md",
|
||||||
|
"00 INBOX/ai/clew-ast-exploration.md",
|
||||||
|
"00 INBOX/ai/steamcmd servers.md",
|
||||||
|
"00 INBOX/ai/ariadne-pipeline-stage-boundary.md",
|
||||||
|
"00 INBOX/ai/ariadne-project-goals.md",
|
||||||
|
"02 notes/steamcmd server setup.md",
|
||||||
|
"00 INBOX/how to setup a steamcmd server.md",
|
||||||
"00 INBOX/ai/clew-ast-exploration.md~",
|
"00 INBOX/ai/clew-ast-exploration.md~",
|
||||||
"03 wrk/machoke.md~",
|
"03 wrk/machoke.md~",
|
||||||
"03 wrk/falinks.md~",
|
"03 wrk/falinks.md~",
|
||||||
"99 meta/attachments/bases/unlinked mentions.base",
|
"99 meta/attachments/bases/unlinked mentions.base",
|
||||||
"00 INBOX/ai",
|
"00 INBOX/ai",
|
||||||
"03 wrk/ai"
|
"03 wrk/ai",
|
||||||
|
"99 meta/Untitled"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Caps Escape Design
|
|
||||||
|
|
||||||
## Facts
|
|
||||||
|
|
||||||
- Commit `250b0b71f` removed `services.xserver.xkb.options` from the shared host module.
|
|
||||||
- The removed option was `caps:escape_shifted_capslock`.
|
|
||||||
- That XKB option maps Caps Lock to Escape.
|
|
||||||
- Shift+Caps Lock still enables Caps Lock.
|
|
||||||
- The shared host module configures Neogaia, Zeus, and Pikachu.
|
|
||||||
- Zeus has QMK firmware that provides equivalent keyboard behavior.
|
|
||||||
- Neogaia and Zeus enable `modules.host.cachyos` and `modules.desktop`.
|
|
||||||
- Pikachu enables neither module.
|
|
||||||
- `configs/nixos/hosts/neogaia/default.nix` directly sets Neogaia-specific hardware, networking, desktop application, and Waybar settings.
|
|
||||||
- The current configuration has no Caps Lock or Escape remapping.
|
|
||||||
- This session runs on Zeus, not Neogaia.
|
|
||||||
|
|
||||||
## Design Tree
|
|
||||||
|
|
||||||
- Restore Caps Lock behavior
|
|
||||||
- Scope the configuration
|
|
||||||
- Configure Neogaia only. ✓
|
|
||||||
- Select the mapping behavior.
|
|
||||||
- Restore `caps:escape_shifted_capslock`. ✓
|
|
||||||
- Set `services.xserver.xkb.options` in `configs/nixos/hosts/neogaia/default.nix`. ✓
|
|
||||||
- Define validation for Neogaia.
|
|
||||||
- Run static checks and manually verify both keys after deployment. ✓
|
|
||||||
- Stop after source and static validation. ✓
|
|
||||||
|
|
||||||
## Settled Decisions
|
|
||||||
|
|
||||||
1. Configure the mapping on Neogaia only.
|
|
||||||
2. Restore `caps:escape_shifted_capslock`.
|
|
||||||
3. Set `services.xserver.xkb.options` directly in Neogaia's host configuration.
|
|
||||||
4. Run static checks and manually verify both keys on Neogaia after deployment.
|
|
||||||
5. Stop after source and static validation.
|
|
||||||
|
|
||||||
## Frontier
|
|
||||||
|
|
||||||
None.
|
|
||||||
304
docs/00 INBOX/ai/deadnix-architecture.md
Normal file
304
docs/00 INBOX/ai/deadnix-architecture.md
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
# Deadnix Architecture
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This note explains how `deadnix` finds unused Nix code.
|
||||||
|
It focuses on architecture, not whether each check belongs in our linter.
|
||||||
|
|
||||||
|
Use this note when deciding whether our linter needs scope analysis.
|
||||||
|
|
||||||
|
## What Deadnix Is
|
||||||
|
|
||||||
|
`deadnix` is a dead-code scanner for Nix.
|
||||||
|
It finds bindings that introduce names which no live code uses.
|
||||||
|
|
||||||
|
It is not a general style linter.
|
||||||
|
It does not have many independent rules.
|
||||||
|
It implements one analysis family: unused declarations.
|
||||||
|
|
||||||
|
Like `statix`, it parses source text with `rnix`.
|
||||||
|
It does not evaluate Nix.
|
||||||
|
It does not import files.
|
||||||
|
It reasons over local syntax and local scope only.
|
||||||
|
|
||||||
|
## Minimal Dead-Code Vocabulary
|
||||||
|
|
||||||
|
- A binding introduces a name.
|
||||||
|
- A scope defines where introduced names are visible.
|
||||||
|
- A use is an identifier occurrence that refers to a binding.
|
||||||
|
- Shadowing happens when an inner scope reuses an outer name.
|
||||||
|
- Dead code is a binding that no live code uses.
|
||||||
|
- A fixed point is the final result after repeated analysis no longer changes.
|
||||||
|
|
||||||
|
Dead-code analysis needs more state than a simple syntax lint.
|
||||||
|
It must know which names a scope introduces and where those names can be used.
|
||||||
|
|
||||||
|
## Component Map
|
||||||
|
|
||||||
|
`deadnix` has fewer layers than `statix`.
|
||||||
|
The command-line entry point, file walk, parsing, analysis, reporting, and editing live in one small tool.
|
||||||
|
|
||||||
|
The important concepts are separate modules:
|
||||||
|
|
||||||
|
- settings control analysis options;
|
||||||
|
- scopes describe where names are introduced;
|
||||||
|
- bindings describe individual declared names;
|
||||||
|
- usage search answers whether a name appears in a live body;
|
||||||
|
- dead-code results connect a binding to its scope;
|
||||||
|
- reports render findings;
|
||||||
|
- edits remove or rename unused declarations.
|
||||||
|
|
||||||
|
## Main Pipeline
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Parse CLI options] --> B[Collect input files]
|
||||||
|
B --> C[Read each file]
|
||||||
|
C --> D[Parse with rnix]
|
||||||
|
D --> E[Skip file on parse errors]
|
||||||
|
D --> F[Run dead-code analysis]
|
||||||
|
F --> G[Print reports unless quiet]
|
||||||
|
G --> H[Apply edits when --edit is set]
|
||||||
|
H --> I[Exit 1 when --fail and findings exist]
|
||||||
|
```
|
||||||
|
|
||||||
|
The CLI accepts files or directories.
|
||||||
|
Directory mode walks `.nix` files.
|
||||||
|
Hidden files require an explicit option.
|
||||||
|
Excludes filter paths before analysis.
|
||||||
|
|
||||||
|
The tool skips dead-code diagnostics when a file has parse errors.
|
||||||
|
It reports parse errors to the user and continues with other files.
|
||||||
|
|
||||||
|
## Analysis Settings
|
||||||
|
|
||||||
|
The analysis has four important switches.
|
||||||
|
|
||||||
|
`no_lambda_arg` disables checks for single lambda arguments.
|
||||||
|
This can avoid noise when an API requires an argument name.
|
||||||
|
|
||||||
|
`no_lambda_pattern_names` disables checks for names in attribute-set function patterns.
|
||||||
|
This matters for `callPackage`-style code, where argument names can be external API.
|
||||||
|
|
||||||
|
`no_underscore` ignores bindings that start with `_`.
|
||||||
|
This treats `_name` as an intentional unused placeholder.
|
||||||
|
|
||||||
|
`warn_used_underscore` reports underscore-prefixed bindings that code actually uses.
|
||||||
|
This enforces the convention that `_name` means unused.
|
||||||
|
|
||||||
|
These settings are architecture-relevant because dead-code policy depends on API context.
|
||||||
|
A repository linter needs a way to express that context.
|
||||||
|
|
||||||
|
## Scope Model
|
||||||
|
|
||||||
|
`deadnix` recognizes four scope types.
|
||||||
|
|
||||||
|
### Lambda pattern scope
|
||||||
|
|
||||||
|
A lambda pattern is a function argument written as an attribute set pattern.
|
||||||
|
It can introduce field names and an optional alias for the whole argument set.
|
||||||
|
|
||||||
|
Example shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
args @ { pkgs, lib, ... }: body
|
||||||
|
```
|
||||||
|
|
||||||
|
This scope can introduce `args`, `pkgs`, and `lib`.
|
||||||
|
|
||||||
|
### Lambda argument scope
|
||||||
|
|
||||||
|
A single lambda argument introduces one name.
|
||||||
|
|
||||||
|
Example shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
pkgs: body
|
||||||
|
```
|
||||||
|
|
||||||
|
This scope introduces `pkgs`.
|
||||||
|
|
||||||
|
### Let scope
|
||||||
|
|
||||||
|
A `let` expression introduces bindings and inherited names.
|
||||||
|
Those names are available in binding expressions and in the body.
|
||||||
|
|
||||||
|
Example shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let
|
||||||
|
inherit (pkgs) hello;
|
||||||
|
value = hello;
|
||||||
|
in
|
||||||
|
value
|
||||||
|
```
|
||||||
|
|
||||||
|
This scope introduces `hello` and `value`.
|
||||||
|
|
||||||
|
### Recursive attribute-set scope
|
||||||
|
|
||||||
|
A `rec` attribute set introduces names that sibling attributes can reference.
|
||||||
|
`deadnix` models this scope, but treats its bindings as non-removable.
|
||||||
|
|
||||||
|
That conservative choice avoids deleting public-looking attributes from recursive sets.
|
||||||
|
|
||||||
|
## Binding Model
|
||||||
|
|
||||||
|
A binding stores three facts.
|
||||||
|
|
||||||
|
It stores the identifier name.
|
||||||
|
It stores the syntax node that declared the name.
|
||||||
|
It stores whether the binding is mortal.
|
||||||
|
|
||||||
|
A mortal binding can become a dead-code finding.
|
||||||
|
A non-mortal binding participates in name lookup but will not be removed.
|
||||||
|
|
||||||
|
Single lambda arguments that already start with `_` are non-mortal.
|
||||||
|
Recursive attribute-set bindings are also non-mortal.
|
||||||
|
|
||||||
|
A binding also checks for the `deadnix: skip` pragma.
|
||||||
|
The pragma applies to a nearby following declaration.
|
||||||
|
The scanner searches backward from the declaration and stops after a blank-line boundary.
|
||||||
|
|
||||||
|
## Usage Search
|
||||||
|
|
||||||
|
Usage search asks whether a given name appears inside a syntax subtree.
|
||||||
|
|
||||||
|
The search is scope-aware.
|
||||||
|
When it enters a child scope that binds the same name, it stops following that branch.
|
||||||
|
That prevents an inner binding from counting as a use of an outer binding.
|
||||||
|
|
||||||
|
Attribute paths get special handling.
|
||||||
|
Ordinary attribute keys do not count as uses.
|
||||||
|
Dynamic or string attribute names can contain expressions, so the search looks inside those parts.
|
||||||
|
|
||||||
|
`inherit` also gets special handling.
|
||||||
|
A scope can inherit names from an outer scope.
|
||||||
|
The analysis checks whether the inherited expression uses the searched name.
|
||||||
|
|
||||||
|
## Fixed-Point Analysis
|
||||||
|
|
||||||
|
`deadnix` repeats the scan until the result set stops changing.
|
||||||
|
|
||||||
|
This matters because dead code can depend on other dead code.
|
||||||
|
A binding can appear used during the first pass because another binding references it.
|
||||||
|
If the referring binding is dead, the referenced binding is also dead.
|
||||||
|
|
||||||
|
The loop tracks dead declaration nodes.
|
||||||
|
During later scans, uses inside already-dead bodies do not keep other bindings alive.
|
||||||
|
|
||||||
|
This fixed-point loop is the central architectural difference from simple local syntax lints.
|
||||||
|
It makes dead-code results transitive.
|
||||||
|
|
||||||
|
## Reporting Model
|
||||||
|
|
||||||
|
A result stores the scope and binding for one finding.
|
||||||
|
The display text combines both parts.
|
||||||
|
Examples include unused lambda pattern, unused lambda argument, and unused let binding.
|
||||||
|
|
||||||
|
Human output uses `ariadne`.
|
||||||
|
The reporter groups ranges by line and labels the exact binding names.
|
||||||
|
It uses different colors for different scope kinds.
|
||||||
|
|
||||||
|
JSON output exists behind a feature flag.
|
||||||
|
It emits file, message, line, column, and end column.
|
||||||
|
|
||||||
|
`NO_COLOR` disables colored human output.
|
||||||
|
|
||||||
|
## Edit Model
|
||||||
|
|
||||||
|
`deadnix --edit` converts findings into text edits.
|
||||||
|
Each edit has a start offset, end offset, and replacement string.
|
||||||
|
|
||||||
|
The edit behavior depends on scope kind.
|
||||||
|
|
||||||
|
For unused lambda pattern names, it removes the name and nearby comma or whitespace.
|
||||||
|
For unused pattern aliases, it removes the alias syntax.
|
||||||
|
For unused lambda arguments, it renames the argument with an underscore prefix.
|
||||||
|
For unused `let` assignments, it deletes the assignment.
|
||||||
|
For unused inherited names, it deletes only the inherited name.
|
||||||
|
|
||||||
|
After those edits, it parses the edited source again.
|
||||||
|
Then it removes empty `let in` constructs and empty `inherit` statements.
|
||||||
|
|
||||||
|
This cleanup pass prevents the first edit batch from leaving invalid or useless scaffolding.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
If a path cannot be stated or read, the CLI reports the file error and continues.
|
||||||
|
If a file has parse errors, the CLI reports those errors and skips dead-code analysis for that file.
|
||||||
|
|
||||||
|
The `--fail` flag changes exit status when findings exist.
|
||||||
|
Without `--fail`, findings do not make the command fail.
|
||||||
|
|
||||||
|
This design separates report generation from CI policy.
|
||||||
|
|
||||||
|
## Architectural Strengths
|
||||||
|
|
||||||
|
The scope model is explicit.
|
||||||
|
Each scope type declares which bindings it introduces and where those bindings are visible.
|
||||||
|
|
||||||
|
The fixed-point loop catches transitive dead code.
|
||||||
|
A binding used only by dead code also becomes dead.
|
||||||
|
|
||||||
|
The usage search handles shadowing.
|
||||||
|
That prevents common false positives in nested functions and nested `let` expressions.
|
||||||
|
|
||||||
|
The edit model is tied to analysis results.
|
||||||
|
It knows whether a binding came from a lambda, pattern, `let`, or `inherit`.
|
||||||
|
|
||||||
|
The tool has few moving parts.
|
||||||
|
That makes the analysis easy to follow compared to a general multi-rule linter.
|
||||||
|
|
||||||
|
## Architectural Limits
|
||||||
|
|
||||||
|
`deadnix` does not evaluate Nix.
|
||||||
|
It cannot know which names external callers depend on.
|
||||||
|
This matters for module arguments, package function arguments, and public attribute sets.
|
||||||
|
|
||||||
|
It has global switches rather than project-specific rule contexts.
|
||||||
|
For example, all lambda pattern names can be checked or skipped.
|
||||||
|
The tool does not know that one directory contains `callPackage` functions.
|
||||||
|
|
||||||
|
The analysis is local to one file.
|
||||||
|
It cannot see that another file imports a function and relies on its argument names.
|
||||||
|
|
||||||
|
The fixer performs text edits with byte ranges.
|
||||||
|
It must handle punctuation and whitespace carefully.
|
||||||
|
Pattern edits are harder than simple assignment deletion.
|
||||||
|
|
||||||
|
The tool has one analysis family.
|
||||||
|
It is not a framework for many unrelated rules.
|
||||||
|
|
||||||
|
## Lessons for Our Linter
|
||||||
|
|
||||||
|
Dead-code checks need a scope pass.
|
||||||
|
A per-node rule interface is not enough when a diagnostic depends on name visibility.
|
||||||
|
|
||||||
|
Represent scopes explicitly.
|
||||||
|
Each scope type should provide introduced bindings and visible bodies.
|
||||||
|
|
||||||
|
Track shadowing before reporting unused names.
|
||||||
|
A text search for an identifier is not sufficient.
|
||||||
|
|
||||||
|
Use fixed-point analysis for transitive dead code.
|
||||||
|
One pass misses names used only by dead declarations.
|
||||||
|
|
||||||
|
Separate policy from mechanics.
|
||||||
|
The mechanics can find unused names.
|
||||||
|
Repository policy decides whether module arguments, package arguments, or underscore names count.
|
||||||
|
|
||||||
|
Treat autofix as scope-specific.
|
||||||
|
Removing a pattern name is not the same operation as deleting a `let` assignment.
|
||||||
|
|
||||||
|
Run cleanup after edits.
|
||||||
|
Dead-code fixes can create empty `let` expressions and empty `inherit` statements.
|
||||||
|
|
||||||
|
## Open Design Questions for Our Linter
|
||||||
|
|
||||||
|
- Should dead-code analysis be part of `nix-style-linter`, or a separate command?
|
||||||
|
- Should policy vary by path, file kind, or suppression comment?
|
||||||
|
- Should recursive attribute-set members ever become removable findings?
|
||||||
|
- Should underscore names mean ignored, or should used underscore names be errors?
|
||||||
|
- Should the linter report unused names without offering autofix first?
|
||||||
|
- Should cleanup rules run as ordinary rules or as a post-fix pass?
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# TL;DR command design
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Add a Pi command that summarizes the preceding agent response by default.
|
|
||||||
An explicit command argument identifies the source to summarize.
|
|
||||||
|
|
||||||
## Decision tree
|
|
||||||
|
|
||||||
- Command form and scope
|
|
||||||
- Settled: Create a project prompt template at `.pi/prompts/tldr.md`.
|
|
||||||
- Command interface
|
|
||||||
- Settled: Pass arguments through as a free-form source request.
|
|
||||||
- Default source selection
|
|
||||||
- Settled: Summarize the preceding agent response when no source request exists.
|
|
||||||
- Summary format and length
|
|
||||||
- Settled: Use a compact, format-free summary.
|
|
||||||
- Ambiguous-source behavior
|
|
||||||
- Settled: Add no special handling policy.
|
|
||||||
|
|
||||||
## Settled decisions
|
|
||||||
|
|
||||||
- **Q1 — Command form and scope:** Create a project prompt template.
|
|
||||||
- **Q2 — Explicit source interface:** Accept a free-form source request.
|
|
||||||
- **Default source selection:** Summarize the preceding agent response when the command has no arguments.
|
|
||||||
- **Q3 — Summary shape:** Use a compact, format-free summary.
|
|
||||||
- **Q4 — Unavailable source:** Add no special handling policy.
|
|
||||||
|
|
||||||
## Frontier
|
|
||||||
|
|
||||||
The frontier is empty.
|
|
||||||
|
|
||||||
## Shared understanding pending confirmation
|
|
||||||
|
|
||||||
The project gets a `/tldr` prompt command in `.pi/prompts/tldr.md`.
|
|
||||||
Without arguments, it summarizes the preceding agent response.
|
|
||||||
With arguments, it interprets them as a free-form source request.
|
|
||||||
It produces a compact summary with no fixed format.
|
|
||||||
The prompt adds no special policy for unavailable or unclear sources.
|
|
||||||
@@ -1,939 +0,0 @@
|
|||||||
# Valheim craft and build checklist
|
|
||||||
|
|
||||||
This checklist groups craftable inventory items and non-structural build pieces by biome progression.
|
|
||||||
It excludes floors, walls, roofs, beams, poles, stairs, doors, gates, windows, and generic structure pieces.
|
|
||||||
Biome placement uses the earliest ordinary unlock from recipe materials or required stations.
|
|
||||||
|
|
||||||
## Meadows
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [x] Workbench
|
|
||||||
- [x] Chopping Block
|
|
||||||
- [x] Tanning Rack
|
|
||||||
- [x] Cooking Station
|
|
||||||
- [x] Beehive
|
|
||||||
- [x] Bed
|
|
||||||
- [x] Chest
|
|
||||||
- [x] Sign
|
|
||||||
- [x] Campfire
|
|
||||||
- [x] Standing Wood Torch
|
|
||||||
- [x] Deer Rug
|
|
||||||
- [x] Mysterious Rock
|
|
||||||
- [x] Wood Stack
|
|
||||||
- [x] Stone Pile
|
|
||||||
- [x] Flint Pile
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [x] Stone Axe
|
|
||||||
- [x] Flint Axe
|
|
||||||
- [x] Early Axes
|
|
||||||
- [x] Club
|
|
||||||
- [x] Torch
|
|
||||||
- [x] Flint Spear
|
|
||||||
- [x] Flint Knife
|
|
||||||
- [x] Crude Bow
|
|
||||||
- [x] Hoe
|
|
||||||
- [x] Antler Pickaxe
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [x] Rag Tunic
|
|
||||||
- [x] Rag Pants
|
|
||||||
- [x] Leather Helmet
|
|
||||||
- [x] Leather Tunic
|
|
||||||
- [x] Leather Pants
|
|
||||||
- [x] Deer Hide Cape
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [x] Wood Shield
|
|
||||||
- [x] Wood Tower Shield
|
|
||||||
- [x] Bone Tower Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [x] Wood Arrow
|
|
||||||
- [x] Fire Arrow
|
|
||||||
- [x] Flinthead Arrow
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [x] Tankard
|
|
||||||
|
|
||||||
#### Fireworks
|
|
||||||
|
|
||||||
- [ ] Red fireworks
|
|
||||||
- [ ] Yellow fireworks
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [x] Grilled neck tail
|
|
||||||
- [ ] Cooked boar meat
|
|
||||||
- [x] Honey
|
|
||||||
- [x] Cooked deer meat
|
|
||||||
- [x] Whole roasted Meadow boar
|
|
||||||
|
|
||||||
## Black Forest
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [x] Adze
|
|
||||||
- [x] Cauldron
|
|
||||||
- [x] Mead Ketill
|
|
||||||
- [x] Fermenter
|
|
||||||
- [x] Forge
|
|
||||||
- [x] Forge Cooler
|
|
||||||
- [x] Anvils
|
|
||||||
- [x] Smelter
|
|
||||||
- [x] Charcoal Kiln
|
|
||||||
- [ ] Barrel
|
|
||||||
- [ ] Stool
|
|
||||||
- [ ] Wood Chair
|
|
||||||
- [ ] Sitting Log
|
|
||||||
- [ ] Wood Bench
|
|
||||||
- [x] Table
|
|
||||||
- [x] Item Stand — vertical
|
|
||||||
- [x] Item Stand — horizontal
|
|
||||||
- [ ] Cartography Table
|
|
||||||
- [ ] Barber Station
|
|
||||||
- [ ] Archery Target
|
|
||||||
- [ ] T.W.I.G.
|
|
||||||
- [ ] Wood Dragon Adornment
|
|
||||||
- [ ] Sconce
|
|
||||||
- [ ] Fey Lights
|
|
||||||
- [ ] Flower Garland
|
|
||||||
- [x] Bearskin Rug
|
|
||||||
- [ ] Black Banner
|
|
||||||
- [ ] Blue Banner
|
|
||||||
- [ ] White and Red Striped Banner
|
|
||||||
- [ ] Yellow Banner
|
|
||||||
- [ ] Purple Banner
|
|
||||||
- [ ] Finewood Stack
|
|
||||||
- [ ] Corewood Stack
|
|
||||||
- [x] Coal Pile
|
|
||||||
- [x] Bone Stack
|
|
||||||
- [x] Coin Stack
|
|
||||||
- [x] Coin Pile
|
|
||||||
- [x] Tin Stack
|
|
||||||
- [x] Copper Stack
|
|
||||||
- [x] Bronze Stack
|
|
||||||
- [x] Cart
|
|
||||||
- [x] Portal
|
|
||||||
- [ ] Sharp Stakes
|
|
||||||
- [ ] Ward
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [x] Bronze Axe
|
|
||||||
- [x] Bronze Pickaxe
|
|
||||||
- [x] Cultivator
|
|
||||||
- [x] Butcher Knife
|
|
||||||
- [x] Wooden Sword
|
|
||||||
- [x] Stagbreaker
|
|
||||||
- [x] Bronze Mace
|
|
||||||
- [x] Bronze Sword
|
|
||||||
- [x] Bronze Spear
|
|
||||||
- [x] Bronze Atgeir
|
|
||||||
- [x] Copper Knife
|
|
||||||
- [x] Paws of the Bear
|
|
||||||
- [x] Finewood Bow
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [x] Troll Leather Helmet
|
|
||||||
- [x] Troll Leather Tunic
|
|
||||||
- [x] Troll Leather Pants
|
|
||||||
- [x] Troll Hide Cape
|
|
||||||
- [x] Headdress of the Bear
|
|
||||||
- [x] Patterns of the Bear
|
|
||||||
- [x] Loincloth of the Bear
|
|
||||||
- [x] Bronze Helmet
|
|
||||||
- [x] Bronze Plate Cuirass
|
|
||||||
- [x] Bronze Plate Leggings
|
|
||||||
- [x] Celebratory Cap
|
|
||||||
|
|
||||||
#### Shield
|
|
||||||
|
|
||||||
- [x] Bronze Buckler
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [x] Bronzehead Arrow
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [x] Heart of the Forest
|
|
||||||
- [x] Bronze Pendant
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [x] Mead Horn of Odin
|
|
||||||
|
|
||||||
#### Fireworks
|
|
||||||
|
|
||||||
- [ ] Blue fireworks
|
|
||||||
- [ ] Cyan fireworks
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Carrot
|
|
||||||
- [ ] Boar jerky
|
|
||||||
- [ ] Cooked bear meat
|
|
||||||
- [ ] Minced meat sauce
|
|
||||||
- [ ] Queen's jam
|
|
||||||
- [ ] Carrot soup
|
|
||||||
- [ ] Deer stew
|
|
||||||
- [ ] Pulled Bear
|
|
||||||
- [ ] Black Forest buffet platter
|
|
||||||
- [ ] Mead base: Minor healing `[INFERENCE]`
|
|
||||||
- [ ] Minor healing mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Minor stamina `[INFERENCE]`
|
|
||||||
- [ ] Minor stamina mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Poison resistance `[INFERENCE]`
|
|
||||||
- [ ] Poison resistance mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Tasty `[INFERENCE]`
|
|
||||||
- [ ] Tasty mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Vananidir `[INFERENCE]`
|
|
||||||
- [ ] Draught of Vananidir `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Ratatosk `[INFERENCE]`
|
|
||||||
- [ ] Tonic of Ratatosk `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Troll endurance `[INFERENCE]`
|
|
||||||
- [ ] Mead of Troll endurance `[INFERENCE]`
|
|
||||||
- [ ] Mossy fishing bait
|
|
||||||
|
|
||||||
## Ocean
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Raft
|
|
||||||
- [ ] Karve
|
|
||||||
- [ ] Longship
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Abyssal Razor
|
|
||||||
- [ ] Abyssal Harpoon
|
|
||||||
|
|
||||||
#### Trinket
|
|
||||||
|
|
||||||
- [ ] Fins of Destiny
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Cooked serpent meat `[INFERENCE]`
|
|
||||||
- [ ] Serpent stew `[INFERENCE]`
|
|
||||||
- [ ] Sailor's bounty
|
|
||||||
- [ ] Heavy fishing bait
|
|
||||||
|
|
||||||
## Swamp
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Birds' Nest
|
|
||||||
- [ ] Spice Rack
|
|
||||||
- [ ] Stonecutter
|
|
||||||
- [ ] Food Preparation Table
|
|
||||||
- [ ] Iron Cooking Station
|
|
||||||
- [ ] Forge Bellows
|
|
||||||
- [ ] Grinding Wheel
|
|
||||||
- [ ] Smith's Anvil
|
|
||||||
- [ ] Forge Tool Rack
|
|
||||||
- [ ] Obliterator
|
|
||||||
- [ ] Bonfire
|
|
||||||
- [ ] Hearth
|
|
||||||
- [ ] Hanging Brazier
|
|
||||||
- [ ] Standing Iron Torch
|
|
||||||
- [ ] Standing Blue-burning Iron Torch
|
|
||||||
- [ ] Standing Green-burning Iron Torch
|
|
||||||
- [ ] Resin Candle
|
|
||||||
- [ ] Reinforced Chest
|
|
||||||
- [ ] Personal Chest
|
|
||||||
- [ ] Armour Stand
|
|
||||||
- [ ] Raven Throne
|
|
||||||
- [ ] Red Banner
|
|
||||||
- [ ] Green Banner
|
|
||||||
- [ ] Iron Stack
|
|
||||||
- [ ] Iron Fire Pit
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Iron Axe
|
|
||||||
- [ ] Iron Pickaxe
|
|
||||||
- [ ] Battleaxe
|
|
||||||
- [ ] Iron Mace
|
|
||||||
- [ ] Iron Sledge
|
|
||||||
- [ ] Iron Sword
|
|
||||||
- [ ] Ancient Bark Spear
|
|
||||||
- [ ] Iron Atgeir
|
|
||||||
- [ ] Huntsman Bow
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Root Mask
|
|
||||||
- [ ] Root Harnesk
|
|
||||||
- [ ] Root Leggings
|
|
||||||
- [ ] Iron Helmet
|
|
||||||
- [ ] Iron Scale Mail
|
|
||||||
- [ ] Iron Greaves
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [ ] Banded Shield
|
|
||||||
- [ ] Shield of Roots
|
|
||||||
- [ ] Iron Buckler
|
|
||||||
- [ ] Iron Tower Shield
|
|
||||||
- [ ] Serpent Scale Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Ironhead Arrow
|
|
||||||
|
|
||||||
#### Bombs
|
|
||||||
|
|
||||||
- [ ] Ooze Bomb
|
|
||||||
- [ ] Blob Bomb: Poison
|
|
||||||
- [ ] Blob Bomb: Elite poison
|
|
||||||
|
|
||||||
#### Fishing bait
|
|
||||||
|
|
||||||
- [ ] Mossy Fishing Bait
|
|
||||||
- [ ] Sticky Fishing Bait
|
|
||||||
- [ ] Heavy Fishing Bait
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Iron Brooch
|
|
||||||
- [ ] Nimble Anklet
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [ ] Horn of Celebration
|
|
||||||
|
|
||||||
#### Fireworks
|
|
||||||
|
|
||||||
- [ ] Green fireworks
|
|
||||||
- [ ] Purple fireworks
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Cooked fish
|
|
||||||
- [ ] Muckshake
|
|
||||||
- [ ] Turnip stew
|
|
||||||
- [ ] Sausages
|
|
||||||
- [ ] Black soup
|
|
||||||
- [ ] Swamp dweller's delight
|
|
||||||
- [ ] Mead base: Frost resistance `[INFERENCE]`
|
|
||||||
- [ ] Frost resistance mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Medium healing `[INFERENCE]`
|
|
||||||
- [ ] Medium healing mead `[INFERENCE]`
|
|
||||||
- [ ] Sticky fishing bait
|
|
||||||
|
|
||||||
## Mountain
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Tool Shelf
|
|
||||||
- [ ] Butcher's Table
|
|
||||||
- [ ] Dragon Bed
|
|
||||||
- [ ] Stone Throne
|
|
||||||
- [ ] Standing Brazier
|
|
||||||
- [ ] Blue Standing Brazier
|
|
||||||
- [ ] Wolf Rug
|
|
||||||
- [ ] Treasure Chest
|
|
||||||
- [ ] Silver Stack
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Crystal Battleaxe
|
|
||||||
- [ ] Frostner
|
|
||||||
- [ ] Silver Sword
|
|
||||||
- [ ] Fang Spear
|
|
||||||
- [ ] Silver Knife
|
|
||||||
- [ ] Flesh Rippers
|
|
||||||
- [ ] Draugr Fang
|
|
||||||
- [ ] Scythe
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Fenris Hood
|
|
||||||
- [ ] Fenris Coat
|
|
||||||
- [ ] Fenris Leggings
|
|
||||||
- [ ] Drake Helmet
|
|
||||||
- [ ] Wolf Hide Chestpiece
|
|
||||||
- [ ] Wolf Hide Trousers
|
|
||||||
- [ ] Wolf Fur Cape
|
|
||||||
|
|
||||||
#### Shield
|
|
||||||
|
|
||||||
- [ ] Silver Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Silver Arrow
|
|
||||||
- [ ] Obsidian Arrow
|
|
||||||
- [ ] Poison Arrow
|
|
||||||
- [ ] Frost Arrow
|
|
||||||
|
|
||||||
#### Bomb
|
|
||||||
|
|
||||||
- [ ] Blob Bomb: Frost
|
|
||||||
|
|
||||||
#### Fishing bait
|
|
||||||
|
|
||||||
- [ ] Cold Fishing Bait
|
|
||||||
- [ ] Frosty Fishing Bait
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Wolf Sight
|
|
||||||
- [ ] Crystal Heart
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Onion
|
|
||||||
- [ ] Cooked wolf meat
|
|
||||||
- [ ] Wolf jerky
|
|
||||||
- [ ] Onion soup
|
|
||||||
- [ ] Wolf skewer
|
|
||||||
- [ ] Eyescream
|
|
||||||
- [ ] Hearty Mountain logger's stew
|
|
||||||
- [ ] Mead base: Animal whispers `[INFERENCE]`
|
|
||||||
- [ ] Brew of animal whispers `[INFERENCE]`
|
|
||||||
- [ ] Cold fishing bait
|
|
||||||
- [ ] Frosty fishing bait `[INFERENCE]`
|
|
||||||
|
|
||||||
## Plains
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Artisan Table
|
|
||||||
- [ ] Blast Furnace
|
|
||||||
- [ ] Spinning Wheel
|
|
||||||
- [ ] Windmill
|
|
||||||
- [ ] Stone Oven
|
|
||||||
- [ ] Pots and Pans
|
|
||||||
- [ ] Darkwood Chair
|
|
||||||
- [ ] Long Heavy Table
|
|
||||||
- [ ] Round Table
|
|
||||||
- [ ] Hot Tub
|
|
||||||
- [ ] Black Metal Chest
|
|
||||||
- [ ] Lox Rug
|
|
||||||
- [ ] Straw
|
|
||||||
- [ ] Red Jute Carpet
|
|
||||||
- [ ] Red Jute Curtain
|
|
||||||
- [ ] Raven Adornment
|
|
||||||
- [ ] Wolf Adornment
|
|
||||||
- [ ] Blue, Red and White Banner
|
|
||||||
- [ ] White and Blue Striped Banner
|
|
||||||
- [ ] Orange Banner
|
|
||||||
- [ ] White Banner
|
|
||||||
- [ ] Black Metal Stack
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Black Metal Axe
|
|
||||||
- [ ] Black Metal Battleaxe
|
|
||||||
- [ ] Porcupine
|
|
||||||
- [ ] Black Metal Sword
|
|
||||||
- [ ] Black Metal Atgeir
|
|
||||||
- [ ] Black Metal Knife
|
|
||||||
- [ ] Vilebone Maulclaws
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Padded Helmet
|
|
||||||
- [ ] Padded Cuirass
|
|
||||||
- [ ] Padded Greaves
|
|
||||||
- [ ] Linen Cape
|
|
||||||
- [ ] Lox Cape
|
|
||||||
- [ ] Lox Fur Hood
|
|
||||||
- [ ] Lox Fur Jacket
|
|
||||||
- [ ] Lox Fur Trousers
|
|
||||||
- [ ] Vilebone Visage
|
|
||||||
- [ ] Vilebone Cage
|
|
||||||
- [ ] Vilebone Drapes
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [ ] Black Metal Shield
|
|
||||||
- [ ] Black Metal Tower Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Needle Arrow
|
|
||||||
- [ ] Wooden Missile
|
|
||||||
- [ ] Black Metal Missile
|
|
||||||
|
|
||||||
#### Bomb
|
|
||||||
|
|
||||||
- [ ] Blob Bomb: Tar
|
|
||||||
|
|
||||||
#### Fishing bait
|
|
||||||
|
|
||||||
- [ ] Stingy Fishing Bait
|
|
||||||
- [ ] Misty Fishing Bait
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Bracelets of the Brave
|
|
||||||
- [ ] Evasion Mantle
|
|
||||||
|
|
||||||
#### Accessories and utility
|
|
||||||
|
|
||||||
- [ ] Wisplight
|
|
||||||
- [ ] Lox Saddle
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Cooked egg
|
|
||||||
- [ ] Cooked chicken meat
|
|
||||||
- [ ] Cooked lox meat
|
|
||||||
- [ ] Fish wraps
|
|
||||||
- [ ] Lox meat pie
|
|
||||||
- [ ] Blood pudding
|
|
||||||
- [ ] Bread
|
|
||||||
- [ ] Frosted sweetbread
|
|
||||||
- [ ] Plains pie picnic
|
|
||||||
- [ ] Barley wine base: Fire resistance `[INFERENCE]`
|
|
||||||
- [ ] Fire resistance barley wine `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Medium stamina `[INFERENCE]`
|
|
||||||
- [ ] Medium stamina mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Berserkir `[INFERENCE]`
|
|
||||||
- [ ] Berserkir mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Anti-sting `[INFERENCE]`
|
|
||||||
- [ ] Anti-sting concoction `[INFERENCE]`
|
|
||||||
- [ ] Stingy fishing bait
|
|
||||||
- [ ] Misty fishing bait `[INFERENCE]`
|
|
||||||
|
|
||||||
## Mistlands
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Wisp Fountain
|
|
||||||
- [ ] Artisan Press
|
|
||||||
- [ ] Mortar and Pestle
|
|
||||||
- [ ] Black Forge
|
|
||||||
- [ ] Black Forge Cooler
|
|
||||||
- [ ] Vice
|
|
||||||
- [ ] Galdr Table
|
|
||||||
- [ ] Rune Table
|
|
||||||
- [ ] Unfading Candles
|
|
||||||
- [ ] Sap Extractor
|
|
||||||
- [ ] Eitr Refinery
|
|
||||||
- [ ] Dvergr Wall Lantern
|
|
||||||
- [ ] Dvergr Pole Lantern
|
|
||||||
- [ ] Wisp Torch
|
|
||||||
- [ ] Black Marble Bench
|
|
||||||
- [ ] Black Marble Table
|
|
||||||
- [ ] Black Marble Throne
|
|
||||||
- [ ] Hare Rug
|
|
||||||
- [ ] Blue Jute Carpet
|
|
||||||
- [ ] Blue Jute Curtain
|
|
||||||
- [ ] Blue Jute Drapes
|
|
||||||
- [ ] Yggdrasil Wood Stack
|
|
||||||
- [ ] Black Marble Pile
|
|
||||||
- [ ] Dvergr Sharp Stakes
|
|
||||||
- [ ] Trap
|
|
||||||
- [ ] Ballista
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Jotun Bane
|
|
||||||
- [ ] Skull Splittur
|
|
||||||
- [ ] Demolisher
|
|
||||||
- [ ] Mistwalker
|
|
||||||
- [ ] Krom
|
|
||||||
- [ ] Carapace Spear
|
|
||||||
- [ ] Himminafl
|
|
||||||
- [ ] Skoll and Hati
|
|
||||||
- [ ] Spinesnap
|
|
||||||
- [ ] Arbalest
|
|
||||||
- [ ] Staff of Frost
|
|
||||||
- [ ] Staff of Embers
|
|
||||||
- [ ] Staff of Protection
|
|
||||||
- [ ] Dead Raiser
|
|
||||||
- [ ] Black Metal Pickaxe
|
|
||||||
- [ ] Dvergr Lantern
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Carapace Helmet
|
|
||||||
- [ ] Carapace Breastplate
|
|
||||||
- [ ] Carapace Greaves
|
|
||||||
- [ ] Eitr-weave Hood
|
|
||||||
- [ ] Eitr-weave Robe
|
|
||||||
- [ ] Eitr-weave Trousers
|
|
||||||
- [ ] Feather Cape
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [ ] Carapace Shield
|
|
||||||
- [ ] Carapace Buckler
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Carapace Arrow
|
|
||||||
- [ ] Bone Bolt
|
|
||||||
- [ ] Iron Bolt
|
|
||||||
- [ ] Black Metal Bolt
|
|
||||||
- [ ] Carapace Bolt
|
|
||||||
|
|
||||||
#### Bomb
|
|
||||||
|
|
||||||
- [ ] Bile Bomb
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Pulsating Earrings
|
|
||||||
- [ ] Resounding Shackle
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [ ] Sealbreaker
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Magecap
|
|
||||||
- [ ] Jotun puffs
|
|
||||||
- [ ] Cooked seeker meat
|
|
||||||
- [ ] Cooked hare meat
|
|
||||||
- [ ] Meat platter
|
|
||||||
- [ ] Honey glazed chicken
|
|
||||||
- [ ] Misthare supreme
|
|
||||||
- [ ] Salad
|
|
||||||
- [ ] Mushroom omelette
|
|
||||||
- [ ] Fish 'n' bread
|
|
||||||
- [ ] Stuffed mushroom
|
|
||||||
- [ ] Yggdrasil porridge
|
|
||||||
- [ ] Seeker aspic
|
|
||||||
- [ ] Mushrooms galore á la Mistlands
|
|
||||||
- [ ] Mead base: Major healing `[INFERENCE]`
|
|
||||||
- [ ] Major healing mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Lingering stamina `[INFERENCE]`
|
|
||||||
- [ ] Lingering stamina mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Minor eitr `[INFERENCE]`
|
|
||||||
- [ ] Minor eitr mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Lightfoot `[INFERENCE]`
|
|
||||||
- [ ] Lightfoot mead `[INFERENCE]`
|
|
||||||
|
|
||||||
## Ashlands
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Rolling Pins and Cutting Boards
|
|
||||||
- [ ] Metal Cutter
|
|
||||||
- [ ] Gem Cutter
|
|
||||||
- [ ] Feathery Wreath
|
|
||||||
- [ ] Eternal Pyre
|
|
||||||
- [ ] Shield Generator
|
|
||||||
- [ ] Ashwood Bed
|
|
||||||
- [ ] Ashwood Bench
|
|
||||||
- [ ] Bone Throne
|
|
||||||
- [ ] Grausten Chest
|
|
||||||
- [ ] Small Green Pot
|
|
||||||
- [ ] Medium Green Pot
|
|
||||||
- [ ] Large Green Pot
|
|
||||||
- [ ] Lava Lantern
|
|
||||||
- [ ] Asksvin Rug
|
|
||||||
- [ ] Asksvin Skeleton
|
|
||||||
- [ ] Ashwood Stack
|
|
||||||
- [ ] Grausten Pile
|
|
||||||
- [ ] Flametal Stack
|
|
||||||
- [ ] Pile of Skulls
|
|
||||||
- [ ] Portal Stone
|
|
||||||
- [ ] Drakkar
|
|
||||||
- [ ] Battering Ram
|
|
||||||
- [ ] Catapult
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons
|
|
||||||
|
|
||||||
- [ ] Berserkir Axes
|
|
||||||
- [ ] Bleeding Berserkir Axes
|
|
||||||
- [ ] Thundering Berserkir Axes
|
|
||||||
- [ ] Primal Berserkir Axes
|
|
||||||
- [ ] Flametal Mace
|
|
||||||
- [ ] Bloodgeon
|
|
||||||
- [ ] Storm Star
|
|
||||||
- [ ] Klossen
|
|
||||||
- [ ] Nidhögg
|
|
||||||
- [ ] Nidhögg the Bleeding
|
|
||||||
- [ ] Nidhögg the Thundering
|
|
||||||
- [ ] Nidhögg the Primal
|
|
||||||
- [ ] Dyrnwyn
|
|
||||||
- [ ] Slayer
|
|
||||||
- [ ] Brutal Slayer
|
|
||||||
- [ ] Scourging Slayer
|
|
||||||
- [ ] Primal Slayer
|
|
||||||
- [ ] Splitnir
|
|
||||||
- [ ] Splitnir the Bleeding
|
|
||||||
- [ ] Splitnir the Storming
|
|
||||||
- [ ] Splitnir the Primal
|
|
||||||
- [ ] Ash Fang
|
|
||||||
- [ ] Blood Fang
|
|
||||||
- [ ] Storm Fang
|
|
||||||
- [ ] Root Fang
|
|
||||||
- [ ] Ripper
|
|
||||||
- [ ] Wound Ripper
|
|
||||||
- [ ] Storm Ripper
|
|
||||||
- [ ] Root Ripper
|
|
||||||
- [ ] Staff of Fracturing
|
|
||||||
- [ ] Dundr
|
|
||||||
- [ ] Staff of the Wild
|
|
||||||
- [ ] Trollstav
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Hood of Embla
|
|
||||||
- [ ] Robes of Embla
|
|
||||||
- [ ] Trousers of Embla
|
|
||||||
- [ ] Hood of Ask
|
|
||||||
- [ ] Breastplate of Ask
|
|
||||||
- [ ] Trousers of Ask
|
|
||||||
- [ ] Flametal Helmet
|
|
||||||
- [ ] Flametal Breastplate
|
|
||||||
- [ ] Flametal Greaves
|
|
||||||
- [ ] Asksvin Cloak
|
|
||||||
- [ ] Ashen Cape
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [ ] Flametal Shield
|
|
||||||
- [ ] Flametal Tower Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Charred Arrow
|
|
||||||
- [ ] Charred Bolt
|
|
||||||
- [ ] Flametal Missile
|
|
||||||
- [ ] Grausten Payload
|
|
||||||
- [ ] Explosive Payload
|
|
||||||
|
|
||||||
#### Bombs
|
|
||||||
|
|
||||||
- [ ] Smoke Bomb
|
|
||||||
- [ ] Basalt Bomb
|
|
||||||
- [ ] Blob Bomb: Lava
|
|
||||||
|
|
||||||
#### Fishing bait
|
|
||||||
|
|
||||||
- [ ] Hot Fishing Bait
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Jörmundling
|
|
||||||
- [ ] Brimstone
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [ ] Asksvin Saddle
|
|
||||||
- [ ] Bell
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Vineberry cluster
|
|
||||||
- [ ] Cooked asksvin tail
|
|
||||||
- [ ] Cooked volture meat
|
|
||||||
- [ ] Cooked bonemaw meat
|
|
||||||
- [ ] Fiery svinstew
|
|
||||||
- [ ] Mashed meat
|
|
||||||
- [ ] Piquant pie
|
|
||||||
- [ ] Spicy marmalade
|
|
||||||
- [ ] Scorching medley
|
|
||||||
- [ ] Roasted crust pie
|
|
||||||
- [ ] Sizzling berry broth
|
|
||||||
- [ ] Sparkling shroomshake
|
|
||||||
- [ ] Marinated greens
|
|
||||||
- [ ] Ashlands gourmet bowl
|
|
||||||
- [ ] Mead base: Lingering health `[INFERENCE]`
|
|
||||||
- [ ] Lingering healing mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Lingering eitr `[INFERENCE]`
|
|
||||||
- [ ] Lingering eitr mead `[INFERENCE]`
|
|
||||||
- [ ] Hot fishing bait
|
|
||||||
|
|
||||||
## Deep North
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Eternal Pyre
|
|
||||||
- [ ] Frigid Kiln
|
|
||||||
- [ ] Frost Foundry
|
|
||||||
- [ ] Smith's Aprons
|
|
||||||
- [ ] Smoker
|
|
||||||
- [ ] Standing Loom
|
|
||||||
- [ ] Timberwood Drawbridge
|
|
||||||
- [ ] Antler Throne
|
|
||||||
- [ ] Carved Bench
|
|
||||||
- [ ] Carved Chair
|
|
||||||
- [ ] Hooded Lantern
|
|
||||||
- [ ] Long Carved Table
|
|
||||||
- [ ] Moose Hide Carpet
|
|
||||||
- [ ] Sealskin Rug
|
|
||||||
- [ ] Snow Lantern
|
|
||||||
- [ ] Square Carved Table
|
|
||||||
- [ ] Wardrobe
|
|
||||||
- [ ] Bloodgold Stack
|
|
||||||
- [ ] Timberwood Stack
|
|
||||||
- [ ] Oat Straw `[INFERENCE]`
|
|
||||||
- [ ] Seed Kale
|
|
||||||
- [ ] Timberwood Sapling
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Equipment and inventory
|
|
||||||
|
|
||||||
- [ ] Nord Atgeir
|
|
||||||
- [ ] Nord Axe
|
|
||||||
- [ ] Nord Bow
|
|
||||||
- [ ] Nord Crossbow
|
|
||||||
- [ ] Nord Dagger
|
|
||||||
- [ ] Nord Greataxe
|
|
||||||
- [ ] Nord Greatsword
|
|
||||||
- [ ] Nord Knucklechains
|
|
||||||
- [ ] Nord Mace
|
|
||||||
- [ ] Nord Sledge
|
|
||||||
- [ ] Nord Spear
|
|
||||||
- [ ] Nord Sword
|
|
||||||
- [ ] Frostfire Atgeir
|
|
||||||
- [ ] Frostfire Axe
|
|
||||||
- [ ] Frostfire Bow
|
|
||||||
- [ ] Frostfire Crossbow
|
|
||||||
- [ ] Frostfire Dagger
|
|
||||||
- [ ] Frostfire Greataxe
|
|
||||||
- [ ] Frostfire Greatsword
|
|
||||||
- [ ] Frostfire Knucklechains
|
|
||||||
- [ ] Frostfire Mace
|
|
||||||
- [ ] Frostfire Sledge
|
|
||||||
- [ ] Frostfire Spear
|
|
||||||
- [ ] Frostfire Sword
|
|
||||||
- [ ] Thunderblood Atgeir
|
|
||||||
- [ ] Thunderblood Axe
|
|
||||||
- [ ] Thunderblood Bow
|
|
||||||
- [ ] Thunderblood Crossbow
|
|
||||||
- [ ] Thunderblood Dagger
|
|
||||||
- [ ] Thunderblood Greataxe
|
|
||||||
- [ ] Thunderblood Greatsword
|
|
||||||
- [ ] Thunderblood Knucklechains
|
|
||||||
- [ ] Thunderblood Mace
|
|
||||||
- [ ] Thunderblood Sledge
|
|
||||||
- [ ] Thunderblood Spear
|
|
||||||
- [ ] Thunderblood Sword
|
|
||||||
- [ ] Echo Spike
|
|
||||||
- [ ] Lightning Strike
|
|
||||||
- [ ] Northern Vengeance
|
|
||||||
- [ ] Spirit Caller
|
|
||||||
- [ ] Nord Buckler
|
|
||||||
- [ ] Nord Greatshield
|
|
||||||
- [ ] Nord Shield
|
|
||||||
- [ ] Helmet of the Protector
|
|
||||||
- [ ] Breastplate of the Protector
|
|
||||||
- [ ] Trousers of the Protector
|
|
||||||
- [ ] Hood of the Vanguard
|
|
||||||
- [ ] Chestpiece of the Vanguard
|
|
||||||
- [ ] Trousers of the Vanguard
|
|
||||||
- [ ] Headdress of the Caller
|
|
||||||
- [ ] Robes of the Caller
|
|
||||||
- [ ] Trousers of the Caller
|
|
||||||
- [ ] Cape of the Caller
|
|
||||||
- [ ] Moose Hide Cape
|
|
||||||
- [ ] Crown of Valheim `[INFERENCE]`
|
|
||||||
- [ ] Fishing Hat
|
|
||||||
- [ ] Neckstabber `[INFERENCE]`
|
|
||||||
- [ ] Witch Crown
|
|
||||||
- [ ] Snow Shovel
|
|
||||||
- [ ] Intricate Key
|
|
||||||
- [ ] Moose Saddle `[INFERENCE]`
|
|
||||||
- [ ] Bloodgold Arrow
|
|
||||||
- [ ] Bloodgold Bolt
|
|
||||||
- [ ] Bloodgold Missile
|
|
||||||
- [ ] Bloodgold Payload
|
|
||||||
- [ ] Blob Bomb: Pulp `[INFERENCE]`
|
|
||||||
- [ ] Ember Charge
|
|
||||||
- [ ] Snowball
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Baked Poteitr
|
|
||||||
- [ ] Cooked Moose Meat
|
|
||||||
- [ ] Cooked Seal Blubber `[INFERENCE]`
|
|
||||||
- [ ] Fish Soup `[INFERENCE]`
|
|
||||||
- [ ] Kale *(also the cultivator crop piece; listed once)*
|
|
||||||
- [ ] Kale Chips `[INFERENCE]`
|
|
||||||
- [ ] Lingonberries
|
|
||||||
- [ ] Lingonberry Juice
|
|
||||||
- [ ] Luminous Larva
|
|
||||||
- [ ] Meat In Bread
|
|
||||||
- [ ] Meatballs and Poteitr
|
|
||||||
- [ ] Oat Milk `[INFERENCE]`
|
|
||||||
- [ ] Oatmeal
|
|
||||||
- [ ] Oats `[INFERENCE]`
|
|
||||||
- [ ] Oven Pancake
|
|
||||||
- [ ] Pancakes
|
|
||||||
- [ ] Poteitr `[INFERENCE]`
|
|
||||||
- [ ] Seal Meat Soup
|
|
||||||
- [ ] Smoked Fish
|
|
||||||
- [ ] Smoked Moose Meat
|
|
||||||
- [ ] Northern Morning Fare
|
|
||||||
- [ ] Oat Flour `[INFERENCE]`
|
|
||||||
- [ ] Oven Pancake Batter
|
|
||||||
- [ ] Raw Kale Chips `[INFERENCE]`
|
|
||||||
- [ ] Unbaked Poteitr
|
|
||||||
- [ ] Frosty fishing bait
|
|
||||||
|
|
||||||
## Seasonal
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Maypole
|
|
||||||
- [ ] Jack-o-turnip
|
|
||||||
- [ ] Mistletoe
|
|
||||||
- [ ] Yule Tree
|
|
||||||
- [ ] Yule Wreath
|
|
||||||
- [ ] Yule Garland
|
|
||||||
- [ ] Yuleklapp — small
|
|
||||||
- [ ] Yuleklapp — medium
|
|
||||||
- [ ] Yuleklapp — large
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Midsummer Crown
|
|
||||||
- [ ] Pointy Hat
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
The Valheim Wiki gives most current lists and progression tables.
|
|
||||||
Deep North names use the 1.0 notes and game-derived catalogs when Wiki pages lag release data.
|
|
||||||
|
|
||||||
- https://valheim.wiki/w/Weapons
|
|
||||||
- https://valheim.wiki/w/Armor
|
|
||||||
- https://valheim.wiki/w/Tools
|
|
||||||
- https://valheim.wiki/w/Accessories
|
|
||||||
- https://valheim.wiki/w/Trinkets
|
|
||||||
- https://valheim.wiki/w/Food
|
|
||||||
- https://valheim.wiki/w/Mead
|
|
||||||
- https://valheim.wiki/w/Bait
|
|
||||||
- https://valheim.wiki/w/Building
|
|
||||||
- https://valheim.wiki/w/Deep_North
|
|
||||||
- https://www.valheimgame.com/news/valheim-1-0-has-arrived-/
|
|
||||||
- https://www.valheimplanner.com/recipes/
|
|
||||||
- https://www.valheimplanner.com/build/
|
|
||||||
- https://www.valheimplanner.com/food/
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- Credits
|
|
||||||
date-created: 2026-10-17 18:08
|
|
||||||
date-modified: 2026-10-17 23:33
|
|
||||||
---
|
|
||||||
|
|
||||||
# Credits
|
|
||||||
|
|
||||||
| Path | Original | Notes | |
|
|
||||||
|-------------------------------------|-----------------------------------------------------------------------------------------|-------|---|
|
|
||||||
| `//src/skills/agent-writing-skills` | <https://github.com/mattpocock/skills/tree/main/skills/productivity/writing-for-agents> | | |
|
|
||||||
| `//src/skills/retrospect` | <https://github.com/mattpocock/skills/tree/main/skills/engineering/retro> | | |
|
|
||||||
| | | | |
|
|
||||||
@@ -5,28 +5,22 @@ date-created: 2026-09-23 07:13
|
|||||||
# Valheim Progression
|
# Valheim Progression
|
||||||
|
|
||||||
## Notation & Terms
|
## Notation & Terms
|
||||||
| Notation | Meaning |
|
| Notation | Meaning |
|
||||||
| --------- | ------------------------------------------------------------------ |
|
| -------- | ---------------------------------------------------------------- |
|
||||||
| A <- B | A can be OBTAINED from/through B |
|
| A <- B | A can be OBTAINED from/through B |
|
||||||
| A <- B{x} | A can be purchased from B for x coins |
|
| A => B | A DISCOVERS/UNLOCKS B in-game |
|
||||||
| A => B | A DISCOVERS/UNLOCKS B in-game |
|
| A -> B | A is REQUIRED to make/do B |
|
||||||
| A -> B | A is REQUIRED to make/do B |
|
| ![B] A | Completion objective of B category |
|
||||||
| A ~> B | A is an UPGRADE for B |
|
| A @ B | A is FOUND/LOCATED at/in B |
|
||||||
| ![B] A | Completion objective of B category |
|
| ^ A#B | A is an alternative method for B |
|
||||||
| A @ B | A is FOUND/LOCATED at/in B |
|
| ? A | Open question about A |
|
||||||
| ^ A#B | A is an alternative method for B |
|
| x A | A is prohibited/illegal method |
|
||||||
| ? A | Open question about A |
|
| A + B | A AND B |
|
||||||
| x A | A is prohibited/illegal method |
|
| A \| B | A OR B |
|
||||||
| A + B | A AND B |
|
| \* A | A is available from the START / has no progression prerequisites |
|
||||||
| A \| B | A OR B |
|
| A[n] | A with game-defined LEVEL/TIER n |
|
||||||
| \* A | A is available from the START / has no progression prerequisites |
|
|
||||||
| A[n] | A at QUALITY/LEVEL n |
|
|
||||||
| A$n | A with CAPABILITY tier n (higher tiers satisfy lower requirements) |
|
|
||||||
| | |
|
|
||||||
|
|
||||||
- [farm]: a deliberately constructed setup that repeatedly produces a resource or creature drop more efficiently/reliably than ordinary gathering, though it may require consumable inputs or periodic maintenance.
|
- [farm]: a deliberately constructed setup that repeatedly produces a resource or creature drop more efficiently/reliably than ordinary gathering, though it may require consumable inputs or periodic maintenance.
|
||||||
- [build]: furniture, whether functional or decorative only.
|
|
||||||
- [craft]: craft the item
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
@@ -46,13 +40,13 @@ Copper Ore{1} + Coal{2} @ Smelter -> Copper
|
|||||||
|
|
||||||
Copper Ore <- Mine Copper Deposit
|
Copper Ore <- Mine Copper Deposit
|
||||||
|
|
||||||
Copper Deposit + Pickaxe$0 -> Mine Copper Deposit
|
Copper Deposit + Pickaxe[0] -> Mine Copper Deposit
|
||||||
|
|
||||||
Copper Deposit @ Black Forest
|
Copper Deposit @ Black Forest
|
||||||
|
|
||||||
* Black Forest
|
* Black Forest
|
||||||
|
|
||||||
Antler Pickaxe -> Pickaxe$0
|
Antler Pickaxe -> Pickaxe[0]
|
||||||
|
|
||||||
Wood + Hard Antler + Workbench[1] => Antler Pickaxe
|
Wood + Hard Antler + Workbench[1] => Antler Pickaxe
|
||||||
|
|
||||||
@@ -102,7 +96,7 @@ Tin Ore{1} + Coal{2} @ Smelter -> Tin
|
|||||||
|
|
||||||
Tin Ore <- Mine Tin Deposit
|
Tin Ore <- Mine Tin Deposit
|
||||||
|
|
||||||
Tin Deposit + Pickaxe$0 -> Mine Tin Deposit
|
Tin Deposit + Pickaxe[0] -> Mine Tin Deposit
|
||||||
|
|
||||||
Tin Deposit @ Black Forest
|
Tin Deposit @ Black Forest
|
||||||
|
|
||||||
@@ -110,9 +104,9 @@ Stone + Coal + Wood + Copper + Workbench[1] + Hammer => Forge[1]
|
|||||||
|
|
||||||
Stone{4} + Coal{4} + Wood{10} + Copper{6} @ Workbench[1] -> Forge[1]
|
Stone{4} + Coal{4} + Wood{10} + Copper{6} @ Workbench[1] -> Forge[1]
|
||||||
|
|
||||||
Chain @ Sunken Crypts
|
Chain @ Sunken Crypt
|
||||||
|
|
||||||
Swamp Key -> Sunken Crypts
|
Swamp Key -> Sunken Crypt
|
||||||
|
|
||||||
Swamp Key <- Kill Elder
|
Swamp Key <- Kill Elder
|
||||||
|
|
||||||
@@ -124,144 +118,8 @@ Ancient Seed <- Greydwarf Nest
|
|||||||
|
|
||||||
Elder Forsaken Altar @ Black Forest
|
Elder Forsaken Altar @ Black Forest
|
||||||
|
|
||||||
Sunken Crypts @ Swamp
|
Sunken Crypt @ Swamp
|
||||||
|
|
||||||
* Swamp
|
* Swamp
|
||||||
|
|
||||||
Greydwarf Nest @ Black Forest
|
Greydwarf Nest @ Black Forest
|
||||||
|
|
||||||
![build] Hearth
|
|
||||||
|
|
||||||
Stone + Stonecutter[1] + Hammer => Hearth
|
|
||||||
|
|
||||||
Stone{15} @ Stonecutter[1] -> Hearth
|
|
||||||
|
|
||||||
Wood + Iron + Stone + Workbench[1] + Hammer => Stonecutter[1]
|
|
||||||
|
|
||||||
Wood{10} + Iron{2} + Stone{4} @ Workbench[1] -> Stonecutter[1]
|
|
||||||
|
|
||||||
Scrap Iron{1} + Coal{2} @ Smelter -> Iron
|
|
||||||
|
|
||||||
Scrap Iron <- Sunken Crypts
|
|
||||||
|
|
||||||
Scrap Iron <- Mine Muddy Scrap Pile
|
|
||||||
|
|
||||||
Muddy Scrap Pile + Pickaxe$0 -> Mine Muddy Scrap Pile
|
|
||||||
|
|
||||||
Muddy Scrap Pile @ Sunken Crypts
|
|
||||||
|
|
||||||
![build] Iron Cooking Station
|
|
||||||
|
|
||||||
Iron + Chain + Forge[1] + Hammer => Iron Cooking Station
|
|
||||||
|
|
||||||
Iron{3} + Chain{3} @ Forge[1] -> Iron Cooking Station
|
|
||||||
|
|
||||||
![build] Obliterator
|
|
||||||
|
|
||||||
Iron + Copper + Thunder Stone + Forge[1] + Hammer => Obliterator
|
|
||||||
|
|
||||||
Iron{8} + Copper{4} + Thunder Stone{1} @ Forge[1] -> Obliterator
|
|
||||||
|
|
||||||
Thunder Stone <- Haldor{50}
|
|
||||||
|
|
||||||
Haldor @ Black Forest
|
|
||||||
|
|
||||||
Coins <- Troll
|
|
||||||
|
|
||||||
Troll @ Black Forest
|
|
||||||
|
|
||||||
Coins @ Burial Chambers
|
|
||||||
|
|
||||||
Coins @ Troll Cave
|
|
||||||
|
|
||||||
Troll Cave @ Black Forest
|
|
||||||
|
|
||||||
Coins @ Sunken Crypts
|
|
||||||
|
|
||||||
![craft] Iron Greaves
|
|
||||||
|
|
||||||
Iron + Deer Hide + Forge[2] => Iron Greaves
|
|
||||||
|
|
||||||
Iron{20} + Deer Hide{2} @ Forge[2] -> Iron Greaves
|
|
||||||
|
|
||||||
![craft] Iron Scale Mail
|
|
||||||
|
|
||||||
Iron + Deer Hide + Forge[2] => Iron Scale Mail
|
|
||||||
|
|
||||||
Iron{20} + Deer Hide{2} @ Forge[2] -> Iron Scale Mail
|
|
||||||
|
|
||||||
![build] Forge Tool Rack
|
|
||||||
|
|
||||||
Forge Tool Rack ~> Forge
|
|
||||||
|
|
||||||
Iron + Wood + Workbench[1] + Hammer => Forge Tool Rack
|
|
||||||
|
|
||||||
Iron{15} + Wood{10} @ Workbench[1] -> Forge Tool Rack
|
|
||||||
|
|
||||||
![build] Cauldron
|
|
||||||
|
|
||||||
Tin + Forge[1] + Hammer => Cauldron
|
|
||||||
|
|
||||||
Tin{10} @ Forge[1] -> Cauldron
|
|
||||||
|
|
||||||
![build] Mysterious Rock
|
|
||||||
|
|
||||||
Rock + Coal + Hammer => Mysterious Rock
|
|
||||||
|
|
||||||
Rock{1} + Coal{1} -> Mysterious Rock
|
|
||||||
|
|
||||||
Rock @ Black Forest
|
|
||||||
|
|
||||||
Rock @ Meadows
|
|
||||||
|
|
||||||
Rock @ Swamp
|
|
||||||
|
|
||||||
![build] Chopping Block
|
|
||||||
|
|
||||||
Chopping Block ~> Workbench
|
|
||||||
|
|
||||||
Wood + Flint + Workbench[1] + Hammer => Chopping Block
|
|
||||||
|
|
||||||
Wood{10} + Flint{10} @ Workbench[1] -> Chopping Block
|
|
||||||
|
|
||||||
![build] Tanning Rack
|
|
||||||
|
|
||||||
Tanning Rack ~> Workbench
|
|
||||||
|
|
||||||
Wood + Flint + Leather Scraps + Deer Hide + Workbench[1] + Hammer => Tanning Rack
|
|
||||||
|
|
||||||
Wood{10} + Flint{15} + Leather Scraps{20} + Deer Hide{5} @ Workbench[1] -> Tanning Rack
|
|
||||||
|
|
||||||
* Flint
|
|
||||||
|
|
||||||
Leather Scraps <- Boar
|
|
||||||
|
|
||||||
Boar @ Meadows
|
|
||||||
|
|
||||||
Deer Hide <- Deer
|
|
||||||
|
|
||||||
![build] Adze
|
|
||||||
|
|
||||||
Adze ~> Workbench
|
|
||||||
|
|
||||||
Finewood + Bronze + Forge[1] + Hammer => Adze
|
|
||||||
|
|
||||||
Finewood{10} + Bronze{3} @ Forge[1] -> Adze
|
|
||||||
|
|
||||||
Finewood <- Chop Birch
|
|
||||||
|
|
||||||
Birch + Axe[2] -> Chop Birch
|
|
||||||
|
|
||||||
Birch @ Meadows
|
|
||||||
|
|
||||||
Bronze Axe -> Axe[2]
|
|
||||||
|
|
||||||
Finewood <- Chop Oak
|
|
||||||
|
|
||||||
Oak + Axe[2] -> Chop Oak
|
|
||||||
|
|
||||||
Oak @ Meadows
|
|
||||||
|
|
||||||
Wood + Bronze + Leather Scraps + Forge[1] => Bronze Axe
|
|
||||||
|
|
||||||
Wood{4} + Bronze{8} + Leather Scraps{2} @ Forge[1] -> Bronze Axe
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user