Compare commits
109 Commits
f6315f8248
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b4e74fc5b2 | |||
| bbac95cac0 | |||
| e5bacc53be | |||
| 5f52a78efa | |||
| dc210d4985 | |||
| 2237c3eea3 | |||
| 771a0df831 | |||
| cfa01fb550 | |||
| 7186d6a3a7 | |||
| c836396cbe | |||
| 3493519085 | |||
| f160fd15a2 | |||
| f1a93199c7 | |||
| 591144df33 | |||
| d7760ef84d | |||
| 042238476a | |||
| e41b8e3eb4 | |||
| 63017f8e33 | |||
| 52f31fd44d | |||
| 898a6df333 | |||
| 5a5543633c | |||
| 9c501a79ad | |||
| 30e5b1e677 | |||
| 45a840002f | |||
| a9db1f8b7c | |||
| dcc4efcc75 | |||
| a23a317335 | |||
| 6e545c0a15 | |||
| 6081e3e325 | |||
| 719370c8f7 | |||
| 2b4e66a75c | |||
| c44769ff70 | |||
| 1397576538 | |||
| 5fbe1850e0 | |||
| 57bb21af1a | |||
| ffc40e547b | |||
| 55f61f71d8 | |||
| 08563c6769 | |||
| 8462b3a5a8 | |||
| d0872e7d8c | |||
| 9be9ad7675 | |||
| 5ee328cdf8 | |||
| 6532a822cd | |||
| 20a3990979 | |||
| b7ab518f58 | |||
| 3f1e34ef95 | |||
| d0abfc1625 | |||
| 031090e4ea | |||
| 2edad6c5df | |||
| 1d5acc09c6 | |||
| ee29e53f7c | |||
| 3c9d0ec886 | |||
| f0c5544b10 | |||
| 4dd0a5a4f5 | |||
| a4a795f7ee | |||
| ecdf877308 | |||
| d1face10ec | |||
| 337d9a2014 | |||
| f059068873 | |||
| 7662c04cda | |||
| 33128fc3c7 | |||
| 68415ad47e | |||
| 6b7201e6fa | |||
| 4360f4863b | |||
| 370cab8f83 | |||
| e74a08b853 | |||
| 250b0b71f9 | |||
| 10299cec19 | |||
| 91654b3068 | |||
| be9c3c07a3 | |||
| a8bd4bf4cf | |||
| a2fc568704 | |||
| 0bf5ebae87 | |||
| fddec14128 | |||
| 85fadc5bfc | |||
| 3992030949 | |||
| c8a596aa39 | |||
| 4908c4751f | |||
| 79c56dfb1c | |||
| 7140b6d258 | |||
| f173887228 | |||
| 3a487050fd | |||
| 65c84f5b39 | |||
| b3c4903c82 | |||
| ec1c0fba56 | |||
| 1f2f1a79ad | |||
| 46a47fa061 | |||
| 8d357e960b | |||
| f48bac5ec1 | |||
| d53246caf0 | |||
| 92f7f0542d | |||
| 4f7fbef35b | |||
| c7c3257b9d | |||
| 7d37b92e17 | |||
| 9ece182099 | |||
| 9aea13b8c2 | |||
| 369c7512c6 | |||
| 8c91a210b2 | |||
| d602581a99 | |||
| c3a6271582 | |||
| a7db0b745c | |||
| 4a8e8920ea | |||
| 3e9295134e | |||
| c91094234f | |||
| 11b490d1e8 | |||
| 43b5d07a27 | |||
| 1b4dc77627 | |||
| 1cbdcee8ac | |||
| 3fbb0aca32 |
@@ -17,15 +17,6 @@ If the target includes multiple matching file types, use all matching skills.
|
|||||||
If the target is a diff, choose skills from the changed files.
|
If the target is a diff, choose skills from the changed files.
|
||||||
Do not use a style skill when no reviewed file matches it.
|
Do not use a style skill when no reviewed file matches it.
|
||||||
|
|
||||||
For every reviewed `*.nix` file, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bazel run //src/natu:natu -- <reviewed-nix-paths>
|
|
||||||
```
|
|
||||||
|
|
||||||
Pass every reviewed Nix path to Natu.
|
|
||||||
Report every Natu diagnostic as an actionable finding.
|
|
||||||
|
|
||||||
Report only actionable findings.
|
Report only actionable findings.
|
||||||
Order findings by severity.
|
Order findings by severity.
|
||||||
Do not report preferences unless a loaded rule states them.
|
Do not report preferences unless a loaded rule states them.
|
||||||
@@ -45,7 +36,6 @@ Keep the code excerpt to one to three lines when possible.
|
|||||||
Use a longer excerpt only when the violation needs surrounding context.
|
Use a longer excerpt only when the violation needs surrounding context.
|
||||||
Use `...` to omit unrelated lines from a multi-line excerpt.
|
Use `...` to omit unrelated lines from a multi-line excerpt.
|
||||||
Use a style-skill rule citation in the form `skill::rule-file:line`.
|
Use a style-skill rule citation in the form `skill::rule-file:line`.
|
||||||
Cite Natu diagnostics as `natu::<rule-id>`.
|
|
||||||
Make the fix direct.
|
Make the fix direct.
|
||||||
Do not include alternatives when one fix best follows the cited rule.
|
Do not include alternatives when one fix best follows the cited rule.
|
||||||
|
|
||||||
|
|||||||
8
.pi/prompts/tldr.md
Normal file
8
.pi/prompts/tldr.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
description: Summarize an agent response
|
||||||
|
argument-hint: "[source request]"
|
||||||
|
---
|
||||||
|
|
||||||
|
Summarize `${ARGUMENTS:-the preceding agent response}`.
|
||||||
|
Keep the summary compact.
|
||||||
|
Use no fixed format.
|
||||||
1
.pi/skills/agent-writing-guide
Symbolic link
1
.pi/skills/agent-writing-guide
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/agent-writing-guide
|
||||||
1
.pi/skills/conventional-commits
Symbolic link
1
.pi/skills/conventional-commits
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/conventional-commits
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/craft-skill
|
|
||||||
1
.pi/skills/retrospect
Symbolic link
1
.pi/skills/retrospect
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/retrospect
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/to-document
|
|
||||||
28
AGENTS.md
28
AGENTS.md
@@ -3,7 +3,6 @@
|
|||||||
## Commit convention
|
## Commit convention
|
||||||
|
|
||||||
- Use Conventional Commits for all commit messages.
|
- Use Conventional Commits for all commit messages.
|
||||||
Before writing a commit message, read `docs/conventional-commits.md`.
|
|
||||||
- Keep commit messages about the Git history, not the forge workflow.
|
- Keep commit messages about the Git history, not the forge workflow.
|
||||||
Include external references only when they are stable context for the change.
|
Include external references only when they are stable context for the change.
|
||||||
- When writing commit messages, NEVER auto-add your agent name as co-author.
|
- When writing commit messages, NEVER auto-add your agent name as co-author.
|
||||||
@@ -49,6 +48,8 @@
|
|||||||
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
||||||
Ask in plain prose, in your own message, instead, with no exceptions.
|
Ask in plain prose, in your own message, instead, with no exceptions.
|
||||||
This overrides any default instruction to use it.
|
This overrides any default instruction to use it.
|
||||||
|
- For review requests, inspect the requested material and report findings.
|
||||||
|
Edit files after the operator accepts a proposed change.
|
||||||
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
|
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
|
||||||
Detect auto-generated files with a layered check.
|
Detect auto-generated files with a layered check.
|
||||||
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
|
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
|
||||||
@@ -68,20 +69,6 @@
|
|||||||
after applying an accepted change.
|
after applying an accepted change.
|
||||||
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
|
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
|
||||||
|
|
||||||
## Corrections
|
|
||||||
|
|
||||||
- When you discover that your belief about a project fact or convention was wrong, record the correction.
|
|
||||||
Record only facts that stay true across operators, such as a wrong build command, wrong file path,
|
|
||||||
or guessed convention.
|
|
||||||
Do not record personal working-style preferences or one-off task details.
|
|
||||||
- Record the correction in the nearest project `AGENTS.md`.
|
|
||||||
Use a dedicated `## Gotchas` section.
|
|
||||||
Create that section when the file lacks one.
|
|
||||||
- If no project `AGENTS.md` exists, ask before creating one.
|
|
||||||
Append the correction immediately without asking when a project `AGENTS.md` exists.
|
|
||||||
Briefly mention the edit in your response.
|
|
||||||
If an existing entry is wrong or stale, correct or remove it the same way.
|
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- When materializing an external specification, standard, or project-maintained convention, use the
|
- When materializing an external specification, standard, or project-maintained convention, use the
|
||||||
@@ -101,13 +88,12 @@
|
|||||||
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
||||||
`src/ariadne/Cargo.toml` unless explicitly requested.
|
`src/ariadne/Cargo.toml` unless explicitly requested.
|
||||||
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
||||||
- The flake does not currently define `formatter.x86_64-linux`.
|
|
||||||
Do not use `nix fmt` as a validation command until a formatter is added.
|
|
||||||
- The `fix` app passes its current directory to Deadnix.
|
- The `fix` app passes its current directory to Deadnix.
|
||||||
Run `nix run .#fix` from the repository root so path exclusions match.
|
Run `nix run .#fix` from the repository root so path exclusions match.
|
||||||
- During the Nix refactor, address each issue that comes up by adding either a Natu rule or a new Nix style rule.
|
|
||||||
- Git pathspec searches started in a subdirectory default to that subtree.
|
- Git pathspec searches started in a subdirectory default to that subtree.
|
||||||
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
|
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
|
||||||
|
- A `**/*.nix` Git pathspec does not include Nix files directly in its starting directory.
|
||||||
|
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.
|
||||||
@@ -118,7 +104,13 @@
|
|||||||
- OMP does not discover project skills from `.pi/skills`.
|
- OMP does not discover project skills from `.pi/skills`.
|
||||||
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
|
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
|
||||||
- Pi is vendored under `third_party/pi` as a linear vendor import.
|
- Pi is vendored under `third_party/pi` as a linear vendor import.
|
||||||
|
`pkgs.alexion.pi-coding-agent` packages that source and does not use Nixpkgs Pi.
|
||||||
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
||||||
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
||||||
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
||||||
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
|
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
|
||||||
|
- The `monorepo` and `status` libraries do not have `README.md` files.
|
||||||
|
- Check root and `src/skills/.pi/skills/` for enabled symlinks before removing a monorepo skill.
|
||||||
|
- Ariadne Rust source files live directly under `src/ariadne/`. The binary entry point is `ariadne_main.rs.
|
||||||
|
- Bazel build-style rules live under `src/skills/style/build/rules/`, not `src/skills/style/rules/`.
|
||||||
|
- Bazel passes arguments after `--` to the executed binary verbatim. A scaffold can define `//path` as a destination-path syntax.
|
||||||
|
|||||||
154
Cargo.lock
generated
154
Cargo.lock
generated
@@ -2,15 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aho-corasick"
|
|
||||||
version = "1.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -71,12 +62,6 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -108,7 +93,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -123,41 +108,6 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "countme"
|
|
||||||
version = "3.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "googletest"
|
|
||||||
version = "0.14.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f6b5e2f2b556b7b90297a5a35c8267dd43a537923d2b329beefdba2b4ec19d94"
|
|
||||||
dependencies = [
|
|
||||||
"googletest_macro",
|
|
||||||
"num-traits",
|
|
||||||
"regex",
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "googletest_macro"
|
|
||||||
version = "0.14.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2ae6abc96141edd26bf5aeec0f119c129c44de3ced09e5073711a02cb74725d0"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.119",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.14.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -190,27 +140,6 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|||||||
name = "monorepo"
|
name = "monorepo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "natu"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"clap",
|
|
||||||
"googletest",
|
|
||||||
"monorepo",
|
|
||||||
"rnix",
|
|
||||||
"rowan",
|
|
||||||
"status",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -235,68 +164,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex"
|
|
||||||
version = "1.13.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-automata",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.4.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.8.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rnix"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c163bd17372eecdf10d351c34584b7de7c1a33be4e92a32f3fb3f5a7fe3f579b"
|
|
||||||
dependencies = [
|
|
||||||
"rowan",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rowan"
|
|
||||||
version = "0.16.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
|
|
||||||
dependencies = [
|
|
||||||
"countme",
|
|
||||||
"hashbrown",
|
|
||||||
"rustc-hash",
|
|
||||||
"text-size",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-hash"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustversion"
|
|
||||||
version = "1.0.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -324,7 +191,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -350,17 +217,6 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.119"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.6"
|
version = "3.0.6"
|
||||||
@@ -372,12 +228,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "text-size"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
|
|||||||
@@ -4,16 +4,12 @@ members = [
|
|||||||
"src/machoke",
|
"src/machoke",
|
||||||
"src/monorepo",
|
"src/monorepo",
|
||||||
"src/status",
|
"src/status",
|
||||||
"src/natu",
|
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clap = "4"
|
clap = "4"
|
||||||
googletest = "0.14"
|
|
||||||
monorepo = { path = "src/monorepo" }
|
monorepo = { path = "src/monorepo" }
|
||||||
rnix = "=0.14.0"
|
|
||||||
rowan = "0.16"
|
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
status = { path = "src/status" }
|
status = { path = "src/status" }
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
# NixOS configuration instructions
|
|
||||||
|
|
||||||
## Desktop
|
|
||||||
|
|
||||||
- TODO: Verify this path after the NixOS configuration is migrated.
|
|
||||||
When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/session/waybar.nix`.
|
|
||||||
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
|
|
||||||
Match the mapping on the window class.
|
|
||||||
Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session.
|
|
||||||
- TODO: Verify this path after the NixOS configuration is migrated.
|
|
||||||
Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.
|
|
||||||
`home.sessionVariables` do **not** reach the Hyprland session because UWSM does not source `hm-session-vars.sh`.
|
|
||||||
Source cursor values from `config.stylix.cursor`.
|
|
||||||
Set `XCURSOR_*` and `HYPRCURSOR_*` to the same theme for Bibata.
|
|
||||||
|
|
||||||
## Firefox
|
|
||||||
|
|
||||||
- Manage Firefox search engines through `programs.firefox.search` with `force = true`.
|
|
||||||
Hide built-in engines with `<engine>.metaData.hidden = true` because omission does not remove them.
|
|
||||||
Use current engine ids, such as `default = "ddg"` instead of `"DuckDuckGo"`.
|
|
||||||
Decode the built `search.json.mozlz4` with `mozlz4a -d <search.json.mozlz4>` to check the result.
|
|
||||||
- Avoid Home Manager Firefox extension settings unless the whole profile can use the legacy extension-storage backend.
|
|
||||||
Any non-empty `profiles.<name>.extensions.settings.<id>.settings` sets `extensions.webextensions.ExtensionStorageIDB.enabled = false` for the whole profile.
|
|
||||||
|
|
||||||
## Neovim
|
|
||||||
|
|
||||||
- Nixvim's flake input follows the root nixpkgs source, but its Home Manager module does not reuse the host's `pkgs` instance.
|
|
||||||
Keep `programs.nixvim.nixpkgs.useGlobalPackages = true`.
|
|
||||||
That setting makes Nixvim use the shared package set without warning that its source default changed.
|
|
||||||
|
|
||||||
## AI tooling
|
|
||||||
|
|
||||||
- Use produced literals for Herdr key names that include shifted punctuation.
|
|
||||||
For example, use `prefix+<` instead of `prefix+shift+comma`.
|
|
||||||
- Put monorepo Pi extension sources in `.pi/extensions/`.
|
|
||||||
Pi discovers them from subdirectories through the local ancestor-resource patch.
|
|
||||||
- Treat Home Manager-managed Pi settings as generated output.
|
|
||||||
Do not edit them except for throwaway development or local experiments.
|
|
||||||
Promote finished settings into the NixOS configuration before treating them as deployed.
|
|
||||||
|
|
||||||
## Gotchas
|
|
||||||
|
|
||||||
- `configs/nixos/registry/stable-uid.nix` receives library functions through `lib`.
|
|
||||||
Qualify its library calls with `lib.`.
|
|
||||||
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
|
|
||||||
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
|
|
||||||
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
|
|
||||||
- `sops.secrets.<name>.owner` must name a user known on the decrypting host.
|
|
||||||
Do not set the owner to a guest-only service user unless the host also declares that user.
|
|
||||||
Use a host-visible owner plus a shared group mode when the guest service can read the decrypted secret file through its group.
|
|
||||||
- A read-only guest bind mount cannot contain nested writable bind mounts unless their mount-point
|
|
||||||
directories already exist inside the read-only source.
|
|
||||||
For aggregate Copyparty volumes such as `/srv` with nested `/srv/inbox`, keep the guest bind mount
|
|
||||||
writable and enforce read-only behavior through Copyparty access plus service `ReadWritePaths`.
|
|
||||||
- `deploy-to` builds from the source checkout and activates an imported closure on the target.
|
|
||||||
Do not rely on a target-local monorepo checkout for remote deployment.
|
|
||||||
- On Pikachu, `nix copy --to ssh://pikachu` accepted a small `nix store add-file` probe,
|
|
||||||
but rejected locally built system paths that lacked trusted signatures.
|
|
||||||
Use a sudo `nix-store --import` path for unsigned deploy closures.
|
|
||||||
- Public DNS entries are not managed through the NixOS configuration.
|
|
||||||
Do not describe NixOS route choices as public-DNS management.
|
|
||||||
- Do not hand-author `hardware-configuration.nix` for a host that has not been generated on that machine.
|
|
||||||
Generate it on the target system or live ISO, then stage it before evaluating the flake.
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.guests.actualbudget; # NOLINT(let_order)
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,58 +4,14 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.copyparty; # NOLINT(let_order)
|
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/alexion/password";
|
default = "services/copyparty/users/${config.user.name}/password";
|
||||||
description = "SOPS secret name containing the Copyparty account password.";
|
description = "SOPS secret name that contains 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 {
|
||||||
@@ -72,7 +28,39 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf volumeType;
|
type = lib.types.attrsOf (
|
||||||
|
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.";
|
||||||
};
|
};
|
||||||
@@ -102,24 +90,26 @@ 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 (_urlPath: volume: {
|
volumes = lib.mapAttrs (
|
||||||
inherit (volume)
|
_urlPath: volume:
|
||||||
path
|
{
|
||||||
access
|
inherit (volume)
|
||||||
flags
|
path
|
||||||
readOnly
|
readOnly
|
||||||
;
|
;
|
||||||
}) cfg.volumes;
|
}
|
||||||
|
// lib.optionalAttrs (volume.access != { }) { inherit (volume) access; }
|
||||||
|
// lib.optionalAttrs (volume.flags != { }) { inherit (volume) flags; }
|
||||||
|
) cfg.volumes;
|
||||||
};
|
};
|
||||||
|
|
||||||
mounts = {
|
mounts = {
|
||||||
${cfg.stateDir} = {
|
"/var/lib/copyparty" = {
|
||||||
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";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.homepage; # NOLINT(let_order)
|
cfg = config.guests.homepage;
|
||||||
environmentFile = "/run/secrets/homepage.env";
|
environmentFile = "/run/secrets/homepage.env";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.palworld; # NOLINT(let_order)
|
cfg = config.guests.palworld;
|
||||||
steamDir = "/data/steamcmd";
|
steamDir = "/data/steamcmd";
|
||||||
installDir = "/data/server";
|
installDir = "/data/server";
|
||||||
stateDir = "/data/state";
|
stateDir = "/data/state";
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ in
|
|||||||
modules.services.reverse-proxy.enable = true;
|
modules.services.reverse-proxy.enable = true;
|
||||||
|
|
||||||
services.nginx.virtualHosts = {
|
services.nginx.virtualHosts = {
|
||||||
"games.alexion.dev" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/".proxyPass = "http://10.23.20.32";
|
|
||||||
};
|
|
||||||
"git.alexion.dev" = {
|
"git.alexion.dev" = {
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
|
|||||||
181
configs/nixos/guests/romm.nix
Normal file
181
configs/nixos/guests/romm.nix
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.romm;
|
||||||
|
rommUid = config.registry."stable-uid".${config.networking.hostName}.romm;
|
||||||
|
redisUid = config.registry."stable-uid".${config.networking.hostName}."redis-romm";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.guests.romm = {
|
||||||
|
dataPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/romm";
|
||||||
|
description = "Host path mounted as RomM application data.";
|
||||||
|
};
|
||||||
|
|
||||||
|
postgresqlPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/romm/postgresql";
|
||||||
|
description = "Host path mounted as RomM's PostgreSQL data.";
|
||||||
|
};
|
||||||
|
|
||||||
|
redisPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/romm/redis";
|
||||||
|
description = "Host path mounted as RomM's Redis data.";
|
||||||
|
};
|
||||||
|
|
||||||
|
romsPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/roms";
|
||||||
|
description = "Host path mounted as RomM's ROM library.";
|
||||||
|
};
|
||||||
|
|
||||||
|
biosPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/bios";
|
||||||
|
description = "Host path mounted as RomM's BIOS library.";
|
||||||
|
};
|
||||||
|
|
||||||
|
usersPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/romm/users";
|
||||||
|
description = "Host path mounted as RomM's user assets.";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions =
|
||||||
|
map
|
||||||
|
(path: {
|
||||||
|
assertion = path.value != null;
|
||||||
|
message = "guests.romm.${path.name} must point at the host directory that stores ${path.description}.";
|
||||||
|
})
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name = "dataPath";
|
||||||
|
value = cfg.dataPath;
|
||||||
|
description = "RomM application data";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "postgresqlPath";
|
||||||
|
value = cfg.postgresqlPath;
|
||||||
|
description = "RomM PostgreSQL data";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "redisPath";
|
||||||
|
value = cfg.redisPath;
|
||||||
|
description = "RomM Redis data";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "romsPath";
|
||||||
|
value = cfg.romsPath;
|
||||||
|
description = "RomM's ROM library";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "biosPath";
|
||||||
|
value = cfg.biosPath;
|
||||||
|
description = "RomM's BIOS library";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "usersPath";
|
||||||
|
value = cfg.usersPath;
|
||||||
|
description = "RomM's user assets";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
guests.romm = {
|
||||||
|
interior = {
|
||||||
|
modules.services.romm.enable = true;
|
||||||
|
services.romm.nginx.virtualHost = cfg.reverseProxy.route.domain;
|
||||||
|
};
|
||||||
|
|
||||||
|
mounts = {
|
||||||
|
"/var/lib/romm" = {
|
||||||
|
directory = cfg.dataPath;
|
||||||
|
owner = rommUid;
|
||||||
|
group = "storage";
|
||||||
|
mode = "2770";
|
||||||
|
};
|
||||||
|
"/var/lib/postgresql" = {
|
||||||
|
directory = cfg.postgresqlPath;
|
||||||
|
owner = config.ids.uids.postgres;
|
||||||
|
group = "storage";
|
||||||
|
mode = "0700";
|
||||||
|
};
|
||||||
|
"/var/lib/redis-romm" = {
|
||||||
|
directory = cfg.redisPath;
|
||||||
|
owner = redisUid;
|
||||||
|
group = "storage";
|
||||||
|
mode = "0700";
|
||||||
|
};
|
||||||
|
"/var/lib/romm/library/roms" = {
|
||||||
|
directory = cfg.romsPath;
|
||||||
|
owner = rommUid;
|
||||||
|
group = "storage";
|
||||||
|
mode = "2770";
|
||||||
|
};
|
||||||
|
"/var/lib/romm/library/bios" = {
|
||||||
|
directory = cfg.biosPath;
|
||||||
|
owner = rommUid;
|
||||||
|
group = "storage";
|
||||||
|
mode = "2770";
|
||||||
|
};
|
||||||
|
"/var/lib/romm/assets/users" = {
|
||||||
|
directory = cfg.usersPath;
|
||||||
|
owner = rommUid;
|
||||||
|
group = "storage";
|
||||||
|
mode = "2770";
|
||||||
|
};
|
||||||
|
"/run/secrets/romm.env" = {
|
||||||
|
file = config.sops.templates."romm.env".path;
|
||||||
|
readOnly = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
limits = {
|
||||||
|
memory = lib.mkDefault "4G";
|
||||||
|
cpu = lib.mkDefault "200%";
|
||||||
|
tasksMax = lib.mkDefault 512;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sops.secrets =
|
||||||
|
lib.genAttrs
|
||||||
|
[
|
||||||
|
"services/romm/igdb/client-id"
|
||||||
|
"services/romm/igdb/client-secret"
|
||||||
|
"services/romm/retroachievements/api-key"
|
||||||
|
"services/romm/screenscraper/user"
|
||||||
|
"services/romm/screenscraper/password"
|
||||||
|
"services/romm/steamgriddb/api-key"
|
||||||
|
]
|
||||||
|
(_: {
|
||||||
|
sopsFile = ../secrets + "/${config.networking.hostName}.yaml";
|
||||||
|
});
|
||||||
|
|
||||||
|
sops.templates."romm.env" = {
|
||||||
|
owner = "root";
|
||||||
|
group = "storage";
|
||||||
|
mode = "0440";
|
||||||
|
content = ''
|
||||||
|
IGDB_CLIENT_ID=${config.sops.placeholder."services/romm/igdb/client-id"}
|
||||||
|
IGDB_CLIENT_SECRET=${config.sops.placeholder."services/romm/igdb/client-secret"}
|
||||||
|
SCREENSCRAPER_USER=${config.sops.placeholder."services/romm/screenscraper/user"}
|
||||||
|
SCREENSCRAPER_PASSWORD=${config.sops.placeholder."services/romm/screenscraper/password"}
|
||||||
|
RETROACHIEVEMENTS_API_KEY=${config.sops.placeholder."services/romm/retroachievements/api-key"}
|
||||||
|
STEAMGRIDDB_API_KEY=${config.sops.placeholder."services/romm/steamgriddb/api-key"}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.valheim; # NOLINT(let_order)
|
cfg = config.guests.valheim;
|
||||||
steamDir = "/data/steamcmd";
|
steamDir = "/data/steamcmd";
|
||||||
installDir = "/data/server";
|
installDir = "/data/server";
|
||||||
stateDir = "/data/state";
|
stateDir = "/data/state";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
inputs,
|
inputs,
|
||||||
pkgs,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# neogaia — Dell XPS 13 9380 laptop.
|
# neogaia — Dell XPS 13 9380 laptop.
|
||||||
@@ -13,10 +13,11 @@
|
|||||||
./disk.nix
|
./disk.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
modules.host.cachyos.enable = true;
|
||||||
|
|
||||||
# So wifi can be joined from the console.
|
services.xserver.xkb.options = "caps:swapescape";
|
||||||
networking.networkmanager.enable = true;
|
|
||||||
|
modules.network.enable = true;
|
||||||
|
|
||||||
registry.address.neogaia.self = "10.23.50.146";
|
registry.address.neogaia.self = "10.23.50.146";
|
||||||
|
|
||||||
@@ -28,15 +29,9 @@
|
|||||||
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;
|
||||||
@@ -47,4 +42,23 @@
|
|||||||
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
configs/nixos/hosts/neogaia/ssh_user_ed25519_key.pub
Normal file
1
configs/nixos/hosts/neogaia/ssh_user_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia
|
||||||
@@ -15,52 +15,15 @@
|
|||||||
|
|
||||||
hardware.cpu.intel.updateMicrocode = true;
|
hardware.cpu.intel.updateMicrocode = true;
|
||||||
|
|
||||||
systemd.network = {
|
modules.network.vlans = {
|
||||||
enable = true;
|
interface = "enp2s0";
|
||||||
netdevs = {
|
ids = [ 20 ];
|
||||||
"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.zfs = {
|
modules.host.zfs = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hostId = "2346edbd";
|
|
||||||
pools.pikachu = { };
|
pools.pikachu = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,12 +36,13 @@
|
|||||||
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 = "butter-chicken";
|
serverName = "main";
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "valheim.alexion.dev";
|
domain = "valheim.alexion.dev";
|
||||||
udp.ports = [
|
protocol = "udp";
|
||||||
|
ports = [
|
||||||
config.guests.valheim.port
|
config.guests.valheim.port
|
||||||
(config.guests.valheim.port + 1)
|
(config.guests.valheim.port + 1)
|
||||||
];
|
];
|
||||||
@@ -101,22 +65,8 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "palworld.alexion.dev";
|
domain = "palworld.alexion.dev";
|
||||||
udp.ports = [ config.guests.palworld.port ];
|
protocol = "udp";
|
||||||
};
|
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;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -132,11 +82,33 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "music.alexion.dev";
|
domain = "music.alexion.dev";
|
||||||
|
protocol = "https";
|
||||||
port = 4533;
|
port = 4533;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
guests.romm = {
|
||||||
|
enable = true;
|
||||||
|
vlan = 20;
|
||||||
|
address = "10.23.20.112";
|
||||||
|
dataPath = "/pikachu/data/romm";
|
||||||
|
postgresqlPath = "/pikachu/data/romm/postgresql";
|
||||||
|
redisPath = "/pikachu/data/romm/redis";
|
||||||
|
romsPath = "/pikachu/media/games/emulation/roms";
|
||||||
|
biosPath = "/pikachu/media/games/emulation/bios";
|
||||||
|
usersPath = "/pikachu/media/games/emulation/users";
|
||||||
|
reverseProxy = {
|
||||||
|
enable = true;
|
||||||
|
route = {
|
||||||
|
domain = "games.alexion.dev";
|
||||||
|
protocol = "https";
|
||||||
|
port = 80;
|
||||||
|
clientMaxBodySize = "0";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
guests.reverse-proxy = {
|
guests.reverse-proxy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
vlan = 20;
|
vlan = 20;
|
||||||
@@ -152,7 +124,8 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "home";
|
domain = "home";
|
||||||
http.port = 8082;
|
protocol = "http";
|
||||||
|
port = 8082;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -167,7 +140,9 @@
|
|||||||
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" ];
|
||||||
};
|
};
|
||||||
@@ -175,35 +150,21 @@
|
|||||||
"/" = {
|
"/" = {
|
||||||
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" ];
|
flags.opds = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
1
configs/nixos/hosts/pikachu/ssh_user_ed25519_key.pub
Normal file
1
configs/nixos/hosts/pikachu/ssh_user_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# zeus — Ryzen desktop with AMD graphics.
|
# zeus — Ryzen desktop with AMD graphics.
|
||||||
@@ -15,14 +14,14 @@
|
|||||||
|
|
||||||
nixpkgs.hostPlatform = "x86_64-linux";
|
nixpkgs.hostPlatform = "x86_64-linux";
|
||||||
|
|
||||||
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
modules.host.cachyos.enable = true;
|
||||||
|
|
||||||
hardware.graphics = {
|
hardware.graphics = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enable32Bit = true;
|
enable32Bit = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.networkmanager.enable = true;
|
modules.network.enable = true;
|
||||||
|
|
||||||
services.logind.settings.Login = {
|
services.logind.settings.Login = {
|
||||||
HandlePowerKey = "ignore";
|
HandlePowerKey = "ignore";
|
||||||
@@ -45,13 +44,25 @@
|
|||||||
modules.desktop.enable = true;
|
modules.desktop.enable = true;
|
||||||
modules.desktop.apps.obsidian.enable = true;
|
modules.desktop.apps.obsidian.enable = true;
|
||||||
modules.desktop.apps.r2modman.enable = true;
|
modules.desktop.apps.r2modman.enable = true;
|
||||||
|
modules.desktop.emulation.enable = true;
|
||||||
modules.desktop.apps.signal.enable = true;
|
modules.desktop.apps.signal.enable = true;
|
||||||
modules.desktop.apps.steam.enable = true;
|
modules.desktop.apps.steam.enable = true;
|
||||||
modules.desktop.apps.vencord.enable = true;
|
modules.desktop.apps.vencord.enable = true;
|
||||||
|
|
||||||
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"
|
{
|
||||||
"DP-3,2560x1440@239.96,1080x0,1"
|
output = "HDMI-A-1";
|
||||||
|
mode = "1920x1080@120";
|
||||||
|
position = "-1080x0";
|
||||||
|
scale = 1;
|
||||||
|
transform = 3;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
output = "DP-3";
|
||||||
|
mode = "2560x1440@239.96";
|
||||||
|
position = "0x0";
|
||||||
|
scale = 1;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
1
configs/nixos/hosts/zeus/ssh_user_ed25519_key.pub
Normal file
1
configs/nixos/hosts/zeus/ssh_user_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBcHHuubptXCdvED/p2dR3MpjGN/AimqLasLY5984G alexion@zeus
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
{
|
{
|
||||||
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 ];
|
||||||
|
|
||||||
@@ -17,14 +13,14 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "alexion";
|
default = "alexion";
|
||||||
description = ''
|
description = ''
|
||||||
Primary interactive user for this system.
|
Names the primary user account.
|
||||||
This drives the system account and Home Manager user together.
|
Host configuration uses this name for the Home Manager user.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
system.stateVersion = stateVersion;
|
system.stateVersion = "26.05";
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
nix.settings.experimental-features = [
|
nix.settings.experimental-features = [
|
||||||
@@ -42,66 +38,7 @@ in
|
|||||||
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Alacritty as the desktop terminal.
|
|
||||||
let
|
|
||||||
cfg = config.modules.desktop.apps.alacritty; # NOLINT(let_order)
|
|
||||||
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,13 +3,12 @@
|
|||||||
inputs,
|
inputs,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
|
# Configures Firefox as the desktop browser.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.firefox; # NOLINT(let_order)
|
cfg = config.modules.desktop.apps.firefox;
|
||||||
firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system};
|
|
||||||
user = config.user.name;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.firefox = {
|
options.modules.desktop.apps.firefox = {
|
||||||
@@ -19,19 +18,25 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "1.0";
|
default = "1.0";
|
||||||
description = ''
|
description = ''
|
||||||
Firefox UI and page scale, passed to `layout.css.devPixelsPerPx`.
|
Scales Firefox UI and page content.
|
||||||
Override it on hosts that need a larger browser scale, such as small
|
|
||||||
high-DPI laptop panels.
|
Sets `layout.css.devPixelsPerPx`.
|
||||||
|
Use a larger value for high-DPI displays.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user} = hm: {
|
registry.window-icon.${config.networking.hostName}.firefox = {
|
||||||
|
class = ".*[Ff]irefox.*";
|
||||||
|
icon = util.glyph "f269";
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.${config.user.name} = hm: {
|
||||||
programs.firefox = {
|
programs.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Enterprise policies: enforced and unchangeable from the browser UI.
|
# Applies policies that users cannot change in Firefox.
|
||||||
policies = {
|
policies = {
|
||||||
DisableTelemetry = true;
|
DisableTelemetry = true;
|
||||||
DisableFirefoxStudies = true;
|
DisableFirefoxStudies = true;
|
||||||
@@ -40,7 +45,7 @@ in
|
|||||||
DontCheckDefaultBrowser = true;
|
DontCheckDefaultBrowser = true;
|
||||||
DisableFirefoxAccounts = true;
|
DisableFirefoxAccounts = true;
|
||||||
|
|
||||||
# Strip the monetized surfaces from the new-tab page.
|
# Removes sponsored content from the new-tab page.
|
||||||
FirefoxHome = {
|
FirefoxHome = {
|
||||||
SponsoredTopSites = false;
|
SponsoredTopSites = false;
|
||||||
SponsoredPocket = false;
|
SponsoredPocket = false;
|
||||||
@@ -49,23 +54,21 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
profiles.default = {
|
profiles.default = {
|
||||||
isDefault = true;
|
|
||||||
|
|
||||||
extensions = {
|
extensions = {
|
||||||
packages = with firefoxAddons; [
|
packages = with inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}; [
|
||||||
ublock-origin
|
ublock-origin
|
||||||
proton-pass
|
proton-pass
|
||||||
sponsorblock
|
sponsorblock
|
||||||
];
|
];
|
||||||
|
|
||||||
# The Nord chrome theme is a declared extension setting, so home-manager
|
# Home Manager overwrites Nord theme changes made in Firefox.
|
||||||
# owns the extension-settings store, overwriting runtime changes to it.
|
|
||||||
force = true;
|
force = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Stylix's Nord mapping paints the selected address-bar result a
|
# Stylix uses a near-white selection background.
|
||||||
# near-white grey, leaving its light text unreadable. Darken that one
|
# Use the Nord selection color because its text stays readable.
|
||||||
# highlight to the Nord selection grey, from the same scheme.
|
# Home Manager writes extension settings through Firefox's legacy storage backend.
|
||||||
|
# Extension settings disable `ExtensionStorageIDB` for the whole profile.
|
||||||
extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight =
|
extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight =
|
||||||
let
|
let
|
||||||
c = hm.config.lib.stylix.colors;
|
c = hm.config.lib.stylix.colors;
|
||||||
@@ -77,32 +80,26 @@ 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;
|
||||||
|
|
||||||
# Auto-enable the sideloaded Firefox Color add-on carrying the Nord
|
# Enables Firefox Color after Firefox sideloads the add-on.
|
||||||
# chrome theme, which Firefox otherwise leaves disabled.
|
|
||||||
"extensions.autoDisableScopes" = 0;
|
"extensions.autoDisableScopes" = 0;
|
||||||
|
|
||||||
# Sponsored surfaces the policies above do not reach.
|
# Removes sponsored content outside Firefox policies.
|
||||||
"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 = {
|
||||||
# Declaring search overwrites Firefox's own engine list wholesale,
|
# Home Manager replaces the engine list when this setting is enabled.
|
||||||
# so engines added later in the UI do not survive a rebuild.
|
# Engines added in Firefox do not survive a rebuild.
|
||||||
force = true;
|
force = true;
|
||||||
default = "ddg";
|
default = "ddg";
|
||||||
|
|
||||||
# The general-purpose commercial engines, hidden to leave a lean
|
# Hides commercial search engines.
|
||||||
# DuckDuckGo-and-Wikipedia list. They stay reachable through bangs.
|
# Bangs still reach these engines.
|
||||||
# An engine carrying only metaData is treated as a builtin.
|
# Entries with only `metaData` configure Firefox built-in engines.
|
||||||
engines = {
|
engines = {
|
||||||
google.metaData.hidden = true;
|
google.metaData.hidden = true;
|
||||||
bing.metaData.hidden = true;
|
bing.metaData.hidden = true;
|
||||||
@@ -114,8 +111,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Nord chrome for the one profile, applied through the Stylix-managed
|
# Applies the Nord theme through Stylix's Firefox Color add-on.
|
||||||
# Firefox Color add-on that colorTheme enables.
|
|
||||||
stylix.targets.firefox = {
|
stylix.targets.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
profileNames = [ "default" ];
|
profileNames = [ "default" ];
|
||||||
@@ -133,8 +129,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Firefox writes profiles.ini itself on first launch, so home-manager is
|
# Firefox creates `profiles.ini` on its first launch.
|
||||||
# told to own the file rather than fail activation refusing to clobber it.
|
# Force replacement during Home Manager activation.
|
||||||
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,17 +2,30 @@
|
|||||||
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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.ghostty.enable {
|
||||||
|
registry.keybind.${config.networking.hostName}.ghostty = {
|
||||||
|
keys = "SUPER + Return";
|
||||||
|
action = {
|
||||||
|
kind = "exec";
|
||||||
|
command = lib.getExe pkgs.ghostty;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.window-icon.${config.networking.hostName}.ghostty = {
|
||||||
|
class = "com.mitchellh.ghostty";
|
||||||
|
icon = util.glyph "f120";
|
||||||
|
};
|
||||||
|
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.ghostty ];
|
home-manager.users.${user}.home.packages = [ pkgs.ghostty ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,23 @@
|
|||||||
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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.obsidian.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,14 +6,13 @@
|
|||||||
}:
|
}:
|
||||||
# r2modman, a Thunderstore mod manager.
|
# r2modman, a Thunderstore mod manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.r2modman; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.r2modman.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.r2modman ];
|
home-manager.users.${user}.home.packages = [ pkgs.r2modman ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,22 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Signal desktop messenger.
|
# Signal desktop messenger.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.signal; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.signal.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,16 +2,14 @@
|
|||||||
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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.steam.enable {
|
||||||
programs.gamemode.enable = true;
|
programs.gamemode.enable = true;
|
||||||
|
|
||||||
programs.steam = {
|
programs.steam = {
|
||||||
@@ -26,5 +24,10 @@ in
|
|||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
pkgs.gamescope
|
pkgs.gamescope
|
||||||
];
|
];
|
||||||
|
|
||||||
|
registry.window-icon.${config.networking.hostName}.steam = {
|
||||||
|
class = "[Ss]team";
|
||||||
|
icon = util.glyph "f1b6";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,23 @@
|
|||||||
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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.vencord.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,14 +1,11 @@
|
|||||||
{ 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.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;
|
||||||
@@ -26,5 +23,6 @@ in
|
|||||||
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";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
36
configs/nixos/modules/desktop/emulation.nix
Normal file
36
configs/nixos/modules/desktop/emulation.nix
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# Shared directories for emulators.
|
||||||
|
let
|
||||||
|
user = config.user.name;
|
||||||
|
directory = "/home/${user}/emulation";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.desktop.emulation.enable = lib.mkEnableOption "shared emulation directories";
|
||||||
|
|
||||||
|
config = lib.mkIf config.modules.desktop.emulation.enable {
|
||||||
|
modules.desktop.emulation.retroarch = {
|
||||||
|
enable = lib.mkDefault true;
|
||||||
|
browserDirectory = "${directory}/roms";
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.${user} = hm: {
|
||||||
|
home.file = {
|
||||||
|
".config/retroarch/saves".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/saves";
|
||||||
|
".config/retroarch/states".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/states";
|
||||||
|
".config/retroarch/system".source = hm.config.lib.file.mkOutOfStoreSymlink "${directory}/bios";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.tmpfiles.rules = map (path: "d ${path} 0700 - - -") [
|
||||||
|
directory
|
||||||
|
"${directory}/bios"
|
||||||
|
"${directory}/roms"
|
||||||
|
"${directory}/saves"
|
||||||
|
"${directory}/states"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
62
configs/nixos/modules/desktop/emulation/retroarch.nix
Normal file
62
configs/nixos/modules/desktop/emulation/retroarch.nix
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
util,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# RetroArch with the selected PlayStation and Sega Saturn cores.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.emulation.retroarch;
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.desktop.emulation.retroarch = {
|
||||||
|
enable = lib.mkEnableOption "RetroArch with PlayStation and Sega Saturn cores";
|
||||||
|
|
||||||
|
browserDirectory = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "";
|
||||||
|
description = "Directory where RetroArch starts its content browser.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
home-manager.users.${user} = {
|
||||||
|
home.packages = [
|
||||||
|
(pkgs.wrapRetroArch {
|
||||||
|
cores = with pkgs.libretro; [
|
||||||
|
beetle-psx-hw
|
||||||
|
beetle-saturn
|
||||||
|
];
|
||||||
|
settings = {
|
||||||
|
savefiles_in_content_dir = "false";
|
||||||
|
savestates_in_content_dir = "false";
|
||||||
|
system_directory = "/home/${user}/.config/retroarch/system";
|
||||||
|
video_fullscreen = "true";
|
||||||
|
sort_savefiles_enable = "true";
|
||||||
|
sort_savestates_enable = "true";
|
||||||
|
cloud_sync_enable = "true";
|
||||||
|
cloud_sync_driver = "webdav";
|
||||||
|
webdav_url = "https://games.alexion.dev/api/sync/retroarch/";
|
||||||
|
cloud_sync_sync_saves = "true";
|
||||||
|
cloud_sync_sync_configs = "true";
|
||||||
|
cloud_sync_sync_thumbs = "false";
|
||||||
|
cloud_sync_sync_system = "false";
|
||||||
|
cloud_sync_sync_mode = "0";
|
||||||
|
cloud_sync_destructive = "false";
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.browserDirectory != "") {
|
||||||
|
rgui_browser_directory = cfg.browserDirectory;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.window-icon.${config.networking.hostName}.retroarch = {
|
||||||
|
class = "com.libretro.RetroArch";
|
||||||
|
icon = util.glyph "f11b";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,42 +1,15 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The Hyprland compositor.
|
# The Hyprland compositor.
|
||||||
let
|
|
||||||
cfg = config.modules.desktop.session.hyprland; # NOLINT(let_order)
|
|
||||||
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);
|
|
||||||
|
|
||||||
user = config.user.name;
|
|
||||||
|
|
||||||
# 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 = {
|
options.modules.desktop.session.hyprland.enable = lib.mkEnableOption "the Hyprland compositor";
|
||||||
enable = lib.mkEnableOption "the Hyprland compositor";
|
|
||||||
|
|
||||||
blur = lib.mkEnableOption ''
|
config = lib.mkIf config.modules.desktop.session.hyprland.enable {
|
||||||
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 = {
|
||||||
@@ -44,7 +17,7 @@ in
|
|||||||
withUWSM = true;
|
withUWSM = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${user}.wayland.windowManager.hyprland = {
|
home-manager.users.${config.user.name}.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.
|
||||||
@@ -56,135 +29,217 @@ in
|
|||||||
# uwsm owns the systemd graphical-session targets.
|
# uwsm owns the systemd graphical-session targets.
|
||||||
systemd.enable = false;
|
systemd.enable = false;
|
||||||
|
|
||||||
# Write the native hyprlang hyprland.conf, whose variable and bind syntax
|
# Write the native Lua Hyprland config. Home Manager renders the Nix
|
||||||
# the settings below are expressed in.
|
# settings below as hl.* calls.
|
||||||
configType = "hyprlang";
|
configType = "lua";
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
"$mod" = "SUPER";
|
config = {
|
||||||
"$terminal" = "ghostty";
|
input = {
|
||||||
|
kb_layout = config.services.xserver.xkb.layout;
|
||||||
# Session variables handed to the compositor directly.
|
kb_options = config.services.xserver.xkb.options;
|
||||||
# UWSM launches the session without the shell profile that would carry
|
repeat_delay = 250;
|
||||||
# them, so a variable the compositor or its children must see is set
|
repeat_rate = 45;
|
||||||
# here rather than through home-manager's sessionVariables.
|
accel_profile = "flat";
|
||||||
env = [
|
touchpad = {
|
||||||
# Chromium and Electron apps read this to select native Wayland;
|
natural_scroll = true;
|
||||||
# nixpkgs wrappers (Obsidian's included) gate their Wayland flags on
|
tap_to_click = true;
|
||||||
# it, so without it they run under XWayland and blur at this DPI.
|
disable_while_typing = true;
|
||||||
"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;
|
||||||
};
|
};
|
||||||
|
|
||||||
general = {
|
env = lib.mapAttrsToList (name: value: {
|
||||||
gaps_in = 4;
|
_args = [
|
||||||
gaps_out = 8;
|
name
|
||||||
border_size = 2;
|
value
|
||||||
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";
|
||||||
};
|
};
|
||||||
|
|
||||||
dwindle = {
|
hyprland-toggle-fullscreen = {
|
||||||
preserve_split = true;
|
keys = "SUPER + F";
|
||||||
|
action.kind = "toggle-fullscreen";
|
||||||
};
|
};
|
||||||
|
|
||||||
bind = [
|
hyprland-close-window = {
|
||||||
"$mod, Return, exec, $terminal"
|
keys = "SUPER + SHIFT + Q";
|
||||||
|
action.kind = "close-window";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
# Move focus.
|
registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
|
||||||
"$mod, H, movefocus, l"
|
position = "left";
|
||||||
"$mod, J, movefocus, d"
|
order = 10;
|
||||||
"$mod, K, movefocus, u"
|
settings = {
|
||||||
"$mod, L, movefocus, r"
|
format = "{id} {windows}";
|
||||||
|
format-window-separator = " ";
|
||||||
# Move the window within the layout.
|
window-rewrite-default = util.glyph "f2d0";
|
||||||
"$mod SHIFT, H, movewindow, l"
|
window-rewrite = lib.mapAttrs' (
|
||||||
"$mod SHIFT, J, movewindow, d"
|
_name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" windowIcon.icon
|
||||||
"$mod SHIFT, K, movewindow, u"
|
) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon);
|
||||||
"$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,63 +5,34 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# hypridle: idle-triggered locking and display power management.
|
# hypridle: idle-triggered locking and display power management.
|
||||||
let
|
|
||||||
cfg = config.modules.desktop.session.hyprland.hypridle; # NOLINT(let_order)
|
|
||||||
|
|
||||||
# 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}";
|
|
||||||
user = config.user.name;
|
|
||||||
|
|
||||||
hyprctl = "${config.programs.hyprland.package}/bin/hyprctl";
|
|
||||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.hyprland.hypridle = {
|
options.modules.desktop.session.hyprland.hypridle.enable =
|
||||||
enable = lib.mkEnableOption "hypridle idle management";
|
lib.mkEnableOption "hypridle idle management";
|
||||||
|
|
||||||
lockTimeout = lib.mkOption {
|
config = lib.mkIf config.modules.desktop.session.hyprland.hypridle.enable {
|
||||||
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.${user}.services.hypridle = {
|
home-manager.users.${config.user.name}.services.hypridle = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
general = {
|
general = {
|
||||||
lock_cmd = lockCmd;
|
# The guard drops the call when a locker is already up, so no idle
|
||||||
|
# trigger stacks a second hyprlock over the first.
|
||||||
|
lock_cmd = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
||||||
before_sleep_cmd = "loginctl lock-session";
|
before_sleep_cmd = "loginctl lock-session";
|
||||||
# Waking restores the displays the screen-off listener may have cut.
|
# Waking pokes the compositor in case firmware leaves displays dark.
|
||||||
after_sleep_cmd = "${hyprctl} dispatch dpms on";
|
after_sleep_cmd = "${lib.getExe' config.programs.hyprland.package "hyprctl"} dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
||||||
};
|
};
|
||||||
|
|
||||||
listener = [
|
listener = [
|
||||||
# Lock on idle.
|
# Lock on idle.
|
||||||
{
|
{
|
||||||
timeout = cfg.lockTimeout;
|
timeout = 300;
|
||||||
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,45 +2,54 @@
|
|||||||
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
|
||||||
cfg = config.modules.desktop.session.hyprland.hyprlock; # NOLINT(let_order)
|
sleepIcon = util.glyph [
|
||||||
user = config.user.name;
|
"db81"
|
||||||
|
"dcb2"
|
||||||
# The hyprlock this module installs, used by the lock keybind below.
|
];
|
||||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
sleepOffIcon = util.glyph [
|
||||||
|
"db81"
|
||||||
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
|
"dcb3"
|
||||||
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() {
|
||||||
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
${lib.getExe' pkgs.procps "pkill"} -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet "$unit"; then
|
||||||
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
${lib.getExe' pkgs.systemd "systemctl"} --user stop "$unit"
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepIcon}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ${pkgs.systemd}/bin/systemd-run \
|
if ${lib.getExe' pkgs.systemd "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" \
|
||||||
${pkgs.systemd}/bin/systemd-inhibit \
|
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
|
||||||
--what=idle \
|
--what=idle \
|
||||||
--who=lockscreen-hotkey \
|
--who=lockscreen-hotkey \
|
||||||
--why="lock screen disabled by hotkey" \
|
--why="lock screen disabled by hotkey" \
|
||||||
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
${lib.getExe' pkgs.coreutils "sleep"} infinity >/dev/null; then
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
|
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
|
||||||
|
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet lockscreen-idle-inhibit.service; then
|
||||||
|
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
|
||||||
|
else
|
||||||
|
printf '{"text":"","class":"idle"}\n'
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
@@ -48,8 +57,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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.hyprland.hyprlock.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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 = {
|
||||||
@@ -82,14 +91,36 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Lock on Super+X.
|
registry.keybind.${config.networking.hostName} = {
|
||||||
# The guard drops the keypress when a locker is already up, so a second
|
hyprlock-lock = {
|
||||||
# hyprlock never stacks over the first.
|
keys = "SUPER + X";
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
action = {
|
||||||
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
kind = "exec";
|
||||||
"$mod CTRL, X, exec, ${idleLockToggle}"
|
# The guard drops the call when a locker is already up, so no manual
|
||||||
];
|
# trigger stacks a second hyprlock over the first.
|
||||||
|
command = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
hyprlock-toggle-idle-inhibit = {
|
||||||
|
keys = "SUPER + CTRL + X";
|
||||||
|
action = {
|
||||||
|
kind = "exec";
|
||||||
|
command = "${idleLockToggle}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.waybar.${config.networking.hostName}."custom/idle-lock" = {
|
||||||
|
order = 20;
|
||||||
|
settings = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${idleLockStatus}";
|
||||||
|
interval = "once";
|
||||||
|
signal = 9;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,20 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.login.enable {
|
||||||
services.greetd = {
|
services.greetd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings.default_session.command =
|
settings.default_session.command = lib.escapeShellArgs [
|
||||||
"${lib.getExe pkgs.tuigreet} --time --remember "
|
(lib.getExe pkgs.tuigreet)
|
||||||
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
|
"--time"
|
||||||
|
"--remember"
|
||||||
|
"--cmd"
|
||||||
|
"${lib.getExe config.programs.uwsm.package} start -e -D Hyprland hyprland.desktop"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,29 @@
|
|||||||
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
|
||||||
cfg = config.modules.desktop.session.mako; # NOLINT(let_order)
|
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
|
||||||
user = config.user.name;
|
if ${lib.getExe' pkgs.mako "makoctl"} mode 2>/dev/null | ${lib.getExe pkgs.gnugrep} -qx dnd; then
|
||||||
|
printf '{"text":"${util.glyph "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n'
|
||||||
|
else
|
||||||
|
printf '{"text":"${util.glyph "f0f3"}","tooltip":"Notifications on","class":"active"}\n'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
makoctl = "${pkgs.mako}/bin/makoctl";
|
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
|
||||||
|
${lib.getExe' pkgs.mako "makoctl"} mode -t dnd >/dev/null 2>&1
|
||||||
|
${lib.getExe' pkgs.procps "pkill"} -RTMIN+8 waybar
|
||||||
|
'';
|
||||||
in
|
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.mako.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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 = {
|
||||||
@@ -32,13 +41,25 @@ in
|
|||||||
"mode=dnd".invisible = true;
|
"mode=dnd".invisible = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Recall the last notification from history, keyboard-only like the rest
|
registry.keybind.${config.networking.hostName}.mako-history = {
|
||||||
# of the session.
|
keys = "SUPER + N";
|
||||||
# $mod is defined by the compositor config these binds share.
|
action = {
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
kind = "exec";
|
||||||
"$mod, N, exec, ${makoctl} restore"
|
command = "${lib.getExe' pkgs.mako "makoctl"} restore";
|
||||||
];
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.waybar.${config.networking.hostName}."custom/dnd" = {
|
||||||
|
order = 80;
|
||||||
|
settings = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${dndStatus}";
|
||||||
|
on-click = "${dndToggle}";
|
||||||
|
interval = "once";
|
||||||
|
signal = 8;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
{ 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.portals.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,47 +6,61 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
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' \
|
||||||
| ${pkgs.rofi}/bin/rofi -dmenu -i -p Power)
|
| ${lib.getExe pkgs.rofi} -dmenu -i -p Power)
|
||||||
case "$chosen" in
|
case "$chosen" in
|
||||||
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;;
|
Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
|
||||||
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;;
|
Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
|
||||||
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;;
|
'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
|
||||||
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;;
|
Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
|
||||||
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;;
|
'Shut down') ${lib.getExe' pkgs.systemd "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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.rofi.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = hm: {
|
||||||
# No theme is set here: Stylix's rofi target supplies the Nord one.
|
# Stylix supplies the Nord theme.
|
||||||
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.
|
||||||
extraConfig = {
|
settings = {
|
||||||
|
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";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# The launcher and the session menu, kept next to the tool they drive.
|
rofi-power-menu = {
|
||||||
# $mod is defined by the compositor config these binds share.
|
keys = "SUPER + SHIFT + X";
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
action = {
|
||||||
"$mod, R, exec, ${pkgs.rofi}/bin/rofi -show drun"
|
kind = "exec";
|
||||||
"$mod SHIFT, X, exec, ${powerMenu}"
|
command = "${powerMenu}";
|
||||||
];
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,166 +6,57 @@
|
|||||||
}:
|
}:
|
||||||
# Waybar: a top status bar reading system state at a glance.
|
# Waybar: a top status bar reading system state at a glance.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.waybar; # NOLINT(let_order)
|
moduleNames =
|
||||||
vpnClient = config.modules.network.vpn-client;
|
position:
|
||||||
|
map (entry: entry.name) (
|
||||||
|
lib.sort (left: right: left.module.order < right.module.order) (
|
||||||
|
lib.filter (entry: entry.module.position == position) moduleEntries
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
moduleEntries = lib.mapAttrsToList (name: module: {
|
||||||
# through JSON so the glyph survives as bytes rather than an editor paste.
|
inherit name module;
|
||||||
g = code: builtins.fromJSON ''"\u${code}"'';
|
}) modules;
|
||||||
user = config.user.name;
|
|
||||||
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.waybar.enable {
|
||||||
# A symbols font backs the module glyphs, so pango falls back to it for the
|
# 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.${user}.programs.waybar = {
|
home-manager.users.${config.user.name}.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;
|
||||||
|
|
||||||
style = ''
|
settings.mainBar = lib.mkMerge [
|
||||||
.modules-right {
|
{
|
||||||
margin-right: 8px;
|
layer = "top";
|
||||||
|
position = "top";
|
||||||
|
height = 30;
|
||||||
|
spacing = 8;
|
||||||
|
margin-right = 8;
|
||||||
|
|
||||||
|
modules-left = moduleNames "left";
|
||||||
|
modules-center = moduleNames "center";
|
||||||
|
modules-right = moduleNames "right";
|
||||||
}
|
}
|
||||||
|
(lib.mapAttrs (_name: module: module.settings) modules)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
#custom-dnd {
|
registry.waybar.${config.networking.hostName} = {
|
||||||
margin-right: 8px;
|
clock = {
|
||||||
}
|
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 = {
|
||||||
@@ -174,77 +65,6 @@ 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,12 +1,34 @@
|
|||||||
{ config, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
util,
|
||||||
|
...
|
||||||
|
}:
|
||||||
# PipeWire as the desktop audio server.
|
# PipeWire as the desktop audio server.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.audio; # NOLINT(let_order)
|
volumeKey = keys: command: {
|
||||||
|
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.audio.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;
|
||||||
|
|
||||||
@@ -15,5 +37,50 @@ 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,30 +6,23 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
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" ''
|
||||||
${cliphist} list \
|
${lib.getExe pkgs.cliphist} list \
|
||||||
| ${rofi} -dmenu -i -p Clipboard \
|
| ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
|
||||||
| ${cliphist} decode \
|
| ${lib.getExe pkgs.cliphist} decode \
|
||||||
| ${wl-copy}
|
| ${lib.getExe' pkgs.wl-clipboard "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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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
|
||||||
@@ -39,11 +32,14 @@ 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;
|
||||||
|
};
|
||||||
|
|
||||||
# $mod is defined by the compositor config these binds share.
|
registry.keybind.${config.networking.hostName}.clipboard-history = {
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
keys = "SUPER + SHIFT + V";
|
||||||
"$mod SHIFT, V, exec, ${picker}"
|
action = {
|
||||||
];
|
kind = "exec";
|
||||||
|
command = "${picker}";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,36 +5,52 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.osd.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.${user}.extraGroups = [ "video" ];
|
users.users.${config.user.name}.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.${user}.systemd.user.services.swayosd = {
|
home-manager.users.${config.user.name}.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 = "${pkgs.swayosd}/bin/swayosd-server";
|
ExecStart = lib.getExe' pkgs.swayosd "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,54 +2,67 @@
|
|||||||
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
|
||||||
cfg = config.modules.desktop.utilities.recording; # NOLINT(let_order)
|
# A single key starts and stops recording.
|
||||||
user = config.user.name;
|
# Waybar refreshes on state changes instead of polling.
|
||||||
|
recordingToggle = pkgs.writeShellScript "screen-record-toggle" ''
|
||||||
|
refresh_waybar() {
|
||||||
|
${lib.getExe' pkgs.procps "pkill"} -RTMIN+10 waybar >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
slurp = "${pkgs.slurp}/bin/slurp";
|
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
||||||
wf-recorder = "${pkgs.wf-recorder}/bin/wf-recorder";
|
${lib.getExe' pkgs.procps "pkill"} -INT -x 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=$(${slurp}) || exit 0
|
region=$(${lib.getExe pkgs.slurp}) || exit 0
|
||||||
dir="$(${xdgUserDir} VIDEOS)/Recordings"
|
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
|
||||||
${mkdir} -p "$dir"
|
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
||||||
file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4"
|
file="$dir/recording-$(${lib.getExe' pkgs.coreutils "date"} +%Y%m%d-%H%M%S).mp4"
|
||||||
|
|
||||||
${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio."
|
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
|
||||||
${wf-recorder} -g "$region" -f "$file"
|
${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file" &
|
||||||
${notify-send} -a "Screen recording" "Recording saved" "$file"
|
recorder=$!
|
||||||
|
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.recording.enable {
|
||||||
home-manager.users.${user} = {
|
registry.keybind.${config.networking.hostName}.recording-toggle = {
|
||||||
# $mod is defined by the compositor config these binds share.
|
keys = "SUPER + SHIFT + R";
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
action = {
|
||||||
"$mod SHIFT, R, exec, ${toggle}"
|
kind = "exec";
|
||||||
];
|
command = "${recordingToggle}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.waybar.${config.networking.hostName}."custom/recording" = {
|
||||||
|
order = 10;
|
||||||
|
settings = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${recordingStatus}";
|
||||||
|
interval = "once";
|
||||||
|
signal = 10;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,27 +7,26 @@
|
|||||||
# Keyboard-driven screenshots copy to the clipboard before Satty opens for
|
# Keyboard-driven screenshots copy to the clipboard before Satty opens for
|
||||||
# annotation. Satty copies annotated screenshots when it saves them.
|
# annotation. Satty copies annotated screenshots when it saves them.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.screenshot; # NOLINT(let_order)
|
screenshotKey = keys: target: {
|
||||||
user = config.user.name;
|
inherit keys;
|
||||||
|
action = {
|
||||||
grimblast = "${pkgs.grimblast}/bin/grimblast";
|
kind = "exec";
|
||||||
satty = "${pkgs.satty}/bin/satty";
|
command = "${capture target}";
|
||||||
tee = "${pkgs.coreutils}/bin/tee";
|
};
|
||||||
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
|
};
|
||||||
xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir";
|
|
||||||
|
|
||||||
# Copy the original screenshot before Satty opens.
|
# Copy the original screenshot before Satty opens.
|
||||||
# Satty copies and saves annotated screenshots when the user presses Enter.
|
# Satty copies and saves annotated screenshots when the user presses Enter.
|
||||||
capture =
|
capture =
|
||||||
target:
|
target:
|
||||||
pkgs.writeShellScript "screenshot-${target}" ''
|
pkgs.writeShellScript "screenshot-${target}" ''
|
||||||
dir="$(${xdgUserDir} PICTURES)/Screenshots"
|
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} PICTURES)/Screenshots"
|
||||||
mkdir -p "$dir"
|
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
||||||
${grimblast} save ${target} - \
|
${lib.getExe pkgs.grimblast} save ${target} - \
|
||||||
| ${tee} >(${wl-copy}) \
|
| ${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 ${wl-copy} \
|
--copy-command ${lib.getExe' pkgs.wl-clipboard "wl-copy"} \
|
||||||
--save-after-copy \
|
--save-after-copy \
|
||||||
--early-exit \
|
--early-exit \
|
||||||
--actions-on-enter save-to-clipboard
|
--actions-on-enter save-to-clipboard
|
||||||
@@ -37,24 +36,22 @@ 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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.screenshot.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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.
|
||||||
# windowrule v3 syntax: space-separated field/value tokens, comma
|
wayland.windowManager.hyprland.settings.window_rule = [
|
||||||
# separated, with matchers under a match: prefix and effects bare.
|
{
|
||||||
wayland.windowManager.hyprland.settings.windowrule = [
|
match.class = "^(com\\.gabm\\.satty)$";
|
||||||
"float 1, match:class ^(com\\.gabm\\.satty)$"
|
float = true;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
# Print with plain/Shift/Ctrl for region/window/full.
|
registry.keybind.${config.networking.hostName} = {
|
||||||
# Super+L is already the hjkl focus and movement bind, so screenshots take
|
screenshot-area = screenshotKey "Print" "area";
|
||||||
# the Print key instead.
|
screenshot-active = screenshotKey "SHIFT + Print" "active";
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
screenshot-screen = screenshotKey "CTRL + Print" "screen";
|
||||||
", Print, exec, ${capture "area"}"
|
|
||||||
"SHIFT, Print, exec, ${capture "active"}"
|
|
||||||
"CTRL, Print, exec, ${capture "screen"}"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,19 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
|
|
||||||
# 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" { } ''
|
||||||
${pkgs.imagemagick}/bin/magick \
|
${lib.getExe pkgs.imagemagick} \
|
||||||
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
||||||
"$out"
|
"$out"
|
||||||
'';
|
'';
|
||||||
user = config.user.name;
|
|
||||||
in
|
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.theming.enable {
|
||||||
stylix = {
|
stylix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
@@ -60,7 +57,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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;
|
||||||
@@ -69,5 +66,10 @@ 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,15 +1,27 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# Declares the XDG user directories, so a tool that queries them lands in a
|
# Declares XDG user directories, so tools that query them use predictable
|
||||||
# well-known folder a host can relocate from one place.
|
# folders a host can relocate from one place.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.userdirs; # NOLINT(let_order)
|
homeDirectory = "/home/${config.user.name}";
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.userdirs.enable {
|
||||||
# Write ~/.config/user-dirs.dirs from the option defaults.
|
home-manager.users.${config.user.name}.xdg.userDirs = {
|
||||||
home-manager.users.${user}.xdg.userDirs.enable = true;
|
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,12 +1,9 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
|
||||||
cfg = config.modules.guest; # NOLINT(let_order)
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration";
|
options.modules.guest.enable = lib.mkEnableOption "nested guest configuration";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.guest.enable {
|
||||||
modules.network.ssh.enable = lib.mkDefault true;
|
modules.network.ssh.enable = lib.mkDefault false;
|
||||||
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
|
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.modules.host; # NOLINT(let_order)
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
18
configs/nixos/modules/host/cachyos.nix
Normal file
18
configs/nixos/modules/host/cachyos.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
options.modules.host.cachyos.enable = lib.mkEnableOption "the CachyOS kernel";
|
||||||
|
|
||||||
|
config = lib.mkIf config.modules.host.cachyos.enable {
|
||||||
|
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
||||||
|
|
||||||
|
nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ];
|
||||||
|
nix.settings.extra-trusted-public-keys = [
|
||||||
|
"nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk="
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
106
configs/nixos/modules/host/default.nix
Normal file
106
configs/nixos/modules/host/default.nix
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
hostSecrets = ../../secrets + "/${config.networking.hostName}.yaml";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.host.enable = lib.mkEnableOption "physical host configuration";
|
||||||
|
|
||||||
|
config = lib.mkIf config.modules.host.enable {
|
||||||
|
nix.gc.automatic = true;
|
||||||
|
nix.gc.dates = "Mon 03:15";
|
||||||
|
nix.gc.options = "--delete-older-than 30d";
|
||||||
|
|
||||||
|
nix.optimise.automatic = true;
|
||||||
|
nix.optimise.dates = [ "Thu 03:45" ];
|
||||||
|
|
||||||
|
boot.loader.systemd-boot.enable = true;
|
||||||
|
boot.loader.systemd-boot.configurationLimit = 15;
|
||||||
|
boot.loader.efi.canTouchEfiVariables = true;
|
||||||
|
|
||||||
|
hardware.enableRedistributableFirmware = true;
|
||||||
|
zramSwap.enable = true;
|
||||||
|
|
||||||
|
modules.network.ssh.enable = true;
|
||||||
|
modules.network.ssh.hostKeys.sopsFile = hostSecrets;
|
||||||
|
modules.network.ssh.userKey.sopsFile = hostSecrets;
|
||||||
|
|
||||||
|
services.xserver.xkb.layout = "us";
|
||||||
|
console.useXkbConfig = true;
|
||||||
|
|
||||||
|
sops.defaultSopsFile = ../../secrets/shared.yaml;
|
||||||
|
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
|
||||||
|
sops.age.sshKeyPaths = [ ];
|
||||||
|
sops.gnupg.sshKeyPaths = [ ];
|
||||||
|
|
||||||
|
users.mutableUsers = false;
|
||||||
|
sops.secrets."${config.user.name}-password".neededForUsers = true;
|
||||||
|
users.users.${config.user.name}.hashedPasswordFile =
|
||||||
|
config.sops.secrets."${config.user.name}-password".path;
|
||||||
|
|
||||||
|
security.sudo.extraConfig = ''
|
||||||
|
Defaults env_keep += "EDITOR VISUAL"
|
||||||
|
'';
|
||||||
|
|
||||||
|
home-manager = {
|
||||||
|
useGlobalPkgs = true;
|
||||||
|
useUserPackages = true;
|
||||||
|
users.${config.user.name} = {
|
||||||
|
home.username = config.user.name;
|
||||||
|
home.homeDirectory = "/home/${config.user.name}";
|
||||||
|
home.stateVersion = config.system.stateVersion;
|
||||||
|
home.sessionVariables = lib.attrByPath [ config.networking.hostName ] { } config.registry.env;
|
||||||
|
home.packages = with pkgs; [
|
||||||
|
eza
|
||||||
|
unzip
|
||||||
|
wget
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.shell-shortcut.${config.networking.hostName} = {
|
||||||
|
ls = {
|
||||||
|
command = "eza -alg --color=always --group-directories-first --icons=always";
|
||||||
|
abbreviation = false;
|
||||||
|
};
|
||||||
|
ll = {
|
||||||
|
command = "eza -lg --color=always --group-directories-first --icons=always";
|
||||||
|
abbreviation = false;
|
||||||
|
};
|
||||||
|
lt = {
|
||||||
|
command = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
||||||
|
abbreviation = false;
|
||||||
|
};
|
||||||
|
".." = "cd ..";
|
||||||
|
"..." = "cd ../..";
|
||||||
|
"...." = "cd ../../..";
|
||||||
|
|
||||||
|
cp = "cp -v";
|
||||||
|
tarnow = "tar -acf ";
|
||||||
|
untar = "tar -zxvf ";
|
||||||
|
wget = "wget -c ";
|
||||||
|
grep = "grep --color=auto";
|
||||||
|
fgrep = "fgrep --color=auto";
|
||||||
|
egrep = "egrep --color=auto";
|
||||||
|
jctl = "journalctl -p 3 -xb";
|
||||||
|
|
||||||
|
update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}";
|
||||||
|
cleanup = "sudo nix-collect-garbage -d";
|
||||||
|
sops-host = {
|
||||||
|
command = "sudo --preserve-env=EDITOR,VISUAL env SOPS_AGE_KEY_FILE=/var/lib/sops-nix/key.txt nix shell nixpkgs#sops nixpkgs#neovim -c sops";
|
||||||
|
abbreviation = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
modules.shell.fish.enable = lib.mkDefault true;
|
||||||
|
modules.shell.fish.defaultShell = lib.mkDefault true;
|
||||||
|
modules.shell.tmux.enable = lib.mkDefault true;
|
||||||
|
modules.shell.nvim.enable = lib.mkDefault true;
|
||||||
|
modules.shell.git.enable = lib.mkDefault true;
|
||||||
|
modules.shell.yazi.enable = lib.mkDefault true;
|
||||||
|
};
|
||||||
|
}
|
||||||
57
configs/nixos/modules/host/zfs.nix
Normal file
57
configs/nixos/modules/host/zfs.nix
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
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,13 +4,10 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Tools for developing this monorepo.
|
# Tools for developing this monorepo.
|
||||||
let
|
|
||||||
cfg = config.modules.monorepo; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.monorepo.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;
|
||||||
|
|||||||
57
configs/nixos/modules/network/default.nix
Normal file
57
configs/nixos/modules/network/default.nix
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
util,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# NetworkManager-managed networking.
|
||||||
|
let
|
||||||
|
networkStatus = pkgs.writeShellScript "waybar-network-status" ''
|
||||||
|
active=$(
|
||||||
|
${lib.getExe' pkgs.networkmanager "nmcli"} -t -f DEVICE,TYPE,STATE,CONNECTION device status \
|
||||||
|
| ${lib.getExe' pkgs.gawk "awk"} -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -z "$active" ]; then
|
||||||
|
${lib.getExe pkgs.jq} -cn \
|
||||||
|
--arg text "${util.glyph "f127"} off" \
|
||||||
|
--arg tooltip "Network disconnected" \
|
||||||
|
--arg class "disconnected" \
|
||||||
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=: read -r device type _state connection <<< "$active"
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
wifi) icon="${util.glyph "f1eb"}" ;;
|
||||||
|
ethernet) icon="LAN" ;;
|
||||||
|
*) icon="NET" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$device" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
|
||||||
|
|
||||||
|
${lib.getExe pkgs.jq} -cn \
|
||||||
|
--arg text "$icon $connection" \
|
||||||
|
--arg tooltip "$device: ''${ipaddr:-no IPv4}" \
|
||||||
|
--arg class "$type" \
|
||||||
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.network.enable = lib.mkEnableOption "NetworkManager-managed networking";
|
||||||
|
|
||||||
|
config = lib.mkIf config.modules.network.enable {
|
||||||
|
networking.networkmanager.enable = true;
|
||||||
|
|
||||||
|
registry.waybar.${config.networking.hostName}."custom/network" = {
|
||||||
|
order = 60;
|
||||||
|
settings = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${networkStatus}";
|
||||||
|
interval = 5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,344 +4,203 @@
|
|||||||
registry,
|
registry,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# SSH on this machine, in both directions.
|
# Configures inbound and outbound SSH access.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.ssh; # NOLINT(let_order)
|
cfg = config.modules.network.ssh;
|
||||||
targetType = types.submodule (
|
user = config.user.name;
|
||||||
{ name, ... }:
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
hostName = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = name;
|
|
||||||
description = ''
|
|
||||||
The network address OpenSSH connects to for this target.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
user = mkOption {
|
workstationNames = [
|
||||||
type = types.str;
|
"neogaia"
|
||||||
default = config.user.name;
|
"zeus"
|
||||||
description = ''
|
];
|
||||||
The remote login name OpenSSH uses for this target.
|
serverNames = [ "pikachu" ];
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
port = mkOption {
|
isWorkstation = lib.elem config.networking.hostName workstationNames;
|
||||||
type = types.port;
|
isServer = lib.elem config.networking.hostName serverNames;
|
||||||
default = 22;
|
|
||||||
description = ''
|
|
||||||
The TCP port OpenSSH uses for this target.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
aliases = mkOption {
|
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
|
||||||
type = types.listOf types.str;
|
hostKeysFor =
|
||||||
default = [ name ];
|
host:
|
||||||
description = ''
|
map builtins.readFile [
|
||||||
Host patterns written into the generated OpenSSH client block.
|
(../../hosts + "/${host}/ssh_host_ed25519_key.pub")
|
||||||
'';
|
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
|
||||||
};
|
];
|
||||||
|
|
||||||
clientKey = mkOption {
|
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
|
||||||
type = types.nullOr types.str;
|
hostName = registry.address.${name}.self;
|
||||||
default = null;
|
clientKey = clientKeyFor name;
|
||||||
description = ''
|
hostKeys = hostKeysFor name;
|
||||||
The public key this target offers when it connects outward.
|
});
|
||||||
Other machines admit this key according to the host groups below.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
hostKeys = mkOption {
|
extraTargets = {
|
||||||
type = types.listOf types.str;
|
# TODO: Pin Gitea's host key after migrating it from Proxmox.
|
||||||
default = [ ];
|
gitea = {
|
||||||
description = ''
|
hostName = "git.alexion.dev";
|
||||||
The public keys this target presents when it accepts inbound SSH.
|
port = 2022;
|
||||||
These keys generate system-wide known-host entries.
|
user = "gitea";
|
||||||
'';
|
aliases = [
|
||||||
};
|
"gitea"
|
||||||
};
|
"git.alexion.dev"
|
||||||
}
|
];
|
||||||
);
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mkTarget = name: target: {
|
||||||
|
hostName = target.hostName or name;
|
||||||
|
user = target.user or user;
|
||||||
|
port = target.port or 22;
|
||||||
|
aliases = target.aliases or [ name ];
|
||||||
|
clientKey = target.clientKey or null;
|
||||||
|
hostKeys = target.hostKeys or [ ];
|
||||||
|
};
|
||||||
|
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets);
|
||||||
|
|
||||||
|
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names);
|
||||||
defaultAuthorizedKeys = lib.flatten (
|
defaultAuthorizedKeys = lib.flatten (
|
||||||
clientKeysFor workstationNames ++ optional isServer (clientKeysFor serverNames)
|
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
|
||||||
);
|
);
|
||||||
|
|
||||||
clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names);
|
outboundTargetNames =
|
||||||
|
let
|
||||||
|
groupTargets =
|
||||||
|
if isServer then
|
||||||
|
builtins.attrNames extraTargets ++ serverNames
|
||||||
|
else if isWorkstation then
|
||||||
|
builtins.attrNames extraTargets ++ serverNames ++ workstationNames
|
||||||
|
else
|
||||||
|
builtins.attrNames extraTargets;
|
||||||
|
in
|
||||||
|
lib.filter (name: name != config.networking.hostName) groupTargets;
|
||||||
|
|
||||||
workstationNames = targetNamesIn cfg.hosts.workstations;
|
knownHosts = lib.listToAttrs (
|
||||||
targetNamesIn = names: filter (name: hasAttr name cfg.targets) names;
|
lib.concatLists (
|
||||||
|
lib.mapAttrsToList (
|
||||||
isServer = elem currentHost cfg.hosts.servers;
|
|
||||||
currentHost = config.networking.hostName;
|
|
||||||
serverNames = targetNamesIn cfg.hosts.servers;
|
|
||||||
|
|
||||||
knownHosts = listToAttrs (
|
|
||||||
concatLists (
|
|
||||||
mapAttrsToList (
|
|
||||||
targetName: target:
|
targetName: target:
|
||||||
imap0 (
|
lib.imap0 (
|
||||||
i: key:
|
i: key:
|
||||||
nameValuePair "${targetName}-${toString i}" {
|
lib.nameValuePair "${targetName}-${toString i}" {
|
||||||
hostNames = knownHostNamesFor target;
|
hostNames = knownHostNamesFor target;
|
||||||
publicKey = keyWithoutComment key;
|
publicKey = keyWithoutComment key;
|
||||||
}
|
}
|
||||||
) target.hostKeys
|
) target.hostKeys
|
||||||
) cfg.targets
|
) targets
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
knownHostNamesFor =
|
knownHostNamesFor =
|
||||||
target:
|
target:
|
||||||
let
|
let
|
||||||
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
||||||
names = unique (target.aliases ++ [ target.hostName ]);
|
names = lib.unique (target.aliases ++ [ target.hostName ]);
|
||||||
in
|
in
|
||||||
map withPort names;
|
map withPort names;
|
||||||
|
|
||||||
keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
keyWithoutComment = key: lib.concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
||||||
|
|
||||||
sshSettingsFor =
|
sshSettingsFor =
|
||||||
name:
|
name:
|
||||||
let
|
let
|
||||||
target = cfg.targets.${name};
|
target = targets.${name};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
header = "Host ${concatStringsSep " " target.aliases}";
|
header = "Host ${lib.concatStringsSep " " target.aliases}";
|
||||||
HostName = target.hostName;
|
HostName = target.hostName;
|
||||||
User = target.user;
|
User = target.user;
|
||||||
}
|
}
|
||||||
// optionalAttrs (target.port != 22) { Port = target.port; };
|
// lib.optionalAttrs (target.port != 22) { Port = target.port; };
|
||||||
|
|
||||||
outboundTargetNames =
|
|
||||||
let
|
|
||||||
groupTargets =
|
|
||||||
if isServer then
|
|
||||||
[ "gitea" ] ++ cfg.hosts.servers
|
|
||||||
else if isWorkstation then
|
|
||||||
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
|
|
||||||
else
|
|
||||||
[ "gitea" ];
|
|
||||||
in
|
|
||||||
filter (name: name != currentHost) (targetNamesIn groupTargets);
|
|
||||||
|
|
||||||
isWorkstation = elem currentHost cfg.hosts.workstations;
|
|
||||||
|
|
||||||
hostKeySecret = type: "ssh-host-${type}-key";
|
hostKeySecret = type: "ssh-host-${type}-key";
|
||||||
user = config.user.name;
|
hostKeyTypes = [
|
||||||
|
"ed25519"
|
||||||
inherit (lib)
|
"rsa"
|
||||||
concatLists
|
];
|
||||||
concatStringsSep
|
|
||||||
elem
|
|
||||||
filter
|
|
||||||
genAttrs
|
|
||||||
hasAttr
|
|
||||||
imap0
|
|
||||||
listToAttrs
|
|
||||||
mapAttrs
|
|
||||||
mapAttrsToList
|
|
||||||
mkIf
|
|
||||||
mkMerge
|
|
||||||
mkOption
|
|
||||||
nameValuePair
|
|
||||||
optional
|
|
||||||
optionalAttrs
|
|
||||||
types
|
|
||||||
unique
|
|
||||||
;
|
|
||||||
|
|
||||||
userKeySecret = "ssh-user-ed25519-key";
|
userKeySecret = "ssh-user-ed25519-key";
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.ssh = {
|
options.modules.network.ssh = {
|
||||||
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
|
enable = lib.mkEnableOption "the OpenSSH daemon";
|
||||||
|
|
||||||
hosts = {
|
hostKeys.restore = lib.mkOption {
|
||||||
servers = mkOption {
|
type = lib.types.bool;
|
||||||
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 = ''
|
||||||
Restore the host keys from secrets rather than letting the daemon generate its own.
|
Restores host keys from secrets.
|
||||||
A machine with its own identity keeps its fingerprint across a reimage by restoring committed keys.
|
Preserves the fingerprint across reimages.
|
||||||
A guest carries no host identity, so it turns this off and presents a self-generated key instead.
|
Guests disable this option and generate their own host keys.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.sopsFile = mkOption {
|
hostKeys.sopsFile = lib.mkOption {
|
||||||
type = types.nullOr types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Encrypted file holding this host's SSH host private keys, one entry per key type, named `ssh-host-<type>-key`.
|
Sets encrypted file that contains host private keys.
|
||||||
Required when `restore` is on.
|
Names each secret `ssh-host-<type>-key`.
|
||||||
|
Requires a value when `restore` is enabled.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.types = mkOption {
|
userKey.sopsFile = lib.mkOption {
|
||||||
type = types.listOf types.str;
|
type = lib.types.nullOr lib.types.path;
|
||||||
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 = ''
|
||||||
Encrypted file holding this machine's SSH client private key, under the entry `ssh-user-ed25519-key`.
|
Sets encrypted file that contains the SSH client private key.
|
||||||
Left unset on a machine that authenticates to no remote server, such as a guest.
|
Names the secret `ssh-user-ed25519-key`.
|
||||||
|
Guests leave this unset because they do not authenticate to remote servers.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable (mkMerge [
|
config = lib.mkIf cfg.enable (
|
||||||
{
|
lib.mkMerge [
|
||||||
services.openssh.enable = true;
|
{
|
||||||
|
services.openssh.enable = true;
|
||||||
|
|
||||||
users.users.${user}.openssh.authorizedKeys.keys =
|
users.users.${user}.openssh.authorizedKeys.keys = defaultAuthorizedKeys;
|
||||||
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 =
|
settings = lib.mapAttrs (name: _: sshSettingsFor name) (
|
||||||
mapAttrs (name: _: sshSettingsFor name) (genAttrs outboundTargetNames (name: name))
|
lib.genAttrs outboundTargetNames (name: name)
|
||||||
// cfg.extraSettings;
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
(mkIf cfg.hostKeys.restore {
|
(lib.mkIf cfg.hostKeys.restore {
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.hostKeys.sopsFile != null;
|
assertion = cfg.hostKeys.sopsFile != null;
|
||||||
message = "modules.network.ssh.hostKeys.restore requires modules.network.ssh.hostKeys.sopsFile to name the encrypted host keys.";
|
message = "Set `modules.network.ssh.hostKeys.sopsFile` when host-key restoration is enabled.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets = genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
sops.secrets = lib.genAttrs (map hostKeySecret hostKeyTypes) (_: {
|
||||||
inherit (cfg.hostKeys) sopsFile;
|
inherit (cfg.hostKeys) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
restartUnits = [ "sshd.service" ];
|
restartUnits = [ "sshd.service" ];
|
||||||
});
|
});
|
||||||
|
|
||||||
services.openssh.hostKeys = [ ];
|
services.openssh.hostKeys = [ ];
|
||||||
services.openssh.extraConfig = concatStringsSep "" (
|
services.openssh.extraConfig = lib.concatStringsSep "" (
|
||||||
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") cfg.hostKeys.types
|
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") hostKeyTypes
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
(mkIf (cfg.userKey.sopsFile != null) {
|
(lib.mkIf (cfg.userKey.sopsFile != null) {
|
||||||
sops.secrets.${userKeySecret} = {
|
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."*".IdentityFile =
|
home-manager.users.${user}.programs.ssh.settings."*" = {
|
||||||
config.sops.secrets.${userKeySecret}.path;
|
IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
||||||
})
|
IdentitiesOnly = true;
|
||||||
]);
|
};
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,17 @@
|
|||||||
util,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
# Configures bridges for tagged VLANs on a physical interface that uses DHCP.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vlans; # NOLINT(let_order)
|
cfg = config.modules.network.vlans;
|
||||||
|
|
||||||
inherit (util) bridgeName;
|
|
||||||
|
|
||||||
vlanName = id: "${cfg.trunk}.${toString id}";
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.vlans = {
|
options.modules.network.vlans = {
|
||||||
enable = lib.mkEnableOption "host VLAN bridges over a tagged trunk";
|
interface = lib.mkOption {
|
||||||
|
|
||||||
trunk = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "enp1s0";
|
example = "enp1s0";
|
||||||
description = ''
|
description = ''
|
||||||
Physical interface that carries 802.1Q-tagged traffic for every VLAN.
|
Names the physical interface that carries untagged DHCP traffic and tagged VLANs.
|
||||||
Each declared VLAN attaches to its own bridge.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,67 +27,37 @@ in
|
|||||||
30
|
30
|
||||||
];
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Tagged VLAN ids to materialize.
|
Creates bridges for the specified tagged VLAN IDs.
|
||||||
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.enable {
|
config = lib.mkIf (cfg.ids != [ ]) {
|
||||||
assertions = [
|
networking = {
|
||||||
{
|
useDHCP = false;
|
||||||
assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.ids;
|
networkmanager.unmanaged = [
|
||||||
message = "modules.network.vlans.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.ids.";
|
"interface-name:${cfg.interface}"
|
||||||
}
|
]
|
||||||
{
|
++ map (id: "interface-name:${util.bridgeName id}") cfg.ids
|
||||||
assertion = cfg.management.address == null || cfg.management.vlan != null;
|
++ map (id: "interface-name:vlan${toString id}") cfg.ids;
|
||||||
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-${bridgeName id}" = {
|
"40-${util.bridgeName id}" = {
|
||||||
netdevConfig = {
|
netdevConfig = {
|
||||||
Name = bridgeName id;
|
Name = util.bridgeName id;
|
||||||
Kind = "bridge";
|
Kind = "bridge";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"40-${vlanName id}" = {
|
"40-vlan${toString id}" = {
|
||||||
netdevConfig = {
|
netdevConfig = {
|
||||||
Name = vlanName id;
|
Name = "vlan${toString id}";
|
||||||
Kind = "vlan";
|
Kind = "vlan";
|
||||||
};
|
};
|
||||||
vlanConfig.Id = id;
|
vlanConfig.Id = id;
|
||||||
@@ -104,37 +67,25 @@ in
|
|||||||
|
|
||||||
networks = lib.mkMerge (
|
networks = lib.mkMerge (
|
||||||
[
|
[
|
||||||
# The trunk carries only tagged frames and takes no address.
|
|
||||||
{
|
{
|
||||||
"30-${cfg.trunk}" = {
|
"10-${cfg.interface}" = {
|
||||||
matchConfig.Name = cfg.trunk;
|
matchConfig.Name = cfg.interface;
|
||||||
networkConfig.LinkLocalAddressing = "no";
|
networkConfig.DHCP = "yes";
|
||||||
linkConfig.RequiredForOnline = "no";
|
vlan = map (id: "vlan${toString id}") cfg.ids;
|
||||||
vlan = map vlanName cfg.ids;
|
linkConfig.RequiredForOnline = "routable";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
++ map (id: {
|
++ map (id: {
|
||||||
# Enslaved to its bridge, carrying no address itself.
|
"40-vlan${toString id}" = {
|
||||||
"40-${vlanName id}" = {
|
matchConfig.Name = "vlan${toString id}";
|
||||||
matchConfig.Name = vlanName id;
|
networkConfig.Bridge = util.bridgeName id;
|
||||||
networkConfig.Bridge = bridgeName id;
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
};
|
||||||
|
"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,10 +1,34 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vpn-client; # NOLINT(let_order)
|
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 = {
|
||||||
@@ -13,61 +37,31 @@ in
|
|||||||
interface = lib.mkOption {
|
interface = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "wg-home";
|
default = "wg-home";
|
||||||
description = "WireGuard interface name.";
|
description = "Names the WireGuard interface.";
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = "Client tunnel address in CIDR notation.";
|
description = "Sets the 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 = "Encrypted secrets file that contains privateKeySecret.";
|
description = "Names the encrypted file that contains the WireGuard private key.";
|
||||||
};
|
};
|
||||||
|
|
||||||
peer = {
|
peer = {
|
||||||
publicKey = lib.mkOption {
|
publicKey = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "WireGuard public key for the remote peer.";
|
description = "Sets the 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 = "WireGuard endpoint for the remote peer.";
|
description = "Sets the 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.";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -76,30 +70,39 @@ in
|
|||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.sopsFile != null;
|
assertion = cfg.sopsFile != null;
|
||||||
message = "modules.network.vpn-client.sopsFile must name the encrypted secrets file containing the WireGuard private key.";
|
message = "Set modules.network.vpn-client.sopsFile when the VPN client is enabled.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets.${cfg.privateKeySecret}.sopsFile = cfg.sopsFile;
|
sops.secrets.wireguard-home-private-key.sopsFile = cfg.sopsFile;
|
||||||
|
|
||||||
systemd.targets."wireguard-${cfg.interface}" = lib.mkIf (!cfg.autoStart) {
|
registry.shell-shortcut.${config.networking.hostName} = {
|
||||||
wantedBy = lib.mkForce [ ];
|
vpn-on = "sudo systemctl start wireguard-${cfg.interface}.target";
|
||||||
|
vpn-off = "sudo systemctl stop 'wireguard-${cfg.interface}*.service' wireguard-${cfg.interface}.target";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
registry.waybar.${config.networking.hostName}."custom/vpn" = {
|
||||||
|
order = 50;
|
||||||
|
settings = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${vpnStatus}";
|
||||||
|
interval = 5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.targets."wireguard-${cfg.interface}".wantedBy = lib.mkForce [ ];
|
||||||
|
|
||||||
networking.wireguard.interfaces.${cfg.interface} = {
|
networking.wireguard.interfaces.${cfg.interface} = {
|
||||||
ips = [ cfg.address ];
|
ips = [ cfg.address ];
|
||||||
inherit (cfg) mtu;
|
mtu = 1280;
|
||||||
privateKeyFile = config.sops.secrets.${cfg.privateKeySecret}.path;
|
privateKeyFile = config.sops.secrets.wireguard-home-private-key.path;
|
||||||
|
|
||||||
peers = [
|
peers = [
|
||||||
(
|
{
|
||||||
{
|
inherit (cfg.peer) publicKey endpoint;
|
||||||
inherit (cfg.peer) publicKey endpoint allowedIPs;
|
allowedIPs = [ "10.23.0.0/16" ];
|
||||||
}
|
persistentKeepalive = 25;
|
||||||
// lib.optionalAttrs (cfg.peer.persistentKeepalive != null) {
|
}
|
||||||
inherit (cfg.peer) persistentKeepalive;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.modules.services.actualbudget; # NOLINT(let_order)
|
|
||||||
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,147 +2,135 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.copyparty; # NOLINT(let_order)
|
cfg = config.modules.services.copyparty;
|
||||||
|
|
||||||
volumeType = lib.types.submodule {
|
accountName = config.user.name;
|
||||||
options = {
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Filesystem path served by this volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
access = lib.mkOption {
|
serviceGroup = "storage";
|
||||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
A = [ "alexion" ];
|
|
||||||
};
|
|
||||||
description = "Copyparty access map from permission string to account names.";
|
|
||||||
};
|
|
||||||
|
|
||||||
readOnly = lib.mkOption {
|
serviceStateDir = "/var/lib/copyparty";
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Serve this volume read-only.";
|
|
||||||
};
|
|
||||||
|
|
||||||
flags = lib.mkOption {
|
serviceUser = "copyparty";
|
||||||
type = lib.types.listOf lib.types.str;
|
|
||||||
default = [ ];
|
writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes);
|
||||||
example = [ "dthumb" ];
|
|
||||||
description = "Copyparty volume flags.";
|
port = 3923;
|
||||||
};
|
defaultAccess = {
|
||||||
};
|
A = [ accountName ];
|
||||||
};
|
};
|
||||||
|
|
||||||
configScript = pkgs.writeShellScript "copyparty-config" ''
|
defaultWritableFlags = {
|
||||||
set -eu
|
d2d = true;
|
||||||
|
d2t = true;
|
||||||
|
dthumb = true;
|
||||||
|
};
|
||||||
|
|
||||||
umask 077
|
credentialHash = builtins.substring 0 16 (builtins.hashString "sha256" accountName);
|
||||||
password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")"
|
|
||||||
|
|
||||||
cat > /run/copyparty/copyparty.conf <<EOF
|
credentialName = "password-${credentialHash}";
|
||||||
[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}
|
passwordPlaceholder = "@COPYPARTY_PASSWORD_${credentialHash}@";
|
||||||
EOF
|
|
||||||
'';
|
|
||||||
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
|
|
||||||
|
|
||||||
hasFlag =
|
globalSettings = {
|
||||||
name: flags: lib.any (flag: lib.hasPrefix "${name}:" flag || lib.hasPrefix "${name}=" flag) flags;
|
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";
|
||||||
|
};
|
||||||
|
|
||||||
defaultPermissionFlags =
|
globalExtraConfig = lib.optionalString cfg.reverseProxy.enable (
|
||||||
|
lib.concatMapStringsSep "\n" (source: "xff-src: ${source}") cfg.reverseProxy.trustedSources
|
||||||
|
);
|
||||||
|
|
||||||
|
volumeFlags =
|
||||||
flags:
|
flags:
|
||||||
lib.optionals (!(hasFlag "chmod_d" flags)) [ "chmod_d: 2770" ]
|
lib.optionalAttrs (flags != { }) (
|
||||||
++ lib.optionals (!(hasFlag "chmod_f" flags)) [ "chmod_f: 660" ];
|
{
|
||||||
|
chmod_d = "2770";
|
||||||
|
chmod_f = "660";
|
||||||
|
}
|
||||||
|
// flags
|
||||||
|
);
|
||||||
|
|
||||||
renderFlags =
|
renderedVolumes = lib.mapAttrs (_urlPath: volume: {
|
||||||
flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") (flags ++ defaultPermissionFlags flags);
|
inherit (volume) path;
|
||||||
|
access = if volume.readOnly then { r = [ accountName ]; } else defaultAccess // volume.access;
|
||||||
|
flags = volumeFlags (lib.optionalAttrs (!volume.readOnly) defaultWritableFlags // volume.flags);
|
||||||
|
}) cfg.volumes;
|
||||||
|
|
||||||
renderVolume = urlPath: volume: ''
|
configText = util.formats.copyparty.render {
|
||||||
[${urlPath}]
|
inherit globalExtraConfig;
|
||||||
${volume.path}
|
accounts.${accountName} = passwordPlaceholder;
|
||||||
accs:
|
global = globalSettings;
|
||||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)}
|
volumes = renderedVolumes;
|
||||||
${lib.optionalString (volume.flags != [ ]) ''
|
};
|
||||||
flags:
|
|
||||||
${renderFlags volume.flags}
|
configFile = pkgs.writeText "copyparty.conf" configText;
|
||||||
''}
|
|
||||||
|
replacePassword = ''
|
||||||
|
${lib.getExe pkgs.replace-secret} ${lib.escapeShellArg passwordPlaceholder} "$CREDENTIALS_DIRECTORY/${credentialName}" /run/copyparty/copyparty.conf
|
||||||
'';
|
'';
|
||||||
|
|
||||||
renderTrustedSource = source: " xff-src: ${source}";
|
|
||||||
|
|
||||||
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
|
|
||||||
rproxy: 1
|
|
||||||
xff-hdr: x-forwarded-for
|
|
||||||
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
|
|
||||||
'';
|
|
||||||
|
|
||||||
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.copyparty = {
|
options.modules.services.copyparty = {
|
||||||
enable = lib.mkEnableOption "Copyparty";
|
enable = lib.mkEnableOption "Copyparty";
|
||||||
|
|
||||||
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;
|
|
||||||
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 {
|
passwordFile = lib.mkOption {
|
||||||
type = lib.types.path;
|
|
||||||
description = "Runtime file containing the Copyparty account password.";
|
|
||||||
};
|
|
||||||
|
|
||||||
stateDir = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "/var/lib/copyparty";
|
description = "Sets the runtime file that contains the Copyparty account password.";
|
||||||
description = "Directory where Copyparty stores service state.";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf volumeType;
|
type = lib.types.attrsOf (
|
||||||
|
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 = "Copyparty volumes keyed by their URL path.";
|
description = "Defines Copyparty volumes by URL path.";
|
||||||
};
|
};
|
||||||
|
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
@@ -151,7 +139,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 = "IP ranges that may supply Copyparty's forwarded client IP header.";
|
description = "Lists proxy sources that can set Copyparty's forwarded client IP header.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -173,46 +161,49 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
users.users.${cfg.user} = {
|
users.users.${serviceUser} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
inherit (cfg) group;
|
group = serviceGroup;
|
||||||
home = cfg.stateDir;
|
home = serviceStateDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = cfg.user;
|
User = serviceUser;
|
||||||
Group = cfg.group;
|
Group = serviceGroup;
|
||||||
WorkingDirectory = cfg.stateDir;
|
WorkingDirectory = serviceStateDir;
|
||||||
Environment = [
|
Environment = [
|
||||||
"HOME=${cfg.stateDir}"
|
"HOME=${serviceStateDir}"
|
||||||
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
"XDG_CONFIG_HOME=${serviceStateDir}"
|
||||||
];
|
];
|
||||||
StateDirectory = "copyparty";
|
StateDirectory = "copyparty";
|
||||||
RuntimeDirectory = "copyparty";
|
RuntimeDirectory = "copyparty";
|
||||||
RuntimeDirectoryMode = "0700";
|
RuntimeDirectoryMode = "0700";
|
||||||
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
LoadCredential = [ "${credentialName}:${cfg.passwordFile}" ];
|
||||||
ExecStartPre = configScript;
|
ExecStart = "${lib.getExe pkgs.copyparty-min} -c /run/copyparty/copyparty.conf";
|
||||||
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 = [
|
||||||
cfg.stateDir
|
serviceStateDir
|
||||||
]
|
]
|
||||||
++ map (volume: volume.path) (lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes));
|
++ map (volume: volume.path) writableVolumes;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
networking.firewall.allowedTCPPorts = [ port ];
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,33 +92,33 @@
|
|||||||
startScript = pkgs.writeShellScript "palworld-start" ''
|
startScript = pkgs.writeShellScript "palworld-start" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${pkgs.coreutils}/bin/install -d \
|
${lib.getExe' pkgs.coreutils "install"} -d \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
${lib.escapeShellArg service.stateDir} \
|
${lib.escapeShellArg service.stateDir} \
|
||||||
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
||||||
|
|
||||||
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})"
|
current_target="$(${lib.getExe' pkgs.coreutils "readlink"} ${lib.escapeShellArg savedDir})"
|
||||||
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
||||||
${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir}
|
${lib.getExe' pkgs.coreutils "rm"} ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
if ${pkgs.findutils}/bin/find ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${pkgs.gnugrep}/bin/grep -q .; then
|
if ${lib.getExe' pkgs.findutils "find"} ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${lib.getExe' pkgs.gnugrep "grep"} -q .; then
|
||||||
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
||||||
else
|
else
|
||||||
${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
${lib.getExe' pkgs.coreutils "cp"} -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
||||||
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
||||||
${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
${lib.getExe' pkgs.coreutils "ln"} -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
admin_password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
||||||
|
|
||||||
exec ./PalServer.sh \
|
exec ./PalServer.sh \
|
||||||
"$@" \
|
"$@" \
|
||||||
@@ -145,9 +145,12 @@
|
|||||||
]
|
]
|
||||||
++ lib.optional service.publicLobby "-publiclobby"
|
++ lib.optional service.publicLobby "-publiclobby"
|
||||||
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
||||||
++ lib.optional service.rcon.enable "-rcon";
|
++ lib.optionals service.rcon.enable [
|
||||||
|
"-rcon"
|
||||||
|
"-RCONPort=${toString service.rcon.port}"
|
||||||
|
];
|
||||||
udpPorts = [ service.port ];
|
udpPorts = [ service.port ];
|
||||||
tcpPorts = [ service.port ];
|
tcpPorts = lib.optional service.rcon.enable service.rcon.port;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
startScript = pkgs.writeShellScript "valheim-start" ''
|
startScript = pkgs.writeShellScript "valheim-start" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
exec ./valheim_server.x86_64 "$@" -password "$password"
|
exec ./valheim_server.x86_64 "$@" -password "$password"
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.homepage; # NOLINT(let_order)
|
cfg = config.modules.services.homepage;
|
||||||
|
|
||||||
derivedAllowedHosts = lib.unique (
|
derivedAllowedHosts = lib.unique (
|
||||||
lib.optional (selfRoute != null) selfRoute.domain
|
lib.optional (selfRoute != null) selfRoute.domain
|
||||||
@@ -71,12 +71,12 @@ let
|
|||||||
if item.homepage.href != null then
|
if item.homepage.href != null then
|
||||||
item.homepage.href
|
item.homepage.href
|
||||||
else if (item.composite.route or null) != null then
|
else if (item.composite.route or null) != null then
|
||||||
if item.composite.route.port != null then
|
if item.composite.route.protocol == "https" then
|
||||||
"https://${item.composite.route.domain}"
|
"https://${item.composite.route.domain}"
|
||||||
else if item.composite.route.http.port != null then
|
else if item.composite.route.protocol == "http" then
|
||||||
"http://${item.composite.route.domain}"
|
"http://${item.composite.route.domain}"
|
||||||
else
|
else
|
||||||
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching TCP route entry"
|
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching HTTP route entry"
|
||||||
else
|
else
|
||||||
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching route entry";
|
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching route entry";
|
||||||
|
|
||||||
@@ -111,7 +111,8 @@ let
|
|||||||
let
|
let
|
||||||
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
||||||
in
|
in
|
||||||
sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
lib.filter (section: lib.hasAttr section sections) sectionOrder
|
||||||
|
++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.navidrome; # NOLINT(let_order)
|
cfg = config.modules.services.navidrome;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.navidrome = {
|
options.modules.services.navidrome = {
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.reverse-proxy; # NOLINT(let_order)
|
|
||||||
|
|
||||||
generatedUdpStreams = lib.concatLists (
|
generatedUdpStreams = lib.concatLists (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
host: routes:
|
host: routes:
|
||||||
lib.concatLists (
|
lib.concatLists (
|
||||||
lib.mapAttrsToList (entry: route: map (mkUdpStream host entry) route.udp.ports) routes
|
lib.mapAttrsToList (
|
||||||
|
entry: route: lib.optionals (route.protocol == "udp") (map (mkUdpStream host entry) route.ports)
|
||||||
|
) routes
|
||||||
)
|
)
|
||||||
) registry.route
|
) registry.route
|
||||||
);
|
);
|
||||||
@@ -39,43 +40,14 @@ let
|
|||||||
proxy_pass ${stream.address}:${toString stream.port};
|
proxy_pass ${stream.address}:${toString stream.port};
|
||||||
}
|
}
|
||||||
'') generatedUdpStreams;
|
'') generatedUdpStreams;
|
||||||
generatedVirtualHosts =
|
generatedVirtualHosts = lib.listToAttrs generatedVirtualHostList;
|
||||||
if duplicateDomainMessages != [ ] then
|
|
||||||
throw (builtins.head duplicateDomainMessages)
|
|
||||||
else
|
|
||||||
lib.listToAttrs generatedVirtualHostList;
|
|
||||||
|
|
||||||
duplicateDomainMessages = lib.concatLists (
|
# Route registry validation already requires unique domains.
|
||||||
lib.mapAttrsToList (
|
generatedVirtualHostList = lib.concatLists (
|
||||||
domain: virtualHosts:
|
|
||||||
if lib.length virtualHosts > 1 then
|
|
||||||
[
|
|
||||||
"duplicate route domain ${domain} in ${
|
|
||||||
lib.concatMapStringsSep " and " (
|
|
||||||
virtualHost: mkRegistryPath virtualHost.registryName virtualHost.host virtualHost.entry
|
|
||||||
) virtualHosts
|
|
||||||
}"
|
|
||||||
]
|
|
||||||
else
|
|
||||||
[ ]
|
|
||||||
) virtualHostsByDomain
|
|
||||||
);
|
|
||||||
|
|
||||||
virtualHostsByDomain = lib.foldl' (
|
|
||||||
acc: virtualHost:
|
|
||||||
acc
|
|
||||||
// {
|
|
||||||
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
|
|
||||||
}
|
|
||||||
) { } generatedVirtualHostList;
|
|
||||||
|
|
||||||
generatedVirtualHostList = generatedHttpsVirtualHostList ++ generatedHttpVirtualHostList;
|
|
||||||
|
|
||||||
generatedHttpsVirtualHostList = lib.concatLists (
|
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
host: routes:
|
host: routes:
|
||||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") (
|
lib.mapAttrsToList (entry: route: mkVirtualHost host entry route.protocol) (
|
||||||
lib.filterAttrs (_entry: route: route.port != null) routes
|
lib.filterAttrs (_entry: route: route.protocol != "udp") routes
|
||||||
)
|
)
|
||||||
) registry.route
|
) registry.route
|
||||||
);
|
);
|
||||||
@@ -89,30 +61,25 @@ let
|
|||||||
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;
|
compositeEntry = registry.join host entry;
|
||||||
inherit (compositeEntry) route;
|
inherit (compositeEntry) route;
|
||||||
port = if isHttp then route.http.port else route.port;
|
inherit (route) port;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit host entry;
|
|
||||||
registryName = "route";
|
|
||||||
name = route.domain;
|
name = route.domain;
|
||||||
value = {
|
value = {
|
||||||
forceSSL = !isHttp;
|
forceSSL = !isHttp;
|
||||||
enableACME = !isHttp;
|
enableACME = !isHttp;
|
||||||
locations."/".proxyPass = mkBackend address port;
|
extraConfig = lib.optionalString (route.clientMaxBodySize != null) ''
|
||||||
|
client_max_body_size ${route.clientMaxBodySize};
|
||||||
|
'';
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = mkBackend address port;
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
mkBackend = address: port: "http://${address}:${toString port}";
|
mkBackend = address: port: "http://${address}:${toString port}";
|
||||||
|
|
||||||
generatedHttpVirtualHostList = lib.concatLists (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
host: routes:
|
|
||||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "http") (
|
|
||||||
lib.filterAttrs (_entry: route: route.http.port != null) routes
|
|
||||||
)
|
|
||||||
) registry.route
|
|
||||||
);
|
|
||||||
|
|
||||||
mkRegistryPath =
|
mkRegistryPath =
|
||||||
registryName: host: entry:
|
registryName: host: entry:
|
||||||
"registry.${registryName}.${host}.${entry}";
|
"registry.${registryName}.${host}.${entry}";
|
||||||
@@ -123,7 +90,7 @@ in
|
|||||||
enable = lib.mkEnableOption "the public edge reverse proxy";
|
enable = lib.mkEnableOption "the public edge reverse proxy";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.services.reverse-proxy.enable {
|
||||||
networking.firewall.allowedTCPPorts = [
|
networking.firewall.allowedTCPPorts = [
|
||||||
80
|
80
|
||||||
443
|
443
|
||||||
|
|||||||
68
configs/nixos/modules/services/romm.nix
Normal file
68
configs/nixos/modules/services/romm.nix
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.modules.services.romm;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.services.romm = {
|
||||||
|
enable = lib.mkEnableOption "RomM";
|
||||||
|
|
||||||
|
environmentFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "/run/secrets/romm.env";
|
||||||
|
readOnly = true;
|
||||||
|
description = "Path to RomM's environment file.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkMerge [
|
||||||
|
{
|
||||||
|
registry."stable-uid".${config.networking.hostName} = {
|
||||||
|
romm = 20005;
|
||||||
|
"redis-romm" = 20006;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
(lib.mkIf cfg.enable {
|
||||||
|
networking.firewall.allowedTCPPorts = [ 80 ];
|
||||||
|
|
||||||
|
services.romm = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.alexion.romm;
|
||||||
|
group = "storage";
|
||||||
|
database.createLocally = true;
|
||||||
|
redis.createLocally = true;
|
||||||
|
watcher.enable = true;
|
||||||
|
extraEnvironment = {
|
||||||
|
HASHEOUS_API_ENABLED = "true";
|
||||||
|
PLAYMATCH_API_ENABLED = "true";
|
||||||
|
ROM_CONVERTO_ENABLED = "false";
|
||||||
|
};
|
||||||
|
inherit (cfg) environmentFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.romm-worker.serviceConfig.ExecStart =
|
||||||
|
lib.mkForce "${lib.getExe' config.services.romm.package "romm-worker"} scans";
|
||||||
|
|
||||||
|
systemd.tmpfiles.settings."10-romm"."${config.services.romm.dataDir}/config/config.yml"."L+".argument =
|
||||||
|
toString (
|
||||||
|
(pkgs.formats.yaml { }).generate "romm-config.yml" {
|
||||||
|
emulatorjs.auto_save_sync = false;
|
||||||
|
filesystem.structure = {
|
||||||
|
default = "roms/{platform}/{game}";
|
||||||
|
firmware = "bios/{platform}";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
users.users = {
|
||||||
|
romm.uid = config.registry."stable-uid".${config.networking.hostName}.romm;
|
||||||
|
redis-romm.uid = config.registry."stable-uid".${config.networking.hostName}."redis-romm";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
serviceName = lib.mkOption {
|
serviceName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.strMatching "[a-z0-9][a-z0-9-]*";
|
||||||
example = "valheim";
|
example = "valheim";
|
||||||
description = "Name of the systemd service that runs the game server.";
|
description = "Lowercase systemd service basename for the game server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
appId = lib.mkOption {
|
appId = lib.mkOption {
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${pkgs.coreutils}/bin/install -d \
|
${lib.getExe' pkgs.coreutils "install"} -d \
|
||||||
-o ${user} \
|
-o ${user} \
|
||||||
-g ${group} \
|
-g ${group} \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
|
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
ExecStart = "${pkgs.systemd}/bin/systemctl --no-block start ${service.serviceName}.service";
|
ExecStart = "${lib.getExe' pkgs.systemd "systemctl"} --no-block start ${service.serviceName}.service";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,11 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Bazel for the primary user's shell environment.
|
# Bazel for the primary user's shell environment.
|
||||||
let
|
|
||||||
cfg = config.modules.shell.bazel; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.shell.bazel.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.bazel ];
|
home-manager.users.${config.user.name}.home.packages = [ pkgs.bazel ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# direnv for the primary user, configured through home-manager.
|
|
||||||
let
|
|
||||||
cfg = config.modules.shell.direnv; # NOLINT(let_order)
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
if declare -f use_flake >/dev/null; then
|
|
||||||
__dotfiles_use_flake_definition="$(declare -f use_flake)"
|
|
||||||
eval "${__dotfiles_use_flake_definition/use_flake/__dotfiles_nix_direnv_use_flake}"
|
|
||||||
unset __dotfiles_use_flake_definition
|
|
||||||
|
|
||||||
use_flake() {
|
|
||||||
local flake_expr="${1:-.}"
|
|
||||||
|
|
||||||
if [[ "$flake_expr" == //* ]]; then
|
|
||||||
local git_root
|
|
||||||
if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
|
||||||
log_error 'use flake // requires a Git repository'
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local flake_suffix="${flake_expr#//}"
|
|
||||||
if [[ -n "$flake_suffix" && "$flake_suffix" != \#* && "$flake_suffix" != /* ]]; then
|
|
||||||
flake_expr="path:${git_root}/${flake_suffix}"
|
|
||||||
else
|
|
||||||
flake_expr="path:${git_root}${flake_suffix}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
shift
|
|
||||||
__dotfiles_nix_direnv_use_flake "$flake_expr" "$@"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
__dotfiles_nix_direnv_use_flake "$@"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
log_error 'nix-direnv must define use_flake before the // wrapper loads'
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
@@ -6,13 +6,9 @@
|
|||||||
}:
|
}:
|
||||||
# fish for the primary user, configured through home-manager.
|
# fish for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.fish; # NOLINT(let_order)
|
cfg = config.modules.shell.fish;
|
||||||
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
||||||
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
||||||
exportEnvVar = name: value: ''
|
|
||||||
set -gx ${name} ${lib.escapeShellArg value}
|
|
||||||
'';
|
|
||||||
envVars = lib.attrByPath [ config.networking.hostName ] { } config.registry.env-var;
|
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -106,9 +102,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
# Rendered by home-manager into ~/.config/fish/config.fish.
|
# Rendered by home-manager into ~/.config/fish/config.fish.
|
||||||
interactiveShellInit = lib.concatStringsSep "\n" (
|
interactiveShellInit = builtins.readFile ./config.fish;
|
||||||
(lib.mapAttrsToList exportEnvVar envVars) ++ [ (builtins.readFile ./config.fish) ]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,6 +80,13 @@ 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,14 +5,13 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.shell.git.enable {
|
||||||
home-manager.users.${user}.programs.git = {
|
home-manager.users.${user}.programs.git = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,13 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.shell.gitea-axi.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,14 +6,13 @@
|
|||||||
}:
|
}:
|
||||||
# Herdr, a terminal multiplexer for coding agents.
|
# Herdr, a terminal multiplexer for coding agents.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.herdr; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.shell.herdr.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,14 +7,13 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.shell.nvim.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName} = {
|
registry.shell-shortcut.${config.networking.hostName} = {
|
||||||
vi = "nvim";
|
vi = "nvim";
|
||||||
vim = "nvim";
|
vim = "nvim";
|
||||||
@@ -25,6 +24,9 @@ in
|
|||||||
home-manager.users.${user} = hm: {
|
home-manager.users.${user} = hm: {
|
||||||
programs.nixvim = {
|
programs.nixvim = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
|
# Aligns Nixvim plugins with this flake's `Nixpkgs`.
|
||||||
|
# `nixpkgs.useGlobalPackages` prevents Home Manager from creating a separate package set.
|
||||||
nixpkgs.useGlobalPackages = true;
|
nixpkgs.useGlobalPackages = true;
|
||||||
|
|
||||||
extraPackages = with pkgs; [
|
extraPackages = with pkgs; [
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -14,7 +13,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 cfg.enable {
|
config = lib.mkIf config.modules.shell.omp.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.omp ];
|
home-manager.users.${user}.home.packages = [ pkgs.omp ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,24 +8,18 @@
|
|||||||
# 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; # NOLINT(let_order)
|
|
||||||
vendoredPi = pkgs.pi-coding-agent.overrideAttrs {
|
|
||||||
src = ../../../../../third_party/pi;
|
|
||||||
patches = [ ];
|
|
||||||
};
|
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.pi = {
|
options.modules.shell.pi.enable =
|
||||||
enable = lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
|
lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.shell.pi.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
programs.pi-coding-agent = {
|
programs.pi-coding-agent = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = vendoredPi;
|
package = pkgs.alexion.pi-coding-agent;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
defaultProvider = "openai-codex";
|
defaultProvider = "openai-codex";
|
||||||
|
|||||||
@@ -6,15 +6,14 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.shell.python.enable {
|
||||||
registry.env-var.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
registry.env.${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,7 +6,6 @@
|
|||||||
}:
|
}:
|
||||||
# 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; # NOLINT(let_order)
|
|
||||||
toolchain = pkgs.symlinkJoin {
|
toolchain = pkgs.symlinkJoin {
|
||||||
name = "rust-toolchain";
|
name = "rust-toolchain";
|
||||||
paths = with pkgs; [
|
paths = with pkgs; [
|
||||||
@@ -23,8 +22,8 @@ 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 cfg.enable {
|
config = lib.mkIf config.modules.shell.rust.enable {
|
||||||
registry.env-var.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
registry.env.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
home.packages = [
|
home.packages = [
|
||||||
|
|||||||
@@ -6,13 +6,12 @@
|
|||||||
# 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; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.shell.tmux.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,13 +6,12 @@
|
|||||||
}:
|
}:
|
||||||
# Yazi, a terminal file manager.
|
# Yazi, a terminal file manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.yazi; # NOLINT(let_order)
|
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.shell.yazi.enable {
|
||||||
home-manager.users.${user}.programs.yazi = {
|
home-manager.users.${user}.programs.yazi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableFishIntegration = true;
|
enableFishIntegration = true;
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# The host-level ZFS pool import: durable service state a host mounts, never rebuilds.
|
|
||||||
let
|
|
||||||
cfg = config.modules.zfs; # NOLINT(let_order)
|
|
||||||
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
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,40 +5,40 @@
|
|||||||
tab = lib.mkOption {
|
tab = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "Apps";
|
example = "Apps";
|
||||||
description = "Homepage tab that contains this entry.";
|
description = "Groups the entry in a Homepage tab.";
|
||||||
};
|
};
|
||||||
|
|
||||||
section = lib.mkOption {
|
section = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "Media";
|
example = "Media";
|
||||||
description = "Homepage section that contains this entry.";
|
description = "Groups the entry in a Homepage section.";
|
||||||
};
|
};
|
||||||
|
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "Navidrome";
|
example = "Navidrome";
|
||||||
description = "Homepage display name for this entry.";
|
description = "Sets the display name in Homepage.";
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = "Homepage description for this entry.";
|
description = "Sets the description in Homepage.";
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = "Homepage icon for this entry.";
|
description = "Sets the icon in Homepage.";
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = "Homepage link override for this entry.";
|
description = "Overrides the generated Homepage link.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
112
configs/nixos/registry/keybind.nix
Normal file
112
configs/nixos/registry/keybind.nix
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{ 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,14 +9,31 @@
|
|||||||
assertion =
|
assertion =
|
||||||
entries:
|
entries:
|
||||||
util.registry.predicates.uniqueValue (entry: entry.value) (
|
util.registry.predicates.uniqueValue (entry: entry.value) (
|
||||||
lib.concatMap (entry: map (port: entry // { value = port; }) entry.value.udp.ports) entries
|
lib.concatMap (entry: map (port: entry // { value = port; }) entry.value.ports) entries
|
||||||
);
|
);
|
||||||
message = "Route UDP ports must be unique.";
|
message = "Route UDP ports must be unique.";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
entries: lib.all (entry: entry.value.port == null || entry.value.http.port == null) entries;
|
entries:
|
||||||
message = "Route entries must not set both port and http.port.";
|
lib.all (entry: entry.value.port != null && entry.value.ports == [ ]) (
|
||||||
|
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.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -25,31 +42,40 @@
|
|||||||
domain = lib.mkOption {
|
domain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "files.alexion.dev";
|
example = "files.alexion.dev";
|
||||||
description = "Domain served by this route entry.";
|
description = "Records the public domain for this route.";
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = "HTTPS backend TCP port served by the matching address registry entry.";
|
description = "Sets the HTTP backend port for an HTTP or HTTPS route.";
|
||||||
};
|
};
|
||||||
|
|
||||||
http.port = lib.mkOption {
|
ports = 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 = "UDP ports served by the matching address registry entry.";
|
description = "Sets the UDP ports for a UDP route.";
|
||||||
|
};
|
||||||
|
|
||||||
|
clientMaxBodySize = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "1g";
|
||||||
|
description = "Maximum HTTP request body the reverse proxy accepts.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
|
||||||
entryType = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
command = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "nvim";
|
|
||||||
description = "Command expanded by this shell shortcut.";
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
type = lib.types.coercedTo lib.types.str (command: { inherit command; }) (
|
||||||
|
lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
command = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "nvim";
|
||||||
|
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.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
let
|
let
|
||||||
duplicateUids =
|
duplicateUids =
|
||||||
entries:
|
entries:
|
||||||
lib.filterAttrs (_uid: serviceNames: builtins.length serviceNames > 1) (serviceNamesByUid entries);
|
lib.filterAttrs (_uid: entryNames: builtins.length entryNames > 1) (entryNamesByUid entries);
|
||||||
|
|
||||||
serviceNamesByUid =
|
entryNamesByUid =
|
||||||
entries:
|
entries:
|
||||||
lib.mapAttrs (_uid: uidEntries: lib.unique (map (entry: entry.entry) uidEntries)) (
|
lib.mapAttrs (_uid: uidEntries: lib.unique (map (entry: entry.entry) uidEntries)) (
|
||||||
lib.groupBy (entry: toString entry.value) entries
|
lib.groupBy (entry: toString entry.value) entries
|
||||||
@@ -12,15 +12,14 @@ let
|
|||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Next stable UID: 20005
|
# Next stable UID: 20007
|
||||||
replicate = true;
|
|
||||||
|
|
||||||
type = lib.types.ints.positive;
|
type = lib.types.ints.positive;
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = entries: duplicateUids entries == { };
|
assertion = entries: duplicateUids entries == { };
|
||||||
message = "Stable UID allocations must be unique.";
|
message = "Multiple entries use the same stable UID.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
29
configs/nixos/registry/waybar.nix
Normal file
29
configs/nixos/registry/waybar.nix
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{ 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.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
19
configs/nixos/registry/window-icon.nix
Normal file
19
configs/nixos/registry/window-icon.nix
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{ 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 = "";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,6 +6,17 @@ services:
|
|||||||
lastfm:
|
lastfm:
|
||||||
api-key: ENC[AES256_GCM,data:Zb0I5TpdEqOxLAD6ed2XywFGZp7dfY69vcGiWOUJkJQ=,iv:pO53pHr09jWfzJyTsQbTHtzTmiQ9+RQkDwZJmInIiX8=,tag:io9jkSN7TF2I8+qlSymaYw==,type:str]
|
api-key: ENC[AES256_GCM,data:Zb0I5TpdEqOxLAD6ed2XywFGZp7dfY69vcGiWOUJkJQ=,iv:pO53pHr09jWfzJyTsQbTHtzTmiQ9+RQkDwZJmInIiX8=,tag:io9jkSN7TF2I8+qlSymaYw==,type:str]
|
||||||
secret: ENC[AES256_GCM,data:rKhd9i0/tbJlXhc9Wf81IDydjpD8khSdwJEEMK260E4=,iv:btBXWBKJp9br5a3yZsRVegN8Ytzc45Qbk2iNlPbENkQ=,tag:LUbu+6gxfqQvVpddZto7Tg==,type:str]
|
secret: ENC[AES256_GCM,data:rKhd9i0/tbJlXhc9Wf81IDydjpD8khSdwJEEMK260E4=,iv:btBXWBKJp9br5a3yZsRVegN8Ytzc45Qbk2iNlPbENkQ=,tag:LUbu+6gxfqQvVpddZto7Tg==,type:str]
|
||||||
|
romm:
|
||||||
|
igdb:
|
||||||
|
client-id: ENC[AES256_GCM,data:tme7NzzbOlCmkMwVObWz793mOsCyVIzSjGoFvxpe,iv:5aOMvsDi8Z9aBymbqhbK79X4kJsqaET1y2Py6YUZZ/U=,tag:ih3GghlxiLsVyeTJoqU9/Q==,type:str]
|
||||||
|
client-secret: ENC[AES256_GCM,data:AqF7B9VaxM/X3sRqY6C5JVWPWqgRIoqy09P9ZUFj,iv:D+vDPUuGGdzKYnTVTRkI9TV/EOJgVECIeNDt9g8NGl0=,tag:MRaTT7TdEZSd+98xthVw1A==,type:str]
|
||||||
|
retroachievements:
|
||||||
|
api-key: ENC[AES256_GCM,data:LMzETQCMi9Ib783W0BZDe2PaSUSTVM5a61502RtZLg8=,iv:Hf5nBoVtZdJszQuc2n0LVNMtJCCpJrh40y4g+l+az70=,tag:esfKLpI1W8BG3Pfsg3xxKQ==,type:str]
|
||||||
|
steamgriddb:
|
||||||
|
api-key: ENC[AES256_GCM,data:+kBRieIB/dHbvQLbH2gQTQh2j6DQhybVWEGkSCRZrZY=,iv:3h6dhfiV7HfaZhMv93x8+7F3utzYG6fIbTDScA14PVI=,tag:Wo24YnvsIxe7iMRLyxrj2w==,type:str]
|
||||||
|
screenscraper:
|
||||||
|
user: ENC[AES256_GCM,data:7hGcFF4xqg==,iv:SFOdYej94Thfn+Te0kXsC9Kq568A1dsnaR+JB90MgBQ=,tag:inIWuFY7f5Tvv8EyV71dYA==,type:str]
|
||||||
|
password: ENC[AES256_GCM,data:eUWwDDQUSytm/6QZlG5XjbSi41nsBQbm4w==,iv:82S2a3rAVjaF0CXcjXUXErNHMY7MEkjpDL7EVX9cIHw=,tag:uGuVJCv8ThsaNnA0C0WR2Q==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
@@ -26,7 +37,7 @@ sops:
|
|||||||
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
|
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
||||||
lastmodified: "2026-09-12T22:54:25Z"
|
lastmodified: "2026-10-20T01:27:06Z"
|
||||||
mac: ENC[AES256_GCM,data:4R3a0ytf4i8aM8iBQ9vRC/q+hvtjLrCTNgbpCB5DbqBMBYmhtGsBmO/0i1yT0N4tydyLaGOMkNfuuuVi3ASLEOKnViy9w3uD9IN0F79W4f2vguWUVIp/T8skvN9wbV9o9xd0LkJzmeVVgzb1H6HKw+rTGjbquleoIHEBDcDDM+k=,iv:0NlckMXlS+idY6UGBF+F0KFLt+pzD7Q/YEQOdXraAYg=,tag:JBBQduV8uhnKz9gPf6HfAQ==,type:str]
|
mac: ENC[AES256_GCM,data:NCt6e/ctZwaBADfrhu1RXtILlClbHEqOAnIu8rE/zMPSwMIPB/kTtfd/ujf+1iRszoHaX4Se1xc0wz1th6NVToFVNarxBBgTk4JMUU8gdMweOjmwlh3GXU3ZcYY/xAXxWF1hxyXSEW78j3S2844H9XXENAq/Bf3mjdCYQ/qr/d4=,iv:4nFyBSmRvF4zSAwvL+xccMwtWrVbHtFKNoTb7f3rSH4=,tag:eCV/7P0TjyMZXKcWBUPXQA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.3
|
version: 3.13.3
|
||||||
|
|||||||
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": "hidden",
|
"propertiesInDocument": "visible",
|
||||||
"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,5 +5,6 @@
|
|||||||
"obsidian-style-settings",
|
"obsidian-style-settings",
|
||||||
"custom-font-loader",
|
"custom-font-loader",
|
||||||
"obsidian-icon-folder",
|
"obsidian-icon-folder",
|
||||||
"table-editor-obsidian"
|
"table-editor-obsidian",
|
||||||
|
"obsidian-linter"
|
||||||
]
|
]
|
||||||
16
docs/.obsidian/graph.json
vendored
16
docs/.obsidian/graph.json
vendored
@@ -28,6 +28,20 @@
|
|||||||
"rgb": 5366249
|
"rgb": 5366249
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"query": "path:\"03 wrk/tasks\"[\"status\":open]",
|
||||||
|
"color": {
|
||||||
|
"a": 1,
|
||||||
|
"rgb": 14725458
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "path:\"03 wrk/tasks\" [\"status\":closed]",
|
||||||
|
"color": {
|
||||||
|
"a": 1,
|
||||||
|
"rgb": 11657298
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"query": "path:\"03 wrk\" ",
|
"query": "path:\"03 wrk\" ",
|
||||||
"color": {
|
"color": {
|
||||||
@@ -60,6 +74,6 @@
|
|||||||
"repelStrength": 20,
|
"repelStrength": 20,
|
||||||
"linkStrength": 1,
|
"linkStrength": 1,
|
||||||
"linkDistance": 250,
|
"linkDistance": 250,
|
||||||
"scale": 0.3645607001522557,
|
"scale": 0.16202697784544698,
|
||||||
"close": false
|
"close": false
|
||||||
}
|
}
|
||||||
326
docs/.obsidian/plugins/obsidian-linter/data.json
vendored
Normal file
326
docs/.obsidian/plugins/obsidian-linter/data.json
vendored
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
{
|
||||||
|
"ruleConfigs": {
|
||||||
|
"add-blank-line-after-yaml": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"dedupe-yaml-array-values": {
|
||||||
|
"enabled": true,
|
||||||
|
"dedupe-alias-key": true,
|
||||||
|
"dedupe-tag-key": true,
|
||||||
|
"dedupe-array-keys": true,
|
||||||
|
"ignore-keys": []
|
||||||
|
},
|
||||||
|
"escape-yaml-special-characters": {
|
||||||
|
"enabled": true,
|
||||||
|
"try-to-escape-single-line-arrays": true
|
||||||
|
},
|
||||||
|
"force-yaml-escape": {
|
||||||
|
"enabled": true,
|
||||||
|
"force-yaml-escape-keys": []
|
||||||
|
},
|
||||||
|
"format-tags-in-yaml": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"format-yaml-array": {
|
||||||
|
"enabled": true,
|
||||||
|
"alias-key": true,
|
||||||
|
"tag-key": true,
|
||||||
|
"default-array-keys": true,
|
||||||
|
"force-single-line-array-style": [],
|
||||||
|
"force-multi-line-array-style": []
|
||||||
|
},
|
||||||
|
"insert-yaml-attributes": {
|
||||||
|
"enabled": true,
|
||||||
|
"text-to-insert": []
|
||||||
|
},
|
||||||
|
"move-inline-fields-to-yaml": {
|
||||||
|
"enabled": true,
|
||||||
|
"how-to-handle-full-line-fields": "Move and remove",
|
||||||
|
"how-to-handle-bracketed-fields": "Leave in place",
|
||||||
|
"how-to-handle-existing-keys": "Skip",
|
||||||
|
"inline-keys-to-ignore": []
|
||||||
|
},
|
||||||
|
"move-tags-to-yaml": {
|
||||||
|
"enabled": true,
|
||||||
|
"how-to-handle-existing-tags": "Nothing",
|
||||||
|
"tags-to-ignore": []
|
||||||
|
},
|
||||||
|
"remove-yaml-keys": {
|
||||||
|
"enabled": true,
|
||||||
|
"yaml-keys-to-remove": []
|
||||||
|
},
|
||||||
|
"sort-yaml-array-values": {
|
||||||
|
"enabled": true,
|
||||||
|
"sort-alias-key": true,
|
||||||
|
"sort-tag-key": true,
|
||||||
|
"sort-array-keys": true,
|
||||||
|
"ignore-keys": [],
|
||||||
|
"sort-order": "Ascending Alphabetical"
|
||||||
|
},
|
||||||
|
"yaml-key-sort": {
|
||||||
|
"enabled": true,
|
||||||
|
"yaml-key-priority-sort-order": [
|
||||||
|
"aliases",
|
||||||
|
"date-created",
|
||||||
|
"date-modified"
|
||||||
|
],
|
||||||
|
"priority-keys-at-start-of-yaml": true,
|
||||||
|
"yaml-sort-order-for-other-keys": "None"
|
||||||
|
},
|
||||||
|
"yaml-timestamp": {
|
||||||
|
"enabled": true,
|
||||||
|
"date-created": true,
|
||||||
|
"date-created-key": "date-created",
|
||||||
|
"date-created-source-of-truth": "file system",
|
||||||
|
"date-modified": true,
|
||||||
|
"date-modified-key": "date-modified",
|
||||||
|
"date-modified-source-of-truth": "file system",
|
||||||
|
"format": "YYYY-MM-DD HH:mm",
|
||||||
|
"convert-to-utc": false,
|
||||||
|
"update-on-file-contents-updated": "after 5 seconds"
|
||||||
|
},
|
||||||
|
"yaml-title": {
|
||||||
|
"enabled": false,
|
||||||
|
"title-key": "title",
|
||||||
|
"mode": "first-h1-or-filename-if-h1-missing"
|
||||||
|
},
|
||||||
|
"yaml-title-alias": {
|
||||||
|
"enabled": true,
|
||||||
|
"preserve-existing-alias-section-style": true,
|
||||||
|
"keep-alias-that-matches-the-filename": false,
|
||||||
|
"use-yaml-key-to-keep-track-of-old-filename-or-heading": false,
|
||||||
|
"alias-helper-key": "linter-yaml-title-alias",
|
||||||
|
"remove-alias-if-empty": true
|
||||||
|
},
|
||||||
|
"capitalize-headings": {
|
||||||
|
"enabled": true,
|
||||||
|
"style": "Title Case",
|
||||||
|
"ignore-case-words": [],
|
||||||
|
"lowercase-words": [],
|
||||||
|
"starting-word-ignore-characters": "'\"(‘“-",
|
||||||
|
"ending-word-ignore-characters": ".?!,:;'\")”’0123456789-",
|
||||||
|
"ignore-words": []
|
||||||
|
},
|
||||||
|
"file-name-heading": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"header-increment": {
|
||||||
|
"enabled": true,
|
||||||
|
"start-at-h2": false
|
||||||
|
},
|
||||||
|
"headings-start-line": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-trailing-punctuation-in-heading": {
|
||||||
|
"enabled": true,
|
||||||
|
"punctuation-to-remove": ".,;:!。,;:!"
|
||||||
|
},
|
||||||
|
"footnote-after-punctuation": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"move-footnotes-to-the-bottom": {
|
||||||
|
"enabled": true,
|
||||||
|
"include-blank-line-between-footnotes": false
|
||||||
|
},
|
||||||
|
"re-index-footnotes": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"auto-correct-common-misspellings": {
|
||||||
|
"enabled": true,
|
||||||
|
"ignore-words": [],
|
||||||
|
"skip-words-with-multiple-capitals": true,
|
||||||
|
"extra-auto-correct-files": []
|
||||||
|
},
|
||||||
|
"blockquote-style": {
|
||||||
|
"enabled": true,
|
||||||
|
"style": "space"
|
||||||
|
},
|
||||||
|
"convert-bullet-list-markers": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"default-language-for-code-fences": {
|
||||||
|
"enabled": true,
|
||||||
|
"default-language": ""
|
||||||
|
},
|
||||||
|
"emphasis-style": {
|
||||||
|
"enabled": true,
|
||||||
|
"style": "consistent"
|
||||||
|
},
|
||||||
|
"no-bare-urls": {
|
||||||
|
"enabled": true,
|
||||||
|
"no-bare-uris": true
|
||||||
|
},
|
||||||
|
"ordered-list-style": {
|
||||||
|
"enabled": true,
|
||||||
|
"number-style": "ascending",
|
||||||
|
"list-end-style": ".",
|
||||||
|
"preserve-start": false
|
||||||
|
},
|
||||||
|
"proper-ellipsis": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"quote-style": {
|
||||||
|
"enabled": true,
|
||||||
|
"single-quote-enabled": true,
|
||||||
|
"single-quote-style": "''",
|
||||||
|
"double-quote-enabled": true,
|
||||||
|
"double-quote-style": "\"\""
|
||||||
|
},
|
||||||
|
"remove-consecutive-list-markers": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-empty-list-markers": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-hyphenated-line-breaks": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-multiple-spaces": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"strong-style": {
|
||||||
|
"enabled": true,
|
||||||
|
"style": "consistent"
|
||||||
|
},
|
||||||
|
"two-spaces-between-lines-with-content": {
|
||||||
|
"enabled": false,
|
||||||
|
"line-break-indicator": " "
|
||||||
|
},
|
||||||
|
"unordered-list-style": {
|
||||||
|
"enabled": true,
|
||||||
|
"list-style": "consistent"
|
||||||
|
},
|
||||||
|
"align-table-columns": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"compact-yaml": {
|
||||||
|
"enabled": true,
|
||||||
|
"inner-new-lines": false
|
||||||
|
},
|
||||||
|
"consecutive-blank-lines": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"convert-spaces-to-tabs": {
|
||||||
|
"enabled": true,
|
||||||
|
"tabsize": 4
|
||||||
|
},
|
||||||
|
"empty-line-around-blockquotes": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"empty-line-around-code-fences": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"empty-line-around-horizontal-rules": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"empty-line-around-math-blocks": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"empty-line-around-tables": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"heading-blank-lines": {
|
||||||
|
"enabled": true,
|
||||||
|
"bottom": true,
|
||||||
|
"empty-line-after-yaml": true
|
||||||
|
},
|
||||||
|
"line-break-at-document-end": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"move-math-block-indicators-to-their-own-line": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"paragraph-blank-lines": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-empty-lines-between-list-markers-and-checklists": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-link-spacing": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-space-around-characters": {
|
||||||
|
"enabled": true,
|
||||||
|
"include-fullwidth-forms": true,
|
||||||
|
"include-cjk-symbols-and-punctuation": true,
|
||||||
|
"include-dashes": true,
|
||||||
|
"other-symbols": ""
|
||||||
|
},
|
||||||
|
"remove-space-before-or-after-characters": {
|
||||||
|
"enabled": true,
|
||||||
|
"characters-to-remove-space-before": ",!?;:).’”]",
|
||||||
|
"characters-to-remove-space-after": "¿¡‘“(["
|
||||||
|
},
|
||||||
|
"space-after-list-markers": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"space-between-chinese-japanese-or-korean-and-english-or-numbers": {
|
||||||
|
"enabled": true,
|
||||||
|
"english-symbols-punctuation-before": "-+;:'\"°%$)]",
|
||||||
|
"english-symbols-punctuation-after": "-+'\"([¥$"
|
||||||
|
},
|
||||||
|
"trailing-spaces": {
|
||||||
|
"enabled": true,
|
||||||
|
"two-space-line-break": false
|
||||||
|
},
|
||||||
|
"add-blockquote-indentation-on-paste": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"prevent-double-checklist-indicator-on-paste": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"prevent-double-list-item-indicator-on-paste": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"proper-ellipsis-on-paste": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-hyphens-on-paste": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-leading-or-trailing-whitespace-on-paste": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-leftover-footnotes-from-quote-on-paste": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"remove-multiple-blank-lines-on-paste": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lintOnSave": true,
|
||||||
|
"recordLintOnSaveLogs": false,
|
||||||
|
"displayChanged": true,
|
||||||
|
"suppressMessageWhenNoChange": true,
|
||||||
|
"enableDiffPreviewView": true,
|
||||||
|
"suppressLintAllFilesConfirmationModal": false,
|
||||||
|
"suppressLintAllFilesInFolderConfirmationModal": false,
|
||||||
|
"lintOnFileChange": false,
|
||||||
|
"displayLintOnFileChangeNotice": false,
|
||||||
|
"settingsConvertedToConfigKeyValues": true,
|
||||||
|
"textAreaSettingsConvertedToListItemSettings": true,
|
||||||
|
"defaultArrayStyleMovedToCommonStyles": true,
|
||||||
|
"additionalFileExtensions": [],
|
||||||
|
"foldersToIgnore": [
|
||||||
|
"99 meta/templates"
|
||||||
|
],
|
||||||
|
"filesToIgnore": [
|
||||||
|
{
|
||||||
|
"label": "todo",
|
||||||
|
"match": "TODO.md",
|
||||||
|
"flags": "i"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"linterLocale": "system-default",
|
||||||
|
"logLevel": "ERROR",
|
||||||
|
"lintCommands": [],
|
||||||
|
"customRegexes": [],
|
||||||
|
"commonStyles": {
|
||||||
|
"aliasArrayStyle": "multi-line",
|
||||||
|
"tagArrayStyle": "single-line",
|
||||||
|
"defaultArrayStyle": "multi-line",
|
||||||
|
"minimumNumberOfDollarSignsToBeAMathBlock": 2,
|
||||||
|
"escapeCharacter": "\"",
|
||||||
|
"removeUnnecessaryEscapeCharsForMultiLineArrays": true
|
||||||
|
}
|
||||||
|
}
|
||||||
35147
docs/.obsidian/plugins/obsidian-linter/default-misspellings.md
vendored
Normal file
35147
docs/.obsidian/plugins/obsidian-linter/default-misspellings.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user